From 5f65d007d91a441634634f6fb787bb1e56bad802 Mon Sep 17 00:00:00 2001 From: Louis Date: Thu, 25 Jan 2024 11:26:55 +0700 Subject: [PATCH 01/71] fix: bring back open app directory --- web/screens/Settings/Advanced/index.tsx | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/web/screens/Settings/Advanced/index.tsx b/web/screens/Settings/Advanced/index.tsx index 227bae47d..e1f733699 100644 --- a/web/screens/Settings/Advanced/index.tsx +++ b/web/screens/Settings/Advanced/index.tsx @@ -182,6 +182,30 @@ const Advanced = () => { /> + {/* Open app directory */} + {window.electronAPI && ( +
+
+
+
+ Open App Directory +
+
+

+ Open the directory where your app data, like conversation history + and model configurations, is located. +

+
+ +
+ )} + {/* Claer log */}
From 79049eeefbb3be678fe48798826f9cee5f14061b Mon Sep 17 00:00:00 2001 From: Hien To Date: Thu, 25 Jan 2024 11:27:29 +0700 Subject: [PATCH 02/71] Remove paths trigger for pre-release ci --- .github/workflows/jan-electron-build-pre-release.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/jan-electron-build-pre-release.yml b/.github/workflows/jan-electron-build-pre-release.yml index d37cda5ab..86be3e1f2 100644 --- a/.github/workflows/jan-electron-build-pre-release.yml +++ b/.github/workflows/jan-electron-build-pre-release.yml @@ -4,8 +4,6 @@ on: push: branches: - main - paths: - - "!README.md" jobs: From f73e7388e2597ce8e4839057db3a2a9fd820e984 Mon Sep 17 00:00:00 2001 From: Hien To Date: Thu, 25 Jan 2024 14:53:44 +0700 Subject: [PATCH 03/71] Combine 2 ci pipeline pre-release and nightly into one for correcting build number nightly --- .../workflows/jan-electron-build-nightly.yml | 26 +++++++++- .../jan-electron-build-pre-release.yml | 50 ------------------- 2 files changed, 24 insertions(+), 52 deletions(-) delete mode 100644 .github/workflows/jan-electron-build-pre-release.yml diff --git a/.github/workflows/jan-electron-build-nightly.yml b/.github/workflows/jan-electron-build-nightly.yml index f961ccd6f..7198f13bd 100644 --- a/.github/workflows/jan-electron-build-nightly.yml +++ b/.github/workflows/jan-electron-build-nightly.yml @@ -1,6 +1,9 @@ name: Jan Build Electron App Nightly or Manual on: + push: + branches: + - main schedule: - cron: '0 20 * * 2,3,4' # At 8 PM UTC on Tuesday, Wednesday, and Thursday, which is 3 AM UTC+7 workflow_dispatch: @@ -27,8 +30,16 @@ jobs: echo "::set-output name=public_provider::${{ github.event.inputs.public_provider }}" echo "::set-output name=ref::${{ github.ref }}" else - echo "::set-output name=public_provider::cloudflare-r2" - echo "::set-output name=ref::refs/heads/dev" + if [ ${{ github.event_name == 'schedule' }} ]; then + echo "::set-output name=public_provider::cloudflare-r2" + echo "::set-output name=ref::refs/heads/dev" + else if [ ${{ github.event_name == 'push' }} ]; then + echo "::set-output name=public_provider::cloudflare-r2" + echo "::set-output name=ref::${{ github.ref }}" + else + echo "::set-output name=public_provider::none" + echo "::set-output name=ref::${{ github.ref }}" + fi fi # Job create Update app version based on latest release tag with build number and save to output get-update-version: @@ -73,6 +84,17 @@ jobs: push_to_branch: dev new_version: ${{ needs.get-update-version.outputs.new_version }} + noti-discord-pre-release-and-update-url-readme: + needs: [build-macos, build-windows-x64, build-linux-x64, get-update-version, set-public-provider] + secrets: inherit + if: github.event_name == 'push' + uses: ./.github/workflows/template-noti-discord-and-update-url-readme.yml + with: + ref: refs/heads/dev + build_reason: Pre-release + push_to_branch: dev + new_version: ${{ needs.get-update-version.outputs.new_version }} + noti-discord-manual-and-update-url-readme: needs: [build-macos, build-windows-x64, build-linux-x64, get-update-version, set-public-provider] secrets: inherit diff --git a/.github/workflows/jan-electron-build-pre-release.yml b/.github/workflows/jan-electron-build-pre-release.yml deleted file mode 100644 index 86be3e1f2..000000000 --- a/.github/workflows/jan-electron-build-pre-release.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: Jan Build Electron Pre Release - -on: - push: - branches: - - main - -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 }} From 8e28c2a43dd482b53fe2d539e941135277df8270 Mon Sep 17 00:00:00 2001 From: Hien To Date: Thu, 25 Jan 2024 14:57:54 +0700 Subject: [PATCH 04/71] Add ignore trigger pre-release to main for docs and README.md --- .github/workflows/jan-electron-build-nightly.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/jan-electron-build-nightly.yml b/.github/workflows/jan-electron-build-nightly.yml index 7198f13bd..9234e22c1 100644 --- a/.github/workflows/jan-electron-build-nightly.yml +++ b/.github/workflows/jan-electron-build-nightly.yml @@ -4,6 +4,9 @@ on: push: branches: - main + paths-ignore: + - 'README.md' + - 'docs/**' schedule: - cron: '0 20 * * 2,3,4' # At 8 PM UTC on Tuesday, Wednesday, and Thursday, which is 3 AM UTC+7 workflow_dispatch: From 023ab04e949d90edb0ea3f1e80d812b30a126217 Mon Sep 17 00:00:00 2001 From: Hien To Date: Thu, 25 Jan 2024 15:42:54 +0700 Subject: [PATCH 05/71] Correct bash script syntax in ci --- .github/workflows/jan-electron-build-nightly.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/jan-electron-build-nightly.yml b/.github/workflows/jan-electron-build-nightly.yml index 9234e22c1..4531152d4 100644 --- a/.github/workflows/jan-electron-build-nightly.yml +++ b/.github/workflows/jan-electron-build-nightly.yml @@ -29,14 +29,14 @@ jobs: - name: Set public provider id: set-public-provider run: | - if [ ${{ github.event == 'workflow_dispatch' }} ]; then + if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then echo "::set-output name=public_provider::${{ github.event.inputs.public_provider }}" echo "::set-output name=ref::${{ github.ref }}" else - if [ ${{ github.event_name == 'schedule' }} ]; then + if [ "${{ github.event_name }}" == "schedule" ]; then echo "::set-output name=public_provider::cloudflare-r2" echo "::set-output name=ref::refs/heads/dev" - else if [ ${{ github.event_name == 'push' }} ]; then + elif [ "${{ github.event_name }}" == "push" ]; then echo "::set-output name=public_provider::cloudflare-r2" echo "::set-output name=ref::${{ github.ref }}" else From 7f55c1bed1e7278e7d7811fcd3ea77eddc729b58 Mon Sep 17 00:00:00 2001 From: NamH Date: Thu, 25 Jan 2024 15:58:45 +0700 Subject: [PATCH 06/71] fix(Log): server log is not display in windows (#1764) Signed-off-by: James Co-authored-by: James --- web/screens/LocalServer/Logs.tsx | 11 +++++++++-- web/screens/SystemMonitor/index.tsx | 26 +++++++++++++------------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/web/screens/LocalServer/Logs.tsx b/web/screens/LocalServer/Logs.tsx index e301e38ff..125bd93ef 100644 --- a/web/screens/LocalServer/Logs.tsx +++ b/web/screens/LocalServer/Logs.tsx @@ -3,19 +3,26 @@ import { useEffect, useState } from 'react' import React from 'react' +import { useAtomValue } from 'jotai' + import { useServerLog } from '@/hooks/useServerLog' +import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom' + const Logs = () => { const { getServerLog } = useServerLog() + const serverEnabled = useAtomValue(serverEnabledAtom) const [logs, setLogs] = useState([]) useEffect(() => { getServerLog().then((log) => { - if (typeof log?.split === 'function') setLogs(log.split(/\r?\n|\r|\n/g)) + if (typeof log?.split === 'function') { + setLogs(log.split(/\r?\n|\r|\n/g)) + } }) // eslint-disable-next-line react-hooks/exhaustive-deps - }, [logs]) + }, [logs, serverEnabled]) return (
diff --git a/web/screens/SystemMonitor/index.tsx b/web/screens/SystemMonitor/index.tsx index 941f024f6..ed3b057a1 100644 --- a/web/screens/SystemMonitor/index.tsx +++ b/web/screens/SystemMonitor/index.tsx @@ -37,6 +37,19 @@ export default function SystemMonitorScreen() {
+
+
+

+ cpu ({cpuUsage}%) +

+ + {cpuUsage}% of 100% + +
+
+ +
+

@@ -53,19 +66,6 @@ export default function SystemMonitorScreen() { />

-
-
-

- cpu ({cpuUsage}%) -

- - {cpuUsage}% of 100% - -
-
- -
-
{activeModel && ( From acc774dc8048e19250cf9a0a734aac9994644d12 Mon Sep 17 00:00:00 2001 From: NamH Date: Thu, 25 Jan 2024 16:02:01 +0700 Subject: [PATCH 07/71] fix(Wording): #1758 correct text for windows (#1768) Signed-off-by: James Co-authored-by: James --- web/containers/CardSidebar/index.tsx | 13 ++++--------- web/containers/Layout/TopBar/index.tsx | 6 ++++-- web/utils/titleUtils.ts | 9 +++++++++ 3 files changed, 17 insertions(+), 11 deletions(-) create mode 100644 web/utils/titleUtils.ts diff --git a/web/containers/CardSidebar/index.tsx b/web/containers/CardSidebar/index.tsx index bc5047497..552856921 100644 --- a/web/containers/CardSidebar/index.tsx +++ b/web/containers/CardSidebar/index.tsx @@ -13,6 +13,8 @@ import { useClickOutside } from '@/hooks/useClickOutside' import { usePath } from '@/hooks/usePath' +import { openFileTitle } from '@/utils/titleUtils' + import { activeThreadAtom } from '@/helpers/atoms/Thread.atom' interface Props { @@ -36,13 +38,6 @@ export default function CardSidebar({ useClickOutside(() => setMore(false), null, [menu, toggle]) - let openFolderTitle: string = 'Open Containing Folder' - if (isMac) { - openFolderTitle = 'Show in Finder' - } else if (isWindows) { - openFolderTitle = 'Show in File Explorer' - } - return (
- {openFolderTitle} + {openFileTitle()} Opens thread.json. Changes affect this thread only. @@ -118,7 +113,7 @@ export default function CardSidebar({
) : ( - Show in Finder + {openFileTitle()} )} diff --git a/web/containers/Layout/TopBar/index.tsx b/web/containers/Layout/TopBar/index.tsx index ac05e4e1a..8762152ab 100644 --- a/web/containers/Layout/TopBar/index.tsx +++ b/web/containers/Layout/TopBar/index.tsx @@ -27,6 +27,8 @@ import { usePath } from '@/hooks/usePath' import { showRightSideBarAtom } from '@/screens/Chat/Sidebar' +import { openFileTitle } from '@/utils/titleUtils' + import { activeThreadAtom } from '@/helpers/atoms/Thread.atom' const TopBar = () => { @@ -161,7 +163,7 @@ const TopBar = () => { className="text-muted-foreground" /> - Show in Finder + {openFileTitle()}
{ />
- Show in Finder + {openFileTitle()}
diff --git a/web/utils/titleUtils.ts b/web/utils/titleUtils.ts new file mode 100644 index 000000000..a227a7985 --- /dev/null +++ b/web/utils/titleUtils.ts @@ -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' + } +} From b9584db826ba241e2207f39cf29f9c9831911c4d Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Thu, 25 Jan 2024 16:56:29 +0700 Subject: [PATCH 08/71] fix: input port local server not accepted alphabets --- web/screens/LocalServer/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/screens/LocalServer/index.tsx b/web/screens/LocalServer/index.tsx index ce709d831..20d4d70e5 100644 --- a/web/screens/LocalServer/index.tsx +++ b/web/screens/LocalServer/index.tsx @@ -176,6 +176,7 @@ const LocalServerScreen = () => { 'w-[70px] flex-shrink-0', errorRangePort && 'border-danger' )} + type="number" value={port} onChange={(e) => { handleChangePort(e.target.value) From 917d69db379c970440d3ecc171a03c0699ad9a5f Mon Sep 17 00:00:00 2001 From: Louis Date: Thu, 25 Jan 2024 18:11:31 +0700 Subject: [PATCH 09/71] fix: can not start model when server is not enabled from model settings page (#1774) --- web/screens/Settings/Models/Row.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/web/screens/Settings/Models/Row.tsx b/web/screens/Settings/Models/Row.tsx index d32f0fe63..aad060afa 100644 --- a/web/screens/Settings/Models/Row.tsx +++ b/web/screens/Settings/Models/Row.tsx @@ -56,10 +56,8 @@ export default function RowModel(props: RowModelProps) { stopModel() window.core?.api?.stopServer() setServerEnabled(false) - } else { - if (serverEnabled) { - startModel(modelId) - } + } else if (!serverEnabled) { + startModel(modelId) } } From c2310ed0300845e46443a88d56b75985042fb0e9 Mon Sep 17 00:00:00 2001 From: Louis Date: Thu, 25 Jan 2024 19:52:54 +0700 Subject: [PATCH 10/71] fix: could not delete model (#1779) --- web/screens/Settings/Models/Row.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/screens/Settings/Models/Row.tsx b/web/screens/Settings/Models/Row.tsx index aad060afa..5ade3bad6 100644 --- a/web/screens/Settings/Models/Row.tsx +++ b/web/screens/Settings/Models/Row.tsx @@ -180,7 +180,7 @@ export default function RowModel(props: RowModelProps) { )} onClick={() => { setTimeout(async () => { - if (serverEnabled) { + if (!serverEnabled) { await stopModel() deleteModel(props.data) } From f0e88d62137642e57543b160fdf9422673702466 Mon Sep 17 00:00:00 2001 From: hiento09 <136591877+hiento09@users.noreply.github.com> Date: Thu, 25 Jan 2024 21:18:45 +0700 Subject: [PATCH 11/71] Docs publish to github page trigger on push to docs branch (#1782) Co-authored-by: Hien To --- .github/workflows/jan-docs.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/jan-docs.yml b/.github/workflows/jan-docs.yml index 8ce4e91ed..8135935bd 100644 --- a/.github/workflows/jan-docs.yml +++ b/.github/workflows/jan-docs.yml @@ -5,6 +5,7 @@ on: branches: - main - dev + - docs paths: - 'docs/**' - '.github/workflows/jan-docs.yml' @@ -12,6 +13,7 @@ on: branches: - main - dev + - docs paths: - 'docs/**' - '.github/workflows/jan-docs.yml' @@ -91,13 +93,13 @@ jobs: Preview URL: ${{ steps.deployCloudflarePages.outputs.url }} - name: Add Custome Domain file - if: github.event_name == 'push' && github.ref == 'refs/heads/main' && github.event.pull_request.head.repo.full_name != github.repository + if: github.event_name == 'push' && github.ref == 'refs/heads/docs' && github.event.pull_request.head.repo.full_name != github.repository run: echo "${{ vars.DOCUSAURUS_DOMAIN }}" > ./docs/build/CNAME # Popular action to deploy to GitHub Pages: # Docs: https://github.com/peaceiris/actions-gh-pages#%EF%B8%8F-docusaurus - name: Deploy to GitHub Pages - if: github.event_name == 'push' && github.ref == 'refs/heads/main' && github.event.pull_request.head.repo.full_name != github.repository + if: github.event_name == 'push' && github.ref == 'refs/heads/docs' && github.event.pull_request.head.repo.full_name != github.repository uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} From 7ed523e18343270003e5e2904521e6c7950adcec Mon Sep 17 00:00:00 2001 From: Louis Date: Thu, 25 Jan 2024 22:19:28 +0700 Subject: [PATCH 12/71] fix: api settings are not applied on changes (#1789) --- core/src/types/config/appConfigEvent.ts | 6 ++++ core/src/types/config/index.ts | 1 + .../inference-openai-extension/src/index.ts | 32 ++++++++++++++----- web/hooks/useEngineSettings.ts | 14 +++++--- 4 files changed, 41 insertions(+), 12 deletions(-) create mode 100644 core/src/types/config/appConfigEvent.ts diff --git a/core/src/types/config/appConfigEvent.ts b/core/src/types/config/appConfigEvent.ts new file mode 100644 index 000000000..50e33cfa2 --- /dev/null +++ b/core/src/types/config/appConfigEvent.ts @@ -0,0 +1,6 @@ +/** + * App configuration event name + */ +export enum AppConfigurationEventName { + OnConfigurationUpdate = 'OnConfigurationUpdate', +} diff --git a/core/src/types/config/index.ts b/core/src/types/config/index.ts index 0fa3645aa..d2e182b99 100644 --- a/core/src/types/config/index.ts +++ b/core/src/types/config/index.ts @@ -1 +1,2 @@ export * from './appConfigEntity' +export * from './appConfigEvent' diff --git a/extensions/inference-openai-extension/src/index.ts b/extensions/inference-openai-extension/src/index.ts index 54572041d..9abfc2c7d 100644 --- a/extensions/inference-openai-extension/src/index.ts +++ b/extensions/inference-openai-extension/src/index.ts @@ -19,6 +19,8 @@ import { MessageEvent, ModelEvent, InferenceEvent, + AppConfigurationEventName, + joinPath, } from "@janhq/core"; import { requestInference } from "./helpers/sse"; import { ulid } from "ulid"; @@ -30,7 +32,7 @@ import { join } from "path"; * It also subscribes to events emitted by the @janhq/core package and handles new message requests. */ export default class JanInferenceOpenAIExtension extends BaseExtension { - private static readonly _homeDir = "file://engines"; + private static readonly _engineDir = "file://engines"; private static readonly _engineMetadataFileName = "openai.json"; private static _currentModel: OpenAIModel; @@ -47,9 +49,9 @@ export default class JanInferenceOpenAIExtension extends BaseExtension { * Subscribes to events emitted by the @janhq/core package. */ async onLoad() { - if (!(await fs.existsSync(JanInferenceOpenAIExtension._homeDir))) { + if (!(await fs.existsSync(JanInferenceOpenAIExtension._engineDir))) { await fs - .mkdirSync(JanInferenceOpenAIExtension._homeDir) + .mkdirSync(JanInferenceOpenAIExtension._engineDir) .catch((err) => console.debug(err)); } @@ -57,7 +59,7 @@ export default class JanInferenceOpenAIExtension extends BaseExtension { // Events subscription events.on(MessageEvent.OnMessageSent, (data) => - JanInferenceOpenAIExtension.handleMessageRequest(data, this) + JanInferenceOpenAIExtension.handleMessageRequest(data, this), ); events.on(ModelEvent.OnModelInit, (model: OpenAIModel) => { @@ -70,6 +72,20 @@ export default class JanInferenceOpenAIExtension extends BaseExtension { events.on(InferenceEvent.OnInferenceStopped, () => { JanInferenceOpenAIExtension.handleInferenceStopped(this); }); + + const settingsFilePath = await joinPath([ + JanInferenceOpenAIExtension._engineDir, + JanInferenceOpenAIExtension._engineMetadataFileName, + ]); + + events.on( + AppConfigurationEventName.OnConfigurationUpdate, + (settingsKey: string) => { + // Update settings on changes + if (settingsKey === settingsFilePath) + JanInferenceOpenAIExtension.writeDefaultEngineSettings(); + }, + ); } /** @@ -80,8 +96,8 @@ export default class JanInferenceOpenAIExtension extends BaseExtension { static async writeDefaultEngineSettings() { try { const engineFile = join( - JanInferenceOpenAIExtension._homeDir, - JanInferenceOpenAIExtension._engineMetadataFileName + JanInferenceOpenAIExtension._engineDir, + JanInferenceOpenAIExtension._engineMetadataFileName, ); if (await fs.existsSync(engineFile)) { const engine = await fs.readFileSync(engineFile, "utf-8"); @@ -90,7 +106,7 @@ export default class JanInferenceOpenAIExtension extends BaseExtension { } else { await fs.writeFileSync( engineFile, - JSON.stringify(JanInferenceOpenAIExtension._engineSettings, null, 2) + JSON.stringify(JanInferenceOpenAIExtension._engineSettings, null, 2), ); } } catch (err) { @@ -116,7 +132,7 @@ export default class JanInferenceOpenAIExtension extends BaseExtension { } private static async handleInferenceStopped( - instance: JanInferenceOpenAIExtension + instance: JanInferenceOpenAIExtension, ) { instance.isCancelled = true; instance.controller?.abort(); diff --git a/web/hooks/useEngineSettings.ts b/web/hooks/useEngineSettings.ts index 258a89aa4..49e6607b3 100644 --- a/web/hooks/useEngineSettings.ts +++ b/web/hooks/useEngineSettings.ts @@ -1,4 +1,4 @@ -import { fs, joinPath } from '@janhq/core' +import { fs, joinPath, events, AppConfigurationEventName } from '@janhq/core' export const useEngineSettings = () => { const readOpenAISettings = async () => { @@ -21,10 +21,16 @@ export const useEngineSettings = () => { apiKey: string | undefined }) => { const settings = await readOpenAISettings() + const settingFilePath = await joinPath(['file://engines', 'openai.json']) + settings.api_key = apiKey - await fs.writeFileSync( - await joinPath(['file://engines', 'openai.json']), - JSON.stringify(settings) + + await fs.writeFileSync(settingFilePath, JSON.stringify(settings)) + + // Sec: Don't attach the settings data to the event + events.emit( + AppConfigurationEventName.OnConfigurationUpdate, + settingFilePath ) } return { readOpenAISettings, saveOpenAISettings } From b2ff76ce805cff941e1abbce576b146148b1d02d Mon Sep 17 00:00:00 2001 From: NamH Date: Thu, 25 Jan 2024 22:29:27 +0700 Subject: [PATCH 13/71] fix: app log not being printed (#1790) Signed-off-by: James Co-authored-by: James --- core/src/node/log.ts | 30 ++++++++++++++---------------- web/hooks/useVaultDirectory.ts | 30 ++++++------------------------ 2 files changed, 20 insertions(+), 40 deletions(-) diff --git a/core/src/node/log.ts b/core/src/node/log.ts index 8a5155d8d..6f2c2f80f 100644 --- a/core/src/node/log.ts +++ b/core/src/node/log.ts @@ -2,38 +2,36 @@ import fs from 'fs' import util from 'util' import { getAppLogPath, getServerLogPath } from './utils' -export const log = function (message: string) { - const appLogPath = getAppLogPath() +export const log = (message: string) => { + const path = getAppLogPath() if (!message.startsWith('[')) { message = `[APP]::${message}` } message = `${new Date().toISOString()} ${message}` - if (fs.existsSync(appLogPath)) { - var log_file = fs.createWriteStream(appLogPath, { - flags: 'a', - }) - log_file.write(util.format(message) + '\n') - log_file.close() - console.debug(message) - } + writeLog(message, path) } -export const logServer = function (message: string) { - const serverLogPath = getServerLogPath() +export const logServer = (message: string) => { + const path = getServerLogPath() if (!message.startsWith('[')) { message = `[SERVER]::${message}` } message = `${new Date().toISOString()} ${message}` + writeLog(message, path) +} - if (fs.existsSync(serverLogPath)) { - var log_file = fs.createWriteStream(serverLogPath, { +const writeLog = (message: string, logPath: string) => { + if (!fs.existsSync(logPath)) { + fs.writeFileSync(logPath, message) + } else { + const logFile = fs.createWriteStream(logPath, { flags: 'a', }) - log_file.write(util.format(message) + '\n') - log_file.close() + logFile.write(util.format(message) + '\n') + logFile.close() console.debug(message) } } diff --git a/web/hooks/useVaultDirectory.ts b/web/hooks/useVaultDirectory.ts index 3aa7383c9..9d7adf2ab 100644 --- a/web/hooks/useVaultDirectory.ts +++ b/web/hooks/useVaultDirectory.ts @@ -1,32 +1,15 @@ -import { useEffect } from 'react' +import { useEffect, useState } from 'react' import { fs, AppConfiguration } from '@janhq/core' -import { atom, useAtom } from 'jotai' - -import { useMainViewState } from './useMainViewState' - -const isSameDirectoryAtom = atom(false) -const isDirectoryConfirmAtom = atom(false) -const isErrorSetNewDestAtom = atom(false) -const currentPathAtom = atom('') -const newDestinationPathAtom = atom('') - export const SUCCESS_SET_NEW_DESTINATION = 'successSetNewDestination' export function useVaultDirectory() { - const [isSameDirectory, setIsSameDirectory] = useAtom(isSameDirectoryAtom) - const { setMainViewState } = useMainViewState() - const [isDirectoryConfirm, setIsDirectoryConfirm] = useAtom( - isDirectoryConfirmAtom - ) - const [isErrorSetNewDest, setIsErrorSetNewDest] = useAtom( - isErrorSetNewDestAtom - ) - const [currentPath, setCurrentPath] = useAtom(currentPathAtom) - const [newDestinationPath, setNewDestinationPath] = useAtom( - newDestinationPathAtom - ) + const [isSameDirectory, setIsSameDirectory] = useState(false) + const [isDirectoryConfirm, setIsDirectoryConfirm] = useState(false) + const [isErrorSetNewDest, setIsErrorSetNewDest] = useState(false) + const [currentPath, setCurrentPath] = useState('') + const [newDestinationPath, setNewDestinationPath] = useState('') useEffect(() => { window.core?.api @@ -34,7 +17,6 @@ export function useVaultDirectory() { ?.then((appConfig: AppConfiguration) => { setCurrentPath(appConfig.data_folder) }) - // eslint-disable-next-line react-hooks/exhaustive-deps }, []) const setNewDestination = async () => { From 663bf0c9027491632bd464073c94d750b3063bb7 Mon Sep 17 00:00:00 2001 From: NamH Date: Thu, 25 Jan 2024 23:14:24 +0700 Subject: [PATCH 14/71] fix: clean last message when user clean thread message (#1793) Signed-off-by: James Co-authored-by: James --- web/hooks/useDeleteThread.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/web/hooks/useDeleteThread.ts b/web/hooks/useDeleteThread.ts index 84dd8a468..00ba98b99 100644 --- a/web/hooks/useDeleteThread.ts +++ b/web/hooks/useDeleteThread.ts @@ -49,6 +49,14 @@ export default function useDeleteThread() { threadId, messages.filter((msg) => msg.role === ChatCompletionRole.System) ) + + thread.metadata = { + ...thread.metadata, + lastMessage: undefined, + } + await extensionManager + .get(ExtensionTypeEnum.Conversational) + ?.saveThread(thread) updateThreadLastMessage(threadId, undefined) } } From a7edd37bfc7c4adfe08f5ce4f349ca5403200fb5 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Fri, 26 Jan 2024 12:19:10 +0700 Subject: [PATCH 15/71] fix: disabled input darkmode (#1800) --- uikit/src/input/styles.scss | 2 +- uikit/src/select/styles.scss | 3 ++- web/screens/LocalServer/index.tsx | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/uikit/src/input/styles.scss b/uikit/src/input/styles.scss index b78db270a..9990da8b4 100644 --- a/uikit/src/input/styles.scss +++ b/uikit/src/input/styles.scss @@ -1,6 +1,6 @@ .input { @apply border-border placeholder:text-muted-foreground flex h-9 w-full rounded-lg border bg-transparent px-3 py-1 transition-colors; - @apply disabled:cursor-not-allowed disabled:bg-zinc-100; + @apply disabled:cursor-not-allowed disabled:bg-zinc-100 disabled:dark:bg-zinc-800 disabled:dark:text-zinc-600; @apply focus-within:outline-none focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1; @apply file:border-0 file:bg-transparent file:font-medium; } diff --git a/uikit/src/select/styles.scss b/uikit/src/select/styles.scss index 665ca8cba..bc5b6c0cc 100644 --- a/uikit/src/select/styles.scss +++ b/uikit/src/select/styles.scss @@ -1,5 +1,6 @@ .select { - @apply placeholder:text-muted-foreground border-border flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border bg-transparent px-3 py-2 text-sm shadow-sm disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1; + @apply placeholder:text-muted-foreground border-border flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border bg-transparent px-3 py-2 text-sm shadow-sm disabled:cursor-not-allowed [&>span]:line-clamp-1; + @apply disabled:cursor-not-allowed disabled:bg-zinc-100 disabled:dark:bg-zinc-800 disabled:dark:text-zinc-600; @apply focus-within:outline-none focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1; &-caret { diff --git a/web/screens/LocalServer/index.tsx b/web/screens/LocalServer/index.tsx index 20d4d70e5..5e9eb1d04 100644 --- a/web/screens/LocalServer/index.tsx +++ b/web/screens/LocalServer/index.tsx @@ -276,7 +276,7 @@ const LocalServerScreen = () => { {/* Middle Bar */} -
+

Server Logs

- + ) } + +export default DropdownListSidebar diff --git a/web/containers/OpenAiKeyInput/index.tsx b/web/containers/OpenAiKeyInput/index.tsx index abd79e6a8..444c8074f 100644 --- a/web/containers/OpenAiKeyInput/index.tsx +++ b/web/containers/OpenAiKeyInput/index.tsx @@ -1,16 +1,19 @@ import React, { useEffect, useState } from 'react' -import { InferenceEngine, Model } from '@janhq/core' +import { InferenceEngine } from '@janhq/core' import { Input } from '@janhq/uikit' +import { useAtomValue } from 'jotai' + import { useEngineSettings } from '@/hooks/useEngineSettings' -type Props = { - selectedModel?: Model - serverEnabled: boolean -} +import { selectedModelAtom } from '../DropdownListSidebar' -const OpenAiKeyInput: React.FC = ({ selectedModel, serverEnabled }) => { +import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom' + +const OpenAiKeyInput: React.FC = () => { + const selectedModel = useAtomValue(selectedModelAtom) + const serverEnabled = useAtomValue(serverEnabledAtom) const [openAISettings, setOpenAISettings] = useState< { api_key: string } | undefined >(undefined) @@ -20,8 +23,7 @@ const OpenAiKeyInput: React.FC = ({ selectedModel, serverEnabled }) => { readOpenAISettings().then((settings) => { setOpenAISettings(settings) }) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) + }, [readOpenAISettings]) if (!selectedModel || selectedModel.engine !== InferenceEngine.openai) { return null diff --git a/web/hooks/useEngineSettings.ts b/web/hooks/useEngineSettings.ts index 49e6607b3..4a17f91df 100644 --- a/web/hooks/useEngineSettings.ts +++ b/web/hooks/useEngineSettings.ts @@ -1,7 +1,9 @@ +import { useCallback } from 'react' + import { fs, joinPath, events, AppConfigurationEventName } from '@janhq/core' export const useEngineSettings = () => { - const readOpenAISettings = async () => { + const readOpenAISettings = useCallback(async () => { if ( !(await fs.existsSync(await joinPath(['file://engines', 'openai.json']))) ) @@ -14,7 +16,8 @@ export const useEngineSettings = () => { return typeof settings === 'object' ? settings : JSON.parse(settings) } return {} - } + }, []) + const saveOpenAISettings = async ({ apiKey, }: { diff --git a/web/hooks/useRecommendedModel.ts b/web/hooks/useRecommendedModel.ts index dd474d0b5..cc47d3fe6 100644 --- a/web/hooks/useRecommendedModel.ts +++ b/web/hooks/useRecommendedModel.ts @@ -43,9 +43,7 @@ export default function useRecommendedModel() { Model | undefined > => { const models = await getAndSortDownloadedModels() - if (!activeThread) { - return - } + if (!activeThread) return const finishInit = threadStates[activeThread.id].isFinishInit ?? true if (finishInit) { diff --git a/web/screens/Chat/Sidebar/index.tsx b/web/screens/Chat/Sidebar/index.tsx index 64e58d4d3..500787218 100644 --- a/web/screens/Chat/Sidebar/index.tsx +++ b/web/screens/Chat/Sidebar/index.tsx @@ -140,10 +140,8 @@ const Sidebar: React.FC = () => {
-
-
- -
+
+ {componentDataRuntimeSetting.length !== 0 && (
diff --git a/web/screens/LocalServer/index.tsx b/web/screens/LocalServer/index.tsx index 5e9eb1d04..f752baf2d 100644 --- a/web/screens/LocalServer/index.tsx +++ b/web/screens/LocalServer/index.tsx @@ -29,6 +29,7 @@ import { ExternalLinkIcon, InfoIcon } from 'lucide-react' import { twMerge } from 'tailwind-merge' import CardSidebar from '@/containers/CardSidebar' + import DropdownListSidebar, { selectedModelAtom, } from '@/containers/DropdownListSidebar' @@ -66,7 +67,7 @@ const LocalServerScreen = () => { const { openServerLog, clearServerLog } = useServerLog() const { startModel, stateModel } = useActiveModel() - const [selectedModel] = useAtom(selectedModelAtom) + const selectedModel = useAtomValue(selectedModelAtom) const [isCorsEnabled, setIsCorsEnabled] = useAtom(corsEnabledAtom) const [isVerboseEnabled, setIsVerboseEnabled] = useAtom(verboseEnabledAtom) @@ -351,10 +352,8 @@ const LocalServerScreen = () => { : 'w-0 translate-x-full opacity-0' )} > -
-
- -
+
+ {componentDataEngineSetting.filter( (x) => x.name === 'prompt_template' From 532a589354c74aa17f57518d6b6a865e6cb9fa26 Mon Sep 17 00:00:00 2001 From: Louis Date: Fri, 26 Jan 2024 18:57:52 +0700 Subject: [PATCH 18/71] fix: model selection does not show in api settings page (#1802) --- web/containers/DropdownListSidebar/index.tsx | 9 +++++++-- web/screens/LocalServer/index.tsx | 8 ++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/web/containers/DropdownListSidebar/index.tsx b/web/containers/DropdownListSidebar/index.tsx index 31753c39c..eb867f54e 100644 --- a/web/containers/DropdownListSidebar/index.tsx +++ b/web/containers/DropdownListSidebar/index.tsx @@ -42,7 +42,12 @@ import { export const selectedModelAtom = atom(undefined) -const DropdownListSidebar: React.FC = () => { +// TODO: Move all of the unscoped logics outside of the component +const DropdownListSidebar = ({ + strictedThread = true, +}: { + strictedThread?: boolean +}) => { const activeThread = useAtomValue(activeThreadAtom) const threadStates = useAtomValue(threadStatesAtom) const [selectedModel, setSelectedModel] = useAtom(selectedModelAtom) @@ -152,7 +157,7 @@ const DropdownListSidebar: React.FC = () => { ] ) - if (!activeThread) { + if (strictedThread && !activeThread) { return null } diff --git a/web/screens/LocalServer/index.tsx b/web/screens/LocalServer/index.tsx index f752baf2d..6e606a952 100644 --- a/web/screens/LocalServer/index.tsx +++ b/web/screens/LocalServer/index.tsx @@ -59,7 +59,7 @@ const portAtom = atom('1337') const LocalServerScreen = () => { const [errorRangePort, setErrorRangePort] = useState(false) const [serverEnabled, setServerEnabled] = useAtom(serverEnabledAtom) - const showing = useAtomValue(showRightSideBarAtom) + const showRightSideBar = useAtomValue(showRightSideBarAtom) const activeModelParams = useAtomValue(getActiveThreadModelParamsAtom) const modelEngineParams = toSettingParams(activeModelParams) @@ -117,7 +117,7 @@ const LocalServerScreen = () => { - diff --git a/web/screens/Settings/Advanced/DataFolder/ModalErrorSetDestGlobal.tsx b/web/screens/Settings/Advanced/DataFolder/ModalErrorSetDestGlobal.tsx index 44a9db097..3729dc0d8 100644 --- a/web/screens/Settings/Advanced/DataFolder/ModalErrorSetDestGlobal.tsx +++ b/web/screens/Settings/Advanced/DataFolder/ModalErrorSetDestGlobal.tsx @@ -10,16 +10,15 @@ import { ModalClose, Button, } from '@janhq/uikit' +import { atom, useAtom } from 'jotai' -import { useVaultDirectory } from '@/hooks/useVaultDirectory' +export const showChangeFolderErrorAtom = atom(false) const ModalErrorSetDestGlobal = () => { - const { isErrorSetNewDest, setIsErrorSetNewDest } = useVaultDirectory() + const [show, setShow] = useAtom(showChangeFolderErrorAtom) + return ( - setIsErrorSetNewDest(false)} - > + @@ -31,7 +30,7 @@ const ModalErrorSetDestGlobal = () => {

- setIsErrorSetNewDest(false)}> + setShow(false)}>
diff --git a/web/screens/Settings/Advanced/DataFolder/ModalSameDirectory.tsx b/web/screens/Settings/Advanced/DataFolder/ModalSameDirectory.tsx index bd4d32e6a..8b2d90c61 100644 --- a/web/screens/Settings/Advanced/DataFolder/ModalSameDirectory.tsx +++ b/web/screens/Settings/Advanced/DataFolder/ModalSameDirectory.tsx @@ -11,16 +11,15 @@ import { Button, } from '@janhq/uikit' -import { useVaultDirectory } from '@/hooks/useVaultDirectory' +import { atom, useAtom } from 'jotai' + +export const showSamePathModalAtom = atom(false) const ModalSameDirectory = () => { - const { isSameDirectory, setIsSameDirectory, setNewDestination } = - useVaultDirectory() + const [show, setShow] = useAtom(showSamePathModalAtom) + return ( - setIsSameDirectory(false)} - > + @@ -31,11 +30,11 @@ const ModalSameDirectory = () => {

- setIsSameDirectory(false)}> + setShow(false)}> - diff --git a/web/screens/Settings/Advanced/DataFolder/index.tsx b/web/screens/Settings/Advanced/DataFolder/index.tsx index 936992c9d..90a0dd38b 100644 --- a/web/screens/Settings/Advanced/DataFolder/index.tsx +++ b/web/screens/Settings/Advanced/DataFolder/index.tsx @@ -1,17 +1,73 @@ +import { Fragment, useCallback, useEffect, useState } from 'react' + +import { fs, AppConfiguration } from '@janhq/core' import { Button, Input } from '@janhq/uikit' +import { useSetAtom } from 'jotai' import { PencilIcon, FolderOpenIcon } from 'lucide-react' -import { useVaultDirectory } from '@/hooks/useVaultDirectory' +import { SUCCESS_SET_NEW_DESTINATION } from '@/hooks/useVaultDirectory' -import ModalChangeDirectory from './ModalChangeDirectory' -import ModalErrorSetDestGlobal from './ModalErrorSetDestGlobal' -import ModalSameDirectory from './ModalSameDirectory' +import ModalChangeDirectory, { + showDirectoryConfirmModalAtom, +} from './ModalChangeDirectory' +import ModalErrorSetDestGlobal, { + showChangeFolderErrorAtom, +} from './ModalErrorSetDestGlobal' +import ModalSameDirectory, { showSamePathModalAtom } from './ModalSameDirectory' const DataFolder = () => { - const { currentPath, setNewDestination } = useVaultDirectory() + const [janDataFolderPath, setJanDataFolderPath] = useState('') + const setShowDirectoryConfirm = useSetAtom(showDirectoryConfirmModalAtom) + const setShowSameDirectory = useSetAtom(showSamePathModalAtom) + const setShowChangeFolderError = useSetAtom(showChangeFolderErrorAtom) + const [destinationPath, setDestinationPath] = useState(undefined) + + useEffect(() => { + window.core?.api + ?.getAppConfigurations() + ?.then((appConfig: AppConfiguration) => { + setJanDataFolderPath(appConfig.data_folder) + }) + }, []) + + const onChangeFolderClick = useCallback(async () => { + const destFolder = await window.core?.api?.selectDirectory() + if (!destFolder) return + + if (destFolder === janDataFolderPath) { + setShowSameDirectory(true) + return + } + + setDestinationPath(destFolder) + setShowDirectoryConfirm(true) + }, [janDataFolderPath, setShowSameDirectory, setShowDirectoryConfirm]) + + const onUserConfirmed = useCallback(async () => { + const destination = destinationPath + if (!destination) return + try { + const appConfiguration: AppConfiguration = + await window.core?.api?.getAppConfigurations() + const currentJanDataFolder = appConfiguration.data_folder + appConfiguration.data_folder = destination + await fs.syncFile(currentJanDataFolder, destination) + await window.core?.api?.updateAppConfiguration(appConfiguration) + + console.debug( + `File sync finished from ${currentJanDataFolder} to ${destination}` + ) + + localStorage.setItem(SUCCESS_SET_NEW_DESTINATION, 'true') + await window.core?.api?.relaunch() + } catch (e) { + console.error(`Error: ${e}`) + setShowChangeFolderError(true) + } + }, [destinationPath, setShowChangeFolderError]) return ( - <> +
@@ -26,7 +82,11 @@ const DataFolder = () => {
- + { size="sm" themes="outline" className="h-9 w-9 p-0" - onClick={setNewDestination} + onClick={onChangeFolderClick} >
- + - + ) } From 642d7aacc9bf876f79fa4d12933ba932077a6f98 Mon Sep 17 00:00:00 2001 From: NamH Date: Sat, 27 Jan 2024 12:56:45 +0700 Subject: [PATCH 20/71] fix: user can't view model setting in local api server (#1807) * fix: cannot change jan data folder Signed-off-by: James * fix: user can't view model setting in local api server Signed-off-by: James --------- Signed-off-by: James Co-authored-by: James --- web/hooks/usePath.ts | 38 ++++++++++++------- .../Settings/Advanced/DataFolder/index.tsx | 9 ++--- 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/web/hooks/usePath.ts b/web/hooks/usePath.ts index db6284f93..70dbfa6bb 100644 --- a/web/hooks/usePath.ts +++ b/web/hooks/usePath.ts @@ -11,20 +11,23 @@ export const usePath = () => { const selectedModel = useAtomValue(selectedModelAtom) const onReviewInFinder = async (type: string) => { - if (!activeThread) return - const activeThreadState = threadStates[activeThread.id] - if (!activeThreadState.isFinishInit) { - alert('Thread is not started yet') - return + // TODO: this logic should be refactored. + if (type !== 'Model') { + if (!activeThread) return + const activeThreadState = threadStates[activeThread.id] + if (!activeThreadState.isFinishInit) { + alert('Thread is not started yet') + return + } } const userSpace = await getJanDataFolderPath() let filePath = undefined - const assistantId = activeThread.assistants[0]?.assistant_id + const assistantId = activeThread?.assistants[0]?.assistant_id switch (type) { case 'Engine': case 'Thread': - filePath = await joinPath(['threads', activeThread.id]) + filePath = await joinPath(['threads', activeThread?.id ?? '']) break case 'Model': if (!selectedModel) return @@ -44,20 +47,27 @@ export const usePath = () => { } const onViewJson = async (type: string) => { - if (!activeThread) return - const activeThreadState = threadStates[activeThread.id] - if (!activeThreadState.isFinishInit) { - alert('Thread is not started yet') - return + // TODO: this logic should be refactored. + if (type !== 'Model') { + if (!activeThread) return + const activeThreadState = threadStates[activeThread.id] + if (!activeThreadState.isFinishInit) { + alert('Thread is not started yet') + return + } } const userSpace = await getJanDataFolderPath() let filePath = undefined - const assistantId = activeThread.assistants[0]?.assistant_id + const assistantId = activeThread?.assistants[0]?.assistant_id switch (type) { case 'Engine': case 'Thread': - filePath = await joinPath(['threads', activeThread.id, 'thread.json']) + filePath = await joinPath([ + 'threads', + activeThread?.id ?? '', + 'thread.json', + ]) break case 'Model': if (!selectedModel) return diff --git a/web/screens/Settings/Advanced/DataFolder/index.tsx b/web/screens/Settings/Advanced/DataFolder/index.tsx index 90a0dd38b..9a1863fa2 100644 --- a/web/screens/Settings/Advanced/DataFolder/index.tsx +++ b/web/screens/Settings/Advanced/DataFolder/index.tsx @@ -44,18 +44,17 @@ const DataFolder = () => { }, [janDataFolderPath, setShowSameDirectory, setShowDirectoryConfirm]) const onUserConfirmed = useCallback(async () => { - const destination = destinationPath - if (!destination) return + if (!destinationPath) return try { const appConfiguration: AppConfiguration = await window.core?.api?.getAppConfigurations() const currentJanDataFolder = appConfiguration.data_folder - appConfiguration.data_folder = destination - await fs.syncFile(currentJanDataFolder, destination) + appConfiguration.data_folder = destinationPath + await fs.syncFile(currentJanDataFolder, destinationPath) await window.core?.api?.updateAppConfiguration(appConfiguration) console.debug( - `File sync finished from ${currentJanDataFolder} to ${destination}` + `File sync finished from ${currentJanDataFolder} to ${destinationPath}` ) localStorage.setItem(SUCCESS_SET_NEW_DESTINATION, 'true') From 5fe68c16fda1f5147ebf3db6f5adad73079de0f8 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Sun, 28 Jan 2024 12:55:44 +0700 Subject: [PATCH 21/71] feat: put timestamp under thread name in left panel --- web/hooks/useSendChatMessage.ts | 15 ++++++++++++--- web/screens/Chat/ThreadList/index.tsx | 16 ++++++---------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/web/hooks/useSendChatMessage.ts b/web/hooks/useSendChatMessage.ts index c8a32564b..15caa62c9 100644 --- a/web/hooks/useSendChatMessage.ts +++ b/web/hooks/useSendChatMessage.ts @@ -172,7 +172,7 @@ export default function useSendChatMessage() { const instructions = activeThread.assistants[0].instructions ?? '' const tools = activeThread.assistants[0].tools ?? [] - const updatedThread: Thread = { + const initThread: Thread = { ...activeThread, assistants: [ { @@ -189,12 +189,13 @@ export default function useSendChatMessage() { }, ], } + updateThreadInitSuccess(activeThread.id) - updateThread(updatedThread) + updateThread(initThread) await extensionManager .get(ExtensionTypeEnum.Conversational) - ?.saveThread(updatedThread) + ?.saveThread(initThread) } updateThreadWaiting(activeThread.id, true) @@ -326,6 +327,14 @@ export default function useSendChatMessage() { setFileUpload([]) } + const updatedThread: Thread = { + ...activeThread, + updated: timestamp, + } + + // cheange last update thread when send message + updateThread(updatedThread) + await extensionManager .get(ExtensionTypeEnum.Conversational) ?.addNewMessage(threadMessage) diff --git a/web/screens/Chat/ThreadList/index.tsx b/web/screens/Chat/ThreadList/index.tsx index b2e15d111..19298062b 100644 --- a/web/screens/Chat/ThreadList/index.tsx +++ b/web/screens/Chat/ThreadList/index.tsx @@ -84,7 +84,6 @@ export default function ThreadList() { threads.map((thread, i) => { const lastMessage = threadStates[thread.id]?.lastMessage ?? 'No new message' - return (
-
-

{thread.title}

-

- {thread.updated && - displayDate(new Date(thread.updated).getTime())} -

-
+

+ {thread.updated && displayDate(thread.updated)} +

+

{thread.title}

{lastMessage || 'No new message'}

@@ -161,9 +157,9 @@ export default function ThreadList() {
- + Delete thread
From 61d5c6abe81709e464284522b49ae16cf21f0fd4 Mon Sep 17 00:00:00 2001 From: Louis Date: Mon, 29 Jan 2024 10:34:09 +0700 Subject: [PATCH 22/71] fix: model selection does not show in API settings page (#1828) --- web/containers/Layout/TopBar/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/containers/Layout/TopBar/index.tsx b/web/containers/Layout/TopBar/index.tsx index 8762152ab..f72f5f066 100644 --- a/web/containers/Layout/TopBar/index.tsx +++ b/web/containers/Layout/TopBar/index.tsx @@ -128,7 +128,8 @@ const TopBar = () => { showing && 'border-l border-border' )} > - {activeThread && ( + {((activeThread && mainViewState === MainViewState.Thread) || + mainViewState === MainViewState.LocalServer) && (
{showing && (
From 3445a25606caaeb89da4063401061099656f7d46 Mon Sep 17 00:00:00 2001 From: Service Account Date: Mon, 29 Jan 2024 03:50:23 +0000 Subject: [PATCH 23/71] janhq/jan: Update README.md with nightly build artifact URL --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f7ae2de86..121934260 100644 --- a/README.md +++ b/README.md @@ -76,31 +76,31 @@ Jan is an open-source ChatGPT alternative that runs 100% offline on your compute Experimental (Nightly Build) - + jan.exe - + Intel - + M1/M2 - + jan.deb - + jan.AppImage From 50c499601e8a962ac5769d29123fe2b7686cbb15 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Mon, 29 Jan 2024 11:57:56 +0700 Subject: [PATCH 24/71] fix: highlight menu dropdown server options --- web/screens/LocalServer/index.tsx | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/web/screens/LocalServer/index.tsx b/web/screens/LocalServer/index.tsx index 1a954c692..0964b7e25 100644 --- a/web/screens/LocalServer/index.tsx +++ b/web/screens/LocalServer/index.tsx @@ -73,6 +73,8 @@ const LocalServerScreen = () => { const [host, setHost] = useAtom(hostAtom) const [port, setPort] = useAtom(portAtom) + const hostOptions = ['127.0.0.1', '0.0.0.0'] + const FIRST_TIME_VISIT_API_SERVER = 'firstTimeVisitAPIServer' const [firstTimeVisitAPIServer, setFirstTimeVisitAPIServer] = @@ -166,8 +168,19 @@ const LocalServerScreen = () => { - 127.0.0.1 - 0.0.0.0 + {hostOptions.map((option, i) => { + return ( + + {option} + + ) + })} From 025415f88bf673a28835337f056acae30b7a1443 Mon Sep 17 00:00:00 2001 From: Service Account Date: Mon, 29 Jan 2024 05:19:54 +0000 Subject: [PATCH 25/71] Update README.md with Stable Download URLs --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index faa04e70a..3a99407f5 100644 --- a/README.md +++ b/README.md @@ -43,31 +43,31 @@ Jan is an open-source ChatGPT alternative that runs 100% offline on your compute Stable (Recommended) - + jan.exe - + Intel - + M1/M2 - + jan.deb - + jan.AppImage From aa94fe25d042268d11f06e21858d84225950c2b8 Mon Sep 17 00:00:00 2001 From: hiento09 <136591877+hiento09@users.noreply.github.com> Date: Mon, 29 Jan 2024 12:34:40 +0700 Subject: [PATCH 26/71] Update release url on README to default branch instead of main branch (#1832) Co-authored-by: Hien To --- .github/workflows/update-release-url.yml | 4 ++-- README.md | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/update-release-url.yml b/.github/workflows/update-release-url.yml index 545d6542e..99a3db0e0 100644 --- a/.github/workflows/update-release-url.yml +++ b/.github/workflows/update-release-url.yml @@ -17,7 +17,7 @@ jobs: with: fetch-depth: "0" token: ${{ secrets.PAT_SERVICE_ACCOUNT }} - ref: main + ref: dev - name: Get Latest Release uses: pozetroninc/github-action-get-latest-release@v0.7.0 @@ -46,4 +46,4 @@ jobs: git config --global user.name "Service Account" git add README.md git commit -m "Update README.md with Stable Download URLs" - git -c http.extraheader="AUTHORIZATION: bearer ${{ secrets.PAT_SERVICE_ACCOUNT }}" push origin HEAD:main + git -c http.extraheader="AUTHORIZATION: bearer ${{ secrets.PAT_SERVICE_ACCOUNT }}" push origin HEAD:dev diff --git a/README.md b/README.md index 121934260..1e2d76853 100644 --- a/README.md +++ b/README.md @@ -43,31 +43,31 @@ Jan is an open-source ChatGPT alternative that runs 100% offline on your compute Stable (Recommended) - + jan.exe - + Intel - + M1/M2 - + jan.deb - + jan.AppImage From 7c30c56277422bae1ecdfa3665a14d2254924895 Mon Sep 17 00:00:00 2001 From: Louis Date: Mon, 29 Jan 2024 12:44:32 +0700 Subject: [PATCH 27/71] chore: resolve (#1833) --- .../inference-openai-extension/src/index.ts | 8 ++++---- web/containers/Layout/TopBar/index.tsx | 19 ++++++++++--------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/extensions/inference-openai-extension/src/index.ts b/extensions/inference-openai-extension/src/index.ts index 44525b631..da06d5dc6 100644 --- a/extensions/inference-openai-extension/src/index.ts +++ b/extensions/inference-openai-extension/src/index.ts @@ -31,7 +31,7 @@ import { join } from "path"; * It also subscribes to events emitted by the @janhq/core package and handles new message requests. */ export default class JanInferenceOpenAIExtension extends BaseExtension { - private static readonly _homeDir = "file://engines"; + private static readonly _engineDir = "file://engines"; private static readonly _engineMetadataFileName = "openai.json"; private static _currentModel: OpenAIModel; @@ -48,9 +48,9 @@ export default class JanInferenceOpenAIExtension extends BaseExtension { * Subscribes to events emitted by the @janhq/core package. */ async onLoad() { - if (!(await fs.existsSync(JanInferenceOpenAIExtension._homeDir))) { + if (!(await fs.existsSync(JanInferenceOpenAIExtension._engineDir))) { await fs - .mkdirSync(JanInferenceOpenAIExtension._homeDir) + .mkdirSync(JanInferenceOpenAIExtension._engineDir) .catch((err) => console.debug(err)); } @@ -81,7 +81,7 @@ export default class JanInferenceOpenAIExtension extends BaseExtension { static async writeDefaultEngineSettings() { try { const engineFile = join( - JanInferenceOpenAIExtension._homeDir, + JanInferenceOpenAIExtension._engineDir, JanInferenceOpenAIExtension._engineMetadataFileName, ); if (await fs.existsSync(engineFile)) { diff --git a/web/containers/Layout/TopBar/index.tsx b/web/containers/Layout/TopBar/index.tsx index ab67cb3b7..807b8be55 100644 --- a/web/containers/Layout/TopBar/index.tsx +++ b/web/containers/Layout/TopBar/index.tsx @@ -120,13 +120,14 @@ const TopBar = () => {
- {activeThread && ( -
+
+ {((activeThread && mainViewState === MainViewState.Thread) || + mainViewState === MainViewState.LocalServer) && (
{showing && (
@@ -227,8 +228,8 @@ const TopBar = () => { />
-
- )} + )} +
)} From bb47d6869d0700b2cb31420b66eceab4e3129997 Mon Sep 17 00:00:00 2001 From: NamH Date: Mon, 29 Jan 2024 13:53:18 +0700 Subject: [PATCH 28/71] perf: remove unnecessary rerender when user typing input (#1818) Co-authored-by: Faisal Amir --- web/containers/Providers/EventHandler.tsx | 4 -- web/helpers/atoms/ChatMessage.atom.ts | 2 - web/hooks/useInference.ts | 15 +++++++ web/hooks/useSendChatMessage.ts | 38 ++++++++---------- web/screens/Chat/ChatBody/index.tsx | 10 ++--- web/screens/Chat/ChatInput/index.tsx | 25 +++++++----- .../Chat/MessageQueuedBanner/index.tsx | 6 ++- web/screens/Chat/index.tsx | 40 ++++++++++--------- 8 files changed, 74 insertions(+), 66 deletions(-) create mode 100644 web/hooks/useInference.ts diff --git a/web/containers/Providers/EventHandler.tsx b/web/containers/Providers/EventHandler.tsx index 01d32b346..2b990ec0a 100644 --- a/web/containers/Providers/EventHandler.tsx +++ b/web/containers/Providers/EventHandler.tsx @@ -22,7 +22,6 @@ import { extensionManager } from '@/extension' import { addNewMessageAtom, updateMessageAtom, - generateResponseAtom, } from '@/helpers/atoms/ChatMessage.atom' import { updateThreadWaitingForResponseAtom, @@ -35,7 +34,6 @@ export default function EventHandler({ children }: { children: ReactNode }) { const { downloadedModels } = useGetDownloadedModels() const setActiveModel = useSetAtom(activeModelAtom) const setStateModel = useSetAtom(stateModelAtom) - const setGenerateResponse = useSetAtom(generateResponseAtom) const updateThreadWaiting = useSetAtom(updateThreadWaitingForResponseAtom) const threads = useAtomValue(threadsAtom) @@ -52,7 +50,6 @@ export default function EventHandler({ children }: { children: ReactNode }) { const onNewMessageResponse = useCallback( (message: ThreadMessage) => { - setGenerateResponse(false) addNewMessage(message) }, [addNewMessage] @@ -96,7 +93,6 @@ export default function EventHandler({ children }: { children: ReactNode }) { const onMessageResponseUpdate = useCallback( (message: ThreadMessage) => { - setGenerateResponse(false) updateMessage( message.id, message.thread_id, diff --git a/web/helpers/atoms/ChatMessage.atom.ts b/web/helpers/atoms/ChatMessage.atom.ts index 0d9211649..b11e8f3be 100644 --- a/web/helpers/atoms/ChatMessage.atom.ts +++ b/web/helpers/atoms/ChatMessage.atom.ts @@ -14,8 +14,6 @@ import { /** * Stores all chat messages for all threads */ -export const generateResponseAtom = atom(false) - export const chatMessages = atom>({}) /** diff --git a/web/hooks/useInference.ts b/web/hooks/useInference.ts new file mode 100644 index 000000000..8ada18cb7 --- /dev/null +++ b/web/hooks/useInference.ts @@ -0,0 +1,15 @@ +import { useAtomValue } from 'jotai' + +import { threadStatesAtom } from '@/helpers/atoms/Thread.atom' + +export default function useInference() { + const threadStates = useAtomValue(threadStatesAtom) + + const isGeneratingResponse = Object.values(threadStates).some( + (threadState) => threadState.waitingForResponse + ) + + return { + isGeneratingResponse, + } +} diff --git a/web/hooks/useSendChatMessage.ts b/web/hooks/useSendChatMessage.ts index c8a32564b..5df715bb0 100644 --- a/web/hooks/useSendChatMessage.ts +++ b/web/hooks/useSendChatMessage.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { useEffect, useRef, useState } from 'react' +import { useEffect, useRef } from 'react' import { ChatCompletionMessage, @@ -18,7 +18,7 @@ import { ChatCompletionMessageContentType, AssistantTool, } from '@janhq/core' -import { useAtom, useAtomValue, useSetAtom } from 'jotai' +import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai' import { ulid } from 'ulid' @@ -35,7 +35,6 @@ import { useActiveModel } from './useActiveModel' import { extensionManager } from '@/extension/ExtensionManager' import { addNewMessageAtom, - generateResponseAtom, getCurrentChatMessagesAtom, } from '@/helpers/atoms/ChatMessage.atom' import { @@ -48,29 +47,30 @@ import { updateThreadWaitingForResponseAtom, } from '@/helpers/atoms/Thread.atom' +export const queuedMessageAtom = atom(false) +export const reloadModelAtom = atom(false) + export default function useSendChatMessage() { const activeThread = useAtomValue(activeThreadAtom) const addNewMessage = useSetAtom(addNewMessageAtom) const updateThread = useSetAtom(updateThreadAtom) const updateThreadWaiting = useSetAtom(updateThreadWaitingForResponseAtom) - const [currentPrompt, setCurrentPrompt] = useAtom(currentPromptAtom) - const setGenerateResponse = useSetAtom(generateResponseAtom) + const setCurrentPrompt = useSetAtom(currentPromptAtom) const currentMessages = useAtomValue(getCurrentChatMessagesAtom) const { activeModel } = useActiveModel() const selectedModel = useAtomValue(selectedModelAtom) const { startModel } = useActiveModel() - const [queuedMessage, setQueuedMessage] = useState(false) + const setQueuedMessage = useSetAtom(queuedMessageAtom) const modelRef = useRef() const threadStates = useAtomValue(threadStatesAtom) const updateThreadInitSuccess = useSetAtom(updateThreadInitSuccessAtom) const activeModelParams = useAtomValue(getActiveThreadModelParamsAtom) - const engineParamsUpdate = useAtomValue(engineParamsUpdateAtom) - const setEngineParamsUpdate = useSetAtom(engineParamsUpdateAtom) - const [reloadModel, setReloadModel] = useState(false) + const setEngineParamsUpdate = useSetAtom(engineParamsUpdateAtom) + const setReloadModel = useSetAtom(reloadModelAtom) const [fileUpload, setFileUpload] = useAtom(fileUploadAtom) useEffect(() => { @@ -82,9 +82,7 @@ export default function useSendChatMessage() { console.error('No active thread') return } - updateThreadWaiting(activeThread.id, true) - const messages: ChatCompletionMessage[] = [ activeThread.assistants[0]?.instructions, ] @@ -121,19 +119,19 @@ export default function useSendChatMessage() { if (activeModel?.id !== modelId) { setQueuedMessage(true) startModel(modelId) - await WaitForModelStarting(modelId) + await waitForModelStarting(modelId) setQueuedMessage(false) } events.emit(MessageEvent.OnMessageSent, messageRequest) } // TODO: Refactor @louis - const WaitForModelStarting = async (modelId: string) => { + const waitForModelStarting = async (modelId: string) => { return new Promise((resolve) => { setTimeout(async () => { if (modelRef.current?.id !== modelId) { console.debug('waiting for model to start') - await WaitForModelStarting(modelId) + await waitForModelStarting(modelId) resolve() } else { resolve() @@ -142,10 +140,8 @@ export default function useSendChatMessage() { }) } - const sendChatMessage = async () => { - setGenerateResponse(true) - - if (!currentPrompt || currentPrompt.trim().length === 0) return + const sendChatMessage = async (message: string) => { + if (!message || message.trim().length === 0) return if (!activeThread) { console.error('No active thread') @@ -199,7 +195,7 @@ export default function useSendChatMessage() { updateThreadWaiting(activeThread.id, true) - const prompt = currentPrompt.trim() + const prompt = message.trim() setCurrentPrompt('') const base64Blob = fileUpload[0] @@ -335,7 +331,7 @@ export default function useSendChatMessage() { if (activeModel?.id !== modelId) { setQueuedMessage(true) startModel(modelId) - await WaitForModelStarting(modelId) + await waitForModelStarting(modelId) setQueuedMessage(false) } @@ -346,9 +342,7 @@ export default function useSendChatMessage() { } return { - reloadModel, sendChatMessage, resendChatMessage, - queuedMessage, } } diff --git a/web/screens/Chat/ChatBody/index.tsx b/web/screens/Chat/ChatBody/index.tsx index 9f629e627..daf27f8dd 100644 --- a/web/screens/Chat/ChatBody/index.tsx +++ b/web/screens/Chat/ChatBody/index.tsx @@ -15,23 +15,21 @@ import { MainViewState } from '@/constants/screens' import { activeModelAtom } from '@/hooks/useActiveModel' import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels' +import useInference from '@/hooks/useInference' import { useMainViewState } from '@/hooks/useMainViewState' import ChatItem from '../ChatItem' import ErrorMessage from '../ErrorMessage' -import { - generateResponseAtom, - getCurrentChatMessagesAtom, -} from '@/helpers/atoms/ChatMessage.atom' +import { getCurrentChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom' const ChatBody: React.FC = () => { const messages = useAtomValue(getCurrentChatMessagesAtom) const activeModel = useAtomValue(activeModelAtom) const { downloadedModels } = useGetDownloadedModels() const { setMainViewState } = useMainViewState() - const generateResponse = useAtomValue(generateResponseAtom) + const { isGeneratingResponse } = useInference() if (downloadedModels.length === 0) return ( @@ -101,7 +99,7 @@ const ChatBody: React.FC = () => { ))} {activeModel && - (generateResponse || + (isGeneratingResponse || (messages.length && messages[messages.length - 1].status === MessageStatus.Pending && diff --git a/web/screens/Chat/ChatInput/index.tsx b/web/screens/Chat/ChatInput/index.tsx index b960bdc57..9293cdc7a 100644 --- a/web/screens/Chat/ChatInput/index.tsx +++ b/web/screens/Chat/ChatInput/index.tsx @@ -64,13 +64,18 @@ const ChatInput: React.FC = () => { useEffect(() => { if (isWaitingToSend && activeThreadId) { setIsWaitingToSend(false) - sendChatMessage() + sendChatMessage(currentPrompt) } if (textareaRef.current) { textareaRef.current.focus() } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [waitingToSendMessage, activeThreadId]) + }, [ + activeThreadId, + isWaitingToSend, + currentPrompt, + setIsWaitingToSend, + sendChatMessage, + ]) useEffect(() => { if (textareaRef.current) { @@ -81,13 +86,11 @@ const ChatInput: React.FC = () => { }, [currentPrompt]) const onKeyDown = async (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - if (!e.shiftKey) { - e.preventDefault() - if (messages[messages.length - 1]?.status !== MessageStatus.Pending) - sendChatMessage() - else onStopInferenceClick() - } + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault() + if (messages[messages.length - 1]?.status !== MessageStatus.Pending) + sendChatMessage(currentPrompt) + else onStopInferenceClick() } } @@ -237,7 +240,7 @@ const ChatInput: React.FC = () => { } themes="primary" className="min-w-[100px]" - onClick={sendChatMessage} + onClick={() => sendChatMessage(currentPrompt)} > Send diff --git a/web/screens/Chat/MessageQueuedBanner/index.tsx b/web/screens/Chat/MessageQueuedBanner/index.tsx index df9aa5a21..5847394b4 100644 --- a/web/screens/Chat/MessageQueuedBanner/index.tsx +++ b/web/screens/Chat/MessageQueuedBanner/index.tsx @@ -1,7 +1,9 @@ -import useSendChatMessage from '@/hooks/useSendChatMessage' +import { useAtomValue } from 'jotai' + +import { queuedMessageAtom } from '@/hooks/useSendChatMessage' const MessageQueuedBanner: React.FC = () => { - const { queuedMessage } = useSendChatMessage() + const queuedMessage = useAtomValue(queuedMessageAtom) return (
diff --git a/web/screens/Chat/index.tsx b/web/screens/Chat/index.tsx index 6da8af13f..cfd47ad39 100644 --- a/web/screens/Chat/index.tsx +++ b/web/screens/Chat/index.tsx @@ -15,7 +15,7 @@ import ModelStart from '@/containers/Loader/ModelStart' import { currentPromptAtom, fileUploadAtom } from '@/containers/Providers/Jotai' import { showLeftSideBarAtom } from '@/containers/Providers/KeyListener' -import useSendChatMessage from '@/hooks/useSendChatMessage' +import { queuedMessageAtom, reloadModelAtom } from '@/hooks/useSendChatMessage' import ChatBody from '@/screens/Chat/ChatBody' @@ -30,20 +30,37 @@ import { engineParamsUpdateAtom, } from '@/helpers/atoms/Thread.atom' +const renderError = (code: string) => { + switch (code) { + case 'multiple-upload': + return 'Currently, we only support 1 attachment at the same time' + + case 'retrieval-off': + return 'Turn on Retrieval in Assistant Settings to use this feature' + + case 'file-invalid-type': + return 'We do not support this file type' + + default: + return 'Oops, something error, please try again.' + } +} + const ChatScreen: React.FC = () => { const setCurrentPrompt = useSetAtom(currentPromptAtom) const activeThread = useAtomValue(activeThreadAtom) const showLeftSideBar = useAtomValue(showLeftSideBarAtom) const engineParamsUpdate = useAtomValue(engineParamsUpdateAtom) - const { queuedMessage, reloadModel } = useSendChatMessage() const [dragOver, setDragOver] = useState(false) + + const queuedMessage = useAtomValue(queuedMessageAtom) + const reloadModel = useAtomValue(reloadModelAtom) const [dragRejected, setDragRejected] = useState({ code: '' }) const setFileUpload = useSetAtom(fileUploadAtom) const { getRootProps, isDragReject } = useDropzone({ noClick: true, multiple: false, accept: { - // 'image/*': ['.png', '.jpg', '.jpeg'], 'application/pdf': ['.pdf'], }, @@ -104,22 +121,6 @@ const ChatScreen: React.FC = () => { }, 2000) }, [dragRejected.code]) - const renderError = (code: string) => { - switch (code) { - case 'multiple-upload': - return 'Currently, we only support 1 attachment at the same time' - - case 'retrieval-off': - return 'Turn on Retrieval in Assistant Settings to use this feature' - - case 'file-invalid-type': - return 'We do not support this file type' - - default: - return 'Oops, something error, please try again.' - } - } - return (
{/* Left side bar */} @@ -216,6 +217,7 @@ const ChatScreen: React.FC = () => {
+ {/* Right side bar */} {activeThread && }
From 391f053266c1b068815942288a3f17987f535b0e Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Mon, 29 Jan 2024 14:04:17 +0700 Subject: [PATCH 29/71] fix: typo copy --- web/hooks/useSendChatMessage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/hooks/useSendChatMessage.ts b/web/hooks/useSendChatMessage.ts index 15caa62c9..f72f5c36a 100644 --- a/web/hooks/useSendChatMessage.ts +++ b/web/hooks/useSendChatMessage.ts @@ -332,7 +332,7 @@ export default function useSendChatMessage() { updated: timestamp, } - // cheange last update thread when send message + // change last update thread when send message updateThread(updatedThread) await extensionManager From 7b1337aee7cc368c9c35b2e643d1c940e14aa155 Mon Sep 17 00:00:00 2001 From: NamH Date: Mon, 29 Jan 2024 14:31:17 +0700 Subject: [PATCH 30/71] fix: preserve focused thread when navigating in jan app (#1814) * fix: preserve focused thread when navigating in jan app Signed-off-by: James * Update web/hooks/useThreads.ts Co-authored-by: Louis --------- Signed-off-by: James Co-authored-by: James Co-authored-by: Louis --- web/hooks/useThreads.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/web/hooks/useThreads.ts b/web/hooks/useThreads.ts index b79cfea92..44be485fe 100644 --- a/web/hooks/useThreads.ts +++ b/web/hooks/useThreads.ts @@ -5,13 +5,14 @@ import { ConversationalExtension, } from '@janhq/core' -import { useAtom } from 'jotai' +import { useAtom, useAtomValue } from 'jotai' import useSetActiveThread from './useSetActiveThread' import { extensionManager } from '@/extension/ExtensionManager' import { ModelParams, + activeThreadAtom, threadModelParamsAtom, threadStatesAtom, threadsAtom, @@ -23,6 +24,7 @@ const useThreads = () => { const [threadModelRuntimeParams, setThreadModelRuntimeParams] = useAtom( threadModelParamsAtom ) + const activeThread = useAtomValue(activeThreadAtom) const { setActiveThread } = useSetActiveThread() const getThreads = async () => { @@ -84,7 +86,7 @@ const useThreads = () => { setThreadStates(localThreadStates) setThreads(allThreads) setThreadModelRuntimeParams(threadModelParams) - if (allThreads.length > 0) { + if (allThreads.length && !activeThread) { setActiveThread(allThreads[0]) } } catch (error) { From 86a5de2f1c4c7967a70ccf71bd6216bcc9c87cdd Mon Sep 17 00:00:00 2001 From: hiento09 <136591877+hiento09@users.noreply.github.com> Date: Mon, 29 Jan 2024 15:27:43 +0700 Subject: [PATCH 31/71] Add code sign step for darwin assistant extension (#1841) Co-authored-by: Service Account --- extensions/assistant-extension/package.json | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/extensions/assistant-extension/package.json b/extensions/assistant-extension/package.json index f4e4dd825..84bcdf47e 100644 --- a/extensions/assistant-extension/package.json +++ b/extensions/assistant-extension/package.json @@ -8,7 +8,10 @@ "license": "AGPL-3.0", "scripts": { "build": "tsc --module commonjs && rollup -c rollup.config.ts", - "build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/pre-install" + "build:publish:linux": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/pre-install", + "build:publish:darwin": "rimraf *.tgz --glob && npm run build && ../../.github/scripts/auto-sign.sh && npm pack && cpx *.tgz ../../electron/pre-install", + "build:publish:win32": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/pre-install", + "build:publish": "run-script-os" }, "devDependencies": { "@rollup/plugin-commonjs": "^25.0.7", @@ -22,7 +25,8 @@ "rollup-plugin-define": "^1.0.1", "rollup-plugin-sourcemaps": "^0.6.3", "rollup-plugin-typescript2": "^0.36.0", - "typescript": "^5.3.3" + "typescript": "^5.3.3", + "run-script-os": "^1.1.6" }, "dependencies": { "@janhq/core": "file:../../core", From f19db6c2ebf5f5d05c68aefb160cdf8e4da93358 Mon Sep 17 00:00:00 2001 From: Louis Date: Mon, 29 Jan 2024 15:28:31 +0700 Subject: [PATCH 32/71] chore: The Data Folder is no longer an experimental feature (#1847) --- web/screens/Settings/Advanced/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/screens/Settings/Advanced/index.tsx b/web/screens/Settings/Advanced/index.tsx index e1f733699..5c85a0e1e 100644 --- a/web/screens/Settings/Advanced/index.tsx +++ b/web/screens/Settings/Advanced/index.tsx @@ -137,7 +137,7 @@ const Advanced = () => { )} {/* Directory */} - {experimentalFeature && } + {/* Proxy */}
From 00a109d46b4ec459c7e76ab8147fd935b585c8e0 Mon Sep 17 00:00:00 2001 From: Service Account Date: Mon, 29 Jan 2024 08:48:40 +0000 Subject: [PATCH 33/71] janhq/jan: Update README.md with nightly build artifact URL --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 1e2d76853..ee9862d45 100644 --- a/README.md +++ b/README.md @@ -76,31 +76,31 @@ Jan is an open-source ChatGPT alternative that runs 100% offline on your compute Experimental (Nightly Build) - + jan.exe - + Intel - + M1/M2 - + jan.deb - + jan.AppImage From a4772202af594e8a3612eee170c6e0aef9f5d7b5 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Mon, 29 Jan 2024 17:00:23 +0700 Subject: [PATCH 34/71] fix: add loader when user change folder --- web/containers/Layout/index.tsx | 1 - web/screens/Settings/Advanced/DataFolder/index.tsx | 11 +++++++++-- web/screens/Settings/index.tsx | 1 + 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/web/containers/Layout/index.tsx b/web/containers/Layout/index.tsx index 033038bad..e7bde49c0 100644 --- a/web/containers/Layout/index.tsx +++ b/web/containers/Layout/index.tsx @@ -27,7 +27,6 @@ const BaseLayout = (props: PropsWithChildren) => { useEffect(() => { if (localStorage.getItem(SUCCESS_SET_NEW_DESTINATION) === 'true') { setMainViewState(MainViewState.Settings) - localStorage.removeItem(SUCCESS_SET_NEW_DESTINATION) } }, [setMainViewState]) diff --git a/web/screens/Settings/Advanced/DataFolder/index.tsx b/web/screens/Settings/Advanced/DataFolder/index.tsx index 9a1863fa2..4b242f235 100644 --- a/web/screens/Settings/Advanced/DataFolder/index.tsx +++ b/web/screens/Settings/Advanced/DataFolder/index.tsx @@ -5,6 +5,8 @@ import { Button, Input } from '@janhq/uikit' import { useSetAtom } from 'jotai' import { PencilIcon, FolderOpenIcon } from 'lucide-react' +import Loader from '@/containers/Loader' + import { SUCCESS_SET_NEW_DESTINATION } from '@/hooks/useVaultDirectory' import ModalChangeDirectory, { @@ -13,10 +15,12 @@ import ModalChangeDirectory, { import ModalErrorSetDestGlobal, { showChangeFolderErrorAtom, } from './ModalErrorSetDestGlobal' + import ModalSameDirectory, { showSamePathModalAtom } from './ModalSameDirectory' const DataFolder = () => { const [janDataFolderPath, setJanDataFolderPath] = useState('') + const [showLoader, setShowLoader] = useState(false) const setShowDirectoryConfirm = useSetAtom(showDirectoryConfirmModalAtom) const setShowSameDirectory = useSetAtom(showSamePathModalAtom) const setShowChangeFolderError = useSetAtom(showChangeFolderErrorAtom) @@ -46,18 +50,20 @@ const DataFolder = () => { const onUserConfirmed = useCallback(async () => { if (!destinationPath) return try { + setShowLoader(true) 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') + setTimeout(() => { + setShowLoader(false) + }, 1200) await window.core?.api?.relaunch() } catch (e) { console.error(`Error: ${e}`) @@ -107,6 +113,7 @@ const DataFolder = () => { onUserConfirmed={onUserConfirmed} /> + {showLoader && } ) } diff --git a/web/screens/Settings/index.tsx b/web/screens/Settings/index.tsx index ea12ccc20..8f3860ee9 100644 --- a/web/screens/Settings/index.tsx +++ b/web/screens/Settings/index.tsx @@ -49,6 +49,7 @@ const SettingsScreen = () => { useEffect(() => { if (localStorage.getItem(SUCCESS_SET_NEW_DESTINATION) === 'true') { setActiveStaticMenu('Advanced Settings') + localStorage.removeItem(SUCCESS_SET_NEW_DESTINATION) } }, []) From 1689702dcdda534612be432b11f4b53dc19fbb12 Mon Sep 17 00:00:00 2001 From: NamH Date: Mon, 29 Jan 2024 18:53:49 +0700 Subject: [PATCH 35/71] fix: all input text box are disabled (#1855) Signed-off-by: James Co-authored-by: James --- web/screens/Chat/ChatInput/index.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/web/screens/Chat/ChatInput/index.tsx b/web/screens/Chat/ChatInput/index.tsx index 9293cdc7a..b760ab44c 100644 --- a/web/screens/Chat/ChatInput/index.tsx +++ b/web/screens/Chat/ChatInput/index.tsx @@ -66,9 +66,6 @@ const ChatInput: React.FC = () => { setIsWaitingToSend(false) sendChatMessage(currentPrompt) } - if (textareaRef.current) { - textareaRef.current.focus() - } }, [ activeThreadId, isWaitingToSend, @@ -77,11 +74,16 @@ const ChatInput: React.FC = () => { sendChatMessage, ]) + useEffect(() => { + if (textareaRef.current) { + textareaRef.current.focus() + } + }, [activeThreadId]) + useEffect(() => { if (textareaRef.current) { textareaRef.current.style.height = '40px' textareaRef.current.style.height = textareaRef.current.scrollHeight + 'px' - textareaRef.current.focus() } }, [currentPrompt]) From 24fc0f027d01b55b8cc9a28830b647f2d6116b84 Mon Sep 17 00:00:00 2001 From: Service Account Date: Mon, 29 Jan 2024 12:17:38 +0000 Subject: [PATCH 36/71] janhq/jan: Update README.md with nightly build artifact URL --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ee9862d45..f8b09d8d0 100644 --- a/README.md +++ b/README.md @@ -76,31 +76,31 @@ Jan is an open-source ChatGPT alternative that runs 100% offline on your compute Experimental (Nightly Build) - + jan.exe - + Intel - + M1/M2 - + jan.deb - + jan.AppImage From 12ebf272d614083f22e421166a811272f5a3bd7a Mon Sep 17 00:00:00 2001 From: Louis Date: Mon, 29 Jan 2024 22:44:13 +0700 Subject: [PATCH 37/71] fix: retrieval always ask for api key --- .../src/node/tools/retrieval/index.ts | 9 +++++---- extensions/conversational-extension/src/index.ts | 13 +++++++------ web/containers/Providers/EventHandler.tsx | 2 ++ web/screens/Chat/ChatBody/index.tsx | 9 +-------- web/screens/Chat/MessageToolbar/index.tsx | 5 ++++- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/extensions/assistant-extension/src/node/tools/retrieval/index.ts b/extensions/assistant-extension/src/node/tools/retrieval/index.ts index f9d5c4029..cd7e9abb1 100644 --- a/extensions/assistant-extension/src/node/tools/retrieval/index.ts +++ b/extensions/assistant-extension/src/node/tools/retrieval/index.ts @@ -12,12 +12,11 @@ export class Retrieval { public chunkOverlap?: number = 0; private retriever: any; - private embeddingModel: any = undefined; + private embeddingModel?: OpenAIEmbeddings = undefined; private textSplitter?: RecursiveCharacterTextSplitter; constructor(chunkSize: number = 4000, chunkOverlap: number = 200) { this.updateTextSplitter(chunkSize, chunkOverlap); - this.embeddingModel = new OpenAIEmbeddings({}); } public updateTextSplitter(chunkSize: number, chunkOverlap: number): void { @@ -36,7 +35,7 @@ export class Retrieval { if (engine === "nitro") { this.embeddingModel = new OpenAIEmbeddings( { openAIApiKey: "nitro-embedding" }, - { basePath: "http://127.0.0.1:3928/v1" }, + { basePath: "http://127.0.0.1:3928/v1" } ); } else { // Fallback to OpenAI Settings @@ -50,11 +49,12 @@ export class Retrieval { public ingestAgentKnowledge = async ( filePath: string, - memoryPath: string, + memoryPath: string ): Promise => { const loader = new PDFLoader(filePath, { splitPages: true, }); + if (!this.embeddingModel) return Promise.reject(); const doc = await loader.load(); const docs = await this.textSplitter!.splitDocuments(doc); const vectorStore = await HNSWLib.fromDocuments(docs, this.embeddingModel); @@ -62,6 +62,7 @@ export class Retrieval { }; public loadRetrievalAgent = async (memoryPath: string): Promise => { + if (!this.embeddingModel) return Promise.reject(); const vectorStore = await HNSWLib.load(memoryPath, this.embeddingModel); this.retriever = vectorStore.asRetriever(2); return Promise.resolve(); diff --git a/extensions/conversational-extension/src/index.ts b/extensions/conversational-extension/src/index.ts index 61f0fd0e9..3d28a9c1d 100644 --- a/extensions/conversational-extension/src/index.ts +++ b/extensions/conversational-extension/src/index.ts @@ -119,19 +119,20 @@ export default class JSONConversationalExtension extends ConversationalExtension if (!(await fs.existsSync(threadDirPath))) await fs.mkdirSync(threadDirPath) - if (message.content[0].type === 'image') { + if (message.content[0]?.type === 'image') { const filesPath = await joinPath([threadDirPath, 'files']) if (!(await fs.existsSync(filesPath))) await fs.mkdirSync(filesPath) const imagePath = await joinPath([filesPath, `${message.id}.png`]) const base64 = message.content[0].text.annotations[0] await this.storeImage(base64, imagePath) - // if (fs.existsSync(imagePath)) { - // message.content[0].text.annotations[0] = imagePath - // } + if ((await fs.existsSync(imagePath)) && message.content?.length) { + // Use file path instead of blob + message.content[0].text.annotations[0] = `threads/${message.thread_id}/files/${message.id}.png` + } } - if (message.content[0].type === 'pdf') { + if (message.content[0]?.type === 'pdf') { const filesPath = await joinPath([threadDirPath, 'files']) if (!(await fs.existsSync(filesPath))) await fs.mkdirSync(filesPath) @@ -139,7 +140,7 @@ export default class JSONConversationalExtension extends ConversationalExtension const blob = message.content[0].text.annotations[0] await this.storeFile(blob, filePath) - if (await fs.existsSync(filePath)) { + if ((await fs.existsSync(filePath)) && message.content?.length) { // Use file path instead of blob message.content[0].text.annotations[0] = `threads/${message.thread_id}/files/${message.id}.pdf` } diff --git a/web/containers/Providers/EventHandler.tsx b/web/containers/Providers/EventHandler.tsx index 2b990ec0a..5af6d4917 100644 --- a/web/containers/Providers/EventHandler.tsx +++ b/web/containers/Providers/EventHandler.tsx @@ -100,6 +100,8 @@ export default function EventHandler({ children }: { children: ReactNode }) { message.status ) if (message.status === MessageStatus.Pending) { + if (message.content.length) + updateThreadWaiting(message.thread_id, false) return } // Mark the thread as not waiting for response diff --git a/web/screens/Chat/ChatBody/index.tsx b/web/screens/Chat/ChatBody/index.tsx index daf27f8dd..e0a34a1a1 100644 --- a/web/screens/Chat/ChatBody/index.tsx +++ b/web/screens/Chat/ChatBody/index.tsx @@ -98,14 +98,7 @@ const ChatBody: React.FC = () => {
))} - {activeModel && - (isGeneratingResponse || - (messages.length && - messages[messages.length - 1].status === - MessageStatus.Pending && - !messages[messages.length - 1].content.length)) && ( - - )} + {activeModel && isGeneratingResponse && } )} diff --git a/web/screens/Chat/MessageToolbar/index.tsx b/web/screens/Chat/MessageToolbar/index.tsx index dfa8d63c6..070022122 100644 --- a/web/screens/Chat/MessageToolbar/index.tsx +++ b/web/screens/Chat/MessageToolbar/index.tsx @@ -4,6 +4,7 @@ import { ThreadMessage, ChatCompletionRole, ConversationalExtension, + ContentType, } from '@janhq/core' import { useAtomValue, useSetAtom } from 'jotai' import { RefreshCcw, CopyIcon, Trash2Icon, CheckIcon } from 'lucide-react' @@ -53,7 +54,9 @@ const MessageToolbar = ({ message }: { message: ThreadMessage }) => {
{message.id === messages[messages.length - 1]?.id && - messages[messages.length - 1].status !== MessageStatus.Error && ( + messages[messages.length - 1].status !== MessageStatus.Error && + messages[messages.length - 1].content[0]?.type !== + ContentType.Pdf && (
Date: Mon, 29 Jan 2024 20:22:44 +0000 Subject: [PATCH 38/71] janhq/jan: Update README.md with nightly build artifact URL --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f8b09d8d0..c31602fdd 100644 --- a/README.md +++ b/README.md @@ -76,31 +76,31 @@ Jan is an open-source ChatGPT alternative that runs 100% offline on your compute Experimental (Nightly Build) - + jan.exe - + Intel - + M1/M2 - + jan.deb - + jan.AppImage From a2b605591100aa1df9d9d2b9249a8b74978d07d3 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Tue, 30 Jan 2024 11:20:52 +0700 Subject: [PATCH 39/71] fix: auto collapse retrieval setting while update config --- web/screens/Chat/AssistantSetting/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/screens/Chat/AssistantSetting/index.tsx b/web/screens/Chat/AssistantSetting/index.tsx index b97c39e67..df516def0 100644 --- a/web/screens/Chat/AssistantSetting/index.tsx +++ b/web/screens/Chat/AssistantSetting/index.tsx @@ -57,7 +57,7 @@ const AssistantSetting = ({ tools: [ { type: 'retrieval', - enabled: false, + enabled: true, settings: { ...(activeThread.assistants[0].tools && activeThread.assistants[0].tools[0]?.settings), From 5e13fd2f53bb6127770b053f1a922db2eb428999 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Tue, 30 Jan 2024 16:36:58 +0700 Subject: [PATCH 40/71] fix: loader error change folder --- web/containers/Layout/index.tsx | 3 +- web/hooks/useVaultDirectory.ts | 87 ------------------- .../DataFolder/ModalSameDirectory.tsx | 15 +++- .../Settings/Advanced/DataFolder/index.tsx | 5 +- web/screens/Settings/index.tsx | 4 +- 5 files changed, 20 insertions(+), 94 deletions(-) delete mode 100644 web/hooks/useVaultDirectory.ts diff --git a/web/containers/Layout/index.tsx b/web/containers/Layout/index.tsx index e7bde49c0..77a1fe971 100644 --- a/web/containers/Layout/index.tsx +++ b/web/containers/Layout/index.tsx @@ -12,7 +12,8 @@ import TopBar from '@/containers/Layout/TopBar' import { MainViewState } from '@/constants/screens' import { useMainViewState } from '@/hooks/useMainViewState' -import { SUCCESS_SET_NEW_DESTINATION } from '@/hooks/useVaultDirectory' + +import { SUCCESS_SET_NEW_DESTINATION } from '@/screens/Settings/Advanced/DataFolder' const BaseLayout = (props: PropsWithChildren) => { const { children } = props diff --git a/web/hooks/useVaultDirectory.ts b/web/hooks/useVaultDirectory.ts deleted file mode 100644 index 9d7adf2ab..000000000 --- a/web/hooks/useVaultDirectory.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { useEffect, useState } from 'react' - -import { fs, AppConfiguration } from '@janhq/core' - -export const SUCCESS_SET_NEW_DESTINATION = 'successSetNewDestination' - -export function useVaultDirectory() { - const [isSameDirectory, setIsSameDirectory] = useState(false) - const [isDirectoryConfirm, setIsDirectoryConfirm] = useState(false) - const [isErrorSetNewDest, setIsErrorSetNewDest] = useState(false) - const [currentPath, setCurrentPath] = useState('') - const [newDestinationPath, setNewDestinationPath] = useState('') - - useEffect(() => { - window.core?.api - ?.getAppConfigurations() - ?.then((appConfig: AppConfiguration) => { - setCurrentPath(appConfig.data_folder) - }) - }, []) - - const setNewDestination = async () => { - const destFolder = await window.core?.api?.selectDirectory() - setNewDestinationPath(destFolder) - - if (destFolder) { - console.debug(`Destination folder selected: ${destFolder}`) - try { - const appConfiguration: AppConfiguration = - await window.core?.api?.getAppConfigurations() - const currentJanDataFolder = appConfiguration.data_folder - - if (currentJanDataFolder === destFolder) { - console.debug( - `Destination folder is the same as current folder. Ignore..` - ) - setIsSameDirectory(true) - setIsDirectoryConfirm(false) - return - } else { - setIsSameDirectory(false) - setIsDirectoryConfirm(true) - } - setIsErrorSetNewDest(false) - } catch (e) { - console.error(`Error: ${e}`) - setIsErrorSetNewDest(true) - } - } - } - - const applyNewDestination = async () => { - try { - const appConfiguration: AppConfiguration = - await window.core?.api?.getAppConfigurations() - const currentJanDataFolder = appConfiguration.data_folder - - appConfiguration.data_folder = newDestinationPath - - await fs.syncFile(currentJanDataFolder, newDestinationPath) - await window.core?.api?.updateAppConfiguration(appConfiguration) - console.debug( - `File sync finished from ${currentPath} to ${newDestinationPath}` - ) - - setIsErrorSetNewDest(false) - localStorage.setItem(SUCCESS_SET_NEW_DESTINATION, 'true') - await window.core?.api?.relaunch() - } catch (e) { - console.error(`Error: ${e}`) - setIsErrorSetNewDest(true) - } - } - - return { - setNewDestination, - newDestinationPath, - applyNewDestination, - isSameDirectory, - setIsDirectoryConfirm, - isDirectoryConfirm, - setIsSameDirectory, - currentPath, - isErrorSetNewDest, - setIsErrorSetNewDest, - } -} diff --git a/web/screens/Settings/Advanced/DataFolder/ModalSameDirectory.tsx b/web/screens/Settings/Advanced/DataFolder/ModalSameDirectory.tsx index 8b2d90c61..1909e6428 100644 --- a/web/screens/Settings/Advanced/DataFolder/ModalSameDirectory.tsx +++ b/web/screens/Settings/Advanced/DataFolder/ModalSameDirectory.tsx @@ -15,7 +15,11 @@ import { atom, useAtom } from 'jotai' export const showSamePathModalAtom = atom(false) -const ModalSameDirectory = () => { +type Props = { + onChangeFolderClick: () => void +} + +const ModalSameDirectory = ({ onChangeFolderClick }: Props) => { const [show, setShow] = useAtom(showSamePathModalAtom) return ( @@ -34,7 +38,14 @@ const ModalSameDirectory = () => { - diff --git a/web/screens/Settings/Advanced/DataFolder/index.tsx b/web/screens/Settings/Advanced/DataFolder/index.tsx index 4b242f235..e653e4b9b 100644 --- a/web/screens/Settings/Advanced/DataFolder/index.tsx +++ b/web/screens/Settings/Advanced/DataFolder/index.tsx @@ -7,7 +7,7 @@ import { PencilIcon, FolderOpenIcon } from 'lucide-react' import Loader from '@/containers/Loader' -import { SUCCESS_SET_NEW_DESTINATION } from '@/hooks/useVaultDirectory' +export const SUCCESS_SET_NEW_DESTINATION = 'successSetNewDestination' import ModalChangeDirectory, { showDirectoryConfirmModalAtom, @@ -67,6 +67,7 @@ const DataFolder = () => { await window.core?.api?.relaunch() } catch (e) { console.error(`Error: ${e}`) + setShowLoader(false) setShowChangeFolderError(true) } }, [destinationPath, setShowChangeFolderError]) @@ -107,7 +108,7 @@ const DataFolder = () => {
- + { const [activeStaticMenu, setActiveStaticMenu] = useState('My Models') const [menus, setMenus] = useState([]) From 00c4397be66901545353b11e56a67fbb549b734e Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Tue, 30 Jan 2024 16:43:58 +0700 Subject: [PATCH 41/71] fix: loader error change folder --- .../Advanced/DataFolder/ModalErrorSetDestGlobal.tsx | 7 +++++-- web/screens/Settings/Advanced/DataFolder/index.tsx | 9 +++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/web/screens/Settings/Advanced/DataFolder/ModalErrorSetDestGlobal.tsx b/web/screens/Settings/Advanced/DataFolder/ModalErrorSetDestGlobal.tsx index 3729dc0d8..125cd18bd 100644 --- a/web/screens/Settings/Advanced/DataFolder/ModalErrorSetDestGlobal.tsx +++ b/web/screens/Settings/Advanced/DataFolder/ModalErrorSetDestGlobal.tsx @@ -10,13 +10,15 @@ import { ModalClose, Button, } from '@janhq/uikit' -import { atom, useAtom } from 'jotai' +import { atom, useAtom, useAtomValue } from 'jotai' + +import { errorAtom } from '.' export const showChangeFolderErrorAtom = atom(false) const ModalErrorSetDestGlobal = () => { const [show, setShow] = useAtom(showChangeFolderErrorAtom) - + const error = useAtomValue(errorAtom) return ( @@ -28,6 +30,7 @@ const ModalErrorSetDestGlobal = () => { Oops! Something went wrong. Jan data folder remains the same. Please try again.

+

{error}

setShow(false)}> diff --git a/web/screens/Settings/Advanced/DataFolder/index.tsx b/web/screens/Settings/Advanced/DataFolder/index.tsx index e653e4b9b..b7bb88cdd 100644 --- a/web/screens/Settings/Advanced/DataFolder/index.tsx +++ b/web/screens/Settings/Advanced/DataFolder/index.tsx @@ -2,7 +2,7 @@ import { Fragment, useCallback, useEffect, useState } from 'react' import { fs, AppConfiguration } from '@janhq/core' import { Button, Input } from '@janhq/uikit' -import { useSetAtom } from 'jotai' +import { atom, useSetAtom } from 'jotai' import { PencilIcon, FolderOpenIcon } from 'lucide-react' import Loader from '@/containers/Loader' @@ -18,6 +18,8 @@ import ModalErrorSetDestGlobal, { import ModalSameDirectory, { showSamePathModalAtom } from './ModalSameDirectory' +export const errorAtom = atom('') + const DataFolder = () => { const [janDataFolderPath, setJanDataFolderPath] = useState('') const [showLoader, setShowLoader] = useState(false) @@ -25,6 +27,7 @@ const DataFolder = () => { const setShowSameDirectory = useSetAtom(showSamePathModalAtom) const setShowChangeFolderError = useSetAtom(showChangeFolderErrorAtom) const [destinationPath, setDestinationPath] = useState(undefined) + const setError = useSetAtom(errorAtom) useEffect(() => { window.core?.api @@ -65,8 +68,10 @@ const DataFolder = () => { setShowLoader(false) }, 1200) await window.core?.api?.relaunch() - } catch (e) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (e: any) { console.error(`Error: ${e}`) + setError(e.message) setShowLoader(false) setShowChangeFolderError(true) } From 96aded6b035a284e1b12c0c23325dcb53c0cd2e3 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Tue, 30 Jan 2024 17:04:56 +0700 Subject: [PATCH 42/71] fix: showing catch error on modal when change folder --- .../Settings/Advanced/DataFolder/ModalErrorSetDestGlobal.tsx | 2 +- web/screens/Settings/Advanced/DataFolder/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/screens/Settings/Advanced/DataFolder/ModalErrorSetDestGlobal.tsx b/web/screens/Settings/Advanced/DataFolder/ModalErrorSetDestGlobal.tsx index 125cd18bd..d80e34f3c 100644 --- a/web/screens/Settings/Advanced/DataFolder/ModalErrorSetDestGlobal.tsx +++ b/web/screens/Settings/Advanced/DataFolder/ModalErrorSetDestGlobal.tsx @@ -30,7 +30,7 @@ const ModalErrorSetDestGlobal = () => { Oops! Something went wrong. Jan data folder remains the same. Please try again.

-

{error}

+

{error}

setShow(false)}> diff --git a/web/screens/Settings/Advanced/DataFolder/index.tsx b/web/screens/Settings/Advanced/DataFolder/index.tsx index b7bb88cdd..1704cd964 100644 --- a/web/screens/Settings/Advanced/DataFolder/index.tsx +++ b/web/screens/Settings/Advanced/DataFolder/index.tsx @@ -75,7 +75,7 @@ const DataFolder = () => { setShowLoader(false) setShowChangeFolderError(true) } - }, [destinationPath, setShowChangeFolderError]) + }, [destinationPath, setError, setShowChangeFolderError]) return ( From 282dd58d0519c92ab5a335b3c5b1f60f8fe6f262 Mon Sep 17 00:00:00 2001 From: James Date: Tue, 30 Jan 2024 23:03:20 +0700 Subject: [PATCH 43/71] fix: not allow user to choose sub directory as jan data folder Signed-off-by: James --- core/src/api/index.ts | 1 + core/src/core.ts | 20 ++++++++++++++-- electron/handlers/app.ts | 23 ++++++++++++++++++- .../DataFolder/ModalErrorSetDestGlobal.tsx | 6 +---- .../Settings/Advanced/DataFolder/index.tsx | 22 ++++++++++-------- 5 files changed, 55 insertions(+), 17 deletions(-) diff --git a/core/src/api/index.ts b/core/src/api/index.ts index a232c4090..0adc8b7e2 100644 --- a/core/src/api/index.ts +++ b/core/src/api/index.ts @@ -12,6 +12,7 @@ export enum AppRoute { updateAppConfiguration = 'updateAppConfiguration', relaunch = 'relaunch', joinPath = 'joinPath', + isSubdirectory = 'isSubdirectory', baseName = 'baseName', startServer = 'startServer', stopServer = 'stopServer', diff --git a/core/src/core.ts b/core/src/core.ts index aa545e10e..24053e55c 100644 --- a/core/src/core.ts +++ b/core/src/core.ts @@ -22,7 +22,11 @@ const executeOnMain: (extension: string, method: string, ...args: any[]) => Prom * @param {object} network - Optional object to specify proxy/whether to ignore SSL certificates. * @returns {Promise} A promise that resolves when the file is downloaded. */ -const downloadFile: (url: string, fileName: string, network?: { proxy?: string, ignoreSSL?: boolean }) => Promise = (url, fileName, network) => { +const downloadFile: ( + url: string, + fileName: string, + network?: { proxy?: string; ignoreSSL?: boolean } +) => Promise = (url, fileName, network) => { return global.core?.api?.downloadFile(url, fileName, network) } @@ -87,6 +91,17 @@ const getResourcePath: () => Promise = () => global.core.api?.getResourc const log: (message: string, fileName?: string) => void = (message, fileName) => global.core.api?.log(message, fileName) +/** + * Check whether the path is a subdirectory of another path. + * + * @param from - The path to check. + * @param to - The path to check against. + * + * @returns {Promise} - A promise that resolves with a boolean indicating whether the path is a subdirectory. + */ +const isSubdirectory: (from: string, to: string) => Promise = (from: string, to: string) => + global.core.api?.isSubdirectory(from, to) + /** * Register extension point function type definition */ @@ -94,7 +109,7 @@ export type RegisterExtensionPoint = ( extensionName: string, extensionId: string, method: Function, - priority?: number, + priority?: number ) => void /** @@ -111,5 +126,6 @@ export { openExternalUrl, baseName, log, + isSubdirectory, FileStat, } diff --git a/electron/handlers/app.ts b/electron/handlers/app.ts index bdb70047a..c1f431ef3 100644 --- a/electron/handlers/app.ts +++ b/electron/handlers/app.ts @@ -1,5 +1,5 @@ import { app, ipcMain, dialog, shell } from 'electron' -import { join, basename } from 'path' +import { join, basename, relative as getRelative, isAbsolute } from 'path' import { WindowManager } from './../managers/window' import { getResourcePath } from './../utils/path' import { AppRoute, AppConfiguration } from '@janhq/core' @@ -50,6 +50,27 @@ export function handleAppIPCs() { join(...paths) ) + /** + * Checks if the given path is a subdirectory of the given directory. + * + * @param _event - The IPC event object. + * @param from - The path to check. + * @param to - The directory to check against. + * + * @returns {Promise} - A promise that resolves with the result. + */ + ipcMain.handle( + AppRoute.isSubdirectory, + async (_event, from: string, to: string) => { + const relative = getRelative(from, to) + const isSubdir = + relative && !relative.startsWith('..') && !isAbsolute(relative) + + if (isSubdir === '') return false + else return isSubdir + } + ) + /** * Retrieve basename from given path, respect to the current OS. */ diff --git a/web/screens/Settings/Advanced/DataFolder/ModalErrorSetDestGlobal.tsx b/web/screens/Settings/Advanced/DataFolder/ModalErrorSetDestGlobal.tsx index d80e34f3c..84646e735 100644 --- a/web/screens/Settings/Advanced/DataFolder/ModalErrorSetDestGlobal.tsx +++ b/web/screens/Settings/Advanced/DataFolder/ModalErrorSetDestGlobal.tsx @@ -10,15 +10,12 @@ import { ModalClose, Button, } from '@janhq/uikit' -import { atom, useAtom, useAtomValue } from 'jotai' - -import { errorAtom } from '.' +import { atom, useAtom } from 'jotai' export const showChangeFolderErrorAtom = atom(false) const ModalErrorSetDestGlobal = () => { const [show, setShow] = useAtom(showChangeFolderErrorAtom) - const error = useAtomValue(errorAtom) return ( @@ -30,7 +27,6 @@ const ModalErrorSetDestGlobal = () => { Oops! Something went wrong. Jan data folder remains the same. Please try again.

-

{error}

setShow(false)}> diff --git a/web/screens/Settings/Advanced/DataFolder/index.tsx b/web/screens/Settings/Advanced/DataFolder/index.tsx index 1704cd964..5abd5390b 100644 --- a/web/screens/Settings/Advanced/DataFolder/index.tsx +++ b/web/screens/Settings/Advanced/DataFolder/index.tsx @@ -1,8 +1,8 @@ import { Fragment, useCallback, useEffect, useState } from 'react' -import { fs, AppConfiguration } from '@janhq/core' +import { fs, AppConfiguration, isSubdirectory } from '@janhq/core' import { Button, Input } from '@janhq/uikit' -import { atom, useSetAtom } from 'jotai' +import { useSetAtom } from 'jotai' import { PencilIcon, FolderOpenIcon } from 'lucide-react' import Loader from '@/containers/Loader' @@ -18,8 +18,6 @@ import ModalErrorSetDestGlobal, { import ModalSameDirectory, { showSamePathModalAtom } from './ModalSameDirectory' -export const errorAtom = atom('') - const DataFolder = () => { const [janDataFolderPath, setJanDataFolderPath] = useState('') const [showLoader, setShowLoader] = useState(false) @@ -27,7 +25,6 @@ const DataFolder = () => { const setShowSameDirectory = useSetAtom(showSamePathModalAtom) const setShowChangeFolderError = useSetAtom(showChangeFolderErrorAtom) const [destinationPath, setDestinationPath] = useState(undefined) - const setError = useSetAtom(errorAtom) useEffect(() => { window.core?.api @@ -46,6 +43,15 @@ const DataFolder = () => { return } + const appConfiguration: AppConfiguration = + await window.core?.api?.getAppConfigurations() + const currentJanDataFolder = appConfiguration.data_folder + + if (await isSubdirectory(currentJanDataFolder, destFolder)) { + setShowSameDirectory(true) + return + } + setDestinationPath(destFolder) setShowDirectoryConfirm(true) }, [janDataFolderPath, setShowSameDirectory, setShowDirectoryConfirm]) @@ -68,14 +74,12 @@ const DataFolder = () => { setShowLoader(false) }, 1200) await window.core?.api?.relaunch() - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (e: any) { + } catch (e) { console.error(`Error: ${e}`) - setError(e.message) setShowLoader(false) setShowChangeFolderError(true) } - }, [destinationPath, setError, setShowChangeFolderError]) + }, [destinationPath, setShowChangeFolderError]) return ( From 0b20a401f55ab9884426b505898ff810147bca3e Mon Sep 17 00:00:00 2001 From: Service Account Date: Tue, 30 Jan 2024 20:26:39 +0000 Subject: [PATCH 44/71] janhq/jan: Update README.md with nightly build artifact URL --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c31602fdd..e30f310ea 100644 --- a/README.md +++ b/README.md @@ -76,31 +76,31 @@ Jan is an open-source ChatGPT alternative that runs 100% offline on your compute Experimental (Nightly Build) - + jan.exe - + Intel - + M1/M2 - + jan.deb - + jan.AppImage From 2ec6037b8a96a4735a38393b3cb1d16b6dc91a3c Mon Sep 17 00:00:00 2001 From: hiro Date: Wed, 31 Jan 2024 10:45:13 +0700 Subject: [PATCH 45/71] chore: Bump nitro to 0.3.3 for fixing hungup 2nd request --- extensions/inference-nitro-extension/bin/version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/inference-nitro-extension/bin/version.txt b/extensions/inference-nitro-extension/bin/version.txt index 769ed6ae7..1c09c74e2 100644 --- a/extensions/inference-nitro-extension/bin/version.txt +++ b/extensions/inference-nitro-extension/bin/version.txt @@ -1 +1 @@ -0.2.14 +0.3.3 From cfadd130e9685b2f0402699479c3d39270485042 Mon Sep 17 00:00:00 2001 From: hiento09 <136591877+hiento09@users.noreply.github.com> Date: Wed, 31 Jan 2024 11:26:30 +0700 Subject: [PATCH 46/71] Increase timeout for explore.e2e.spec test (#1844) * Increase timeout for explore.e2e.spec test * fix: test cases and timeout --------- Co-authored-by: Hien To Co-authored-by: Louis --- electron/playwright.config.ts | 10 ++-- .../{explore.e2e.spec.ts => hub.e2e.spec.ts} | 18 ++++-- electron/tests/main.e2e.spec.ts | 55 ------------------- electron/tests/navigation.e2e.spec.ts | 29 +++++----- electron/tests/settings.e2e.spec.ts | 10 +++- electron/tests/system-monitor.e2e.spec.ts | 41 -------------- web/screens/ExploreModels/index.tsx | 7 ++- web/screens/Settings/index.tsx | 5 +- web/screens/SystemMonitor/index.tsx | 2 +- 9 files changed, 51 insertions(+), 126 deletions(-) rename electron/tests/{explore.e2e.spec.ts => hub.e2e.spec.ts} (68%) delete mode 100644 electron/tests/main.e2e.spec.ts delete mode 100644 electron/tests/system-monitor.e2e.spec.ts diff --git a/electron/playwright.config.ts b/electron/playwright.config.ts index 98b2c7b45..1fa3313f2 100644 --- a/electron/playwright.config.ts +++ b/electron/playwright.config.ts @@ -1,9 +1,9 @@ -import { PlaywrightTestConfig } from "@playwright/test"; +import { PlaywrightTestConfig } from '@playwright/test' const config: PlaywrightTestConfig = { - testDir: "./tests", + testDir: './tests', retries: 0, - timeout: 120000, -}; + globalTimeout: 300000, +} -export default config; +export default config diff --git a/electron/tests/explore.e2e.spec.ts b/electron/tests/hub.e2e.spec.ts similarity index 68% rename from electron/tests/explore.e2e.spec.ts rename to electron/tests/hub.e2e.spec.ts index 77eb3dbda..6bfe45ac4 100644 --- a/electron/tests/explore.e2e.spec.ts +++ b/electron/tests/hub.e2e.spec.ts @@ -9,6 +9,7 @@ import { let electronApp: ElectronApplication let page: Page +const TIMEOUT: number = parseInt(process.env.TEST_TIMEOUT || '300000') test.beforeAll(async () => { process.env.CI = 'e2e' @@ -26,7 +27,9 @@ test.beforeAll(async () => { }) await stubDialog(electronApp, 'showMessageBox', { response: 1 }) - page = await electronApp.firstWindow() + page = await electronApp.firstWindow({ + timeout: TIMEOUT, + }) }) test.afterAll(async () => { @@ -34,8 +37,13 @@ test.afterAll(async () => { await page.close() }) -test('explores models', async () => { - await page.getByTestId('Hub').first().click() - await page.getByTestId('testid-explore-models').isVisible() - // More test cases here... +test('explores hub', async () => { + // Set the timeout for this test to 60 seconds + test.setTimeout(TIMEOUT) + await page.getByTestId('Hub').first().click({ + timeout: TIMEOUT, + }) + await page.getByTestId('hub-container-test-id').isVisible({ + timeout: TIMEOUT, + }) }) diff --git a/electron/tests/main.e2e.spec.ts b/electron/tests/main.e2e.spec.ts deleted file mode 100644 index 1a5bfe696..000000000 --- a/electron/tests/main.e2e.spec.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { _electron as electron } from 'playwright' -import { ElectronApplication, Page, expect, test } from '@playwright/test' - -import { - findLatestBuild, - parseElectronApp, - stubDialog, -} from 'electron-playwright-helpers' - -let electronApp: ElectronApplication -let page: Page - -test.beforeAll(async () => { - process.env.CI = 'e2e' - - const latestBuild = findLatestBuild('dist') - expect(latestBuild).toBeTruthy() - - // parse the packaged Electron app and find paths and other info - const appInfo = parseElectronApp(latestBuild) - expect(appInfo).toBeTruthy() - expect(appInfo.asar).toBe(true) - expect(appInfo.executable).toBeTruthy() - expect(appInfo.main).toBeTruthy() - expect(appInfo.name).toBe('jan') - expect(appInfo.packageJson).toBeTruthy() - expect(appInfo.packageJson.name).toBe('jan') - expect(appInfo.platform).toBeTruthy() - expect(appInfo.platform).toBe(process.platform) - expect(appInfo.resourcesDir).toBeTruthy() - - electronApp = await electron.launch({ - args: [appInfo.main], // main file from package.json - executablePath: appInfo.executable, // path to the Electron executable - }) - await stubDialog(electronApp, 'showMessageBox', { response: 1 }) - - page = await electronApp.firstWindow() -}) - -test.afterAll(async () => { - await electronApp.close() - await page.close() -}) - -test('renders the home page', async () => { - expect(page).toBeDefined() - - // Welcome text is available - const welcomeText = await page - .getByTestId('testid-welcome-title') - .first() - .isVisible() - expect(welcomeText).toBe(false) -}) diff --git a/electron/tests/navigation.e2e.spec.ts b/electron/tests/navigation.e2e.spec.ts index 2f4f7b767..2066fa60a 100644 --- a/electron/tests/navigation.e2e.spec.ts +++ b/electron/tests/navigation.e2e.spec.ts @@ -9,6 +9,7 @@ import { let electronApp: ElectronApplication let page: Page +const TIMEOUT: number = parseInt(process.env.TEST_TIMEOUT || '300000') test.beforeAll(async () => { process.env.CI = 'e2e' @@ -26,7 +27,9 @@ test.beforeAll(async () => { }) await stubDialog(electronApp, 'showMessageBox', { response: 1 }) - page = await electronApp.firstWindow() + page = await electronApp.firstWindow({ + timeout: TIMEOUT, + }) }) test.afterAll(async () => { @@ -35,20 +38,20 @@ test.afterAll(async () => { }) test('renders left navigation panel', async () => { - // Chat section should be there - const chatSection = await page.getByTestId('Chat').first().isVisible() - expect(chatSection).toBe(false) - - // Home actions - /* Disable unstable feature tests - ** const botBtn = await page.getByTestId("Bot").first().isEnabled(); - ** Enable back when it is whitelisted - */ - const systemMonitorBtn = await page .getByTestId('System Monitor') .first() - .isEnabled() - const settingsBtn = await page.getByTestId('Settings').first().isEnabled() + .isEnabled({ + timeout: TIMEOUT, + }) + const settingsBtn = await page + .getByTestId('Thread') + .first() + .isEnabled({ timeout: TIMEOUT }) expect([systemMonitorBtn, settingsBtn].filter((e) => !e).length).toBe(0) + // Chat section should be there + const apiServer = await page.getByTestId('Local API Server').first() + expect(apiServer).toBeVisible({ + timeout: TIMEOUT, + }) }) diff --git a/electron/tests/settings.e2e.spec.ts b/electron/tests/settings.e2e.spec.ts index 798504c70..765c3cba7 100644 --- a/electron/tests/settings.e2e.spec.ts +++ b/electron/tests/settings.e2e.spec.ts @@ -9,6 +9,7 @@ import { let electronApp: ElectronApplication let page: Page +const TIMEOUT: number = parseInt(process.env.TEST_TIMEOUT || '300000') test.beforeAll(async () => { process.env.CI = 'e2e' @@ -26,7 +27,9 @@ test.beforeAll(async () => { }) await stubDialog(electronApp, 'showMessageBox', { response: 1 }) - page = await electronApp.firstWindow() + page = await electronApp.firstWindow({ + timeout: TIMEOUT, + }) }) test.afterAll(async () => { @@ -35,6 +38,7 @@ test.afterAll(async () => { }) test('shows settings', async () => { - await page.getByTestId('Settings').first().click() - await page.getByTestId('testid-setting-description').isVisible() + await page.getByTestId('Settings').first().click({ timeout: TIMEOUT }) + const settingDescription = page.getByTestId('testid-setting-description') + expect(settingDescription).toBeVisible({ timeout: TIMEOUT }) }) diff --git a/electron/tests/system-monitor.e2e.spec.ts b/electron/tests/system-monitor.e2e.spec.ts deleted file mode 100644 index 747a8ae18..000000000 --- a/electron/tests/system-monitor.e2e.spec.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { _electron as electron } from 'playwright' -import { ElectronApplication, Page, expect, test } from '@playwright/test' - -import { - findLatestBuild, - parseElectronApp, - stubDialog, -} from 'electron-playwright-helpers' - -let electronApp: ElectronApplication -let page: Page - -test.beforeAll(async () => { - process.env.CI = 'e2e' - - const latestBuild = findLatestBuild('dist') - expect(latestBuild).toBeTruthy() - - // parse the packaged Electron app and find paths and other info - const appInfo = parseElectronApp(latestBuild) - expect(appInfo).toBeTruthy() - - electronApp = await electron.launch({ - args: [appInfo.main], // main file from package.json - executablePath: appInfo.executable, // path to the Electron executable - }) - await stubDialog(electronApp, 'showMessageBox', { response: 1 }) - - page = await electronApp.firstWindow() -}) - -test.afterAll(async () => { - await electronApp.close() - await page.close() -}) - -test('shows system monitor', async () => { - await page.getByTestId('System Monitor').first().click() - await page.getByTestId('testid-system-monitor').isVisible() - // More test cases here... -}) diff --git a/web/screens/ExploreModels/index.tsx b/web/screens/ExploreModels/index.tsx index d988fcafc..398b2db08 100644 --- a/web/screens/ExploreModels/index.tsx +++ b/web/screens/ExploreModels/index.tsx @@ -52,9 +52,12 @@ const ExploreModelsScreen = () => { if (loading) return return ( -
+
-
+
{ }, []) return ( -
+
diff --git a/web/screens/SystemMonitor/index.tsx b/web/screens/SystemMonitor/index.tsx index ed3b057a1..3bf8bb35e 100644 --- a/web/screens/SystemMonitor/index.tsx +++ b/web/screens/SystemMonitor/index.tsx @@ -35,7 +35,7 @@ export default function SystemMonitorScreen() { return (
-
+
From 5e58f67abd7acc43f0decfc36dcff0e69ebe08b8 Mon Sep 17 00:00:00 2001 From: Helloyunho Date: Wed, 31 Jan 2024 14:32:49 +0900 Subject: [PATCH 47/71] chore: add react developer tools to electron (#1858) Co-authored-by: NamH --- electron/main.ts | 16 ++++++++++++++++ electron/package.json | 1 + 2 files changed, 17 insertions(+) diff --git a/electron/main.ts b/electron/main.ts index fb7066cd0..5d7e59c0f 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -28,6 +28,22 @@ import { setupCore } from './utils/setup' app .whenReady() + .then(async () => { + if (!app.isPackaged) { + // Which means you're running from source code + const { default: installExtension, REACT_DEVELOPER_TOOLS } = await import( + 'electron-devtools-installer' + ) // Don't use import on top level, since the installer package is dev-only + try { + const name = installExtension(REACT_DEVELOPER_TOOLS) + console.log(`Added Extension: ${name}`) + } catch (err) { + console.log('An error occurred while installing devtools:') + console.error(err) + // Only log the error and don't throw it because it's not critical + } + } + }) .then(setupCore) .then(createUserSpace) .then(migrateExtensions) diff --git a/electron/package.json b/electron/package.json index 173e54f2b..4ee9a19b4 100644 --- a/electron/package.json +++ b/electron/package.json @@ -99,6 +99,7 @@ "@typescript-eslint/parser": "^6.7.3", "electron": "28.0.0", "electron-builder": "^24.9.1", + "electron-devtools-installer": "^3.2.0", "electron-playwright-helpers": "^1.6.0", "eslint-plugin-react": "^7.33.2", "run-script-os": "^1.1.6" From 50fb0bc9078e22a89135b8b34aadc7bed48d1b09 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Tue, 30 Jan 2024 21:19:01 +0700 Subject: [PATCH 48/71] feat: add snackbar component and update style side banner --- web/containers/Providers/EventHandler.tsx | 1 + web/containers/Providers/index.tsx | 2 +- web/containers/Toast/index.tsx | 161 ++++++++++++++++++---- web/hooks/useActiveModel.ts | 1 + web/hooks/useDeleteModel.ts | 1 + web/hooks/useDeleteThread.ts | 1 + web/hooks/useDownloadState.ts | 2 + web/hooks/useSendChatMessage.ts | 2 +- web/screens/Chat/index.tsx | 38 ++--- web/screens/Settings/Advanced/index.tsx | 1 + 10 files changed, 152 insertions(+), 58 deletions(-) diff --git a/web/containers/Providers/EventHandler.tsx b/web/containers/Providers/EventHandler.tsx index 5af6d4917..ac793b4ae 100644 --- a/web/containers/Providers/EventHandler.tsx +++ b/web/containers/Providers/EventHandler.tsx @@ -61,6 +61,7 @@ export default function EventHandler({ children }: { children: ReactNode }) { toaster({ title: 'Success!', description: `Model ${model.id} has been started.`, + type: 'success', }) setStateModel(() => ({ state: 'stop', diff --git a/web/containers/Providers/index.tsx b/web/containers/Providers/index.tsx index f9726e43d..dd9069a95 100644 --- a/web/containers/Providers/index.tsx +++ b/web/containers/Providers/index.tsx @@ -82,7 +82,7 @@ const Providers = (props: PropsWithChildren) => { {!isMac && } - + )} diff --git a/web/containers/Toast/index.tsx b/web/containers/Toast/index.tsx index c5e5f03da..7cffa89b9 100644 --- a/web/containers/Toast/index.tsx +++ b/web/containers/Toast/index.tsx @@ -6,7 +6,99 @@ import { twMerge } from 'tailwind-merge' type Props = { title?: string description?: string - type?: 'default' | 'error' | 'success' + type?: 'default' | 'error' | 'success' | 'warning' +} + +const ErrorIcon = () => { + return ( + + + + ) +} + +const WarningIcon = () => { + return ( + + + + ) +} + +const SuccessIcon = () => { + return ( + + + + ) +} + +const DefaultIcon = () => { + return ( + + + + ) +} + +const renderIcon = (type: string) => { + switch (type) { + case 'warning': + return + + case 'error': + return + + case 'success': + return + + default: + return + } } export function toaster(props: Props) { @@ -16,37 +108,52 @@ export function toaster(props: Props) { return (
-
-

- {title} -

-

- {description} -

+
+
{renderIcon(type)}
+
+

{title}

+

{description}

+
+ toast.dismiss(t.id)} + />
- toast.dismiss(t.id)} - />
) }, - { id: 'toast', duration: 3000 } + { id: 'toast', duration: 2000, position: 'top-right' } + ) +} + +export function snackbar(props: Props) { + const { description, type = 'default' } = props + return toast.custom( + (t) => { + return ( +
+
+
{renderIcon(type)}
+

{description}

+ toast.dismiss(t.id)} + /> +
+
+ ) + }, + { id: 'snackbar', duration: 2000, position: 'bottom-center' } ) } diff --git a/web/hooks/useActiveModel.ts b/web/hooks/useActiveModel.ts index 336f0be21..a456d8787 100644 --- a/web/hooks/useActiveModel.ts +++ b/web/hooks/useActiveModel.ts @@ -42,6 +42,7 @@ export function useActiveModel() { toaster({ title: `Model ${modelId} not found!`, description: `Please download the model first.`, + type: 'warning', }) setStateModel(() => ({ state: 'start', diff --git a/web/hooks/useDeleteModel.ts b/web/hooks/useDeleteModel.ts index cd7292997..fa0cfb45e 100644 --- a/web/hooks/useDeleteModel.ts +++ b/web/hooks/useDeleteModel.ts @@ -19,6 +19,7 @@ export default function useDeleteModel() { toaster({ title: 'Model Deletion Successful', description: `The model ${model.id} has been successfully deleted.`, + type: 'success', }) } diff --git a/web/hooks/useDeleteThread.ts b/web/hooks/useDeleteThread.ts index 00ba98b99..88710f777 100644 --- a/web/hooks/useDeleteThread.ts +++ b/web/hooks/useDeleteThread.ts @@ -86,6 +86,7 @@ export default function useDeleteThread() { toaster({ title: 'Thread successfully deleted.', description: `Thread ${threadId} has been successfully deleted.`, + type: 'success', }) } diff --git a/web/hooks/useDownloadState.ts b/web/hooks/useDownloadState.ts index d39ab5e58..37f41d2a1 100644 --- a/web/hooks/useDownloadState.ts +++ b/web/hooks/useDownloadState.ts @@ -26,6 +26,7 @@ const setDownloadStateSuccessAtom = atom(null, (get, set, modelId: string) => { toaster({ title: 'Download Completed', description: `Download ${modelId} completed`, + type: 'success', }) }) @@ -61,6 +62,7 @@ const setDownloadStateCancelledAtom = atom( toaster({ title: 'Cancel Download', description: `Model ${modelId} cancel download`, + type: 'warning', }) return diff --git a/web/hooks/useSendChatMessage.ts b/web/hooks/useSendChatMessage.ts index 71dc99d21..835bdfed4 100644 --- a/web/hooks/useSendChatMessage.ts +++ b/web/hooks/useSendChatMessage.ts @@ -160,7 +160,7 @@ export default function useSendChatMessage() { activeThread.assistants[0].model.id !== selectedModel?.id ) { if (!selectedModel) { - toaster({ title: 'Please select a model' }) + toaster({ title: 'Please select a model', type: 'warning' }) return } const assistantId = activeThread.assistants[0].assistant_id ?? '' diff --git a/web/screens/Chat/index.tsx b/web/screens/Chat/index.tsx index cfd47ad39..887d9d035 100644 --- a/web/screens/Chat/index.tsx +++ b/web/screens/Chat/index.tsx @@ -5,7 +5,7 @@ import { useDropzone } from 'react-dropzone' import { useAtomValue, useSetAtom } from 'jotai' -import { UploadCloudIcon, XIcon } from 'lucide-react' +import { UploadCloudIcon } from 'lucide-react' import { twMerge } from 'tailwind-merge' @@ -15,6 +15,8 @@ import ModelStart from '@/containers/Loader/ModelStart' import { currentPromptAtom, fileUploadAtom } from '@/containers/Providers/Jotai' import { showLeftSideBarAtom } from '@/containers/Providers/KeyListener' +import { snackbar } from '@/containers/Toast' + import { queuedMessageAtom, reloadModelAtom } from '@/hooks/useSendChatMessage' import ChatBody from '@/screens/Chat/ChatBody' @@ -112,8 +114,13 @@ const ChatScreen: React.FC = () => { }, }) - // TODO @faisal change this until we have sneakbar component useEffect(() => { + if (dragRejected.code) { + snackbar({ + description: renderError(dragRejected.code), + type: 'error', + }) + } setTimeout(() => { if (dragRejected.code) { setDragRejected({ code: '' }) @@ -134,33 +141,6 @@ const ChatScreen: React.FC = () => { className="relative flex h-full w-full flex-col overflow-auto bg-background outline-none" {...getRootProps()} > - {dragRejected.code !== '' && ( -
-
- - - -

{renderError(dragRejected.code)}

- setDragRejected({ code: '' })} - /> -
-
- )} - {dragOver && (
{ toaster({ title: 'Logs cleared', description: 'All logs have been cleared.', + type: 'success', }) } From 8151ef031343e671b525b0b2f4f93544719ab8b5 Mon Sep 17 00:00:00 2001 From: NamH Date: Wed, 31 Jan 2024 13:23:48 +0700 Subject: [PATCH 49/71] feat: add factory reset feature (#1750) * feat(FactoryReset): add factory reset feature Signed-off-by: nam Signed-off-by: James Co-authored-by: Faisal Amir Co-authored-by: James --- core/src/api/index.ts | 2 +- core/src/core.ts | 7 ++ core/src/node/api/routes/fileManager.ts | 2 + electron/handlers/fileManager.ts | 6 +- electron/package.json | 2 +- .../inference-nitro-extension/package.json | 2 +- uikit/package.json | 1 + uikit/src/button/styles.scss | 8 +- uikit/src/checkbox/index.tsx | 29 ++++++ uikit/src/checkbox/styles.scss | 7 ++ uikit/src/index.ts | 1 + uikit/src/main.scss | 1 + web/hooks/useFactoryReset.ts | 59 +++++++++++ web/hooks/useSettings.ts | 7 +- web/screens/LocalServer/index.tsx | 6 +- .../FactoryReset/ModalConfirmReset.tsx | 99 +++++++++++++++++++ .../Settings/Advanced/FactoryReset/index.tsx | 37 +++++++ web/screens/Settings/Advanced/index.tsx | 39 +++----- 18 files changed, 277 insertions(+), 38 deletions(-) create mode 100644 uikit/src/checkbox/index.tsx create mode 100644 uikit/src/checkbox/styles.scss create mode 100644 web/hooks/useFactoryReset.ts create mode 100644 web/screens/Settings/Advanced/FactoryReset/ModalConfirmReset.tsx create mode 100644 web/screens/Settings/Advanced/FactoryReset/index.tsx diff --git a/core/src/api/index.ts b/core/src/api/index.ts index 0adc8b7e2..0d7cc51f7 100644 --- a/core/src/api/index.ts +++ b/core/src/api/index.ts @@ -3,7 +3,6 @@ * @description Enum of all the routes exposed by the app */ export enum AppRoute { - appDataPath = 'appDataPath', openExternalUrl = 'openExternalUrl', openAppDirectory = 'openAppDirectory', openFileExplore = 'openFileExplorer', @@ -62,6 +61,7 @@ export enum FileManagerRoute { syncFile = 'syncFile', getJanDataFolderPath = 'getJanDataFolderPath', getResourcePath = 'getResourcePath', + getUserHomePath = 'getUserHomePath', fileStat = 'fileStat', writeBlob = 'writeBlob', } diff --git a/core/src/core.ts b/core/src/core.ts index 24053e55c..8831c6001 100644 --- a/core/src/core.ts +++ b/core/src/core.ts @@ -83,6 +83,12 @@ const openExternalUrl: (url: string) => Promise = (url) => */ const getResourcePath: () => Promise = () => global.core.api?.getResourcePath() +/** + * Gets the user's home path. + * @returns return user's home path + */ +const getUserHomePath = (): Promise => global.core.api?.getUserHomePath() + /** * Log to file from browser processes. * @@ -127,5 +133,6 @@ export { baseName, log, isSubdirectory, + getUserHomePath, FileStat, } diff --git a/core/src/node/api/routes/fileManager.ts b/core/src/node/api/routes/fileManager.ts index 159c23a0c..66056444e 100644 --- a/core/src/node/api/routes/fileManager.ts +++ b/core/src/node/api/routes/fileManager.ts @@ -8,5 +8,7 @@ export const fsRouter = async (app: HttpServer) => { app.post(`/app/${FileManagerRoute.getResourcePath}`, async (request: any, reply: any) => {}) + app.post(`/app/${FileManagerRoute.getUserHomePath}`, async (request: any, reply: any) => {}) + app.post(`/app/${FileManagerRoute.fileStat}`, async (request: any, reply: any) => {}) } diff --git a/electron/handlers/fileManager.ts b/electron/handlers/fileManager.ts index 2528aef71..e328cb53b 100644 --- a/electron/handlers/fileManager.ts +++ b/electron/handlers/fileManager.ts @@ -1,4 +1,4 @@ -import { ipcMain } from 'electron' +import { ipcMain, app } from 'electron' // @ts-ignore import reflect from '@alumna/reflect' @@ -38,6 +38,10 @@ export function handleFileMangerIPCs() { getResourcePath() ) + ipcMain.handle(FileManagerRoute.getUserHomePath, async (_event) => + app.getPath('home') + ) + // handle fs is directory here ipcMain.handle( FileManagerRoute.fileStat, diff --git a/electron/package.json b/electron/package.json index 4ee9a19b4..2892fedc6 100644 --- a/electron/package.json +++ b/electron/package.json @@ -86,7 +86,7 @@ "request": "^2.88.2", "request-progress": "^3.0.0", "rimraf": "^5.0.5", - "typescript": "^5.3.3", + "typescript": "^5.2.2", "ulid": "^2.3.0", "use-debounce": "^9.0.4" }, diff --git a/extensions/inference-nitro-extension/package.json b/extensions/inference-nitro-extension/package.json index 44727eb70..8ad516ad9 100644 --- a/extensions/inference-nitro-extension/package.json +++ b/extensions/inference-nitro-extension/package.json @@ -35,7 +35,7 @@ "rollup-plugin-sourcemaps": "^0.6.3", "rollup-plugin-typescript2": "^0.36.0", "run-script-os": "^1.1.6", - "typescript": "^5.3.3" + "typescript": "^5.2.2" }, "dependencies": { "@janhq/core": "file:../../core", diff --git a/uikit/package.json b/uikit/package.json index 43e73dcf2..66f05840b 100644 --- a/uikit/package.json +++ b/uikit/package.json @@ -18,6 +18,7 @@ }, "dependencies": { "@radix-ui/react-avatar": "^1.0.4", + "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-context": "^1.0.1", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-icons": "^1.3.0", diff --git a/uikit/src/button/styles.scss b/uikit/src/button/styles.scss index 74585ed1e..003df5b4d 100644 --- a/uikit/src/button/styles.scss +++ b/uikit/src/button/styles.scss @@ -9,7 +9,7 @@ } &-secondary-blue { - @apply bg-blue-200 text-blue-600 hover:bg-blue-500/50; + @apply bg-blue-200 text-blue-600 hover:bg-blue-300/50 dark:hover:bg-blue-200/80; } &-danger { @@ -17,7 +17,7 @@ } &-secondary-danger { - @apply bg-red-200 text-red-600 hover:bg-red-500/50; + @apply bg-red-200 text-red-600 hover:bg-red-300/50 dark:hover:bg-red-200/80; } &-outline { @@ -67,14 +67,18 @@ [type='submit'] { &.btn-primary { @apply bg-primary hover:bg-primary/90; + @apply disabled:pointer-events-none disabled:bg-zinc-100 disabled:text-zinc-400; } &.btn-secondary { @apply bg-secondary hover:bg-secondary/80; + @apply disabled:pointer-events-none disabled:bg-zinc-100 disabled:text-zinc-400; } &.btn-secondary-blue { @apply bg-blue-200 text-blue-900 hover:bg-blue-200/80; + @apply disabled:pointer-events-none disabled:bg-zinc-100 disabled:text-zinc-400; } &.btn-danger { @apply bg-danger hover:bg-danger/90; + @apply disabled:pointer-events-none disabled:bg-zinc-100 disabled:text-zinc-400; } } diff --git a/uikit/src/checkbox/index.tsx b/uikit/src/checkbox/index.tsx new file mode 100644 index 000000000..1e78aeafb --- /dev/null +++ b/uikit/src/checkbox/index.tsx @@ -0,0 +1,29 @@ +'use client' + +import * as React from 'react' +import * as CheckboxPrimitive from '@radix-ui/react-checkbox' +import { CheckIcon } from '@radix-ui/react-icons' + +import { twMerge } from 'tailwind-merge' + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)) +Checkbox.displayName = CheckboxPrimitive.Root.displayName + +export { Checkbox } diff --git a/uikit/src/checkbox/styles.scss b/uikit/src/checkbox/styles.scss new file mode 100644 index 000000000..33610f837 --- /dev/null +++ b/uikit/src/checkbox/styles.scss @@ -0,0 +1,7 @@ +.checkbox { + @apply border-border data-[state=checked]:bg-primary h-5 w-5 flex-shrink-0 rounded-md border data-[state=checked]:text-white; + + &--icon { + @apply h-4 w-4; + } +} diff --git a/uikit/src/index.ts b/uikit/src/index.ts index 3d5eaa82a..1b0a26bd1 100644 --- a/uikit/src/index.ts +++ b/uikit/src/index.ts @@ -12,3 +12,4 @@ export * from './command' export * from './textarea' export * from './select' export * from './slider' +export * from './checkbox' diff --git a/uikit/src/main.scss b/uikit/src/main.scss index 546f22811..c1326ba19 100644 --- a/uikit/src/main.scss +++ b/uikit/src/main.scss @@ -16,6 +16,7 @@ @import './textarea/styles.scss'; @import './select/styles.scss'; @import './slider/styles.scss'; +@import './checkbox/styles.scss'; .animate-spin { animation: spin 1s linear infinite; diff --git a/web/hooks/useFactoryReset.ts b/web/hooks/useFactoryReset.ts new file mode 100644 index 000000000..56994d4c4 --- /dev/null +++ b/web/hooks/useFactoryReset.ts @@ -0,0 +1,59 @@ +import { useEffect, useState } from 'react' + +import { fs, AppConfiguration, joinPath, getUserHomePath } from '@janhq/core' + +export default function useFactoryReset() { + const [defaultJanDataFolder, setdefaultJanDataFolder] = useState('') + + useEffect(() => { + async function getDefaultJanDataFolder() { + const homePath = await getUserHomePath() + const defaultJanDataFolder = await joinPath([homePath, 'jan']) + setdefaultJanDataFolder(defaultJanDataFolder) + } + getDefaultJanDataFolder() + }, []) + + const resetAll = async (keepCurrentFolder?: boolean) => { + // read the place of jan data folder + const appConfiguration: AppConfiguration | undefined = + await window.core?.api?.getAppConfigurations() + + if (!appConfiguration) { + console.debug('Failed to get app configuration') + } + + console.debug('appConfiguration: ', appConfiguration) + const janDataFolderPath = appConfiguration!.data_folder + + if (defaultJanDataFolder === janDataFolderPath) { + console.debug('Jan data folder is already at user home') + } else { + // if jan data folder is not at user home, we update the app configuration to point to user home + if (!keepCurrentFolder) { + const configuration: AppConfiguration = { + data_folder: defaultJanDataFolder, + } + await window.core?.api?.updateAppConfiguration(configuration) + } + } + + const modelPath = await joinPath([janDataFolderPath, 'models']) + const threadPath = await joinPath([janDataFolderPath, 'threads']) + + console.debug(`Removing models at ${modelPath}`) + await fs.rmdirSync(modelPath, { recursive: true }) + + console.debug(`Removing threads at ${threadPath}`) + await fs.rmdirSync(threadPath, { recursive: true }) + + // reset the localStorage + localStorage.clear() + await window.core?.api?.relaunch() + } + + return { + defaultJanDataFolder, + resetAll, + } +} diff --git a/web/hooks/useSettings.ts b/web/hooks/useSettings.ts index ef4e08480..168e72489 100644 --- a/web/hooks/useSettings.ts +++ b/web/hooks/useSettings.ts @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' import { fs, joinPath } from '@janhq/core' import { atom, useAtom } from 'jotai' @@ -32,7 +32,7 @@ export const useSettings = () => { }) } - const readSettings = async () => { + const readSettings = useCallback(async () => { if (!window?.core?.api) { return } @@ -42,7 +42,8 @@ export const useSettings = () => { return typeof settings === 'object' ? settings : JSON.parse(settings) } return {} - } + }, []) + const saveSettings = async ({ runMode, notify, diff --git a/web/screens/LocalServer/index.tsx b/web/screens/LocalServer/index.tsx index d75274f16..7e1ba1fab 100644 --- a/web/screens/LocalServer/index.tsx +++ b/web/screens/LocalServer/index.tsx @@ -91,11 +91,7 @@ const LocalServerScreen = () => { } useEffect(() => { - if ( - localStorage.getItem(FIRST_TIME_VISIT_API_SERVER) === null || - localStorage.getItem(FIRST_TIME_VISIT_API_SERVER) === 'true' - ) { - localStorage.setItem(FIRST_TIME_VISIT_API_SERVER, 'true') + if (localStorage.getItem(FIRST_TIME_VISIT_API_SERVER) == null) { setFirstTimeVisitAPIServer(true) } }, [firstTimeVisitAPIServer]) diff --git a/web/screens/Settings/Advanced/FactoryReset/ModalConfirmReset.tsx b/web/screens/Settings/Advanced/FactoryReset/ModalConfirmReset.tsx new file mode 100644 index 000000000..d8a2321a9 --- /dev/null +++ b/web/screens/Settings/Advanced/FactoryReset/ModalConfirmReset.tsx @@ -0,0 +1,99 @@ +import React, { useCallback, useEffect, useState } from 'react' + +import { fs, AppConfiguration, joinPath, getUserHomePath } from '@janhq/core' + +import { + Modal, + ModalPortal, + ModalContent, + ModalHeader, + ModalTitle, + ModalFooter, + ModalClose, + Button, + Checkbox, + Input, +} from '@janhq/uikit' +import { atom, useAtom } from 'jotai' + +import useFactoryReset from '@/hooks/useFactoryReset' + +export const modalValidationAtom = atom(false) + +const ModalConfirmReset = () => { + const [modalValidation, setModalValidation] = useAtom(modalValidationAtom) + const { resetAll, defaultJanDataFolder } = useFactoryReset() + const [inputValue, setInputValue] = useState('') + const [currentDirectoryChecked, setCurrentDirectoryChecked] = useState(true) + const onFactoryResetClick = useCallback( + () => resetAll(currentDirectoryChecked), + [currentDirectoryChecked, resetAll] + ) + + return ( + setModalValidation(false)} + > + + + + + Are you sure you want to reset to default settings? + + +

+ It will reset the application to its original state, deleting all your + usage data, including model customizations and conversation history. + This action is irreversible. +

+
+

{`To confirm, please enter the word "RESET" below:`}

+ setInputValue(e.target.value)} + /> +
+
+ setCurrentDirectoryChecked(Boolean(e))} + /> +
+ +

+ Otherwise it will reset back to its original location at: + {/* TODO should be from system */} + {defaultJanDataFolder} +

+
+
+ +
+ setModalValidation(false)}> + + + + + +
+
+
+
+ ) +} + +export default ModalConfirmReset diff --git a/web/screens/Settings/Advanced/FactoryReset/index.tsx b/web/screens/Settings/Advanced/FactoryReset/index.tsx new file mode 100644 index 000000000..e7b1e2995 --- /dev/null +++ b/web/screens/Settings/Advanced/FactoryReset/index.tsx @@ -0,0 +1,37 @@ +import { Button } from '@janhq/uikit' + +import { useSetAtom } from 'jotai' + +import ModalValidation, { modalValidationAtom } from './ModalConfirmReset' + +const FactoryReset = () => { + const setModalValidation = useSetAtom(modalValidationAtom) + + return ( +
+
+
+
+ Reset to Factory Default +
+
+

+ Reset the application to its original state, deleting all your usage + data, including model customizations and conversation history. This + action is irreversible and recommended only if the application is in a + corrupted state. +

+
+ + +
+ ) +} + +export default FactoryReset diff --git a/web/screens/Settings/Advanced/index.tsx b/web/screens/Settings/Advanced/index.tsx index 5c85a0e1e..df92afdd4 100644 --- a/web/screens/Settings/Advanced/index.tsx +++ b/web/screens/Settings/Advanced/index.tsx @@ -1,4 +1,3 @@ -/* eslint-disable react-hooks/exhaustive-deps */ 'use client' import { @@ -21,6 +20,7 @@ import { FeatureToggleContext } from '@/context/FeatureToggle' import { useSettings } from '@/hooks/useSettings' import DataFolder from './DataFolder' +import FactoryReset from './FactoryReset' const Advanced = () => { const { @@ -36,6 +36,7 @@ const Advanced = () => { const { readSettings, saveSettings, validateSettings, setShowNotification } = useSettings() + const onProxyChange = useCallback( (event: ChangeEvent) => { const value = event.target.value || '' @@ -50,10 +51,12 @@ const Advanced = () => { ) useEffect(() => { - readSettings().then((settings) => { + const setUseGpuIfPossible = async () => { + const settings = await readSettings() setGpuEnabled(settings.run_mode === 'gpu') - }) - }, []) + } + setUseGpuIfPossible() + }, [readSettings]) const clearLogs = async () => { if (await fs.existsSync(`file://logs`)) { @@ -96,13 +99,7 @@ const Advanced = () => {
{ - if (e === true) { - setExperimentalFeature(true) - } else { - setExperimentalFeature(false) - } - }} + onCheckedChange={setExperimentalFeature} />
@@ -119,7 +116,7 @@ const Advanced = () => {
{ + onCheckedChange={(e) => { if (e === true) { saveSettings({ runMode: 'gpu' }) setGpuEnabled(true) @@ -137,7 +134,7 @@ const Advanced = () => { )} {/* Directory */} - + {experimentalFeature && } {/* Proxy */}
@@ -170,16 +167,7 @@ const Advanced = () => { certain proxies.

- { - if (e === true) { - setIgnoreSSL(true) - } else { - setIgnoreSSL(false) - } - }} - /> + setIgnoreSSL(e)} />
{/* Open app directory */} @@ -206,7 +194,7 @@ const Advanced = () => {
)} - {/* Claer log */} + {/* Clear log */}
@@ -218,6 +206,9 @@ const Advanced = () => { Clear
+ + {/* Factory Reset */} +
) } From 509f6cba3947fb28593b3fdfd1cf1a1f3ea5937c Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Wed, 31 Jan 2024 17:32:37 +0700 Subject: [PATCH 50/71] feat: move open app directory into icon folder (#1879) --- .../Settings/Advanced/DataFolder/index.tsx | 3 ++- web/screens/Settings/Advanced/index.tsx | 24 ------------------- 2 files changed, 2 insertions(+), 25 deletions(-) diff --git a/web/screens/Settings/Advanced/DataFolder/index.tsx b/web/screens/Settings/Advanced/DataFolder/index.tsx index 5abd5390b..8fa6af505 100644 --- a/web/screens/Settings/Advanced/DataFolder/index.tsx +++ b/web/screens/Settings/Advanced/DataFolder/index.tsx @@ -104,7 +104,8 @@ const DataFolder = () => { /> window.core?.api?.openAppDirectory()} />
- {/* Open app directory */} - {window.electronAPI && ( -
-
-
-
- Open App Directory -
-
-

- Open the directory where your app data, like conversation history - and model configurations, is located. -

-
- -
- )} - {/* Clear log */}
From 8150f2f7b74316a66cdfb1f3d22bca1d0c6d09be Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Wed, 31 Jan 2024 18:23:16 +0700 Subject: [PATCH 51/71] fix: add dialog confirm when move folder and next dest isn't empty --- .../DataFolder/ModalConfirmDestNotEmpty.tsx | 59 +++++++++++++++++++ .../Settings/Advanced/DataFolder/index.tsx | 22 ++++++- 2 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 web/screens/Settings/Advanced/DataFolder/ModalConfirmDestNotEmpty.tsx diff --git a/web/screens/Settings/Advanced/DataFolder/ModalConfirmDestNotEmpty.tsx b/web/screens/Settings/Advanced/DataFolder/ModalConfirmDestNotEmpty.tsx new file mode 100644 index 000000000..e4aba41cc --- /dev/null +++ b/web/screens/Settings/Advanced/DataFolder/ModalConfirmDestNotEmpty.tsx @@ -0,0 +1,59 @@ +import React from 'react' + +import { + Modal, + ModalPortal, + ModalContent, + ModalHeader, + ModalTitle, + ModalFooter, + ModalClose, + Button, +} from '@janhq/uikit' + +import { atom, useAtom } from 'jotai' + +export const showDestNotEmptyConfirmAtom = atom(false) + +type Props = { + onUserConfirmed: () => void +} + +const ModalChangeDestNotEmpty: React.FC = ({ onUserConfirmed }) => { + const [show, setShow] = useAtom(showDestNotEmptyConfirmAtom) + + return ( + + + + + + + This folder is not empty. Are you sure you want to relocate Jan + Data Folder here? + + + +

+ You may accidentally delete your other personal data when uninstalling + the app in the future. Are you sure you want to proceed with this + folder? Please review your selection carefully. +

+ +
+ setShow(false)}> + + + + + +
+
+
+
+ ) +} + +export default ModalChangeDestNotEmpty diff --git a/web/screens/Settings/Advanced/DataFolder/index.tsx b/web/screens/Settings/Advanced/DataFolder/index.tsx index 8fa6af505..fe590bfaa 100644 --- a/web/screens/Settings/Advanced/DataFolder/index.tsx +++ b/web/screens/Settings/Advanced/DataFolder/index.tsx @@ -12,6 +12,9 @@ export const SUCCESS_SET_NEW_DESTINATION = 'successSetNewDestination' import ModalChangeDirectory, { showDirectoryConfirmModalAtom, } from './ModalChangeDirectory' +import ModalChangeDestNotEmpty, { + showDestNotEmptyConfirmAtom, +} from './ModalConfirmDestNotEmpty' import ModalErrorSetDestGlobal, { showChangeFolderErrorAtom, } from './ModalErrorSetDestGlobal' @@ -24,6 +27,7 @@ const DataFolder = () => { const setShowDirectoryConfirm = useSetAtom(showDirectoryConfirmModalAtom) const setShowSameDirectory = useSetAtom(showSamePathModalAtom) const setShowChangeFolderError = useSetAtom(showChangeFolderErrorAtom) + const showDestNotEmptyConfirm = useSetAtom(showDestNotEmptyConfirmAtom) const [destinationPath, setDestinationPath] = useState(undefined) useEffect(() => { @@ -52,9 +56,24 @@ const DataFolder = () => { return } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const newDestChildren: any[] = await fs.readdirSync(destFolder) + const isNotEmpty = + newDestChildren.filter((x) => x !== '.DS_Store').length > 0 + + if (isNotEmpty) { + showDestNotEmptyConfirm(true) + return + } + setDestinationPath(destFolder) setShowDirectoryConfirm(true) - }, [janDataFolderPath, setShowSameDirectory, setShowDirectoryConfirm]) + }, [ + janDataFolderPath, + setShowDirectoryConfirm, + setShowSameDirectory, + showDestNotEmptyConfirm, + ]) const onUserConfirmed = useCallback(async () => { if (!destinationPath) return @@ -124,6 +143,7 @@ const DataFolder = () => { onUserConfirmed={onUserConfirmed} /> + {showLoader && } ) From ad842dbc70756865ddf3e1c50faa5d4e4b80af78 Mon Sep 17 00:00:00 2001 From: Louis Date: Wed, 31 Jan 2024 23:53:30 +0700 Subject: [PATCH 52/71] chore: mark RAG as experimental feature --- web/screens/Chat/ChatInput/index.tsx | 89 +++++++++--------- web/screens/Chat/Sidebar/index.tsx | 136 ++++++++++++++------------- web/screens/Chat/index.tsx | 10 +- 3 files changed, 127 insertions(+), 108 deletions(-) diff --git a/web/screens/Chat/ChatInput/index.tsx b/web/screens/Chat/ChatInput/index.tsx index b760ab44c..ee1ac9a41 100644 --- a/web/screens/Chat/ChatInput/index.tsx +++ b/web/screens/Chat/ChatInput/index.tsx @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { useEffect, useRef, useState } from 'react' +import { useContext, useEffect, useRef, useState } from 'react' import { InferenceEvent, MessageStatus, events } from '@janhq/core' @@ -24,6 +24,8 @@ import { twMerge } from 'tailwind-merge' import { currentPromptAtom, fileUploadAtom } from '@/containers/Providers/Jotai' +import { FeatureToggleContext } from '@/context/FeatureToggle' + import { useActiveModel } from '@/hooks/useActiveModel' import { useClickOutside } from '@/hooks/useClickOutside' @@ -53,7 +55,8 @@ const ChatInput: React.FC = () => { const textareaRef = useRef(null) const fileInputRef = useRef(null) const imageInputRef = useRef(null) - const [ShowAttacmentMenus, setShowAttacmentMenus] = useState(false) + const [showAttacmentMenus, setShowAttacmentMenus] = useState(false) + const { experimentalFeature } = useContext(FeatureToggleContext) const onPromptChange = (e: React.ChangeEvent) => { setCurrentPrompt(e.target.value) @@ -147,50 +150,52 @@ const ChatInput: React.FC = () => { value={currentPrompt} onChange={onPromptChange} /> - - - - { - if ( - fileUpload.length > 0 || - (activeThread?.assistants[0].tools && - !activeThread?.assistants[0].tools[0]?.enabled) - ) { - e.stopPropagation() - } else { - setShowAttacmentMenus(!ShowAttacmentMenus) - } - }} - /> - - - {fileUpload.length > 0 || - (activeThread?.assistants[0].tools && - !activeThread?.assistants[0].tools[0]?.enabled && ( - - {fileUpload.length !== 0 && ( - - Currently, we only support 1 attachment at the same time - - )} - {activeThread?.assistants[0].tools && - activeThread?.assistants[0].tools[0]?.enabled === - false && ( + {experimentalFeature && ( + + + { + if ( + fileUpload.length > 0 || + (activeThread?.assistants[0].tools && + !activeThread?.assistants[0].tools[0]?.enabled) + ) { + e.stopPropagation() + } else { + setShowAttacmentMenus(!showAttacmentMenus) + } + }} + /> + + + {fileUpload.length > 0 || + (activeThread?.assistants[0].tools && + !activeThread?.assistants[0].tools[0]?.enabled && ( + + {fileUpload.length !== 0 && ( - Turn on Retrieval in Assistant Settings to use this - feature + Currently, we only support 1 attachment at the same + time )} - - - ))} - - + {activeThread?.assistants[0].tools && + activeThread?.assistants[0].tools[0]?.enabled === + false && ( + + Turn on Retrieval in Assistant Settings to use this + feature + + )} + + + ))} + + + )} - {ShowAttacmentMenus && ( + {showAttacmentMenus && (
{ const activeModelParams = useAtomValue(getActiveThreadModelParamsAtom) const selectedModel = useAtomValue(selectedModelAtom) const { updateThreadMetadata } = useCreateNewThread() + const { experimentalFeature } = useContext(FeatureToggleContext) const modelEngineParams = toSettingParams(activeModelParams) const modelRuntimeParams = toRuntimeParams(activeModelParams) @@ -131,71 +134,74 @@ const Sidebar: React.FC = () => { }} />
- -
- {activeThread?.assistants[0]?.tools && - componentDataAssistantSetting.length > 0 && ( -
- { - if (activeThread) - updateThreadMetadata({ - ...activeThread, - assistants: [ - { - ...activeThread.assistants[0], - tools: [ - { - type: 'retrieval', - enabled: e, - settings: - (activeThread.assistants[0].tools && - activeThread.assistants[0].tools[0] - ?.settings) ?? - {}, - }, - ], - }, - ], - }) - }} - /> - } - > - {activeThread?.assistants[0]?.tools[0].enabled && ( -
-
- -
- -
-
- + {activeThread?.assistants[0]?.tools && + componentDataAssistantSetting.length > 0 && ( +
+ { + if (activeThread) + updateThreadMetadata({ + ...activeThread, + assistants: [ + { + ...activeThread.assistants[0], + tools: [ + { + type: 'retrieval', + enabled: e, + settings: + (activeThread.assistants[0].tools && + activeThread.assistants[0] + .tools[0]?.settings) ?? + {}, + }, + ], + }, + ], + }) + }} /> -
- )} - -
- )} -
+ } + > + {activeThread?.assistants[0]?.tools[0].enabled && ( +
+
+ +
+ +
+
+ +
+ )} + +
+ )} +
+ )}
diff --git a/web/screens/Chat/index.tsx b/web/screens/Chat/index.tsx index 887d9d035..e7cb82740 100644 --- a/web/screens/Chat/index.tsx +++ b/web/screens/Chat/index.tsx @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import React, { useEffect, useState } from 'react' +import React, { useContext, useEffect, useState } from 'react' import { useDropzone } from 'react-dropzone' @@ -17,6 +17,8 @@ import { showLeftSideBarAtom } from '@/containers/Providers/KeyListener' import { snackbar } from '@/containers/Toast' +import { FeatureToggleContext } from '@/context/FeatureToggle' + import { queuedMessageAtom, reloadModelAtom } from '@/hooks/useSendChatMessage' import ChatBody from '@/screens/Chat/ChatBody' @@ -59,6 +61,8 @@ const ChatScreen: React.FC = () => { const reloadModel = useAtomValue(reloadModelAtom) const [dragRejected, setDragRejected] = useState({ code: '' }) const setFileUpload = useSetAtom(fileUploadAtom) + const { experimentalFeature } = useContext(FeatureToggleContext) + const { getRootProps, isDragReject } = useDropzone({ noClick: true, multiple: false, @@ -67,6 +71,8 @@ const ChatScreen: React.FC = () => { }, onDragOver: (e) => { + // Retrieval file drag and drop is experimental feature + if (!experimentalFeature) return if ( e.dataTransfer.items.length === 1 && activeThread?.assistants[0].tools && @@ -84,6 +90,8 @@ const ChatScreen: React.FC = () => { }, onDragLeave: () => setDragOver(false), onDrop: (files, rejectFiles) => { + // Retrieval file drag and drop is experimental feature + if (!experimentalFeature) return if ( !files || files.length !== 1 || From 4b8b13b5d3f7b038f0bccf7a4c0a3fe0c68d9507 Mon Sep 17 00:00:00 2001 From: Service Account Date: Wed, 31 Jan 2024 20:23:20 +0000 Subject: [PATCH 53/71] janhq/jan: Update README.md with nightly build artifact URL --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index e30f310ea..e4817f8dc 100644 --- a/README.md +++ b/README.md @@ -76,31 +76,31 @@ Jan is an open-source ChatGPT alternative that runs 100% offline on your compute Experimental (Nightly Build) - + jan.exe - + Intel - + M1/M2 - + jan.deb - + jan.AppImage From 4116aaa98a390b35fb6cde7d02de76c52e2f04b0 Mon Sep 17 00:00:00 2001 From: NamH Date: Thu, 1 Feb 2024 11:25:34 +0700 Subject: [PATCH 54/71] feat: add start/stop model via http api (#1862) Signed-off-by: nam --- core/src/node/api/common/builder.ts | 17 +- core/src/node/api/common/consts.ts | 19 + core/src/node/api/common/startStopModel.ts | 351 ++++++++++++++++++ core/src/node/api/routes/common.ts | 22 +- core/src/node/utils/index.ts | 148 ++++++-- core/src/types/index.ts | 1 + core/src/types/miscellaneous/index.ts | 2 + .../src/types/miscellaneous/promptTemplate.ts | 6 + .../types/miscellaneous/systemResourceInfo.ts | 4 + core/src/types/model/modelEntity.ts | 1 + .../src/@types/global.d.ts | 21 -- .../inference-nitro-extension/src/index.ts | 3 +- .../src/node/index.ts | 64 ++-- .../src/node/utils.ts | 56 --- server/package.json | 3 + web/hooks/useCreateNewThread.ts | 5 +- web/hooks/useSetActiveThread.ts | 2 - web/screens/LocalServer/index.tsx | 24 +- .../FactoryReset/ModalConfirmReset.tsx | 4 +- 19 files changed, 559 insertions(+), 194 deletions(-) create mode 100644 core/src/node/api/common/consts.ts create mode 100644 core/src/node/api/common/startStopModel.ts create mode 100644 core/src/types/miscellaneous/index.ts create mode 100644 core/src/types/miscellaneous/promptTemplate.ts create mode 100644 core/src/types/miscellaneous/systemResourceInfo.ts delete mode 100644 extensions/inference-nitro-extension/src/node/utils.ts diff --git a/core/src/node/api/common/builder.ts b/core/src/node/api/common/builder.ts index a9819bce6..5c99cf4d8 100644 --- a/core/src/node/api/common/builder.ts +++ b/core/src/node/api/common/builder.ts @@ -2,7 +2,8 @@ import fs from 'fs' import { JanApiRouteConfiguration, RouteConfiguration } from './configuration' import { join } from 'path' import { ContentType, MessageStatus, Model, ThreadMessage } from './../../../index' -import { getJanDataFolderPath } from '../../utils' +import { getEngineConfiguration, getJanDataFolderPath } from '../../utils' +import { DEFAULT_CHAT_COMPLETION_URL } from './consts' export const getBuilder = async (configuration: RouteConfiguration) => { const directoryPath = join(getJanDataFolderPath(), configuration.dirName) @@ -309,7 +310,7 @@ export const chatCompletions = async (request: any, reply: any) => { const engineConfiguration = await getEngineConfiguration(requestedModel.engine) let apiKey: string | undefined = undefined - let apiUrl: string = 'http://127.0.0.1:3928/inferences/llamacpp/chat_completion' // default nitro url + let apiUrl: string = DEFAULT_CHAT_COMPLETION_URL if (engineConfiguration) { apiKey = engineConfiguration.api_key @@ -320,7 +321,7 @@ export const chatCompletions = async (request: any, reply: any) => { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', - "Access-Control-Allow-Origin": "*" + 'Access-Control-Allow-Origin': '*', }) const headers: Record = { @@ -346,13 +347,3 @@ export const chatCompletions = async (request: any, reply: any) => { response.body.pipe(reply.raw) } } - -const getEngineConfiguration = async (engineId: string) => { - if (engineId !== 'openai') { - return undefined - } - const directoryPath = join(getJanDataFolderPath(), 'engines') - const filePath = join(directoryPath, `${engineId}.json`) - const data = await fs.readFileSync(filePath, 'utf-8') - return JSON.parse(data) -} diff --git a/core/src/node/api/common/consts.ts b/core/src/node/api/common/consts.ts new file mode 100644 index 000000000..bc3cfe300 --- /dev/null +++ b/core/src/node/api/common/consts.ts @@ -0,0 +1,19 @@ +// The PORT to use for the Nitro subprocess +export const NITRO_DEFAULT_PORT = 3928 + +// The HOST address to use for the Nitro subprocess +export const LOCAL_HOST = '127.0.0.1' + +export const SUPPORTED_MODEL_FORMAT = '.gguf' + +// The URL for the Nitro subprocess +const NITRO_HTTP_SERVER_URL = `http://${LOCAL_HOST}:${NITRO_DEFAULT_PORT}` +// The URL for the Nitro subprocess to load a model +export const NITRO_HTTP_LOAD_MODEL_URL = `${NITRO_HTTP_SERVER_URL}/inferences/llamacpp/loadmodel` +// The URL for the Nitro subprocess to validate a model +export const NITRO_HTTP_VALIDATE_MODEL_URL = `${NITRO_HTTP_SERVER_URL}/inferences/llamacpp/modelstatus` + +// The URL for the Nitro subprocess to kill itself +export const NITRO_HTTP_KILL_URL = `${NITRO_HTTP_SERVER_URL}/processmanager/destroy` + +export const DEFAULT_CHAT_COMPLETION_URL = `http://${LOCAL_HOST}:${NITRO_DEFAULT_PORT}/inferences/llamacpp/chat_completion` // default nitro url diff --git a/core/src/node/api/common/startStopModel.ts b/core/src/node/api/common/startStopModel.ts new file mode 100644 index 000000000..766588380 --- /dev/null +++ b/core/src/node/api/common/startStopModel.ts @@ -0,0 +1,351 @@ +import fs from 'fs' +import { join } from 'path' +import { getJanDataFolderPath, getJanExtensionsPath, getSystemResourceInfo } from '../../utils' +import { logServer } from '../../log' +import { ChildProcessWithoutNullStreams, spawn } from 'child_process' +import { Model, ModelSettingParams, PromptTemplate } from '../../../types' +import { + LOCAL_HOST, + NITRO_DEFAULT_PORT, + NITRO_HTTP_KILL_URL, + NITRO_HTTP_LOAD_MODEL_URL, + NITRO_HTTP_VALIDATE_MODEL_URL, + SUPPORTED_MODEL_FORMAT, +} from './consts' + +// The subprocess instance for Nitro +let subprocess: ChildProcessWithoutNullStreams | undefined = undefined + +// TODO: move this to core type +interface NitroModelSettings extends ModelSettingParams { + llama_model_path: string + cpu_threads: number +} + +export const startModel = async (modelId: string, settingParams?: ModelSettingParams) => { + try { + await runModel(modelId, settingParams) + + return { + message: `Model ${modelId} started`, + } + } catch (e) { + return { + error: e, + } + } +} + +const runModel = async (modelId: string, settingParams?: ModelSettingParams): Promise => { + const janDataFolderPath = getJanDataFolderPath() + const modelFolderFullPath = join(janDataFolderPath, 'models', modelId) + + if (!fs.existsSync(modelFolderFullPath)) { + throw `Model not found: ${modelId}` + } + + const files: string[] = fs.readdirSync(modelFolderFullPath) + + // Look for GGUF model file + const ggufBinFile = files.find((file) => file.toLowerCase().includes(SUPPORTED_MODEL_FORMAT)) + + const modelMetadataPath = join(modelFolderFullPath, 'model.json') + const modelMetadata: Model = JSON.parse(fs.readFileSync(modelMetadataPath, 'utf-8')) + + if (!ggufBinFile) { + throw 'No GGUF model file found' + } + const modelBinaryPath = join(modelFolderFullPath, ggufBinFile) + + const nitroResourceProbe = await getSystemResourceInfo() + const nitroModelSettings: NitroModelSettings = { + ...modelMetadata.settings, + ...settingParams, + llama_model_path: modelBinaryPath, + // This is critical and requires real CPU physical core count (or performance core) + cpu_threads: Math.max(1, nitroResourceProbe.numCpuPhysicalCore), + ...(modelMetadata.settings.mmproj && { + mmproj: join(modelFolderFullPath, modelMetadata.settings.mmproj), + }), + } + + logServer(`[NITRO]::Debug: Nitro model settings: ${JSON.stringify(nitroModelSettings)}`) + + // Convert settings.prompt_template to system_prompt, user_prompt, ai_prompt + if (modelMetadata.settings.prompt_template) { + const promptTemplate = modelMetadata.settings.prompt_template + const prompt = promptTemplateConverter(promptTemplate) + if (prompt?.error) { + return Promise.reject(prompt.error) + } + nitroModelSettings.system_prompt = prompt.system_prompt + nitroModelSettings.user_prompt = prompt.user_prompt + nitroModelSettings.ai_prompt = prompt.ai_prompt + } + + await runNitroAndLoadModel(modelId, nitroModelSettings) +} + +// TODO: move to util +const promptTemplateConverter = (promptTemplate: string): PromptTemplate => { + // Split the string using the markers + const systemMarker = '{system_message}' + const promptMarker = '{prompt}' + + if (promptTemplate.includes(systemMarker) && promptTemplate.includes(promptMarker)) { + // Find the indices of the markers + const systemIndex = promptTemplate.indexOf(systemMarker) + const promptIndex = promptTemplate.indexOf(promptMarker) + + // Extract the parts of the string + const system_prompt = promptTemplate.substring(0, systemIndex) + const user_prompt = promptTemplate.substring(systemIndex + systemMarker.length, promptIndex) + const ai_prompt = promptTemplate.substring(promptIndex + promptMarker.length) + + // Return the split parts + return { system_prompt, user_prompt, ai_prompt } + } else if (promptTemplate.includes(promptMarker)) { + // Extract the parts of the string for the case where only promptMarker is present + const promptIndex = promptTemplate.indexOf(promptMarker) + const user_prompt = promptTemplate.substring(0, promptIndex) + const ai_prompt = promptTemplate.substring(promptIndex + promptMarker.length) + + // Return the split parts + return { user_prompt, ai_prompt } + } + + // Return an error if none of the conditions are met + return { error: 'Cannot split prompt template' } +} + +const runNitroAndLoadModel = async (modelId: string, modelSettings: NitroModelSettings) => { + // Gather system information for CPU physical cores and memory + const tcpPortUsed = require('tcp-port-used') + + await stopModel(modelId) + await tcpPortUsed.waitUntilFree(NITRO_DEFAULT_PORT, 300, 5000) + + /** + * There is a problem with Windows process manager + * Should wait for awhile to make sure the port is free and subprocess is killed + * The tested threshold is 500ms + **/ + if (process.platform === 'win32') { + await new Promise((resolve) => setTimeout(resolve, 500)) + } + + await spawnNitroProcess() + await loadLLMModel(modelSettings) + await validateModelStatus() +} + +const spawnNitroProcess = async (): Promise => { + logServer(`[NITRO]::Debug: Spawning Nitro subprocess...`) + + let binaryFolder = join( + getJanExtensionsPath(), + '@janhq', + 'inference-nitro-extension', + 'dist', + 'bin' + ) + + let executableOptions = executableNitroFile() + const tcpPortUsed = require('tcp-port-used') + + const args: string[] = ['1', LOCAL_HOST, NITRO_DEFAULT_PORT.toString()] + // Execute the binary + logServer( + `[NITRO]::Debug: Spawn nitro at path: ${executableOptions.executablePath}, and args: ${args}` + ) + subprocess = spawn( + executableOptions.executablePath, + ['1', LOCAL_HOST, NITRO_DEFAULT_PORT.toString()], + { + cwd: binaryFolder, + env: { + ...process.env, + CUDA_VISIBLE_DEVICES: executableOptions.cudaVisibleDevices, + }, + } + ) + + // Handle subprocess output + subprocess.stdout.on('data', (data: any) => { + logServer(`[NITRO]::Debug: ${data}`) + }) + + subprocess.stderr.on('data', (data: any) => { + logServer(`[NITRO]::Error: ${data}`) + }) + + subprocess.on('close', (code: any) => { + logServer(`[NITRO]::Debug: Nitro exited with code: ${code}`) + subprocess = undefined + }) + + tcpPortUsed.waitUntilUsed(NITRO_DEFAULT_PORT, 300, 30000).then(() => { + logServer(`[NITRO]::Debug: Nitro is ready`) + }) +} + +type NitroExecutableOptions = { + executablePath: string + cudaVisibleDevices: string +} + +const executableNitroFile = (): NitroExecutableOptions => { + const nvidiaInfoFilePath = join(getJanDataFolderPath(), 'settings', 'settings.json') + let binaryFolder = join( + getJanExtensionsPath(), + '@janhq', + 'inference-nitro-extension', + 'dist', + 'bin' + ) + + let cudaVisibleDevices = '' + let binaryName = 'nitro' + /** + * The binary folder is different for each platform. + */ + if (process.platform === 'win32') { + /** + * For Windows: win-cpu, win-cuda-11-7, win-cuda-12-0 + */ + let nvidiaInfo = JSON.parse(fs.readFileSync(nvidiaInfoFilePath, 'utf-8')) + if (nvidiaInfo['run_mode'] === 'cpu') { + binaryFolder = join(binaryFolder, 'win-cpu') + } else { + if (nvidiaInfo['cuda'].version === '12') { + binaryFolder = join(binaryFolder, 'win-cuda-12-0') + } else { + binaryFolder = join(binaryFolder, 'win-cuda-11-7') + } + cudaVisibleDevices = nvidiaInfo['gpu_highest_vram'] + } + binaryName = 'nitro.exe' + } else if (process.platform === 'darwin') { + /** + * For MacOS: mac-arm64 (Silicon), mac-x64 (InteL) + */ + if (process.arch === 'arm64') { + binaryFolder = join(binaryFolder, 'mac-arm64') + } else { + binaryFolder = join(binaryFolder, 'mac-x64') + } + } else { + /** + * For Linux: linux-cpu, linux-cuda-11-7, linux-cuda-12-0 + */ + let nvidiaInfo = JSON.parse(fs.readFileSync(nvidiaInfoFilePath, 'utf-8')) + if (nvidiaInfo['run_mode'] === 'cpu') { + binaryFolder = join(binaryFolder, 'linux-cpu') + } else { + if (nvidiaInfo['cuda'].version === '12') { + binaryFolder = join(binaryFolder, 'linux-cuda-12-0') + } else { + binaryFolder = join(binaryFolder, 'linux-cuda-11-7') + } + cudaVisibleDevices = nvidiaInfo['gpu_highest_vram'] + } + } + + return { + executablePath: join(binaryFolder, binaryName), + cudaVisibleDevices, + } +} + +const validateModelStatus = async (): Promise => { + // Send a GET request to the validation URL. + // Retry the request up to 3 times if it fails, with a delay of 500 milliseconds between retries. + const fetchRT = require('fetch-retry') + const fetchRetry = fetchRT(fetch) + + return fetchRetry(NITRO_HTTP_VALIDATE_MODEL_URL, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + retries: 5, + retryDelay: 500, + }).then(async (res: Response) => { + logServer(`[NITRO]::Debug: Validate model state success with response ${JSON.stringify(res)}`) + // If the response is OK, check model_loaded status. + if (res.ok) { + const body = await res.json() + // If the model is loaded, return an empty object. + // Otherwise, return an object with an error message. + if (body.model_loaded) { + return Promise.resolve() + } + } + return Promise.reject('Validate model status failed') + }) +} + +const loadLLMModel = async (settings: NitroModelSettings): Promise => { + logServer(`[NITRO]::Debug: Loading model with params ${JSON.stringify(settings)}`) + const fetchRT = require('fetch-retry') + const fetchRetry = fetchRT(fetch) + + return fetchRetry(NITRO_HTTP_LOAD_MODEL_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(settings), + retries: 3, + retryDelay: 500, + }) + .then((res: any) => { + logServer(`[NITRO]::Debug: Load model success with response ${JSON.stringify(res)}`) + return Promise.resolve(res) + }) + .catch((err: any) => { + logServer(`[NITRO]::Error: Load model failed with error ${err}`) + return Promise.reject() + }) +} + +/** + * Stop model and kill nitro process. + */ +export const stopModel = async (_modelId: string) => { + if (!subprocess) { + return { + error: "Model isn't running", + } + } + return new Promise((resolve, reject) => { + const controller = new AbortController() + setTimeout(() => { + controller.abort() + reject({ + error: 'Failed to stop model: Timedout', + }) + }, 5000) + const tcpPortUsed = require('tcp-port-used') + logServer(`[NITRO]::Debug: Request to kill Nitro`) + + fetch(NITRO_HTTP_KILL_URL, { + method: 'DELETE', + signal: controller.signal, + }) + .then(() => { + subprocess?.kill() + subprocess = undefined + }) + .catch(() => { + // don't need to do anything, we still kill the subprocess + }) + .then(() => tcpPortUsed.waitUntilFree(NITRO_DEFAULT_PORT, 300, 5000)) + .then(() => logServer(`[NITRO]::Debug: Nitro process is terminated`)) + .then(() => + resolve({ + message: 'Model stopped', + }) + ) + }) +} diff --git a/core/src/node/api/routes/common.ts b/core/src/node/api/routes/common.ts index a6c65a382..27385e561 100644 --- a/core/src/node/api/routes/common.ts +++ b/core/src/node/api/routes/common.ts @@ -10,6 +10,8 @@ import { } from '../common/builder' import { JanApiRouteConfiguration } from '../common/configuration' +import { startModel, stopModel } from '../common/startStopModel' +import { ModelSettingParams } from '../../../types' export const commonRouter = async (app: HttpServer) => { // Common Routes @@ -17,19 +19,33 @@ export const commonRouter = async (app: HttpServer) => { app.get(`/${key}`, async (_request) => getBuilder(JanApiRouteConfiguration[key])) app.get(`/${key}/:id`, async (request: any) => - retrieveBuilder(JanApiRouteConfiguration[key], request.params.id), + retrieveBuilder(JanApiRouteConfiguration[key], request.params.id) ) app.delete(`/${key}/:id`, async (request: any) => - deleteBuilder(JanApiRouteConfiguration[key], request.params.id), + deleteBuilder(JanApiRouteConfiguration[key], request.params.id) ) }) // Download Model Routes app.get(`/models/download/:modelId`, async (request: any) => - downloadModel(request.params.modelId, { ignoreSSL: request.query.ignoreSSL === 'true', proxy: request.query.proxy }), + downloadModel(request.params.modelId, { + ignoreSSL: request.query.ignoreSSL === 'true', + proxy: request.query.proxy, + }) ) + app.put(`/models/:modelId/start`, async (request: any) => { + let settingParams: ModelSettingParams | undefined = undefined + if (Object.keys(request.body).length !== 0) { + settingParams = JSON.parse(request.body) as ModelSettingParams + } + + return startModel(request.params.modelId, settingParams) + }) + + app.put(`/models/:modelId/stop`, async (request: any) => stopModel(request.params.modelId)) + // Chat Completion Routes app.post(`/chat/completions`, async (request: any, reply: any) => chatCompletions(request, reply)) diff --git a/core/src/node/utils/index.ts b/core/src/node/utils/index.ts index 00db04c9b..4bcbf13b1 100644 --- a/core/src/node/utils/index.ts +++ b/core/src/node/utils/index.ts @@ -1,16 +1,18 @@ -import { AppConfiguration } from "../../types"; -import { join } from "path"; -import fs from "fs"; -import os from "os"; +import { AppConfiguration, SystemResourceInfo } from '../../types' +import { join } from 'path' +import fs from 'fs' +import os from 'os' +import { log, logServer } from '../log' +import childProcess from 'child_process' // TODO: move this to core -const configurationFileName = "settings.json"; +const configurationFileName = 'settings.json' // TODO: do no specify app name in framework module -const defaultJanDataFolder = join(os.homedir(), "jan"); +const defaultJanDataFolder = join(os.homedir(), 'jan') const defaultAppConfig: AppConfiguration = { data_folder: defaultJanDataFolder, -}; +} /** * Getting App Configurations. @@ -20,39 +22,39 @@ const defaultAppConfig: AppConfiguration = { export const getAppConfigurations = (): AppConfiguration => { // Retrieve Application Support folder path // Fallback to user home directory if not found - const configurationFile = getConfigurationFilePath(); + const configurationFile = getConfigurationFilePath() if (!fs.existsSync(configurationFile)) { // create default app config if we don't have one - console.debug(`App config not found, creating default config at ${configurationFile}`); - fs.writeFileSync(configurationFile, JSON.stringify(defaultAppConfig)); - return defaultAppConfig; + console.debug(`App config not found, creating default config at ${configurationFile}`) + fs.writeFileSync(configurationFile, JSON.stringify(defaultAppConfig)) + return defaultAppConfig } try { const appConfigurations: AppConfiguration = JSON.parse( - fs.readFileSync(configurationFile, "utf-8"), - ); - return appConfigurations; + fs.readFileSync(configurationFile, 'utf-8') + ) + return appConfigurations } catch (err) { - console.error(`Failed to read app config, return default config instead! Err: ${err}`); - return defaultAppConfig; + console.error(`Failed to read app config, return default config instead! Err: ${err}`) + return defaultAppConfig } -}; +} const getConfigurationFilePath = () => join( - global.core?.appPath() || process.env[process.platform == "win32" ? "USERPROFILE" : "HOME"], - configurationFileName, - ); + global.core?.appPath() || process.env[process.platform == 'win32' ? 'USERPROFILE' : 'HOME'], + configurationFileName + ) export const updateAppConfiguration = (configuration: AppConfiguration): Promise => { - const configurationFile = getConfigurationFilePath(); - console.debug("updateAppConfiguration, configurationFile: ", configurationFile); + const configurationFile = getConfigurationFilePath() + console.debug('updateAppConfiguration, configurationFile: ', configurationFile) - fs.writeFileSync(configurationFile, JSON.stringify(configuration)); - return Promise.resolve(); -}; + fs.writeFileSync(configurationFile, JSON.stringify(configuration)) + return Promise.resolve() +} /** * Utility function to get server log path @@ -60,13 +62,13 @@ export const updateAppConfiguration = (configuration: AppConfiguration): Promise * @returns {string} The log path. */ export const getServerLogPath = (): string => { - const appConfigurations = getAppConfigurations(); - const logFolderPath = join(appConfigurations.data_folder, "logs"); + const appConfigurations = getAppConfigurations() + const logFolderPath = join(appConfigurations.data_folder, 'logs') if (!fs.existsSync(logFolderPath)) { - fs.mkdirSync(logFolderPath, { recursive: true }); + fs.mkdirSync(logFolderPath, { recursive: true }) } - return join(logFolderPath, "server.log"); -}; + return join(logFolderPath, 'server.log') +} /** * Utility function to get app log path @@ -74,13 +76,13 @@ export const getServerLogPath = (): string => { * @returns {string} The log path. */ export const getAppLogPath = (): string => { - const appConfigurations = getAppConfigurations(); - const logFolderPath = join(appConfigurations.data_folder, "logs"); + const appConfigurations = getAppConfigurations() + const logFolderPath = join(appConfigurations.data_folder, 'logs') if (!fs.existsSync(logFolderPath)) { - fs.mkdirSync(logFolderPath, { recursive: true }); + fs.mkdirSync(logFolderPath, { recursive: true }) } - return join(logFolderPath, "app.log"); -}; + return join(logFolderPath, 'app.log') +} /** * Utility function to get data folder path @@ -88,9 +90,9 @@ export const getAppLogPath = (): string => { * @returns {string} The data folder path. */ export const getJanDataFolderPath = (): string => { - const appConfigurations = getAppConfigurations(); - return appConfigurations.data_folder; -}; + const appConfigurations = getAppConfigurations() + return appConfigurations.data_folder +} /** * Utility function to get extension path @@ -98,6 +100,70 @@ export const getJanDataFolderPath = (): string => { * @returns {string} The extensions path. */ export const getJanExtensionsPath = (): string => { - const appConfigurations = getAppConfigurations(); - return join(appConfigurations.data_folder, "extensions"); -}; + const appConfigurations = getAppConfigurations() + return join(appConfigurations.data_folder, 'extensions') +} + +/** + * Utility function to physical cpu count + * + * @returns {number} The physical cpu count. + */ +export const physicalCpuCount = async (): Promise => { + const platform = os.platform() + if (platform === 'linux') { + const output = await exec('lscpu -p | egrep -v "^#" | sort -u -t, -k 2,4 | wc -l') + return parseInt(output.trim(), 10) + } else if (platform === 'darwin') { + const output = await exec('sysctl -n hw.physicalcpu_max') + return parseInt(output.trim(), 10) + } else if (platform === 'win32') { + const output = await exec('WMIC CPU Get NumberOfCores') + return output + .split(os.EOL) + .map((line: string) => parseInt(line)) + .filter((value: number) => !isNaN(value)) + .reduce((sum: number, number: number) => sum + number, 1) + } else { + const cores = os.cpus().filter((cpu: any, index: number) => { + const hasHyperthreading = cpu.model.includes('Intel') + const isOdd = index % 2 === 1 + return !hasHyperthreading || isOdd + }) + return cores.length + } +} + +const exec = async (command: string): Promise => { + return new Promise((resolve, reject) => { + childProcess.exec(command, { encoding: 'utf8' }, (error, stdout) => { + if (error) { + reject(error) + } else { + resolve(stdout) + } + }) + }) +} + +export const getSystemResourceInfo = async (): Promise => { + const cpu = await physicalCpuCount() + const message = `[NITRO]::CPU informations - ${cpu}` + log(message) + logServer(message) + + return { + numCpuPhysicalCore: cpu, + memAvailable: 0, // TODO: this should not be 0 + } +} + +export const getEngineConfiguration = async (engineId: string) => { + if (engineId !== 'openai') { + return undefined + } + const directoryPath = join(getJanDataFolderPath(), 'engines') + const filePath = join(directoryPath, `${engineId}.json`) + const data = fs.readFileSync(filePath, 'utf-8') + return JSON.parse(data) +} diff --git a/core/src/types/index.ts b/core/src/types/index.ts index 3bdcb5421..ee6f4ef08 100644 --- a/core/src/types/index.ts +++ b/core/src/types/index.ts @@ -6,3 +6,4 @@ export * from './inference' export * from './monitoring' export * from './file' export * from './config' +export * from './miscellaneous' diff --git a/core/src/types/miscellaneous/index.ts b/core/src/types/miscellaneous/index.ts new file mode 100644 index 000000000..02c973323 --- /dev/null +++ b/core/src/types/miscellaneous/index.ts @@ -0,0 +1,2 @@ +export * from './systemResourceInfo' +export * from './promptTemplate' diff --git a/core/src/types/miscellaneous/promptTemplate.ts b/core/src/types/miscellaneous/promptTemplate.ts new file mode 100644 index 000000000..a6743c67c --- /dev/null +++ b/core/src/types/miscellaneous/promptTemplate.ts @@ -0,0 +1,6 @@ +export type PromptTemplate = { + system_prompt?: string + ai_prompt?: string + user_prompt?: string + error?: string +} diff --git a/core/src/types/miscellaneous/systemResourceInfo.ts b/core/src/types/miscellaneous/systemResourceInfo.ts new file mode 100644 index 000000000..1472cda47 --- /dev/null +++ b/core/src/types/miscellaneous/systemResourceInfo.ts @@ -0,0 +1,4 @@ +export type SystemResourceInfo = { + numCpuPhysicalCore: number + memAvailable: number +} diff --git a/core/src/types/model/modelEntity.ts b/core/src/types/model/modelEntity.ts index 727ff085f..644c34dfb 100644 --- a/core/src/types/model/modelEntity.ts +++ b/core/src/types/model/modelEntity.ts @@ -123,6 +123,7 @@ export type ModelSettingParams = { user_prompt?: string llama_model_path?: string mmproj?: string + cont_batching?: boolean } /** diff --git a/extensions/inference-nitro-extension/src/@types/global.d.ts b/extensions/inference-nitro-extension/src/@types/global.d.ts index 5fb41f0f8..bc126337f 100644 --- a/extensions/inference-nitro-extension/src/@types/global.d.ts +++ b/extensions/inference-nitro-extension/src/@types/global.d.ts @@ -2,22 +2,6 @@ declare const NODE: string; declare const INFERENCE_URL: string; declare const TROUBLESHOOTING_URL: string; -/** - * The parameters for the initModel function. - * @property settings - The settings for the machine learning model. - * @property settings.ctx_len - The context length. - * @property settings.ngl - The number of generated tokens. - * @property settings.cont_batching - Whether to use continuous batching. - * @property settings.embedding - Whether to use embedding. - */ -interface EngineSettings { - ctx_len: number; - ngl: number; - cpu_threads: number; - cont_batching: boolean; - embedding: boolean; -} - /** * The response from the initModel function. * @property error - An error message if the model fails to load. @@ -26,8 +10,3 @@ interface ModelOperationResponse { error?: any; modelFile?: string; } - -interface ResourcesInfo { - numCpuPhysicalCore: number; - memAvailable: number; -} \ No newline at end of file diff --git a/extensions/inference-nitro-extension/src/index.ts b/extensions/inference-nitro-extension/src/index.ts index 0e6edb992..aaa230ca3 100644 --- a/extensions/inference-nitro-extension/src/index.ts +++ b/extensions/inference-nitro-extension/src/index.ts @@ -24,6 +24,7 @@ import { MessageEvent, ModelEvent, InferenceEvent, + ModelSettingParams, } from "@janhq/core"; import { requestInference } from "./helpers/sse"; import { ulid } from "ulid"; @@ -45,7 +46,7 @@ export default class JanInferenceNitroExtension extends InferenceExtension { private _currentModel: Model | undefined; - private _engineSettings: EngineSettings = { + private _engineSettings: ModelSettingParams = { ctx_len: 2048, ngl: 100, cpu_threads: 1, diff --git a/extensions/inference-nitro-extension/src/node/index.ts b/extensions/inference-nitro-extension/src/node/index.ts index 77060e414..443e686e8 100644 --- a/extensions/inference-nitro-extension/src/node/index.ts +++ b/extensions/inference-nitro-extension/src/node/index.ts @@ -3,11 +3,19 @@ import path from "path"; import { ChildProcessWithoutNullStreams, spawn } from "child_process"; import tcpPortUsed from "tcp-port-used"; import fetchRT from "fetch-retry"; -import { log, getJanDataFolderPath } from "@janhq/core/node"; +import { + log, + getJanDataFolderPath, + getSystemResourceInfo, +} from "@janhq/core/node"; import { getNitroProcessInfo, updateNvidiaInfo } from "./nvidia"; -import { Model, InferenceEngine, ModelSettingParams } from "@janhq/core"; +import { + Model, + InferenceEngine, + ModelSettingParams, + PromptTemplate, +} from "@janhq/core"; import { executableNitroFile } from "./execute"; -import { physicalCpuCount } from "./utils"; // Polyfill fetch with retry const fetchRetry = fetchRT(fetch); @@ -20,16 +28,6 @@ interface ModelInitOptions { model: Model; } -/** - * The response object of Prompt Template parsing. - */ -interface PromptTemplate { - system_prompt?: string; - ai_prompt?: string; - user_prompt?: string; - error?: string; -} - /** * Model setting args for Nitro model load. */ @@ -78,7 +76,7 @@ function stopModel(): Promise { * TODO: Should pass absolute of the model file instead of just the name - So we can modurize the module.ts to npm package */ async function runModel( - wrapper: ModelInitOptions, + wrapper: ModelInitOptions ): Promise { if (wrapper.model.engine !== InferenceEngine.nitro) { // Not a nitro model @@ -96,7 +94,7 @@ async function runModel( const ggufBinFile = files.find( (file) => file === path.basename(currentModelFile) || - file.toLowerCase().includes(SUPPORTED_MODEL_FORMAT), + file.toLowerCase().includes(SUPPORTED_MODEL_FORMAT) ); if (!ggufBinFile) return Promise.reject("No GGUF model file found"); @@ -106,7 +104,7 @@ async function runModel( if (wrapper.model.engine !== InferenceEngine.nitro) { return Promise.reject("Not a nitro model"); } else { - const nitroResourceProbe = await getResourcesInfo(); + const nitroResourceProbe = await getSystemResourceInfo(); // Convert settings.prompt_template to system_prompt, user_prompt, ai_prompt if (wrapper.model.settings.prompt_template) { const promptTemplate = wrapper.model.settings.prompt_template; @@ -191,10 +189,10 @@ function promptTemplateConverter(promptTemplate: string): PromptTemplate { const system_prompt = promptTemplate.substring(0, systemIndex); const user_prompt = promptTemplate.substring( systemIndex + systemMarker.length, - promptIndex, + promptIndex ); const ai_prompt = promptTemplate.substring( - promptIndex + promptMarker.length, + promptIndex + promptMarker.length ); // Return the split parts @@ -204,7 +202,7 @@ function promptTemplateConverter(promptTemplate: string): PromptTemplate { const promptIndex = promptTemplate.indexOf(promptMarker); const user_prompt = promptTemplate.substring(0, promptIndex); const ai_prompt = promptTemplate.substring( - promptIndex + promptMarker.length, + promptIndex + promptMarker.length ); // Return the split parts @@ -233,8 +231,8 @@ function loadLLMModel(settings: any): Promise { .then((res) => { log( `[NITRO]::Debug: Load model success with response ${JSON.stringify( - res, - )}`, + res + )}` ); return Promise.resolve(res); }) @@ -263,8 +261,8 @@ async function validateModelStatus(): Promise { }).then(async (res: Response) => { log( `[NITRO]::Debug: Validate model state success with response ${JSON.stringify( - res, - )}`, + res + )}` ); // If the response is OK, check model_loaded status. if (res.ok) { @@ -315,7 +313,7 @@ function spawnNitroProcess(): Promise { const args: string[] = ["1", LOCAL_HOST, PORT.toString()]; // Execute the binary log( - `[NITRO]::Debug: Spawn nitro at path: ${executableOptions.executablePath}, and args: ${args}`, + `[NITRO]::Debug: Spawn nitro at path: ${executableOptions.executablePath}, and args: ${args}` ); subprocess = spawn( executableOptions.executablePath, @@ -326,7 +324,7 @@ function spawnNitroProcess(): Promise { ...process.env, CUDA_VISIBLE_DEVICES: executableOptions.cudaVisibleDevices, }, - }, + } ); // Handle subprocess output @@ -351,22 +349,6 @@ function spawnNitroProcess(): Promise { }); } -/** - * Get the system resources information - * TODO: Move to Core so that it can be reused - */ -function getResourcesInfo(): Promise { - return new Promise(async (resolve) => { - const cpu = await physicalCpuCount(); - log(`[NITRO]::CPU informations - ${cpu}`); - const response: ResourcesInfo = { - numCpuPhysicalCore: cpu, - memAvailable: 0, - }; - resolve(response); - }); -} - /** * Every module should have a dispose function * This will be called when the extension is unloaded and should clean up any resources diff --git a/extensions/inference-nitro-extension/src/node/utils.ts b/extensions/inference-nitro-extension/src/node/utils.ts deleted file mode 100644 index c7ef2e9a6..000000000 --- a/extensions/inference-nitro-extension/src/node/utils.ts +++ /dev/null @@ -1,56 +0,0 @@ -import os from "os"; -import childProcess from "child_process"; - -function exec(command: string): Promise { - return new Promise((resolve, reject) => { - childProcess.exec(command, { encoding: "utf8" }, (error, stdout) => { - if (error) { - reject(error); - } else { - resolve(stdout); - } - }); - }); -} - -let amount: number; -const platform = os.platform(); - -export async function physicalCpuCount(): Promise { - return new Promise((resolve, reject) => { - if (platform === "linux") { - exec('lscpu -p | egrep -v "^#" | sort -u -t, -k 2,4 | wc -l') - .then((output) => { - amount = parseInt(output.trim(), 10); - resolve(amount); - }) - .catch(reject); - } else if (platform === "darwin") { - exec("sysctl -n hw.physicalcpu_max") - .then((output) => { - amount = parseInt(output.trim(), 10); - resolve(amount); - }) - .catch(reject); - } else if (platform === "win32") { - exec("WMIC CPU Get NumberOfCores") - .then((output) => { - amount = output - .split(os.EOL) - .map((line: string) => parseInt(line)) - .filter((value: number) => !isNaN(value)) - .reduce((sum: number, number: number) => sum + number, 1); - resolve(amount); - }) - .catch(reject); - } else { - const cores = os.cpus().filter((cpu: any, index: number) => { - const hasHyperthreading = cpu.model.includes("Intel"); - const isOdd = index % 2 === 1; - return !hasHyperthreading || isOdd; - }); - amount = cores.length; - resolve(amount); - } - }); -} diff --git a/server/package.json b/server/package.json index 9495a0d65..f61730da4 100644 --- a/server/package.json +++ b/server/package.json @@ -26,6 +26,8 @@ "dotenv": "^16.3.1", "fastify": "^4.24.3", "request": "^2.88.2", + "fetch-retry": "^5.0.6", + "tcp-port-used": "^1.0.2", "request-progress": "^3.0.0" }, "devDependencies": { @@ -35,6 +37,7 @@ "@typescript-eslint/parser": "^6.7.3", "eslint-plugin-react": "^7.33.2", "run-script-os": "^1.1.6", + "@types/tcp-port-used": "^1.0.4", "typescript": "^5.2.2" } } diff --git a/web/hooks/useCreateNewThread.ts b/web/hooks/useCreateNewThread.ts index d9451a46c..aad42aba9 100644 --- a/web/hooks/useCreateNewThread.ts +++ b/web/hooks/useCreateNewThread.ts @@ -7,7 +7,7 @@ import { ThreadState, Model, } from '@janhq/core' -import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai' +import { atom, useAtomValue, useSetAtom } from 'jotai' import { fileUploadAtom } from '@/containers/Providers/Jotai' @@ -48,7 +48,8 @@ export const useCreateNewThread = () => { const createNewThread = useSetAtom(createNewThreadAtom) const setActiveThreadId = useSetAtom(setActiveThreadIdAtom) const updateThread = useSetAtom(updateThreadAtom) - const [fileUpload, setFileUpload] = useAtom(fileUploadAtom) + + const setFileUpload = useSetAtom(fileUploadAtom) const { deleteThread } = useDeleteThread() const requestCreateNewThread = async ( diff --git a/web/hooks/useSetActiveThread.ts b/web/hooks/useSetActiveThread.ts index 035f0551a..76a744bcd 100644 --- a/web/hooks/useSetActiveThread.ts +++ b/web/hooks/useSetActiveThread.ts @@ -1,5 +1,3 @@ -import { useEffect } from 'react' - import { InferenceEvent, ExtensionTypeEnum, diff --git a/web/screens/LocalServer/index.tsx b/web/screens/LocalServer/index.tsx index 7e1ba1fab..e7f3c7fc2 100644 --- a/web/screens/LocalServer/index.tsx +++ b/web/screens/LocalServer/index.tsx @@ -1,7 +1,6 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ 'use client' -import React, { useEffect, useState } from 'react' +import React, { useCallback, useEffect, useState } from 'react' import ScrollToBottom from 'react-scroll-to-bottom' @@ -81,14 +80,17 @@ const LocalServerScreen = () => { const [firstTimeVisitAPIServer, setFirstTimeVisitAPIServer] = useState(false) - const handleChangePort = (value: any) => { - if (Number(value) <= 0 || Number(value) >= 65536) { - setErrorRangePort(true) - } else { - setErrorRangePort(false) - } - setPort(value) - } + const handleChangePort = useCallback( + (value: string) => { + if (Number(value) <= 0 || Number(value) >= 65536) { + setErrorRangePort(true) + } else { + setErrorRangePort(false) + } + setPort(value) + }, + [setPort] + ) useEffect(() => { if (localStorage.getItem(FIRST_TIME_VISIT_API_SERVER) == null) { @@ -98,7 +100,7 @@ const LocalServerScreen = () => { useEffect(() => { handleChangePort(port) - }, []) + }, [handleChangePort, port]) return (
diff --git a/web/screens/Settings/Advanced/FactoryReset/ModalConfirmReset.tsx b/web/screens/Settings/Advanced/FactoryReset/ModalConfirmReset.tsx index d8a2321a9..89a875955 100644 --- a/web/screens/Settings/Advanced/FactoryReset/ModalConfirmReset.tsx +++ b/web/screens/Settings/Advanced/FactoryReset/ModalConfirmReset.tsx @@ -1,6 +1,4 @@ -import React, { useCallback, useEffect, useState } from 'react' - -import { fs, AppConfiguration, joinPath, getUserHomePath } from '@janhq/core' +import React, { useCallback, useState } from 'react' import { Modal, From 5ddc6ea4c82e818fca10812d5668bc754a35cab4 Mon Sep 17 00:00:00 2001 From: Louis Date: Thu, 1 Feb 2024 12:31:26 +0700 Subject: [PATCH 55/71] fix: the selected model auto revert back to previous used model with setting mismatch (#1883) * fix: the selected model auto revert back to previous used model with setting mismatch * fix: view in finder and view file action --- core/src/types/thread/threadEntity.ts | 1 - .../inference-nitro-extension/bin/version.txt | 2 +- .../src/node/index.ts | 22 +++---- web/containers/DropdownListSidebar/index.tsx | 51 ++++----------- web/helpers/atoms/Thread.atom.ts | 12 ---- web/hooks/useCreateNewThread.ts | 64 +++++++++---------- web/hooks/useDeleteThread.ts | 22 ++----- web/hooks/usePath.ts | 38 +++++------ web/hooks/useRecommendedModel.ts | 25 ++------ web/hooks/useSendChatMessage.ts | 46 ------------- web/hooks/useThreads.ts | 46 ++----------- web/hooks/useUpdateModelParameters.ts | 32 ++++++---- .../Chat/ModelSetting/SettingComponent.tsx | 15 ++++- web/screens/Chat/SimpleTextMessage/index.tsx | 16 +++-- web/screens/Settings/Advanced/index.tsx | 2 +- 15 files changed, 132 insertions(+), 262 deletions(-) diff --git a/core/src/types/thread/threadEntity.ts b/core/src/types/thread/threadEntity.ts index 37136eae6..dd88b10ec 100644 --- a/core/src/types/thread/threadEntity.ts +++ b/core/src/types/thread/threadEntity.ts @@ -43,5 +43,4 @@ export type ThreadState = { waitingForResponse: boolean error?: Error lastMessage?: string - isFinishInit?: boolean } diff --git a/extensions/inference-nitro-extension/bin/version.txt b/extensions/inference-nitro-extension/bin/version.txt index 1c09c74e2..c2c0004f0 100644 --- a/extensions/inference-nitro-extension/bin/version.txt +++ b/extensions/inference-nitro-extension/bin/version.txt @@ -1 +1 @@ -0.3.3 +0.3.5 diff --git a/extensions/inference-nitro-extension/src/node/index.ts b/extensions/inference-nitro-extension/src/node/index.ts index 443e686e8..749b68b1c 100644 --- a/extensions/inference-nitro-extension/src/node/index.ts +++ b/extensions/inference-nitro-extension/src/node/index.ts @@ -76,7 +76,7 @@ function stopModel(): Promise { * TODO: Should pass absolute of the model file instead of just the name - So we can modurize the module.ts to npm package */ async function runModel( - wrapper: ModelInitOptions + wrapper: ModelInitOptions, ): Promise { if (wrapper.model.engine !== InferenceEngine.nitro) { // Not a nitro model @@ -94,7 +94,7 @@ async function runModel( const ggufBinFile = files.find( (file) => file === path.basename(currentModelFile) || - file.toLowerCase().includes(SUPPORTED_MODEL_FORMAT) + file.toLowerCase().includes(SUPPORTED_MODEL_FORMAT), ); if (!ggufBinFile) return Promise.reject("No GGUF model file found"); @@ -189,10 +189,10 @@ function promptTemplateConverter(promptTemplate: string): PromptTemplate { const system_prompt = promptTemplate.substring(0, systemIndex); const user_prompt = promptTemplate.substring( systemIndex + systemMarker.length, - promptIndex + promptIndex, ); const ai_prompt = promptTemplate.substring( - promptIndex + promptMarker.length + promptIndex + promptMarker.length, ); // Return the split parts @@ -202,7 +202,7 @@ function promptTemplateConverter(promptTemplate: string): PromptTemplate { const promptIndex = promptTemplate.indexOf(promptMarker); const user_prompt = promptTemplate.substring(0, promptIndex); const ai_prompt = promptTemplate.substring( - promptIndex + promptMarker.length + promptIndex + promptMarker.length, ); // Return the split parts @@ -231,8 +231,8 @@ function loadLLMModel(settings: any): Promise { .then((res) => { log( `[NITRO]::Debug: Load model success with response ${JSON.stringify( - res - )}` + res, + )}`, ); return Promise.resolve(res); }) @@ -261,8 +261,8 @@ async function validateModelStatus(): Promise { }).then(async (res: Response) => { log( `[NITRO]::Debug: Validate model state success with response ${JSON.stringify( - res - )}` + res, + )}`, ); // If the response is OK, check model_loaded status. if (res.ok) { @@ -313,7 +313,7 @@ function spawnNitroProcess(): Promise { const args: string[] = ["1", LOCAL_HOST, PORT.toString()]; // Execute the binary log( - `[NITRO]::Debug: Spawn nitro at path: ${executableOptions.executablePath}, and args: ${args}` + `[NITRO]::Debug: Spawn nitro at path: ${executableOptions.executablePath}, and args: ${args}`, ); subprocess = spawn( executableOptions.executablePath, @@ -324,7 +324,7 @@ function spawnNitroProcess(): Promise { ...process.env, CUDA_VISIBLE_DEVICES: executableOptions.cudaVisibleDevices, }, - } + }, ); // Handle subprocess output diff --git a/web/containers/DropdownListSidebar/index.tsx b/web/containers/DropdownListSidebar/index.tsx index eb867f54e..140a1aba1 100644 --- a/web/containers/DropdownListSidebar/index.tsx +++ b/web/containers/DropdownListSidebar/index.tsx @@ -26,6 +26,8 @@ import { useMainViewState } from '@/hooks/useMainViewState' import useRecommendedModel from '@/hooks/useRecommendedModel' +import useUpdateModelParameters from '@/hooks/useUpdateModelParameters' + import { toGibibytes } from '@/utils/converter' import ModelLabel from '../ModelLabel' @@ -34,10 +36,8 @@ import OpenAiKeyInput from '../OpenAiKeyInput' import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom' import { - ModelParams, activeThreadAtom, setThreadModelParamsAtom, - threadStatesAtom, } from '@/helpers/atoms/Thread.atom' export const selectedModelAtom = atom(undefined) @@ -49,7 +49,6 @@ const DropdownListSidebar = ({ strictedThread?: boolean }) => { const activeThread = useAtomValue(activeThreadAtom) - const threadStates = useAtomValue(threadStatesAtom) const [selectedModel, setSelectedModel] = useAtom(selectedModelAtom) const setThreadModelParams = useSetAtom(setThreadModelParamsAtom) @@ -58,15 +57,7 @@ const DropdownListSidebar = ({ const { setMainViewState } = useMainViewState() const [loader, setLoader] = useState(0) const { recommendedModel, downloadedModels } = useRecommendedModel() - - /** - * Default value for max_tokens and ctx_len - * Its to avoid OOM issue since a model can set a big number for these settings - */ - const defaultValue = (value?: number) => { - if (value && value < 4096) return value - return 4096 - } + const { updateModelParameter } = useUpdateModelParameters() useEffect(() => { if (!activeThread) return @@ -78,31 +69,7 @@ const DropdownListSidebar = ({ model = recommendedModel } setSelectedModel(model) - const finishInit = threadStates[activeThread.id].isFinishInit ?? true - if (finishInit) return - const modelParams: ModelParams = { - ...model?.parameters, - ...model?.settings, - /** - * This is to set default value for these settings instead of maximum value - * Should only apply when model.json has these settings - */ - ...(model?.parameters.max_tokens && { - max_tokens: defaultValue(model?.parameters.max_tokens), - }), - ...(model?.settings.ctx_len && { - ctx_len: defaultValue(model?.settings.ctx_len), - }), - } - setThreadModelParams(activeThread.id, modelParams) - }, [ - recommendedModel, - activeThread, - threadStates, - downloadedModels, - setThreadModelParams, - setSelectedModel, - ]) + }, [recommendedModel, activeThread, downloadedModels, setSelectedModel]) // This is fake loader please fix this when we have realtime percentage when load model useEffect(() => { @@ -144,7 +111,16 @@ const DropdownListSidebar = ({ ...model?.parameters, ...model?.settings, } + // Update model paramter to the thread state setThreadModelParams(activeThread.id, modelParams) + + // Update model parameter to the thread file + if (model) + updateModelParameter(activeThread.id, { + params: modelParams, + modelId: model.id, + engine: model.engine, + }) } }, [ @@ -154,6 +130,7 @@ const DropdownListSidebar = ({ setSelectedModel, setServerEnabled, setThreadModelParams, + updateModelParameter, ] ) diff --git a/web/helpers/atoms/Thread.atom.ts b/web/helpers/atoms/Thread.atom.ts index 26b1e9c59..fcaa2a4af 100644 --- a/web/helpers/atoms/Thread.atom.ts +++ b/web/helpers/atoms/Thread.atom.ts @@ -46,18 +46,6 @@ export const deleteThreadStateAtom = atom( } ) -export const updateThreadInitSuccessAtom = atom( - null, - (get, set, threadId: string) => { - const currentState = { ...get(threadStatesAtom) } - currentState[threadId] = { - ...currentState[threadId], - isFinishInit: true, - } - set(threadStatesAtom, currentState) - } -) - export const updateThreadWaitingForResponseAtom = atom( null, (get, set, threadId: string, waitingForResponse: boolean) => { diff --git a/web/hooks/useCreateNewThread.ts b/web/hooks/useCreateNewThread.ts index aad42aba9..f2ae4fbd3 100644 --- a/web/hooks/useCreateNewThread.ts +++ b/web/hooks/useCreateNewThread.ts @@ -9,19 +9,21 @@ import { } from '@janhq/core' import { atom, useAtomValue, useSetAtom } from 'jotai' +import { selectedModelAtom } from '@/containers/DropdownListSidebar' import { fileUploadAtom } from '@/containers/Providers/Jotai' import { generateThreadId } from '@/utils/thread' -import useDeleteThread from './useDeleteThread' +import useRecommendedModel from './useRecommendedModel' + +import useSetActiveThread from './useSetActiveThread' import { extensionManager } from '@/extension' import { threadsAtom, - setActiveThreadIdAtom, threadStatesAtom, updateThreadAtom, - updateThreadInitSuccessAtom, + setThreadModelParamsAtom, } from '@/helpers/atoms/Thread.atom' const createNewThreadAtom = atom(null, (get, set, newThread: Thread) => { @@ -32,7 +34,6 @@ const createNewThreadAtom = atom(null, (get, set, newThread: Thread) => { hasMore: false, waitingForResponse: false, lastMessage: undefined, - isFinishInit: false, } currentState[newThread.id] = threadState set(threadStatesAtom, currentState) @@ -43,47 +44,35 @@ const createNewThreadAtom = atom(null, (get, set, newThread: Thread) => { }) export const useCreateNewThread = () => { - const threadStates = useAtomValue(threadStatesAtom) - const updateThreadFinishInit = useSetAtom(updateThreadInitSuccessAtom) const createNewThread = useSetAtom(createNewThreadAtom) - const setActiveThreadId = useSetAtom(setActiveThreadIdAtom) + const { setActiveThread } = useSetActiveThread() const updateThread = useSetAtom(updateThreadAtom) - const setFileUpload = useSetAtom(fileUploadAtom) - const { deleteThread } = useDeleteThread() + const setSelectedModel = useSetAtom(selectedModelAtom) + const setThreadModelParams = useSetAtom(setThreadModelParamsAtom) + + const { recommendedModel, downloadedModels } = useRecommendedModel() const requestCreateNewThread = async ( assistant: Assistant, model?: Model | undefined ) => { - // loop through threads state and filter if there's any thread that is not finish init - let unfinishedInitThreadId: string | undefined = undefined - for (const key in threadStates) { - const isFinishInit = threadStates[key].isFinishInit ?? true - if (!isFinishInit) { - unfinishedInitThreadId = key - break - } - } + const defaultModel = model ?? recommendedModel ?? downloadedModels[0] - if (unfinishedInitThreadId) { - await deleteThread(unfinishedInitThreadId) - } - - const modelId = model ? model.id : '*' const createdAt = Date.now() const assistantInfo: ThreadAssistantInfo = { assistant_id: assistant.id, assistant_name: assistant.name, tools: assistant.tools, model: { - id: modelId, - settings: {}, - parameters: {}, - engine: undefined, + id: defaultModel?.id ?? '*', + settings: defaultModel?.settings ?? {}, + parameters: defaultModel?.parameters ?? {}, + engine: defaultModel?.engine, }, instructions: assistant.instructions, } + const threadId = generateThreadId(assistant.id) const thread: Thread = { id: threadId, @@ -95,22 +84,27 @@ export const useCreateNewThread = () => { } // add the new thread on top of the thread list to the state + //TODO: Why do we have thread list then thread states? Should combine them createNewThread(thread) - setActiveThreadId(thread.id) + + setSelectedModel(defaultModel) + setThreadModelParams(thread.id, { + ...defaultModel?.settings, + ...defaultModel?.parameters, + }) // Delete the file upload state setFileUpload([]) + // Update thread metadata + await updateThreadMetadata(thread) + + setActiveThread(thread) } - function updateThreadMetadata(thread: Thread) { + async function updateThreadMetadata(thread: Thread) { updateThread(thread) - const threadState = threadStates[thread.id] - const isFinishInit = threadState?.isFinishInit ?? true - if (!isFinishInit) { - updateThreadFinishInit(thread.id) - } - extensionManager + await extensionManager .get(ExtensionTypeEnum.Conversational) ?.saveThread(thread) } diff --git a/web/hooks/useDeleteThread.ts b/web/hooks/useDeleteThread.ts index 88710f777..87cee125d 100644 --- a/web/hooks/useDeleteThread.ts +++ b/web/hooks/useDeleteThread.ts @@ -21,7 +21,6 @@ import { threadsAtom, setActiveThreadIdAtom, deleteThreadStateAtom, - threadStatesAtom, updateThreadStateLastMessageAtom, } from '@/helpers/atoms/Thread.atom' @@ -34,7 +33,6 @@ export default function useDeleteThread() { const deleteMessages = useSetAtom(deleteChatMessagesAtom) const cleanMessages = useSetAtom(cleanChatMessagesAtom) const deleteThreadState = useSetAtom(deleteThreadStateAtom) - const threadStates = useAtomValue(threadStatesAtom) const updateThreadLastMessage = useSetAtom(updateThreadStateLastMessageAtom) const cleanThread = async (threadId: string) => { @@ -74,22 +72,16 @@ export default function useDeleteThread() { const availableThreads = threads.filter((c) => c.id !== threadId) setThreads(availableThreads) - const deletingThreadState = threadStates[threadId] - const isFinishInit = deletingThreadState?.isFinishInit ?? true - // delete the thread state deleteThreadState(threadId) - if (isFinishInit) { - deleteMessages(threadId) - setCurrentPrompt('') - toaster({ - title: 'Thread successfully deleted.', - description: `Thread ${threadId} has been successfully deleted.`, - type: 'success', - }) - } - + deleteMessages(threadId) + setCurrentPrompt('') + toaster({ + title: 'Thread successfully deleted.', + description: `Thread ${threadId} has been successfully deleted.`, + type: 'success', + }) if (availableThreads.length > 0) { setActiveThreadId(availableThreads[0].id) } else { diff --git a/web/hooks/usePath.ts b/web/hooks/usePath.ts index e37c2457d..aea25bef1 100644 --- a/web/hooks/usePath.ts +++ b/web/hooks/usePath.ts @@ -3,23 +3,15 @@ import { useAtomValue } from 'jotai' import { selectedModelAtom } from '@/containers/DropdownListSidebar' -import { activeThreadAtom, threadStatesAtom } from '@/helpers/atoms/Thread.atom' +import { activeThreadAtom } from '@/helpers/atoms/Thread.atom' export const usePath = () => { const activeThread = useAtomValue(activeThreadAtom) - const threadStates = useAtomValue(threadStatesAtom) const selectedModel = useAtomValue(selectedModelAtom) const onReviewInFinder = async (type: string) => { // TODO: this logic should be refactored. - if (type !== 'Model') { - if (!activeThread) return - const activeThreadState = threadStates[activeThread.id] - if (!activeThreadState.isFinishInit) { - alert('Thread is not started yet') - return - } - } + if (type !== 'Model' && !activeThread) return const userSpace = await getJanDataFolderPath() let filePath = undefined @@ -48,14 +40,7 @@ export const usePath = () => { const onViewJson = async (type: string) => { // TODO: this logic should be refactored. - if (type !== 'Model') { - if (!activeThread) return - const activeThreadState = threadStates[activeThread.id] - if (!activeThreadState.isFinishInit) { - alert('Thread is not started yet') - return - } - } + if (type !== 'Model' && !activeThread) return const userSpace = await getJanDataFolderPath() let filePath = undefined @@ -88,11 +73,6 @@ export const usePath = () => { const onViewFile = async (id: string) => { if (!activeThread) return - const activeThreadState = threadStates[activeThread.id] - if (!activeThreadState.isFinishInit) { - alert('Thread is not started yet') - return - } const userSpace = await getJanDataFolderPath() let filePath = undefined @@ -102,9 +82,21 @@ export const usePath = () => { openFileExplorer(fullPath) } + const onViewFileContainer = async () => { + if (!activeThread) return + + const userSpace = await getJanDataFolderPath() + let filePath = undefined + filePath = await joinPath(['threads', `${activeThread.id}/files`]) + if (!filePath) return + const fullPath = await joinPath([userSpace, filePath]) + openFileExplorer(fullPath) + } + return { onReviewInFinder, onViewJson, onViewFile, + onViewFileContainer, } } diff --git a/web/hooks/useRecommendedModel.ts b/web/hooks/useRecommendedModel.ts index 582c8a949..427d2bf73 100644 --- a/web/hooks/useRecommendedModel.ts +++ b/web/hooks/useRecommendedModel.ts @@ -26,7 +26,6 @@ export default function useRecommendedModel() { const activeModel = useAtomValue(activeModelAtom) const [downloadedModels, setDownloadedModels] = useState([]) const [recommendedModel, setRecommendedModel] = useState() - const threadStates = useAtomValue(threadStatesAtom) const activeThread = useAtomValue(activeThreadAtom) const getAndSortDownloadedModels = useCallback(async (): Promise => { @@ -44,27 +43,11 @@ export default function useRecommendedModel() { > => { const models = await getAndSortDownloadedModels() if (!activeThread) return + const modelId = activeThread.assistants[0]?.model.id + const model = models.find((model) => model.id === modelId) - const finishInit = threadStates[activeThread.id].isFinishInit ?? true - if (finishInit) { - const modelId = activeThread.assistants[0]?.model.id - const model = models.find((model) => model.id === modelId) - - if (model) { - setRecommendedModel(model) - } - - return - } else { - const modelId = activeThread.assistants[0]?.model.id - if (modelId !== '*') { - const model = models.find((model) => model.id === modelId) - - if (model) { - setRecommendedModel(model) - } - return - } + if (model) { + setRecommendedModel(model) } if (activeModel) { diff --git a/web/hooks/useSendChatMessage.ts b/web/hooks/useSendChatMessage.ts index 835bdfed4..379defa15 100644 --- a/web/hooks/useSendChatMessage.ts +++ b/web/hooks/useSendChatMessage.ts @@ -41,9 +41,7 @@ import { activeThreadAtom, engineParamsUpdateAtom, getActiveThreadModelParamsAtom, - threadStatesAtom, updateThreadAtom, - updateThreadInitSuccessAtom, updateThreadWaitingForResponseAtom, } from '@/helpers/atoms/Thread.atom' @@ -64,8 +62,6 @@ export default function useSendChatMessage() { const setQueuedMessage = useSetAtom(queuedMessageAtom) const modelRef = useRef() - const threadStates = useAtomValue(threadStatesAtom) - const updateThreadInitSuccess = useSetAtom(updateThreadInitSuccessAtom) const activeModelParams = useAtomValue(getActiveThreadModelParamsAtom) const engineParamsUpdate = useAtomValue(engineParamsUpdateAtom) @@ -150,52 +146,10 @@ export default function useSendChatMessage() { if (engineParamsUpdate) setReloadModel(true) - const activeThreadState = threadStates[activeThread.id] const runtimeParams = toRuntimeParams(activeModelParams) const settingParams = toSettingParams(activeModelParams) - // if the thread is not initialized, we need to initialize it first - if ( - !activeThreadState.isFinishInit || - activeThread.assistants[0].model.id !== selectedModel?.id - ) { - if (!selectedModel) { - toaster({ title: 'Please select a model', type: 'warning' }) - return - } - const assistantId = activeThread.assistants[0].assistant_id ?? '' - const assistantName = activeThread.assistants[0].assistant_name ?? '' - const instructions = activeThread.assistants[0].instructions ?? '' - const tools = activeThread.assistants[0].tools ?? [] - - const initThread: Thread = { - ...activeThread, - assistants: [ - { - assistant_id: assistantId, - assistant_name: assistantName, - instructions: instructions, - tools: tools, - model: { - id: selectedModel.id, - settings: settingParams, - parameters: runtimeParams, - engine: selectedModel.engine, - }, - }, - ], - } - - updateThreadInitSuccess(activeThread.id) - updateThread(initThread) - - await extensionManager - .get(ExtensionTypeEnum.Conversational) - ?.saveThread(initThread) - } - updateThreadWaiting(activeThread.id, true) - const prompt = message.trim() setCurrentPrompt('') diff --git a/web/hooks/useThreads.ts b/web/hooks/useThreads.ts index 44be485fe..b7de014cc 100644 --- a/web/hooks/useThreads.ts +++ b/web/hooks/useThreads.ts @@ -5,7 +5,7 @@ import { ConversationalExtension, } from '@janhq/core' -import { useAtom, useAtomValue } from 'jotai' +import { useAtomValue, useSetAtom } from 'jotai' import useSetActiveThread from './useSetActiveThread' @@ -19,11 +19,9 @@ import { } from '@/helpers/atoms/Thread.atom' const useThreads = () => { - const [threadStates, setThreadStates] = useAtom(threadStatesAtom) - const [threads, setThreads] = useAtom(threadsAtom) - const [threadModelRuntimeParams, setThreadModelRuntimeParams] = useAtom( - threadModelParamsAtom - ) + const setThreadStates = useSetAtom(threadStatesAtom) + const setThreads = useSetAtom(threadsAtom) + const setThreadModelRuntimeParams = useSetAtom(threadModelParamsAtom) const activeThread = useAtomValue(activeThreadAtom) const { setActiveThread } = useSetActiveThread() @@ -41,7 +39,6 @@ const useThreads = () => { hasMore: false, waitingForResponse: false, lastMessage, - isFinishInit: true, } const modelParams = thread.assistants?.[0]?.model?.parameters @@ -53,41 +50,12 @@ const useThreads = () => { } }) - // allow at max 1 unfinished init thread and it should be at the top of the list - let unfinishedThreadId: string | undefined = undefined - const unfinishedThreadState: Record = {} - - for (const key of Object.keys(threadStates)) { - const threadState = threadStates[key] - if (threadState.isFinishInit === false) { - unfinishedThreadState[key] = threadState - unfinishedThreadId = key - break - } - } - const unfinishedThread: Thread | undefined = threads.find( - (thread) => thread.id === unfinishedThreadId - ) - - let allThreads: Thread[] = [...localThreads] - if (unfinishedThread) { - allThreads = [unfinishedThread, ...localThreads] - } - - if (unfinishedThreadId) { - localThreadStates[unfinishedThreadId] = - unfinishedThreadState[unfinishedThreadId] - - threadModelParams[unfinishedThreadId] = - threadModelRuntimeParams[unfinishedThreadId] - } - // updating app states setThreadStates(localThreadStates) - setThreads(allThreads) + setThreads(localThreads) setThreadModelRuntimeParams(threadModelParams) - if (allThreads.length && !activeThread) { - setActiveThread(allThreads[0]) + if (localThreads.length && !activeThread) { + setActiveThread(localThreads[0]) } } catch (error) { console.error(error) diff --git a/web/hooks/useUpdateModelParameters.ts b/web/hooks/useUpdateModelParameters.ts index 80070ef26..694394cee 100644 --- a/web/hooks/useUpdateModelParameters.ts +++ b/web/hooks/useUpdateModelParameters.ts @@ -2,12 +2,15 @@ import { ConversationalExtension, ExtensionTypeEnum, + InferenceEngine, Thread, ThreadAssistantInfo, } from '@janhq/core' import { useAtomValue, useSetAtom } from 'jotai' +import { selectedModelAtom } from '@/containers/DropdownListSidebar' + import { toRuntimeParams, toSettingParams } from '@/utils/modelParam' import { extensionManager } from '@/extension' @@ -19,16 +22,22 @@ import { threadsAtom, } from '@/helpers/atoms/Thread.atom' +export type UpdateModelParameter = { + params?: ModelParams + modelId?: string + engine?: InferenceEngine +} + export default function useUpdateModelParameters() { const threads = useAtomValue(threadsAtom) const setThreadModelParams = useSetAtom(setThreadModelParamsAtom) const activeThreadState = useAtomValue(activeThreadStateAtom) const activeModelParams = useAtomValue(getActiveThreadModelParamsAtom) + const selectedModel = useAtomValue(selectedModelAtom) const updateModelParameter = async ( threadId: string, - name: string, - value: number | boolean | string + settings: UpdateModelParameter ) => { const thread = threads.find((thread) => thread.id === threadId) if (!thread) { @@ -40,21 +49,18 @@ export default function useUpdateModelParameters() { console.error('No active thread') return } + + const params = settings.modelId + ? settings.params + : { ...activeModelParams, ...settings.params } + const updatedModelParams: ModelParams = { - ...activeModelParams, - // Explicitly set the value to an array if the name is 'stop' - // This is because the inference engine would only accept an array for the 'stop' parameter - [name]: name === 'stop' ? (value === '' ? [] : [value]) : value, + ...params, } // update the state setThreadModelParams(thread.id, updatedModelParams) - if (!activeThreadState.isFinishInit) { - // if thread is not initialized, we don't need to update thread.json - return - } - const assistants = thread.assistants.map( (assistant: ThreadAssistantInfo) => { const runtimeParams = toRuntimeParams(updatedModelParams) @@ -62,6 +68,10 @@ export default function useUpdateModelParameters() { assistant.model.parameters = runtimeParams assistant.model.settings = settingParams + if (selectedModel) { + assistant.model.id = settings.modelId ?? selectedModel?.id + assistant.model.engine = settings.engine ?? selectedModel?.engine + } return assistant } ) diff --git a/web/screens/Chat/ModelSetting/SettingComponent.tsx b/web/screens/Chat/ModelSetting/SettingComponent.tsx index bb91d47e7..e2e43e944 100644 --- a/web/screens/Chat/ModelSetting/SettingComponent.tsx +++ b/web/screens/Chat/ModelSetting/SettingComponent.tsx @@ -56,7 +56,7 @@ const SettingComponent = ({ updater?: ( threadId: string, name: string, - value: string | number | boolean + value: string | number | boolean | string[] ) => void }) => { const { updateModelParameter } = useUpdateModelParameters() @@ -73,7 +73,10 @@ const SettingComponent = ({ const { stopModel } = useActiveModel() - const onValueChanged = (name: string, value: string | number | boolean) => { + const onValueChanged = ( + name: string, + value: string | number | boolean | string[] + ) => { if (!threadId) return if (engineParams.some((x) => x.name.includes(name))) { setEngineParamsUpdate(true) @@ -83,7 +86,13 @@ const SettingComponent = ({ } if (updater) updater(threadId, name, value) else { - updateModelParameter(threadId, name, value) + // Convert stop string to array + if (name === 'stop' && typeof value === 'string') { + value = [value] + } + updateModelParameter(threadId, { + params: { [name]: value }, + }) } } diff --git a/web/screens/Chat/SimpleTextMessage/index.tsx b/web/screens/Chat/SimpleTextMessage/index.tsx index feed96168..261bb3497 100644 --- a/web/screens/Chat/SimpleTextMessage/index.tsx +++ b/web/screens/Chat/SimpleTextMessage/index.tsx @@ -43,7 +43,7 @@ const SimpleTextMessage: React.FC = (props) => { text = props.content[0]?.text?.value ?? '' } const clipboard = useClipboard({ timeout: 1000 }) - const { onViewFile } = usePath() + const { onViewFile, onViewFileContainer } = usePath() const marked: Marked = new Marked( markedHighlight({ @@ -200,13 +200,14 @@ const SimpleTextMessage: React.FC = (props) => { className="aspect-auto h-[300px]" alt={props.content[0]?.text.name} src={props.content[0]?.text.annotations[0]} + onClick={() => onViewFile(`${props.id}.png`)} />
onViewFile(`${props.id}.png`)} + onClick={onViewFileContainer} >
@@ -223,14 +224,17 @@ const SimpleTextMessage: React.FC = (props) => { {props.content[0]?.type === ContentType.Pdf && (
-
+
+ onViewFile(`${props.id}.${props.content[0]?.type}`) + } + />
- onViewFile(`${props.id}.${props.content[0]?.type}`) - } + onClick={onViewFileContainer} >
diff --git a/web/screens/Settings/Advanced/index.tsx b/web/screens/Settings/Advanced/index.tsx index 5ddf4ecf4..109431515 100644 --- a/web/screens/Settings/Advanced/index.tsx +++ b/web/screens/Settings/Advanced/index.tsx @@ -135,7 +135,7 @@ const Advanced = () => { )} {/* Directory */} - {experimentalFeature && } + {/* Proxy */}
From ae073d2703d79438b5b99878b8c8cbace07b40ff Mon Sep 17 00:00:00 2001 From: Service Account Date: Thu, 1 Feb 2024 06:09:29 +0000 Subject: [PATCH 56/71] janhq/jan: Update README.md with nightly build artifact URL --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index e4817f8dc..34eecc9f3 100644 --- a/README.md +++ b/README.md @@ -76,31 +76,31 @@ Jan is an open-source ChatGPT alternative that runs 100% offline on your compute Experimental (Nightly Build) - + jan.exe - + Intel - + M1/M2 - + jan.deb - + jan.AppImage From 11e2a763cbfcdf16d0bc16b687213663da6cddf0 Mon Sep 17 00:00:00 2001 From: hiro <22463238+hiro-v@users.noreply.github.com> Date: Thu, 1 Feb 2024 19:11:05 +0700 Subject: [PATCH 57/71] feat: Add default value for ngl (#1886) * fix: Add fallback value for ngl * fix: Handling type --- .../src/node/index.ts | 36 ++++++++----------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/extensions/inference-nitro-extension/src/node/index.ts b/extensions/inference-nitro-extension/src/node/index.ts index 749b68b1c..296433d42 100644 --- a/extensions/inference-nitro-extension/src/node/index.ts +++ b/extensions/inference-nitro-extension/src/node/index.ts @@ -27,15 +27,6 @@ interface ModelInitOptions { modelFullPath: string; model: Model; } - -/** - * Model setting args for Nitro model load. - */ -interface ModelSettingArgs extends ModelSettingParams { - llama_model_path: string; - cpu_threads: number; -} - // The PORT to use for the Nitro subprocess const PORT = 3928; // The HOST address to use for the Nitro subprocess @@ -58,7 +49,7 @@ let subprocess: ChildProcessWithoutNullStreams | undefined = undefined; // The current model file url let currentModelFile: string = ""; // The current model settings -let currentSettings: ModelSettingArgs | undefined = undefined; +let currentSettings: ModelSettingParams | undefined = undefined; /** * Stops a Nitro subprocess. @@ -76,7 +67,7 @@ function stopModel(): Promise { * TODO: Should pass absolute of the model file instead of just the name - So we can modurize the module.ts to npm package */ async function runModel( - wrapper: ModelInitOptions, + wrapper: ModelInitOptions ): Promise { if (wrapper.model.engine !== InferenceEngine.nitro) { // Not a nitro model @@ -94,7 +85,7 @@ async function runModel( const ggufBinFile = files.find( (file) => file === path.basename(currentModelFile) || - file.toLowerCase().includes(SUPPORTED_MODEL_FORMAT), + file.toLowerCase().includes(SUPPORTED_MODEL_FORMAT) ); if (!ggufBinFile) return Promise.reject("No GGUF model file found"); @@ -189,10 +180,10 @@ function promptTemplateConverter(promptTemplate: string): PromptTemplate { const system_prompt = promptTemplate.substring(0, systemIndex); const user_prompt = promptTemplate.substring( systemIndex + systemMarker.length, - promptIndex, + promptIndex ); const ai_prompt = promptTemplate.substring( - promptIndex + promptMarker.length, + promptIndex + promptMarker.length ); // Return the split parts @@ -202,7 +193,7 @@ function promptTemplateConverter(promptTemplate: string): PromptTemplate { const promptIndex = promptTemplate.indexOf(promptMarker); const user_prompt = promptTemplate.substring(0, promptIndex); const ai_prompt = promptTemplate.substring( - promptIndex + promptMarker.length, + promptIndex + promptMarker.length ); // Return the split parts @@ -218,6 +209,9 @@ function promptTemplateConverter(promptTemplate: string): PromptTemplate { * @returns A Promise that resolves when the model is loaded successfully, or rejects with an error message if the model is not found or fails to load. */ function loadLLMModel(settings: any): Promise { + if (!settings?.ngl) { + settings.ngl = 100; + } log(`[NITRO]::Debug: Loading model with params ${JSON.stringify(settings)}`); return fetchRetry(NITRO_HTTP_LOAD_MODEL_URL, { method: "POST", @@ -231,8 +225,8 @@ function loadLLMModel(settings: any): Promise { .then((res) => { log( `[NITRO]::Debug: Load model success with response ${JSON.stringify( - res, - )}`, + res + )}` ); return Promise.resolve(res); }) @@ -261,8 +255,8 @@ async function validateModelStatus(): Promise { }).then(async (res: Response) => { log( `[NITRO]::Debug: Validate model state success with response ${JSON.stringify( - res, - )}`, + res + )}` ); // If the response is OK, check model_loaded status. if (res.ok) { @@ -313,7 +307,7 @@ function spawnNitroProcess(): Promise { const args: string[] = ["1", LOCAL_HOST, PORT.toString()]; // Execute the binary log( - `[NITRO]::Debug: Spawn nitro at path: ${executableOptions.executablePath}, and args: ${args}`, + `[NITRO]::Debug: Spawn nitro at path: ${executableOptions.executablePath}, and args: ${args}` ); subprocess = spawn( executableOptions.executablePath, @@ -324,7 +318,7 @@ function spawnNitroProcess(): Promise { ...process.env, CUDA_VISIBLE_DEVICES: executableOptions.cudaVisibleDevices, }, - }, + } ); // Handle subprocess output From 36cd5988d42a5f7104b8fd2724ee92b77377bb78 Mon Sep 17 00:00:00 2001 From: Hieu <150573299+hieu-jan@users.noreply.github.com> Date: Thu, 1 Feb 2024 23:00:16 +0900 Subject: [PATCH 58/71] feat: integrate umami (#1809) * feat: integrate umami * fix: linter issue * fix: run eslint * fix window umami null * fix property type error * fix: check configuration before requesting analytics script * fix: test cases --------- Co-authored-by: Louis --- .../workflows/template-build-linux-x64.yml | 4 +- .github/workflows/template-build-macos.yml | 4 +- .../workflows/template-build-windows-x64.yml | 4 +- docs/.env.example | 4 +- electron/tests/hub.e2e.spec.ts | 1 - electron/tests/navigation.e2e.spec.ts | 8 ++- electron/tests/settings.e2e.spec.ts | 3 +- web/containers/Providers/index.tsx | 39 +++++------ web/next.config.js | 6 +- web/screens/LocalServer/index.tsx | 2 +- web/utils/posthog.ts | 50 -------------- web/utils/umami.tsx | 65 +++++++++++++++++++ 12 files changed, 101 insertions(+), 89 deletions(-) delete mode 100644 web/utils/posthog.ts create mode 100644 web/utils/umami.tsx diff --git a/.github/workflows/template-build-linux-x64.yml b/.github/workflows/template-build-linux-x64.yml index c6d1eac97..08cb1dada 100644 --- a/.github/workflows/template-build-linux-x64.yml +++ b/.github/workflows/template-build-linux-x64.yml @@ -98,8 +98,8 @@ jobs: make build-and-publish env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - ANALYTICS_ID: ${{ secrets.JAN_APP_POSTHOG_PROJECT_API_KEY }} - ANALYTICS_HOST: ${{ secrets.JAN_APP_POSTHOG_URL }} + ANALYTICS_ID: ${{ secrets.JAN_APP_UMAMI_PROJECT_API_KEY }} + ANALYTICS_HOST: ${{ secrets.JAN_APP_UMAMI_URL }} - name: Upload Artifact .deb file if: inputs.public_provider != 'github' diff --git a/.github/workflows/template-build-macos.yml b/.github/workflows/template-build-macos.yml index bc48e6c21..0ad1d3a6a 100644 --- a/.github/workflows/template-build-macos.yml +++ b/.github/workflows/template-build-macos.yml @@ -137,8 +137,8 @@ jobs: APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} APP_PATH: "." DEVELOPER_ID: ${{ secrets.DEVELOPER_ID }} - ANALYTICS_ID: ${{ secrets.JAN_APP_POSTHOG_PROJECT_API_KEY }} - ANALYTICS_HOST: ${{ secrets.JAN_APP_POSTHOG_URL }} + ANALYTICS_ID: ${{ secrets.JAN_APP_UMAMI_PROJECT_API_KEY }} + ANALYTICS_HOST: ${{ secrets.JAN_APP_UMAMI_URL }} - name: Upload Artifact if: inputs.public_provider != 'github' diff --git a/.github/workflows/template-build-windows-x64.yml b/.github/workflows/template-build-windows-x64.yml index 5d96b3f49..b81997bde 100644 --- a/.github/workflows/template-build-windows-x64.yml +++ b/.github/workflows/template-build-windows-x64.yml @@ -127,8 +127,8 @@ jobs: make build-and-publish env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - ANALYTICS_ID: ${{ secrets.JAN_APP_POSTHOG_PROJECT_API_KEY }} - ANALYTICS_HOST: ${{ secrets.JAN_APP_POSTHOG_URL }} + ANALYTICS_ID: ${{ secrets.JAN_APP_UMAMI_PROJECT_API_KEY }} + ANALYTICS_HOST: ${{ secrets.JAN_APP_UMAMI_URL }} AZURE_KEY_VAULT_URI: ${{ secrets.AZURE_KEY_VAULT_URI }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} diff --git a/docs/.env.example b/docs/.env.example index 6755f2520..b4a7fa5f1 100644 --- a/docs/.env.example +++ b/docs/.env.example @@ -1,5 +1,5 @@ GTM_ID=xxxx -POSTHOG_PROJECT_API_KEY=xxxx -POSTHOG_APP_URL=xxxx +UMAMI_PROJECT_API_KEY=xxxx +UMAMI_APP_URL=xxxx ALGOLIA_API_KEY=xxxx ALGOLIA_APP_ID=xxxx \ No newline at end of file diff --git a/electron/tests/hub.e2e.spec.ts b/electron/tests/hub.e2e.spec.ts index 6bfe45ac4..cc72e037e 100644 --- a/electron/tests/hub.e2e.spec.ts +++ b/electron/tests/hub.e2e.spec.ts @@ -38,7 +38,6 @@ test.afterAll(async () => { }) test('explores hub', async () => { - // Set the timeout for this test to 60 seconds test.setTimeout(TIMEOUT) await page.getByTestId('Hub').first().click({ timeout: TIMEOUT, diff --git a/electron/tests/navigation.e2e.spec.ts b/electron/tests/navigation.e2e.spec.ts index 2066fa60a..5c8721c2f 100644 --- a/electron/tests/navigation.e2e.spec.ts +++ b/electron/tests/navigation.e2e.spec.ts @@ -38,6 +38,7 @@ test.afterAll(async () => { }) test('renders left navigation panel', async () => { + test.setTimeout(TIMEOUT) const systemMonitorBtn = await page .getByTestId('System Monitor') .first() @@ -50,8 +51,11 @@ test('renders left navigation panel', async () => { .isEnabled({ timeout: TIMEOUT }) expect([systemMonitorBtn, settingsBtn].filter((e) => !e).length).toBe(0) // Chat section should be there - const apiServer = await page.getByTestId('Local API Server').first() - expect(apiServer).toBeVisible({ + await page.getByTestId('Local API Server').first().click({ + timeout: TIMEOUT, + }) + const localServer = await page.getByTestId('local-server-testid').first() + await expect(localServer).toBeVisible({ timeout: TIMEOUT, }) }) diff --git a/electron/tests/settings.e2e.spec.ts b/electron/tests/settings.e2e.spec.ts index 765c3cba7..ad2d7b4a4 100644 --- a/electron/tests/settings.e2e.spec.ts +++ b/electron/tests/settings.e2e.spec.ts @@ -38,7 +38,8 @@ test.afterAll(async () => { }) test('shows settings', async () => { + test.setTimeout(TIMEOUT) await page.getByTestId('Settings').first().click({ timeout: TIMEOUT }) const settingDescription = page.getByTestId('testid-setting-description') - expect(settingDescription).toBeVisible({ timeout: TIMEOUT }) + await expect(settingDescription).toBeVisible({ timeout: TIMEOUT }) }) diff --git a/web/containers/Providers/index.tsx b/web/containers/Providers/index.tsx index dd9069a95..895c22177 100644 --- a/web/containers/Providers/index.tsx +++ b/web/containers/Providers/index.tsx @@ -6,8 +6,6 @@ import { Toaster } from 'react-hot-toast' import { TooltipProvider } from '@janhq/uikit' -import { PostHogProvider } from 'posthog-js/react' - import GPUDriverPrompt from '@/containers/GPUDriverPromptModal' import EventListenerWrapper from '@/containers/Providers/EventListener' import JotaiWrapper from '@/containers/Providers/Jotai' @@ -21,7 +19,7 @@ import { setupBaseExtensions, } from '@/services/extensionService' -import { instance } from '@/utils/posthog' +import Umami from '@/utils/umami' import KeyListener from './KeyListener' @@ -70,25 +68,22 @@ const Providers = (props: PropsWithChildren) => { }, [setupCore]) return ( - - - - {setupCore && activated && ( - - - - - {children} - - {!isMac && } - - - - - )} - - - + + + + {setupCore && activated && ( + + + + {children} + {!isMac && } + + + + + )} + + ) } diff --git a/web/next.config.js b/web/next.config.js index 0b6a8bc92..a2e202c51 100644 --- a/web/next.config.js +++ b/web/next.config.js @@ -25,10 +25,8 @@ const nextConfig = { ...config.plugins, new webpack.DefinePlugin({ VERSION: JSON.stringify(packageJson.version), - ANALYTICS_ID: - JSON.stringify(process.env.ANALYTICS_ID) ?? JSON.stringify('xxx'), - ANALYTICS_HOST: - JSON.stringify(process.env.ANALYTICS_HOST) ?? JSON.stringify('xxx'), + ANALYTICS_ID: JSON.stringify(process.env.ANALYTICS_ID), + ANALYTICS_HOST: JSON.stringify(process.env.ANALYTICS_HOST), API_BASE_URL: JSON.stringify('http://localhost:1337'), isMac: process.platform === 'darwin', isWindows: process.platform === 'win32', diff --git a/web/screens/LocalServer/index.tsx b/web/screens/LocalServer/index.tsx index e7f3c7fc2..b96f4c228 100644 --- a/web/screens/LocalServer/index.tsx +++ b/web/screens/LocalServer/index.tsx @@ -103,7 +103,7 @@ const LocalServerScreen = () => { }, [handleChangePort, port]) return ( -
+
{/* Left SideBar */}
diff --git a/web/utils/posthog.ts b/web/utils/posthog.ts deleted file mode 100644 index 9bcbaa8ce..000000000 --- a/web/utils/posthog.ts +++ /dev/null @@ -1,50 +0,0 @@ -import posthog, { Properties } from 'posthog-js' - -// Initialize PostHog -posthog.init(ANALYTICS_ID, { - api_host: ANALYTICS_HOST, - autocapture: false, - capture_pageview: false, - capture_pageleave: false, - rageclick: false, -}) -// Export the PostHog instance -export const instance = posthog - -// Enum for Analytics Events -export enum AnalyticsEvent { - Ping = 'Ping', -} - -// Function to determine the operating system -function getOperatingSystem(): string { - if (isMac) return 'MacOS' - if (isWindows) return 'Windows' - if (isLinux) return 'Linux' - return 'Unknown' -} - -function captureAppVersionAndOS() { - const properties: Properties = { - $appVersion: VERSION, - $userOperatingSystem: getOperatingSystem(), - // Set the following Posthog default properties to empty strings - $initial_browser: '', - $browser: '', - $initial_browser_version: '', - $browser_version: '', - $initial_current_url: '', - $current_url: '', - $initial_device_type: '', - $device_type: '', - $initial_pathname: '', - $pathname: '', - $initial_referrer: '', - $referrer: '', - $initial_referring_domain: '', - $referring_domain: '', - } - posthog.capture(AnalyticsEvent.Ping, properties) -} - -captureAppVersionAndOS() diff --git a/web/utils/umami.tsx b/web/utils/umami.tsx new file mode 100644 index 000000000..ac9e70304 --- /dev/null +++ b/web/utils/umami.tsx @@ -0,0 +1,65 @@ +import { useEffect } from 'react' + +import Script from 'next/script' + +// Define the type for the umami data object +interface UmamiData { + version: string +} + +declare global { + interface Window { + umami: + | { + track: (event: string, data?: UmamiData) => void + } + | undefined + } +} + +const Umami = () => { + const appVersion = VERSION + const analyticsHost = ANALYTICS_HOST + const analyticsId = ANALYTICS_ID + + useEffect(() => { + if (!appVersion || !analyticsHost || !analyticsId) return + const ping = () => { + // Check if umami is defined before ping + if (window.umami !== null && typeof window.umami !== 'undefined') { + window.umami.track(appVersion, { + version: appVersion, + }) + } + } + + // Wait for umami to be defined before ping + if (window.umami !== null && typeof window.umami !== 'undefined') { + ping() + } else { + // Listen for umami script load event + document.addEventListener('umami:loaded', ping) + } + + // Cleanup function to remove event listener if the component unmounts + return () => { + document.removeEventListener('umami:loaded', ping) + } + }, [appVersion, analyticsHost, analyticsId]) + + return ( + <> + {appVersion && analyticsHost && analyticsId && ( +