From 5f65d007d91a441634634f6fb787bb1e56bad802 Mon Sep 17 00:00:00 2001 From: Louis Date: Thu, 25 Jan 2024 11:26:55 +0700 Subject: [PATCH 001/104] 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 002/104] 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 045e544007bd24a36c0a4940ef9c2b33bee5d9ec Mon Sep 17 00:00:00 2001 From: avb-is-me <104213687+avb-is-me@users.noreply.github.com> Date: Thu, 25 Jan 2024 06:14:47 +0000 Subject: [PATCH 003/104] adds devcontainer --- .devcontainer/devcontainer.json | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..f980b9df7 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,4 @@ +{ + "name": "jan", + "image": "node:20" +} \ No newline at end of file From f73e7388e2597ce8e4839057db3a2a9fd820e984 Mon Sep 17 00:00:00 2001 From: Hien To Date: Thu, 25 Jan 2024 14:53:44 +0700 Subject: [PATCH 004/104] 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 005/104] 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 006/104] 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 007/104] 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 008/104] 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 009/104] 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 010/104] 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 011/104] 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 012/104] 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 b2b1479fc34a34a10129a765ac37906e86d7ee54 Mon Sep 17 00:00:00 2001 From: copyhold Date: Thu, 25 Jan 2024 16:38:32 +0200 Subject: [PATCH 013/104] Added focus --- web/screens/Chat/index.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/web/screens/Chat/index.tsx b/web/screens/Chat/index.tsx index 684027e49..f86460f1c 100644 --- a/web/screens/Chat/index.tsx +++ b/web/screens/Chat/index.tsx @@ -74,6 +74,9 @@ const ChatScreen = () => { } useEffect(() => { + if (textareaRef.current) { + textareaRef.current.focus() + } if (isWaitingToSend && activeThreadId) { setIsWaitingToSend(false) sendChatMessage() @@ -85,6 +88,7 @@ const ChatScreen = () => { if (textareaRef.current) { textareaRef.current.style.height = '40px' textareaRef.current.style.height = textareaRef.current.scrollHeight + 'px' + textareaRef.current.focus() } }, [currentPrompt]) From 7ed523e18343270003e5e2904521e6c7950adcec Mon Sep 17 00:00:00 2001 From: Louis Date: Thu, 25 Jan 2024 22:19:28 +0700 Subject: [PATCH 014/104] 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 015/104] 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 a78e183eeb71825de3341fb11ac7719dd6bb1ac8 Mon Sep 17 00:00:00 2001 From: Service Account Date: Thu, 25 Jan 2024 15:36:30 +0000 Subject: [PATCH 016/104] 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 2722a2870..fb11cfbde 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 e8796a7ad860536a91fb45f56a2cb181bbaa3b81 Mon Sep 17 00:00:00 2001 From: Service Account Date: Thu, 25 Jan 2024 15:43:16 +0000 Subject: [PATCH 017/104] 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 fb11cfbde..53558185c 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 663bf0c9027491632bd464073c94d750b3063bb7 Mon Sep 17 00:00:00 2001 From: NamH Date: Thu, 25 Jan 2024 23:14:24 +0700 Subject: [PATCH 018/104] 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 b0d63c988d6fff570130e5b4fcba471b7951c107 Mon Sep 17 00:00:00 2001 From: Service Account Date: Thu, 25 Jan 2024 16:30:09 +0000 Subject: [PATCH 019/104] 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 53558185c..8811dbe0c 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 a52d81c79a979b234882c468ad056049f7c8d757 Mon Sep 17 00:00:00 2001 From: Service Account Date: Thu, 25 Jan 2024 20:16:35 +0000 Subject: [PATCH 020/104] 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 8811dbe0c..9183b0652 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 99025cb02f6affabd2c4786b6e5247e5177ffc37 Mon Sep 17 00:00:00 2001 From: hiento09 <136591877+hiento09@users.noreply.github.com> Date: Fri, 26 Jan 2024 09:24:09 +0700 Subject: [PATCH 021/104] CI nightly update trigger to 3AM UTC +7 Tuesday, Wednesday, and Thursday (#1798) Co-authored-by: Hien To --- .github/workflows/jan-electron-build-nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/jan-electron-build-nightly.yml b/.github/workflows/jan-electron-build-nightly.yml index f961ccd6f..08b6ad476 100644 --- a/.github/workflows/jan-electron-build-nightly.yml +++ b/.github/workflows/jan-electron-build-nightly.yml @@ -2,7 +2,7 @@ name: Jan Build Electron App Nightly or Manual on: schedule: - - cron: '0 20 * * 2,3,4' # At 8 PM UTC on Tuesday, Wednesday, and Thursday, which is 3 AM UTC+7 + - cron: '0 20 * * 1,2,3' # At 8 PM UTC on Monday, Tuesday, and Wednesday which is 3 AM UTC+7 Tuesday, Wednesday, and Thursday workflow_dispatch: inputs: public_provider: From 85b58eb82400cb9d75af4f16ca6e1b4ef96c8224 Mon Sep 17 00:00:00 2001 From: Service Account Date: Fri, 26 Jan 2024 04:00:07 +0000 Subject: [PATCH 022/104] 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 9183b0652..770b88266 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 143e76a0dedcf49fb3d32a9715fc3958aaad6907 Mon Sep 17 00:00:00 2001 From: NamH Date: Fri, 26 Jan 2024 12:19:02 +0700 Subject: [PATCH 023/104] chore: clean up code and remove some lint suppression (#1777) * chore: clean up code and remove some lint suppression Signed-off-by: James * Update .gitignore --------- Signed-off-by: James Co-authored-by: James Co-authored-by: Louis --- .gitignore | 3 + extensions/assistant-extension/src/index.ts | 3 +- extensions/monitoring-extension/src/index.ts | 3 +- web/containers/Layout/index.tsx | 2 +- web/containers/Providers/EventHandler.tsx | 118 +++++++++++-------- web/containers/Providers/EventListener.tsx | 16 +-- web/hooks/useGetConfiguredModels.ts | 23 ++-- web/screens/Chat/MessageToolbar/index.tsx | 2 +- 8 files changed, 96 insertions(+), 74 deletions(-) diff --git a/.gitignore b/.gitignore index e3e4635fc..4540e5c7a 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,8 @@ build electron/renderer electron/models electron/docs +electron/engines +server/pre-install package-lock.json *.log @@ -26,3 +28,4 @@ extensions/inference-nitro-extension/bin/*/*.exp extensions/inference-nitro-extension/bin/*/*.lib extensions/inference-nitro-extension/bin/saved-* extensions/inference-nitro-extension/bin/*.tar.gz + diff --git a/extensions/assistant-extension/src/index.ts b/extensions/assistant-extension/src/index.ts index 96de33b7b..098ab1f54 100644 --- a/extensions/assistant-extension/src/index.ts +++ b/extensions/assistant-extension/src/index.ts @@ -1,5 +1,4 @@ -import { fs, Assistant } from "@janhq/core"; -import { AssistantExtension } from "@janhq/core"; +import { fs, Assistant, AssistantExtension } from "@janhq/core"; import { join } from "path"; export default class JanAssistantExtension extends AssistantExtension { diff --git a/extensions/monitoring-extension/src/index.ts b/extensions/monitoring-extension/src/index.ts index d3f20b437..9297a770f 100644 --- a/extensions/monitoring-extension/src/index.ts +++ b/extensions/monitoring-extension/src/index.ts @@ -1,5 +1,4 @@ -import { MonitoringExtension } from "@janhq/core"; -import { executeOnMain } from "@janhq/core"; +import { MonitoringExtension, executeOnMain } from "@janhq/core"; /** * JanMonitoringExtension is a extension that provides system monitoring functionality. diff --git a/web/containers/Layout/index.tsx b/web/containers/Layout/index.tsx index 54a7845a4..e7bde49c0 100644 --- a/web/containers/Layout/index.tsx +++ b/web/containers/Layout/index.tsx @@ -28,7 +28,7 @@ const BaseLayout = (props: PropsWithChildren) => { if (localStorage.getItem(SUCCESS_SET_NEW_DESTINATION) === 'true') { setMainViewState(MainViewState.Settings) } - }, []) + }, [setMainViewState]) return (
diff --git a/web/containers/Providers/EventHandler.tsx b/web/containers/Providers/EventHandler.tsx index 1f9d6d7af..114370359 100644 --- a/web/containers/Providers/EventHandler.tsx +++ b/web/containers/Providers/EventHandler.tsx @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { ReactNode, useEffect, useRef } from 'react' +import { ReactNode, useCallback, useEffect, useRef } from 'react' import { events, @@ -48,48 +48,61 @@ export default function EventHandler({ children }: { children: ReactNode }) { modelsRef.current = downloadedModels }, [downloadedModels]) - async function handleNewMessageResponse(message: ThreadMessage) { - addNewMessage(message) - } + const onNewMessageResponse = useCallback( + (message: ThreadMessage) => { + addNewMessage(message) + }, + [addNewMessage] + ) - async function handleModelReady(model: Model) { - setActiveModel(model) - toaster({ - title: 'Success!', - description: `Model ${model.id} has been started.`, - }) - setStateModel(() => ({ - state: 'stop', - loading: false, - model: model.id, - })) - } + const onModelReady = useCallback( + (model: Model) => { + setActiveModel(model) + toaster({ + title: 'Success!', + description: `Model ${model.id} has been started.`, + }) + setStateModel(() => ({ + state: 'stop', + loading: false, + model: model.id, + })) + }, + [setActiveModel, setStateModel] + ) - async function handleModelStopped() { - setTimeout(async () => { + const onModelStopped = useCallback(() => { + setTimeout(() => { setActiveModel(undefined) setStateModel({ state: 'start', loading: false, model: '' }) }, 500) - } + }, [setActiveModel, setStateModel]) - async function handleModelFail(res: any) { - const errorMessage = `${res.error}` - alert(errorMessage) - setStateModel(() => ({ - state: 'start', - loading: false, - model: res.modelId, - })) - } + const onModelInitFailed = useCallback( + (res: any) => { + const errorMessage = `${res.error}` + console.error('Failed to load model: ' + errorMessage) + setStateModel(() => ({ + state: 'start', + loading: false, + model: res.modelId, + })) + }, + [setStateModel] + ) + + const onMessageResponseUpdate = useCallback( + (message: ThreadMessage) => { + updateMessage( + message.id, + message.thread_id, + message.content, + message.status + ) + if (message.status === MessageStatus.Pending) { + return + } - async function handleMessageResponseUpdate(message: ThreadMessage) { - updateMessage( - message.id, - message.thread_id, - message.content, - message.status - ) - if (message.status !== MessageStatus.Pending) { // Mark the thread as not waiting for response updateThreadWaiting(message.thread_id, false) @@ -111,26 +124,33 @@ export default function EventHandler({ children }: { children: ReactNode }) { .get(ExtensionTypeEnum.Conversational) ?.addNewMessage(message) } - } - } + }, + [updateMessage, updateThreadWaiting] + ) useEffect(() => { + console.log('Registering events') if (window.core?.events) { - events.on(MessageEvent.OnMessageResponse, handleNewMessageResponse) - events.on(MessageEvent.OnMessageUpdate, handleMessageResponseUpdate) - events.on(ModelEvent.OnModelReady, handleModelReady) - events.on(ModelEvent.OnModelFail, handleModelFail) - events.on(ModelEvent.OnModelStopped, handleModelStopped) + events.on(MessageEvent.OnMessageResponse, onNewMessageResponse) + events.on(MessageEvent.OnMessageUpdate, onMessageResponseUpdate) + + events.on(ModelEvent.OnModelReady, onModelReady) + events.on(ModelEvent.OnModelFail, onModelInitFailed) + events.on(ModelEvent.OnModelStopped, onModelStopped) } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) + }, [ + onNewMessageResponse, + onMessageResponseUpdate, + onModelReady, + onModelInitFailed, + onModelStopped, + ]) useEffect(() => { return () => { - events.off(MessageEvent.OnMessageResponse, handleNewMessageResponse) - events.off(MessageEvent.OnMessageUpdate, handleMessageResponseUpdate) + events.off(MessageEvent.OnMessageResponse, onNewMessageResponse) + events.off(MessageEvent.OnMessageUpdate, onMessageResponseUpdate) } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) + }, [onNewMessageResponse, onMessageResponseUpdate]) return <>{children} } diff --git a/web/containers/Providers/EventListener.tsx b/web/containers/Providers/EventListener.tsx index 2816c88e2..62d4cacb6 100644 --- a/web/containers/Providers/EventListener.tsx +++ b/web/containers/Providers/EventListener.tsx @@ -105,12 +105,14 @@ export default function EventListenerWrapper({ children }: PropsWithChildren) { }) } return () => {} - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) + }, [ + setDownloadState, + setDownloadStateCancelled, + setDownloadStateFailed, + setDownloadStateSuccess, + setDownloadedModels, + setProgress, + ]) - return ( -
- {children} -
- ) + return {children} } diff --git a/web/hooks/useGetConfiguredModels.ts b/web/hooks/useGetConfiguredModels.ts index 919f43754..8be052ae2 100644 --- a/web/hooks/useGetConfiguredModels.ts +++ b/web/hooks/useGetConfiguredModels.ts @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' import { ExtensionTypeEnum, ModelExtension, Model } from '@janhq/core' @@ -8,24 +8,23 @@ export function useGetConfiguredModels() { const [loading, setLoading] = useState(false) const [models, setModels] = useState([]) - const getConfiguredModels = async (): Promise => { - const models = await extensionManager - .get(ExtensionTypeEnum.Model) - ?.getConfiguredModels() - return models ?? [] - } - - async function fetchModels() { + const fetchModels = useCallback(async () => { setLoading(true) const models = await getConfiguredModels() setLoading(false) setModels(models) - } + }, []) useEffect(() => { fetchModels() - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) + }, [fetchModels]) return { loading, models } } + +const getConfiguredModels = async (): Promise => { + const models = await extensionManager + .get(ExtensionTypeEnum.Model) + ?.getConfiguredModels() + return models ?? [] +} diff --git a/web/screens/Chat/MessageToolbar/index.tsx b/web/screens/Chat/MessageToolbar/index.tsx index 183eae814..dfa8d63c6 100644 --- a/web/screens/Chat/MessageToolbar/index.tsx +++ b/web/screens/Chat/MessageToolbar/index.tsx @@ -3,8 +3,8 @@ import { ExtensionTypeEnum, ThreadMessage, ChatCompletionRole, + ConversationalExtension, } from '@janhq/core' -import { ConversationalExtension } from '@janhq/core' import { useAtomValue, useSetAtom } from 'jotai' import { RefreshCcw, CopyIcon, Trash2Icon, CheckIcon } from 'lucide-react' From a7edd37bfc7c4adfe08f5ce4f349ca5403200fb5 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Fri, 26 Jan 2024 12:19:10 +0700 Subject: [PATCH 024/104] 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 ba07f92636f39ef82646d19a638c605d47c85246 Mon Sep 17 00:00:00 2001 From: Service Account Date: Fri, 26 Jan 2024 07:58:45 +0000 Subject: [PATCH 030/104] 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 36e8a171f..d7397a8da 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 532a589354c74aa17f57518d6b6a865e6cb9fa26 Mon Sep 17 00:00:00 2001 From: Louis Date: Fri, 26 Jan 2024 18:57:52 +0700 Subject: [PATCH 031/104] 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 38f757dd4d8a3463f0a53f8dcd5437681b16a7b9 Mon Sep 17 00:00:00 2001 From: Service Account Date: Fri, 26 Jan 2024 13:26:21 +0000 Subject: [PATCH 034/104] 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 e20fb5dbe..f5ce84e70 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 28e4405498c5de9558b1e39d890b17aa6689c42a Mon Sep 17 00:00:00 2001 From: hiro Date: Tue, 19 Dec 2023 23:33:27 +0700 Subject: [PATCH 035/104] feat: support RAG chore: Update new model.json with multiple binaries feat: Add updates for handling multiple model binaries chore: jan can see Update Model.json (#1005) * add(mixtral): add model.json for mixtral * archived some models + update the model.json * add(model): add pandora 10.7b * fix(model): update description * fix(model): pump vers and change the featured model to trinity * fix(model): archive neuralchat * fix(model): decapriated all old models * fix(trinity): add cover image and change description * fix(trinity): update cover png * add(pandora): cover image * fix(pandora): cover image * add(mixtral): add model.json for mixtral * archived some models + update the model.json * add(model): add pandora 10.7b * fix(model): update description * fix(model): pump vers and change the featured model to trinity * fix(model): archive neuralchat * fix(model): decapriated all old models * fix(trinity): add cover image and change description * fix(trinity): update cover png * add(pandora): cover image * fix(pandora): cover image * chore: model desc nits * fix(models): adjust the size for solars and pandoras * add(mixtral): description --------- Co-authored-by: 0xSage chore: reformat model.json and use new template fix(Model): download/abort model (#1163) * fix(Model): download/abort model * fix: image preview Signed-off-by: James --------- Signed-off-by: James Co-authored-by: James Co-authored-by: Louis add preview and reduce time re-render all chat screen Signed-off-by: James store files under thread_id/files Signed-off-by: James fix: Update llava 1.5 size fix: Nitro extension path resolver feat: Add upload preview clearance chore: Update FileType to multiple targets fix: delete file preview once new thread created chore: Add langchain import support storing pdf file Signed-off-by: James feat: add retrieval tool in node runtime fix: import module done Co-authored-by: Louis feat: Add type assistant tool chore: Add tool_retrieval_enabled to InferenceEngine chore: Add AssistantTool to thread entity chore: refactor tool retrieval base class feat: Add handler for assistant with rag enabled chore: Update inferenceEngine type properly chore: Update inferenceEngine type properly fix: Update retrieval tool chore: main entry correction refactor: tsconfig files chore: Update ModelRuntimeParams type refactor: Remove unused files fix: wip chore: remove unused console.log for FileUploadPreview fix: Update mapping correctly for engine and proxyEngine feat: Add proxyEngine to type ModelInfo fix: WIP with test route fix: Add bundleDependencies to package.json chore: remove conversational history memory fix: refactor data passing reafactor: remove unused code fix: Update module chore: export import correction fix conflict Signed-off-by: James fix: resolve after rebased fix: Update llava 1.5 model json feat: Add bakllava 1 model json refactor: node module export, ES syntax and langchain import fix: WIP fix: WIP fix: WIP fix: external module import fix: WIP Add UI attachment fot file upload Prepare Thumbnail UI image chore: rebase model folder to dev branch chore: remove multiple binaries related commits fix: remove multiple binaries related commits part 2 fix: Remove transformer.js related deps Fix truncate file name attachment remove unused code image preview attachment fix: remove multi binaries error chore: remove commented code for ModelArtifacts type Dropzone for drag and drop attachment Avoid conditional showing 0 using length fix symbol windows avoid undefined tools fix: add tool retrieval to true by default and disable the change chore: remove unused code fix: Enable nitro embedding by default fix: Update code WIP with nitro embedding chore: remove unused running function fix: assistant extension missing module feat: Retrieval ingest, query and reforward fix: Update hnswlib version conflict fix: Add tool settings fix: Update path to thread_id/memory fix: Add support for nitro embedding usage fix: RAG does not work with plain content message fix(Model): #1662 imported model does not use gpu (#1723) Signed-off-by: James Co-authored-by: James feat: allow users to update retrieval settings chore: pass thread assistant settings to assistant extensions chore: eslint fix fix bug border right panel showing in thread while not have active thread Update setting layout retrieval assistant Renaming file settingcomponent change default value in core extention add fake loader generate response fix conditional fake loader remove unused import Proper error message on file typr fix: loading indicator fix: chunk size and overlap constraint conditional drag and drop when retrieval off fix: enable retrieval middleware as soon as its tool is enabled fix: configure embedding engine according to request fix: Retrieval false by default fix: engine json chore: migrate assistant disabled collapse panel when retrieval or children null chore: remove unused log chore: Bump nitro version to 0.2.14 for batch embedding chore: remove unused console.log --- core/src/api/index.ts | 1 + core/src/fs.ts | 16 +- core/src/node/api/routes/fs.ts | 9 +- core/src/types/assistant/assistantEntity.ts | 9 +- core/src/types/inference/inferenceEntity.ts | 29 +- core/src/types/message/messageEntity.ts | 15 +- core/src/types/model/modelEntity.ts | 12 +- core/src/types/thread/index.ts | 1 + core/src/types/thread/threadEntity.ts | 2 + core/src/types/thread/threadEvent.ts | 4 + electron/handlers/fileManager.ts | 16 + electron/handlers/fs.ts | 8 +- extensions/assistant-extension/package.json | 28 +- .../assistant-extension/rollup.config.ts | 81 +++++ .../src/@types/global.d.ts | 4 +- extensions/assistant-extension/src/index.ts | 197 +++++++++++- .../assistant-extension/src/node/engine.ts | 13 + .../assistant-extension/src/node/index.ts | 39 +++ .../src/node/tools/retrieval/index.ts | 78 +++++ extensions/assistant-extension/tsconfig.json | 22 +- .../assistant-extension/webpack.config.js | 38 --- .../conversational-extension/src/index.ts | 56 +++- .../inference-nitro-extension/bin/version.txt | 2 +- .../inference-nitro-extension/package.json | 1 + .../inference-nitro-extension/src/index.ts | 14 +- .../src/node/index.ts | 23 +- .../inference-openai-extension/src/index.ts | 15 +- .../inference-openai-extension/tsconfig.json | 5 +- .../tsconfig.json | 5 +- web/containers/CardSidebar/index.tsx | 44 +-- web/containers/Checkbox/index.tsx | 50 +-- web/containers/Layout/TopBar/index.tsx | 18 +- web/containers/Loader/GenerateResponse.tsx | 39 +++ web/containers/ModelConfigInput/index.tsx | 51 +--- web/containers/Providers/EventHandler.tsx | 5 +- web/containers/Providers/Jotai.tsx | 8 + web/containers/Shortcut/index.tsx | 2 +- web/containers/SliderRightPanel/index.tsx | 58 +--- web/helpers/atoms/ChatMessage.atom.ts | 2 + web/hooks/useCreateNewThread.ts | 10 +- web/hooks/usePath.ts | 17 ++ web/hooks/useSendChatMessage.ts | 100 +++++- web/hooks/useSetActiveThread.ts | 2 + web/next.config.js | 3 - web/package.json | 2 + web/screens/Chat/AssistantSetting/index.tsx | 78 +++++ web/screens/Chat/ChatBody/index.tsx | 24 +- web/screens/Chat/ChatInput/index.tsx | 254 ++++++++++++++++ web/screens/Chat/EngineSetting/index.tsx | 17 +- web/screens/Chat/FileUploadPreview/Icon.tsx | 95 ++++++ web/screens/Chat/FileUploadPreview/index.tsx | 47 +++ web/screens/Chat/ImageUploadPreview/index.tsx | 54 ++++ .../Chat/MessageQueuedBanner/index.tsx | 19 ++ ...ponentBuilder.tsx => SettingComponent.tsx} | 73 ++++- web/screens/Chat/ModelSetting/index.tsx | 7 +- .../Chat/ModelSetting/predefinedComponent.ts | 50 ++- .../Chat/RequestDownloadModel/index.tsx | 42 +++ web/screens/Chat/Sidebar/index.tsx | 108 +++++-- web/screens/Chat/SimpleTextMessage/index.tsx | 114 +++++-- web/screens/Chat/ThreadList/index.tsx | 1 + web/screens/Chat/index.tsx | 286 +++++++++--------- .../ExploreModelItemHeader/index.tsx | 2 + web/screens/LocalServer/index.tsx | 10 +- web/tsconfig.json | 10 +- web/types/index.d.ts | 1 - web/utils/base64.ts | 9 + web/utils/componentSettings.ts | 25 +- web/utils/modelParam.ts | 4 +- 68 files changed, 1959 insertions(+), 525 deletions(-) create mode 100644 core/src/types/thread/threadEvent.ts create mode 100644 extensions/assistant-extension/rollup.config.ts create mode 100644 extensions/assistant-extension/src/node/engine.ts create mode 100644 extensions/assistant-extension/src/node/index.ts create mode 100644 extensions/assistant-extension/src/node/tools/retrieval/index.ts delete mode 100644 extensions/assistant-extension/webpack.config.js create mode 100644 web/containers/Loader/GenerateResponse.tsx create mode 100644 web/screens/Chat/AssistantSetting/index.tsx create mode 100644 web/screens/Chat/ChatInput/index.tsx create mode 100644 web/screens/Chat/FileUploadPreview/Icon.tsx create mode 100644 web/screens/Chat/FileUploadPreview/index.tsx create mode 100644 web/screens/Chat/ImageUploadPreview/index.tsx create mode 100644 web/screens/Chat/MessageQueuedBanner/index.tsx rename web/screens/Chat/ModelSetting/{settingComponentBuilder.tsx => SettingComponent.tsx} (50%) create mode 100644 web/screens/Chat/RequestDownloadModel/index.tsx create mode 100644 web/utils/base64.ts diff --git a/core/src/api/index.ts b/core/src/api/index.ts index a3d0361e7..a232c4090 100644 --- a/core/src/api/index.ts +++ b/core/src/api/index.ts @@ -62,6 +62,7 @@ export enum FileManagerRoute { getJanDataFolderPath = 'getJanDataFolderPath', getResourcePath = 'getResourcePath', fileStat = 'fileStat', + writeBlob = 'writeBlob', } export type ApiFunction = (...args: any[]) => any diff --git a/core/src/fs.ts b/core/src/fs.ts index ea636977a..0e570d1f5 100644 --- a/core/src/fs.ts +++ b/core/src/fs.ts @@ -1,4 +1,4 @@ -import { FileStat } from "./types" +import { FileStat } from './types' /** * Writes data to a file at the specified path. @@ -6,6 +6,15 @@ import { FileStat } from "./types" */ const writeFileSync = (...args: any[]) => global.core.api?.writeFileSync(...args) +/** + * Writes blob data to a file at the specified path. + * @param path - The path to file. + * @param data - The blob data. + * @returns + */ +const writeBlob: (path: string, data: string) => Promise = (path, data) => + global.core.api?.writeBlob(path, data) + /** * Reads the contents of a file at the specified path. * @returns {Promise} A Promise that resolves with the contents of the file. @@ -60,7 +69,6 @@ const syncFile: (src: string, dest: string) => Promise = (src, dest) => */ const copyFileSync = (...args: any[]) => global.core.api?.copyFileSync(...args) - /** * Gets the file's stats. * @@ -70,7 +78,6 @@ const copyFileSync = (...args: any[]) => global.core.api?.copyFileSync(...args) const fileStat: (path: string) => Promise = (path) => global.core.api?.fileStat(path) - // TODO: Export `dummy` fs functions automatically // Currently adding these manually export const fs = { @@ -84,5 +91,6 @@ export const fs = { appendFileSync, copyFileSync, syncFile, - fileStat + fileStat, + writeBlob, } diff --git a/core/src/node/api/routes/fs.ts b/core/src/node/api/routes/fs.ts index 5f511af27..c5404ccce 100644 --- a/core/src/node/api/routes/fs.ts +++ b/core/src/node/api/routes/fs.ts @@ -2,6 +2,7 @@ import { FileSystemRoute } from '../../../api' import { join } from 'path' import { HttpServer } from '../HttpServer' import { getJanDataFolderPath } from '../../utils' +import { normalizeFilePath } from '../../path' export const fsRouter = async (app: HttpServer) => { const moduleName = 'fs' @@ -13,10 +14,10 @@ export const fsRouter = async (app: HttpServer) => { const result = await import(moduleName).then((mdl) => { return mdl[route]( ...body.map((arg: any) => - typeof arg === 'string' && arg.includes('file:/') - ? join(getJanDataFolderPath(), arg.replace('file:/', '')) - : arg, - ), + typeof arg === 'string' && (arg.startsWith(`file:/`) || arg.startsWith(`file:\\`)) + ? join(getJanDataFolderPath(), normalizeFilePath(arg)) + : arg + ) ) }) res.status(200).send(result) diff --git a/core/src/types/assistant/assistantEntity.ts b/core/src/types/assistant/assistantEntity.ts index 91bb2bb22..733dbea8d 100644 --- a/core/src/types/assistant/assistantEntity.ts +++ b/core/src/types/assistant/assistantEntity.ts @@ -2,6 +2,13 @@ * Assistant type defines the shape of an assistant object. * @stored */ + +export type AssistantTool = { + type: string + enabled: boolean + settings: any +} + export type Assistant = { /** Represents the avatar of the user. */ avatar: string @@ -22,7 +29,7 @@ export type Assistant = { /** Represents the instructions for the object. */ instructions?: string /** Represents the tools associated with the object. */ - tools?: any + tools?: AssistantTool[] /** Represents the file identifiers associated with the object. */ file_ids: string[] /** Represents the metadata of the object. */ diff --git a/core/src/types/inference/inferenceEntity.ts b/core/src/types/inference/inferenceEntity.ts index 58b838ae7..c37e3b079 100644 --- a/core/src/types/inference/inferenceEntity.ts +++ b/core/src/types/inference/inferenceEntity.ts @@ -1,3 +1,5 @@ +import { ContentType, ContentValue } from '../message' + /** * The role of the author of this message. */ @@ -13,7 +15,32 @@ export enum ChatCompletionRole { */ export type ChatCompletionMessage = { /** The contents of the message. **/ - content?: string + content?: ChatCompletionMessageContent /** The role of the author of this message. **/ role: ChatCompletionRole } + +export type ChatCompletionMessageContent = + | string + | (ChatCompletionMessageContentText & + ChatCompletionMessageContentImage & + ChatCompletionMessageContentDoc)[] + +export enum ChatCompletionMessageContentType { + Text = 'text', + Image = 'image_url', + Doc = 'doc_url', +} + +export type ChatCompletionMessageContentText = { + type: ChatCompletionMessageContentType + text: string +} +export type ChatCompletionMessageContentImage = { + type: ChatCompletionMessageContentType + image_url: { url: string } +} +export type ChatCompletionMessageContentDoc = { + type: ChatCompletionMessageContentType + doc_url: { url: string } +} diff --git a/core/src/types/message/messageEntity.ts b/core/src/types/message/messageEntity.ts index 199743796..87e4b1997 100644 --- a/core/src/types/message/messageEntity.ts +++ b/core/src/types/message/messageEntity.ts @@ -1,5 +1,6 @@ import { ChatCompletionMessage, ChatCompletionRole } from '../inference' import { ModelInfo } from '../model' +import { Thread } from '../thread' /** * The `ThreadMessage` type defines the shape of a thread's message object. @@ -35,7 +36,10 @@ export type ThreadMessage = { export type MessageRequest = { id?: string - /** The thread id of the message request. **/ + /** + * @deprecated Use thread object instead + * The thread id of the message request. + */ threadId: string /** @@ -48,6 +52,10 @@ export type MessageRequest = { /** Settings for constructing a chat completion request **/ model?: ModelInfo + + /** The thread of this message is belong to. **/ + // TODO: deprecate threadId field + thread?: Thread } /** @@ -62,7 +70,7 @@ export enum MessageStatus { /** Message loaded with error. **/ Error = 'error', /** Message is cancelled streaming */ - Stopped = "stopped" + Stopped = 'stopped', } /** @@ -71,6 +79,7 @@ export enum MessageStatus { export enum ContentType { Text = 'text', Image = 'image', + Pdf = 'pdf', } /** @@ -80,6 +89,8 @@ export enum ContentType { export type ContentValue = { value: string annotations: string[] + name?: string + size?: number } /** diff --git a/core/src/types/model/modelEntity.ts b/core/src/types/model/modelEntity.ts index c60ab7650..727ff085f 100644 --- a/core/src/types/model/modelEntity.ts +++ b/core/src/types/model/modelEntity.ts @@ -7,6 +7,7 @@ export type ModelInfo = { settings: ModelSettingParams parameters: ModelRuntimeParams engine?: InferenceEngine + proxyEngine?: InferenceEngine } /** @@ -18,7 +19,8 @@ export enum InferenceEngine { nitro = 'nitro', openai = 'openai', triton_trtllm = 'triton_trtllm', - hf_endpoint = 'hf_endpoint', + + tool_retrieval_enabled = 'tool_retrieval_enabled', } export type ModelArtifact = { @@ -90,6 +92,13 @@ export type Model = { * The model engine. */ engine: InferenceEngine + + proxyEngine?: InferenceEngine + + /** + * Is multimodal or not. + */ + visionModel?: boolean } export type ModelMetadata = { @@ -129,4 +138,5 @@ export type ModelRuntimeParams = { stop?: string[] frequency_penalty?: number presence_penalty?: number + engine?: string } diff --git a/core/src/types/thread/index.ts b/core/src/types/thread/index.ts index c6ff6204a..32155e1cd 100644 --- a/core/src/types/thread/index.ts +++ b/core/src/types/thread/index.ts @@ -1,2 +1,3 @@ export * from './threadEntity' export * from './threadInterface' +export * from './threadEvent' diff --git a/core/src/types/thread/threadEntity.ts b/core/src/types/thread/threadEntity.ts index 4ff3aa1fc..37136eae6 100644 --- a/core/src/types/thread/threadEntity.ts +++ b/core/src/types/thread/threadEntity.ts @@ -1,3 +1,4 @@ +import { AssistantTool } from '../assistant' import { ModelInfo } from '../model' /** @@ -30,6 +31,7 @@ export type ThreadAssistantInfo = { assistant_name: string model: ModelInfo instructions?: string + tools?: AssistantTool[] } /** diff --git a/core/src/types/thread/threadEvent.ts b/core/src/types/thread/threadEvent.ts new file mode 100644 index 000000000..4b19b09c1 --- /dev/null +++ b/core/src/types/thread/threadEvent.ts @@ -0,0 +1,4 @@ +export enum ThreadEvent { + /** The `OnThreadStarted` event is emitted when a thread is started. */ + OnThreadStarted = 'OnThreadStarted', +} diff --git a/electron/handlers/fileManager.ts b/electron/handlers/fileManager.ts index f41286934..2528aef71 100644 --- a/electron/handlers/fileManager.ts +++ b/electron/handlers/fileManager.ts @@ -59,4 +59,20 @@ export function handleFileMangerIPCs() { return fileStat } ) + + ipcMain.handle( + FileManagerRoute.writeBlob, + async (_event, path: string, data: string): Promise => { + try { + const normalizedPath = normalizeFilePath(path) + const dataBuffer = Buffer.from(data, 'base64') + fs.writeFileSync( + join(getJanDataFolderPath(), normalizedPath), + dataBuffer + ) + } catch (err) { + console.error(`writeFile ${path} result: ${err}`) + } + } + ) } diff --git a/electron/handlers/fs.ts b/electron/handlers/fs.ts index 408a5fd10..34026b940 100644 --- a/electron/handlers/fs.ts +++ b/electron/handlers/fs.ts @@ -1,9 +1,9 @@ import { ipcMain } from 'electron' -import { FileSystemRoute } from '@janhq/core' -import { join } from 'path' import { getJanDataFolderPath, normalizeFilePath } from '@janhq/core/node' - +import fs from 'fs' +import { FileManagerRoute, FileSystemRoute } from '@janhq/core' +import { join } from 'path' /** * Handles file system operations. */ @@ -15,7 +15,7 @@ export function handleFsIPCs() { mdl[route]( ...args.map((arg) => typeof arg === 'string' && - (arg.includes(`file:/`) || arg.includes(`file:\\`)) + (arg.startsWith(`file:/`) || arg.startsWith(`file:\\`)) ? join(getJanDataFolderPath(), normalizeFilePath(arg)) : arg ) diff --git a/extensions/assistant-extension/package.json b/extensions/assistant-extension/package.json index 4e84aa573..f4e4dd825 100644 --- a/extensions/assistant-extension/package.json +++ b/extensions/assistant-extension/package.json @@ -3,26 +3,46 @@ "version": "1.0.0", "description": "This extension enables assistants, including Jan, a default assistant that can call all downloaded models", "main": "dist/index.js", - "module": "dist/module.js", + "node": "dist/node/index.js", "author": "Jan ", "license": "AGPL-3.0", "scripts": { - "build": "tsc -b . && webpack --config webpack.config.js", + "build": "tsc --module commonjs && rollup -c rollup.config.ts", "build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/pre-install" }, "devDependencies": { + "@rollup/plugin-commonjs": "^25.0.7", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-replace": "^5.0.5", + "@types/pdf-parse": "^1.1.4", + "cpx": "^1.5.0", "rimraf": "^3.0.2", - "webpack": "^5.88.2", - "webpack-cli": "^5.1.4" + "rollup": "^2.38.5", + "rollup-plugin-define": "^1.0.1", + "rollup-plugin-sourcemaps": "^0.6.3", + "rollup-plugin-typescript2": "^0.36.0", + "typescript": "^5.3.3" }, "dependencies": { "@janhq/core": "file:../../core", + "@langchain/community": "0.0.13", + "hnswlib-node": "^1.4.2", + "langchain": "^0.0.214", "path-browserify": "^1.0.1", + "pdf-parse": "^1.1.1", "ts-loader": "^9.5.0" }, "files": [ "dist/*", "package.json", "README.md" + ], + "bundleDependencies": [ + "@janhq/core", + "@langchain/community", + "hnswlib-node", + "langchain", + "pdf-parse" ] } diff --git a/extensions/assistant-extension/rollup.config.ts b/extensions/assistant-extension/rollup.config.ts new file mode 100644 index 000000000..7916ef9c8 --- /dev/null +++ b/extensions/assistant-extension/rollup.config.ts @@ -0,0 +1,81 @@ +import resolve from "@rollup/plugin-node-resolve"; +import commonjs from "@rollup/plugin-commonjs"; +import sourceMaps from "rollup-plugin-sourcemaps"; +import typescript from "rollup-plugin-typescript2"; +import json from "@rollup/plugin-json"; +import replace from "@rollup/plugin-replace"; + +const packageJson = require("./package.json"); + +const pkg = require("./package.json"); + +export default [ + { + input: `src/index.ts`, + output: [{ file: pkg.main, format: "es", sourcemap: true }], + // Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash') + external: [], + watch: { + include: "src/**", + }, + plugins: [ + replace({ + NODE: JSON.stringify(`${packageJson.name}/${packageJson.node}`), + EXTENSION_NAME: JSON.stringify(packageJson.name), + VERSION: JSON.stringify(packageJson.version), + }), + // Allow json resolution + json(), + // Compile TypeScript files + typescript({ useTsconfigDeclarationDir: true }), + // Compile TypeScript files + // Allow bundling cjs modules (unlike webpack, rollup doesn't understand cjs) + commonjs(), + // Allow node_modules resolution, so you can use 'external' to control + // which external modules to include in the bundle + // https://github.com/rollup/rollup-plugin-node-resolve#usage + resolve({ + extensions: [".js", ".ts", ".svelte"], + }), + + // Resolve source maps to the original source + sourceMaps(), + ], + }, + { + input: `src/node/index.ts`, + output: [{ dir: "dist/node", format: "cjs", sourcemap: false }], + // Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash') + external: [ + "@janhq/core/node", + "@langchain/community", + "langchain", + "langsmith", + "path", + "hnswlib-node", + ], + watch: { + include: "src/node/**", + }, + // inlineDynamicImports: true, + plugins: [ + // Allow json resolution + json(), + // Compile TypeScript files + typescript({ useTsconfigDeclarationDir: true }), + // Allow bundling cjs modules (unlike webpack, rollup doesn't understand cjs) + commonjs({ + ignoreDynamicRequires: true, + }), + // Allow node_modules resolution, so you can use 'external' to control + // which external modules to include in the bundle + // https://github.com/rollup/rollup-plugin-node-resolve#usage + resolve({ + extensions: [".ts", ".js", ".json"], + }), + + // Resolve source maps to the original source + // sourceMaps(), + ], + }, +]; diff --git a/extensions/assistant-extension/src/@types/global.d.ts b/extensions/assistant-extension/src/@types/global.d.ts index 3b45ccc5a..dc11709a4 100644 --- a/extensions/assistant-extension/src/@types/global.d.ts +++ b/extensions/assistant-extension/src/@types/global.d.ts @@ -1 +1,3 @@ -declare const MODULE: string; +declare const NODE: string; +declare const EXTENSION_NAME: string; +declare const VERSION: string; diff --git a/extensions/assistant-extension/src/index.ts b/extensions/assistant-extension/src/index.ts index 098ab1f54..6495ea786 100644 --- a/extensions/assistant-extension/src/index.ts +++ b/extensions/assistant-extension/src/index.ts @@ -1,15 +1,151 @@ -import { fs, Assistant, AssistantExtension } from "@janhq/core"; -import { join } from "path"; +import { + fs, + Assistant, + MessageRequest, + events, + InferenceEngine, + MessageEvent, + InferenceEvent, + joinPath, + executeOnMain, + AssistantExtension, +} from "@janhq/core"; export default class JanAssistantExtension extends AssistantExtension { private static readonly _homeDir = "file://assistants"; + controller = new AbortController(); + isCancelled = false; + retrievalThreadId: string | undefined = undefined; + async onLoad() { // making the assistant directory - if (!(await fs.existsSync(JanAssistantExtension._homeDir))) - fs.mkdirSync(JanAssistantExtension._homeDir).then(() => { - this.createJanAssistant(); - }); + const assistantDirExist = await fs.existsSync( + JanAssistantExtension._homeDir, + ); + if ( + localStorage.getItem(`${EXTENSION_NAME}-version`) !== VERSION || + !assistantDirExist + ) { + if (!assistantDirExist) + await fs.mkdirSync(JanAssistantExtension._homeDir); + + // Write assistant metadata + this.createJanAssistant(); + // Finished migration + localStorage.setItem(`${EXTENSION_NAME}-version`, VERSION); + } + + // Events subscription + events.on(MessageEvent.OnMessageSent, (data: MessageRequest) => + JanAssistantExtension.handleMessageRequest(data, this), + ); + + events.on(InferenceEvent.OnInferenceStopped, () => { + JanAssistantExtension.handleInferenceStopped(this); + }); + } + + private static async handleInferenceStopped(instance: JanAssistantExtension) { + instance.isCancelled = true; + instance.controller?.abort(); + } + + private static async handleMessageRequest( + data: MessageRequest, + instance: JanAssistantExtension, + ) { + instance.isCancelled = false; + instance.controller = new AbortController(); + + if ( + data.model?.engine !== InferenceEngine.tool_retrieval_enabled || + !data.messages || + !data.thread?.assistants[0]?.tools + ) { + return; + } + + const latestMessage = data.messages[data.messages.length - 1]; + + // Ingest the document if needed + if ( + latestMessage && + latestMessage.content && + typeof latestMessage.content !== "string" + ) { + const docFile = latestMessage.content[1]?.doc_url?.url; + if (docFile) { + await executeOnMain( + NODE, + "toolRetrievalIngestNewDocument", + docFile, + data.model?.proxyEngine, + ); + } + } + + // Load agent on thread changed + if (instance.retrievalThreadId !== data.threadId) { + await executeOnMain(NODE, "toolRetrievalLoadThreadMemory", data.threadId); + + instance.retrievalThreadId = data.threadId; + + // Update the text splitter + await executeOnMain( + NODE, + "toolRetrievalUpdateTextSplitter", + data.thread.assistants[0].tools[0]?.settings?.chunk_size ?? 4000, + data.thread.assistants[0].tools[0]?.settings?.chunk_overlap ?? 200, + ); + } + + if (latestMessage.content) { + const prompt = + typeof latestMessage.content === "string" + ? latestMessage.content + : latestMessage.content[0].text; + // Retrieve the result + console.debug("toolRetrievalQuery", latestMessage.content); + const retrievalResult = await executeOnMain( + NODE, + "toolRetrievalQueryResult", + prompt, + ); + + // Update the message content + // Using the retrieval template with the result and query + if (data.thread?.assistants[0].tools) + data.messages[data.messages.length - 1].content = + data.thread.assistants[0].tools[0].settings?.retrieval_template + ?.replace("{CONTEXT}", retrievalResult) + .replace("{QUESTION}", prompt); + } + + // Filter out all the messages that are not text + data.messages = data.messages.map((message) => { + if ( + message.content && + typeof message.content !== "string" && + (message.content.length ?? 0) > 0 + ) { + return { + ...message, + content: [message.content[0]], + }; + } + return message; + }); + + // Reroute the result to inference engine + const output = { + ...data, + model: { + ...data.model, + engine: data.model.proxyEngine, + }, + }; + events.emit(MessageEvent.OnMessageSent, output); } /** @@ -18,15 +154,21 @@ export default class JanAssistantExtension extends AssistantExtension { onUnload(): void {} async createAssistant(assistant: Assistant): Promise { - const assistantDir = join(JanAssistantExtension._homeDir, assistant.id); + const assistantDir = await joinPath([ + JanAssistantExtension._homeDir, + assistant.id, + ]); if (!(await fs.existsSync(assistantDir))) await fs.mkdirSync(assistantDir); // store the assistant metadata json - const assistantMetadataPath = join(assistantDir, "assistant.json"); + const assistantMetadataPath = await joinPath([ + assistantDir, + "assistant.json", + ]); try { await fs.writeFileSync( assistantMetadataPath, - JSON.stringify(assistant, null, 2) + JSON.stringify(assistant, null, 2), ); } catch (err) { console.error(err); @@ -38,14 +180,17 @@ export default class JanAssistantExtension extends AssistantExtension { // get all the assistant metadata json const results: Assistant[] = []; const allFileName: string[] = await fs.readdirSync( - JanAssistantExtension._homeDir + JanAssistantExtension._homeDir, ); for (const fileName of allFileName) { - const filePath = join(JanAssistantExtension._homeDir, fileName); + const filePath = await joinPath([ + JanAssistantExtension._homeDir, + fileName, + ]); if (filePath.includes(".DS_Store")) continue; const jsonFiles: string[] = (await fs.readdirSync(filePath)).filter( - (file: string) => file === "assistant.json" + (file: string) => file === "assistant.json", ); if (jsonFiles.length !== 1) { @@ -54,8 +199,8 @@ export default class JanAssistantExtension extends AssistantExtension { } const content = await fs.readFileSync( - join(filePath, jsonFiles[0]), - "utf-8" + await joinPath([filePath, jsonFiles[0]]), + "utf-8", ); const assistant: Assistant = typeof content === "object" ? content : JSON.parse(content); @@ -72,7 +217,10 @@ export default class JanAssistantExtension extends AssistantExtension { } // remove the directory - const assistantDir = join(JanAssistantExtension._homeDir, assistant.id); + const assistantDir = await joinPath([ + JanAssistantExtension._homeDir, + assistant.id, + ]); await fs.rmdirSync(assistantDir); return Promise.resolve(); } @@ -88,7 +236,24 @@ export default class JanAssistantExtension extends AssistantExtension { description: "A default assistant that can use all downloaded models", model: "*", instructions: "", - tools: undefined, + tools: [ + { + type: "retrieval", + enabled: false, + settings: { + top_k: 2, + chunk_size: 1024, + chunk_overlap: 64, + retrieval_template: `Use the following pieces of context to answer the question at the end. If you don't know the answer, just say that you don't know, don't try to make up an answer. + ---------------- + CONTEXT: {CONTEXT} + ---------------- + QUESTION: {QUESTION} + ---------------- + Helpful Answer:`, + }, + }, + ], file_ids: [], metadata: undefined, }; diff --git a/extensions/assistant-extension/src/node/engine.ts b/extensions/assistant-extension/src/node/engine.ts new file mode 100644 index 000000000..54b2a6ba1 --- /dev/null +++ b/extensions/assistant-extension/src/node/engine.ts @@ -0,0 +1,13 @@ +import fs from "fs"; +import path from "path"; +import { getJanDataFolderPath } from "@janhq/core/node"; + +// Sec: Do not send engine settings over requests +// Read it manually instead +export const readEmbeddingEngine = (engineName: string) => { + const engineSettings = fs.readFileSync( + path.join(getJanDataFolderPath(), "engines", `${engineName}.json`), + "utf-8", + ); + return JSON.parse(engineSettings); +}; diff --git a/extensions/assistant-extension/src/node/index.ts b/extensions/assistant-extension/src/node/index.ts new file mode 100644 index 000000000..95a7243a4 --- /dev/null +++ b/extensions/assistant-extension/src/node/index.ts @@ -0,0 +1,39 @@ +import { getJanDataFolderPath, normalizeFilePath } from "@janhq/core/node"; +import { Retrieval } from "./tools/retrieval"; +import path from "path"; + +const retrieval = new Retrieval(); + +export async function toolRetrievalUpdateTextSplitter( + chunkSize: number, + chunkOverlap: number, +) { + retrieval.updateTextSplitter(chunkSize, chunkOverlap); + return Promise.resolve(); +} +export async function toolRetrievalIngestNewDocument( + file: string, + engine: string, +) { + const filePath = path.join(getJanDataFolderPath(), normalizeFilePath(file)); + const threadPath = path.dirname(filePath.replace("files", "")); + retrieval.updateEmbeddingEngine(engine); + await retrieval.ingestAgentKnowledge(filePath, `${threadPath}/memory`); + return Promise.resolve(); +} + +export async function toolRetrievalLoadThreadMemory(threadId: string) { + try { + await retrieval.loadRetrievalAgent( + path.join(getJanDataFolderPath(), "threads", threadId, "memory"), + ); + return Promise.resolve(); + } catch (err) { + console.debug(err); + } +} + +export async function toolRetrievalQueryResult(query: string) { + const res = await retrieval.generateResult(query); + return Promise.resolve(res); +} diff --git a/extensions/assistant-extension/src/node/tools/retrieval/index.ts b/extensions/assistant-extension/src/node/tools/retrieval/index.ts new file mode 100644 index 000000000..f9d5c4029 --- /dev/null +++ b/extensions/assistant-extension/src/node/tools/retrieval/index.ts @@ -0,0 +1,78 @@ +import { RecursiveCharacterTextSplitter } from "langchain/text_splitter"; +import { formatDocumentsAsString } from "langchain/util/document"; +import { PDFLoader } from "langchain/document_loaders/fs/pdf"; + +import { HNSWLib } from "langchain/vectorstores/hnswlib"; + +import { OpenAIEmbeddings } from "langchain/embeddings/openai"; +import { readEmbeddingEngine } from "../../engine"; + +export class Retrieval { + public chunkSize: number = 100; + public chunkOverlap?: number = 0; + private retriever: any; + + private embeddingModel: any = 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 { + this.chunkSize = chunkSize; + this.chunkOverlap = chunkOverlap; + this.textSplitter = new RecursiveCharacterTextSplitter({ + chunkSize: chunkSize, + chunkOverlap: chunkOverlap, + }); + } + + public updateEmbeddingEngine(engine: string): void { + // Engine settings are not compatible with the current embedding model params + // Switch case manually for now + const settings = readEmbeddingEngine(engine); + if (engine === "nitro") { + this.embeddingModel = new OpenAIEmbeddings( + { openAIApiKey: "nitro-embedding" }, + { basePath: "http://127.0.0.1:3928/v1" }, + ); + } else { + // Fallback to OpenAI Settings + this.embeddingModel = new OpenAIEmbeddings({ + configuration: { + apiKey: settings.api_key, + }, + }); + } + } + + public ingestAgentKnowledge = async ( + filePath: string, + memoryPath: string, + ): Promise => { + const loader = new PDFLoader(filePath, { + splitPages: true, + }); + const doc = await loader.load(); + const docs = await this.textSplitter!.splitDocuments(doc); + const vectorStore = await HNSWLib.fromDocuments(docs, this.embeddingModel); + return vectorStore.save(memoryPath); + }; + + public loadRetrievalAgent = async (memoryPath: string): Promise => { + const vectorStore = await HNSWLib.load(memoryPath, this.embeddingModel); + this.retriever = vectorStore.asRetriever(2); + return Promise.resolve(); + }; + + public generateResult = async (query: string): Promise => { + if (!this.retriever) { + return Promise.resolve(" "); + } + const relevantDocs = await this.retriever.getRelevantDocuments(query); + const serializedDoc = formatDocumentsAsString(relevantDocs); + return Promise.resolve(serializedDoc); + }; +} diff --git a/extensions/assistant-extension/tsconfig.json b/extensions/assistant-extension/tsconfig.json index 2477d58ce..d3794cace 100644 --- a/extensions/assistant-extension/tsconfig.json +++ b/extensions/assistant-extension/tsconfig.json @@ -1,14 +1,20 @@ { "compilerOptions": { - "target": "es2016", - "module": "ES6", "moduleResolution": "node", - "outDir": "./dist", - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "strict": false, + "target": "es5", + "module": "ES2020", + "lib": ["es2015", "es2016", "es2017", "dom"], + "strict": true, + "sourceMap": true, + "declaration": true, + "allowSyntheticDefaultImports": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "declarationDir": "dist/types", + "outDir": "dist", + "importHelpers": true, + "typeRoots": ["node_modules/@types"], "skipLibCheck": true, - "rootDir": "./src" }, - "include": ["./src"] + "include": ["src"], } diff --git a/extensions/assistant-extension/webpack.config.js b/extensions/assistant-extension/webpack.config.js deleted file mode 100644 index 74d16fc8e..000000000 --- a/extensions/assistant-extension/webpack.config.js +++ /dev/null @@ -1,38 +0,0 @@ -const path = require("path"); -const webpack = require("webpack"); -const packageJson = require("./package.json"); - -module.exports = { - experiments: { outputModule: true }, - entry: "./src/index.ts", // Adjust the entry point to match your project's main file - mode: "production", - module: { - rules: [ - { - test: /\.tsx?$/, - use: "ts-loader", - exclude: /node_modules/, - }, - ], - }, - output: { - filename: "index.js", // Adjust the output file name as needed - path: path.resolve(__dirname, "dist"), - library: { type: "module" }, // Specify ESM output format - }, - plugins: [ - new webpack.DefinePlugin({ - MODULE: JSON.stringify(`${packageJson.name}/${packageJson.module}`), - }), - ], - resolve: { - extensions: [".ts", ".js"], - fallback: { - path: require.resolve("path-browserify"), - }, - }, - optimization: { - minimize: false, - }, - // Add loaders and other configuration as needed for your project -}; diff --git a/extensions/conversational-extension/src/index.ts b/extensions/conversational-extension/src/index.ts index 66becb748..61f0fd0e9 100644 --- a/extensions/conversational-extension/src/index.ts +++ b/extensions/conversational-extension/src/index.ts @@ -4,15 +4,14 @@ import { ConversationalExtension, Thread, ThreadMessage, + events, } from '@janhq/core' /** * JSONConversationalExtension is a ConversationalExtension implementation that provides * functionality for managing threads. */ -export default class JSONConversationalExtension - extends ConversationalExtension -{ +export default class JSONConversationalExtension extends ConversationalExtension { private static readonly _homeDir = 'file://threads' private static readonly _threadInfoFileName = 'thread.json' private static readonly _threadMessagesFileName = 'messages.jsonl' @@ -119,6 +118,32 @@ export default class JSONConversationalExtension ]) if (!(await fs.existsSync(threadDirPath))) await fs.mkdirSync(threadDirPath) + + 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 (message.content[0].type === 'pdf') { + const filesPath = await joinPath([threadDirPath, 'files']) + if (!(await fs.existsSync(filesPath))) await fs.mkdirSync(filesPath) + + const filePath = await joinPath([filesPath, `${message.id}.pdf`]) + const blob = message.content[0].text.annotations[0] + await this.storeFile(blob, filePath) + + if (await fs.existsSync(filePath)) { + // Use file path instead of blob + message.content[0].text.annotations[0] = `threads/${message.thread_id}/files/${message.id}.pdf` + } + } await fs.appendFileSync(threadMessagePath, JSON.stringify(message) + '\n') Promise.resolve() } catch (err) { @@ -126,6 +151,25 @@ export default class JSONConversationalExtension } } + async storeImage(base64: string, filePath: string): Promise { + const base64Data = base64.replace(/^data:image\/\w+;base64,/, '') + + try { + await fs.writeBlob(filePath, base64Data) + } catch (err) { + console.error(err) + } + } + + async storeFile(base64: string, filePath: string): Promise { + const base64Data = base64.replace(/^data:application\/pdf;base64,/, '') + try { + await fs.writeBlob(filePath, base64Data) + } catch (err) { + console.error(err) + } + } + async writeMessages( threadId: string, messages: ThreadMessage[] @@ -229,7 +273,11 @@ export default class JSONConversationalExtension const messages: ThreadMessage[] = [] result.forEach((line: string) => { - messages.push(JSON.parse(line) as ThreadMessage) + try { + messages.push(JSON.parse(line) as ThreadMessage) + } catch (err) { + console.error(err) + } }) return messages } catch (err) { diff --git a/extensions/inference-nitro-extension/bin/version.txt b/extensions/inference-nitro-extension/bin/version.txt index f2722b133..769ed6ae7 100644 --- a/extensions/inference-nitro-extension/bin/version.txt +++ b/extensions/inference-nitro-extension/bin/version.txt @@ -1 +1 @@ -0.2.12 +0.2.14 diff --git a/extensions/inference-nitro-extension/package.json b/extensions/inference-nitro-extension/package.json index 9379e194b..44727eb70 100644 --- a/extensions/inference-nitro-extension/package.json +++ b/extensions/inference-nitro-extension/package.json @@ -40,6 +40,7 @@ "dependencies": { "@janhq/core": "file:../../core", "@rollup/plugin-replace": "^5.0.5", + "@types/os-utils": "^0.0.4", "fetch-retry": "^5.0.6", "path-browserify": "^1.0.1", "rxjs": "^7.8.1", diff --git a/extensions/inference-nitro-extension/src/index.ts b/extensions/inference-nitro-extension/src/index.ts index 735383a61..0e6edb992 100644 --- a/extensions/inference-nitro-extension/src/index.ts +++ b/extensions/inference-nitro-extension/src/index.ts @@ -50,7 +50,7 @@ export default class JanInferenceNitroExtension extends InferenceExtension { ngl: 100, cpu_threads: 1, cont_batching: false, - embedding: false, + embedding: true, }; controller = new AbortController(); @@ -83,19 +83,19 @@ export default class JanInferenceNitroExtension extends InferenceExtension { // Events subscription events.on(MessageEvent.OnMessageSent, (data: MessageRequest) => - this.onMessageRequest(data) + this.onMessageRequest(data), ); events.on(ModelEvent.OnModelInit, (model: Model) => - this.onModelInit(model) + this.onModelInit(model), ); events.on(ModelEvent.OnModelStop, (model: Model) => - this.onModelStop(model) + this.onModelStop(model), ); events.on(InferenceEvent.OnInferenceStopped, () => - this.onInferenceStopped() + this.onInferenceStopped(), ); // Attempt to fetch nvidia info @@ -120,7 +120,7 @@ export default class JanInferenceNitroExtension extends InferenceExtension { } else { await fs.writeFileSync( engineFile, - JSON.stringify(this._engineSettings, null, 2) + JSON.stringify(this._engineSettings, null, 2), ); } } catch (err) { @@ -148,7 +148,7 @@ export default class JanInferenceNitroExtension extends InferenceExtension { this.getNitroProcesHealthIntervalId = setInterval( () => this.periodicallyGetNitroHealth(), - JanInferenceNitroExtension._intervalHealthCheck + JanInferenceNitroExtension._intervalHealthCheck, ); } diff --git a/extensions/inference-nitro-extension/src/node/index.ts b/extensions/inference-nitro-extension/src/node/index.ts index a75f33df2..77060e414 100644 --- a/extensions/inference-nitro-extension/src/node/index.ts +++ b/extensions/inference-nitro-extension/src/node/index.ts @@ -78,7 +78,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 +96,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"); @@ -133,7 +133,6 @@ async function runModel( mmproj: path.join(modelFolderPath, wrapper.model.settings.mmproj), }), }; - console.log(currentSettings); return runNitroAndLoadModel(); } } @@ -192,10 +191,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 @@ -205,7 +204,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 @@ -234,8 +233,8 @@ function loadLLMModel(settings: any): Promise { .then((res) => { log( `[NITRO]::Debug: Load model success with response ${JSON.stringify( - res - )}` + res, + )}`, ); return Promise.resolve(res); }) @@ -264,8 +263,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) { @@ -316,7 +315,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, @@ -327,7 +326,7 @@ function spawnNitroProcess(): Promise { ...process.env, CUDA_VISIBLE_DEVICES: executableOptions.cudaVisibleDevices, }, - } + }, ); // Handle subprocess output diff --git a/extensions/inference-openai-extension/src/index.ts b/extensions/inference-openai-extension/src/index.ts index 54572041d..44525b631 100644 --- a/extensions/inference-openai-extension/src/index.ts +++ b/extensions/inference-openai-extension/src/index.ts @@ -15,6 +15,7 @@ import { ThreadMessage, events, fs, + InferenceEngine, BaseExtension, MessageEvent, ModelEvent, @@ -57,7 +58,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) => { @@ -81,7 +82,7 @@ export default class JanInferenceOpenAIExtension extends BaseExtension { try { const engineFile = join( JanInferenceOpenAIExtension._homeDir, - JanInferenceOpenAIExtension._engineMetadataFileName + JanInferenceOpenAIExtension._engineMetadataFileName, ); if (await fs.existsSync(engineFile)) { const engine = await fs.readFileSync(engineFile, "utf-8"); @@ -90,7 +91,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) { @@ -98,7 +99,7 @@ export default class JanInferenceOpenAIExtension extends BaseExtension { } } private static async handleModelInit(model: OpenAIModel) { - if (model.engine !== "openai") { + if (model.engine !== InferenceEngine.openai) { return; } else { JanInferenceOpenAIExtension._currentModel = model; @@ -116,7 +117,7 @@ export default class JanInferenceOpenAIExtension extends BaseExtension { } private static async handleInferenceStopped( - instance: JanInferenceOpenAIExtension + instance: JanInferenceOpenAIExtension, ) { instance.isCancelled = true; instance.controller?.abort(); @@ -130,7 +131,7 @@ export default class JanInferenceOpenAIExtension extends BaseExtension { */ private static async handleMessageRequest( data: MessageRequest, - instance: JanInferenceOpenAIExtension + instance: JanInferenceOpenAIExtension, ) { if (data.model.engine !== "openai") { return; @@ -160,7 +161,7 @@ export default class JanInferenceOpenAIExtension extends BaseExtension { ...JanInferenceOpenAIExtension._currentModel, parameters: data.model.parameters, }, - instance.controller + instance.controller, ).subscribe({ next: (content) => { const messageContent: ThreadContent = { diff --git a/extensions/inference-openai-extension/tsconfig.json b/extensions/inference-openai-extension/tsconfig.json index b48175a16..7bfdd9009 100644 --- a/extensions/inference-openai-extension/tsconfig.json +++ b/extensions/inference-openai-extension/tsconfig.json @@ -3,13 +3,12 @@ "target": "es2016", "module": "ES6", "moduleResolution": "node", - "outDir": "./dist", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": false, "skipLibCheck": true, - "rootDir": "./src" + "rootDir": "./src", }, - "include": ["./src"] + "include": ["./src"], } diff --git a/extensions/inference-triton-trtllm-extension/tsconfig.json b/extensions/inference-triton-trtllm-extension/tsconfig.json index b48175a16..7bfdd9009 100644 --- a/extensions/inference-triton-trtllm-extension/tsconfig.json +++ b/extensions/inference-triton-trtllm-extension/tsconfig.json @@ -3,13 +3,12 @@ "target": "es2016", "module": "ES6", "moduleResolution": "node", - "outDir": "./dist", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": false, "skipLibCheck": true, - "rootDir": "./src" + "rootDir": "./src", }, - "include": ["./src"] + "include": ["./src"], } diff --git a/web/containers/CardSidebar/index.tsx b/web/containers/CardSidebar/index.tsx index bc5047497..c0dd19ba5 100644 --- a/web/containers/CardSidebar/index.tsx +++ b/web/containers/CardSidebar/index.tsx @@ -17,6 +17,7 @@ import { activeThreadAtom } from '@/helpers/atoms/Thread.atom' interface Props { children: ReactNode + rightAction?: ReactNode title: string asChild?: boolean hideMoreVerticalAction?: boolean @@ -25,6 +26,7 @@ export default function CardSidebar({ children, title, asChild, + rightAction, hideMoreVerticalAction, }: Props) { const [show, setShow] = useState(true) @@ -53,27 +55,16 @@ export default function CardSidebar({
- {title} -
- {!asChild && ( - <> - {!hideMoreVerticalAction && ( -
setMore(!more)} - > - -
- )} - - )} +
+ {title} +
+
+ {rightAction && rightAction} + {!asChild && ( + <> + {!hideMoreVerticalAction && ( +
setMore(!more)} + > + +
+ )} + + )}
{more && ( diff --git a/web/containers/Checkbox/index.tsx b/web/containers/Checkbox/index.tsx index e8f916d98..a545771b6 100644 --- a/web/containers/Checkbox/index.tsx +++ b/web/containers/Checkbox/index.tsx @@ -9,54 +9,26 @@ import { TooltipTrigger, } from '@janhq/uikit' -import { useAtomValue, useSetAtom } from 'jotai' import { InfoIcon } from 'lucide-react' -import { useActiveModel } from '@/hooks/useActiveModel' -import useUpdateModelParameters from '@/hooks/useUpdateModelParameters' - -import { getConfigurationsData } from '@/utils/componentSettings' -import { toSettingParams } from '@/utils/modelParam' - -import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom' -import { - engineParamsUpdateAtom, - getActiveThreadIdAtom, - getActiveThreadModelParamsAtom, -} from '@/helpers/atoms/Thread.atom' - type Props = { name: string title: string + enabled?: boolean description: string checked: boolean + onValueChanged?: (e: string | number | boolean) => void } -const Checkbox: React.FC = ({ name, title, checked, description }) => { - const { updateModelParameter } = useUpdateModelParameters() - const threadId = useAtomValue(getActiveThreadIdAtom) - - const activeModelParams = useAtomValue(getActiveThreadModelParamsAtom) - - const modelSettingParams = toSettingParams(activeModelParams) - - const engineParams = getConfigurationsData(modelSettingParams) - - const setEngineParamsUpdate = useSetAtom(engineParamsUpdateAtom) - - const serverEnabled = useAtomValue(serverEnabledAtom) - - const { stopModel } = useActiveModel() - +const Checkbox: React.FC = ({ + title, + checked, + enabled = true, + description, + onValueChanged, +}) => { const onCheckedChange = (checked: boolean) => { - if (!threadId) return - if (engineParams.some((x) => x.name.includes(name))) { - setEngineParamsUpdate(true) - stopModel() - } else { - setEngineParamsUpdate(false) - } - updateModelParameter(threadId, name, checked) + onValueChanged?.(checked) } return ( @@ -80,7 +52,7 @@ const Checkbox: React.FC = ({ name, title, checked, description }) => {
) diff --git a/web/containers/Layout/TopBar/index.tsx b/web/containers/Layout/TopBar/index.tsx index ac05e4e1a..ab67cb3b7 100644 --- a/web/containers/Layout/TopBar/index.tsx +++ b/web/containers/Layout/TopBar/index.tsx @@ -120,13 +120,13 @@ const TopBar = () => {
-
- {activeThread && ( + {activeThread && ( +
{showing && (
@@ -227,8 +227,8 @@ const TopBar = () => { />
- )} -
+
+ )}
)} diff --git a/web/containers/Loader/GenerateResponse.tsx b/web/containers/Loader/GenerateResponse.tsx new file mode 100644 index 000000000..457c44987 --- /dev/null +++ b/web/containers/Loader/GenerateResponse.tsx @@ -0,0 +1,39 @@ +import React, { useEffect, useState } from 'react' + +export default function GenerateResponse() { + const [loader, setLoader] = useState(0) + + // This is fake loader please fix this when we have realtime percentage when load model + useEffect(() => { + if (loader === 24) { + setTimeout(() => { + setLoader(loader + 1) + }, 250) + } else if (loader === 50) { + setTimeout(() => { + setLoader(loader + 1) + }, 250) + } else if (loader === 78) { + setTimeout(() => { + setLoader(loader + 1) + }, 250) + } else if (loader === 85) { + setLoader(85) + } else { + setLoader(loader + 1) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [loader]) + + return ( +
+
+
+ Generating response... +
+
+ ) +} diff --git a/web/containers/ModelConfigInput/index.tsx b/web/containers/ModelConfigInput/index.tsx index e409fd424..d573a0bf9 100644 --- a/web/containers/ModelConfigInput/index.tsx +++ b/web/containers/ModelConfigInput/index.tsx @@ -7,65 +7,26 @@ import { TooltipTrigger, } from '@janhq/uikit' -import { useAtomValue, useSetAtom } from 'jotai' - import { InfoIcon } from 'lucide-react' -import { useActiveModel } from '@/hooks/useActiveModel' -import useUpdateModelParameters from '@/hooks/useUpdateModelParameters' - -import { getConfigurationsData } from '@/utils/componentSettings' - -import { toSettingParams } from '@/utils/modelParam' - -import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom' -import { - engineParamsUpdateAtom, - getActiveThreadIdAtom, - getActiveThreadModelParamsAtom, -} from '@/helpers/atoms/Thread.atom' - type Props = { title: string + enabled?: boolean name: string description: string placeholder: string value: string + onValueChanged?: (e: string | number | boolean) => void } const ModelConfigInput: React.FC = ({ title, - name, + enabled = true, value, description, placeholder, + onValueChanged, }) => { - const { updateModelParameter } = useUpdateModelParameters() - const threadId = useAtomValue(getActiveThreadIdAtom) - - const activeModelParams = useAtomValue(getActiveThreadModelParamsAtom) - - const modelSettingParams = toSettingParams(activeModelParams) - - const engineParams = getConfigurationsData(modelSettingParams) - - const setEngineParamsUpdate = useSetAtom(engineParamsUpdateAtom) - - const { stopModel } = useActiveModel() - - const serverEnabled = useAtomValue(serverEnabledAtom) - - const onValueChanged = (e: React.ChangeEvent) => { - if (!threadId) return - if (engineParams.some((x) => x.name.includes(name))) { - setEngineParamsUpdate(true) - stopModel() - } else { - setEngineParamsUpdate(false) - } - updateModelParameter(threadId, name, e.target.value) - } - return (
@@ -86,9 +47,9 @@ const ModelConfigInput: React.FC = ({