diff --git a/.github/workflows/publish-npm-core.yml b/.github/workflows/publish-npm-core.yml
index 462dbdc8e..403deb100 100644
--- a/.github/workflows/publish-npm-core.yml
+++ b/.github/workflows/publish-npm-core.yml
@@ -1,10 +1,9 @@
name: Publish core Package to npmjs
on:
push:
- tags: ["v[0-9]+.[0-9]+.[0-9]+-core"]
- paths: ["core/**", ".github/workflows/publish-npm-core.yml"]
- pull_request:
- paths: ["core/**", ".github/workflows/publish-npm-core.yml"]
+ tags: ['v[0-9]+.[0-9]+.[0-9]+-core']
+ paths: ['core/**', '.github/workflows/publish-npm-core.yml']
+ workflow_dispatch:
jobs:
build-and-publish-plugins:
environment: production
@@ -12,7 +11,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
- fetch-depth: "0"
+ fetch-depth: '0'
token: ${{ secrets.PAT_SERVICE_ACCOUNT }}
- name: Install jq
@@ -24,7 +23,7 @@ jobs:
env:
GITHUB_REF: ${{ github.ref }}
- - name: "Get Semantic Version from tag"
+ - name: 'Get Semantic Version from tag'
if: github.event_name == 'push'
run: |
# Get the tag from the event
@@ -42,8 +41,8 @@ jobs:
# Setup .npmrc file to publish to npm
- uses: actions/setup-node@v3
with:
- node-version: "20.x"
- registry-url: "https://registry.npmjs.org"
+ node-version: '20.x'
+ registry-url: 'https://registry.npmjs.org'
- run: cd core && corepack enable && corepack prepare yarn@4.5.3 --activate && yarn --version && yarn config set -H enableImmutableInstalls false && yarn install && yarn build
diff --git a/.github/workflows/publish-npm-joi.yml b/.github/workflows/publish-npm-joi.yml
deleted file mode 100644
index 867ad80fe..000000000
--- a/.github/workflows/publish-npm-joi.yml
+++ /dev/null
@@ -1,53 +0,0 @@
-name: Publish joi Package to npmjs
-on:
- push:
- tags: ["v[0-9]+.[0-9]+.[0-9]+-joi"]
- paths: ["joi/**", ".github/workflows/publish-npm-joi.yml"]
- pull_request:
- paths: ["joi/**", ".github/workflows/publish-npm-joi.yml"]
-jobs:
- build-and-publish-plugins:
- environment: production
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- with:
- fetch-depth: "0"
- token: ${{ secrets.PAT_SERVICE_ACCOUNT }}
-
- - name: Install jq
- uses: dcarbone/install-jq-action@v2.0.1
-
- - name: Extract tag name without v prefix
- id: get_version
- run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV && echo "::set-output name=version::${GITHUB_REF#refs/tags/v}"
- env:
- GITHUB_REF: ${{ github.ref }}
-
- - name: "Get Semantic Version from tag"
- if: github.event_name == 'push'
- run: |
- # Get the tag from the event
- tag=${GITHUB_REF#refs/tags/v}
- # remove the -joi suffix
- new_version=$(echo $tag | sed -n 's/-joi//p')
- echo $new_version
- # Replace the old version with the new version in package.json
- jq --arg version "$new_version" '.version = $version' joi/package.json > /tmp/package.json && mv /tmp/package.json joi/package.json
-
- # Print the new version
- echo "Updated package.json version to: $new_version"
- cat joi/package.json
-
- # Setup .npmrc file to publish to npm
- - uses: actions/setup-node@v3
- with:
- node-version: "20.x"
- registry-url: "https://registry.npmjs.org"
-
- - run: cd joi && corepack enable && corepack prepare yarn@4.5.3 --activate && yarn --version && yarn config set -H enableImmutableInstalls false && yarn install && yarn build
-
- - run: cd joi && yarn publish --access public
- if: github.event_name == 'push'
- env:
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
diff --git a/.github/workflows/template-tauri-build-linux-x64.yml b/.github/workflows/template-tauri-build-linux-x64.yml
index 7eb946706..6a15a2ee0 100644
--- a/.github/workflows/template-tauri-build-linux-x64.yml
+++ b/.github/workflows/template-tauri-build-linux-x64.yml
@@ -119,8 +119,8 @@ jobs:
"usr/lib/Jan/binaries/libvulkan.so": "binaries/libvulkan.so"}' ./src-tauri/tauri.conf.json > /tmp/tauri.conf.json
fi
mv /tmp/tauri.conf.json ./src-tauri/tauri.conf.json
- jq --arg version "${{ inputs.new_version }}" '.version = $version' web/package.json > /tmp/package.json
- mv /tmp/package.json web/package.json
+ jq --arg version "${{ inputs.new_version }}" '.version = $version' web-app/package.json > /tmp/package.json
+ mv /tmp/package.json web-app/package.json
ctoml ./src-tauri/Cargo.toml package.version "${{ inputs.new_version }}"
cat ./src-tauri/Cargo.toml
diff --git a/.github/workflows/template-tauri-build-macos.yml b/.github/workflows/template-tauri-build-macos.yml
index 9ad2cf1ba..e3c457433 100644
--- a/.github/workflows/template-tauri-build-macos.yml
+++ b/.github/workflows/template-tauri-build-macos.yml
@@ -120,8 +120,8 @@ jobs:
# Update tauri.conf.json
jq --arg version "${{ inputs.new_version }}" '.version = $version | .bundle.createUpdaterArtifacts = true' ./src-tauri/tauri.conf.json > /tmp/tauri.conf.json
mv /tmp/tauri.conf.json ./src-tauri/tauri.conf.json
- jq --arg version "${{ inputs.new_version }}" '.version = $version' web/package.json > /tmp/package.json
- mv /tmp/package.json web/package.json
+ jq --arg version "${{ inputs.new_version }}" '.version = $version' web-app/package.json > /tmp/package.json
+ mv /tmp/package.json web-app/package.json
ctoml ./src-tauri/Cargo.toml package.version "${{ inputs.new_version }}"
cat ./src-tauri/Cargo.toml
diff --git a/.github/workflows/template-tauri-build-windows-x64.yml b/.github/workflows/template-tauri-build-windows-x64.yml
index e6f948c9a..54c236485 100644
--- a/.github/workflows/template-tauri-build-windows-x64.yml
+++ b/.github/workflows/template-tauri-build-windows-x64.yml
@@ -97,8 +97,8 @@ jobs:
# Update tauri.conf.json
jq --arg version "${{ inputs.new_version }}" '.version = $version | .bundle.createUpdaterArtifacts = true | .bundle.windows.nsis.template = "tauri.bundle.windows.nsis.template"' ./src-tauri/tauri.conf.json > /tmp/tauri.conf.json
mv /tmp/tauri.conf.json ./src-tauri/tauri.conf.json
- jq --arg version "${{ inputs.new_version }}" '.version = $version' web/package.json > /tmp/package.json
- mv /tmp/package.json web/package.json
+ jq --arg version "${{ inputs.new_version }}" '.version = $version' web-app/package.json > /tmp/package.json
+ mv /tmp/package.json web-app/package.json
ctoml ./src-tauri/Cargo.toml package.version "${{ inputs.new_version }}"
echo "---------Cargo.toml---------"
diff --git a/Makefile b/Makefile
index db83e6b21..56b50a9d2 100644
--- a/Makefile
+++ b/Makefile
@@ -24,88 +24,26 @@ ifeq ($(OS),Windows_NT)
echo "skip"
endif
yarn install
- yarn build:joi
yarn build:core
yarn build:extensions
-check-file-counts: install-and-build
-ifeq ($(OS),Windows_NT)
- powershell -Command "if ((Get-ChildItem -Path pre-install -Filter *.tgz | Measure-Object | Select-Object -ExpandProperty Count) -ne (Get-ChildItem -Path extensions -Directory | Where-Object Name -like *-extension* | Measure-Object | Select-Object -ExpandProperty Count)) { Write-Host 'Number of .tgz files in pre-install does not match the number of subdirectories in extensions with package.json'; exit 1 } else { Write-Host 'Extension build successful' }"
-else
- @tgz_count=$$(find pre-install -type f -name "*.tgz" | wc -l); dir_count=$$(find extensions -mindepth 1 -maxdepth 1 -type d -exec test -e '{}/package.json' \; -print | wc -l); if [ $$tgz_count -ne $$dir_count ]; then echo "Number of .tgz files in pre-install ($$tgz_count) does not match the number of subdirectories in extension ($$dir_count)"; exit 1; else echo "Extension build successful"; fi
-endif
-
-dev: check-file-counts
- yarn dev
-
-dev-tauri: check-file-counts
+dev: install-and-build
yarn install:cortex
yarn download:bin
yarn copy:lib
- CLEAN=true yarn dev:tauri
+ yarn dev
+
+# Deprecated soon
+dev-tauri: install-and-build
+ yarn install:cortex
+ yarn download:bin
+ yarn copy:lib
+ yarn dev:tauri
# Linting
-lint: check-file-counts
+lint: install-and-build
yarn lint
-update-playwright-config:
-ifeq ($(OS),Windows_NT)
- echo -e "const RPconfig = {\n\
- apiKey: '$(REPORT_PORTAL_API_KEY)',\n\
- endpoint: '$(REPORT_PORTAL_URL)',\n\
- project: '$(REPORT_PORTAL_PROJECT_NAME)',\n\
- launch: '$(REPORT_PORTAL_LAUNCH_NAME)',\n\
- attributes: [\n\
- {\n\
- key: 'key',\n\
- value: 'value',\n\
- },\n\
- {\n\
- value: 'value',\n\
- },\n\
- ],\n\
- description: '$(REPORT_PORTAL_DESCRIPTION)',\n\
- }\n$$(cat electron/playwright.config.ts)" > electron/playwright.config.ts;
- sed -i "s/^ reporter: .*/ reporter: [['@reportportal\/agent-js-playwright', RPconfig]],/" electron/playwright.config.ts
-
-else ifeq ($(shell uname -s),Linux)
- echo "const RPconfig = {\n\
- apiKey: '$(REPORT_PORTAL_API_KEY)',\n\
- endpoint: '$(REPORT_PORTAL_URL)',\n\
- project: '$(REPORT_PORTAL_PROJECT_NAME)',\n\
- launch: '$(REPORT_PORTAL_LAUNCH_NAME)',\n\
- attributes: [\n\
- {\n\
- key: 'key',\n\
- value: 'value',\n\
- },\n\
- {\n\
- value: 'value',\n\
- },\n\
- ],\n\
- description: '$(REPORT_PORTAL_DESCRIPTION)',\n\
- }\n$$(cat electron/playwright.config.ts)" > electron/playwright.config.ts;
- sed -i "s/^ reporter: .*/ reporter: [['@reportportal\/agent-js-playwright', RPconfig]],/" electron/playwright.config.ts
-else
- echo "const RPconfig = {\n\
- apiKey: '$(REPORT_PORTAL_API_KEY)',\n\
- endpoint: '$(REPORT_PORTAL_URL)',\n\
- project: '$(REPORT_PORTAL_PROJECT_NAME)',\n\
- launch: '$(REPORT_PORTAL_LAUNCH_NAME)',\n\
- attributes: [\n\
- {\n\
- key: 'key',\n\
- value: 'value',\n\
- },\n\
- {\n\
- value: 'value',\n\
- },\n\
- ],\n\
- description: '$(REPORT_PORTAL_DESCRIPTION)',\n\
- }\n$$(cat electron/playwright.config.ts)" > electron/playwright.config.ts;
- sed -i '' "s|^ reporter: .*| reporter: [['@reportportal\/agent-js-playwright', RPconfig]],|" electron/playwright.config.ts
-endif
-
# Testing
test: lint
# yarn build:test
@@ -114,16 +52,17 @@ test: lint
yarn test
# Builds and publishes the app
-build-and-publish: check-file-counts
- yarn build:publish
-
-# Build
-build: check-file-counts
+build-and-publish: install-and-build
yarn build
-build-tauri: check-file-counts
+# Build
+build: install-and-build
+ yarn build
+
+# Deprecated soon
+build-tauri: install-and-build
yarn copy:lib
- yarn build-tauri
+ yarn build
clean:
ifeq ($(OS),Windows_NT)
diff --git a/electron/.eslintrc.js b/electron/.eslintrc.js
deleted file mode 100644
index 20e79804f..000000000
--- a/electron/.eslintrc.js
+++ /dev/null
@@ -1,46 +0,0 @@
-module.exports = {
- root: true,
- parser: '@typescript-eslint/parser',
- plugins: ['@typescript-eslint'],
- env: {
- node: true,
- },
- extends: [
- 'eslint:recommended',
- 'plugin:@typescript-eslint/recommended',
- 'plugin:react/recommended',
- ],
- rules: {
- 'react/prop-types': 'off', // In favor of strong typing - no need to dedupe
- 'react/no-is-mounted': 'off',
- '@typescript-eslint/no-non-null-assertion': 'off',
- '@typescript-eslint/no-var-requires': 'off',
- '@typescript-eslint/ban-ts-comment': 'off',
- '@typescript-eslint/no-unused-vars': 'off',
- '@typescript-eslint/no-explicit-any': 'off',
- },
- settings: {
- react: {
- createClass: 'createReactClass', // Regex for Component Factory to use,
- // default to "createReactClass"
- pragma: 'React', // Pragma to use, default to "React"
- version: 'detect', // React version. "detect" automatically picks the version you have installed.
- // You can also use `16.0`, `16.3`, etc, if you want to override the detected value.
- // default to latest and warns if missing
- // It will default to "detect" in the future
- },
- linkComponents: [
- // Components used as alternatives to for linking, eg.
- 'Hyperlink',
- { name: 'Link', linkAttribute: 'to' },
- ],
- },
- ignorePatterns: [
- 'build',
- 'renderer',
- 'node_modules',
- '@global',
- 'playwright-report',
- 'test-data',
- ],
-}
diff --git a/electron/@global/index.ts b/electron/@global/index.ts
deleted file mode 100644
index b2d55fc1c..000000000
--- a/electron/@global/index.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-export {}
-
-declare global {
- namespace NodeJS {
- interface Global {
- core: any
- }
- }
- var core: any | undefined
-}
diff --git a/electron/entitlements.mac.plist b/electron/entitlements.mac.plist
deleted file mode 100644
index ad77a2a1e..000000000
--- a/electron/entitlements.mac.plist
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
- com.apple.security.cs.allow-jit
-
- com.apple.security.cs.allow-unsigned-executable-memory
-
- com.apple.security.cs.allow-dyld-environment-variables
-
- com.apple.security.cs.disable-library-validation
-
-
-
\ No newline at end of file
diff --git a/electron/handlers/common.ts b/electron/handlers/common.ts
deleted file mode 100644
index a2a1bd2f7..000000000
--- a/electron/handlers/common.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { Handler, RequestHandler } from '@janhq/core/node'
-import { ipcMain } from 'electron'
-import { windowManager } from '../managers/window'
-
-export function injectHandler() {
- const ipcWrapper: Handler = (
- route: string,
- listener: (...args: any[]) => any
- ) =>
- ipcMain.handle(route, async (_event, ...args: any[]) => {
- return listener(...args)
- })
-
- const handler = new RequestHandler(
- ipcWrapper,
- (channel: string, args: any) =>
- windowManager.mainWindow?.webContents.send(channel, args)
- )
- handler.handle()
-}
diff --git a/electron/handlers/native.ts b/electron/handlers/native.ts
deleted file mode 100644
index f8f70c302..000000000
--- a/electron/handlers/native.ts
+++ /dev/null
@@ -1,327 +0,0 @@
-import { app, ipcMain, dialog, shell, nativeTheme } from 'electron'
-import { autoUpdater } from 'electron-updater'
-import { join } from 'path'
-import { windowManager } from '../managers/window'
-import {
- ModuleManager,
- getJanDataFolderPath,
- getJanExtensionsPath,
- init,
- AppEvent,
- NativeRoute,
- SelectFileProp,
-} from '@janhq/core/node'
-import { SelectFileOption } from '@janhq/core'
-import { menu } from '../utils/menu'
-import { migrate } from '../utils/migration'
-import { createUserSpace } from '../utils/path'
-import { setupExtensions } from '../utils/extension'
-
-const isMac = process.platform === 'darwin'
-
-export function handleAppIPCs() {
- /**
- * Handles the "openAppDirectory" IPC message by opening the app's user data directory.
- * The `shell.openPath` method is used to open the directory in the user's default file explorer.
- * @param _event - The IPC event object.
- */
- ipcMain.handle(NativeRoute.openAppDirectory, async (_event) => {
- shell.openPath(getJanDataFolderPath())
- })
-
- ipcMain.handle(NativeRoute.appUpdateDownload, async (_event) => {
- autoUpdater.downloadUpdate()
- })
-
- /**
- * Handles the "setNativeThemeLight" IPC message by setting the native theme source to "light".
- * This will change the appearance of the app to the light theme.
- */
- ipcMain.handle(NativeRoute.setNativeThemeLight, () => {
- nativeTheme.themeSource = 'light'
- })
-
- /**
- * Handles the "setCloseApp" IPC message by closing the main application window.
- * This effectively closes the application if no other windows are open.
- */
- ipcMain.handle(NativeRoute.setCloseApp, () => {
- windowManager.mainWindow?.close()
- })
-
- /**
- * Handles the "setMinimizeApp" IPC message by minimizing the main application window.
- * The window will be minimized to the system's taskbar or dock.
- */
- ipcMain.handle(NativeRoute.setMinimizeApp, () => {
- windowManager.mainWindow?.minimize()
- })
-
- /**
- * Handles the "setMaximizeApp" IPC message. It toggles the maximization state of the main window.
- * If the window is currently maximized, it will be un-maximized (restored to its previous size).
- * If the window is not maximized, it will be maximized to fill the screen.
- * @param _event - The IPC event object.
- */
- ipcMain.handle(NativeRoute.setMaximizeApp, async (_event) => {
- if (windowManager.mainWindow?.isMaximized()) {
- windowManager.mainWindow.unmaximize()
- } else {
- windowManager.mainWindow?.maximize()
- }
- })
-
- /**
- * Handles the "setNativeThemeDark" IPC message by setting the native theme source to "dark".
- * This will change the appearance of the app to the dark theme.
- */
- ipcMain.handle(NativeRoute.setNativeThemeDark, () => {
- nativeTheme.themeSource = 'dark'
- })
-
- /**
- * Opens a URL in the user's default browser.
- * @param _event - The IPC event object.
- * @param url - The URL to open.
- */
- ipcMain.handle(NativeRoute.openExternalUrl, async (_event, url) => {
- shell.openExternal(url)
- })
-
- /**
- * Opens a URL in the user's default browser.
- * @param _event - The IPC event object.
- * @param url - The URL to open.
- */
- ipcMain.handle(NativeRoute.openFileExplore, async (_event, url) => {
- shell.openPath(url)
- })
-
- /**
- * Relaunches the app in production - reload window in development.
- * @param _event - The IPC event object.
- * @param url - The URL to reload.
- */
- ipcMain.handle(NativeRoute.relaunch, async (_event) => {
- ModuleManager.instance.clearImportedModules()
-
- if (app.isPackaged) {
- app.relaunch()
- app.exit()
- } else {
- for (const modulePath in ModuleManager.instance.requiredModules) {
- delete require.cache[
- require.resolve(join(getJanExtensionsPath(), modulePath))
- ]
- }
- init({
- // Function to check from the main process that user wants to install a extension
- confirmInstall: async (_extensions: string[]) => {
- return true
- },
- // Path to install extension to
- extensionsPath: getJanExtensionsPath(),
- })
- windowManager.mainWindow?.reload()
- }
- })
-
- /**
- * Handles the "selectDirectory" IPC message to open a dialog for selecting a directory.
- * If no main window is found, logs an error and exits.
- * @returns {string} The path of the selected directory, or nothing if canceled.
- */
- ipcMain.handle(NativeRoute.selectDirectory, async () => {
- const mainWindow = windowManager.mainWindow
- if (!mainWindow) {
- console.error('No main window found')
- return
- }
- const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow, {
- title: 'Select a folder',
- buttonLabel: 'Select Folder',
- properties: ['openDirectory', 'createDirectory'],
- })
- if (canceled) {
- return
- } else {
- return filePaths[0]
- }
- })
-
- /**
- * Handles the "selectFiles" IPC message to open a dialog for selecting files.
- * Allows options for setting the dialog title, button label, and selection properties.
- * Logs an error if no main window is found.
- * @param _event - The IPC event object.
- * @param option - Options for customizing file selection dialog.
- * @returns {string[]} An array of selected file paths, or nothing if canceled.
- */
- ipcMain.handle(
- NativeRoute.selectFiles,
- async (_event, option?: SelectFileOption) => {
- const mainWindow = windowManager.mainWindow
- if (!mainWindow) {
- console.error('No main window found')
- return
- }
-
- const title = option?.title ?? 'Select files'
- const buttonLabel = option?.buttonLabel ?? 'Select'
- const props: SelectFileProp[] = ['openFile']
-
- if (option?.allowMultiple) {
- props.push('multiSelections')
- }
-
- if (option?.selectDirectory) {
- props.push('openDirectory')
- }
- console.debug(`Select files with props: ${props}`)
- const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow, {
- title,
- buttonLabel,
- properties: props,
- filters: option?.filters,
- })
-
- if (canceled) return
-
- return filePaths
- }
- )
-
- /**
- * Handles the "hideQuickAskWindow" IPC message to hide the quick ask window.
- * @returns A promise that resolves when the window is hidden.
- */
- ipcMain.handle(
- NativeRoute.hideQuickAskWindow,
- async (): Promise => windowManager.hideQuickAskWindow()
- )
-
- /**
- * Handles the "sendQuickAskInput" IPC message to send user input to the main window.
- * @param _event - The IPC event object.
- * @param input - User input string to be sent.
- */
- ipcMain.handle(
- NativeRoute.sendQuickAskInput,
- async (_event, input: string): Promise => {
- windowManager.mainWindow?.webContents.send(
- AppEvent.onUserSubmitQuickAsk,
- input
- )
- }
- )
-
- /**
- * Handles the "showOpenMenu" IPC message to show the context menu at given coordinates.
- * Only applicable on non-Mac platforms.
- * @param e - The event object.
- * @param args - Contains coordinates where the menu should appear.
- */
- ipcMain.handle(NativeRoute.showOpenMenu, function (e, args) {
- if (!isMac && windowManager.mainWindow) {
- menu.popup({
- window: windowManager.mainWindow,
- x: args.x,
- y: args.y,
- })
- }
- })
-
- /**
- * Handles the "hideMainWindow" IPC message to hide the main application window.
- * @returns A promise that resolves when the window is hidden.
- */
- ipcMain.handle(
- NativeRoute.hideMainWindow,
- async (): Promise => windowManager.hideMainWindow()
- )
-
- /**
- * Handles the "showMainWindow" IPC message to show the main application window.
- * @returns A promise that resolves when the window is shown.
- */
- ipcMain.handle(
- NativeRoute.showMainWindow,
- async (): Promise => windowManager.showMainWindow()
- )
-
- /**
- * Handles the "quickAskSizeUpdated" IPC message to update the size of the quick ask window.
- * Resizes window by the given height offset.
- * @param _event - The IPC event object.
- * @param heightOffset - The amount of height to increase.
- * @returns A promise that resolves when the window is resized.
- */
- ipcMain.handle(
- NativeRoute.quickAskSizeUpdated,
- async (_event, heightOffset: number): Promise =>
- windowManager.expandQuickAskWindow(heightOffset)
- )
-
- /**
- * Handles the "ackDeepLink" IPC message to acknowledge a deep link.
- * Triggers handling of deep link in the application.
- * @param _event - The IPC event object.
- * @returns A promise that resolves when the deep link is acknowledged.
- */
- ipcMain.handle(NativeRoute.ackDeepLink, async (_event): Promise => {
- windowManager.ackDeepLink()
- })
-
- /**
- * Handles the "factoryReset" IPC message to reset the application to its initial state.
- * Clears loaded modules, recreates user space, runs migrations, and sets up extensions.
- * @param _event - The IPC event object.
- * @returns A promise that resolves after the reset operations are complete.
- */
- ipcMain.handle(NativeRoute.factoryReset, async (_event): Promise => {
- ModuleManager.instance.clearImportedModules()
- return createUserSpace().then(migrate).then(setupExtensions)
- })
-
- /**
- * Handles the "startServer" IPC message to start the Jan API server.
- * Initializes and starts server with provided configuration options.
- * @param _event - The IPC event object.
- * @param args - Configuration object containing host, port, CORS settings etc.
- * @returns Promise that resolves when server starts successfully
- */
- ipcMain.handle(
- NativeRoute.startServer,
- async (_event, args): Promise => {
- const { startServer } = require('@janhq/server')
- return startServer({
- host: args?.host,
- port: args?.port,
- isCorsEnabled: args?.isCorsEnabled,
- isVerboseEnabled: args?.isVerboseEnabled,
- prefix: args?.prefix,
- })
- }
- )
-
- /**
- * Handles the "stopServer" IPC message to stop the Jan API server.
- * Gracefully shuts down the server instance.
- * @param _event - The IPC event object
- * @returns Promise that resolves when server stops successfully
- */
- ipcMain.handle(NativeRoute.stopServer, async (_event): Promise => {
- /**
- * Stop Jan API Server.
- */
- const { stopServer } = require('@janhq/server')
- return stopServer()
- })
-
- /**
- * Handles the "appToken" IPC message to generate a random app ID.
- */
- ipcMain.handle(NativeRoute.appToken, async (_event): Promise => {
- return process.env.appToken ?? 'cortex.cpp'
- })
-}
diff --git a/electron/handlers/update.ts b/electron/handlers/update.ts
deleted file mode 100644
index 5dcbda582..000000000
--- a/electron/handlers/update.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-import { app, dialog } from 'electron'
-import { windowManager } from './../managers/window'
-import {
- ProgressInfo,
- UpdateDownloadedEvent,
- UpdateInfo,
- autoUpdater,
-} from 'electron-updater'
-import { AppEvent } from '@janhq/core/node'
-import { trayManager } from '../managers/tray'
-
-export let waitingToInstallVersion: string | undefined = undefined
-
-export function handleAppUpdates() {
- /* Should not check for update during development */
- if (!app.isPackaged) {
- return
- }
-
- /* New Update Available */
- autoUpdater.on('update-available', async (_info: UpdateInfo) => {
- windowManager.mainWindow?.webContents.send(
- AppEvent.onAppUpdateAvailable,
- {}
- )
- })
-
- /* App Update Completion Message */
- autoUpdater.on('update-downloaded', async (_info: UpdateDownloadedEvent) => {
- windowManager.mainWindow?.webContents.send(
- AppEvent.onAppUpdateDownloadSuccess,
- {}
- )
- const action = await dialog.showMessageBox({
- message: `Update downloaded. Please restart the application to apply the updates.`,
- buttons: ['Restart', 'Later'],
- })
- if (action.response === 0) {
- trayManager.destroyCurrentTray()
- windowManager.closeQuickAskWindow()
- waitingToInstallVersion = _info?.version
- autoUpdater.quitAndInstall()
- }
- })
-
- /* App Update Error */
- autoUpdater.on('error', (info: Error) => {
- windowManager.mainWindow?.webContents.send(
- AppEvent.onAppUpdateDownloadError,
- { failedToInstallVersion: waitingToInstallVersion, info }
- )
- })
-
- /* App Update Progress */
- autoUpdater.on('download-progress', (progress: ProgressInfo) => {
- console.debug('app update progress: ', progress.percent)
- windowManager.mainWindow?.webContents.send(
- AppEvent.onAppUpdateDownloadUpdate,
- {
- ...progress,
- }
- )
- })
-
- autoUpdater.autoDownload = false
- autoUpdater.autoInstallOnAppQuit = true
- if (process.env.CI !== 'e2e') {
- autoUpdater.checkForUpdates()
- }
-}
diff --git a/electron/icons/512x512.png b/electron/icons/512x512.png
deleted file mode 100644
index 289f99ded..000000000
Binary files a/electron/icons/512x512.png and /dev/null differ
diff --git a/electron/icons/icon-tray.png b/electron/icons/icon-tray.png
deleted file mode 100644
index ab356a9dc..000000000
Binary files a/electron/icons/icon-tray.png and /dev/null differ
diff --git a/electron/icons/icon-tray@2x.png b/electron/icons/icon-tray@2x.png
deleted file mode 100644
index a82c285f5..000000000
Binary files a/electron/icons/icon-tray@2x.png and /dev/null differ
diff --git a/electron/icons/icon.ico b/electron/icons/icon.ico
deleted file mode 100644
index 5d18719e8..000000000
Binary files a/electron/icons/icon.ico and /dev/null differ
diff --git a/electron/icons/icon.png b/electron/icons/icon.png
deleted file mode 100644
index 289f99ded..000000000
Binary files a/electron/icons/icon.png and /dev/null differ
diff --git a/electron/icons_dev/jan-beta-512x512.png b/electron/icons_dev/jan-beta-512x512.png
deleted file mode 100644
index 4b715494d..000000000
Binary files a/electron/icons_dev/jan-beta-512x512.png and /dev/null differ
diff --git a/electron/icons_dev/jan-beta-tray.png b/electron/icons_dev/jan-beta-tray.png
deleted file mode 100644
index eaca9ad9a..000000000
Binary files a/electron/icons_dev/jan-beta-tray.png and /dev/null differ
diff --git a/electron/icons_dev/jan-beta-tray@2x.png b/electron/icons_dev/jan-beta-tray@2x.png
deleted file mode 100644
index deb83aace..000000000
Binary files a/electron/icons_dev/jan-beta-tray@2x.png and /dev/null differ
diff --git a/electron/icons_dev/jan-beta.ico b/electron/icons_dev/jan-beta.ico
deleted file mode 100644
index 85cf0c1b4..000000000
Binary files a/electron/icons_dev/jan-beta.ico and /dev/null differ
diff --git a/electron/icons_dev/jan-beta.png b/electron/icons_dev/jan-beta.png
deleted file mode 100644
index 4b715494d..000000000
Binary files a/electron/icons_dev/jan-beta.png and /dev/null differ
diff --git a/electron/icons_dev/jan-nightly-512x512.png b/electron/icons_dev/jan-nightly-512x512.png
deleted file mode 100644
index 23f532947..000000000
Binary files a/electron/icons_dev/jan-nightly-512x512.png and /dev/null differ
diff --git a/electron/icons_dev/jan-nightly-tray.png b/electron/icons_dev/jan-nightly-tray.png
deleted file mode 100644
index bf164a0a6..000000000
Binary files a/electron/icons_dev/jan-nightly-tray.png and /dev/null differ
diff --git a/electron/icons_dev/jan-nightly-tray@2x.png b/electron/icons_dev/jan-nightly-tray@2x.png
deleted file mode 100644
index 3cab5709d..000000000
Binary files a/electron/icons_dev/jan-nightly-tray@2x.png and /dev/null differ
diff --git a/electron/icons_dev/jan-nightly.ico b/electron/icons_dev/jan-nightly.ico
deleted file mode 100644
index 8e64ba8b1..000000000
Binary files a/electron/icons_dev/jan-nightly.ico and /dev/null differ
diff --git a/electron/icons_dev/jan-nightly.png b/electron/icons_dev/jan-nightly.png
deleted file mode 100644
index 23f532947..000000000
Binary files a/electron/icons_dev/jan-nightly.png and /dev/null differ
diff --git a/electron/jest.config.js b/electron/jest.config.js
deleted file mode 100644
index ec5968ccd..000000000
--- a/electron/jest.config.js
+++ /dev/null
@@ -1,18 +0,0 @@
-module.exports = {
- preset: 'ts-jest',
- testEnvironment: 'node',
- collectCoverageFrom: ['src/**/*.{ts,tsx}'],
- modulePathIgnorePatterns: ['/tests'],
- moduleNameMapper: {
- '@/(.*)': '/src/$1',
- },
- runner: './testRunner.js',
- transform: {
- '^.+\\.tsx?$': [
- 'ts-jest',
- {
- diagnostics: false,
- },
- ],
- },
-}
diff --git a/electron/main.ts b/electron/main.ts
deleted file mode 100644
index 59e72ca24..000000000
--- a/electron/main.ts
+++ /dev/null
@@ -1,161 +0,0 @@
-import { app, BrowserWindow } from 'electron'
-
-import { join, resolve } from 'path'
-/**
- * Managers
- **/
-import { windowManager } from './managers/window'
-import { getAppConfigurations, log } from '@janhq/core/node'
-
-/**
- * IPC Handlers
- **/
-import { injectHandler } from './handlers/common'
-import { handleAppUpdates } from './handlers/update'
-import { handleAppIPCs } from './handlers/native'
-
-/**
- * Utils
- **/
-import { setupMenu } from './utils/menu'
-import { createUserSpace } from './utils/path'
-import { migrate } from './utils/migration'
-import { cleanUpAndQuit } from './utils/clean'
-import { setupExtensions } from './utils/extension'
-import { setupCore } from './utils/setup'
-import { setupReactDevTool } from './utils/dev'
-
-import { trayManager } from './managers/tray'
-import { logSystemInfo } from './utils/system'
-import { registerGlobalShortcuts } from './utils/shortcut'
-import { registerLogger } from './utils/logger'
-import { randomBytes } from 'crypto'
-
-const preloadPath = join(__dirname, 'preload.js')
-const preloadQuickAskPath = join(__dirname, 'preload.quickask.js')
-const rendererPath = join(__dirname, '..', 'renderer')
-const quickAskPath = join(rendererPath, 'search.html')
-const mainPath = join(rendererPath, 'index.html')
-
-const mainUrl = 'http://localhost:3000'
-const quickAskUrl = `${mainUrl}/search`
-
-const gotTheLock = app.requestSingleInstanceLock()
-
-if (process.defaultApp) {
- if (process.argv.length >= 2) {
- app.setAsDefaultProtocolClient('jan', process.execPath, [
- resolve(process.argv[1]),
- ])
- }
-} else {
- app.setAsDefaultProtocolClient('jan')
-}
-
-const createMainWindow = () => {
- const startUrl = app.isPackaged ? `file://${mainPath}` : mainUrl
- windowManager.createMainWindow(preloadPath, startUrl)
-}
-
-// Generate a random token for the app
-// This token is used for authentication when making request to cortex.cpp server
-process.env.appToken = randomBytes(16).toString('hex')
-
-app
- .whenReady()
- .then(() => {
- if (!gotTheLock) {
- app.quit()
- throw new Error('Another instance of the app is already running')
- } else {
- app.on(
- 'second-instance',
- (_event, commandLine, _workingDirectory): void => {
- if (process.platform === 'win32' || process.platform === 'linux') {
- // this is for handling deeplink on windows and linux
- // since those OS will emit second-instance instead of open-url
- const url = commandLine.pop()
- if (url) {
- windowManager.sendMainAppDeepLink(url)
- }
- }
- windowManager.showMainWindow()
- }
- )
- }
- })
- .then(setupCore)
- .then(createUserSpace)
- .then(registerLogger)
- .then(migrate)
- .then(setupExtensions)
- .then(setupMenu)
- .then(handleIPCs)
- .then(() => process.env.CI !== 'e2e' && createQuickAskWindow())
- .then(createMainWindow)
- .then(handleAppUpdates)
- .then(registerGlobalShortcuts)
- .then(() => {
- if (!app.isPackaged) {
- setupReactDevTool()
- windowManager.mainWindow?.webContents.openDevTools()
- }
- })
- .then(() => process.env.CI !== 'e2e' && trayManager.createSystemTray())
- .then(logSystemInfo)
- .then(() => {
- app.on('activate', () => {
- if (!BrowserWindow.getAllWindows().length) {
- createMainWindow()
- } else {
- windowManager.showMainWindow()
- }
- })
- })
-
-app.on('open-url', (_event, url) => {
- windowManager.sendMainAppDeepLink(url)
-})
-
-app.on('before-quit', function (_event) {
- trayManager.destroyCurrentTray()
-})
-
-app.once('quit', () => {
- cleanUpAndQuit()
-})
-
-app.once('window-all-closed', () => {
- // Feature Toggle for Quick Ask
- if (
- getAppConfigurations().quick_ask &&
- !windowManager.isQuickAskWindowDestroyed()
- )
- return
- cleanUpAndQuit()
-})
-
-function createQuickAskWindow() {
- // Feature Toggle for Quick Ask
- if (!getAppConfigurations().quick_ask) return
- const startUrl = app.isPackaged ? `file://${quickAskPath}` : quickAskUrl
- windowManager.createQuickAskWindow(preloadQuickAskPath, startUrl)
-}
-
-/**
- * Handles various IPC messages from the renderer process.
- */
-function handleIPCs() {
- // Inject core handlers for IPCs
- injectHandler()
-
- // Handle native IPCs
- handleAppIPCs()
-}
-
-/*
- ** Suppress Node error messages
- */
-process.on('uncaughtException', function (err) {
- log(`Error: ${err}`)
-})
diff --git a/electron/managers/mainWindowConfig.ts b/electron/managers/mainWindowConfig.ts
deleted file mode 100644
index 997d081c3..000000000
--- a/electron/managers/mainWindowConfig.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-const DEFAULT_MIN_WIDTH = 400
-const DEFAULT_MIN_HEIGHT = 600
-
-export const mainWindowConfig: Electron.BrowserWindowConstructorOptions = {
- skipTaskbar: false,
- minWidth: DEFAULT_MIN_WIDTH,
- minHeight: DEFAULT_MIN_HEIGHT,
- show: true,
- // we want to go frameless on windows and linux
- transparent: process.platform === 'darwin',
- frame: process.platform === 'darwin',
- titleBarStyle: 'hiddenInset',
- vibrancy: 'fullscreen-ui',
- visualEffectState: 'active',
- backgroundMaterial: 'acrylic',
- autoHideMenuBar: true,
- trafficLightPosition: {
- x: 16,
- y: 10,
- },
-}
diff --git a/electron/managers/quickAskWindowConfig.ts b/electron/managers/quickAskWindowConfig.ts
deleted file mode 100644
index 93180dd07..000000000
--- a/electron/managers/quickAskWindowConfig.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-const DEFAULT_WIDTH = 556
-
-const DEFAULT_HEIGHT = 60
-
-export const quickAskWindowConfig: Electron.BrowserWindowConstructorOptions = {
- width: DEFAULT_WIDTH,
- height: DEFAULT_HEIGHT,
- skipTaskbar: true,
- acceptFirstMouse: true,
- hasShadow: true,
- alwaysOnTop: true,
- show: false,
- fullscreenable: false,
- resizable: false,
- center: true,
- movable: true,
- maximizable: false,
- focusable: true,
- transparent: false,
- frame: false,
- type: 'panel',
-}
diff --git a/electron/managers/tray.ts b/electron/managers/tray.ts
deleted file mode 100644
index b81b1e556..000000000
--- a/electron/managers/tray.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import { join } from 'path'
-import { Tray, app, Menu } from 'electron'
-import { windowManager } from '../managers/window'
-import { getAppConfigurations } from '@janhq/core/node'
-
-class TrayManager {
- currentTray: Tray | undefined
-
- createSystemTray = () => {
- // Feature Toggle for Quick Ask
- if (!getAppConfigurations().quick_ask) return
-
- if (this.currentTray) {
- return
- }
- const iconPath = join(app.getAppPath(), 'icons', 'icon-tray.png')
- const tray = new Tray(iconPath)
- tray.setToolTip(app.getName())
-
- tray.on('click', () => {
- windowManager.showQuickAskWindow()
- })
-
- // Add context menu for windows only
- if (process.platform === 'win32') {
- const contextMenu = Menu.buildFromTemplate([
- {
- label: 'Open Jan',
- type: 'normal',
- click: () => windowManager.showMainWindow(),
- },
- {
- label: 'Open Quick Ask',
- type: 'normal',
- click: () => windowManager.showQuickAskWindow(),
- },
- { label: 'Quit', type: 'normal', click: () => app.quit() },
- ])
-
- tray.setContextMenu(contextMenu)
- }
- this.currentTray = tray
- }
-
- destroyCurrentTray() {
- this.currentTray?.destroy()
- this.currentTray = undefined
- }
-}
-
-export const trayManager = new TrayManager()
diff --git a/electron/managers/window.ts b/electron/managers/window.ts
deleted file mode 100644
index dbb3a5101..000000000
--- a/electron/managers/window.ts
+++ /dev/null
@@ -1,215 +0,0 @@
-import { BrowserWindow, app, shell } from 'electron'
-import { quickAskWindowConfig } from './quickAskWindowConfig'
-import { mainWindowConfig } from './mainWindowConfig'
-import { getAppConfigurations, AppEvent } from '@janhq/core/node'
-import { getBounds, saveBounds } from '../utils/setup'
-
-/**
- * Manages the current window instance.
- */
-// TODO: refactor this
-let isAppQuitting = false
-
-class WindowManager {
- public mainWindow?: BrowserWindow
- private _quickAskWindow: BrowserWindow | undefined = undefined
- private _quickAskWindowVisible = false
- private _mainWindowVisible = false
-
- private deeplink: string | undefined
- /**
- * Creates a new window instance.
- * @returns The created window instance.
- */
- async createMainWindow(preloadPath: string, startUrl: string) {
- const bounds = await getBounds()
-
- this.mainWindow = new BrowserWindow({
- ...mainWindowConfig,
- width: bounds.width,
- height: bounds.height,
- show: false,
- x: bounds.x,
- y: bounds.y,
- webPreferences: {
- nodeIntegration: true,
- preload: preloadPath,
- webSecurity: false,
- },
- })
-
- if (process.platform === 'win32' || process.platform === 'linux') {
- /// This is work around for windows deeplink.
- /// second-instance event is not fired when app is not open, so the app
- /// does not received the deeplink.
- const commandLine = process.argv.slice(1)
- if (commandLine.length > 0) {
- const url = commandLine[0]
- this.sendMainAppDeepLink(url)
- }
- }
-
- this.mainWindow.on('resized', () => {
- saveBounds(this.mainWindow?.getBounds())
- })
-
- this.mainWindow.on('moved', () => {
- saveBounds(this.mainWindow?.getBounds())
- })
-
- /* Load frontend app to the window */
- this.mainWindow.loadURL(startUrl)
-
- /* Open external links in the default browser */
- this.mainWindow.webContents.setWindowOpenHandler(({ url }) => {
- shell.openExternal(url)
- return { action: 'deny' }
- })
-
- app.on('before-quit', function () {
- isAppQuitting = true
- })
-
- windowManager.mainWindow?.on('close', function (evt) {
- // Feature Toggle for Quick Ask
- if (!getAppConfigurations().quick_ask) return
-
- if (!isAppQuitting) {
- evt.preventDefault()
- windowManager.hideMainWindow()
- }
- })
-
- windowManager.mainWindow?.on('ready-to-show', function () {
- windowManager.mainWindow?.show()
- })
- }
-
- createQuickAskWindow(preloadPath: string, startUrl: string): void {
- this._quickAskWindow = new BrowserWindow({
- ...quickAskWindowConfig,
- webPreferences: {
- nodeIntegration: true,
- preload: preloadPath,
- webSecurity: false,
- },
- })
-
- this._quickAskWindow.loadURL(startUrl)
- this._quickAskWindow.on('blur', () => {
- this.hideQuickAskWindow()
- })
- }
-
- isMainWindowVisible(): boolean {
- return this._mainWindowVisible
- }
-
- hideMainWindow(): void {
- this.mainWindow?.hide()
- this._mainWindowVisible = false
- }
-
- showMainWindow(): void {
- this.mainWindow?.show()
- this._mainWindowVisible = true
- }
-
- hideQuickAskWindow(): void {
- this._quickAskWindow?.hide()
- this._quickAskWindowVisible = false
- }
-
- showQuickAskWindow(): void {
- this._quickAskWindow?.show()
- this._quickAskWindowVisible = true
- }
-
- closeQuickAskWindow(): void {
- if (this._quickAskWindow?.isDestroyed()) return
- this._quickAskWindow?.close()
- this._quickAskWindow?.destroy()
- this._quickAskWindow = undefined
- this._quickAskWindowVisible = false
- }
-
- isQuickAskWindowVisible(): boolean {
- return this._quickAskWindowVisible
- }
-
- isQuickAskWindowDestroyed(): boolean {
- return this._quickAskWindow?.isDestroyed() ?? true
- }
-
- /**
- * Expand the quick ask window
- */
- expandQuickAskWindow(heightOffset: number): void {
- const width = quickAskWindowConfig.width!
- const height = quickAskWindowConfig.height! + heightOffset
- this._quickAskWindow?.setMinimumSize(width, height)
- this._quickAskWindow?.setSize(width, height, true)
- }
-
- /**
- * Send the selected text to the quick ask window.
- */
- sendQuickAskSelectedText(selectedText: string): void {
- this._quickAskWindow?.webContents.send(
- AppEvent.onSelectedText,
- selectedText
- )
- }
-
- /**
- * Try to send the deep link to the main app.
- */
- sendMainAppDeepLink(url: string): void {
- this.deeplink = url
- const interval = setInterval(() => {
- if (!this.deeplink) clearInterval(interval)
- const mainWindow = this.mainWindow
- if (mainWindow) {
- mainWindow.webContents.send(AppEvent.onDeepLink, this.deeplink)
- if (mainWindow.isMinimized()) mainWindow.restore()
- mainWindow.focus()
- }
- }, 500)
- }
-
- /**
- * Send main view state to the main app.
- */
- sendMainViewState(route: string) {
- if (this.mainWindow && !this.mainWindow.isDestroyed()) {
- this.mainWindow.webContents.send(AppEvent.onMainViewStateChange, route)
- }
- }
-
- /**
- * Clean up all windows.
- */
- cleanUp(): void {
- if (!this.mainWindow?.isDestroyed()) {
- this.mainWindow?.close()
- this.mainWindow?.destroy()
- this.mainWindow = undefined
- this._mainWindowVisible = false
- }
- if (!this._quickAskWindow?.isDestroyed()) {
- this._quickAskWindow?.close()
- this._quickAskWindow?.destroy()
- this._quickAskWindow = undefined
- this._quickAskWindowVisible = false
- }
- }
-
- /**
- * Acknowledges that the window has received a deep link. We can remove it.
- */
- ackDeepLink() {
- this.deeplink = undefined
- }
-}
-
-export const windowManager = new WindowManager()
diff --git a/electron/merge-latest-ymls.js b/electron/merge-latest-ymls.js
deleted file mode 100644
index ee8caf825..000000000
--- a/electron/merge-latest-ymls.js
+++ /dev/null
@@ -1,29 +0,0 @@
-const yaml = require('js-yaml')
-const fs = require('fs')
-
-// get two file paths from arguments:
-const [, , ...args] = process.argv
-const file1 = args[0]
-const file2 = args[1]
-const file3 = args[2]
-
-// check that all arguments are present and throw error instead
-if (!file1 || !file2 || !file3) {
- throw new Error(
- 'Please provide 3 file paths as arguments: path to file1, to file2 and destination path'
- )
-}
-
-const doc1 = yaml.load(fs.readFileSync(file1, 'utf8'))
-console.log('doc1: ', doc1)
-
-const doc2 = yaml.load(fs.readFileSync(file2, 'utf8'))
-console.log('doc2: ', doc2)
-
-const merged = { ...doc1, ...doc2 }
-merged.files.push(...doc1.files)
-
-console.log('merged', merged)
-
-const mergedYml = yaml.dump(merged)
-fs.writeFileSync(file3, mergedYml, 'utf8')
diff --git a/electron/package.json b/electron/package.json
deleted file mode 100644
index 8b673114b..000000000
--- a/electron/package.json
+++ /dev/null
@@ -1,146 +0,0 @@
-{
- "name": "jan",
- "version": "0.1.1740752217",
- "main": "./build/main.js",
- "author": "Jan ",
- "license": "MIT",
- "productName": "Jan",
- "homepage": "https://github.com/menloresearch/jan/tree/main/electron",
- "description": "Use offline LLMs with your own data. Run open source models like Llama2 or Falcon on your internal computers/servers.",
- "build": {
- "appId": "jan.ai.app",
- "productName": "Jan",
- "files": [
- "renderer/**/*",
- "build/**/*.{js,map}",
- "pre-install",
- "themes",
- "scripts/**/*",
- "icons/**/*",
- "themes",
- "shared"
- ],
- "asarUnpack": [
- "pre-install",
- "themes",
- "docs",
- "scripts",
- "icons",
- "themes",
- "shared"
- ],
- "publish": [
- {
- "provider": "github",
- "owner": "janhq",
- "repo": "jan"
- }
- ],
- "extends": null,
- "mac": {
- "type": "distribution",
- "entitlements": "./entitlements.mac.plist",
- "entitlementsInherit": "./entitlements.mac.plist",
- "notarize": {
- "teamId": "F8AH6NHVY5"
- },
- "icon": "icons/icon.png"
- },
- "linux": {
- "target": [
- "deb"
- ],
- "category": "Utility",
- "icon": "icons/"
- },
- "win": {
- "icon": "icons/icon.png",
- "target": [
- "nsis"
- ]
- },
- "nsis": {
- "oneClick": true,
- "installerIcon": "icons/icon.ico",
- "uninstallerIcon": "icons/icon.ico",
- "include": "scripts/uninstaller.nsh",
- "deleteAppDataOnUninstall": true
- },
- "protocols": [
- {
- "name": "Jan",
- "schemes": [
- "jan"
- ]
- }
- ],
- "artifactName": "jan-${os}-${arch}-${version}.${ext}"
- },
- "scripts": {
- "lint": "eslint . --ext \".js,.jsx,.ts,.tsx\"",
- "test:e2e": "DEBUG=pw:browser xvfb-maybe -- playwright test --workers=1",
- "copy:assets": "rimraf --glob \"./pre-install/*.tgz\" && cpx \"../pre-install/*.tgz\" \"./pre-install\"",
- "version-patch": "run-script-os",
- "version-patch:darwin:linux": "jq '.version' package.json | tr -d '\"' > .version.bak && jq --arg ver \"0.1.$(date +%s)\" '.version = $ver' package.json > package.tmp && mv package.tmp package.json",
- "version-patch:win32": "node -e \"const fs=require('fs');const pkg=require('./package.json');const bak=pkg.version;fs.writeFileSync('.version.bak',bak);pkg.version='0.1.'+Math.floor(Date.now()/1000);fs.writeFileSync('package.json',JSON.stringify(pkg,null,2));\"",
- "version-restore": "run-script-os",
- "version-restore:darwin:linux": "jq --arg ver $(cat .version.bak) '.version = $ver' package.json > package.tmp && mv package.tmp package.json && rm .version.bak",
- "version-restore:win32": "node -e \"const fs=require('fs');const pkg=require('./package.json');const bak=fs.readFileSync('.version.bak','utf8');pkg.version=bak;fs.writeFileSync('package.json',JSON.stringify(pkg,null,2));\"",
- "dev:darwin:linux": "yarn copy:assets && tsc -p . && yarn version-patch && electron . && yarn version-restore",
- "dev:windows": "yarn copy:assets && tsc -p . && electron .",
- "dev": "run-script-os",
- "compile": "tsc -p .",
- "start": "electron .",
- "build": "yarn copy:assets && run-script-os",
- "build:test": "yarn copy:assets && run-script-os",
- "build:test:darwin": "tsc -p . && electron-builder -p never -m --dir",
- "build:test:win32": "tsc -p . && electron-builder -p never -w --dir",
- "build:test:linux": "tsc -p . && electron-builder -p never -l --dir",
- "build:darwin": "tsc -p . && electron-builder -p never -m --universal",
- "build:win32": "tsc -p . && electron-builder -p never -w",
- "build:linux": "tsc -p . && electron-builder -p never -l deb -l AppImage",
- "build:publish": "yarn copy:assets && run-script-os",
- "build:publish:darwin": "tsc -p . && electron-builder -p always -m --universal",
- "build:publish:win32": "tsc -p . && electron-builder -p always -w",
- "build:publish:linux": "tsc -p . && electron-builder -p always -l deb -l AppImage"
- },
- "dependencies": {
- "@alumna/reflect": "^1.1.3",
- "@janhq/core": "link:../core",
- "@janhq/server": "link:../server",
- "@kirillvakalov/nut-tree__nut-js": "4.2.1-2",
- "@npmcli/arborist": "^7.1.0",
- "electron-store": "^8.1.0",
- "electron-updater": "^6.1.7",
- "fs-extra": "^11.2.0",
- "pacote": "^21.0.0",
- "request": "^2.88.2",
- "request-progress": "^3.0.0",
- "ulidx": "^2.3.0"
- },
- "devDependencies": {
- "@electron/notarize": "^2.5.0",
- "@playwright/test": "^1.38.1",
- "@reportportal/agent-js-playwright": "^5.1.7",
- "@types/npmcli__arborist": "^5.6.4",
- "@types/pacote": "^11.1.7",
- "@types/request": "^2.48.12",
- "@typescript-eslint/eslint-plugin": "^6.7.3",
- "@typescript-eslint/parser": "^6.7.3",
- "electron": "30.0.6",
- "electron-builder": "^24.13.3",
- "electron-builder-squirrel-windows": "^24.13.3",
- "electron-devtools-installer": "^3.2.0",
- "electron-playwright-helpers": "^1.6.0",
- "eslint": "8.57.0",
- "eslint-plugin-react": "^7.34.0",
- "rimraf": "^5.0.5",
- "run-script-os": "^1.1.6",
- "typescript": "^5.3.3",
- "xvfb-maybe": "^0.2.1"
- },
- "installConfig": {
- "hoistingLimits": "workspaces"
- },
- "packageManager": "yarn@4.5.3"
-}
diff --git a/electron/playwright.config.ts b/electron/playwright.config.ts
deleted file mode 100644
index 71f435f57..000000000
--- a/electron/playwright.config.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { PlaywrightTestConfig } from '@playwright/test'
-
-const config: PlaywrightTestConfig = {
- testDir: './tests/e2e',
- retries: 0,
- globalTimeout: 350000,
- use: {
- screenshot: 'only-on-failure',
- video: 'retain-on-failure',
- trace: 'retain-on-failure',
- },
- // reporter: [['html', { outputFolder: './playwright-report' }]],
-}
-export default config
diff --git a/electron/pre-install/.gitkeep b/electron/pre-install/.gitkeep
deleted file mode 100644
index e69de29bb..000000000
diff --git a/electron/preload.quickask.ts b/electron/preload.quickask.ts
deleted file mode 100644
index 7c2cadeb6..000000000
--- a/electron/preload.quickask.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-/**
- * Exposes a set of APIs to the renderer process via the contextBridge object.
- * @module preload
- */
-
-import { APIEvents, APIRoutes } from '@janhq/core/node'
-import { contextBridge, ipcRenderer } from 'electron'
-
-const interfaces: { [key: string]: (...args: any[]) => any } = {}
-
-// Loop over each route in APIRoutes
-APIRoutes.forEach((method) => {
- // For each method, create a function on the interfaces object
- // This function invokes the method on the ipcRenderer with any provided arguments
-
- interfaces[method] = (...args: any[]) => ipcRenderer.invoke(method, ...args)
-})
-
-// Loop over each method in APIEvents
-APIEvents.forEach((method) => {
- // For each method, create a function on the interfaces object
- // This function sets up an event listener on the ipcRenderer for the method
- // The handler for the event is provided as an argument to the function
- interfaces[method] = (handler: any) => ipcRenderer.on(method, handler)
-})
-
-// Expose the 'interfaces' object in the main world under the name 'electronAPI'
-// This allows the renderer process to access these methods directly
-contextBridge.exposeInMainWorld('electronAPI', {
- ...interfaces,
- isQuickAsk: () => true,
-})
diff --git a/electron/preload.ts b/electron/preload.ts
deleted file mode 100644
index dbfcd1f1e..000000000
--- a/electron/preload.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-/**
- * Exposes a set of APIs to the renderer process via the contextBridge object.
- * @module preload
- */
-
-import { APIEvents, APIRoutes, AppConfiguration } from '@janhq/core/node'
-import { contextBridge, ipcRenderer } from 'electron'
-import { readdirSync } from 'fs'
-
-const interfaces: { [key: string]: (...args: any[]) => any } = {}
-
-// Loop over each route in APIRoutes
-APIRoutes.forEach((method) => {
- // For each method, create a function on the interfaces object
- // This function invokes the method on the ipcRenderer with any provided arguments
-
- interfaces[method] = (...args: any[]) => ipcRenderer.invoke(method, ...args)
-})
-
-// Loop over each method in APIEvents
-APIEvents.forEach((method) => {
- // For each method, create a function on the interfaces object
- // This function sets up an event listener on the ipcRenderer for the method
- // The handler for the event is provided as an argument to the function
- interfaces[method] = (handler: any) => ipcRenderer.on(method, handler)
-})
-
-interfaces['changeDataFolder'] = async (path) => {
- const appConfiguration: AppConfiguration = await ipcRenderer.invoke(
- 'getAppConfigurations'
- )
- const currentJanDataFolder = appConfiguration.data_folder
- appConfiguration.data_folder = path
- const reflect = require('@alumna/reflect')
- const { err } = await reflect({
- src: currentJanDataFolder,
- dest: path,
- recursive: true,
- delete: false,
- overwrite: true,
- errorOnExist: false,
- })
- if (err) {
- console.error(err)
- throw err
- }
- await ipcRenderer.invoke('updateAppConfiguration', appConfiguration)
-}
-
-interfaces['isDirectoryEmpty'] = async (path) => {
- const dirChildren = await readdirSync(path)
- return dirChildren.filter((x) => x !== '.DS_Store').length === 0
-}
-
-// Expose the 'interfaces' object in the main world under the name 'electronAPI'
-// This allows the renderer process to access these methods directly
-contextBridge.exposeInMainWorld('electronAPI', {
- ...interfaces,
- isQuickAsk: () => false,
-})
diff --git a/electron/scripts/uninstaller.nsh b/electron/scripts/uninstaller.nsh
deleted file mode 100644
index 684783258..000000000
--- a/electron/scripts/uninstaller.nsh
+++ /dev/null
@@ -1,46 +0,0 @@
-!include nsDialogs.nsh
-
-XPStyle on
-
-!macro customUnInstall
- ${ifNot} ${isUpdated}
- ; Define the process name of your Electron app
- StrCpy $0 "Jan.exe"
-
- ; Check if the application is running
- nsExec::ExecToStack 'tasklist /FI "IMAGENAME eq $0" /NH'
- Pop $1
-
- StrCmp $1 "" notRunning
-
- ; If the app is running, notify the user and attempt to close it
- MessageBox MB_OK "Jan is being uninstalled, force close app." IDOK forceClose
-
- forceClose:
- ; Attempt to kill the running application
- nsExec::ExecToStack 'taskkill /F /IM $0'
- Pop $1
-
- ; Proceed with uninstallation
- Goto continueUninstall
-
- notRunning:
- ; If the app is not running, proceed with uninstallation
- Goto continueUninstall
-
- continueUninstall:
- ; Proceed with uninstallation
- DeleteRegKey HKLM "Software\Jan"
- RMDir /r "$INSTDIR"
- Delete "$INSTDIR\*.*"
-
- ; Clean up shortcuts and app data
- Delete "$DESKTOP\Jan.lnk"
- Delete "$STARTMENU\Programs\Jan.lnk"
- RMDir /r "$APPDATA\Jan"
- RMDir /r "$LOCALAPPDATA\jan-updater"
-
- ; Close the uninstaller
- Quit
- ${endIf}
-!macroend
\ No newline at end of file
diff --git a/electron/shared/.gitkeep b/electron/shared/.gitkeep
deleted file mode 100644
index e69de29bb..000000000
diff --git a/electron/sign.js b/electron/sign.js
deleted file mode 100644
index 9955e53e8..000000000
--- a/electron/sign.js
+++ /dev/null
@@ -1,69 +0,0 @@
-const { exec } = require('child_process')
-
-function execCommandWithRetry(command, retries = 3) {
- return new Promise((resolve, reject) => {
- const execute = (attempt) => {
- exec(command, (error, stdout, stderr) => {
- if (error) {
- console.error(`Error: ${error}`)
- if (attempt < retries) {
- console.log(`Retrying... Attempt ${attempt + 1}`)
- execute(attempt + 1)
- } else {
- return reject(error)
- }
- } else {
- console.log(`stdout: ${stdout}`)
- console.error(`stderr: ${stderr}`)
- resolve()
- }
- })
- }
- execute(0)
- })
-}
-
-function sign({
- path,
- name,
- certUrl,
- clientId,
- tenantId,
- clientSecret,
- certName,
- timestampServer,
- version,
-}) {
- return new Promise((resolve, reject) => {
- const command = `azuresigntool.exe sign -kvu "${certUrl}" -kvi "${clientId}" -kvt "${tenantId}" -kvs "${clientSecret}" -kvc "${certName}" -tr "${timestampServer}" -v "${path}"`
- execCommandWithRetry(command)
- .then(resolve)
- .catch(reject)
- })
-}
-
-exports.default = async function (options) {
- const certUrl = process.env.AZURE_KEY_VAULT_URI
- const clientId = process.env.AZURE_CLIENT_ID
- const tenantId = process.env.AZURE_TENANT_ID
- const clientSecret = process.env.AZURE_CLIENT_SECRET
- const certName = process.env.AZURE_CERT_NAME
- const timestampServer = 'http://timestamp.globalsign.com/tsa/r6advanced1'
-
- try {
- await sign({
- path: options.path,
- name: 'jan-win-x64',
- certUrl,
- clientId,
- tenantId,
- clientSecret,
- certName,
- timestampServer,
- version: options.version,
- })
- } catch (error) {
- console.error('Failed to sign after 3 attempts:', error)
- process.exit(1)
- }
-}
diff --git a/electron/testRunner.js b/electron/testRunner.js
deleted file mode 100644
index b0d108160..000000000
--- a/electron/testRunner.js
+++ /dev/null
@@ -1,10 +0,0 @@
-const jestRunner = require('jest-runner');
-
-class EmptyTestFileRunner extends jestRunner.default {
- async runTests(tests, watcher, onStart, onResult, onFailure, options) {
- const nonEmptyTests = tests.filter(test => test.context.hasteFS.getSize(test.path) > 0);
- return super.runTests(nonEmptyTests, watcher, onStart, onResult, onFailure, options);
- }
-}
-
-module.exports = EmptyTestFileRunner;
\ No newline at end of file
diff --git a/electron/tests/config/constants.ts b/electron/tests/config/constants.ts
deleted file mode 100644
index 7039ad58c..000000000
--- a/electron/tests/config/constants.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export const Constants = {
- VIDEO_DIR: './playwright-video',
- TIMEOUT: '300000',
-}
diff --git a/electron/tests/config/fixtures.ts b/electron/tests/config/fixtures.ts
deleted file mode 100644
index c36910161..000000000
--- a/electron/tests/config/fixtures.ts
+++ /dev/null
@@ -1,126 +0,0 @@
-import {
- _electron as electron,
- BrowserContext,
- ElectronApplication,
- expect,
- Page,
- test as base,
-} from '@playwright/test'
-import {
- ElectronAppInfo,
- findLatestBuild,
- parseElectronApp,
- stubDialog,
-} from 'electron-playwright-helpers'
-import { Constants } from './constants'
-import { HubPage } from '../pages/hubPage'
-import { CommonActions } from '../pages/commonActions'
-import { rmSync } from 'fs'
-import * as path from 'path'
-
-export let electronApp: ElectronApplication
-export let page: Page
-export let appInfo: ElectronAppInfo
-export const TIMEOUT = parseInt(process.env.TEST_TIMEOUT || Constants.TIMEOUT)
-
-export async function setupElectron() {
- console.log(`TEST TIMEOUT: ${TIMEOUT}`)
-
- process.env.CI = 'e2e'
-
- const latestBuild = findLatestBuild('dist')
- expect(latestBuild).toBeTruthy()
-
- // parse the packaged Electron app and find paths and other info
- appInfo = parseElectronApp(latestBuild)
- expect(appInfo).toBeTruthy()
-
- electronApp = await electron.launch({
- args: [appInfo.main, '--no-sandbox'], // main file from package.json
- executablePath: appInfo.executable, // path to the Electron executable
- // recordVideo: { dir: Constants.VIDEO_DIR }, // Specify the directory for video recordings
- })
- await stubDialog(electronApp, 'showMessageBox', { response: 1 })
-
- page = await electronApp.firstWindow({
- timeout: TIMEOUT,
- })
-}
-
-export async function teardownElectron() {
- await page.close()
- await electronApp.close()
-}
-
-/**
- * this fixture is needed to record and attach videos / screenshot on failed tests when
- * tests are run in serial mode (i.e. browser is not closed between tests)
- */
-export const test = base.extend<
- {
- commonActions: CommonActions
- hubPage: HubPage
- attachVideoPage: Page
- attachScreenshotsToReport: void
- },
- { createVideoContext: BrowserContext }
->({
- commonActions: async ({ request }, use, testInfo) => {
- await use(new CommonActions(page, testInfo))
- },
- hubPage: async ({ commonActions }, use) => {
- await use(new HubPage(page, commonActions))
- },
- createVideoContext: [
- async ({ playwright }, use) => {
- const context = electronApp.context()
- await use(context)
- },
- { scope: 'worker' },
- ],
-
- attachVideoPage: [
- async ({ createVideoContext }, use, testInfo) => {
- await use(page)
-
- if (testInfo.status !== testInfo.expectedStatus) {
- const path = await createVideoContext.pages()[0].video()?.path()
- await createVideoContext.close()
- await testInfo.attach('video', {
- path: path,
- })
- }
- },
- { scope: 'test', auto: true },
- ],
-
- attachScreenshotsToReport: [
- async ({ commonActions }, use, testInfo) => {
- await use()
-
- // After the test, we can check whether the test passed or failed.
- if (testInfo.status !== testInfo.expectedStatus) {
- await commonActions.takeScreenshot('')
- }
- },
- { auto: true },
- ],
-})
-
-test.beforeAll(async () => {
- rmSync(path.join(__dirname, '../../test-data'), {
- recursive: true,
- force: true,
- })
-
- test.setTimeout(TIMEOUT)
- await setupElectron()
- await page.waitForSelector('img[alt="Jan - Logo"]', {
- state: 'visible',
- timeout: TIMEOUT,
- })
-})
-
-test.afterAll(async () => {
- // teardownElectron()
-})
diff --git a/electron/tests/e2e/hub.e2e.spec.ts b/electron/tests/e2e/hub.e2e.spec.ts
deleted file mode 100644
index 58d6a0854..000000000
--- a/electron/tests/e2e/hub.e2e.spec.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { test, appInfo, page, TIMEOUT } from '../config/fixtures'
-import { expect } from '@playwright/test'
-
-test.beforeAll(async () => {
- expect(appInfo).toMatchObject({
- asar: true,
- executable: expect.anything(),
- main: expect.anything(),
- name: 'jan',
- packageJson: expect.objectContaining({ name: 'jan' }),
- platform: process.platform,
- resourcesDir: expect.anything(),
- })
-})
-
-test('explores hub', async ({ hubPage }) => {
- await hubPage.navigateByMenu()
- await hubPage.verifyContainerVisible()
- await hubPage.scrollToBottom()
- const useModelBtn = page.getByTestId(/^setup-btn/).first()
-
- await expect(useModelBtn).toBeVisible({
- timeout: TIMEOUT,
- })
-})
diff --git a/electron/tests/e2e/navigation.e2e.spec.ts b/electron/tests/e2e/navigation.e2e.spec.ts
deleted file mode 100644
index 1b463d381..000000000
--- a/electron/tests/e2e/navigation.e2e.spec.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { expect } from '@playwright/test'
-import { page, test, TIMEOUT } from '../config/fixtures'
-
-test('renders left navigation panel', async () => {
- const threadBtn = page.getByTestId('Thread').first()
- await expect(threadBtn).toBeVisible({ timeout: TIMEOUT })
- // Chat section should be there
- await page.getByTestId('Local API Server').first().click({
- timeout: TIMEOUT,
- })
- const localServer = page.getByTestId('local-server-testid').first()
- await expect(localServer).toBeVisible({
- timeout: TIMEOUT,
- })
-})
diff --git a/electron/tests/e2e/settings.e2e.spec.ts b/electron/tests/e2e/settings.e2e.spec.ts
deleted file mode 100644
index 06b4d1acc..000000000
--- a/electron/tests/e2e/settings.e2e.spec.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { expect } from '@playwright/test'
-
-import { test, page, TIMEOUT } from '../config/fixtures'
-
-test('shows settings', async () => {
- await page.getByTestId('Settings').first().click({
- timeout: TIMEOUT,
- })
- const settingDescription = page.getByTestId('testid-setting-description')
- await expect(settingDescription).toBeVisible({ timeout: TIMEOUT })
-})
diff --git a/electron/tests/e2e/thread.e2e.spec.ts b/electron/tests/e2e/thread.e2e.spec.ts
deleted file mode 100644
index 41efc8437..000000000
--- a/electron/tests/e2e/thread.e2e.spec.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { expect } from '@playwright/test'
-import { page, test, TIMEOUT } from '../config/fixtures'
-
-test('show onboarding screen without any threads created or models downloaded', async () => {
- await page.getByTestId('Thread').first().click({
- timeout: TIMEOUT,
- })
- const denyButton = page.locator('[data-testid="btn-deny-product-analytics"]')
-
- if ((await denyButton.count()) > 0) {
- await denyButton.click({ force: true })
- }
-
- const onboardScreen = page.getByTestId('onboard-screen')
- await expect(onboardScreen).toBeVisible({
- timeout: TIMEOUT,
- })
-})
diff --git a/electron/tests/pages/basePage.ts b/electron/tests/pages/basePage.ts
deleted file mode 100644
index 11e3ba81a..000000000
--- a/electron/tests/pages/basePage.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import { Page, expect } from '@playwright/test'
-import { CommonActions } from './commonActions'
-import { TIMEOUT } from '../config/fixtures'
-
-export class BasePage {
- menuId: string
-
- constructor(
- protected readonly page: Page,
- readonly action: CommonActions,
- protected containerId: string
- ) {}
-
- public getValue(key: string) {
- return this.action.getValue(key)
- }
-
- public setValue(key: string, value: string) {
- this.action.setValue(key, value)
- }
-
- async takeScreenshot(name: string = '') {
- await this.action.takeScreenshot(name)
- }
-
- async navigateByMenu() {
- await this.clickFirstElement(this.menuId)
- }
-
- async clickFirstElement(testId: string) {
- await this.page.getByTestId(testId).first().click()
- }
-
- async verifyContainerVisible() {
- const container = this.page.getByTestId(this.containerId)
- expect(container.isVisible()).toBeTruthy()
- }
-
- async scrollToBottom() {
- await this.page.evaluate(() => {
- window.scrollTo(0, document.body.scrollHeight)
- })
- }
-
- async waitUpdateLoader() {
- await this.isElementVisible('img[alt="Jan - Logo"]')
- }
-
- //wait and find a specific element with its selector and return Visible
- async isElementVisible(selector: any) {
- let isVisible = true
- await this.page
- .waitForSelector(selector, { state: 'visible', timeout: TIMEOUT })
- .catch(() => {
- isVisible = false
- })
- return isVisible
- }
-}
diff --git a/electron/tests/pages/commonActions.ts b/electron/tests/pages/commonActions.ts
deleted file mode 100644
index 08ea15f92..000000000
--- a/electron/tests/pages/commonActions.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import { Page, TestInfo } from '@playwright/test'
-import { page } from '../config/fixtures'
-
-export class CommonActions {
- private testData = new Map()
-
- constructor(
- public page: Page,
- public testInfo: TestInfo
- ) {}
-
- async takeScreenshot(name: string) {
- const screenshot = await page.screenshot({
- fullPage: true,
- })
- const attachmentName = `${this.testInfo.title}_${name || new Date().toISOString().slice(5, 19).replace(/[-:]/g, '').replace('T', '_')}`
- await this.testInfo.attach(attachmentName.replace(/\s+/g, ''), {
- body: screenshot,
- contentType: 'image/png',
- })
- }
-
- async hooks() {
- console.log('hook from the scenario page')
- }
-
- setValue(key: string, value: string) {
- this.testData.set(key, value)
- }
-
- getValue(key: string) {
- return this.testData.get(key)
- }
-}
diff --git a/electron/tests/pages/hubPage.ts b/electron/tests/pages/hubPage.ts
deleted file mode 100644
index 0299ab15d..000000000
--- a/electron/tests/pages/hubPage.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { Page } from '@playwright/test'
-import { BasePage } from './basePage'
-import { CommonActions } from './commonActions'
-
-export class HubPage extends BasePage {
- readonly menuId: string = 'Hub'
- static readonly containerId: string = 'hub-container-test-id'
-
- constructor(
- public page: Page,
- readonly action: CommonActions
- ) {
- super(page, action, HubPage.containerId)
- }
-}
diff --git a/electron/tsconfig.json b/electron/tsconfig.json
deleted file mode 100644
index 5116f0e88..000000000
--- a/electron/tsconfig.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "compilerOptions": {
- "target": "es5",
- "module": "commonjs",
- "noImplicitAny": true,
- "sourceMap": true,
- "strict": true,
- "outDir": "./build",
- "rootDir": "./",
- "noEmitOnError": true,
- "esModuleInterop": true,
- "baseUrl": ".",
- "allowJs": true,
- "skipLibCheck": true,
- "paths": { "*": ["node_modules/*"] },
- "typeRoots": ["node_modules/@types"]
- },
- "ts-node": {
- "esm": true
- },
- "include": ["./**/*.ts"],
- "exclude": ["core", "build", "dist", "tests", "node_modules", "test-data"]
-}
diff --git a/electron/utils/clean.ts b/electron/utils/clean.ts
deleted file mode 100644
index 12a68d39e..000000000
--- a/electron/utils/clean.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { ModuleManager } from '@janhq/core/node'
-import { windowManager } from './../managers/window'
-import { dispose } from './disposable'
-import { app } from 'electron'
-
-export function cleanUpAndQuit() {
- if (!ModuleManager.instance.cleaningResource) {
- ModuleManager.instance.cleaningResource = true
- windowManager.cleanUp()
- dispose(ModuleManager.instance.requiredModules)
- ModuleManager.instance.clearImportedModules()
- app.quit()
- }
-}
diff --git a/electron/utils/dev.ts b/electron/utils/dev.ts
deleted file mode 100644
index bd510096b..000000000
--- a/electron/utils/dev.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-export const setupReactDevTool = async () => {
- // Which means you're running from source code
- const { default: installExtension, REACT_DEVELOPER_TOOLS } = await import(
- 'electron-devtools-installer'
- ) // Don't use import on top level, since the installer package is dev-only
- try {
- const name = await installExtension(REACT_DEVELOPER_TOOLS)
- console.debug(`Added Extension: ${name}`)
- } catch (err) {
- console.error('An error occurred while installing devtools:', err)
- // Only log the error and don't throw it because it's not critical
- }
-}
diff --git a/electron/utils/disposable.ts b/electron/utils/disposable.ts
deleted file mode 100644
index 59018a775..000000000
--- a/electron/utils/disposable.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-export function dispose(requiredModules: Record) {
- for (const key in requiredModules) {
- const module = requiredModules[key]
- if (typeof module['dispose'] === 'function') {
- module['dispose']()
- }
- }
-}
diff --git a/electron/utils/extension.ts b/electron/utils/extension.ts
deleted file mode 100644
index e055411a6..000000000
--- a/electron/utils/extension.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { getJanExtensionsPath, init } from '@janhq/core/node'
-
-export const setupExtensions = async () => {
- init({
- // Function to check from the main process that user wants to install a extension
- confirmInstall: async (_extensions: string[]) => {
- return true
- },
- // Path to install extension to
- extensionsPath: getJanExtensionsPath(),
- })
-}
diff --git a/electron/utils/logger.ts b/electron/utils/logger.ts
deleted file mode 100644
index 48af0b93a..000000000
--- a/electron/utils/logger.ts
+++ /dev/null
@@ -1,167 +0,0 @@
-import {
- createWriteStream,
- existsSync,
- mkdirSync,
- readdir,
- stat,
- unlink,
- writeFileSync,
-} from 'fs'
-import util from 'util'
-import {
- getAppConfigurations,
- getJanDataFolderPath,
- Logger,
- LoggerManager,
-} from '@janhq/core/node'
-import path, { join } from 'path'
-
-/**
- * File Logger
- */
-export class FileLogger implements Logger {
- name = 'file'
- logCleaningInterval: number = 120000
- timeout: NodeJS.Timeout | undefined
- appLogPath: string = './'
- logEnabled: boolean = true
-
- constructor(
- logEnabled: boolean = true,
- logCleaningInterval: number = 120000
- ) {
- this.logEnabled = logEnabled
- if (logCleaningInterval) this.logCleaningInterval = logCleaningInterval
-
- const appConfigurations = getAppConfigurations()
- const logFolderPath = join(appConfigurations.data_folder, 'logs')
- if (!existsSync(logFolderPath)) {
- mkdirSync(logFolderPath, { recursive: true })
- }
-
- this.appLogPath = join(logFolderPath, 'app.log')
- }
-
- log(args: any) {
- if (!this.logEnabled) return
- let message = args[0]
- const scope = args[1]
- if (!message) return
- const path = this.appLogPath
- if (!scope && !message.startsWith('[')) {
- message = `[APP]::${message}`
- } else if (scope) {
- message = `${scope}::${message}`
- }
-
- message = `${new Date().toISOString()} ${message}`
-
- writeLog(message, path)
- }
-
- cleanLogs(
- maxFileSizeBytes?: number | undefined,
- daysToKeep?: number | undefined
- ): void {
- // clear existing timeout
- // in case we rerun it with different values
- if (this.timeout) clearTimeout(this.timeout)
- this.timeout = undefined
-
- if (!this.logEnabled) return
-
- console.log(
- 'Validating app logs. Next attempt in ',
- this.logCleaningInterval
- )
-
- const size = maxFileSizeBytes ?? 1 * 1024 * 1024 // 1 MB
- const days = daysToKeep ?? 7 // 7 days
- const logDirectory = path.join(getJanDataFolderPath(), 'logs')
- // Perform log cleaning
- const currentDate = new Date()
- if (existsSync(logDirectory))
- readdir(logDirectory, (err, files) => {
- if (err) {
- console.error('Error reading log directory:', err)
- return
- }
-
- files.forEach((file) => {
- const filePath = path.join(logDirectory, file)
- stat(filePath, (err, stats) => {
- if (err) {
- console.error('Error getting file stats:', err)
- return
- }
-
- // Check size
- if (stats.size > size) {
- unlink(filePath, (err) => {
- if (err) {
- console.error('Error deleting log file:', err)
- return
- }
- console.debug(
- `Deleted log file due to exceeding size limit: ${filePath}`
- )
- })
- } else {
- // Check age
- const creationDate = new Date(stats.ctime)
- const daysDifference = Math.floor(
- (currentDate.getTime() - creationDate.getTime()) /
- (1000 * 3600 * 24)
- )
- if (daysDifference > days) {
- unlink(filePath, (err) => {
- if (err) {
- console.error('Error deleting log file:', err)
- return
- }
- console.debug(`Deleted old log file: ${filePath}`)
- })
- }
- }
- })
- })
- })
-
- // Schedule the next execution with doubled delays
- this.timeout = setTimeout(
- () => this.cleanLogs(maxFileSizeBytes, daysToKeep),
- this.logCleaningInterval
- )
- }
-}
-
-/**
- * Write log function implementation
- * @param message
- * @param logPath
- */
-const writeLog = (message: string, logPath: string) => {
- if (!existsSync(logPath)) {
- const logDirectory = path.join(getJanDataFolderPath(), 'logs')
- if (!existsSync(logDirectory)) {
- mkdirSync(logDirectory)
- }
- writeFileSync(logPath, message)
- } else {
- const logFile = createWriteStream(logPath, {
- flags: 'a',
- })
- logFile.write(util.format(message) + '\n')
- logFile.close()
- console.debug(message)
- }
-}
-
-/**
- * Register logger for global application logging
- */
-export const registerLogger = () => {
- const logger = new FileLogger()
- LoggerManager.instance().register(logger)
- logger.cleanLogs()
-}
diff --git a/electron/utils/menu.ts b/electron/utils/menu.ts
deleted file mode 100644
index bab70da79..000000000
--- a/electron/utils/menu.ts
+++ /dev/null
@@ -1,129 +0,0 @@
-// @ts-nocheck
-import { app, Menu, shell, dialog } from 'electron'
-import { autoUpdater } from 'electron-updater'
-import { log } from '@janhq/core/node'
-const isMac = process.platform === 'darwin'
-import { windowManager } from '../managers/window'
-
-const template: (Electron.MenuItemConstructorOptions | Electron.MenuItem)[] = [
- {
- label: app.name,
- submenu: [
- {
- label: `About ${app.name}`,
- click: () =>
- dialog.showMessageBox({
- title: `Jan`,
- message: `Jan Version v${app.getVersion()}\n\nCopyright © 2024 Jan`,
- }),
- },
- {
- label: 'Check for Updates...',
- click: () =>
- // Check for updates and notify user if there are any
- autoUpdater
- .checkForUpdatesAndNotify()
- .then((updateCheckResult) => {
- if (
- !updateCheckResult?.updateInfo ||
- updateCheckResult?.updateInfo.version === app.getVersion()
- ) {
- windowManager.mainWindow?.webContents.send(
- AppEvent.onAppUpdateNotAvailable,
- {}
- )
- return
- }
- })
- .catch((error) => {
- log('Error checking for updates:' + JSON.stringify(error))
- }),
- },
- { type: 'separator' },
- { role: 'services' },
- { type: 'separator' },
- { role: 'hide' },
- { role: 'hideOthers' },
- { role: 'unhide' },
- {
- label: `Settings`,
- accelerator: 'CmdOrCtrl+,',
- click: () => {
- windowManager.showMainWindow()
- windowManager.sendMainViewState('Settings')
- },
- },
- { type: 'separator' },
- { role: 'quit' },
- ],
- },
- {
- label: 'Edit',
- submenu: [
- { role: 'undo' },
- { role: 'redo' },
- { type: 'separator' },
- { role: 'cut' },
- { role: 'copy' },
- { role: 'paste' },
- ...(isMac
- ? [
- { role: 'pasteAndMatchStyle' },
- { role: 'delete' },
- { role: 'selectAll' },
- { type: 'separator' },
- {
- label: 'Speech',
- submenu: [{ role: 'startSpeaking' }, { role: 'stopSpeaking' }],
- },
- ]
- : [{ role: 'delete' }, { type: 'separator' }, { role: 'selectAll' }]),
- ],
- },
- {
- label: 'View',
- submenu: [
- { role: 'reload' },
- { role: 'forceReload' },
- { role: 'toggleDevTools' },
- { type: 'separator' },
- { role: 'resetZoom' },
- { role: 'zoomIn' },
- { role: 'zoomOut' },
- { type: 'separator' },
- { role: 'togglefullscreen' },
- ],
- },
- {
- label: 'Window',
- submenu: [
- { role: 'minimize' },
- { role: 'zoom' },
- ...(isMac
- ? [
- { type: 'separator' },
- { role: 'front' },
- { type: 'separator' },
- { role: 'window' },
- ]
- : [{ role: 'close' }]),
- ],
- },
- {
- role: 'help',
- submenu: [
- {
- label: 'Learn More',
- click: async () => {
- await shell.openExternal('https://jan.ai/guides/')
- },
- },
- ],
- },
-]
-
-export const menu = Menu.buildFromTemplate(template)
-
-export const setupMenu = () => {
- Menu.setApplicationMenu(menu)
-}
diff --git a/electron/utils/migration.ts b/electron/utils/migration.ts
deleted file mode 100644
index 505de0f7b..000000000
--- a/electron/utils/migration.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-import { app } from 'electron'
-
-import { join } from 'path'
-import {
- rmdirSync,
- existsSync,
- mkdirSync,
- readdirSync,
- cpSync,
- lstatSync,
-} from 'fs'
-import Store from 'electron-store'
-import {
- getJanDataFolderPath,
- appResourcePath,
- getJanExtensionsPath,
-} from '@janhq/core/node'
-
-/**
- * Migrates the extensions & themes.
- * If the `migrated_version` key in the `Store` object does not match the current app version,
- * the function deletes the `extensions` directory and sets the `migrated_version` key to the current app version.
- * @returns A Promise that resolves when the migration is complete.
- */
-export async function migrate() {
- const store = new Store()
- if (store.get('migrated_version') !== app.getVersion()) {
- console.debug('start migration:', store.get('migrated_version'))
-
- if (existsSync(getJanExtensionsPath()))
- rmdirSync(getJanExtensionsPath(), { recursive: true })
-
- await migrateThemes()
-
- store.set('migrated_version', app.getVersion())
- console.debug('migrate extensions done')
- } else if (!existsSync(join(getJanDataFolderPath(), 'themes'))) {
- await migrateThemes()
- }
-}
-
-async function migrateThemes() {
- if (!existsSync(join(getJanDataFolderPath(), 'themes')))
- mkdirSync(join(getJanDataFolderPath(), 'themes'), { recursive: true })
-
- const themes = readdirSync(join(appResourcePath(), 'themes'))
- for (const theme of themes) {
- const themePath = join(appResourcePath(), 'themes', theme)
- await checkAndMigrateTheme(theme, themePath)
- }
-}
-
-async function checkAndMigrateTheme(
- sourceThemeName: string,
- sourceThemePath: string
-) {
- const janDataThemesFolder = join(getJanDataFolderPath(), 'themes')
- const existingTheme = readdirSync(janDataThemesFolder).find(
- (theme) => theme === sourceThemeName
- )
- if (existingTheme) {
- const desTheme = join(janDataThemesFolder, existingTheme)
- if (!lstatSync(desTheme).isDirectory()) {
- return
- }
- console.debug('Updating theme', existingTheme)
- rmdirSync(desTheme, { recursive: true })
- cpSync(sourceThemePath, join(janDataThemesFolder, sourceThemeName), {
- recursive: true,
- })
- } else {
- console.debug('Adding new theme', sourceThemeName)
- cpSync(sourceThemePath, join(janDataThemesFolder, sourceThemeName), {
- recursive: true,
- })
- }
-}
diff --git a/electron/utils/path.ts b/electron/utils/path.ts
deleted file mode 100644
index 4438156bc..000000000
--- a/electron/utils/path.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { mkdir } from 'fs-extra'
-import { existsSync } from 'fs'
-import { getJanDataFolderPath } from '@janhq/core/node'
-
-export async function createUserSpace(): Promise {
- const janDataFolderPath = getJanDataFolderPath()
- if (!existsSync(janDataFolderPath)) {
- try {
- await mkdir(janDataFolderPath)
- } catch (err) {
- console.error(
- `Unable to create Jan data folder at ${janDataFolderPath}: ${err}`
- )
- }
- }
-}
diff --git a/electron/utils/selectedText.ts b/electron/utils/selectedText.ts
deleted file mode 100644
index 51b2eb762..000000000
--- a/electron/utils/selectedText.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import { clipboard, globalShortcut } from 'electron'
-import { keyboard, Key } from "@kirillvakalov/nut-tree__nut-js"
-
-/**
- * Gets selected text by synthesizing the keyboard shortcut
- * "CommandOrControl+c" then reading text from the clipboard
- */
-export const getSelectedText = async () => {
- const currentClipboardContent = clipboard.readText() // preserve clipboard content
- clipboard.clear()
- const hotkeys: Key[] = [
- process.platform === 'darwin' ? Key.LeftCmd : Key.LeftControl,
- Key.C,
- ]
- await keyboard.pressKey(...hotkeys)
- await keyboard.releaseKey(...hotkeys)
- await new Promise((resolve) => setTimeout(resolve, 200)) // add a delay before checking clipboard
- const selectedText = clipboard.readText()
- clipboard.writeText(currentClipboardContent)
- return selectedText
-}
-
-/**
- * Registers a global shortcut of `accelerator`. The `callback` is called
- * with the selected text when the registered shortcut is pressed by the user
- *
- * Returns `true` if the shortcut was registered successfully
- */
-export const registerShortcut = (
- accelerator: Electron.Accelerator,
- callback: (selectedText: string) => void
-) => {
- return globalShortcut.register(accelerator, async () => {
- callback(await getSelectedText())
- })
-}
-
-/**
- * Unregisters a global shortcut of `accelerator` and
- * is equivalent to electron.globalShortcut.unregister
- */
-export const unregisterShortcut = (accelerator: Electron.Accelerator) => {
- globalShortcut.unregister(accelerator)
-}
diff --git a/electron/utils/setup.ts b/electron/utils/setup.ts
deleted file mode 100644
index 39b8a4133..000000000
--- a/electron/utils/setup.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import { app, screen } from 'electron'
-import Store from 'electron-store'
-
-const DEFAULT_WIDTH = 1000
-const DEFAULT_HEIGHT = 800
-
-const storage = new Store()
-
-export const setupCore = async () => {
- // Setup core api for main process
- global.core = {
- // Define appPath function for app to retrieve app path globally
- appPath: () => app.getPath('userData'),
- }
-}
-
-export const getBounds = async () => {
- const defaultBounds = {
- x: undefined,
- y: undefined,
- width: DEFAULT_WIDTH,
- height: DEFAULT_HEIGHT,
- }
-
- const bounds = (await storage.get('windowBounds')) as
- | Electron.Rectangle
- | undefined
-
- // If no bounds are saved, use the defaults
- if (!bounds) {
- storage.set('windowBounds', defaultBounds)
- return defaultBounds
- }
-
- // Validate that the bounds are on a valid display
- const displays = screen.getAllDisplays()
- const isValid = displays.some((display) => {
- const { x, y, width, height } = display.bounds
- return (
- bounds.x >= x &&
- bounds.x < x + width &&
- bounds.y >= y &&
- bounds.y < y + height
- )
- })
-
- // If the position is valid, return the saved bounds, otherwise return default bounds
- if (isValid) {
- return bounds
- } else {
- const primaryDisplay = screen.getPrimaryDisplay()
- const resetBounds = {
- x: primaryDisplay.bounds.x,
- y: primaryDisplay.bounds.y,
- width: DEFAULT_WIDTH,
- height: DEFAULT_HEIGHT,
- }
- storage.set('windowBounds', resetBounds)
- return resetBounds
- }
-}
-
-export const saveBounds = (bounds: Electron.Rectangle | undefined) => {
- storage.set('windowBounds', bounds)
-}
diff --git a/electron/utils/shortcut.ts b/electron/utils/shortcut.ts
deleted file mode 100644
index aa4607d9a..000000000
--- a/electron/utils/shortcut.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { getAppConfigurations } from '@janhq/core/node'
-import { registerShortcut } from './selectedText'
-import { windowManager } from '../managers/window'
-// TODO: Retrieve from config later
-const quickAskHotKey = 'CommandOrControl+J'
-
-export function registerGlobalShortcuts() {
- if (!getAppConfigurations().quick_ask) return
- const ret = registerShortcut(quickAskHotKey, (selectedText: string) => {
- // Feature Toggle for Quick Ask
- if (!windowManager.isQuickAskWindowVisible()) {
- windowManager.showQuickAskWindow()
- windowManager.sendQuickAskSelectedText(selectedText)
- } else {
- windowManager.hideQuickAskWindow()
- }
- })
-
- if (!ret) {
- console.error('Global shortcut registration failed')
- } else {
- console.log('Global shortcut registered successfully')
- }
-}
diff --git a/electron/utils/system.ts b/electron/utils/system.ts
deleted file mode 100644
index 5799de861..000000000
--- a/electron/utils/system.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { log } from '@janhq/core/node'
-import { app } from 'electron'
-import os from 'os'
-
-export const logSystemInfo = (): void => {
- log(`[SPECS]::Version: ${app.getVersion()}`)
- log(`[SPECS]::CPUs: ${JSON.stringify(os.cpus())}`)
- log(`[SPECS]::Machine: ${os.machine()}`)
- log(`[SPECS]::Endianness: ${os.endianness()}`)
- log(`[SPECS]::Parallelism: ${os.availableParallelism()}`)
- log(`[SPECS]::Free Mem: ${os.freemem()}`)
- log(`[SPECS]::Total Mem: ${os.totalmem()}`)
- log(`[SPECS]::OS Version: ${os.version()}`)
- log(`[SPECS]::OS Platform: ${os.platform()}`)
- log(`[SPECS]::OS Release: ${os.release()}`)
-}
diff --git a/extensions/model-extension/package.json b/extensions/model-extension/package.json
index 9ce48da88..153c22fdf 100644
--- a/extensions/model-extension/package.json
+++ b/extensions/model-extension/package.json
@@ -2,7 +2,7 @@
"name": "@janhq/model-extension",
"productName": "Model Management",
"version": "1.0.36",
- "description": "Handles model list, and settings.",
+ "description": "Manages model operations including listing, importing, updating, and deleting.",
"main": "dist/index.js",
"author": "Jan ",
"license": "AGPL-3.0",
diff --git a/joi/.prettierignore b/joi/.prettierignore
deleted file mode 100644
index e9e840d7e..000000000
--- a/joi/.prettierignore
+++ /dev/null
@@ -1,6 +0,0 @@
-.next/
-node_modules/
-dist/
-*.hbs
-*.mdx
-*.mjs
\ No newline at end of file
diff --git a/joi/.prettierrc b/joi/.prettierrc
deleted file mode 100644
index 933d88d62..000000000
--- a/joi/.prettierrc
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "semi": false,
- "singleQuote": true,
- "quoteProps": "consistent",
- "trailingComma": "es5",
- "endOfLine": "lf",
- "plugins": ["prettier-plugin-tailwindcss"]
-}
diff --git a/joi/README.md b/joi/README.md
deleted file mode 100644
index 161db4156..000000000
--- a/joi/README.md
+++ /dev/null
@@ -1,13 +0,0 @@
-# @janhq/joi
-
-To install dependencies:
-
-```bash
-yarn install
-```
-
-To run:
-
-```bash
-yarn run dev
-```
diff --git a/joi/jest.config.js b/joi/jest.config.js
deleted file mode 100644
index 676042491..000000000
--- a/joi/jest.config.js
+++ /dev/null
@@ -1,9 +0,0 @@
-module.exports = {
- preset: 'ts-jest',
- testEnvironment: 'node',
- roots: ['/src'],
- testMatch: ['**/*.test.*'],
- collectCoverageFrom: ['src/**/*.{ts,tsx}'],
- setupFilesAfterEnv: ['/jest.setup.js'],
- testEnvironment: 'jsdom',
-}
diff --git a/joi/jest.setup.js b/joi/jest.setup.js
deleted file mode 100644
index e69de29bb..000000000
diff --git a/joi/package.json b/joi/package.json
deleted file mode 100644
index 2f7d771d5..000000000
--- a/joi/package.json
+++ /dev/null
@@ -1,78 +0,0 @@
-{
- "name": "@janhq/joi",
- "version": "0.0.0",
- "main": "dist/esm/index.js",
- "types": "dist/index.d.ts",
- "description": "A collection of UI component",
- "files": [
- "dist"
- ],
- "keywords": [
- "design-system"
- ],
- "license": "MIT",
- "homepage": "https://github.com/codecentrum/piksel#readme",
- "repository": {
- "type": "git",
- "url": "https://github.com/codecentrum/piksel.git"
- },
- "bugs": "https://github.com/codecentrum/piksel/issues",
- "scripts": {
- "dev": "rollup -c -w",
- "build": "rimraf ./dist || true && rollup -c",
- "test": "jest"
- },
- "peerDependencies": {
- "class-variance-authority": "^0.7.0",
- "react": "^18",
- "typescript": "^5.0.0"
- },
- "dependencies": {
- "@radix-ui/react-accordion": "^1.1.2",
- "@radix-ui/react-dialog": "^1.0.5",
- "@radix-ui/react-dropdown-menu": "^2.1.4",
- "@radix-ui/react-icons": "^1.3.0",
- "@radix-ui/react-scroll-area": "^1.0.5",
- "@radix-ui/react-select": "^2.0.0",
- "@radix-ui/react-slider": "^1.1.2",
- "@radix-ui/react-slot": "^1.0.2",
- "@radix-ui/react-tabs": "^1.0.4",
- "@radix-ui/react-tooltip": "^1.0.7",
- "@types/jest": "^29.5.12",
- "autoprefixer": "10.4.16",
- "jest": "^29.7.0",
- "tailwind-merge": "^2.2.0",
- "tailwindcss": "^3.4.1",
- "ts-jest": "^29.2.5"
- },
- "devDependencies": {
- "@rollup/plugin-node-resolve": "15.2.3",
- "@rollup/plugin-terser": "0.4.4",
- "@testing-library/dom": "10.4.0",
- "@testing-library/jest-dom": "^6.5.0",
- "@testing-library/react": "^16.0.1",
- "@testing-library/user-event": "^14.5.2",
- "@types/jest": "^29.5.12",
- "@types/react": "^18.3.12",
- "@types/react-dom": "^19",
- "class-variance-authority": "^0.7.0",
- "jest-environment-jsdom": "^29.7.0",
- "jest-transform-css": "^6.0.1",
- "prettier": "^3.0.3",
- "prettier-plugin-tailwindcss": "^0.5.6",
- "react": "^19.0.0",
- "react-dom": "^19.0.0",
- "rimraf": "^6.0.1",
- "rollup": "4.12.0",
- "rollup-plugin-bundle-size": "1.0.3",
- "rollup-plugin-commonjs": "10.1.0",
- "rollup-plugin-copy": "3.5.0",
- "rollup-plugin-dts": "6.1.0",
- "rollup-plugin-peer-deps-external": "2.2.4",
- "rollup-plugin-postcss": "4.0.2",
- "rollup-plugin-typescript2": "0.36.0",
- "sass": "^1.83.1",
- "typescript": "^5.7.2"
- },
- "packageManager": "yarn@4.5.3"
-}
diff --git a/joi/rollup.config.mjs b/joi/rollup.config.mjs
deleted file mode 100644
index 6577135dc..000000000
--- a/joi/rollup.config.mjs
+++ /dev/null
@@ -1,79 +0,0 @@
-import { readFileSync } from 'fs'
-import dts from 'rollup-plugin-dts'
-import terser from '@rollup/plugin-terser'
-import autoprefixer from 'autoprefixer'
-import commonjs from 'rollup-plugin-commonjs'
-import bundleSize from 'rollup-plugin-bundle-size'
-import peerDepsExternal from 'rollup-plugin-peer-deps-external'
-import postcss from 'rollup-plugin-postcss'
-import typescript from 'rollup-plugin-typescript2'
-import tailwindcss from 'tailwindcss'
-import typescriptEngine from 'typescript'
-import resolve from '@rollup/plugin-node-resolve'
-import copy from 'rollup-plugin-copy'
-
-const packageJson = JSON.parse(readFileSync('./package.json'))
-
-import tailwindConfig from './tailwind.config.js'
-
-export default [
- {
- input: `./src/index.ts`,
- output: [
- {
- file: packageJson.main,
- format: 'es',
- exports: 'named',
- sourcemap: false,
- },
- ],
- external: ['react', 'typescript', 'class-variance-authority'],
- plugins: [
- postcss({
- plugins: [autoprefixer(), tailwindcss(tailwindConfig)],
- sourceMap: true,
- use: {
- sass: {
- silenceDeprecations: ['legacy-js-api'],
- api: 'modern',
- },
- },
- minimize: true,
- extract: 'main.css',
- }),
-
- peerDepsExternal({ includeDependencies: true }),
- commonjs(),
- resolve(),
- typescript({
- tsconfig: './tsconfig.json',
- typescript: typescriptEngine,
- sourceMap: false,
- exclude: [
- 'docs',
- 'dist',
- 'node_modules/**',
- '**/*.test.ts',
- '**/*.test.tsx',
- ],
- }),
- terser(),
- ],
- watch: {
- clearScreen: false,
- },
- },
- {
- input: 'dist/esm/index.d.ts',
- output: [{ file: 'dist/index.d.ts', format: 'esm' }],
- external: [/\.(sc|sa|c)ss$/],
- plugins: [
- dts(),
- peerDepsExternal({ includeDependencies: true }),
- copy({
- targets: [{ src: 'dist/esm/main.css', dest: 'dist' }],
- }),
- bundleSize(),
- ],
- },
-]
diff --git a/joi/src/core/Accordion/Accordion.test.tsx b/joi/src/core/Accordion/Accordion.test.tsx
deleted file mode 100644
index 62b575ea3..000000000
--- a/joi/src/core/Accordion/Accordion.test.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-import React from 'react'
-import '@testing-library/jest-dom'
-import { render, screen, fireEvent } from '@testing-library/react'
-import { Accordion, AccordionItem } from './index'
-
-// Mock the SCSS import
-jest.mock('./styles.scss', () => ({}))
-
-describe('Accordion', () => {
- it('renders accordion with items', () => {
- render(
-
-
- Content 1
-
-
- Content 2
-
-
- )
-
- expect(screen.getByText('Item 1')).toBeInTheDocument()
- expect(screen.getByText('Item 2')).toBeInTheDocument()
- })
-
- it('expands and collapses accordion items', () => {
- render(
-
-
- Content 1
-
-
- )
-
- const trigger = screen.getByText('Item 1')
-
- // Initially, content should not be visible
- expect(screen.queryByText('Content 1')).not.toBeInTheDocument()
-
- // Click to expand
- fireEvent.click(trigger)
- expect(screen.getByText('Content 1')).toBeInTheDocument()
-
- // Click to collapse
- fireEvent.click(trigger)
- expect(screen.queryByText('Content 1')).not.toBeInTheDocument()
- })
-
- it('respects defaultValue prop', () => {
- render(
-
-
- Content 1
-
-
- Content 2
-
-
- )
-
- expect(screen.queryByText('Content 1')).not.toBeInTheDocument()
- expect(screen.getByText('Content 2')).toBeInTheDocument()
- })
-})
diff --git a/joi/src/core/Accordion/index.tsx b/joi/src/core/Accordion/index.tsx
deleted file mode 100644
index 75a671ca4..000000000
--- a/joi/src/core/Accordion/index.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import React, { ReactNode } from 'react'
-import * as AccordionPrimitive from '@radix-ui/react-accordion'
-
-import { ChevronDownIcon } from '@radix-ui/react-icons'
-
-import './styles.scss'
-
-type AccordionProps = {
- defaultValue: string[]
- children: ReactNode
-}
-
-type AccordionItemProps = {
- children: ReactNode
- value: string
- title: string
-}
-
-const AccordionItem = ({ children, value, title }: AccordionItemProps) => {
- return (
-
-
-
- {title}
-
-
-
-
- {children}
-
-
- )
-}
-
-const Accordion = ({ defaultValue, children }: AccordionProps) => (
-
- {children}
-
-)
-
-export { Accordion, AccordionItem }
diff --git a/joi/src/core/Accordion/styles.scss b/joi/src/core/Accordion/styles.scss
deleted file mode 100644
index 028cc021c..000000000
--- a/joi/src/core/Accordion/styles.scss
+++ /dev/null
@@ -1,73 +0,0 @@
-.accordion {
- border-top: 1px solid hsla(var(--app-border));
-
- &__item {
- overflow: hidden;
- margin-top: 1px;
- border-bottom: 1px solid hsla(var(--app-border));
-
- :focus-within {
- position: relative;
- z-index: 1;
- }
- }
-
- &__header {
- display: flex;
- }
-
- &__trigger {
- font-family: inherit;
- background-color: transparent;
- padding: 0 16px;
- height: 40px;
- flex: 1;
- display: flex;
- align-items: center;
- justify-content: space-between;
- font-weight: 500;
- }
-
- &__content {
- overflow: hidden;
-
- &--wrapper {
- padding: 4px 16px 16px 16px;
- }
- }
-
- &__chevron {
- color: hsla(var(--text-secondary));
- transition: transform 300ms cubic-bezier(0.87, 0, 0.13, 1);
- }
-}
-
-.accordion__content[data-state='open'] {
- animation: slideDown 300ms cubic-bezier(0.87, 0, 0.13, 1);
-}
-
-.accordion__content[data-state='closed'] {
- animation: slideUp 300ms cubic-bezier(0.87, 0, 0.13, 1);
-}
-
-.accordion__trigger[data-state='open'] > .accordion__chevron {
- transform: rotate(180deg);
-}
-
-@keyframes slideDown {
- from {
- height: 0;
- }
- to {
- height: var(--radix-accordion-content-height);
- }
-}
-
-@keyframes slideUp {
- from {
- height: var(--radix-accordion-content-height);
- }
- to {
- height: 0;
- }
-}
diff --git a/joi/src/core/Badge/Badge.test.tsx b/joi/src/core/Badge/Badge.test.tsx
deleted file mode 100644
index 1d3192be7..000000000
--- a/joi/src/core/Badge/Badge.test.tsx
+++ /dev/null
@@ -1,83 +0,0 @@
-import React from 'react'
-import { render, screen } from '@testing-library/react'
-import '@testing-library/jest-dom'
-import { Badge, badgeConfig } from './index'
-
-// Mock the styles
-jest.mock('./styles.scss', () => ({}))
-
-describe('@joi/core/Badge', () => {
- it('renders with default props', () => {
- render(Test Badge )
- const badge = screen.getByText('Test Badge')
- expect(badge).toBeInTheDocument()
- expect(badge).toHaveClass('badge')
- expect(badge).toHaveClass('badge--primary')
- expect(badge).toHaveClass('badge--medium')
- expect(badge).toHaveClass('badge--solid')
- })
-
- it('applies custom className', () => {
- render(Test Badge )
- const badge = screen.getByText('Test Badge')
- expect(badge).toHaveClass('custom-class')
- })
-
- it('renders with different themes', () => {
- const themes = Object.keys(badgeConfig.variants.theme)
- themes.forEach((theme) => {
- render(Test Badge {theme} )
- const badge = screen.getByText(`Test Badge ${theme}`)
- expect(badge).toHaveClass(`badge--${theme}`)
- })
- })
-
- it('renders with different variants', () => {
- const variants = Object.keys(badgeConfig.variants.variant)
- variants.forEach((variant) => {
- render(Test Badge {variant} )
- const badge = screen.getByText(`Test Badge ${variant}`)
- expect(badge).toHaveClass(`badge--${variant}`)
- })
- })
-
- it('renders with different sizes', () => {
- const sizes = Object.keys(badgeConfig.variants.size)
- sizes.forEach((size) => {
- render(Test Badge {size} )
- const badge = screen.getByText(`Test Badge ${size}`)
- expect(badge).toHaveClass(`badge--${size}`)
- })
- })
-
- it('fails when a new theme is added without updating the test', () => {
- const expectedThemes = [
- 'primary',
- 'secondary',
- 'warning',
- 'success',
- 'info',
- 'destructive',
- ]
- const actualThemes = Object.keys(badgeConfig.variants.theme)
- expect(actualThemes).toEqual(expectedThemes)
- })
-
- it('fails when a new variant is added without updating the test', () => {
- const expectedVariant = ['solid', 'soft', 'outline']
- const actualVariants = Object.keys(badgeConfig.variants.variant)
- expect(actualVariants).toEqual(expectedVariant)
- })
-
- it('fails when a new size is added without updating the test', () => {
- const expectedSizes = ['small', 'medium', 'large']
- const actualSizes = Object.keys(badgeConfig.variants.size)
- expect(actualSizes).toEqual(expectedSizes)
- })
-
- it('fails when a new variant CVA is added without updating the test', () => {
- const expectedVariantsCVA = ['theme', 'variant', 'size']
- const actualVariant = Object.keys(badgeConfig.variants)
- expect(actualVariant).toEqual(expectedVariantsCVA)
- })
-})
diff --git a/joi/src/core/Badge/index.tsx b/joi/src/core/Badge/index.tsx
deleted file mode 100644
index 5aeb19631..000000000
--- a/joi/src/core/Badge/index.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import React, { HTMLAttributes } from 'react'
-
-import { cva, type VariantProps } from 'class-variance-authority'
-
-import { twMerge } from 'tailwind-merge'
-
-import './styles.scss'
-
-export const badgeConfig = {
- variants: {
- theme: {
- primary: 'badge--primary',
- secondary: 'badge--secondary',
- warning: 'badge--warning',
- success: 'badge--success',
- info: 'badge--info',
- destructive: 'badge--destructive',
- },
- variant: {
- solid: 'badge--solid',
- soft: 'badge--soft',
- outline: 'badge--outline',
- },
- size: {
- small: 'badge--small',
- medium: 'badge--medium',
- large: 'badge--large',
- },
- },
- defaultVariants: {
- theme: 'primary' as const,
- size: 'medium' as const,
- variant: 'solid' as const,
- },
-}
-
-const badgeVariants = cva('badge', badgeConfig)
-
-export interface BadgeProps
- extends HTMLAttributes,
- VariantProps {}
-
-const Badge = ({ className, theme, size, variant, ...props }: BadgeProps) => {
- return (
-
- )
-}
-
-export { Badge, badgeVariants }
diff --git a/joi/src/core/Badge/styles.scss b/joi/src/core/Badge/styles.scss
deleted file mode 100644
index a912e9216..000000000
--- a/joi/src/core/Badge/styles.scss
+++ /dev/null
@@ -1,131 +0,0 @@
-.badge {
- @apply inline-flex items-center justify-center px-2 font-medium transition-all;
-
- // Primary
- &--primary {
- color: hsla(var(--primary-fg));
- background-color: hsla(var(--primary-bg));
-
- // Variant soft primary
- &.badge--soft {
- background-color: hsla(var(--primary-bg-soft));
- color: hsla(var(--primary-bg));
- }
-
- // Variant outline primary
- &.badge--outline {
- background-color: transparent;
- border: 1px solid hsla(var(--primary-bg));
- color: hsla(var(--primary-bg));
- }
- }
-
- // Secondary
- &--secondary {
- background-color: hsla(var(--secondary-bg));
- color: hsla(var(--secondary-fg));
-
- &.badge--soft {
- background-color: hsla(var(--secondary-bg-soft));
- color: hsla(var(--secondary-bg));
- }
-
- // Variant outline secondary
- &.badge--outline {
- background-color: transparent;
- border: 1px solid hsla(var(--secondary-bg));
- }
- }
-
- // Destructive
- &--destructive {
- color: hsla(var(--destructive-fg));
- background-color: hsla(var(--destructive-bg));
-
- // Variant soft destructive
- &.badge--soft {
- background-color: hsla(var(--destructive-bg-soft));
- color: hsla(var(--destructive-bg));
- }
-
- // Variant outline destructive
- &.badge--outline {
- background-color: transparent;
- border: 1px solid hsla(var(--destructive-bg));
- color: hsla(var(--destructive-bg));
- }
- }
-
- // Success
- &--success {
- @apply text-white;
- background-color: hsla(var(--success-bg));
-
- // Variant soft success
- &.badge--soft {
- background-color: hsla(var(--success-bg-soft));
- color: hsla(var(--success-bg));
- }
-
- // Variant outline success
- &.badge--outline {
- background-color: transparent;
- border: 1px solid hsla(var(--success-bg));
- color: hsla(var(--success-bg));
- }
- }
-
- // Warning
- &--warning {
- @apply text-white;
- background-color: hsla(var(--warning-bg));
-
- // Variant soft warning
- &.badge--soft {
- background-color: hsla(var(--warning-bg-soft));
- color: hsla(var(--warning-bg));
- }
-
- // Variant outline warning
- &.badge--outline {
- background-color: transparent;
- border: 1px solid hsla(var(--warning-bg));
- color: hsla(var(--warning-bg));
- }
- }
-
- // Info
- &--info {
- @apply text-white;
- background-color: hsla(var(--info-bg));
-
- // Variant soft info
- &.badge--soft {
- background-color: hsla(var(--info-bg-soft));
- color: hsla(var(--info-bg));
- }
-
- // Variant outline info
- &.badge--outline {
- background-color: transparent;
- border: 1px solid hsla(var(--info-bg));
- color: hsla(var(--info-bg));
- }
- }
-
- // Size
- &--small {
- @apply h-5;
- border-radius: 4px;
- }
-
- &--medium {
- @apply h-6;
- border-radius: 6px;
- }
-
- &--large {
- @apply h-7;
- border-radius: 8px;
- }
-}
diff --git a/joi/src/core/Button/Button.test.tsx b/joi/src/core/Button/Button.test.tsx
deleted file mode 100644
index a4c679773..000000000
--- a/joi/src/core/Button/Button.test.tsx
+++ /dev/null
@@ -1,90 +0,0 @@
-import React from 'react'
-import { render, screen } from '@testing-library/react'
-import '@testing-library/jest-dom'
-import { Button, buttonConfig } from './index'
-
-// Mock the styles
-jest.mock('./styles.scss', () => ({}))
-
-describe('@joi/core/Button', () => {
- it('renders with default props', () => {
- render(Click me )
- const button = screen.getByRole('button', { name: /click me/i })
- expect(button).toBeInTheDocument()
- expect(button).toHaveClass('btn btn--primary btn--medium btn--solid')
- })
-
- it('applies custom className', () => {
- render(Test Button )
- const badge = screen.getByText('Test Button')
- expect(badge).toHaveClass('custom-class')
- })
-
- it('renders as a child component when asChild is true', () => {
- render(
-
- Link Button
-
- )
- const link = screen.getByRole('link', { name: /link button/i })
- expect(link).toBeInTheDocument()
- expect(link).toHaveClass('btn btn--primary btn--medium btn--solid')
- })
-
- it.each(Object.keys(buttonConfig.variants.theme))(
- 'renders with theme %s',
- (theme) => {
- render(Theme Button )
- const button = screen.getByRole('button', { name: /theme button/i })
- expect(button).toHaveClass(`btn btn--${theme}`)
- }
- )
-
- it.each(Object.keys(buttonConfig.variants.variant))(
- 'renders with variant %s',
- (variant) => {
- render(Variant Button )
- const button = screen.getByRole('button', { name: /variant button/i })
- expect(button).toHaveClass(`btn btn--${variant}`)
- }
- )
-
- it.each(Object.keys(buttonConfig.variants.size))(
- 'renders with size %s',
- (size) => {
- render(Size Button )
- const button = screen.getByRole('button', { name: /size button/i })
- expect(button).toHaveClass(`btn btn--${size}`)
- }
- )
-
- it('renders with block prop', () => {
- render(Block Button )
- const button = screen.getByRole('button', { name: /block button/i })
- expect(button).toHaveClass('btn btn--block')
- })
-
- it('fails when a new theme is added without updating the test', () => {
- const expectedThemes = ['primary', 'ghost', 'icon', 'destructive']
- const actualThemes = Object.keys(buttonConfig.variants.theme)
- expect(actualThemes).toEqual(expectedThemes)
- })
-
- it('fails when a new variant is added without updating the test', () => {
- const expectedVariant = ['solid', 'soft', 'outline']
- const actualVariants = Object.keys(buttonConfig.variants.variant)
- expect(actualVariants).toEqual(expectedVariant)
- })
-
- it('fails when a new size is added without updating the test', () => {
- const expectedSizes = ['small', 'medium', 'large']
- const actualSizes = Object.keys(buttonConfig.variants.size)
- expect(actualSizes).toEqual(expectedSizes)
- })
-
- it('fails when a new variant CVA is added without updating the test', () => {
- const expectedVariantsCVA = ['theme', 'variant', 'size', 'block']
- const actualVariant = Object.keys(buttonConfig.variants)
- expect(actualVariant).toEqual(expectedVariantsCVA)
- })
-})
diff --git a/joi/src/core/Button/index.tsx b/joi/src/core/Button/index.tsx
deleted file mode 100644
index 9945eb4e9..000000000
--- a/joi/src/core/Button/index.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-import React, { forwardRef, ButtonHTMLAttributes } from 'react'
-
-import { Slot } from '@radix-ui/react-slot'
-import { cva, type VariantProps } from 'class-variance-authority'
-
-import { twMerge } from 'tailwind-merge'
-
-import './styles.scss'
-
-export const buttonConfig = {
- variants: {
- theme: {
- primary: 'btn--primary',
- ghost: 'btn--ghost',
- icon: 'btn--icon',
- destructive: 'btn--destructive',
- },
- variant: {
- solid: 'btn--solid',
- soft: 'btn--soft',
- outline: 'btn--outline',
- },
- size: {
- small: 'btn--small',
- medium: 'btn--medium',
- large: 'btn--large',
- },
- block: {
- true: 'btn--block',
- },
- },
- defaultVariants: {
- theme: 'primary' as const,
- size: 'medium' as const,
- variant: 'solid' as const,
- block: false as const,
- },
-}
-const buttonVariants = cva('btn', buttonConfig)
-
-export interface ButtonProps
- extends ButtonHTMLAttributes,
- VariantProps {
- asChild?: boolean
-}
-
-const Button = forwardRef(
- (
- { className, theme, size, variant, block, asChild = false, ...props },
- ref
- ) => {
- const Comp = asChild ? Slot : 'button'
- return (
-
- )
- }
-)
-
-export { Button }
diff --git a/joi/src/core/Button/styles.scss b/joi/src/core/Button/styles.scss
deleted file mode 100644
index f7cdce6a4..000000000
--- a/joi/src/core/Button/styles.scss
+++ /dev/null
@@ -1,134 +0,0 @@
-.btn {
- @apply inline-flex items-center justify-center px-4 font-semibold transition-all;
-
- &:focus,
- &:focus-within {
- @apply outline-2 outline-offset-4;
- }
- &:hover {
- filter: brightness(95%);
- }
-
- // Primary
- &--primary {
- color: hsla(var(--primary-fg));
- background-color: hsla(var(--primary-bg)) !important;
- &:hover {
- filter: brightness(65%);
- }
-
- // Variant soft primary
- &.btn--soft {
- background-color: hsla(var(--primary-bg-soft)) !important;
- color: hsla(var(--primary-bg));
- }
-
- // Variant outline primary
- &.btn--outline {
- background-color: transparent !important;
- border: 1px solid hsla(var(--primary-bg));
- color: hsla(var(--primary-bg));
- }
- }
-
- // Ghost
- &--ghost {
- background-color: transparent !important;
- &.btn--soft {
- background-color: transparent !important;
- }
-
- // Variant outline ghost
- &.btn--outline {
- background-color: transparent !important;
- border: 1px solid hsla(var(--ghost-border));
- }
- }
-
- // Destructive
- &--destructive {
- color: hsla(var(--destructive-fg));
- background-color: hsla(var(--destructive-bg)) !important;
- &:hover {
- filter: brightness(65%);
- }
-
- // Variant soft destructive
- &.btn--soft {
- background-color: hsla(var(--destructive-bg-soft)) !important;
- color: hsla(var(--destructive-bg));
- }
-
- // Variant outline destructive
- &.btn--outline {
- background-color: transparent !important;
- border: 1px solid hsla(var(--destructive-bg));
- color: hsla(var(--destructive-bg));
- }
- }
-
- // Disabled
- &:disabled {
- color: hsla(var(--disabled-fg));
- background-color: hsla(var(--disabled-bg)) !important;
- cursor: not-allowed;
-
- &:hover {
- filter: brightness(100%);
- }
- }
-
- // Icon
- &--icon {
- width: 24px;
- height: 24px;
- padding: 2px;
- &:hover {
- background-color: hsla(var(--icon-bg)) !important;
- }
-
- &.btn--outline {
- background-color: transparent !important;
- border: 1px solid hsla(var(--icon-border));
- &:hover {
- background-color: hsla(var(--icon-bg)) !important;
- }
- }
- }
-
- // Size
- &--small {
- @apply h-6 px-2;
- font-size: 12px;
- border-radius: 4px;
- &.btn--icon {
- width: 24px;
- height: 24px;
- padding: 2px;
- }
- }
-
- &--medium {
- @apply h-8;
- border-radius: 6px;
- &.btn--icon {
- width: 24px;
- height: 24px;
- padding: 2px;
- }
- }
-
- &--large {
- @apply h-9;
- border-radius: 8px;
- &.btn--icon {
- width: 24px;
- height: 24px;
- padding: 2px;
- }
- }
-
- &--block {
- @apply w-full;
- }
-}
diff --git a/joi/src/core/Checkbox/Checkbox.test.tsx b/joi/src/core/Checkbox/Checkbox.test.tsx
deleted file mode 100644
index ce81132d9..000000000
--- a/joi/src/core/Checkbox/Checkbox.test.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import React from 'react'
-import { render, screen, fireEvent } from '@testing-library/react'
-import '@testing-library/jest-dom'
-import { Checkbox } from './index'
-
-// Mock the styles
-jest.mock('./styles.scss', () => ({}))
-
-describe('@joi/core/Checkbox', () => {
- it('renders correctly with label', () => {
- render( )
- expect(screen.getByLabelText('Test Checkbox')).toBeInTheDocument()
- })
-
- it('renders with helper description', () => {
- render( )
- expect(screen.getByText('Helper text')).toBeInTheDocument()
- })
-
- it('renders error message when provided', () => {
- render( )
- expect(screen.getByText('Error occurred')).toBeInTheDocument()
- })
-
- it('calls onChange when clicked', () => {
- const mockOnChange = jest.fn()
- render(
-
- )
-
- fireEvent.click(screen.getByLabelText('Test Checkbox'))
- expect(mockOnChange).toHaveBeenCalledTimes(1)
- })
-
- it('applies custom className', () => {
- render( )
- expect(screen.getByRole('checkbox').parentElement).toHaveClass(
- 'custom-class'
- )
- })
-
- it('disables the checkbox when disabled prop is true', () => {
- render( )
- expect(screen.getByLabelText('Disabled Checkbox')).toBeDisabled()
- })
-})
diff --git a/joi/src/core/Checkbox/index.tsx b/joi/src/core/Checkbox/index.tsx
deleted file mode 100644
index 71f9523ac..000000000
--- a/joi/src/core/Checkbox/index.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import React, { ChangeEvent, InputHTMLAttributes, ReactNode } from 'react'
-
-import { twMerge } from 'tailwind-merge'
-
-import './styles.scss'
-
-export interface CheckboxProps extends InputHTMLAttributes {
- disabled?: boolean
- className?: string
- label?: ReactNode
- helperDescription?: ReactNode
- errorMessage?: string
- onChange?: (e: ChangeEvent) => void
-}
-
-const Checkbox = ({
- id,
- name,
- checked,
- disabled,
- label,
- defaultChecked,
- helperDescription,
- errorMessage,
- className,
- onChange,
- ...props
-}: CheckboxProps) => {
- return (
-
-
-
-
- {label}
-
-
{helperDescription}
- {errorMessage &&
{errorMessage}
}
-
-
- )
-}
-export { Checkbox }
diff --git a/joi/src/core/Checkbox/styles.scss b/joi/src/core/Checkbox/styles.scss
deleted file mode 100644
index 775a6289b..000000000
--- a/joi/src/core/Checkbox/styles.scss
+++ /dev/null
@@ -1,51 +0,0 @@
-.checkbox {
- @apply inline-flex items-start space-x-2;
-
- > input[type='checkbox'] {
- @apply flex h-4 w-4 flex-shrink-0 cursor-pointer appearance-none items-center justify-center;
- background-color: transparent;
- margin-top: 1px;
- border: 1px solid hsla(var(--app-border));
- border-radius: 4px;
- &:focus,
- &:focus-within {
- @apply outline-2 outline-offset-4;
- }
-
- &:checked {
- background-color: hsla(var(--primary-bg));
- background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 16 16' fill='%23fff' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12.207 4.793a1 1 0 0 1 0 1.414l-5 5a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L6.5 9.086l4.293-4.293a1 1 0 0 1 1.414 0z'/%3E%3C/svg%3E");
- }
-
- &:disabled {
- background-color: hsla(var(----disabled-bg));
- color: hsla(var(--disabled-fg));
-
- &:checked {
- background-color: hsla(var(--primary-bg));
- @apply cursor-not-allowed opacity-50;
- }
-
- & + div > .checkbox__label {
- @apply cursor-not-allowed opacity-50;
- }
- }
- }
-
- &__helper {
- font-size: 12px;
- }
-
- &__error {
- color: hsla(var(--destructive-bg));
- }
-
- &__label {
- @apply inline-block cursor-pointer;
- }
-
- &:disabled {
- background-color: hsla(var(----disabled-bg));
- color: hsla(var(--disabled-fg));
- }
-}
diff --git a/joi/src/core/Dropdown/index.tsx b/joi/src/core/Dropdown/index.tsx
deleted file mode 100644
index 6d9abcbea..000000000
--- a/joi/src/core/Dropdown/index.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import React, { Fragment, PropsWithChildren, ReactNode } from 'react'
-import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
-import './styles.scss'
-import { twMerge } from 'tailwind-merge'
-
-type Props = {
- options?: { name: ReactNode; value: string; suffix?: ReactNode }[]
- className?: string
- onValueChanged?: (value: string) => void
-}
-
-const Dropdown = (props: PropsWithChildren & Props) => {
- return (
-
- {props.children}
-
-
-
- {props.options?.map((e, i) => (
-
- {i !== 0 && (
-
- )}
- props.onValueChanged?.(e.value)}
- >
- {e.name}
-
- {e.suffix}
-
-
- ))}
-
-
-
-
- )
-}
-
-export { Dropdown }
diff --git a/joi/src/core/Dropdown/styles.scss b/joi/src/core/Dropdown/styles.scss
deleted file mode 100644
index d472578df..000000000
--- a/joi/src/core/Dropdown/styles.scss
+++ /dev/null
@@ -1,155 +0,0 @@
-.DropdownMenuContent,
-.DropdownMenuSubContent {
- min-width: 220px;
- background-color: white;
- border-radius: 6px;
- overflow: hidden;
- padding: 0px;
- box-shadow:
- 0px 10px 38px -10px rgba(22, 23, 24, 0.35),
- 0px 10px 20px -15px rgba(22, 23, 24, 0.2);
- animation-duration: 400ms;
- animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
- will-change: transform, opacity;
-}
-.DropdownMenuContent[data-side='top'],
-.DropdownMenuSubContent[data-side='top'] {
- animation-name: slideDownAndFade;
-}
-.DropdownMenuContent[data-side='right'],
-.DropdownMenuSubContent[data-side='right'] {
- animation-name: slideLeftAndFade;
-}
-.DropdownMenuContent[data-side='bottom'],
-.DropdownMenuSubContent[data-side='bottom'] {
- animation-name: slideUpAndFade;
-}
-.DropdownMenuContent[data-side='left'],
-.DropdownMenuSubContent[data-side='left'] {
- animation-name: slideRightAndFade;
-}
-
-.DropdownMenuItem {
- padding: 14px;
- cursor: pointer;
- outline: none;
- flex: 1;
- display: flex;
- justify-content: space-between; /* Distribute space between children */
- align-items: center; /* Optional: Align items vertically */
- gap: 16px;
- border-color: hsla(var(--app-border));
-}
-.DropdownMenuCheckboxItem,
-.DropdownMenuRadioItem,
-.DropdownMenuSubTrigger {
- font-size: 13px;
- line-height: 1;
- border-radius: 3px;
- display: flex;
- align-items: center;
- height: 25px;
- padding: 0 0;
- position: relative;
- padding-left: 25px;
- user-select: none;
- outline: none;
-}
-.DropdownMenuItem[data-disabled],
-.DropdownMenuCheckboxItem[data-disabled],
-.DropdownMenuRadioItem[data-disabled],
-.DropdownMenuSubTrigger[data-disabled] {
- pointer-events: none;
-}
-.DropdownMenuItem[data-highlighted],
-.DropdownMenuCheckboxItem[data-highlighted],
-.DropdownMenuRadioItem[data-highlighted],
-.DropdownMenuSubTrigger[data-highlighted] {
- background-color: hsla(var(--secondary-bg));
-}
-
-.DropdownMenuSeparator {
- height: 1px;
- width: '100%';
- background-color: hsla(var(--app-border));
-}
-
-.DropdownMenuItem::hover {
- background-color: hsla(var(--secondary-bg));
-}
-
-.DropdownMenuLabel {
- padding-left: 25px;
- font-size: 12px;
- line-height: 25px;
- color: var(--mauve-11);
-}
-
-.DropdownMenuItemIndicator {
- position: absolute;
- left: 0;
- width: 25px;
- display: inline-flex;
- align-items: center;
- justify-content: center;
-}
-
-.DropdownMenuArrow {
- fill: white;
-}
-
-.RightSlot {
- margin-left: auto;
- padding-left: 20px;
- color: var(--mauve-11);
-}
-[data-highlighted] > .RightSlot {
- color: white;
-}
-[data-disabled] .RightSlot {
- color: var(--mauve-8);
-}
-
-@keyframes slideUpAndFade {
- from {
- opacity: 0;
- transform: translateY(2px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-@keyframes slideRightAndFade {
- from {
- opacity: 0;
- transform: translateX(-2px);
- }
- to {
- opacity: 1;
- transform: translateX(0);
- }
-}
-
-@keyframes slideDownAndFade {
- from {
- opacity: 0;
- transform: translateY(-2px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-@keyframes slideLeftAndFade {
- from {
- opacity: 0;
- transform: translateX(2px);
- }
- to {
- opacity: 1;
- transform: translateX(0);
- }
-}
diff --git a/joi/src/core/Input/Input.test.tsx b/joi/src/core/Input/Input.test.tsx
deleted file mode 100644
index 55bed74bb..000000000
--- a/joi/src/core/Input/Input.test.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import React from 'react'
-import { render, screen, fireEvent } from '@testing-library/react'
-import '@testing-library/jest-dom'
-import { Input } from './index'
-
-// Mock the styles import
-jest.mock('./styles.scss', () => ({}))
-
-describe('@joi/core/Input', () => {
- it('renders correctly', () => {
- render( )
- expect(screen.getByPlaceholderText('Test input')).toBeInTheDocument()
- })
-
- it('applies custom className', () => {
- render( )
- expect(screen.getByRole('textbox')).toHaveClass('custom-class')
- })
-
- it('aligns text to the right when textAlign prop is set', () => {
- render( )
- expect(screen.getByRole('textbox')).toHaveClass('text-right')
- })
-
- it('renders prefix icon when provided', () => {
- render( Prefix} />)
- expect(screen.getByTestId('prefix-icon')).toBeInTheDocument()
- })
-
- it('renders suffix icon when provided', () => {
- render( Suffix} />)
- expect(screen.getByTestId('suffix-icon')).toBeInTheDocument()
- })
-
- it('renders clear icon when clearable is true', () => {
- render( )
- expect(screen.getByTestId('cross-2-icon')).toBeInTheDocument()
- })
-
- it('calls onClick when input is clicked', () => {
- const onClick = jest.fn()
- render( )
- fireEvent.click(screen.getByRole('textbox'))
- expect(onClick).toHaveBeenCalledTimes(1)
- })
-
- it('calls onClear when clear icon is clicked', () => {
- const onClear = jest.fn()
- render( )
- fireEvent.click(screen.getByTestId('cross-2-icon'))
- expect(onClear).toHaveBeenCalledTimes(1)
- })
-})
diff --git a/joi/src/core/Input/index.tsx b/joi/src/core/Input/index.tsx
deleted file mode 100644
index 9f5e4c663..000000000
--- a/joi/src/core/Input/index.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-import React, { ReactNode, forwardRef } from 'react'
-import { twMerge } from 'tailwind-merge'
-
-import './styles.scss'
-import { Cross2Icon } from '@radix-ui/react-icons'
-
-export interface Props extends React.InputHTMLAttributes {
- textAlign?: 'left' | 'right'
- prefixIcon?: ReactNode
- suffixIcon?: ReactNode
- onCLick?: () => void
- clearable?: boolean
- onClear?: () => void
-}
-
-const Input = forwardRef(
- (
- {
- className,
- type,
- textAlign,
- prefixIcon,
- suffixIcon,
- onClick,
- onClear,
- clearable,
- ...props
- },
- ref
- ) => {
- return (
-
- {prefixIcon && (
-
- {prefixIcon}
-
- )}
- {suffixIcon && (
-
- {suffixIcon}
-
- )}
- {clearable && (
-
-
-
- )}
-
-
- )
- }
-)
-
-export { Input }
diff --git a/joi/src/core/Input/styles.scss b/joi/src/core/Input/styles.scss
deleted file mode 100644
index 540d880b1..000000000
--- a/joi/src/core/Input/styles.scss
+++ /dev/null
@@ -1,50 +0,0 @@
-.input {
- background-color: hsla(var(--input-bg));
- border: 1px solid hsla(var(--app-border));
- @apply inline-flex h-8 w-full items-center rounded-md border px-3 transition-colors;
- @apply focus-within:outline-none focus-visible:outline-0 focus-visible:ring-1 focus-visible:ring-[hsla(var(--primary-bg))] focus-visible:ring-offset-0;
- @apply file:border-0 file:bg-transparent file:font-medium;
- @apply hover:border-[hsla(var(--primary-bg))];
-
- &__wrapper {
- position: relative;
- }
-
- &.text-right {
- text-align: right;
- }
-
- &::placeholder {
- color: hsla(var(--input-placeholder));
- }
-
- &:disabled {
- color: hsla(var(--disabled-fg));
- background-color: hsla(var(--disabled-bg));
- cursor: not-allowed;
- border: none;
- }
-
- &__prefix-icon {
- @apply absolute left-3 top-1/2 -translate-y-1/2 cursor-pointer;
- color: hsla(var(--input-icon));
- + .input {
- padding-left: 32px;
- }
- }
-
- &__suffix-icon {
- @apply absolute right-3 top-1/2 -translate-y-1/2 cursor-pointer;
- color: hsla(var(--input-icon));
- + .input {
- padding-right: 32px;
- }
- }
- &__clear-icon {
- @apply absolute right-3 top-1/2 -translate-y-1/2 cursor-pointer;
- color: hsla(var(--input-icon));
- + .input {
- padding: 0 32px;
- }
- }
-}
diff --git a/joi/src/core/Modal/Modal.test.tsx b/joi/src/core/Modal/Modal.test.tsx
deleted file mode 100644
index fe7ca7eac..000000000
--- a/joi/src/core/Modal/Modal.test.tsx
+++ /dev/null
@@ -1,78 +0,0 @@
-import React from 'react'
-import { render, screen, fireEvent } from '@testing-library/react'
-import '@testing-library/jest-dom'
-import { Modal } from './index'
-
-// Mock the styles
-jest.mock('./styles.scss', () => ({}))
-
-describe('Modal', () => {
- it('renders the modal with trigger and content', () => {
- render(
- Open Modal}
- content={Modal Content
}
- />
- )
-
- expect(screen.getByText('Open Modal')).toBeInTheDocument()
- fireEvent.click(screen.getByText('Open Modal'))
- expect(screen.getByText('Modal Content')).toBeInTheDocument()
- })
-
- it('renders the modal with title', () => {
- render(
- Open Modal}
- content={Modal Content
}
- title="Modal Title"
- />
- )
-
- fireEvent.click(screen.getByText('Open Modal'))
- expect(screen.getByText('Modal Title')).toBeInTheDocument()
- })
-
- it('renders full page modal', () => {
- render(
- Open Modal}
- content={Modal Content
}
- fullPage
- />
- )
-
- fireEvent.click(screen.getByText('Open Modal'))
- expect(screen.getByRole('dialog')).toHaveClass('modal__content--fullpage')
- })
-
- it('hides close button when hideClose is true', () => {
- render(
- Open Modal}
- content={Modal Content
}
- hideClose
- />
- )
-
- fireEvent.click(screen.getByText('Open Modal'))
- expect(screen.queryByLabelText('Close')).not.toBeInTheDocument()
- })
-
- it('calls onOpenChange when opening and closing the modal', () => {
- const onOpenChangeMock = jest.fn()
- render(
- Open Modal}
- content={Modal Content
}
- onOpenChange={onOpenChangeMock}
- />
- )
-
- fireEvent.click(screen.getByText('Open Modal'))
- expect(onOpenChangeMock).toHaveBeenCalledWith(true)
-
- fireEvent.click(screen.getByLabelText('Close'))
- expect(onOpenChangeMock).toHaveBeenCalledWith(false)
- })
-})
diff --git a/joi/src/core/Modal/index.tsx b/joi/src/core/Modal/index.tsx
deleted file mode 100644
index 7754fb3f0..000000000
--- a/joi/src/core/Modal/index.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-import React, { ReactNode } from 'react'
-import * as DialogPrimitive from '@radix-ui/react-dialog'
-import { Cross2Icon } from '@radix-ui/react-icons'
-
-import './styles.scss'
-import { twMerge } from 'tailwind-merge'
-
-type Props = {
- trigger?: ReactNode
- content: ReactNode
- open?: boolean
- className?: string
- fullPage?: boolean
- hideClose?: boolean
- title?: ReactNode
- onOpenChange?: (open: boolean) => void
-}
-
-const ModalClose = DialogPrimitive.Close
-
-const Modal = ({
- trigger,
- content,
- open,
- title,
- fullPage,
- className,
- onOpenChange,
- hideClose,
-}: Props) => (
-
- {trigger}
-
-
-
-
- {title}
-
- {content}
- {!hideClose && (
-
-
-
-
-
- )}
-
-
-
-)
-
-export { Modal, ModalClose }
diff --git a/joi/src/core/Modal/styles.scss b/joi/src/core/Modal/styles.scss
deleted file mode 100644
index 717ce2ac7..000000000
--- a/joi/src/core/Modal/styles.scss
+++ /dev/null
@@ -1,85 +0,0 @@
-/* reset */
-button,
-fieldset,
-.modal {
- &__overlay {
- background-color: hsla(var(--modal-overlay));
- z-index: 200;
- position: fixed;
- inset: 0;
- animation: overlayShow 150ms cubic-bezier(0.16, 1, 0.3, 1);
- }
-
- &__content {
- color: hsla(var(--modal-fg));
- overflow: auto;
- background-color: hsla(var(--modal-bg));
- border-radius: 8px;
- font-size: 14px;
- position: fixed;
- z-index: 300;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- width: 50vw;
- max-width: 560px;
- max-height: 85vh;
- padding: 16px;
- animation: contentShow 150ms cubic-bezier(0.16, 1, 0.3, 1);
- border: 1px solid hsla(var(--app-border));
- @apply w-full;
-
- &--fullpage {
- max-width: none;
- width: 90vw;
- max-height: 90vh;
- }
-
- &:focus {
- outline: none;
- }
- }
-
- &__title {
- @apply leading-relaxed;
- margin: 0 0 8px 0;
- padding-right: 16px;
- font-weight: 600;
- color: hsla(var(--modal-fg));
- font-size: 18px;
- }
-
- &__close-icon {
- font-family: inherit;
- border-radius: 100%;
- height: 24px;
- width: 24px;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- color: hsla(var(--modal-fg));
- position: absolute;
- top: 8px;
- right: 16px;
- }
-}
-
-@keyframes overlayShow {
- from {
- opacity: 0;
- }
- to {
- opacity: 1;
- }
-}
-
-@keyframes contentShow {
- from {
- opacity: 0;
- transform: translate(-50%, -48%) scale(0.96);
- }
- to {
- opacity: 1;
- transform: translate(-50%, -50%) scale(1);
- }
-}
diff --git a/joi/src/core/Progress/Progress.test.tsx b/joi/src/core/Progress/Progress.test.tsx
deleted file mode 100644
index 9d18bf019..000000000
--- a/joi/src/core/Progress/Progress.test.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import React from 'react'
-import { render, screen } from '@testing-library/react'
-import '@testing-library/jest-dom'
-import { Progress } from './index'
-
-// Mock the styles
-jest.mock('./styles.scss', () => ({}))
-
-describe('@joi/core/Progress', () => {
- it('renders with default props', () => {
- render( )
- const progressElement = screen.getByRole('progressbar')
- expect(progressElement).toBeInTheDocument()
- expect(progressElement).toHaveClass('progress')
- expect(progressElement).toHaveClass('progress--medium')
- expect(progressElement).toHaveAttribute('aria-valuenow', '50')
- })
-
- it('applies custom className', () => {
- render( )
- const progressElement = screen.getByRole('progressbar')
- expect(progressElement).toHaveClass('custom-class')
- })
-
- it('renders with different sizes', () => {
- const { rerender } = render( )
- let progressElement = screen.getByRole('progressbar')
- expect(progressElement).toHaveClass('progress--small')
-
- rerender( )
- progressElement = screen.getByRole('progressbar')
- expect(progressElement).toHaveClass('progress--large')
- })
-
- it('sets the correct transform style based on value', () => {
- render( )
- const progressElement = screen.getByRole('progressbar')
- const indicatorElement = progressElement.firstChild as HTMLElement
- expect(indicatorElement).toHaveStyle('transform: translateX(-25%)')
- })
-
- it('handles edge cases for value', () => {
- const { rerender } = render( )
- let progressElement = screen.getByRole('progressbar')
- let indicatorElement = progressElement.firstChild as HTMLElement
- expect(indicatorElement).toHaveStyle('transform: translateX(-100%)')
- expect(progressElement).toHaveAttribute('aria-valuenow', '0')
-
- rerender( )
- progressElement = screen.getByRole('progressbar')
- indicatorElement = progressElement.firstChild as HTMLElement
- expect(indicatorElement).toHaveStyle('transform: translateX(-0%)')
- expect(progressElement).toHaveAttribute('aria-valuenow', '100')
- })
-})
diff --git a/joi/src/core/Progress/index.tsx b/joi/src/core/Progress/index.tsx
deleted file mode 100644
index 01aefbeb0..000000000
--- a/joi/src/core/Progress/index.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import React, { HTMLAttributes } from 'react'
-
-import { cva, type VariantProps } from 'class-variance-authority'
-
-import { twMerge } from 'tailwind-merge'
-
-import './styles.scss'
-
-const progressVariants = cva('progress', {
- variants: {
- size: {
- small: 'progress--small',
- medium: 'progress--medium',
- large: 'progress--large',
- },
- },
- defaultVariants: {
- size: 'medium',
- },
-})
-
-export interface ProgressProps
- extends HTMLAttributes,
- VariantProps {
- value: number
-}
-
-const Progress = ({ className, size, value, ...props }: ProgressProps) => {
- return (
-
- )
-}
-
-export { Progress }
diff --git a/joi/src/core/Progress/styles.scss b/joi/src/core/Progress/styles.scss
deleted file mode 100644
index 02d22f5f4..000000000
--- a/joi/src/core/Progress/styles.scss
+++ /dev/null
@@ -1,25 +0,0 @@
-.progress {
- background-color: hsla(var(--progress-track-bg));
- border-radius: 8px;
- position: relative;
- overflow: hidden;
- @apply transition-all;
-
- &--indicator {
- background-color: hsla(var(--primary-bg));
- position: absolute;
- border-radius: 8px;
- width: 100%;
- height: 100%;
- }
-
- &--small {
- height: 6px;
- }
- &--medium {
- @apply h-2;
- }
- &--large {
- @apply h-3;
- }
-}
diff --git a/joi/src/core/ScrollArea/ScrollArea.test.tsx b/joi/src/core/ScrollArea/ScrollArea.test.tsx
deleted file mode 100644
index 961c5da59..000000000
--- a/joi/src/core/ScrollArea/ScrollArea.test.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import React from 'react'
-import { render, screen } from '@testing-library/react'
-import '@testing-library/jest-dom'
-import { ScrollArea } from './index'
-
-declare const global: typeof globalThis
-
-// Mock the styles
-jest.mock('./styles.scss', () => ({}))
-
-class ResizeObserverMock {
- observe() {}
- unobserve() {}
- disconnect() {}
-}
-
-global.ResizeObserver = ResizeObserverMock
-
-describe('@joi/core/ScrollArea', () => {
- it('renders children correctly', () => {
- render(
-
- Test Content
-
- )
-
- const child = screen.getByTestId('child')
- expect(child).toBeInTheDocument()
- expect(child).toHaveTextContent('Test Content')
- })
-
- it('applies custom className', () => {
- const { container } = render( )
-
- const root = container.firstChild as HTMLElement
- expect(root).toHaveClass('scroll-area__root')
- expect(root).toHaveClass('custom-class')
- })
-
- it('forwards ref to the Viewport component', () => {
- const ref = React.createRef()
- render( )
-
- expect(ref.current).toBeInstanceOf(HTMLDivElement)
- expect(ref.current).toHaveClass('scroll-area__viewport')
- })
-})
diff --git a/joi/src/core/ScrollArea/index.tsx b/joi/src/core/ScrollArea/index.tsx
deleted file mode 100644
index 2d44b4af8..000000000
--- a/joi/src/core/ScrollArea/index.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import React, { PropsWithChildren, forwardRef } from 'react'
-import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'
-import { twMerge } from 'tailwind-merge'
-
-import './styles.scss'
-
-const ScrollArea = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, children, onScroll, ...props }, ref) => (
-
-
- {children}
-
-
-
-
-
-
-
-
-
-))
-
-export { ScrollArea }
diff --git a/joi/src/core/ScrollArea/styles.scss b/joi/src/core/ScrollArea/styles.scss
deleted file mode 100644
index 99ee7de87..000000000
--- a/joi/src/core/ScrollArea/styles.scss
+++ /dev/null
@@ -1,53 +0,0 @@
-.scroll-area {
- position: relative;
- z-index: 999;
-
- &__root {
- width: 200px;
- height: 225px;
- overflow: hidden;
- }
-
- &__viewport {
- width: 100%;
- height: 100%;
- border-radius: inherit;
- }
-
- &__bar {
- display: flex;
- user-select: none;
- touch-action: none;
- padding: 1px;
- background: hsla(var(--scrollbar-tracker));
- transition: background 160ms ease-out;
- }
-
- &__thumb {
- flex: 1;
- background: hsla(var(--scrollbar-thumb));
- border-radius: 20px;
- position: relative;
-
- ::before {
- content: '';
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- width: 100%;
- height: 100%;
- min-width: 44px;
- min-height: 44px;
- }
- }
-}
-
-.scroll-area__bar[data-orientation='vertical'] {
- width: 8px;
-}
-
-.scroll-area__bar[data-orientation='horizontal'] {
- flex-direction: column;
- height: 8px;
-}
diff --git a/joi/src/core/Select/Select.test.tsx b/joi/src/core/Select/Select.test.tsx
deleted file mode 100644
index 1b450706b..000000000
--- a/joi/src/core/Select/Select.test.tsx
+++ /dev/null
@@ -1,107 +0,0 @@
-import React from 'react'
-import { render, screen } from '@testing-library/react'
-import userEvent from '@testing-library/user-event'
-import { Select } from './index'
-import '@testing-library/jest-dom'
-
-// Mock the styles
-jest.mock('./styles.scss', () => ({}))
-
-jest.mock('tailwind-merge', () => ({
- twMerge: (...classes: string[]) => classes.filter(Boolean).join(' '),
-}))
-
-const mockOnValueChange = jest.fn()
-jest.mock('@radix-ui/react-select', () => ({
- Root: ({
- children,
- onValueChange,
- }: {
- children: React.ReactNode
- onValueChange?: (value: string) => void
- }) => {
- mockOnValueChange.mockImplementation(onValueChange)
- return {children}
- },
- Trigger: ({
- children,
- className,
- }: {
- children: React.ReactNode
- className?: string
- }) => (
-
- {children}
-
- ),
- Value: ({ placeholder }: { placeholder?: string }) => (
- {placeholder}
- ),
- Icon: ({ children }: { children: React.ReactNode }) => (
- {children}
- ),
- Portal: ({ children }: { children: React.ReactNode }) => (
- {children}
- ),
- Content: ({ children }: { children: React.ReactNode }) => (
- {children}
- ),
- Viewport: ({ children }: { children: React.ReactNode }) => (
- {children}
- ),
- Item: ({ children, value }: { children: React.ReactNode; value: string }) => (
- mockOnValueChange(value)}
- >
- {children}
-
- ),
- ItemText: ({ children }: { children: React.ReactNode }) => (
- {children}
- ),
- ItemIndicator: ({ children }: { children: React.ReactNode }) => (
- {children}
- ),
- Arrow: () =>
,
-}))
-describe('@joi/core/Select', () => {
- const options = [
- { name: 'Option 1', value: 'option1' },
- { name: 'Option 2', value: 'option2' },
- ]
-
- it('renders with placeholder', () => {
- render( )
- expect(screen.getByTestId('select-value')).toHaveTextContent(
- 'Select an option'
- )
- })
-
- it('renders options', () => {
- render( )
- expect(screen.getByTestId('select-item-option1')).toBeInTheDocument()
- expect(screen.getByTestId('select-item-option2')).toBeInTheDocument()
- })
-
- it('calls onValueChange when an option is selected', async () => {
- const user = userEvent.setup()
- const onValueChange = jest.fn()
- render( )
-
- await user.click(screen.getByTestId('select-trigger'))
- await user.click(screen.getByTestId('select-item-option1'))
-
- expect(onValueChange).toHaveBeenCalledWith('option1')
- })
-
- it('applies disabled class when disabled prop is true', () => {
- render( )
- expect(screen.getByTestId('select-trigger')).toHaveClass('select__disabled')
- })
-
- it('applies block class when block prop is true', () => {
- render( )
- expect(screen.getByTestId('select-trigger')).toHaveClass('w-full')
- })
-})
diff --git a/joi/src/core/Select/index.tsx b/joi/src/core/Select/index.tsx
deleted file mode 100644
index d8935bd83..000000000
--- a/joi/src/core/Select/index.tsx
+++ /dev/null
@@ -1,98 +0,0 @@
-import React from 'react'
-
-import * as SelectPrimitive from '@radix-ui/react-select'
-import { CheckIcon, ChevronDownIcon } from '@radix-ui/react-icons'
-
-import './styles.scss'
-import { twMerge } from 'tailwind-merge'
-
-type Props = {
- options?: { name: string; value: string; recommend?: boolean }[]
- open?: boolean
- block?: boolean
- value?: string
- side?: 'top' | 'right' | 'bottom' | 'left'
- position?: 'item-aligned' | 'popper'
- placeholder?: string
- disabled?: boolean
- containerPortal?: HTMLDivElement | undefined | null
- className?: string
- sideOffset?: number
- onValueChange?: (value: string) => void
- onOpenChange?: (open: boolean) => void
-}
-
-const Select = ({
- placeholder,
- options,
- value,
- disabled,
- containerPortal,
- block,
- sideOffset,
- position,
- className,
- side,
- open,
- onValueChange,
- onOpenChange,
-}: Props) => (
-
-
-
-
-
-
-
-
-
-
-
- {options &&
- options.map((item, i) => {
- return (
-
-
-
- {item.name}
-
- {item.recommend && (
-
- Recommended
-
- )}
-
-
-
-
-
- )
- })}
-
-
-
-
-)
-
-export { Select }
diff --git a/joi/src/core/Select/styles.scss b/joi/src/core/Select/styles.scss
deleted file mode 100644
index 573833890..000000000
--- a/joi/src/core/Select/styles.scss
+++ /dev/null
@@ -1,77 +0,0 @@
-.select {
- padding: 0 16px;
- background-color: hsla(var(--select-input-bg)) !important;
- border: 1px solid hsla(var(--app-border));
- @apply inline-flex h-8 items-center justify-between gap-8 rounded-md px-3 transition-colors;
- @apply focus-within:outline-none focus-visible:outline-0 focus-visible:ring-1 focus-visible:ring-[hsla(var(--primary-bg))] focus-visible:ring-offset-0;
- @apply text-sm hover:border-[hsla(var(--primary-bg))];
-
- &[data-placeholder] {
- color: hsla(var(--select-placeholder));
- }
-
- &__icon {
- color: hsla(var(--select-icon));
- }
-
- &__content {
- overflow: hidden;
- background-color: hsla(var(--select-bg));
- z-index: 999;
- border: 1px solid hsla(var(--select-border));
- border-radius: 8px;
- box-shadow:
- 0px 10px 38px -10px rgba(22, 23, 24, 0.35),
- 0px 10px 20px -15px rgba(22, 23, 24, 0.2);
- }
-
- &__viewport {
- }
-
- &__disabled {
- cursor: not-allowed;
- pointer-events: none;
- background-color: hsla(var(--disabled-bg)) !important;
- color: hsla(var(--disabled-fg));
- border: none;
- }
-
- &__item {
- display: flex;
- align-items: center;
- padding: 8px 32px 8px 16px;
- position: relative;
- cursor: pointer;
- @apply text-sm;
-
- &:hover {
- background-color: hsla(var(--select-options-active-bg));
- }
-
- &[data-disabled] {
- pointer-events: none;
- }
-
- &[data-highlighted] {
- outline: none;
- }
- }
-
- &__item-indicator {
- position: absolute;
- right: 0;
- width: 25px;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- }
-
- &__scroll-button {
- display: flex;
- align-items: center;
- justify-content: center;
- height: 25px;
- background-color: white;
- cursor: default;
- }
-}
diff --git a/joi/src/core/Slider/Slider.test.tsx b/joi/src/core/Slider/Slider.test.tsx
deleted file mode 100644
index e74bf5cac..000000000
--- a/joi/src/core/Slider/Slider.test.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-import React from 'react'
-import { render, screen, fireEvent } from '@testing-library/react'
-import '@testing-library/jest-dom'
-import { Slider } from './index'
-
-// Mock the styles
-jest.mock('./styles.scss', () => ({}))
-
-// Mock Radix UI Slider
-jest.mock('@radix-ui/react-slider', () => ({
- Root: ({ children, onValueChange, ...props }: any) => (
-
- onValueChange && onValueChange([parseInt(e.target.value)])
- }
- >
-
- {children}
-
- ),
- Track: ({ children }: any) => (
- {children}
- ),
- Range: () =>
,
- Thumb: () =>
,
-}))
-
-describe('@joi/core/Slider', () => {
- it('renders correctly with default props', () => {
- render()
- expect(screen.getByTestId('slider-root')).toBeInTheDocument()
- expect(screen.getByTestId('slider-track')).toBeInTheDocument()
- expect(screen.getByTestId('slider-range')).toBeInTheDocument()
- expect(screen.getByTestId('slider-thumb')).toBeInTheDocument()
- })
-
- it('passes props correctly to SliderPrimitive.Root', () => {
- const props = {
- name: 'test-slider',
- min: 0,
- max: 100,
- value: [50],
- step: 1,
- disabled: true,
- }
- render( )
- const sliderRoot = screen.getByTestId('slider-root')
- expect(sliderRoot).toHaveAttribute('name', 'test-slider')
- expect(sliderRoot).toHaveAttribute('min', '0')
- expect(sliderRoot).toHaveAttribute('max', '100')
- expect(sliderRoot).toHaveAttribute('value', '50')
- expect(sliderRoot).toHaveAttribute('step', '1')
- expect(sliderRoot).toHaveAttribute('disabled', '')
- })
-
- it('calls onValueChange when value changes', () => {
- const onValueChange = jest.fn()
- render( )
- const input = screen.getByTestId('slider-root').querySelector('input')
- fireEvent.change(input!, { target: { value: '75' } })
- expect(onValueChange).toHaveBeenCalledWith([75])
- })
-})
diff --git a/joi/src/core/Slider/index.tsx b/joi/src/core/Slider/index.tsx
deleted file mode 100644
index ea3d8dfca..000000000
--- a/joi/src/core/Slider/index.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-import React from 'react'
-import * as SliderPrimitive from '@radix-ui/react-slider'
-import { twMerge } from 'tailwind-merge'
-
-import './styles.scss'
-
-type Props = {
- name?: string
- min?: number
- max?: number
- onValueChange?(value: number[]): void
- value?: number[]
- defaultValue?: number[]
- step?: number
- disabled?: boolean
-}
-
-const Slider = ({
- name,
- min,
- max,
- onValueChange,
- value,
- defaultValue,
- step,
- disabled,
-}: Props) => (
-
-
-
-
- {value?.map((_, i) => (
-
- ))}
-
-)
-
-export { Slider }
diff --git a/joi/src/core/Slider/styles.scss b/joi/src/core/Slider/styles.scss
deleted file mode 100644
index 6b7cf8575..000000000
--- a/joi/src/core/Slider/styles.scss
+++ /dev/null
@@ -1,43 +0,0 @@
-.slider {
- position: relative;
- display: flex;
- align-items: center;
- user-select: none;
- touch-action: none;
- height: 16px;
-
- &--disabled {
- cursor: not-allowed;
- opacity: 0.2;
- }
-
- &__track {
- background-color: hsla(var(--slider-track-bg));
- position: relative;
- flex-grow: 1;
- border-radius: 9999px;
- height: 4px;
- }
-
- &__range {
- position: absolute;
- background-color: hsla(var(--primary-bg));
- border-radius: 9999px;
- height: 100%;
- }
-
- &__thumb {
- display: block;
- width: 16px;
- height: 16px;
- background-color: hsla(var(--slider-thumb-bg));
- border-radius: 10px;
- padding: 2px;
- border: 2px solid hsla(var(--primary-bg));
-
- &:focus {
- outline: none;
- box-shadow: 0 0 0 5px hsla(var(--slider-track-bg), 50%);
- }
- }
-}
diff --git a/joi/src/core/Switch/Switch.test.tsx b/joi/src/core/Switch/Switch.test.tsx
deleted file mode 100644
index 72f3d8007..000000000
--- a/joi/src/core/Switch/Switch.test.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import React from 'react'
-import { render, fireEvent } from '@testing-library/react'
-import '@testing-library/jest-dom'
-import { Switch } from './index'
-
-// Mock the styles
-jest.mock('./styles.scss', () => ({}))
-
-describe('@joi/core/Switch', () => {
- it('renders correctly', () => {
- const { getByRole } = render( )
- const checkbox = getByRole('checkbox')
- expect(checkbox).toBeInTheDocument()
- })
-
- it('applies custom className', () => {
- const { container } = render( )
- expect(container.firstChild).toHaveClass('switch custom-class')
- })
-
- it('can be checked and unchecked', () => {
- const { getByRole } = render( )
- const checkbox = getByRole('checkbox') as HTMLInputElement
-
- expect(checkbox.checked).toBe(false)
- fireEvent.click(checkbox)
- expect(checkbox.checked).toBe(true)
- fireEvent.click(checkbox)
- expect(checkbox.checked).toBe(false)
- })
-
- it('can be disabled', () => {
- const { getByRole } = render( )
- const checkbox = getByRole('checkbox') as HTMLInputElement
- expect(checkbox).toBeDisabled()
- })
-
- it('calls onChange when clicked', () => {
- const handleChange = jest.fn()
- const { getByRole } = render( )
- const checkbox = getByRole('checkbox')
-
- fireEvent.click(checkbox)
- expect(handleChange).toHaveBeenCalledTimes(1)
- })
-
- it('can have a default checked state', () => {
- const { getByRole } = render( )
- const checkbox = getByRole('checkbox') as HTMLInputElement
- expect(checkbox.checked).toBe(true)
- })
-})
diff --git a/joi/src/core/Switch/index.tsx b/joi/src/core/Switch/index.tsx
deleted file mode 100644
index 28eabe6e6..000000000
--- a/joi/src/core/Switch/index.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import React, { ChangeEvent, InputHTMLAttributes } from 'react'
-
-import { twMerge } from 'tailwind-merge'
-
-import './styles.scss'
-
-export interface SwitchProps extends InputHTMLAttributes {
- disabled?: boolean
- className?: string
- onChange?: (e: ChangeEvent) => void
-}
-
-const Switch = ({
- name,
- checked,
- disabled,
- defaultChecked,
- className,
- onChange,
- ...props
-}: SwitchProps) => {
- return (
-
-
-
-
- )
-}
-export { Switch }
diff --git a/joi/src/core/Switch/styles.scss b/joi/src/core/Switch/styles.scss
deleted file mode 100644
index 9f7adbd4f..000000000
--- a/joi/src/core/Switch/styles.scss
+++ /dev/null
@@ -1,67 +0,0 @@
-.switch {
- position: relative;
- display: inline-block;
- width: 32px;
- height: 18px;
-
- > input {
- opacity: 0;
- width: 0;
- height: 0;
-
- // disabled
- &:disabled {
- + .switch--thumb {
- cursor: not-allowed;
- background-color: hsla(var(--disabled-bg));
- &:before {
- background-color: hsla(var(--disabled-fg));
- }
- }
- // disabled and checked
- &:checked + .switch--thumb {
- cursor: not-allowed;
- background-color: hsla(var(--primary-bg));
- &:before {
- background-color: hsla(var(--disabled-fg));
- }
- }
- }
-
- &:checked + .switch--thumb {
- background-color: hsla(var(--primary-bg));
-
- &::before {
- -webkit-transform: translateX(14px);
- -ms-transform: translateX(14px);
- transform: translateX(14px);
- }
- }
- }
-
- &--thumb {
- position: absolute;
- cursor: pointer;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background-color: hsla(var(--switch-bg));
- -webkit-transition: 0.4s;
- transition: 0.4s;
- border-radius: 20px;
-
- &:before {
- position: absolute;
- content: '';
- height: 14px;
- width: 14px;
- left: 2px;
- bottom: 2px;
- background-color: hsla(var(--switch-fg));
- -webkit-transition: 0.4s;
- transition: 0.4s;
- border-radius: 50%;
- }
- }
-}
diff --git a/joi/src/core/Tabs/Tabs.test.tsx b/joi/src/core/Tabs/Tabs.test.tsx
deleted file mode 100644
index 46bd48435..000000000
--- a/joi/src/core/Tabs/Tabs.test.tsx
+++ /dev/null
@@ -1,115 +0,0 @@
-import React from 'react'
-import { render, screen, fireEvent } from '@testing-library/react'
-import '@testing-library/jest-dom'
-import { Tabs, TabsContent } from './index'
-
-// Mock the Tooltip component
-jest.mock('../Tooltip', () => ({
- Tooltip: ({ children, content, trigger }) => (
-
- {trigger || children}
-
- ),
-}))
-
-// Mock the styles
-jest.mock('./styles.scss', () => ({}))
-
-describe('@joi/core/Tabs', () => {
- const mockOptions = [
- { name: 'Tab 1', value: 'tab1' },
- { name: 'Tab 2', value: 'tab2' },
- {
- name: 'Tab 3',
- value: 'tab3',
- disabled: true,
- tooltipContent: 'Disabled tab',
- },
- ]
-
- it('renders tabs correctly', () => {
- render(
- {}}>
- Content 1
- Content 2
- Content 3
-
- )
-
- expect(screen.getByText('Tab 1')).toBeInTheDocument()
- expect(screen.getByText('Tab 2')).toBeInTheDocument()
- expect(screen.getByText('Tab 3')).toBeInTheDocument()
- expect(screen.getByText('Content 1')).toBeInTheDocument()
- })
-
- it('changes tab content when clicked', () => {
- const { rerender } = render(
- {}}>
- Content 1
- Content 2
- Content 3
-
- )
-
- expect(screen.getByText('Content 1')).toBeInTheDocument()
- expect(screen.queryByText('Content 2')).not.toBeInTheDocument()
-
- fireEvent.click(screen.getByText('Tab 2'))
-
- // Rerender with the new value to simulate the state change
- rerender(
- {}}>
- Content 1
- Content 2
- Content 3
-
- )
-
- expect(screen.queryByText('Content 1')).not.toBeInTheDocument()
- expect(screen.getByText('Content 2')).toBeInTheDocument()
- })
-
- it('disables tab when specified', () => {
- render(
- {}}>
- Content 1
- Content 2
- Content 3
-
- )
-
- expect(screen.getByText('Tab 3')).toHaveAttribute('disabled')
- })
-
- it('renders tooltip for disabled tab', () => {
- render(
- {}}>
- Content 1
- Content 2
- Content 3
-
- )
-
- const tooltipWrapper = screen.getByTestId('mock-tooltip')
- expect(tooltipWrapper).toHaveAttribute(
- 'data-tooltip-content',
- 'Disabled tab'
- )
- })
-
- it('applies the tabStyle if provided', () => {
- render(
- {}}
- tabStyle="segmented"
- />
- )
-
- const tabsContainer = screen.getByTestId('segmented-style')
- expect(tabsContainer).toHaveClass('tabs')
- expect(tabsContainer).toHaveClass('tabs--segmented')
- })
-})
diff --git a/joi/src/core/Tabs/index.tsx b/joi/src/core/Tabs/index.tsx
deleted file mode 100644
index 2dca19831..000000000
--- a/joi/src/core/Tabs/index.tsx
+++ /dev/null
@@ -1,94 +0,0 @@
-import React, { ReactNode } from 'react'
-
-import * as TabsPrimitive from '@radix-ui/react-tabs'
-
-import { Tooltip } from '../Tooltip'
-
-import './styles.scss'
-import { twMerge } from 'tailwind-merge'
-
-type TabStyles = 'segmented'
-
-type TabsProps = {
- options: {
- name: string
- value: string
- disabled?: boolean
- tooltipContent?: string
- }[]
- children?: ReactNode
-
- defaultValue?: string
- tabStyle?: TabStyles
- value: string
- onValueChange?: (value: string) => void
-}
-
-type TabsContentProps = {
- value: string
- children: ReactNode
- className?: string
-}
-
-const TabsContent = ({ value, children, className }: TabsContentProps) => {
- return (
-
- {children}
-
- )
-}
-
-const Tabs = ({
- options,
- children,
- tabStyle,
- defaultValue,
- value,
- onValueChange,
- ...props
-}: TabsProps) => (
-
-
- {options.map((option, i) => {
- return option.disabled ? (
-
- {option.name}
-
- }
- />
- ) : (
-
- {option.name}
-
- )
- })}
-
-
- {children}
-
-)
-
-export { Tabs, TabsContent }
diff --git a/joi/src/core/Tabs/styles.scss b/joi/src/core/Tabs/styles.scss
deleted file mode 100644
index 932b8431a..000000000
--- a/joi/src/core/Tabs/styles.scss
+++ /dev/null
@@ -1,66 +0,0 @@
-.tabs {
- display: flex;
- flex-direction: column;
- width: 100%;
-
- &--segmented {
- background-color: hsla(var(--secondary-bg));
- border-radius: 6px;
- height: 33px;
-
- .tabs__list {
- border: none;
- justify-content: center;
- align-items: center;
- height: 33px;
- }
-
- .tabs__trigger[data-state='active'] {
- background-color: hsla(var(--app-bg));
- border: none;
- height: 25px;
- margin: 0 4px;
- border-radius: 5px;
- }
- }
-
- &__list {
- flex-shrink: 0;
- display: flex;
- border-bottom: 1px solid hsla(var(--app-border));
- }
-
- &__trigger {
- padding: 0 12px;
- flex: 1;
- height: 38px;
- display: flex;
- white-space: nowrap;
- color: hsla(var(--text-secondary));
- align-items: center;
- justify-content: center;
- line-height: 1;
- font-weight: medium;
- user-select: none;
- &:focus {
- position: relative;
- }
- &:disabled {
- cursor: not-allowed;
- opacity: 0.5;
- }
- }
-
- &__content {
- flex-grow: 1;
- border-bottom-left-radius: 6px;
- border-bottom-right-radius: 6px;
- outline: none;
- }
-}
-
-.tabs__trigger[data-state='active'] {
- border-bottom: 1px solid hsla(var(--primary-bg));
- font-weight: 600;
- color: hsla(var(--text-primary));
-}
diff --git a/joi/src/core/TextArea/TextArea.test.tsx b/joi/src/core/TextArea/TextArea.test.tsx
deleted file mode 100644
index e29eed5d0..000000000
--- a/joi/src/core/TextArea/TextArea.test.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-import React from 'react'
-import { render, screen, act } from '@testing-library/react'
-import '@testing-library/jest-dom'
-import { TextArea } from './index'
-
-jest.mock('./styles.scss', () => ({}))
-
-describe('@joi/core/TextArea', () => {
- it('renders correctly', () => {
- render()
- const textareaElement = screen.getByPlaceholderText('Enter text here')
- expect(textareaElement).toBeInTheDocument()
- })
-
- it('applies custom className', () => {
- render()
- const textareaElement = screen.getByRole('textbox')
- expect(textareaElement).toHaveClass('textarea')
- expect(textareaElement).toHaveClass('custom-class')
- })
-
- it('forwards ref correctly', () => {
- const ref = React.createRef()
- render()
- expect(ref.current).toBeInstanceOf(HTMLTextAreaElement)
- })
-
- it('passes through additional props', () => {
- render()
- const textareaElement = screen.getByTestId('custom-textarea')
- expect(textareaElement).toHaveAttribute('rows', '5')
- })
-
- it('should auto resize the textarea based on minResize', () => {
- render()
-
- const textarea = screen.getByRole('textbox') as HTMLTextAreaElement
-
- Object.defineProperty(textarea, 'scrollHeight', {
- value: 20,
- writable: true,
- })
-
- act(() => {
- textarea.value = 'Short text'
- textarea.dispatchEvent(new Event('input', { bubbles: true }))
- })
-
- expect(textarea.style.height).toBe('10px')
- })
-
- it('should auto resize the textarea based on maxResize', () => {
- render()
-
- const textarea = screen.getByRole('textbox') as HTMLTextAreaElement
-
- Object.defineProperty(textarea, 'scrollHeight', {
- value: 100,
- writable: true,
- })
-
- act(() => {
- textarea.value = 'A very long text that should exceed max height'
- textarea.dispatchEvent(new Event('input', { bubbles: true }))
- })
-
- expect(textarea.style.height).toBe('40px')
- })
-})
diff --git a/joi/src/core/TextArea/index.tsx b/joi/src/core/TextArea/index.tsx
deleted file mode 100644
index 306e18d05..000000000
--- a/joi/src/core/TextArea/index.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import React, { forwardRef, useRef, useEffect } from 'react'
-import { twMerge } from 'tailwind-merge'
-
-import './styles.scss'
-
-type ResizeProps = {
- autoResize?: boolean
- minResize?: number
- maxResize?: number
-}
-
-export interface TextAreaProps
- extends ResizeProps,
- React.TextareaHTMLAttributes {}
-
-const TextArea = forwardRef(
- (
- { autoResize, minResize = 80, maxResize = 250, className, ...props },
- ref
- ) => {
- const textareaRef = useRef(null)
-
- useEffect(() => {
- if (autoResize && textareaRef.current) {
- const textarea = textareaRef.current
- textarea.style.height = 'auto'
- const scrollHeight = textarea.scrollHeight
- const newHeight = Math.min(maxResize, Math.max(minResize, scrollHeight))
- textarea.style.height = `${newHeight}px`
- textarea.style.overflow = newHeight >= maxResize ? 'auto' : 'hidden'
- }
- }, [props.value, autoResize, minResize, maxResize])
-
- return (
-
-
-
- )
- }
-)
-
-export { TextArea }
diff --git a/joi/src/core/TextArea/styles.scss b/joi/src/core/TextArea/styles.scss
deleted file mode 100644
index 2920460f1..000000000
--- a/joi/src/core/TextArea/styles.scss
+++ /dev/null
@@ -1,54 +0,0 @@
-.textarea {
- background-color: hsla(var(--textarea-bg));
- border: 1px solid hsla(var(--app-border));
- @apply inline-flex w-full items-center rounded-md border p-3 transition-colors;
- @apply focus-within:outline-none focus-visible:outline-0 focus-visible:ring-1 focus-visible:ring-[hsla(var(--primary-bg))] focus-visible:ring-offset-0;
- @apply file:border-0 file:bg-transparent file:font-medium;
- @apply hover:border-[hsla(var(--primary-bg))];
-
- &__wrapper {
- position: relative;
- }
-
- &.text-right {
- text-align: right;
- }
-
- &::placeholder {
- color: hsla(var(--textarea-placeholder));
- }
-
- &:disabled {
- color: hsla(var(--disabled-fg));
- background-color: hsla(var(--disabled-bg));
- cursor: not-allowed;
- border: none;
- }
-
- &__prefix-icon {
- @apply absolute left-2 top-1/2 -translate-y-1/2;
- color: hsla(var(--textarea-icon));
- + .textarea {
- padding-left: 32px;
- }
- }
- & {
- /* Arrow mouse cursor over the scrollbar */
- cursor: auto;
- }
- &::-webkit-scrollbar {
- width: 6px;
- height: 6px;
- }
- &::-webkit-scrollbar-track,
- &::-webkit-scrollbar-thumb {
- background-clip: content-box;
- border-radius: inherit;
- }
- &::-webkit-scrollbar-track {
- background: hsla(var(--scrollbar-tracker));
- }
- &::-webkit-scrollbar-thumb {
- background: hsla(var(--scrollbar-thumb));
- }
-}
diff --git a/joi/src/core/Tooltip/Tooltip.test.tsx b/joi/src/core/Tooltip/Tooltip.test.tsx
deleted file mode 100644
index 880792b63..000000000
--- a/joi/src/core/Tooltip/Tooltip.test.tsx
+++ /dev/null
@@ -1,121 +0,0 @@
-import React from 'react'
-import '@testing-library/jest-dom'
-import { render, screen, waitFor } from '@testing-library/react'
-import userEvent from '@testing-library/user-event'
-import { Tooltip } from './index'
-
-declare const global: typeof globalThis
-
-// Mock the styles
-jest.mock('./styles.scss', () => ({}))
-
-class ResizeObserverMock {
- observe() {}
- unobserve() {}
- disconnect() {}
-}
-
-global.ResizeObserver = ResizeObserverMock
-
-describe('@joi/core/Tooltip', () => {
- it('renders trigger content', () => {
- render(
- Hover me} content="Tooltip content" />
- )
- expect(screen.getByText('Hover me')).toBeInTheDocument()
- })
-
- it('shows tooltip content on hover', async () => {
- const user = userEvent.setup()
- render(
- Hover me}
- content={Tooltip content }
- />
- )
-
- const trigger = screen.getByTestId('tooltip-trigger')
- await user.hover(trigger)
-
- await waitFor(() => {
- const tooltipContents = screen.queryAllByTestId('tooltip-content')
- expect(tooltipContents.length).toBeGreaterThan(0)
- expect(tooltipContents[tooltipContents.length - 1]).toBeVisible()
- })
- })
-
- it('does not show tooltip when disabled', async () => {
- const user = userEvent.setup()
- render(
- Hover me}
- content={Tooltip content }
- disabled
- />
- )
-
- const trigger = screen.getByTestId('tooltip-trigger')
- await user.hover(trigger)
-
- await waitFor(() => {
- const tooltipContents = screen.queryAllByTestId('tooltip-content')
- tooltipContents.forEach((content) => {
- expect(content).not.toBeVisible()
- })
- })
- })
-
- it('renders arrow when withArrow is true', async () => {
- const user = userEvent.setup()
- render(
- Hover me}
- content={Tooltip content }
- withArrow
- />
- )
-
- const trigger = screen.getByTestId('tooltip-trigger')
- await user.hover(trigger)
-
- await waitFor(() => {
- const tooltipContents = screen.queryAllByTestId('tooltip-content')
- const visibleTooltip = tooltipContents.find((content) =>
- content.matches(':not([style*="display: none"])')
- )
- expect(visibleTooltip?.closest('.tooltip__content')).toBeInTheDocument()
- expect(
- visibleTooltip
- ?.closest('.tooltip__content')
- ?.querySelector('.tooltip__arrow')
- ).toBeInTheDocument()
- })
- })
-
- it('does not render arrow when withArrow is false', async () => {
- const user = userEvent.setup()
- render(
- Hover me}
- content={Tooltip content }
- withArrow={false}
- />
- )
-
- const trigger = screen.getByTestId('tooltip-trigger')
- await user.hover(trigger)
-
- await waitFor(() => {
- const tooltipContents = screen.queryAllByTestId('tooltip-content')
- const visibleTooltip = tooltipContents.find((content) =>
- content.matches(':not([style*="display: none"])')
- )
- expect(visibleTooltip?.closest('.tooltip__content')).toBeInTheDocument()
- expect(
- visibleTooltip
- ?.closest('.tooltip__content')
- ?.querySelector('.tooltip__arrow')
- ).not.toBeInTheDocument()
- })
- })
-})
diff --git a/joi/src/core/Tooltip/index.tsx b/joi/src/core/Tooltip/index.tsx
deleted file mode 100644
index 68e1c5bb4..000000000
--- a/joi/src/core/Tooltip/index.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import React, { ReactNode } from 'react'
-import * as TooltipPrimitive from '@radix-ui/react-tooltip'
-
-import './styles.scss'
-
-export interface TooltipProps {
- trigger?: ReactNode
- content: ReactNode
- side?: 'top' | 'right' | 'bottom' | 'left'
- open?: boolean
- disabled?: boolean
- withArrow?: boolean
- onOpenChange?: (open: boolean) => void
-}
-
-export const Tooltip = ({
- trigger,
- disabled,
- content,
- side = 'top',
- withArrow = true,
- open,
- onOpenChange,
-}: TooltipProps) => {
- return (
-
-
-
- {trigger}
-
-
- {!disabled && content && (
-
- {content}
- {withArrow && (
-
- )}
-
- )}
-
-
-
- )
-}
diff --git a/joi/src/core/Tooltip/styles.scss b/joi/src/core/Tooltip/styles.scss
deleted file mode 100644
index 1ec9a5699..000000000
--- a/joi/src/core/Tooltip/styles.scss
+++ /dev/null
@@ -1,82 +0,0 @@
-.tooltip {
- &__content {
- border-radius: 8px;
- padding: 8px 14px;
- line-height: 1;
- color: hsla(var(--tooltip-fg));
- background-color: hsla(var(--tooltip-bg));
- user-select: none;
- animation-duration: 400ms;
- animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
- will-change: transform, opacity;
- font-weight: 500;
- z-index: 999999999;
- max-width: 240px;
- @apply text-sm leading-normal;
- }
-
- &__arrow {
- fill: hsla(var(--tooltip-bg));
- }
-
- &__trigger {
- @apply cursor-pointer;
- }
-}
-
-.tooltip__content[data-state='delayed-open'][data-side='top'] {
- animation-name: slideDownAndFade;
-}
-.tooltip__content[data-state='delayed-open'][data-side='right'] {
- animation-name: slideLeftAndFade;
-}
-.tooltip__content[data-state='delayed-open'][data-side='bottom'] {
- animation-name: slideUpAndFade;
-}
-.tooltip__content[data-state='delayed-open'][data-side='left'] {
- animation-name: slideRightAndFade;
-}
-
-@keyframes slideUpAndFade {
- from {
- opacity: 0;
- transform: translateY(4px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-@keyframes slideRightAndFade {
- from {
- opacity: 0;
- transform: translateX(-4px);
- }
- to {
- opacity: 1;
- transform: translateX(0);
- }
-}
-
-@keyframes slideDownAndFade {
- from {
- opacity: 0;
- transform: translateY(-4px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-@keyframes slideLeftAndFade {
- from {
- opacity: 0;
- transform: translateX(4px);
- }
- to {
- opacity: 1;
- transform: translateX(0);
- }
-}
diff --git a/joi/src/hooks/useClickOutside/index.tsx b/joi/src/hooks/useClickOutside/index.tsx
deleted file mode 100644
index af47ba484..000000000
--- a/joi/src/hooks/useClickOutside/index.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import { useEffect, useRef } from 'react'
-
-const DEFAULT_EVENTS = ['mousedown', 'touchstart']
-
-export function useClickOutside(
- handler: () => void,
- events?: string[] | null,
- nodes?: (HTMLElement | null)[]
-) {
- const ref = useRef(null)
-
- useEffect(() => {
- const listener = (event: Event) => {
- const target = event.target as HTMLElement
-
- // Check if the target or any ancestor has the data-ignore-outside-clicks attribute
- const shouldIgnore =
- target.closest('[data-ignore-outside-clicks]') !== null
-
- if (Array.isArray(nodes)) {
- const shouldTrigger = nodes.every(
- (node) => !!node && !event.composedPath().includes(node)
- )
- if (shouldTrigger && !shouldIgnore) {
- handler()
- }
- } else if (
- ref.current &&
- !ref.current.contains(target) &&
- !shouldIgnore
- ) {
- handler()
- }
- }
-
- const eventList = events || DEFAULT_EVENTS
- eventList.forEach((event) =>
- document.documentElement.addEventListener(event, listener)
- )
-
- return () => {
- eventList.forEach((event) =>
- document.documentElement.removeEventListener(event, listener)
- )
- }
- }, [handler, nodes, events])
-
- return ref
-}
diff --git a/joi/src/hooks/useClickOutside/useClickOutside.test.tsx b/joi/src/hooks/useClickOutside/useClickOutside.test.tsx
deleted file mode 100644
index 8997721cd..000000000
--- a/joi/src/hooks/useClickOutside/useClickOutside.test.tsx
+++ /dev/null
@@ -1,84 +0,0 @@
-import React from 'react'
-import { render, screen, fireEvent, cleanup } from '@testing-library/react'
-import { useClickOutside } from './index'
-
-const TestComponent = ({
- handler,
- nodes,
-}: {
- handler: () => void
- nodes?: (HTMLElement | null)[]
-}) => {
- const ref = useClickOutside(handler, undefined, nodes)
-
- return (
-
- Click me
-
- )
-}
-
-describe('useClickOutside', () => {
- afterEach(cleanup)
-
- it('should call handler when clicking outside the element', () => {
- const handler = jest.fn()
- render( )
-
- fireEvent.mouseDown(document.body)
- expect(handler).toHaveBeenCalledTimes(1)
- })
-
- it('should not call handler when clicking inside the element', () => {
- const handler = jest.fn()
- render( )
-
- fireEvent.mouseDown(screen.getByTestId('clickable'))
- expect(handler).not.toHaveBeenCalled()
- })
-
- it('should not call handler if target has data-ignore-outside-clicks attribute', () => {
- const handler = jest.fn()
- render(
- <>
-
- Ignore this
- >
- )
-
- // Ensure that the div with the attribute is correctly queried
- fireEvent.mouseDown(screen.getByText('Ignore this'))
- expect(handler).not.toHaveBeenCalled()
- })
-
- it('should call handler when clicking outside if nodes is an empty array', () => {
- const handler = jest.fn()
- render( )
-
- fireEvent.mouseDown(document.body)
- expect(handler).toHaveBeenCalledTimes(1)
- })
-
- it('should not call handler if clicking inside nodes', () => {
- const handler = jest.fn()
- const node = document.createElement('div')
- document.body.appendChild(node)
-
- render(
- <>
-
- >
- )
-
- fireEvent.mouseDown(node)
- expect(handler).not.toHaveBeenCalled()
- })
-
- it('should call handler if nodes is undefined', () => {
- const handler = jest.fn()
- render( )
-
- fireEvent.mouseDown(document.body)
- expect(handler).toHaveBeenCalledTimes(1)
- })
-})
diff --git a/joi/src/hooks/useClipboard/index.ts b/joi/src/hooks/useClipboard/index.ts
deleted file mode 100644
index 219918d73..000000000
--- a/joi/src/hooks/useClipboard/index.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-/* eslint-disable @typescript-eslint/no-explicit-any */
-
-import { useState } from 'react'
-
-export function useClipboard({ timeout = 2000 } = {}) {
- const [error, setError] = useState(null)
- const [copied, setCopied] = useState(false)
- const [copyTimeout, setCopyTimeout] = useState(null)
-
- const handleCopyResult = (value: boolean) => {
- window.clearTimeout(copyTimeout!)
- setCopyTimeout(window.setTimeout(() => setCopied(false), timeout))
- setCopied(value)
- }
-
- const copy = (valueToCopy: any) => {
- if ('clipboard' in navigator) {
- navigator.clipboard
- .writeText(valueToCopy)
- .then(() => handleCopyResult(true))
- .catch((err) => setError(err))
- } else {
- setError(new Error('useClipboard: navigator.clipboard is not supported'))
- }
- }
-
- const reset = () => {
- setCopied(false)
- setError(null)
- window.clearTimeout(copyTimeout!)
- }
-
- return { copy, reset, error, copied }
-}
diff --git a/joi/src/hooks/useClipboard/useClipboard.test.ts b/joi/src/hooks/useClipboard/useClipboard.test.ts
deleted file mode 100644
index 53b4ccd27..000000000
--- a/joi/src/hooks/useClipboard/useClipboard.test.ts
+++ /dev/null
@@ -1,102 +0,0 @@
-import { renderHook, act } from '@testing-library/react'
-import { useClipboard } from './index'
-
-// Mock the navigator.clipboard
-const mockClipboard = {
- writeText: jest.fn(() => Promise.resolve()),
-}
-Object.assign(navigator, { clipboard: mockClipboard })
-
-describe('@joi/hooks/useClipboard', () => {
- beforeEach(() => {
- jest.useFakeTimers()
- jest.spyOn(window, 'setTimeout')
- jest.spyOn(window, 'clearTimeout')
- mockClipboard.writeText.mockClear()
- })
-
- afterEach(() => {
- jest.useRealTimers()
- jest.clearAllMocks()
- })
-
- it('should copy text to clipboard', async () => {
- const { result } = renderHook(() => useClipboard())
-
- await act(async () => {
- result.current.copy('Test text')
- })
-
- expect(navigator.clipboard.writeText).toHaveBeenCalledWith('Test text')
- expect(result.current.copied).toBe(true)
- expect(result.current.error).toBe(null)
- })
-
- it('should set error if clipboard write fails', async () => {
- mockClipboard.writeText.mockRejectedValueOnce(
- new Error('Clipboard write failed')
- )
-
- const { result } = renderHook(() => useClipboard())
-
- await act(async () => {
- result.current.copy('Test text')
- })
-
- expect(result.current.error).toBeInstanceOf(Error)
- expect(result.current.error?.message).toBe('Clipboard write failed')
- })
-
- it('should set error if clipboard is not supported', async () => {
- const originalClipboard = navigator.clipboard
- // @ts-ignore
- delete navigator.clipboard
-
- const { result } = renderHook(() => useClipboard())
-
- await act(async () => {
- result.current.copy('Test text')
- })
-
- expect(result.current.error).toBeInstanceOf(Error)
- expect(result.current.error?.message).toBe(
- 'useClipboard: navigator.clipboard is not supported'
- )
-
- // Restore clipboard support
- Object.assign(navigator, { clipboard: originalClipboard })
- })
-
- it('should reset copied state after timeout', async () => {
- const { result } = renderHook(() => useClipboard({ timeout: 1000 }))
-
- await act(async () => {
- result.current.copy('Test text')
- })
-
- expect(result.current.copied).toBe(true)
-
- act(() => {
- jest.advanceTimersByTime(1000)
- })
-
- expect(result.current.copied).toBe(false)
- })
-
- it('should reset state when reset is called', async () => {
- const { result } = renderHook(() => useClipboard())
-
- await act(async () => {
- result.current.copy('Test text')
- })
-
- expect(result.current.copied).toBe(true)
-
- act(() => {
- result.current.reset()
- })
-
- expect(result.current.copied).toBe(false)
- expect(result.current.error).toBe(null)
- })
-})
diff --git a/joi/src/hooks/useMediaQuery/index.ts b/joi/src/hooks/useMediaQuery/index.ts
deleted file mode 100644
index 31b548db0..000000000
--- a/joi/src/hooks/useMediaQuery/index.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import { useEffect, useRef, useState } from 'react'
-
-export interface UseMediaQueryOptions {
- getInitialValueInEffect: boolean
-}
-
-type MediaQueryCallback = (event: { matches: boolean; media: string }) => void
-
-/**
- * Older versions of Safari (shipped withCatalina and before) do not support addEventListener on matchMedia
- * https://stackoverflow.com/questions/56466261/matchmedia-addlistener-marked-as-deprecated-addeventlistener-equivalent
- * */
-function attachMediaListener(
- query: MediaQueryList,
- callback: MediaQueryCallback
-) {
- try {
- query.addEventListener('change', callback)
- return () => query.removeEventListener('change', callback)
- } catch (e) {
- query.addListener(callback)
- return () => query.removeListener(callback)
- }
-}
-
-export function getInitialValue(query: string, initialValue?: boolean) {
- if (typeof initialValue === 'boolean') {
- return initialValue
- }
-
- if (typeof window !== 'undefined' && 'matchMedia' in window) {
- return window.matchMedia(query).matches
- }
-
- return false
-}
-
-export function useMediaQuery(
- query: string,
- initialValue?: boolean,
- { getInitialValueInEffect }: UseMediaQueryOptions = {
- getInitialValueInEffect: true,
- }
-) {
- const [matches, setMatches] = useState(
- getInitialValueInEffect ? initialValue : getInitialValue(query)
- )
- const queryRef = useRef()
-
- useEffect(() => {
- if ('matchMedia' in window) {
- queryRef.current = window.matchMedia(query)
- setMatches(queryRef.current.matches)
- return attachMediaListener(queryRef.current, (event) =>
- setMatches(event.matches)
- )
- }
-
- return undefined
- }, [query])
-
- return matches
-}
diff --git a/joi/src/hooks/useMediaQuery/useMediaQuery.test.ts b/joi/src/hooks/useMediaQuery/useMediaQuery.test.ts
deleted file mode 100644
index 1d0fa20be..000000000
--- a/joi/src/hooks/useMediaQuery/useMediaQuery.test.ts
+++ /dev/null
@@ -1,160 +0,0 @@
-import { renderHook, act } from '@testing-library/react'
-import { useMediaQuery, getInitialValue } from './index'
-
-const global = globalThis
-const originalWindow = global.window
-
-describe('@joi/hooks/useMediaQuery', () => {
- const matchMediaMock = jest.fn()
-
- beforeAll(() => {
- window.matchMedia = matchMediaMock
- })
-
- afterEach(() => {
- matchMediaMock.mockClear()
- global.window = originalWindow
- })
-
- it('should return undetermined when window is undefined', () => {
- delete (global as any).window
- expect(getInitialValue('(max-width: 600px)', true)).toBe(true)
- expect(getInitialValue('(max-width: 600px)', false)).toBe(false)
- })
-
- it('should return default return false', () => {
- delete (global as any).window
- expect(getInitialValue('(max-width: 600px)')).toBe(false)
- })
-
- it('should return matchMedia result when window is defined and matchMedia exists', () => {
- // Mock window.matchMedia
- const matchMediaMock = jest.fn().mockImplementation((query) => ({
- matches: query === '(max-width: 600px)',
- media: query,
- addEventListener: jest.fn(),
- removeEventListener: jest.fn(),
- }))
-
- // Mock window and matchMedia
- ;(global as any).window = { matchMedia: matchMediaMock }
-
- // Test the function behavior
- expect(getInitialValue('(max-width: 600px)')).toBe(true) // Query should match
- expect(matchMediaMock).toHaveBeenCalledWith('(max-width: 600px)')
-
- // Test with a non-matching query
- expect(getInitialValue('(min-width: 1200px)')).toBe(false) // Query should not match
- expect(matchMediaMock).toHaveBeenCalledWith('(min-width: 1200px)')
- })
-
- it('should return initial value when getInitialValueInEffect is true', () => {
- matchMediaMock.mockImplementation(() => ({
- matches: true,
- addListener: jest.fn(),
- removeListener: jest.fn(),
- }))
-
- const { result } = renderHook(() =>
- useMediaQuery('(min-width: 768px)', true, {
- getInitialValueInEffect: true,
- })
- )
-
- expect(result.current).toBe(true)
- })
-
- it('should return correct value based on media query', () => {
- matchMediaMock.mockImplementation(() => ({
- matches: true,
- addEventListener: jest.fn(),
- removeEventListener: jest.fn(),
- }))
-
- const { result } = renderHook(() => useMediaQuery('(min-width: 768px)'))
-
- expect(result.current).toBe(true)
- })
-
- it('should update value when media query changes', () => {
- let listener: ((event: { matches: boolean }) => void) | null = null
-
- matchMediaMock.mockImplementation(() => ({
- matches: false,
- addEventListener: (_, cb) => {
- listener = cb
- },
- removeEventListener: jest.fn(),
- }))
-
- const { result } = renderHook(() => useMediaQuery('(min-width: 768px)'))
-
- expect(result.current).toBe(false)
-
- act(() => {
- if (listener) {
- listener({ matches: true })
- }
- })
-
- expect(result.current).toBe(true)
- })
-
- it('should handle older browsers without addEventListener', () => {
- let listener: ((event: { matches: boolean }) => void) | null = null
-
- matchMediaMock.mockImplementation(() => ({
- matches: false,
- addListener: (cb) => {
- listener = cb
- },
- removeListener: jest.fn(),
- }))
-
- const { result } = renderHook(() => useMediaQuery('(min-width: 768px)'))
-
- expect(result.current).toBe(false)
-
- act(() => {
- if (listener) {
- listener({ matches: true })
- }
- })
-
- expect(result.current).toBe(true)
- })
-
- it('should return undefined when matchMedia is not available', () => {
- delete (global as any).window.matchMedia
-
- const { result } = renderHook(() => useMediaQuery('(max-width: 600px)'))
- expect(result.current).toBe(undefined)
- })
-
- it('should use initialValue when getInitialValueInEffect is true', () => {
- const { result } = renderHook(() =>
- useMediaQuery('(max-width: 600px)', true, {
- getInitialValueInEffect: true,
- })
- )
- expect(result.current).toBe(true)
- })
-
- it('should use getInitialValue when getInitialValueInEffect is false', () => {
- const { result } = renderHook(() =>
- useMediaQuery('(max-width: 600px)', undefined, {
- getInitialValueInEffect: false,
- })
- )
- expect(result.current).toBe(false)
- })
-
- it('should use initialValue as false when getInitialValueInEffect is true', () => {
- const { result } = renderHook(() =>
- useMediaQuery('(max-width: 600px)', false, {
- getInitialValueInEffect: true,
- })
- )
- expect(result.current).toBe(false)
- })
-})
diff --git a/joi/src/hooks/useOs/index.tsx b/joi/src/hooks/useOs/index.tsx
deleted file mode 100644
index 12e3d2410..000000000
--- a/joi/src/hooks/useOs/index.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import { useLayoutEffect, useState } from 'react'
-
-export type OS =
- | 'undetermined'
- | 'macos'
- | 'ios'
- | 'windows'
- | 'android'
- | 'linux'
-
-export function getOS(): OS {
- if (typeof window === 'undefined') {
- return 'undetermined'
- }
-
- const { userAgent } = window.navigator
- const macosPlatforms = /(Macintosh)|(MacIntel)|(MacPPC)|(Mac68K)/i
- const windowsPlatforms = /(Win32)|(Win64)|(Windows)|(WinCE)/i
- const iosPlatforms = /(iPhone)|(iPad)|(iPod)/i
-
- if (macosPlatforms.test(userAgent)) {
- return 'macos'
- }
- if (iosPlatforms.test(userAgent)) {
- return 'ios'
- }
- if (windowsPlatforms.test(userAgent)) {
- return 'windows'
- }
- if (/Android/i.test(userAgent)) {
- return 'android'
- }
- if (/Linux/i.test(userAgent)) {
- return 'linux'
- }
-
- return 'undetermined'
-}
-
-interface UseOsOptions {
- getValueInEffect: boolean
-}
-
-export function useOs(options: UseOsOptions = { getValueInEffect: true }): OS {
- const [value, setValue] = useState(
- options.getValueInEffect ? 'undetermined' : getOS()
- )
-
- useLayoutEffect(() => {
- if (options.getValueInEffect) {
- setValue(getOS)
- }
- }, [])
-
- return value
-}
diff --git a/joi/src/hooks/useOs/useOs.test.ts b/joi/src/hooks/useOs/useOs.test.ts
deleted file mode 100644
index b66ad1519..000000000
--- a/joi/src/hooks/useOs/useOs.test.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import { renderHook } from '@testing-library/react'
-import { useOs, getOS } from './index'
-import '@testing-library/jest-dom'
-
-const platforms = {
- windows: [
- 'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0',
- ],
- macos: [
- 'Mozilla/5.0 (Macintosh; Intel Mac OS X x.y; rv:42.0) Gecko/20100101 Firefox/42.0',
- ],
- linux: [
- 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36',
- ],
- ios: [
- 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.1 Mobile/15E148 Safari/604.1',
- ],
- android: [
- 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36',
- ],
- undetermined: ['UNKNOWN'],
-} as const
-
-describe('@joi/hooks/useOS', () => {
- const global = globalThis
- const originalWindow = global.window
-
- afterEach(() => {
- global.window = originalWindow
- jest.clearAllMocks()
- })
-
- it('should return undetermined when window is undefined', () => {
- delete (global as any).window
- expect(getOS()).toBe('undetermined')
- })
-
- it('should return undetermined when getValueInEffect is false', () => {
- jest
- .spyOn(window.navigator, 'userAgent', 'get')
- .mockReturnValueOnce('UNKNOWN_USER_AGENT')
-
- const { result } = renderHook(() => useOs({ getValueInEffect: false }))
- expect(result.current).toBe('undetermined')
- })
-
- Object.entries(platforms).forEach(([os, userAgents]) => {
- it.each(userAgents)(`should detect %s platform on ${os}`, (userAgent) => {
- jest
- .spyOn(window.navigator, 'userAgent', 'get')
- .mockReturnValueOnce(userAgent)
-
- const { result } = renderHook(() => useOs())
-
- expect(result.current).toBe(os)
- })
- })
-})
diff --git a/joi/src/hooks/usePageLeave/index.ts b/joi/src/hooks/usePageLeave/index.ts
deleted file mode 100644
index cbc2af6cc..000000000
--- a/joi/src/hooks/usePageLeave/index.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { useEffect } from 'react'
-
-export function usePageLeave(onPageLeave: () => void) {
- useEffect(() => {
- document.documentElement.addEventListener('mouseleave', onPageLeave)
- return () =>
- document.documentElement.removeEventListener('mouseleave', onPageLeave)
- }, [])
-}
diff --git a/joi/src/hooks/usePageLeave/usePageLeave.test.ts b/joi/src/hooks/usePageLeave/usePageLeave.test.ts
deleted file mode 100644
index 093ae31c1..000000000
--- a/joi/src/hooks/usePageLeave/usePageLeave.test.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { renderHook } from '@testing-library/react'
-import { fireEvent } from '@testing-library/react'
-import { usePageLeave } from './index'
-
-describe('@joi/hooks/usePageLeave', () => {
- it('should call onPageLeave when mouse leaves the document', () => {
- const onPageLeaveMock = jest.fn()
- const { result } = renderHook(() => usePageLeave(onPageLeaveMock))
-
- fireEvent.mouseLeave(document.documentElement)
-
- expect(onPageLeaveMock).toHaveBeenCalledTimes(1)
- })
-
- it('should remove event listener on unmount', () => {
- const onPageLeaveMock = jest.fn()
- const removeEventListenerSpy = jest.spyOn(
- document.documentElement,
- 'removeEventListener'
- )
-
- const { unmount } = renderHook(() => usePageLeave(onPageLeaveMock))
-
- unmount()
-
- expect(removeEventListenerSpy).toHaveBeenCalledWith(
- 'mouseleave',
- expect.any(Function)
- )
- removeEventListenerSpy.mockRestore()
- })
-})
diff --git a/joi/src/hooks/useTextSelection/index.ts b/joi/src/hooks/useTextSelection/index.ts
deleted file mode 100644
index 2aac02d81..000000000
--- a/joi/src/hooks/useTextSelection/index.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { useEffect, useState } from 'react'
-
-import { useReducer } from 'react'
-
-const reducer = (value: number) => (value + 1) % 1000000
-
-export function useTextSelection(): Selection | null {
- const [, update] = useReducer(reducer, 0)
- const [selection, setSelection] = useState(null)
-
- const handleSelectionChange = () => {
- setSelection(document.getSelection())
- update()
- }
-
- useEffect(() => {
- setSelection(document.getSelection())
- document.addEventListener('selectionchange', handleSelectionChange)
- return () =>
- document.removeEventListener('selectionchange', handleSelectionChange)
- }, [])
-
- return selection
-}
diff --git a/joi/src/hooks/useTextSelection/useTextSelection.test.ts b/joi/src/hooks/useTextSelection/useTextSelection.test.ts
deleted file mode 100644
index 26efa23e7..000000000
--- a/joi/src/hooks/useTextSelection/useTextSelection.test.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import { renderHook, act } from '@testing-library/react'
-import { useTextSelection } from './index'
-
-describe('@joi/hooks/useTextSelection', () => {
- let mockSelection: Selection
-
- beforeEach(() => {
- mockSelection = {
- toString: jest.fn(),
- removeAllRanges: jest.fn(),
- addRange: jest.fn(),
- } as unknown as Selection
-
- jest.spyOn(document, 'getSelection').mockReturnValue(mockSelection)
- jest.spyOn(document, 'addEventListener')
- jest.spyOn(document, 'removeEventListener')
- })
-
- afterEach(() => {
- jest.restoreAllMocks()
- })
-
- it('should return the initial selection', () => {
- const { result } = renderHook(() => useTextSelection())
- expect(result.current).toBe(mockSelection)
- })
-
- it('should add and remove event listener', () => {
- const { unmount } = renderHook(() => useTextSelection())
-
- expect(document.addEventListener).toHaveBeenCalledWith(
- 'selectionchange',
- expect.any(Function)
- )
-
- unmount()
-
- expect(document.removeEventListener).toHaveBeenCalledWith(
- 'selectionchange',
- expect.any(Function)
- )
- })
-
- it('should update selection when selectionchange event is triggered', () => {
- const { result } = renderHook(() => useTextSelection())
-
- const newMockSelection = { toString: jest.fn() } as unknown as Selection
- jest.spyOn(document, 'getSelection').mockReturnValue(newMockSelection)
-
- act(() => {
- document.dispatchEvent(new Event('selectionchange'))
- })
-
- expect(result.current).toBe(newMockSelection)
- })
-})
diff --git a/joi/src/index.test.ts b/joi/src/index.test.ts
deleted file mode 100644
index 7937ea138..000000000
--- a/joi/src/index.test.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import * as components from './index'
-
-// Mock styles globally for all components in this test
-jest.mock('./core/Tooltip/styles.scss', () => ({}))
-jest.mock('./core/ScrollArea/styles.scss', () => ({}))
-jest.mock('./core/Button/styles.scss', () => ({}))
-jest.mock('./core/Switch/styles.scss', () => ({}))
-jest.mock('./core/Progress/styles.scss', () => ({}))
-jest.mock('./core/Checkbox/styles.scss', () => ({}))
-jest.mock('./core/Badge/styles.scss', () => ({}))
-jest.mock('./core/Modal/styles.scss', () => ({}))
-jest.mock('./core/Slider/styles.scss', () => ({}))
-jest.mock('./core/Input/styles.scss', () => ({}))
-jest.mock('./core/Select/styles.scss', () => ({}))
-jest.mock('./core/TextArea/styles.scss', () => ({}))
-jest.mock('./core/Tabs/styles.scss', () => ({}))
-jest.mock('./core/Accordion/styles.scss', () => ({}))
-jest.mock('./core/Dropdown/styles.scss', () => ({}))
-
-describe('Exports', () => {
- it('exports all components and hooks', () => {
- expect(components.Tooltip).toBeDefined()
- expect(components.ScrollArea).toBeDefined()
- expect(components.Button).toBeDefined()
- expect(components.Switch).toBeDefined()
- expect(components.Progress).toBeDefined()
- expect(components.Checkbox).toBeDefined()
- expect(components.Badge).toBeDefined()
- expect(components.Modal).toBeDefined()
- expect(components.Slider).toBeDefined()
- expect(components.Input).toBeDefined()
- expect(components.Select).toBeDefined()
- expect(components.TextArea).toBeDefined()
- expect(components.Tabs).toBeDefined()
- expect(components.Accordion).toBeDefined()
-
- expect(components.useClipboard).toBeDefined()
- expect(components.usePageLeave).toBeDefined()
- expect(components.useTextSelection).toBeDefined()
- expect(components.useClickOutside).toBeDefined()
- expect(components.useOs).toBeDefined()
- expect(components.useMediaQuery).toBeDefined()
- })
-})
diff --git a/joi/src/index.ts b/joi/src/index.ts
deleted file mode 100644
index ebeb3ac4d..000000000
--- a/joi/src/index.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-export * from './core/Tooltip'
-export * from './core/ScrollArea'
-export * from './core/Button'
-export * from './core/Switch'
-export * from './core/Progress'
-export * from './core/Checkbox'
-export * from './core/Badge'
-export * from './core/Modal'
-export * from './core/Slider'
-export * from './core/Input'
-export * from './core/Select'
-export * from './core/TextArea'
-export * from './core/Tabs'
-export * from './core/Accordion'
-export * from './core/Dropdown'
-
-export * from './hooks/useClipboard'
-export * from './hooks/usePageLeave'
-export * from './hooks/useTextSelection'
-export * from './hooks/useClickOutside'
-export * from './hooks/useOs'
-export * from './hooks/useMediaQuery'
diff --git a/joi/tailwind.config.js b/joi/tailwind.config.js
deleted file mode 100644
index 8ad039ff0..000000000
--- a/joi/tailwind.config.js
+++ /dev/null
@@ -1,10 +0,0 @@
-/** @type {import('tailwindcss').Config} */
-
-// eslint-disable-next-line no-undef
-module.exports = {
- content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'],
- theme: {
- extend: {},
- },
- plugins: [],
-}
diff --git a/joi/tsconfig.json b/joi/tsconfig.json
deleted file mode 100644
index 4aed01c50..000000000
--- a/joi/tsconfig.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "compilerOptions": {
- "target": "esnext",
- "declaration": true,
- "declarationDir": "dist/types",
- "types": ["jest", "@testing-library/jest-dom"],
- "module": "esnext",
- "lib": ["es6", "dom", "es2016", "es2017"],
- "sourceMap": true,
- "jsx": "react",
- "moduleResolution": "Node",
- "allowSyntheticDefaultImports": true,
- "esModuleInterop": true
- },
- "include": ["src/**/*"],
- "exclude": ["node_modules", "*.test.ts"]
-}
diff --git a/package.json b/package.json
index 8e4f2033c..373205e01 100644
--- a/package.json
+++ b/package.json
@@ -3,52 +3,34 @@
"private": true,
"workspaces": {
"packages": [
- "joi",
"core",
- "electron",
- "web",
- "web-app",
- "server"
+ "web-app"
]
},
"scripts": {
"lint": "yarn workspace @janhq/web-app lint",
- "test:unit": "jest",
- "test:coverage": "yarn workspace @janhq/web-app test",
+ "dev": "yarn dev:tauri",
+ "build": "yarn build:web && yarn build:tauri",
"test": "yarn workspace @janhq/web-app test",
- "test-local": "yarn lint && yarn build:test && yarn test",
- "copy:assets": "cpx \"pre-install/*.tgz\" \"electron/pre-install/\" && cpx \"themes/**\" \"electron/themes\"",
- "copy:assets:tauri": "cpx \"pre-install/*.tgz\" \"src-tauri/resources/pre-install/\" && cpx \"themes/**\" \"src-tauri/resources/themes\"",
- "dev:electron": "yarn copy:assets && yarn workspace jan dev",
- "dev:web:standalone": "concurrently \"yarn workspace @janhq/web dev\" \"wait-on http://localhost:3000 && rsync -av --prune-empty-dirs --include '*/' --include 'dist/***' --include 'package.json' --include 'tsconfig.json' --exclude '*' ./extensions/ web/.next/static/extensions/\"",
+ "test:coverage": "yarn workspace @janhq/web-app test",
"dev:web": "yarn workspace @janhq/web-app dev",
- "dev:server": "yarn workspace @janhq/server dev",
- "dev": "concurrently -n \"NEXT,ELECTRON\" -c \"yellow,blue\" --kill-others \"yarn dev:web\" \"yarn dev:electron\"",
+ "dev:tauri": "CLEAN=true yarn build:icon && yarn copy:assets:tauri && tauri dev",
"install:cortex:linux:darwin": "cd src-tauri/binaries && ./download.sh",
"install:cortex:win32": "cd src-tauri/binaries && download.bat",
"install:cortex": "run-script-os",
- "download:bin": "node ./scripts/download-bin.mjs",
+ "copy:assets:tauri": "cpx \"pre-install/*.tgz\" \"src-tauri/resources/pre-install/\" && cpx \"themes/**\" \"src-tauri/resources/themes\"",
"copy:lib": "run-script-os",
"copy:lib:linux": "cpx \"./lib/linux/*.so\" \"./src-tauri/resources/lib/\"",
"copy:lib:win32": "cpx \"./lib/windows/*.dll\" \"./src-tauri/resources/lib/\"",
"copy:lib:darwin": "mkdir -p \"./src-tauri/resources/lib/\"",
- "dev:tauri": "yarn build:icon && yarn copy:assets:tauri && tauri dev",
+ "download:bin": "node ./scripts/download-bin.mjs",
"build:tauri:linux:win32": "yarn download:bin && yarn install:cortex && yarn build:icon && yarn copy:assets:tauri && yarn tauri build",
"build:tauri:darwin": "yarn install:cortex && yarn build:icon && yarn copy:assets:tauri && yarn tauri build --target universal-apple-darwin",
"build:tauri": "run-script-os",
"build:icon": "tauri icon ./src-tauri/icons/icon.png",
- "build:server": "cd server && yarn build",
"build:core": "cd core && yarn build && yarn pack",
"build:web": "yarn workspace @janhq/web-app build",
- "build:electron": "yarn copy:assets && yarn workspace jan build",
- "build:electron:test": "yarn workspace jan build:test",
"build:extensions": "rimraf ./pre-install/*.tgz || true && yarn workspace @janhq/core build && cd extensions && yarn install && yarn workspaces foreach -Apt run build:publish",
- "build:test": "yarn copy:assets && yarn workspace @janhq/web build && cpx \"web/out/**\" \"electron/renderer/\" && yarn workspace jan build:test",
- "build": "yarn build:web && yarn build:electron",
- "build-tauri": "yarn build:web && yarn build:tauri",
- "build:publish": "yarn copy:assets && yarn build:web && yarn workspace jan build:publish",
- "dev:joi": "yarn workspace @janhq/joi install && yarn workspace @janhq/joi dev",
- "build:joi": "yarn workspace @janhq/joi build",
"prepare": "husky"
},
"devDependencies": {
diff --git a/web/.eslintrc.js b/web/.eslintrc.js
deleted file mode 100644
index d04e3acb1..000000000
--- a/web/.eslintrc.js
+++ /dev/null
@@ -1,154 +0,0 @@
-/* eslint-disable @typescript-eslint/naming-convention */
-module.exports = {
- parser: '@typescript-eslint/parser',
- plugins: ['@typescript-eslint', 'react', 'react-hooks'],
- ignorePatterns: [
- 'build',
- 'dist',
- 'node_modules',
- 'renderer',
- '.next',
- '_next',
- '*.md',
- 'out',
- '**/*.test.tsx',
- '**/*.test.ts',
- 'testRunner.js',
- 'jest.config.js',
- ],
- extends: [
- 'next/core-web-vitals',
- 'eslint:recommended',
- 'plugin:import/typescript',
- 'plugin:@typescript-eslint/recommended',
- 'plugin:react/recommended',
- 'prettier',
- 'plugin:prettier/recommended',
- 'eslint-config-next/core-web-vitals',
- ],
- globals: {
- React: true,
- JSX: true,
- },
- settings: {
- react: {
- version: 'detect',
- },
- },
- rules: {
- '@next/next/no-server-import-in-page': 'off',
-
- '@typescript-eslint/naming-convention': [
- 'error',
- {
- selector: 'default',
- format: ['camelCase', 'PascalCase'],
- },
- {
- selector: 'variableLike',
- format: ['camelCase', 'PascalCase', 'UPPER_CASE'],
- leadingUnderscore: 'allow',
- },
- {
- selector: 'property',
- format: ['camelCase', 'snake_case', 'PascalCase', 'UPPER_CASE'],
- },
- {
- selector: 'memberLike',
- format: ['camelCase', 'PascalCase'],
- },
- {
- selector: 'typeLike',
- format: ['PascalCase'],
- },
- {
- selector: 'property',
- filter: '^__html$',
- format: null,
- },
- ],
- '@typescript-eslint/explicit-module-boundary-types': 'off',
- '@next/next/no-img-element': 'off',
- '@next/next/no-html-link-for-pages': 'off',
- 'react/display-name': 'off',
- 'react/no-is-mounted': 'off',
- 'react-hooks/rules-of-hooks': 'error',
- '@typescript-eslint/no-unused-vars': ['warn'],
- 'import/order': [
- 'error',
- {
- 'alphabetize': { order: 'asc' },
- 'groups': ['builtin', 'external', 'parent', 'sibling', 'index'],
- 'pathGroups': [
- {
- pattern: 'react*',
- group: 'external',
- position: 'before',
- },
- {
- pattern: 'next*',
- group: 'external',
- position: 'before',
- },
- {
- pattern: 'next/*',
- group: 'external',
- position: 'before',
- },
- {
- pattern: '@/assets/**',
- group: 'parent',
- position: 'before',
- },
- {
- pattern: '@/components/**',
- group: 'parent',
- position: 'before',
- },
- {
- pattern: '@/containers/**',
- group: 'parent',
- position: 'before',
- },
- {
- pattern: '@/context/**',
- group: 'parent',
- position: 'before',
- },
- {
- pattern: '@/constants/**',
- group: 'parent',
- position: 'before',
- },
- {
- pattern: '@/hooks/**',
- group: 'parent',
- position: 'before',
- },
- {
- pattern: '@/services/**',
- group: 'parent',
- position: 'before',
- },
- {
- pattern: '@/screens/**',
- group: 'parent',
- position: 'before',
- },
- {
- pattern: '@/utils/**',
- group: 'parent',
- position: 'before',
- },
- {
- pattern: '@/styles/**',
- group: 'parent',
- position: 'before',
- },
- ],
- 'pathGroupsExcludedImportTypes': ['react'],
- 'newlines-between': 'always-and-inside-groups',
- },
- ],
- },
-}
diff --git a/web/.gitignore b/web/.gitignore
deleted file mode 100644
index 824527df8..000000000
--- a/web/.gitignore
+++ /dev/null
@@ -1,42 +0,0 @@
-# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
-
-# dependencies
-/node_modules
-/.pnp
-.pnp.js
-
-# testing
-/coverage
-
-# next.js
-/.next/
-/out/
-
-# production
-/build
-
-# misc
-.DS_Store
-*.pem
-
-# debug
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-
-# local env files
-.env*.local
-
-# vercel
-.vercel
-
-# typescript
-*.tsbuildinfo
-next-env.d.ts
-.env
-
-# Firebase config file
-app/_services/firebase/firebase_configs.json
-# Sentry Auth Token
-.sentryclirc
-
diff --git a/web/.prettierrc b/web/.prettierrc
deleted file mode 100644
index 46f1abcb0..000000000
--- a/web/.prettierrc
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "semi": false,
- "singleQuote": true,
- "quoteProps": "consistent",
- "trailingComma": "es5",
- "endOfLine": "auto",
- "plugins": ["prettier-plugin-tailwindcss"]
-}
diff --git a/web/Dockerfile b/web/Dockerfile
deleted file mode 100644
index 91992c5e9..000000000
--- a/web/Dockerfile
+++ /dev/null
@@ -1,48 +0,0 @@
-FROM node:20-alpine AS base
-
-# 1. Install dependencies only when needed
-FROM base AS deps
-# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
-RUN apk add --no-cache libc6-compat
-
-WORKDIR /app
-
-# Install dependencies based on the preferred package manager
-COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
-RUN yarn add graphql && yarn install
-
-# 2. Rebuild the source code only when needed
-FROM base AS builder
-WORKDIR /app
-COPY --from=deps /app/node_modules ./node_modules
-COPY . .
-# This will do the trick, use the corresponding env file for each environment.
-
-RUN yarn build
-
-# 3. Production image, copy all the files and run next
-FROM base AS runner
-WORKDIR /app
-
-ENV NODE_ENV=production
-
-RUN \
- addgroup -g 1001 -S nodejs; \
- adduser -S nextjs -u 1001
-
-COPY --from=builder /app/public ./public
-
-# Automatically leverage output traces to reduce image size
-# https://nextjs.org/docs/advanced-features/output-file-tracing
-COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next
-COPY --from=builder /app/node_modules ./node_modules
-COPY --from=builder /app/package.json ./package.json
-
-
-USER nextjs
-
-EXPOSE 3000
-
-ENV PORT 3000
-
-CMD ["npm", "start"]
\ No newline at end of file
diff --git a/web/app/error.tsx b/web/app/error.tsx
deleted file mode 100644
index 8dbee29ae..000000000
--- a/web/app/error.tsx
+++ /dev/null
@@ -1,89 +0,0 @@
-'use client' // Error components must be Client Components
-
-import { useEffect, useState } from 'react'
-
-export default function Error({
- error,
-}: {
- error: Error & { digest?: string }
- reset: () => void
-}) {
- const [showFull, setShowFull] = useState(false)
- useEffect(() => {
- // Log the error to an error reporting service
- console.error(error)
- }, [error])
-
- return (
- <>
-
-
-
-
- Oops! Unexpected error occurred.
-
-
- Something went wrong. Try to{' '}
- window.location.reload()}
- >
- refresh this page
- {' '}
- or feel free to{' '}
-
- contact us
- {' '}
- if the problem persists.
-
-
-
Error:
-
{error.message}
-
-
- {showFull ? error.stack : error.stack?.slice(0, 200)}
-
-
setShowFull(!showFull)}
- className="mt-1 text-red-700 underline focus:outline-none"
- >
- {showFull ? 'Show less' : 'Show more'}
-
-
-
-
-
- >
- )
-}
diff --git a/web/app/favicon.ico b/web/app/favicon.ico
deleted file mode 100644
index eb73ad7f0..000000000
Binary files a/web/app/favicon.ico and /dev/null differ
diff --git a/web/app/layout.tsx b/web/app/layout.tsx
deleted file mode 100644
index aaa905a49..000000000
--- a/web/app/layout.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import { PropsWithChildren } from 'react'
-
-import { Metadata } from 'next'
-
-import '@/styles/main.scss'
-
-export const metadata: Metadata = {
- title: 'Jan',
- description:
- 'Self-hosted, local, AI Inference Platform that scales from personal use to production deployments for a team.',
-}
-
-export default function RootLayout({ children }: PropsWithChildren) {
- return (
-
- {children}
-
- )
-}
diff --git a/web/app/page.tsx b/web/app/page.tsx
deleted file mode 100644
index 363ca2de4..000000000
--- a/web/app/page.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import BaseLayout from '@/containers/Layout'
-
-import Providers from '@/containers/Providers'
-
-export default function Page() {
- return (
-
-
-
- )
-}
diff --git a/web/app/privacy/page.tsx b/web/app/privacy/page.tsx
deleted file mode 100644
index 67c897540..000000000
--- a/web/app/privacy/page.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-import { Metadata } from 'next'
-
-import Image from 'next/image'
-import Link from 'next/link'
-
-export const metadata: Metadata = {
- title: 'Privacy - Jan.ai',
- description: 'Privacy',
-}
-
-const Page = () => {
- return (
-
-
-
- Privacy Policy
-
- Jan is committed to protecting your privacy and ensuring that your
- personal information is handled in a safe and responsible way. This
- policy outlines how we collect, store, and use your personal
- information when you use our mobile application.
-
- Data Collection and Usage
-
- When you use Jan, we may collect certain information about you,
- including your name, email address, and other personal information
- that you provide to us. We use this information to provide you with
- the best possible experience when using our app.
-
-
- We may also collect certain non-personal information, such as your
- device type, operating system, and app usage data. This information is
- used to improve our app and to provide you with a better user
- experience.
-
- Data Sharing
-
- We do not share your personal information with third parties except as
- required by law or as necessary to provide you with the services you
- have requested. We may share non-personal information with third
- parties for the purpose of improving our app and providing you with a
- better user experience.
-
- Data Security
-
- We take the security of your personal information seriously and have
- implemented appropriate technical and organizational measures to
- protect your personal information from unauthorized access,
- disclosure, or misuse.
-
- Your Choices
-
- You have the right to access, update, and delete your personal
- information at any time. You may also opt-out of receiving marketing
- communications from us by following the unsubscribe link included in
- our emails.
-
- Contact Us
-
- If you have any questions or concerns about our privacy policy, please
- contact us at hello@jan.ai .
-
-
-
- )
-}
-export default Page
diff --git a/web/app/search/SelectedText.tsx b/web/app/search/SelectedText.tsx
deleted file mode 100644
index fdb24bbfb..000000000
--- a/web/app/search/SelectedText.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import React, { useCallback, useEffect, useRef } from 'react'
-
-import { useAtom } from 'jotai'
-import { X } from 'lucide-react'
-
-import { selectedTextAtom } from '@/containers/Providers/Jotai'
-
-const SelectedText = ({ onCleared }: { onCleared?: () => void }) => {
- const [text, setText] = useAtom(selectedTextAtom)
- const containerRef = useRef(null)
-
- useEffect(() => {
- if (text.trim().length === 0) {
- window.core?.api?.quickAskSizeUpdated(0)
- } else {
- window.core?.api?.quickAskSizeUpdated(
- (containerRef.current?.offsetHeight ?? 0) + 14
- )
- }
- })
-
- const onClearClicked = useCallback(() => {
- setText('')
- onCleared?.()
- }, [setText, onCleared])
-
- const shouldShowSelectedText = text.trim().length > 0
-
- return shouldShowSelectedText ? (
-
- ) : (
-
- )
-}
-
-export default SelectedText
diff --git a/web/app/search/UserInput.tsx b/web/app/search/UserInput.tsx
deleted file mode 100644
index fcabc8ea4..000000000
--- a/web/app/search/UserInput.tsx
+++ /dev/null
@@ -1,85 +0,0 @@
-import React, { useState, useRef, useEffect } from 'react'
-
-import { Button } from '@janhq/joi'
-import { useAtomValue } from 'jotai'
-
-import { Send } from 'lucide-react'
-
-import LogoMark from '@/containers/Brand/Logo/Mark'
-
-import { selectedTextAtom } from '@/containers/Providers/Jotai'
-
-import SelectedText from './SelectedText'
-
-const UserInput = () => {
- const [inputValue, setInputValue] = useState('')
- const inputRef = useRef(null)
- const formRef = useRef(null)
- const selectedText = useAtomValue(selectedTextAtom)
-
- useEffect(() => {
- inputRef.current?.focus()
- })
-
- useEffect(() => {
- const onKeyDown = (e: KeyboardEvent) => {
- if (e.key === 'Escape') {
- window.core?.api?.hideQuickAskWindow()
- }
- }
-
- document.addEventListener('keydown', onKeyDown)
-
- return () => {
- document.removeEventListener('keydown', onKeyDown)
- }
- }, [])
-
- const handleChange = (
- event:
- | React.ChangeEvent
- | React.ChangeEvent
- ) => {
- const { value } = event.target
- setInputValue(value)
- }
-
- const onSubmit = (e: React.FormEvent) => {
- e.preventDefault()
- if (inputValue.trim() !== '') {
- const fullText = `${inputValue} ${selectedText}`.trim()
- window.core?.api?.sendQuickAskInput(fullText)
- setInputValue('')
- window.core?.api?.hideQuickAskWindow()
- window.core?.api?.showMainWindow()
- }
- }
-
- return (
-
-
-
inputRef?.current?.focus()} />
-
- )
-}
-
-export default UserInput
diff --git a/web/app/search/layout.tsx b/web/app/search/layout.tsx
deleted file mode 100644
index f3ce06cb7..000000000
--- a/web/app/search/layout.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-'use client'
-
-import { useEffect } from 'react'
-
-import { AppConfiguration, getUserHomePath } from '@janhq/core'
-
-import { useSetAtom } from 'jotai'
-
-import ClipboardListener from '@/containers/Providers/ClipboardListener'
-
-import ThemeWrapper from '@/containers/Providers/Theme'
-
-import { useLoadTheme } from '@/hooks/useLoadTheme'
-
-import { setupCoreServices } from '@/services/coreService'
-
-import Search from './page'
-
-import { defaultJanDataFolderAtom } from '@/helpers/atoms/App.atom'
-import { janDataFolderPathAtom } from '@/helpers/atoms/AppConfig.atom'
-
-export default function RootLayout() {
- const setJanDataFolderPath = useSetAtom(janDataFolderPathAtom)
- const setJanDefaultDataFolder = useSetAtom(defaultJanDataFolderAtom)
-
- useEffect(() => {
- setupCoreServices()
- }, [])
-
- useEffect(() => {
- window.core?.api
- ?.getAppConfigurations()
- ?.then((appConfig: AppConfiguration) => {
- setJanDataFolderPath(appConfig.data_folder)
- })
- }, [setJanDataFolderPath])
-
- useEffect(() => {
- async function getDefaultJanDataFolder() {
- const defaultJanDataFolder = await getUserHomePath()
-
- setJanDefaultDataFolder(defaultJanDataFolder)
- }
- getDefaultJanDataFolder()
- }, [setJanDefaultDataFolder])
-
- useLoadTheme()
-
- return (
-
-
-
-
- )
-}
diff --git a/web/app/search/page.tsx b/web/app/search/page.tsx
deleted file mode 100644
index 51cf04549..000000000
--- a/web/app/search/page.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-'use client'
-
-import UserInput from './UserInput'
-
-const Search = () => {
- return (
-
- )
-}
-
-export default Search
diff --git a/web/app/support/page.tsx b/web/app/support/page.tsx
deleted file mode 100644
index 198e3bd4f..000000000
--- a/web/app/support/page.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import { Metadata } from 'next'
-
-import Image from 'next/image'
-import Link from 'next/link'
-
-export const metadata: Metadata = {
- title: 'Support - Jan.ai',
- description: 'Support',
-}
-
-const Page = () => {
- return (
-
-
-
- Support
- Get fast support in our Discord channel
-
-
-
-
- If you have any questions or concerns about our privacy policy or
- support services, please contact us at{' '}
- hello@jan.ai .
-
-
-
- )
-}
-export default Page
diff --git a/web/constants/screens.ts b/web/constants/screens.ts
deleted file mode 100644
index cb12be3c2..000000000
--- a/web/constants/screens.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export enum MainViewState {
- Hub,
- Settings,
- Thread,
- LocalServer,
-}
diff --git a/web/constants/tagType.ts b/web/constants/tagType.ts
deleted file mode 100644
index 021dbee72..000000000
--- a/web/constants/tagType.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-export enum ModelPerformance {
- PerformancePositive = 'PerformancePositive',
-
- PerformanceNeutral = 'PerformanceNeutral',
-
- PerformanceNegative = 'PerformanceNegative',
-}
-
-export enum HardwareCompatibility {
- HardwareCompatible = 'HardwareCompatible',
-
- HardwareIncompatible = 'HardwareIncompatible',
-}
-
-export enum ExpectedPerformance {
- ExpectPerformanceMedium = 'ExpectPerformanceMedium',
-}
-
-export enum ModelFormat {
- GGUF = 'GGUF',
-}
-
-export enum FreestyleTag {
- FreeStyle = 'FreeStyle',
-}
-
-export enum VersionTag {
- Version = 'Version',
-}
-
-export enum QuantMethodTag {
- Default = 'Default',
-}
-
-export enum NumOfBit {
- Default = 'Default',
-}
-
-export enum RamRequired {
- RamDefault = 'RamDefault',
-}
-
-export enum UsecaseTag {
- UsecaseDefault = 'UsecaseDefault',
-}
-
-export enum MiscellaneousTag {
- MiscellaneousDefault = 'MiscellaneousDefault',
-}
-
-export type TagType =
- | ModelPerformance
- | HardwareCompatibility
- | ExpectedPerformance
- | ModelFormat
- | FreestyleTag
- | VersionTag
- | QuantMethodTag
- | NumOfBit
- | RamRequired
- | UsecaseTag
- | MiscellaneousTag
diff --git a/web/containers/AutoLink/index.test.tsx b/web/containers/AutoLink/index.test.tsx
deleted file mode 100644
index 9f4610a80..000000000
--- a/web/containers/AutoLink/index.test.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import React from 'react'
-import { render, screen } from '@testing-library/react'
-import '@testing-library/jest-dom'
-import AutoLink from './index'
-
-describe('AutoLink Component', () => {
- it('renders text without links correctly', () => {
- const text = 'This is a test without links.'
- render( )
- expect(screen.getByText(text)).toBeInTheDocument()
- })
-
- it('renders text with a single link correctly', () => {
- const text = 'Check this link: https://example.com'
- render( )
- const link = screen.getByText('https://example.com')
- expect(link).toBeInTheDocument()
- expect(link).toHaveAttribute('href', 'https://example.com')
- expect(link).toHaveAttribute('target', 'blank')
- })
-
- it('renders text with multiple links correctly', () => {
- const text = 'Visit https://example.com and http://test.com'
- render( )
- const link1 = screen.getByText('https://example.com')
- const link2 = screen.getByText('http://test.com')
- expect(link1).toBeInTheDocument()
- expect(link1).toHaveAttribute('href', 'https://example.com')
- expect(link1).toHaveAttribute('target', 'blank')
- expect(link2).toBeInTheDocument()
- expect(link2).toHaveAttribute('href', 'http://test.com')
- expect(link2).toHaveAttribute('target', 'blank')
- })
-
- it('renders text with a link without protocol correctly', () => {
- const text = 'Visit example.com for more info.'
- render( )
- const link = screen.getByText('example.com')
- expect(link).toBeInTheDocument()
- expect(link).toHaveAttribute('href', 'http://example.com')
- expect(link).toHaveAttribute('target', 'blank')
- })
-})
diff --git a/web/containers/AutoLink/index.tsx b/web/containers/AutoLink/index.tsx
deleted file mode 100644
index 0f10f478a..000000000
--- a/web/containers/AutoLink/index.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import { memo } from 'react'
-
-type Props = {
- text: string
-}
-
-const AutoLink = ({ text }: Props) => {
- const delimiter =
- /((?:https?:\/\/)?(?:(?:[a-z0-9]?(?:[a-z0-9-]{1,61}[a-z0-9])?\.[^.|\s])+[a-z.]*[a-z]+|(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3})(?::\d{1,5})*[a-z0-9.,_/~#&=;%+?\-\\(\\)]*)/gi
-
- return (
- <>
- {text &&
- typeof text === 'string' &&
- text.split(delimiter).map((word) => {
- const match = word.match(delimiter)
- if (match) {
- const url = match[0]
- return (
-
- {url}
-
- )
- }
- return word
- })}
- >
- )
-}
-
-export default memo(AutoLink)
diff --git a/web/containers/BlankState/index.test.tsx b/web/containers/BlankState/index.test.tsx
deleted file mode 100644
index 53cb2ece7..000000000
--- a/web/containers/BlankState/index.test.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import React from 'react'
-import { render, screen } from '@testing-library/react'
-import '@testing-library/jest-dom'
-import BlankState from './index'
-
-describe('BlankState Component', () => {
- it('renders title correctly', () => {
- const title = 'Test Title'
- render( )
- expect(screen.getByText(title)).toBeInTheDocument()
- })
-
- it('renders description correctly when provided', () => {
- const title = 'Test Title'
- const description = 'Test Description'
- render( )
- expect(screen.getByText(description)).toBeInTheDocument()
- })
-
- it('does not render description when not provided', () => {
- const title = 'Test Title'
- render( )
- expect(screen.queryByText('Test Description')).not.toBeInTheDocument()
- })
-
- it('renders action correctly when provided', () => {
- const title = 'Test Title'
- const action = Test Action
- render( )
- expect(screen.getByText('Test Action')).toBeInTheDocument()
- })
-
- it('does not render action when not provided', () => {
- const title = 'Test Title'
- render( )
- expect(screen.queryByText('Test Action')).not.toBeInTheDocument()
- })
-})
diff --git a/web/containers/BlankState/index.tsx b/web/containers/BlankState/index.tsx
deleted file mode 100644
index bcfa2b306..000000000
--- a/web/containers/BlankState/index.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import { ReactNode } from 'react'
-
-import LogoMark from '@/containers/Brand/Logo/Mark'
-
-type Props = {
- title: string
- description?: string
- action?: ReactNode
-}
-
-const BlankState = ({ title, description, action }: Props) => {
- return (
-
-
-
{title}
- {description && (
-
{description}
- )}
- {action && action}
-
- )
-}
-
-export default BlankState
diff --git a/web/containers/Brand/Logo/Mark.test.tsx b/web/containers/Brand/Logo/Mark.test.tsx
deleted file mode 100644
index 68df134c9..000000000
--- a/web/containers/Brand/Logo/Mark.test.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import React from 'react'
-import { render, screen } from '@testing-library/react'
-import '@testing-library/jest-dom'
-import LogoMark from './Mark'
-
-describe('LogoMark Component', () => {
- it('renders with default width and height', () => {
- render( )
- const image = screen.getByAltText('Jan - Logo')
- expect(image).toBeInTheDocument()
- expect(image).toHaveAttribute('width', '24')
- expect(image).toHaveAttribute('height', '24')
- })
-
- it('renders with provided width and height', () => {
- render( )
- const image = screen.getByAltText('Jan - Logo')
- expect(image).toBeInTheDocument()
- expect(image).toHaveAttribute('width', '48')
- expect(image).toHaveAttribute('height', '48')
- })
-
- it('applies provided className', () => {
- render( )
- const image = screen.getByAltText('Jan - Logo')
- expect(image).toBeInTheDocument()
- expect(image).toHaveClass('custom-class')
- })
-
- it('renders with the correct src and alt attributes', () => {
- render( )
- const image = screen.getByAltText('Jan - Logo')
- expect(image).toBeInTheDocument()
- expect(image).toHaveAttribute('src', 'icons/app_icon.svg')
- expect(image).toHaveAttribute('alt', 'Jan - Logo')
- })
-})
diff --git a/web/containers/Brand/Logo/Mark.tsx b/web/containers/Brand/Logo/Mark.tsx
deleted file mode 100644
index f26b9ee2a..000000000
--- a/web/containers/Brand/Logo/Mark.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import Image from 'next/image'
-
-type Props = {
- width?: number
- height?: number
- className?: string
-}
-
-export default function LogoMark(props: Props) {
- const { width = 24, height = 24, className } = props
- return (
-
- )
-}
diff --git a/web/containers/CenterPanelContainer/index.test.tsx b/web/containers/CenterPanelContainer/index.test.tsx
deleted file mode 100644
index 9e6fda007..000000000
--- a/web/containers/CenterPanelContainer/index.test.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import { render, screen } from '@testing-library/react'
-import { useAtomValue } from 'jotai'
-import CenterPanelContainer from './index'
-import '@testing-library/jest-dom'
-
-// Mock useAtomValue from jotai
-jest.mock('jotai', () => ({
- ...jest.requireActual('jotai'),
- useAtomValue: jest.fn(),
-}))
-
-describe('CenterPanelContainer', () => {
- it('renders with reduceTransparent set to true', () => {
- // Mock reduceTransparentAtom to be true
- ;(useAtomValue as jest.Mock).mockReturnValue(true)
-
- render(
-
- Test Child
-
- )
-
- // Check that the container renders with no border or rounded corners
- const container = screen.getByText('Test Child').parentElement
- expect(container).not.toHaveClass('rounded-lg border')
- })
-
- it('renders with reduceTransparent set to false', () => {
- // Mock reduceTransparentAtom to be false
- ;(useAtomValue as jest.Mock).mockReturnValue(false)
-
- render(
-
- Test Child
-
- )
-
- // Check that the container renders with border and rounded corners
- const container = screen.getByText('Test Child').parentElement
- expect(container).toHaveClass('rounded-lg border')
- })
-
- it('renders children correctly', () => {
- // Mock reduceTransparentAtom to be true for this test
- ;(useAtomValue as jest.Mock).mockReturnValue(true)
-
- render(
-
- Child Content
-
- )
-
- // Verify that the child content is rendered
- expect(screen.getByText('Child Content')).toBeInTheDocument()
- })
-})
diff --git a/web/containers/CenterPanelContainer/index.tsx b/web/containers/CenterPanelContainer/index.tsx
deleted file mode 100644
index 2be54ac09..000000000
--- a/web/containers/CenterPanelContainer/index.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import { PropsWithChildren } from 'react'
-
-import { useMediaQuery } from '@janhq/joi'
-import { useAtomValue } from 'jotai'
-
-import { twMerge } from 'tailwind-merge'
-
-import { MainViewState } from '@/constants/screens'
-
-import { leftPanelWidthAtom } from '../LeftPanelContainer'
-
-import { rightPanelWidthAtom } from '../RightPanelContainer'
-
-import {
- mainViewStateAtom,
- showLeftPanelAtom,
- showRightPanelAtom,
-} from '@/helpers/atoms/App.atom'
-import { reduceTransparentAtom } from '@/helpers/atoms/Setting.atom'
-
-type Props = {
- isShowStarterScreen?: boolean
-} & PropsWithChildren
-
-const CenterPanelContainer = ({ children, isShowStarterScreen }: Props) => {
- const reduceTransparent = useAtomValue(reduceTransparentAtom)
- const matches = useMediaQuery('(max-width: 880px)')
- const showLeftPanel = useAtomValue(showLeftPanelAtom)
- const showRightPanel = useAtomValue(showRightPanelAtom)
- const mainViewState = useAtomValue(mainViewStateAtom)
- const rightPanelWidth = useAtomValue(rightPanelWidthAtom)
- const leftPanelWidth = useAtomValue(leftPanelWidthAtom)
-
- return (
-
- )
-}
-
-export default CenterPanelContainer
diff --git a/web/containers/Checkbox/index.tsx b/web/containers/Checkbox/index.tsx
deleted file mode 100644
index b81d70c04..000000000
--- a/web/containers/Checkbox/index.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-import { Switch, Tooltip } from '@janhq/joi'
-
-import { InfoIcon } from 'lucide-react'
-
-type Props = {
- name: string
- title: string
- disabled?: boolean
- description: string
- checked: boolean
- onValueChanged?: (e: string | number | boolean) => void
-}
-
-const Checkbox = ({
- title,
- checked,
- disabled = false,
- description,
- onValueChanged,
-}: Props) => {
- const onCheckedChange = (checked: boolean) => {
- onValueChanged?.(checked)
- }
-
- return (
-
-
-
{title}
-
- }
- content={description}
- />
-
-
onCheckedChange(e.target.checked)}
- disabled={disabled}
- />
-
- )
-}
-
-export default Checkbox
diff --git a/web/containers/CopyInstruction/index.test.tsx b/web/containers/CopyInstruction/index.test.tsx
deleted file mode 100644
index 957decec7..000000000
--- a/web/containers/CopyInstruction/index.test.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-import { render, screen, fireEvent } from '@testing-library/react'
-import { useAtom } from 'jotai'
-import '@testing-library/jest-dom'
-import CopyOverInstruction from './index'
-
-// Mock the `useAtom` hook from jotai
-jest.mock('jotai', () => ({
- useAtom: jest.fn(),
- atom: jest.fn(),
-}))
-
-describe('CopyOverInstruction', () => {
- const setCopyOverInstructionEnabled = jest.fn()
-
- beforeEach(() => {
- ;(useAtom as jest.Mock).mockImplementation(() => [
- false,
- setCopyOverInstructionEnabled,
- ])
- })
-
- afterEach(() => {
- jest.clearAllMocks()
- })
-
- it('should render the component with the switch in the correct state', () => {
- render( )
-
- // Assert the text is rendered
- expect(
- screen.getByText(/Save instructions for new threads/i)
- ).toBeInTheDocument()
-
- // Assert the switch is rendered and in the unchecked state
- const switchInput = screen.getByRole('checkbox')
- expect(switchInput).toBeInTheDocument()
- expect(switchInput).not.toBeChecked()
- })
-
- it('should call setCopyOverInstructionEnabled when the switch is toggled', () => {
- render( )
-
- const switchInput = screen.getByRole('checkbox')
-
- // Simulate toggling the switch
- fireEvent.click(switchInput)
-
- // Assert that the atom setter is called with true when checked
- expect(setCopyOverInstructionEnabled).toHaveBeenCalledWith(true)
- })
-
- it('should reflect the updated state when the atom value changes', () => {
- // Mock the atom to return true (enabled state)
- ;(useAtom as jest.Mock).mockImplementation(() => [
- true,
- setCopyOverInstructionEnabled,
- ])
-
- render( )
-
- const switchInput = screen.getByRole('checkbox')
-
- // The switch should now be checked
- expect(switchInput).toBeChecked()
- })
-})
diff --git a/web/containers/CopyInstruction/index.tsx b/web/containers/CopyInstruction/index.tsx
deleted file mode 100644
index 4ce00507d..000000000
--- a/web/containers/CopyInstruction/index.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import { ChangeEvent, useCallback } from 'react'
-
-import { Switch } from '@janhq/joi'
-import { useAtom } from 'jotai'
-
-import { copyOverInstructionEnabledAtom } from '@/helpers/atoms/App.atom'
-
-const CopyOverInstruction: React.FC = () => {
- const [copyOverInstructionEnabled, setCopyOverInstructionEnabled] = useAtom(
- copyOverInstructionEnabledAtom
- )
-
- const onSwitchToggled = useCallback(
- (e: ChangeEvent) => {
- setCopyOverInstructionEnabled(e.target.checked)
- },
- [setCopyOverInstructionEnabled]
- )
-
- return (
-
-
Save instructions for new threads
-
-
- )
-}
-
-export default CopyOverInstruction
diff --git a/web/containers/EngineSetting/index.test.tsx b/web/containers/EngineSetting/index.test.tsx
deleted file mode 100644
index 140a36395..000000000
--- a/web/containers/EngineSetting/index.test.tsx
+++ /dev/null
@@ -1,115 +0,0 @@
-import { render } from '@testing-library/react'
-import '@testing-library/jest-dom'
-import EngineSetting from './index'
-import SettingComponentBuilder from '@/containers/ModelSetting/SettingComponent'
-import { SettingComponentProps } from '@janhq/core'
-
-// Mock the SettingComponentBuilder component
-jest.mock('@/containers/ModelSetting/SettingComponent', () =>
- jest.fn(() => null)
-)
-
-describe('EngineSetting', () => {
- const mockComponentData: SettingComponentProps[] = [
- {
- key: 'setting1',
- title: 'Setting 1',
- description: 'This is the first setting.',
- controllerType: 'input',
- controllerProps: {
- placeholder: 'Enter text',
- value: 'default text',
- type: 'text',
- },
- },
- {
- key: 'setting2',
- title: 'Setting 2',
- description: 'This is the second setting.',
- controllerType: 'slider',
- controllerProps: {
- min: 0,
- max: 100,
- step: 1,
- value: 50,
- },
- },
- {
- key: 'setting3',
- title: 'Setting 3',
- description: 'This is the third setting.',
- controllerType: 'checkbox',
- controllerProps: {
- value: true,
- },
- },
- ]
-
- const onValueChangedMock = jest.fn()
-
- afterEach(() => {
- jest.clearAllMocks() // Clear mocks after each test
- })
-
- it('renders SettingComponentBuilder with the correct props', () => {
- render(
-
- )
-
- // Check that SettingComponentBuilder is called with the correct props
- expect(SettingComponentBuilder).toHaveBeenCalledWith(
- {
- componentProps: mockComponentData,
- disabled: false,
- onValueUpdated: onValueChangedMock,
- },
- {}
- )
- })
-
- it('renders SettingComponentBuilder with disabled prop', () => {
- render(
-
- )
-
- // Check that SettingComponentBuilder is called with disabled=true
- expect(SettingComponentBuilder).toHaveBeenCalledWith(
- {
- componentProps: mockComponentData,
- disabled: true,
- onValueUpdated: onValueChangedMock,
- },
- {}
- )
- })
-
- it('calls onValueChanged when the value is updated', () => {
- // Simulating value update in SettingComponentBuilder
- ;(SettingComponentBuilder as jest.Mock).mockImplementation(
- ({ onValueUpdated }) => {
- // Simulate calling the value update handler
- onValueUpdated('setting1', 'new value')
- return null
- }
- )
-
- render(
-
- )
-
- // Assert that onValueChanged is called with the correct parameters
- expect(onValueChangedMock).toHaveBeenCalledWith('setting1', 'new value')
- })
-})
diff --git a/web/containers/EngineSetting/index.tsx b/web/containers/EngineSetting/index.tsx
deleted file mode 100644
index 0ae2929bf..000000000
--- a/web/containers/EngineSetting/index.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import { SettingComponentProps } from '@janhq/core'
-
-import SettingComponentBuilder from '@/containers/ModelSetting/SettingComponent'
-
-type Props = {
- componentData: SettingComponentProps[]
- onValueChanged: (
- key: string,
- value: string | number | boolean | string[]
- ) => void
- disabled?: boolean
-}
-
-const EngineSetting = ({
- componentData,
- onValueChanged,
- disabled = false,
-}: Props) => {
- return (
-
- )
-}
-
-export default EngineSetting
diff --git a/web/containers/ErrorMessage/index.test.tsx b/web/containers/ErrorMessage/index.test.tsx
deleted file mode 100644
index cb0cc3cf6..000000000
--- a/web/containers/ErrorMessage/index.test.tsx
+++ /dev/null
@@ -1,90 +0,0 @@
-// ErrorMessage.test.tsx
-import React from 'react'
-import { render, screen, fireEvent } from '@testing-library/react'
-import '@testing-library/jest-dom'
-import ErrorMessage from './index'
-import { ThreadMessage, MessageStatus, ErrorCode } from '@janhq/core'
-import { useAtomValue, useSetAtom } from 'jotai'
-import useSendChatMessage from '@/hooks/useSendChatMessage'
-
-// Mock the dependencies
-jest.mock('jotai', () => {
- const originalModule = jest.requireActual('jotai')
- return {
- ...originalModule,
- useAtomValue: jest.fn(),
- useSetAtom: jest.fn(),
- }
-})
-
-jest.mock('@/hooks/useSendChatMessage', () => ({
- __esModule: true,
- default: jest.fn(),
-}))
-
-describe('ErrorMessage Component', () => {
- const mockSetMainState = jest.fn()
- const mockSetSelectedSettingScreen = jest.fn()
- const mockSetModalTroubleShooting = jest.fn()
- const mockResendChatMessage = jest.fn()
-
- beforeEach(() => {
- jest.clearAllMocks()
- ; (useAtomValue as jest.Mock).mockReturnValue([])
- ; (useSetAtom as jest.Mock).mockReturnValue(mockSetMainState)
- ; (useSetAtom as jest.Mock).mockReturnValue(mockSetSelectedSettingScreen)
- ; (useSetAtom as jest.Mock).mockReturnValue(mockSetModalTroubleShooting)
- ; (useSendChatMessage as jest.Mock).mockReturnValue({
- resendChatMessage: mockResendChatMessage,
- })
- })
-
- it('renders error message with InvalidApiKey correctly', () => {
- const message: ThreadMessage = {
- id: '1',
- metadata: {
- error: MessageStatus.Error,
- error_code: ErrorCode.InvalidApiKey,
- },
- status: "completed",
- content: [{ text: { value: 'Invalid API Key' } }],
- } as ThreadMessage
-
- render( )
-
- expect(screen.getByTestId('invalid-API-key-error')).toBeInTheDocument()
- expect(screen.getByText('Settings')).toBeInTheDocument()
- })
-
- it('renders general error message correctly', () => {
- const message: ThreadMessage = {
- id: '1',
- status: "completed",
- metadata: {
- error: MessageStatus.Error,
- error_code: ErrorCode.Unknown
- },
- content: [{ text: { value: 'Unknown error occurred' } }],
- } as ThreadMessage
-
- render( )
-
- expect(screen.getByText('Troubleshooting')).toBeInTheDocument()
- })
-
- it('opens troubleshooting modal when link is clicked', () => {
- const message: ThreadMessage = {
- id: '1',
- status: "completed",
- metadata: {
- error: MessageStatus.Error,
- error_code: ErrorCode.Unknown,
- }, content: [{ text: { value: 'Unknown error occurred' } }],
- } as ThreadMessage
-
- render( )
-
- fireEvent.click(screen.getByText('Troubleshooting'))
- expect(mockSetModalTroubleShooting).toHaveBeenCalledWith(true)
- })
-})
diff --git a/web/containers/ErrorMessage/index.tsx b/web/containers/ErrorMessage/index.tsx
deleted file mode 100644
index ab5a35d32..000000000
--- a/web/containers/ErrorMessage/index.tsx
+++ /dev/null
@@ -1,163 +0,0 @@
-import { useRef, useState } from 'react'
-
-import {
- EngineManager,
- ErrorCode,
- InferenceEngine,
- ThreadMessage,
-} from '@janhq/core'
-
-import { useAtomValue, useSetAtom } from 'jotai'
-
-import { CheckIcon, ClipboardIcon, SearchCodeIcon } from 'lucide-react'
-
-import AutoLink from '@/containers/AutoLink'
-import ModalTroubleShooting, {
- modalTroubleShootingAtom,
-} from '@/containers/ModalTroubleShoot'
-
-import { MainViewState } from '@/constants/screens'
-
-import { mainViewStateAtom } from '@/helpers/atoms/App.atom'
-
-import { activeAssistantAtom } from '@/helpers/atoms/Assistant.atom'
-import { selectedSettingAtom } from '@/helpers/atoms/Setting.atom'
-
-const ErrorMessage = ({
- message,
- errorComponent,
-}: {
- message?: ThreadMessage
- errorComponent?: React.ReactNode
-}) => {
- const setModalTroubleShooting = useSetAtom(modalTroubleShootingAtom)
- const setMainState = useSetAtom(mainViewStateAtom)
- const setSelectedSettingScreen = useSetAtom(selectedSettingAtom)
- const activeAssistant = useAtomValue(activeAssistantAtom)
- const errorDivRef = useRef(null)
- const [copied, setCopied] = useState(false)
-
- const getEngine = () => {
- const engineName = activeAssistant?.model?.engine
- return engineName ? EngineManager.instance().get(engineName) : null
- }
-
- const handleCopy = () => {
- if (errorDivRef.current) {
- const errorText = errorDivRef.current.innerText
- if (errorText) {
- navigator.clipboard.writeText(errorText)
- setCopied(true)
- setTimeout(() => setCopied(false), 2000)
- }
- }
- }
-
- const getErrorTitle = () => {
- const engine = getEngine()
-
- switch (message?.metadata?.error_code) {
- case ErrorCode.InvalidApiKey:
- case ErrorCode.AuthenticationError:
- return (
- <>
-
- Invalid API key. Please check your API key from{' '}
- {
- setMainState(MainViewState.Settings)
- setSelectedSettingScreen(activeAssistant?.model?.engine ?? '')
- }}
- >
- Settings
- {' '}
- and try again.
-
- >
- )
-
- default:
- return (
-
- {message?.content[0]?.text?.value === 'Failed to fetch' &&
- engine &&
- engine?.name !== InferenceEngine.cortex_llamacpp ? (
-
- No internet connection. Switch to an on-device model or
- check connection.
-
- ) : (
- <>
- {message?.content[0]?.text?.value && (
-
- )}
- {!message?.content[0]?.text?.value && (
- Something went wrong. Please try again.
- )}
- >
- )}
-
- )
- }
- }
-
- return (
-
-
-
-
-
- Error
-
-
-
- setModalTroubleShooting(true)}
- >
-
- Troubleshooting
-
-
-
-
- {copied ? (
- <>
-
- Copied
- >
- ) : (
- <>
-
- Copy
- >
- )}
-
-
-
-
-
- {errorComponent ? errorComponent : getErrorTitle()}
-
-
-
-
- )
-}
-export default ErrorMessage
diff --git a/web/containers/Layout/BottomPanel/DownloadingState/index.tsx b/web/containers/Layout/BottomPanel/DownloadingState/index.tsx
deleted file mode 100644
index 1c3419aab..000000000
--- a/web/containers/Layout/BottomPanel/DownloadingState/index.tsx
+++ /dev/null
@@ -1,96 +0,0 @@
-import { Fragment } from 'react'
-
-import { Progress, Modal, Button } from '@janhq/joi'
-
-import { useAtomValue, useSetAtom } from 'jotai'
-
-import useDownloadModel from '@/hooks/useDownloadModel'
-import {
- modelDownloadStateAtom,
- removeDownloadStateAtom,
-} from '@/hooks/useDownloadState'
-
-import { formatDownloadPercentage } from '@/utils/converter'
-
-export default function DownloadingState() {
- const downloadStates = useAtomValue(modelDownloadStateAtom)
- const removeDownloadState = useSetAtom(removeDownloadStateAtom)
- const { abortModelDownload } = useDownloadModel()
-
- const totalCurrentProgress = Object.values(downloadStates)
- .map((a) => a.size.transferred + a.size.transferred)
- .reduce((partialSum, a) => partialSum + a, 0)
-
- const totalSize = Object.values(downloadStates)
- .map((a) => a.size.total + a.size.total)
- .reduce((partialSum, a) => partialSum + a, 0)
-
- const totalPercentage =
- totalSize !== 0 ? ((totalCurrentProgress / totalSize) * 100).toFixed(2) : 0
-
- return (
-
- {Object.values(downloadStates)?.length > 0 && (
-
-
-
- Downloading{' '}
- {Object.values(downloadStates).length > 1 &&
- `1/${Object.values(downloadStates).length}`}
-
-
-
-
- {totalPercentage}%
-
-
- }
- content={
-
- {Object.values(downloadStates).map((item, i) => (
-
-
-
-
-
- {item?.modelId}
-
-
- {formatDownloadPercentage(item?.percent)}
-
-
-
{
- if (item?.modelId) {
- removeDownloadState(item?.modelId)
- abortModelDownload(item?.modelId)
- }
- }}
- >
- Cancel
-
-
-
- ))}
-
- }
- />
- )}
-
- )
-}
diff --git a/web/containers/Layout/BottomPanel/ImportingModelState/index.tsx b/web/containers/Layout/BottomPanel/ImportingModelState/index.tsx
deleted file mode 100644
index 2963f2d88..000000000
--- a/web/containers/Layout/BottomPanel/ImportingModelState/index.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-import { Fragment, useCallback } from 'react'
-
-import { Progress } from '@janhq/joi'
-import { useAtomValue, useSetAtom } from 'jotai'
-
-import { setImportModelStageAtom } from '@/hooks/useImportModel'
-
-import { importingModelsAtom } from '@/helpers/atoms/Model.atom'
-
-const ImportingModelState = () => {
- const importingModels = useAtomValue(importingModelsAtom)
- const setImportModelStage = useSetAtom(setImportModelStageAtom)
-
- const isImportingModels =
- importingModels.filter((m) => m.status === 'IMPORTING').length > 0
-
- const finishedImportModelCount = importingModels.filter(
- (model) => model.status === 'IMPORTED' || model.status === 'FAILED'
- ).length
-
- let transferredSize = 0
- importingModels.forEach((model) => {
- transferredSize += (model.percentage ?? 0) * 100 * model.size
- })
-
- const totalSize = importingModels.reduce((acc, model) => acc + model.size, 0)
-
- const progress = totalSize === 0 ? 0 : transferredSize / totalSize
-
- const onClick = useCallback(() => {
- setImportModelStage('IMPORTING_MODEL')
- }, [setImportModelStage])
-
- return (
-
- {isImportingModels ? (
-
-
- Importing model ({finishedImportModelCount}/{importingModels.length}
- )
-
-
-
-
-
- {progress.toFixed(2)}%
-
-
-
- ) : null}
-
- )
-}
-
-export default ImportingModelState
diff --git a/web/containers/Layout/BottomPanel/SystemMonitor/SystemMonitor.test.tsx b/web/containers/Layout/BottomPanel/SystemMonitor/SystemMonitor.test.tsx
deleted file mode 100644
index ca336b0e5..000000000
--- a/web/containers/Layout/BottomPanel/SystemMonitor/SystemMonitor.test.tsx
+++ /dev/null
@@ -1,127 +0,0 @@
-/**
- * @jest-environment jsdom
- */
-import '@testing-library/jest-dom'
-import React from 'react'
-import { render, screen, waitFor } from '@testing-library/react'
-import SystemMonitor from './index'
-import { useAtom, useAtomValue } from 'jotai'
-import {
- cpuUsageAtom,
- gpusAtom,
- totalRamAtom,
- usedRamAtom,
-} from '@/helpers/atoms/SystemBar.atom'
-import useGetSystemResources from '@/hooks/useGetSystemResources'
-
-// Mock dependencies
-jest.mock('jotai', () => ({
- useAtomValue: jest.fn(),
- useSetAtom: jest.fn(),
- useAtom: jest.fn(),
- atom: jest.fn(),
-}))
-
-// Mock the hooks and atoms
-jest.mock('@/hooks/useGetSystemResources')
-
-jest.mock('@/hooks/usePath', () => ({
- usePath: () => ({ onRevealInFinder: jest.fn() }),
-}))
-
-jest.mock('@/helpers/atoms/App.atom', () => ({
- showSystemMonitorPanelAtom: { init: false },
-}))
-
-jest.mock('@/helpers/atoms/Setting.atom', () => ({
- reduceTransparentAtom: { init: false },
-}))
-
-jest.mock('@/helpers/atoms/SystemBar.atom', () => ({
- totalRamAtom: { init: 16000000000 },
- usedRamAtom: { init: 8000000000 },
- cpuUsageAtom: { init: 50 },
- gpusAtom: { init: [] },
- ramUtilitizedAtom: { init: 50 },
-}))
-
-describe('SystemMonitor', () => {
- const mockWatch = jest.fn()
- const mockStopWatching = jest.fn()
- beforeAll(() => {
- jest.clearAllMocks()
- ;(useGetSystemResources as jest.Mock).mockReturnValue({
- watch: mockWatch,
- stopWatching: mockStopWatching,
- })
- })
- it('renders without crashing', () => {
- ;(useAtom as jest.Mock).mockReturnValue([false, jest.fn()])
- render( )
- expect(screen.getByText('System Monitor')).toBeInTheDocument()
- })
-
- it('renders information on expand', () => {
- const mockGpusAtom = jest.fn()
- const mockShowPanel = jest.fn()
- ;(useAtom as jest.Mock).mockImplementation(mockShowPanel)
- // Mock Jotai hooks
- ;(useAtomValue as jest.Mock).mockImplementation((atom) => {
- switch (atom) {
- case totalRamAtom:
- return 16000000000
- case usedRamAtom:
- return 8000000000
- case cpuUsageAtom:
- return 30
- case gpusAtom:
- return mockGpusAtom
- default:
- return jest.fn()
- }
- })
- mockGpusAtom.mockImplementation(() => [])
- mockShowPanel.mockImplementation(() => [true, jest.fn()])
-
- render( )
-
- expect(screen.getByText('Running Models')).toBeInTheDocument()
- expect(screen.getByText('App Log')).toBeInTheDocument()
- expect(screen.getByText('7.45GB / 14.90GB')).toBeInTheDocument()
- expect(screen.getByText('30%')).toBeInTheDocument()
- })
-
- it('it should not request system resource on close', async () => {
- const mockGpusAtom = jest.fn()
- const mockShowPanel = jest.fn()
- ;(useAtom as jest.Mock).mockImplementation(mockShowPanel)
-
- // Mock Jotai hooks
- ;(useAtomValue as jest.Mock).mockImplementation((atom) => {
- switch (atom) {
- case totalRamAtom:
- return 16000000000
- case usedRamAtom:
- return 8000000000
- case cpuUsageAtom:
- return 30
- case gpusAtom:
- return mockGpusAtom
- default:
- return jest.fn()
- }
- })
- mockGpusAtom.mockImplementation(() => [])
- mockShowPanel.mockImplementation(() => [true, jest.fn()])
-
- await waitFor(async () => {
- await render( )
-
- const toggle = screen.getByTestId('system-monitoring')
- toggle.click()
- })
-
- expect(mockWatch).not.toHaveBeenCalled()
- expect(mockStopWatching).toHaveBeenCalled()
- })
-})
diff --git a/web/containers/Layout/BottomPanel/SystemMonitor/TableActiveModel/index.tsx b/web/containers/Layout/BottomPanel/SystemMonitor/TableActiveModel/index.tsx
deleted file mode 100644
index e47296ffc..000000000
--- a/web/containers/Layout/BottomPanel/SystemMonitor/TableActiveModel/index.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import { Tooltip, Button, Badge } from '@janhq/joi'
-
-import { useAtom } from 'jotai'
-
-import { useActiveModel } from '@/hooks/useActiveModel'
-
-import { useGetEngines } from '@/hooks/useEngineManagement'
-
-import { toGigabytes } from '@/utils/converter'
-
-import { isLocalEngine } from '@/utils/modelEngine'
-
-import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom'
-
-const TableActiveModel = () => {
- const { activeModel, stateModel, stopModel } = useActiveModel()
- const { engines } = useGetEngines()
-
- const [serverEnabled, setServerEnabled] = useAtom(serverEnabledAtom)
-
- return (
-
-
-
- {activeModel && isLocalEngine(engines, activeModel.engine) ? (
-
-
-
- {activeModel.name}
-
-
-
- {activeModel.metadata?.size
- ? toGigabytes(activeModel.metadata?.size)
- : '-'}
-
-
-
- {
- stopModel()
- window.core?.api?.stopServer()
- setServerEnabled(false)
- }}
- >
- Stop
-
- }
- content="The API server is running, stop the model will
- also stop the server"
- disabled={!serverEnabled}
- />
-
-
-
- ) : (
-
-
- No models are loaded into memory
-
-
- )}
-
-
-
- )
-}
-
-export default TableActiveModel
diff --git a/web/containers/Layout/BottomPanel/SystemMonitor/index.tsx b/web/containers/Layout/BottomPanel/SystemMonitor/index.tsx
deleted file mode 100644
index 05ec643fe..000000000
--- a/web/containers/Layout/BottomPanel/SystemMonitor/index.tsx
+++ /dev/null
@@ -1,199 +0,0 @@
-import { Fragment, useCallback, useState } from 'react'
-
-import { Progress } from '@janhq/joi'
-import { useAtom, useAtomValue } from 'jotai'
-import {
- MonitorIcon,
- XIcon,
- ChevronDown,
- ChevronUp,
- FolderOpenIcon,
-} from 'lucide-react'
-
-import { twMerge } from 'tailwind-merge'
-
-import useGetSystemResources from '@/hooks/useGetSystemResources'
-
-import { usePath } from '@/hooks/usePath'
-
-import { toGigabytes } from '@/utils/converter'
-
-import { utilizedMemory } from '@/utils/memory'
-
-import TableActiveModel from './TableActiveModel'
-
-import { showSystemMonitorPanelAtom } from '@/helpers/atoms/App.atom'
-import { reduceTransparentAtom } from '@/helpers/atoms/Setting.atom'
-import {
- cpuUsageAtom,
- gpusAtom,
- ramUtilitizedAtom,
- totalRamAtom,
- usedRamAtom,
-} from '@/helpers/atoms/SystemBar.atom'
-
-const SystemMonitor = () => {
- const totalRam = useAtomValue(totalRamAtom)
- const usedRam = useAtomValue(usedRamAtom)
- const cpuUsage = useAtomValue(cpuUsageAtom)
- const gpus = useAtomValue(gpusAtom)
- const { onRevealInFinder } = usePath()
- const [showFullScreen, setShowFullScreen] = useState(false)
- const ramUtilitized = useAtomValue(ramUtilitizedAtom)
- const [showSystemMonitorPanel, setShowSystemMonitorPanel] = useAtom(
- showSystemMonitorPanelAtom
- )
-
- const reduceTransparent = useAtomValue(reduceTransparentAtom)
-
- const { watch, stopWatching } = useGetSystemResources()
-
- const toggleShowSystemMonitorPanel = useCallback(
- (isShow: boolean) => {
- setShowSystemMonitorPanel(isShow)
- if (isShow) {
- watch()
- } else {
- stopWatching()
- }
- },
- [setShowSystemMonitorPanel, stopWatching, watch]
- )
-
- return (
-
- {
- toggleShowSystemMonitorPanel(!showSystemMonitorPanel)
- setShowFullScreen(false)
- }}
- >
-
- System Monitor
-
- {showSystemMonitorPanel && (
-
-
-
- Running Models
-
-
-
onRevealInFinder('logs')}
- >
- App Log
-
- {showFullScreen ? (
-
setShowFullScreen(!showFullScreen)}
- />
- ) : (
- setShowFullScreen(!showFullScreen)}
- />
- )}
- {
- toggleShowSystemMonitorPanel(false)
- setShowFullScreen(false)
- }}
- />
-
-
-
-
-
-
-
-
-
-
-
Memory
-
- {toGigabytes(usedRam, { hideUnit: true })}GB /{' '}
- {toGigabytes(totalRam, { hideUnit: true })}GB
-
-
-
-
- {gpus.length > 0 && (
-
- {gpus
- .filter((gpu) => gpu.activated === true)
- .map((gpu, index) => {
- const gpuUtilization = utilizedMemory(
- gpu.free_vram,
- gpu.total_vram
- )
- return (
-
-
-
- {gpu.name}
-
-
-
-
- {gpu.total_vram - gpu.free_vram}/
- {gpu.total_vram}
-
- MB
-
-
-
-
-
-
-
- {gpuUtilization}%
-
-
-
- )
- })}
-
- )}
-
-
-
- )}
-
- )
-}
-
-export default SystemMonitor
diff --git a/web/containers/Layout/BottomPanel/UpdateApp/index.tsx b/web/containers/Layout/BottomPanel/UpdateApp/index.tsx
deleted file mode 100644
index e9c22f3ac..000000000
--- a/web/containers/Layout/BottomPanel/UpdateApp/index.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import Image from 'next/image'
-
-import { Progress } from '@janhq/joi'
-
-type Props = {
- total: number
- used: number
-}
-
-const UpdateApp = ({ used, total }: Props) => (
-
-
-
- Updating App
-
-
-
- {((used / total) * 100).toFixed(0)}%
-
-
-)
-
-export default UpdateApp
diff --git a/web/containers/Layout/BottomPanel/UpdateFailedModal/index.tsx b/web/containers/Layout/BottomPanel/UpdateFailedModal/index.tsx
deleted file mode 100644
index 3591b4c6a..000000000
--- a/web/containers/Layout/BottomPanel/UpdateFailedModal/index.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-import React from 'react'
-
-import { Modal, ModalClose, Button } from '@janhq/joi'
-import { Share2Icon } from '@radix-ui/react-icons'
-import { useAtom } from 'jotai'
-
-import { updateVersionErrorAtom } from '@/helpers/atoms/App.atom'
-
-const UpdatedFailedModal = () => {
- const [error, setError] = useAtom(updateVersionErrorAtom)
-
- return (
- setError(undefined)}
- title="Unable to Install Update"
- content={
-
-
- An error occurred while installing Jan{' '}
- {error} . We appreciate your
- help with{' '}
-
- manual downloading and installation.
-
-
-
- setError(undefined)}>
- Remind me later
-
- {
- window.open(
- 'https://github.com/menloresearch/jan#download',
- '_blank'
- )
- setError(undefined)
- }}
- >
-
- Download now
-
-
-
-
-
- }
- />
- )
-}
-
-export default UpdatedFailedModal
diff --git a/web/containers/Layout/BottomPanel/index.tsx b/web/containers/Layout/BottomPanel/index.tsx
deleted file mode 100644
index 4dd4a742c..000000000
--- a/web/containers/Layout/BottomPanel/index.tsx
+++ /dev/null
@@ -1,87 +0,0 @@
-import { Button, Tooltip } from '@janhq/joi'
-import { useAtomValue } from 'jotai'
-
-import { FaGithub, FaDiscord } from 'react-icons/fa'
-import { twMerge } from 'tailwind-merge'
-
-import DownloadingState from './DownloadingState'
-
-import ImportingModelState from './ImportingModelState'
-import SystemMonitor from './SystemMonitor'
-import UpdateApp from './UpdateApp'
-import UpdatedFailedModal from './UpdateFailedModal'
-
-import { appDownloadProgressAtom } from '@/helpers/atoms/App.atom'
-import { reduceTransparentAtom } from '@/helpers/atoms/Setting.atom'
-
-const menuLinks = [
- {
- name: 'Discord',
- icon: ,
- link: 'https://discord.gg/FTk2MvZwJH',
- },
- {
- name: 'Github',
- icon: ,
- link: 'https://github.com/menloresearch/jan',
- },
-]
-
-const BottomPanel = () => {
- const progress = useAtomValue(appDownloadProgressAtom)
- const reduceTransparent = useAtomValue(reduceTransparentAtom)
-
- return (
-
-
-
- {progress && progress > 0 ? (
-
- ) : null}
-
-
-
-
-
-
-
-
- Jan v{VERSION ?? ''}
-
-
- {menuLinks
- .filter((link) => !!link)
- .map((link, i) => (
-
- ))}
-
-
-
- )
-}
-
-export default BottomPanel
diff --git a/web/containers/Layout/RibbonPanel/index.tsx b/web/containers/Layout/RibbonPanel/index.tsx
deleted file mode 100644
index a82fbbe99..000000000
--- a/web/containers/Layout/RibbonPanel/index.tsx
+++ /dev/null
@@ -1,141 +0,0 @@
-import { useEffect } from 'react'
-
-import { Tooltip, useMediaQuery } from '@janhq/joi'
-
-import { useAtom, useAtomValue, useSetAtom } from 'jotai'
-import {
- MessageCircleIcon,
- SettingsIcon,
- LayoutGridIcon,
- SquareCodeIcon,
-} from 'lucide-react'
-
-import { twMerge } from 'tailwind-merge'
-
-import { MainViewState } from '@/constants/screens'
-
-import { mainViewStateAtom, showLeftPanelAtom } from '@/helpers/atoms/App.atom'
-import { editMessageAtom } from '@/helpers/atoms/ChatMessage.atom'
-import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom'
-
-import { isDownloadALocalModelAtom } from '@/helpers/atoms/Model.atom'
-import {
- reduceTransparentAtom,
- selectedSettingAtom,
-} from '@/helpers/atoms/Setting.atom'
-import { threadsAtom } from '@/helpers/atoms/Thread.atom'
-
-export default function RibbonPanel() {
- const [mainViewState, setMainViewState] = useAtom(mainViewStateAtom)
- const [serverEnabled] = useAtom(serverEnabledAtom)
- const setEditMessage = useSetAtom(editMessageAtom)
- const [showLeftPanel, setShowLeftPanel] = useAtom(showLeftPanelAtom)
- const matches = useMediaQuery('(max-width: 880px)')
- const reduceTransparent = useAtomValue(reduceTransparentAtom)
- const setSelectedSetting = useSetAtom(selectedSettingAtom)
-
- const threads = useAtomValue(threadsAtom)
- const isDownloadALocalModel = useAtomValue(isDownloadALocalModelAtom)
-
- useEffect(() => {
- if (mainViewState === MainViewState.Settings) {
- setShowLeftPanel(true)
- }
- return () => setShowLeftPanel(showLeftPanel)
- }, [mainViewState])
-
- const onMenuClick = (state: MainViewState) => {
- if (mainViewState === state) return
- if (serverEnabled && state === MainViewState.Thread) return
- if (state === MainViewState.Settings) setSelectedSetting('My Models')
- setMainViewState(state)
- setEditMessage('')
- }
-
- const RibbonNavMenus = [
- {
- name: 'Thread',
- icon: (
-
- ),
- state: MainViewState.Thread,
- },
- {
- name: 'Hub',
- icon: ,
- state: MainViewState.Hub,
- },
- {
- name: 'Local API Server',
- icon: ,
- state: MainViewState.LocalServer,
- },
- {
- name: 'Settings',
- icon: ,
- state: MainViewState.Settings,
- },
- ]
-
- return (
-
- {RibbonNavMenus.filter((menu) => !!menu).map((menu, i) => {
- const isActive = mainViewState === menu.state
- return (
-
onMenuClick(menu.state)}
- >
-
-
- {menu.icon}
-
-
- }
- content={
- serverEnabled && menu.state === MainViewState.Thread
- ? 'Threads are disabled while the server is running'
- : menu.name
- }
- />
-
- )
- })}
-
- )
-}
diff --git a/web/containers/Layout/TopPanel/index.tsx b/web/containers/Layout/TopPanel/index.tsx
deleted file mode 100644
index 386c315de..000000000
--- a/web/containers/Layout/TopPanel/index.tsx
+++ /dev/null
@@ -1,195 +0,0 @@
-import { Fragment } from 'react'
-
-import { Button, Tooltip } from '@janhq/joi'
-import { useAtom, useAtomValue, useSetAtom } from 'jotai'
-import {
- PanelLeftCloseIcon,
- PanelRightCloseIcon,
- MinusIcon,
- MenuIcon,
- SquareIcon,
- PaletteIcon,
- XIcon,
- PenSquareIcon,
- Settings2,
- History,
- PanelLeftOpenIcon,
-} from 'lucide-react'
-import { twMerge } from 'tailwind-merge'
-
-import LogoMark from '@/containers/Brand/Logo/Mark'
-
-import { toaster } from '@/containers/Toast'
-
-import { MainViewState } from '@/constants/screens'
-
-import { useCreateNewThread } from '@/hooks/useCreateNewThread'
-import { useStarterScreen } from '@/hooks/useStarterScreen'
-
-import {
- mainViewStateAtom,
- showLeftPanelAtom,
- showRightPanelAtom,
-} from '@/helpers/atoms/App.atom'
-import { assistantsAtom } from '@/helpers/atoms/Assistant.atom'
-import {
- reduceTransparentAtom,
- selectedSettingAtom,
-} from '@/helpers/atoms/Setting.atom'
-import { activeTabThreadRightPanelAtom } from '@/helpers/atoms/ThreadRightPanel.atom'
-
-const TopPanel = () => {
- const [showLeftPanel, setShowLeftPanel] = useAtom(showLeftPanelAtom)
- const [showRightPanel, setShowRightPanel] = useAtom(showRightPanelAtom)
- const [mainViewState, setMainViewState] = useAtom(mainViewStateAtom)
- const setSelectedSetting = useSetAtom(selectedSettingAtom)
- const reduceTransparent = useAtomValue(reduceTransparentAtom)
- const { requestCreateNewThread } = useCreateNewThread()
- const assistants = useAtomValue(assistantsAtom)
- const [activeTabThreadRightPanel, setActiveTabThreadRightPanel] = useAtom(
- activeTabThreadRightPanelAtom
- )
-
- const onCreateNewThreadClick = () => {
- if (!assistants.length)
- return toaster({
- title: 'No assistant available.',
- description: `Could not create a new thread. Please add an assistant.`,
- type: 'error',
- })
- requestCreateNewThread(assistants[0])
- }
-
- const { isShowStarterScreen } = useStarterScreen()
-
- return (
-
- {!isMac &&
}
-
-
- {!isMac && (
-
{
- window?.electronAPI?.showOpenMenu(100, 100)
- }}
- >
-
-
- )}
- {mainViewState !== MainViewState.Hub && (
-
- {showLeftPanel ? (
- setShowLeftPanel(false)}>
-
-
- ) : (
- setShowLeftPanel(true)}>
- {mainViewState === MainViewState.Thread ? (
- }
- content="Threads History"
- />
- ) : (
-
- )}
-
- )}
-
- )}
- {mainViewState === MainViewState.Thread && !isShowStarterScreen && (
-
-
-
- )}
-
-
- {mainViewState !== MainViewState.Hub &&
- mainViewState !== MainViewState.Settings && (
-
- {showRightPanel ? (
- {
- setShowRightPanel(false)
- if (activeTabThreadRightPanel === 'model') {
- setActiveTabThreadRightPanel(undefined)
- }
- }}
- >
-
-
- ) : (
- {
- setShowRightPanel(true)
- if (activeTabThreadRightPanel === undefined) {
- setActiveTabThreadRightPanel('model')
- }
- }}
- >
- }
- content="Thread Settings"
- />
-
- )}
-
- )}
-
{
- setMainViewState(MainViewState.Settings)
- setSelectedSetting('Preferences')
- }}
- >
-
-
-
- {!isMac && (
-
- window?.electronAPI?.setMinimizeApp()}
- >
-
-
- window?.electronAPI?.setMaximizeApp()}
- >
-
-
- window?.electronAPI?.setCloseApp()}
- >
-
-
-
- )}
-
-
-
- )
-}
-export default TopPanel
diff --git a/web/containers/Layout/index.tsx b/web/containers/Layout/index.tsx
deleted file mode 100644
index db675d5eb..000000000
--- a/web/containers/Layout/index.tsx
+++ /dev/null
@@ -1,249 +0,0 @@
-'use client'
-
-import { useEffect, useState } from 'react'
-
-import { Button } from '@janhq/joi'
-import { useAtom, useAtomValue, useSetAtom } from 'jotai'
-
-import posthog from 'posthog-js'
-import { twMerge } from 'tailwind-merge'
-
-import BottomPanel from '@/containers/Layout/BottomPanel'
-import RibbonPanel from '@/containers/Layout/RibbonPanel'
-
-import TopPanel from '@/containers/Layout/TopPanel'
-
-import { MainViewState } from '@/constants/screens'
-
-import { getImportModelStageAtom } from '@/hooks/useImportModel'
-
-import { SUCCESS_SET_NEW_DESTINATION } from '@/screens/Settings/Advanced/DataFolder'
-import CancelModelImportModal from '@/screens/Settings/CancelModelImportModal'
-import ChooseWhatToImportModal from '@/screens/Settings/ChooseWhatToImportModal'
-import EditModelInfoModal from '@/screens/Settings/EditModelInfoModal'
-import ImportModelOptionModal from '@/screens/Settings/ImportModelOptionModal'
-import ImportingModelModal from '@/screens/Settings/ImportingModelModal'
-import SelectingModelModal from '@/screens/Settings/SelectingModelModal'
-
-import { getAppDistinctId, updateDistinctId } from '@/utils/settings'
-
-import LoadingModal from '../LoadingModal'
-
-import MainViewContainer from '../MainViewContainer'
-
-import ModalAppUpdaterChangelog from '../ModalAppUpdaterChangelog'
-
-import ModalAppUpdaterNotAvailable from '../ModalAppUpdaterNotAvailable'
-
-import { mainViewStateAtom } from '@/helpers/atoms/App.atom'
-import {
- productAnalyticAtom,
- productAnalyticPromptAtom,
- reduceTransparentAtom,
- showScrollBarAtom,
-} from '@/helpers/atoms/Setting.atom'
-
-const BaseLayout = () => {
- const setMainViewState = useSetAtom(mainViewStateAtom)
- const importModelStage = useAtomValue(getImportModelStageAtom)
- const reduceTransparent = useAtomValue(reduceTransparentAtom)
- const [productAnalytic, setProductAnalytic] = useAtom(productAnalyticAtom)
- const [productAnalyticPrompt, setProductAnalyticPrompt] = useAtom(
- productAnalyticPromptAtom
- )
- const showScrollBar = useAtomValue(showScrollBarAtom)
- const [showProductAnalyticPrompt, setShowProductAnalyticPrompt] =
- useState(false)
-
- useEffect(() => {
- const timer = setTimeout(() => {
- if (productAnalyticPrompt) {
- setShowProductAnalyticPrompt(true)
- }
- return () => clearTimeout(timer)
- }, 3000) // 3 seconds delay
-
- return () => clearTimeout(timer) // Cleanup timer on unmount
- }, [productAnalyticPrompt])
-
- useEffect(() => {
- if (productAnalytic) {
- posthog.init(POSTHOG_KEY, {
- api_host: POSTHOG_HOST,
- autocapture: false,
- capture_pageview: false,
- capture_pageleave: false,
- disable_session_recording: true,
- person_profiles: 'always',
- persistence: 'localStorage',
- opt_out_capturing_by_default: true,
- // eslint-disable-next-line @typescript-eslint/naming-convention
- sanitize_properties: function (properties) {
- const denylist = [
- '$pathname',
- '$initial_pathname',
- '$current_url',
- '$initial_current_url',
- '$host',
- '$initial_host',
- '$initial_person_info',
- ]
-
- denylist.forEach((key) => {
- if (properties[key]) {
- properties[key] = null // Set each denied property to null
- }
- })
-
- return properties
- },
- })
- // Attempt to restore distinct Id from app global settings
- getAppDistinctId()
- .then((id) => {
- if (id) posthog.identify(id)
- })
- .finally(() => {
- posthog.opt_in_capturing()
- posthog.register({ app_version: VERSION })
- updateDistinctId(posthog.get_distinct_id())
- })
- } else {
- posthog.opt_out_capturing()
- }
- }, [productAnalytic])
-
- useEffect(() => {
- if (localStorage.getItem(SUCCESS_SET_NEW_DESTINATION) === 'true') {
- setMainViewState(MainViewState.Settings)
- }
- }, [setMainViewState])
-
- useEffect(() => {
- window.electronAPI?.onMainViewStateChange(
- (_event: string, route: string) => {
- if (route === 'Settings') {
- setMainViewState(MainViewState.Settings)
- }
- }
- )
- }, [setMainViewState])
-
- const handleProductAnalytics = (isAllowed: boolean) => {
- setProductAnalytic(isAllowed)
- setProductAnalyticPrompt(false)
- setShowProductAnalyticPrompt(false)
- if (isAllowed) {
- posthog.opt_in_capturing()
- } else {
- posthog.opt_out_capturing()
- }
- }
-
- return (
-
-
-
-
-
-
- {importModelStage === 'SELECTING_MODEL' &&
}
- {importModelStage === 'MODEL_SELECTED' &&
}
- {importModelStage === 'IMPORTING_MODEL' &&
}
- {importModelStage === 'EDIT_MODEL_INFO' &&
}
- {importModelStage === 'CONFIRM_CANCEL' &&
}
-
- {showProductAnalyticPrompt && (
-
-
-
-
-
-
-
-
-
-
-
-
-
Help Us Improve Jan
-
-
- To improve Jan, we collect anonymous data to understand feature
- usage. Your chats and personal information are never tracked. You
- can change this anytime in
- {`Settings > Privacy.`}
-
-
- Would you like to help us to improve Jan?
-
-
- {
- handleProductAnalytics(true)
- }}
- >
- Allow
-
- {
- handleProductAnalytics(false)
- }}
- >
- Deny
-
-
-
- )}
-
-
-
-
-
- )
-}
-
-export default BaseLayout
diff --git a/web/containers/LeftPanelContainer/index.tsx b/web/containers/LeftPanelContainer/index.tsx
deleted file mode 100644
index ac4b8893f..000000000
--- a/web/containers/LeftPanelContainer/index.tsx
+++ /dev/null
@@ -1,130 +0,0 @@
-import {
- Fragment,
- PropsWithChildren,
- useCallback,
- useEffect,
- useState,
-} from 'react'
-
-import { ScrollArea, useClickOutside, useMediaQuery } from '@janhq/joi'
-import { atom, useAtom, useAtomValue } from 'jotai'
-
-import { twMerge } from 'tailwind-merge'
-
-import { showLeftPanelAtom } from '@/helpers/atoms/App.atom'
-import {
- reduceTransparentAtom,
- showScrollBarAtom,
-} from '@/helpers/atoms/Setting.atom'
-
-type Props = PropsWithChildren
-
-const DEFAULT_LEFT_PANEL_WIDTH = 200
-export const LEFT_PANEL_WIDTH = 'leftPanelWidth'
-export const leftPanelWidthAtom = atom(DEFAULT_LEFT_PANEL_WIDTH)
-
-const LeftPanelContainer = ({ children }: Props) => {
- const [leftPanelRef, setLeftPanelRef] = useState(null)
- const [isResizing, setIsResizing] = useState(false)
- const [leftPanelWidth, setLeftPanelWidth] = useAtom(leftPanelWidthAtom)
- const [showLeftPanel, setShowLeftPanel] = useAtom(showLeftPanelAtom)
- const matches = useMediaQuery('(max-width: 880px)')
- const reduceTransparent = useAtomValue(reduceTransparentAtom)
- const showScrollBar = useAtomValue(showScrollBarAtom)
-
- useClickOutside(
- () => matches && showLeftPanel && setShowLeftPanel(false),
- null,
- [leftPanelRef]
- )
-
- const startResizing = useCallback(() => {
- setIsResizing(true)
- document.body.classList.add('select-none')
- }, [])
-
- const stopResizing = useCallback(() => {
- setIsResizing(false)
- document.body.classList.remove('select-none')
- }, [])
-
- const resize = useCallback(
- (mouseMoveEvent: { clientX: number }) => {
- if (isResizing) {
- if (leftPanelRef !== null) {
- if (
- mouseMoveEvent.clientX -
- leftPanelRef?.getBoundingClientRect().left <
- 170
- ) {
- setIsResizing(false)
- setLeftPanelWidth(DEFAULT_LEFT_PANEL_WIDTH)
- localStorage.setItem(
- LEFT_PANEL_WIDTH,
- String(DEFAULT_LEFT_PANEL_WIDTH)
- )
- setShowLeftPanel(false)
- } else {
- const resized =
- mouseMoveEvent.clientX -
- leftPanelRef?.getBoundingClientRect().left
- localStorage.setItem(LEFT_PANEL_WIDTH, String(resized))
- setLeftPanelWidth(resized)
- }
- }
- }
- },
- [isResizing, leftPanelRef, setLeftPanelWidth, setShowLeftPanel]
- )
-
- useEffect(() => {
- if (localStorage.getItem(LEFT_PANEL_WIDTH) === null) {
- setLeftPanelWidth(DEFAULT_LEFT_PANEL_WIDTH)
- localStorage.setItem(LEFT_PANEL_WIDTH, String(DEFAULT_LEFT_PANEL_WIDTH))
- }
- window.addEventListener('mousemove', resize)
- window.addEventListener('mouseup', stopResizing)
- return () => {
- window.removeEventListener('mousemove', resize)
- window.removeEventListener('mouseup', stopResizing)
- }
- }, [resize, setLeftPanelWidth, stopResizing])
-
- return (
- isResizing && e.stopPropagation()}
- >
-
- {children}
- {showLeftPanel && !matches && (
-
-
-
- )}
-
-
- )
-}
-
-export default LeftPanelContainer
diff --git a/web/containers/ListContainer/index.test.tsx b/web/containers/ListContainer/index.test.tsx
deleted file mode 100644
index 866d8ff4e..000000000
--- a/web/containers/ListContainer/index.test.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-// ListContainer.test.tsx
-import React from 'react'
-import { render, screen, fireEvent } from '@testing-library/react'
-import '@testing-library/jest-dom'
-import ListContainer from './index'
-
-class ResizeObserverMock {
- observe() {}
- unobserve() {}
- disconnect() {}
-}
-
-global.ResizeObserver = ResizeObserverMock
-
-describe('ListContainer', () => {
- const scrollToMock = jest.fn()
- Element.prototype.scrollTo = scrollToMock
-
- it('renders children correctly', () => {
- render(
-
- Test Child
-
- )
- expect(screen.getByTestId('child')).toBeInTheDocument()
- })
-
- it('scrolls to bottom on initial render', () => {
-
- render(
-
- Long content
-
- )
-
- expect(scrollToMock).toHaveBeenCalledWith({
- top: expect.any(Number),
- behavior: 'instant',
- })
- })
-
- it('sets isUserManuallyScrollingUp when scrolling up', () => {
- const { container } = render(
-
- Long content
-
- )
-
- const scrollArea = container.firstChild as HTMLElement
-
- // Simulate scrolling down
- fireEvent.scroll(scrollArea, { target: { scrollTop: 500 } })
-
- // Simulate scrolling up
- fireEvent.scroll(scrollArea, { target: { scrollTop: 300 } })
-
- // We can't directly test the internal state, but we can check that
- // subsequent scroll to bottom doesn't happen (as it would if isUserManuallyScrollingUp was false)
-
- // Trigger a re-render
- render(
-
- Long content
-
- )
-
- expect(scrollToMock).toHaveBeenCalled()
- })
-})
diff --git a/web/containers/ListContainer/index.tsx b/web/containers/ListContainer/index.tsx
deleted file mode 100644
index 3184c171b..000000000
--- a/web/containers/ListContainer/index.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-import { PropsWithChildren, useCallback, useEffect, useRef } from 'react'
-
-import { ScrollArea } from '@janhq/joi'
-
-import { useAtomValue } from 'jotai'
-
-import { showScrollBarAtom } from '@/helpers/atoms/Setting.atom'
-import { activeThreadAtom } from '@/helpers/atoms/Thread.atom'
-
-const ListContainer = ({ children }: PropsWithChildren) => {
- const listRef = useRef(null)
- const prevScrollTop = useRef(0)
- const isUserManuallyScrollingUp = useRef(false)
- const activeThread = useAtomValue(activeThreadAtom)
- const prevActiveThread = useRef(activeThread)
- const showScrollBar = useAtomValue(showScrollBarAtom)
-
- // Handle active thread changes
- useEffect(() => {
- if (prevActiveThread.current?.id !== activeThread?.id) {
- isUserManuallyScrollingUp.current = false
- const scrollHeight = listRef.current?.scrollHeight ?? 0
- listRef.current?.scrollTo({
- top: scrollHeight,
- behavior: 'instant',
- })
- prevActiveThread.current = activeThread // Update the previous active thread reference
- }
- }, [activeThread])
-
- const handleScroll = useCallback((event: React.UIEvent) => {
- const currentScrollTop = event.currentTarget.scrollTop
-
- if (prevScrollTop.current > currentScrollTop) {
- isUserManuallyScrollingUp.current = true
- } else {
- const currentScrollTop = event.currentTarget.scrollTop
- const scrollHeight = event.currentTarget.scrollHeight
- const clientHeight = event.currentTarget.clientHeight
-
- if (currentScrollTop + clientHeight >= scrollHeight) {
- isUserManuallyScrollingUp.current = false
- }
- }
-
- if (isUserManuallyScrollingUp.current === true) {
- event.preventDefault()
- event.stopPropagation()
- }
- prevScrollTop.current = currentScrollTop
- }, [])
-
- useEffect(() => {
- if (isUserManuallyScrollingUp.current === true || !listRef.current) return
- const scrollHeight = listRef.current?.scrollHeight ?? 0
- listRef.current?.scrollTo({
- top: scrollHeight,
- behavior: 'instant',
- })
- }, [listRef.current?.scrollHeight, isUserManuallyScrollingUp])
-
- return (
-
- {children}
-
- )
-}
-
-export default ListContainer
diff --git a/web/containers/Loader/GenerateResponse.tsx b/web/containers/Loader/GenerateResponse.tsx
deleted file mode 100644
index d43c8cab9..000000000
--- a/web/containers/Loader/GenerateResponse.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import React, { useEffect, useState } from 'react'
-
-import { motion as m } from 'framer-motion'
-
-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/Loader/Loader.test.tsx b/web/containers/Loader/Loader.test.tsx
deleted file mode 100644
index 007d0eeba..000000000
--- a/web/containers/Loader/Loader.test.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-// Loader.test.tsx
-import '@testing-library/jest-dom';
-import React from 'react'
-import { render, screen } from '@testing-library/react'
-import Loader from './index'
-
-describe('Loader Component', () => {
- it('renders without crashing', () => {
- render( )
- })
-
- it('displays the correct description', () => {
- const descriptionText = 'Loading...'
- render( )
- expect(screen.getByText(descriptionText)).toBeInTheDocument()
- })
-
- it('renders the correct number of loader elements', () => {
- const { container } = render( )
- const loaderElements = container.querySelectorAll('label')
- expect(loaderElements).toHaveLength(6)
- })
-})
diff --git a/web/containers/Loader/ModelReload.test.tsx b/web/containers/Loader/ModelReload.test.tsx
deleted file mode 100644
index 2de2db4fd..000000000
--- a/web/containers/Loader/ModelReload.test.tsx
+++ /dev/null
@@ -1,124 +0,0 @@
-// ModelReload.test.tsx
-import React from 'react'
-import '@testing-library/jest-dom'
-import { render, screen, act } from '@testing-library/react'
-import ModelReload from './ModelReload'
-import { useActiveModel } from '@/hooks/useActiveModel'
-
-jest.mock('@/hooks/useActiveModel')
-
-describe('ModelReload Component', () => {
- beforeEach(() => {
- jest.useFakeTimers()
- })
-
- afterEach(() => {
- jest.useRealTimers()
- })
-
- it('renders nothing when not loading', () => {
- ;(useActiveModel as jest.Mock).mockReturnValue({
- stateModel: { loading: false },
- })
-
- const { container } = render( )
- expect(container).toBeEmptyDOMElement()
- })
-
- it('renders loading message when loading', () => {
- ;(useActiveModel as jest.Mock).mockReturnValue({
- stateModel: { loading: true, model: { id: 'test-model' } },
- })
-
- render( )
- expect(screen.getByText(/Reloading model test-model/)).toBeInTheDocument()
- })
-
- it('updates loader percentage over time', () => {
- ;(useActiveModel as jest.Mock).mockReturnValue({
- stateModel: { loading: true, model: { id: 'test-model' } },
- })
-
- render( )
-
- // Initial render
- expect(screen.getByText(/Reloading model test-model/)).toBeInTheDocument()
- const loaderElement = screen.getByText(
- /Reloading model test-model/
- ).parentElement
-
- // Check initial width
- expect(loaderElement?.firstChild).toHaveStyle('width: 50%')
-
- // Advance timers and check updated width
- act(() => {
- jest.advanceTimersByTime(250)
- })
- expect(loaderElement?.firstChild).toHaveStyle('width: 78%')
-
- // Advance to 99%
- for (let i = 0; i < 27; i++) {
- act(() => {
- jest.advanceTimersByTime(250)
- })
- }
- expect(loaderElement?.firstChild).toHaveStyle('width: 99%')
-
- // Advance one more time to hit the 250ms delay
- act(() => {
- jest.advanceTimersByTime(250)
- })
- expect(loaderElement?.firstChild).toHaveStyle('width: 99%')
- })
-
- it('stops at 99%', () => {
- ;(useActiveModel as jest.Mock).mockReturnValue({
- stateModel: { loading: true, model: { id: 'test-model' } },
- })
-
- render( )
-
- const loaderElement = screen.getByText(
- /Reloading model test-model/
- ).parentElement
-
- // Advance to 99%
- for (let i = 0; i < 50; i++) {
- act(() => {
- jest.advanceTimersByTime(250)
- })
- }
- expect(loaderElement?.firstChild).toHaveStyle('width: 99%')
-
- // Advance more and check it stays at 99%
- act(() => {
- jest.advanceTimersByTime(1000)
- })
- expect(loaderElement?.firstChild).toHaveStyle('width: 99%')
- })
-
- it('resets to 0% when loading completes', () => {
- const { rerender } = render( )
- ;(useActiveModel as jest.Mock).mockReturnValue({
- stateModel: { loading: true, model: { id: 'test-model' } },
- })
-
- rerender( )
-
- const loaderElement = screen.getByText(
- /Reloading model test-model/
- ).parentElement
-
- expect(loaderElement?.firstChild).toHaveStyle('width: 50%')
- // Set loading to false
- ;(useActiveModel as jest.Mock).mockReturnValue({
- stateModel: { loading: false },
- })
-
- rerender( )
-
- expect(
- screen.queryByText(/Reloading model test-model/)
- ).not.toBeInTheDocument()
- })
-})
diff --git a/web/containers/Loader/ModelReload.tsx b/web/containers/Loader/ModelReload.tsx
deleted file mode 100644
index fbe673788..000000000
--- a/web/containers/Loader/ModelReload.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import React, { useEffect, useState } from 'react'
-
-import { useActiveModel } from '@/hooks/useActiveModel'
-
-export default function ModelReload() {
- const { stateModel } = useActiveModel()
- const [loader, setLoader] = useState(50)
-
- // This is fake loader please fix this when we have realtime percentage when load model
- useEffect(() => {
- if (stateModel.loading) {
- 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 === 99) {
- setLoader(99)
- } else {
- setLoader(loader + 1)
- }
- } else {
- setLoader(0)
- }
- }, [stateModel.loading, loader])
-
- if (!stateModel.loading) return null
-
- return (
-
-
-
-
- Reloading model {stateModel.model?.id}
-
-
-
- )
-}
diff --git a/web/containers/Loader/ModelStart.test.tsx b/web/containers/Loader/ModelStart.test.tsx
deleted file mode 100644
index 62e333da0..000000000
--- a/web/containers/Loader/ModelStart.test.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import '@testing-library/jest-dom'
-import { render, screen, act } from '@testing-library/react'
-import ModelStart from './ModelStart' // Adjust the path based on your file structure
-import { useActiveModel } from '@/hooks/useActiveModel'
-
-// Mock the useActiveModel hook
-jest.mock('@/hooks/useActiveModel', () => ({
- useActiveModel: jest.fn(),
-}))
-
-describe('ModelStart', () => {
- const mockSetStateModel = jest.fn()
- const mockModel = { id: 'test-model' }
-
- beforeEach(() => {
- // Reset the mock implementation before each test
- jest.clearAllMocks()
- })
-
- it('renders correctly when loading is false', () => {
- ;(useActiveModel as jest.Mock).mockReturnValue({
- stateModel: {
- loading: false,
- state: 'start',
- model: mockModel,
- },
- })
-
- render( )
- // Ensure the component returns null when not loading
- expect(screen.queryByText(/Starting model/i)).toBeNull()
- })
-
- it('renders loading state with model id', () => {
- ;(useActiveModel as jest.Mock).mockReturnValue({
- stateModel: {
- loading: true,
- state: 'start',
- model: mockModel,
- },
- })
-
- render( )
- // Ensure the loading text is rendered
- expect(screen.getByText(/Starting model test-model/i)).toBeInTheDocument()
- })
-})
diff --git a/web/containers/Loader/ModelStart.tsx b/web/containers/Loader/ModelStart.tsx
deleted file mode 100644
index cad8afab0..000000000
--- a/web/containers/Loader/ModelStart.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-import React, { useEffect, useState } from 'react'
-
-import { motion as m } from 'framer-motion'
-
-import { useActiveModel } from '@/hooks/useActiveModel'
-
-export default function ModelStart() {
- const { stateModel } = useActiveModel()
- const [loader, setLoader] = useState(0)
-
- // This is fake loader please fix this when we have realtime percentage when load model
- useEffect(() => {
- if (stateModel.loading) {
- 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)
- }
- } else {
- setLoader(0)
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [stateModel.loading, loader])
-
- if (!stateModel.loading) return null
-
- return (
-
-
-
-
- {stateModel.state === 'start' ? 'Starting' : 'Stopping'}
- model
- {stateModel.model?.id}
-
-
-
- )
-}
diff --git a/web/containers/Loader/ProgressCircle.test.tsx b/web/containers/Loader/ProgressCircle.test.tsx
deleted file mode 100644
index 651f9a4f2..000000000
--- a/web/containers/Loader/ProgressCircle.test.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-// ProgressCircle.test.tsx
-import React from 'react'
-import { render, screen } from '@testing-library/react'
-import '@testing-library/jest-dom'
-import ProgressCircle from './ProgressCircle'
-
-describe('ProgressCircle Component', () => {
- test('renders ProgressCircle with default props', () => {
- render( )
- const svg = screen.getByRole('img', { hidden: true })
- expect(svg).toBeInTheDocument()
- expect(svg).toHaveAttribute('width', '100')
- expect(svg).toHaveAttribute('height', '100')
- })
-
- test('renders ProgressCircle with custom size', () => {
- render( )
- const svg = screen.getByRole('img', { hidden: true })
- expect(svg).toHaveAttribute('width', '200')
- expect(svg).toHaveAttribute('height', '200')
- })
-})
diff --git a/web/containers/Loader/ProgressCircle.tsx b/web/containers/Loader/ProgressCircle.tsx
deleted file mode 100644
index aec7c81cc..000000000
--- a/web/containers/Loader/ProgressCircle.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import React from 'react'
-
-interface ProgressCircleProps {
- percentage: number
- size?: number
- strokeWidth?: number
-}
-
-const ProgressCircle: React.FC = ({
- percentage,
- size = 100,
- strokeWidth = 14,
-}) => {
- const radius = (size - strokeWidth) / 2
- const circumference = 2 * Math.PI * radius
- const offset = circumference - (percentage / 100) * circumference
-
- return (
-
-
-
-
- )
-}
-
-export default ProgressCircle
diff --git a/web/containers/Loader/Spinner.tsx b/web/containers/Loader/Spinner.tsx
deleted file mode 100644
index aeeac155f..000000000
--- a/web/containers/Loader/Spinner.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import { motion } from 'framer-motion'
-
-const Spinner = ({ size = 40, strokeWidth = 4, className = '' }) => {
- const radius = size / 2 - strokeWidth
- const circumference = 2 * Math.PI * radius
-
- return (
-
- {/* Static background circle */}
-
- {/* Smooth animated arc */}
-
-
- )
-}
-
-export default Spinner
diff --git a/web/containers/Loader/index.tsx b/web/containers/Loader/index.tsx
deleted file mode 100644
index babbf8c2f..000000000
--- a/web/containers/Loader/index.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-type Props = {
- description: string
-}
-export default function Loader({ description }: Props) {
- return (
-
-
-
-
- {[...Array(6).keys()].map((i) => {
- return (
-
- )
- })}
-
-
-
{description}
-
-
- )
-}
diff --git a/web/containers/LoadingModal/index.test.tsx b/web/containers/LoadingModal/index.test.tsx
deleted file mode 100644
index f5b17234d..000000000
--- a/web/containers/LoadingModal/index.test.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import '@testing-library/jest-dom'
-import { render } from '@testing-library/react'
-import { useAtomValue } from 'jotai'
-import ResettingModal from './index'
-
-// Mocking the Jotai atom
-jest.mock('jotai', () => {
- const originalModule = jest.requireActual('jotai')
-
- return {
- ...originalModule,
- useAtomValue: jest.fn(),
- }
-})
-
-describe('ResettingModal', () => {
- it('renders the modal with loading info when provided', () => {
- const mockLoadingInfo = {
- title: 'Loading...',
- message: 'Please wait while we process your request.',
- }
-
- // Mock the useAtomValue hook to return mock loading info
- ;(useAtomValue as jest.Mock).mockReturnValue(mockLoadingInfo)
-
- const { getByText } = render( )
-
- // Check if the modal title and message are displayed
- expect(getByText('Loading...')).toBeInTheDocument()
- expect(
- getByText('Please wait while we process your request.')
- ).toBeInTheDocument()
- })
-
- it('does not render the modal when loading info is undefined', () => {
- // Mock the useAtomValue hook to return undefined
- ;(useAtomValue as jest.Mock).mockReturnValue(undefined)
-
- const { queryByText } = render( )
-
- // Check that the modal does not appear
- expect(queryByText('Loading...')).not.toBeInTheDocument()
- expect(
- queryByText('Please wait while we process your request.')
- ).not.toBeInTheDocument()
- })
-})
diff --git a/web/containers/LoadingModal/index.tsx b/web/containers/LoadingModal/index.tsx
deleted file mode 100644
index fa0f75e36..000000000
--- a/web/containers/LoadingModal/index.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import { Modal } from '@janhq/joi'
-import { atom, useAtomValue } from 'jotai'
-
-export type LoadingInfo = {
- title: string
- message: string
-}
-
-export const loadingModalInfoAtom = atom(undefined)
-
-const ResettingModal: React.FC = () => {
- const loadingInfo = useAtomValue(loadingModalInfoAtom)
-
- return (
-
- {loadingInfo?.message}
-
- }
- />
- )
-}
-
-export default ResettingModal
diff --git a/web/containers/MainViewContainer/index.test.tsx b/web/containers/MainViewContainer/index.test.tsx
deleted file mode 100644
index bcafa92af..000000000
--- a/web/containers/MainViewContainer/index.test.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import '@testing-library/jest-dom'
-
-import { render } from '@testing-library/react'
-import { useAtomValue } from 'jotai'
-import MainViewContainer from './index'
-import { MainViewState } from '@/constants/screens'
-
-// Mocking the Jotai atom
-jest.mock('jotai', () => {
- const originalModule = jest.requireActual('jotai')
-
- return {
- ...originalModule,
- useAtomValue: jest.fn(),
- }
-})
-
-// Mocking the screen components
-jest.mock('@/screens/Hub', () => () => Hub Screen
)
-jest.mock('@/screens/LocalServer', () => () => Local Server Screen
)
-jest.mock('@/screens/Settings', () => () => Settings Screen
)
-jest.mock('@/screens/Thread', () => () => Thread Screen
)
-
-describe('MainViewContainer', () => {
- it('renders HubScreen when mainViewState is Hub', () => {
- ;(useAtomValue as jest.Mock).mockReturnValue(MainViewState.Hub)
-
- const { getByText } = render( )
-
- expect(getByText('Hub Screen')).toBeInTheDocument()
- })
-
- it('renders SettingsScreen when mainViewState is Settings', () => {
- ;(useAtomValue as jest.Mock).mockReturnValue(MainViewState.Settings)
-
- const { getByText } = render( )
-
- expect(getByText('Settings Screen')).toBeInTheDocument()
- })
-
- it('renders LocalServerScreen when mainViewState is LocalServer', () => {
- ;(useAtomValue as jest.Mock).mockReturnValue(MainViewState.LocalServer)
-
- const { getByText } = render( )
-
- expect(getByText('Local Server Screen')).toBeInTheDocument()
- })
-
- it('renders ThreadScreen when mainViewState is not defined', () => {
- ;(useAtomValue as jest.Mock).mockReturnValue(undefined)
-
- const { getByText } = render( )
-
- expect(getByText('Thread Screen')).toBeInTheDocument()
- })
-})
diff --git a/web/containers/MainViewContainer/index.tsx b/web/containers/MainViewContainer/index.tsx
deleted file mode 100644
index 6417ec63a..000000000
--- a/web/containers/MainViewContainer/index.tsx
+++ /dev/null
@@ -1,81 +0,0 @@
-import { memo, useEffect, useState } from 'react'
-
-import { motion as m } from 'framer-motion'
-import { useAtomValue } from 'jotai'
-
-import { twMerge } from 'tailwind-merge'
-
-import { MainViewState } from '@/constants/screens'
-
-import HubScreen from '@/screens/Hub'
-import LocalServerScreen from '@/screens/LocalServer'
-import SettingsScreen from '@/screens/Settings'
-import ThreadScreen from '@/screens/Thread'
-
-import {
- mainViewStateAtom,
- showSystemMonitorPanelAtom,
-} from '@/helpers/atoms/App.atom'
-
-const MainViewContainer = () => {
- const mainViewState = useAtomValue(mainViewStateAtom)
- const showSystemMonitorPanel = useAtomValue(showSystemMonitorPanelAtom)
- const [height, setHeight] = useState(0)
-
- useEffect(() => {
- if (showSystemMonitorPanel) {
- const element = document.querySelector('.system-monitor-panel')
-
- if (element) {
- setHeight(element.clientHeight) // You can also use offsetHeight if needed
- }
- } else {
- setHeight(0)
- }
- }, [showSystemMonitorPanel])
-
- let children = null
- switch (mainViewState) {
- case MainViewState.Hub:
- children =
- break
-
- case MainViewState.Settings:
- children =
- break
-
- case MainViewState.LocalServer:
- children =
- break
-
- default:
- children =
- break
- }
-
- return (
-
- )
-}
-
-export default memo(MainViewContainer)
diff --git a/web/containers/ModalAppUpdaterChangelog/index.tsx b/web/containers/ModalAppUpdaterChangelog/index.tsx
deleted file mode 100644
index fa519780c..000000000
--- a/web/containers/ModalAppUpdaterChangelog/index.tsx
+++ /dev/null
@@ -1,104 +0,0 @@
-import React, { useEffect, useRef, useState } from 'react'
-
-import { Button, Modal } from '@janhq/joi'
-
-import { check, Update } from '@tauri-apps/plugin-updater'
-import { useAtom } from 'jotai'
-
-import { useGetLatestRelease } from '@/hooks/useGetLatestRelease'
-
-import { MarkdownTextMessage } from '@/screens/Thread/ThreadCenterPanel/TextMessage/MarkdownTextMessage'
-
-import LogoMark from '../Brand/Logo/Mark'
-
-import { appUpdateAvailableAtom } from '@/helpers/atoms/App.atom'
-
-const ModalAppUpdaterChangelog = () => {
- const [appUpdateAvailable, setAppUpdateAvailable] = useAtom(
- appUpdateAvailableAtom
- )
- const updaterRef = useRef(null)
-
- const [open, setOpen] = useState(appUpdateAvailable)
-
- useEffect(() => {
- setOpen(appUpdateAvailable)
- }, [appUpdateAvailable])
-
- const beta = VERSION.includes('beta')
- const nightly = VERSION.includes('-')
-
- const checkForUpdate = async () => {
- const update = await check()
- if (update) {
- setAppUpdateAvailable(true)
- updaterRef.current = update
- }
- }
- useEffect(() => {
- checkForUpdate()
- }, [])
-
- const { release } = useGetLatestRelease(beta ? true : false)
-
- return (
-
-
-
-
App Update
-
- {!nightly && (
-
- Version {release?.name} is available and ready to install.
-
- )}
- >
- }
- open={open}
- onOpenChange={() => setOpen(!open)}
- content={
-
- {nightly ? (
-
- You are using a nightly build. This version is built from the
- latest development branch and may not have release notes.
-
- ) : (
- <>
-
-
-
- >
- )}
-
- {
- setOpen(false)
- setAppUpdateAvailable(false)
- }}
- >
- Later
-
- {
- await updaterRef.current?.downloadAndInstall((event) => {})
- setOpen(false)
- setAppUpdateAvailable(false)
- }}
- >
- Update Now
-
-
-
- }
- />
- )
-}
-
-export default ModalAppUpdaterChangelog
diff --git a/web/containers/ModalAppUpdaterNotAvailable/index.tsx b/web/containers/ModalAppUpdaterNotAvailable/index.tsx
deleted file mode 100644
index 5f2b25fda..000000000
--- a/web/containers/ModalAppUpdaterNotAvailable/index.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-import React, { useEffect, useState } from 'react'
-
-import { Button, Modal } from '@janhq/joi'
-
-import { useAtom } from 'jotai'
-
-import LogoMark from '../Brand/Logo/Mark'
-
-import { appUpdateNotAvailableAtom } from '@/helpers/atoms/App.atom'
-
-const ModalAppUpdaterNotAvailable = () => {
- const [appUpdateNotAvailable, setAppUpdateNotAvailable] = useAtom(
- appUpdateNotAvailableAtom
- )
-
- const [open, setOpen] = useState(appUpdateNotAvailable)
-
- useEffect(() => {
- setOpen(appUpdateNotAvailable)
- }, [appUpdateNotAvailable])
-
- return (
-
-
-
-
App Update
-
- >
- }
- open={open}
- onOpenChange={() => setOpen(!open)}
- content={
-
-
- You’re up to date! No new updates available
-
-
- {
- setOpen(false)
- setAppUpdateNotAvailable(false)
- }}
- >
- Check back later
-
-
-
- }
- />
- )
-}
-
-export default ModalAppUpdaterNotAvailable
diff --git a/web/containers/ModalCancelDownload/index.tsx b/web/containers/ModalCancelDownload/index.tsx
deleted file mode 100644
index 886eb3b68..000000000
--- a/web/containers/ModalCancelDownload/index.tsx
+++ /dev/null
@@ -1,89 +0,0 @@
-import { useCallback } from 'react'
-
-import { Modal, Button, Progress, ModalClose } from '@janhq/joi'
-
-import { useAtomValue, useSetAtom } from 'jotai'
-
-import useDownloadModel from '@/hooks/useDownloadModel'
-
-import {
- modelDownloadStateAtom,
- removeDownloadStateAtom,
-} from '@/hooks/useDownloadState'
-
-import { formatDownloadPercentage } from '@/utils/converter'
-
-type Props = {
- modelId: string
- isFromList?: boolean
-}
-
-const ModalCancelDownload = ({ modelId, isFromList }: Props) => {
- const { abortModelDownload } = useDownloadModel()
- const removeDownloadState = useSetAtom(removeDownloadStateAtom)
- const allDownloadStates = useAtomValue(modelDownloadStateAtom)
- const downloadState = allDownloadStates[modelId]
-
- const cancelText = `Cancel ${formatDownloadPercentage(downloadState?.percent ?? 0)}`
-
- const onAbortDownloadClick = useCallback(() => {
- removeDownloadState(modelId)
- abortModelDownload(downloadState?.modelId ?? modelId)
- }, [downloadState, abortModelDownload, removeDownloadState, modelId])
-
- return (
-
- {cancelText}
-
- ) : (
-
-
-
Cancel
-
-
- {formatDownloadPercentage(downloadState?.percent ?? 0)}
-
-
-
- )
- }
- content={
-
-
- Are you sure you want to cancel the download of
-
- {downloadState?.modelId}?
-
-
-
-
- No
-
-
-
- Yes
-
-
-
-
- }
- />
- )
-}
-
-export default ModalCancelDownload
diff --git a/web/containers/ModalTroubleShoot/AppLogs.test.tsx b/web/containers/ModalTroubleShoot/AppLogs.test.tsx
deleted file mode 100644
index 7b5c957ba..000000000
--- a/web/containers/ModalTroubleShoot/AppLogs.test.tsx
+++ /dev/null
@@ -1,105 +0,0 @@
-// AppLogs.test.tsx
-import '@testing-library/jest-dom'
-import React from 'react'
-import { render, screen, waitFor } from '@testing-library/react'
-import userEvent from '@testing-library/user-event'
-import AppLogs from './AppLogs'
-import { useLogs } from '@/hooks/useLogs'
-import { usePath } from '@/hooks/usePath'
-import { useClipboard } from '@/hooks/useClipboard'
-
-// Mock the hooks
-jest.mock('@/hooks/useLogs')
-jest.mock('@/hooks/usePath')
-jest.mock('@/hooks/useClipboard')
-
-describe('AppLogs Component', () => {
- const mockLogs = ['Log 1', 'Log 2', 'Log 3']
-
- beforeEach(() => {
- // Reset all mocks
- jest.resetAllMocks()
-
- // Setup default mock implementations
- ;(useLogs as jest.Mock).mockReturnValue({
- getLogs: jest.fn().mockResolvedValue(mockLogs.join('\n')),
- })
- ;(usePath as jest.Mock).mockReturnValue({
- onRevealInFinder: jest.fn(),
- })
- ;(useClipboard as jest.Mock).mockReturnValue({
- copy: jest.fn(),
- copied: false,
- })
- })
-
- test('renders AppLogs component with logs', async () => {
- render( )
-
- await waitFor(() => {
- mockLogs.forEach((log) => {
- expect(screen.getByText(log)).toBeInTheDocument()
- })
- })
-
- expect(screen.getByText('Open')).toBeInTheDocument()
- expect(screen.getByText('Copy All')).toBeInTheDocument()
- })
-
- test('renders empty state when no logs', async () => {
- ;(useLogs as jest.Mock).mockReturnValue({
- getLogs: jest.fn().mockResolvedValue(''),
- })
-
- render( )
-
- await waitFor(() => {
- expect(screen.getByText('Empty logs')).toBeInTheDocument()
- })
- })
-
- test('calls onRevealInFinder when Open button is clicked', async () => {
- const mockOnRevealInFinder = jest.fn()
- ;(usePath as jest.Mock).mockReturnValue({
- onRevealInFinder: mockOnRevealInFinder,
- })
-
- render( )
-
- await waitFor(() => {
- const openButton = screen.getByText('Open')
- userEvent.click(openButton)
-
- expect(mockOnRevealInFinder).toHaveBeenCalledWith('logs')
- })
- })
-
- test('calls copy function when Copy All button is clicked', async () => {
- const mockCopy = jest.fn()
- ;(useClipboard as jest.Mock).mockReturnValue({
- copy: mockCopy,
- copied: false,
- })
-
- render( )
-
- await waitFor(() => {
- const copyButton = screen.getByText('Copy All')
- userEvent.click(copyButton)
- expect(mockCopy).toHaveBeenCalled()
- })
- })
-
- test('shows Copying... text when copied is true', async () => {
- ;(useClipboard as jest.Mock).mockReturnValue({
- copy: jest.fn(),
- copied: true,
- })
-
- render( )
-
- await waitFor(() => {
- expect(screen.getByText('Copying...')).toBeInTheDocument()
- })
- })
-})
diff --git a/web/containers/ModalTroubleShoot/AppLogs.tsx b/web/containers/ModalTroubleShoot/AppLogs.tsx
deleted file mode 100644
index 0dedb540f..000000000
--- a/web/containers/ModalTroubleShoot/AppLogs.tsx
+++ /dev/null
@@ -1,226 +0,0 @@
-import React, { useEffect, useState, memo } from 'react'
-
-import { Button } from '@janhq/joi'
-
-import { CopyIcon, CheckIcon, FolderIcon } from 'lucide-react'
-
-import { twMerge } from 'tailwind-merge'
-
-import { useClipboard } from '@/hooks/useClipboard'
-import { useLogs } from '@/hooks/useLogs'
-import { usePath } from '@/hooks/usePath'
-
-const AppLogs = () => {
- const { getLogs } = useLogs()
- const [logs, setLogs] = useState([])
- const { onRevealInFinder } = usePath()
-
- useEffect(() => {
- getLogs('app').then((log) => {
- if (typeof log?.split === 'function') {
- if (log.length > 0) {
- setLogs(log.split(/\r?\n|\r|\n/g))
- }
- }
- })
-
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [])
-
- const clipboard = useClipboard({ timeout: 1000 })
-
- return (
-
-
-
-
onRevealInFinder('logs')}
- >
-
- <>
-
- Open
- >
-
-
-
{
- clipboard.copy(logs.slice(-50).join('\n') ?? '')
- }}
- >
-
- {clipboard.copied ? (
- <>
-
- Copying...
- >
- ) : (
- <>
-
- Copy All
- >
- )}
-
-
-
-
-
- {logs.length > 0 ? (
-
- {logs.slice(-100).map((log, i) => {
- return (
-
- {log}
-
- )
- })}
-
- ) : (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Empty logs
-
- )}
-
-
- )
-}
-
-export default memo(AppLogs)
diff --git a/web/containers/ModalTroubleShoot/CortexLogs.tsx b/web/containers/ModalTroubleShoot/CortexLogs.tsx
deleted file mode 100644
index 6205e82d0..000000000
--- a/web/containers/ModalTroubleShoot/CortexLogs.tsx
+++ /dev/null
@@ -1,226 +0,0 @@
-import React, { useEffect, useState, memo } from 'react'
-
-import { Button } from '@janhq/joi'
-
-import { CopyIcon, CheckIcon, FolderIcon } from 'lucide-react'
-
-import { twMerge } from 'tailwind-merge'
-
-import { useClipboard } from '@/hooks/useClipboard'
-import { useLogs } from '@/hooks/useLogs'
-import { usePath } from '@/hooks/usePath'
-
-const CortexLogs = () => {
- const { getLogs } = useLogs()
- const [logs, setLogs] = useState([])
- const { onRevealInFinder } = usePath()
-
- useEffect(() => {
- getLogs('cortex').then((log) => {
- if (typeof log?.split === 'function') {
- if (log.length > 0) {
- setLogs(log.split(/\r?\n|\r|\n/g))
- }
- }
- })
-
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [])
-
- const clipboard = useClipboard({ timeout: 1000 })
-
- return (
-
-
-
-
onRevealInFinder('logs')}
- >
-
- <>
-
- Open
- >
-
-
-
{
- clipboard.copy(logs.slice(-50).join('\n') ?? '')
- }}
- >
-
- {clipboard.copied ? (
- <>
-
- Copying...
- >
- ) : (
- <>
-
- Copy All
- >
- )}
-
-
-
-
-
- {logs.length > 0 ? (
-
- {logs.slice(-100).map((log, i) => {
- return (
-
- {log}
-
- )
- })}
-
- ) : (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Empty logs
-
- )}
-
-
- )
-}
-
-export default memo(CortexLogs)
diff --git a/web/containers/ModalTroubleShoot/DeviceSpecs.tsx b/web/containers/ModalTroubleShoot/DeviceSpecs.tsx
deleted file mode 100644
index 36a42816f..000000000
--- a/web/containers/ModalTroubleShoot/DeviceSpecs.tsx
+++ /dev/null
@@ -1,210 +0,0 @@
-import React, { memo, useEffect, useState } from 'react'
-
-import { Button } from '@janhq/joi'
-
-import { CopyIcon, CheckIcon } from 'lucide-react'
-
-import { twMerge } from 'tailwind-merge'
-
-import { useClipboard } from '@/hooks/useClipboard'
-import { useLogs } from '@/hooks/useLogs'
-
-const DeviceSpecs = () => {
- const { getLogs } = useLogs()
- const [logs, setLogs] = useState([])
-
- useEffect(() => {
- getLogs('app').then((log) => {
- if (typeof log?.split === 'function') {
- setLogs(
- log.split(/\r?\n|\r|\n/g).filter((e) => e.includes('[SPECS]::'))
- )
- }
- })
-
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [])
-
- const clipboard = useClipboard({ timeout: 1000 })
-
- return (
-
-
-
{
- clipboard.copy(logs.join('\n') ?? '')
- }}
- >
-
- {clipboard.copied ? (
- <>
-
- Copying...
- >
- ) : (
- <>
-
- Copy All
- >
- )}
-
-
-
-
- {logs.length > 0 ? (
-
- {logs.slice(-100).map((log, i) => {
- return (
-
- {log}
-
- )
- })}
-
- ) : (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Empty logs
-
- )}
-
-
- )
-}
-
-export default memo(DeviceSpecs)
diff --git a/web/containers/ModalTroubleShoot/index.tsx b/web/containers/ModalTroubleShoot/index.tsx
deleted file mode 100644
index a069362da..000000000
--- a/web/containers/ModalTroubleShoot/index.tsx
+++ /dev/null
@@ -1,169 +0,0 @@
-import { useState } from 'react'
-
-import { Button, Modal } from '@janhq/joi'
-import { atom, useAtom } from 'jotai'
-import { Maximize2 } from 'lucide-react'
-import { twMerge } from 'tailwind-merge'
-
-import ServerLogs from '@/containers/ServerLogs'
-
-import AppLogs from './AppLogs'
-import CortexLogs from './CortexLogs'
-import DeviceSpecs from './DeviceSpecs'
-
-export const modalTroubleShootingAtom = atom(false)
-const logOption = ['App Logs', 'Cortex Logs', 'Server Logs', 'Device Specs']
-
-const ModalTroubleShooting = () => {
- const [modalTroubleShooting, setModalTroubleShooting] = useAtom(
- modalTroubleShootingAtom
- )
- const [isTabActive, setIsTabActivbe] = useState(0)
- const [showLogFullSize, setshowLogFullSize] = useState(false)
-
- return (
-
-
-
- {`We're here to help! Your report is crucial for debugging and shaping
- the next version. Here’s how you can report & get further support:`}
-
-
-
- {!showLogFullSize && (
-
- )}
-
-
- {!showLogFullSize && (
-
-
Step 2
-
- {`If you can't find what you need in our troubleshooting guide, feel
- free reach out to us for extra help:`}
-
-
-
- )}
-
-
-
-
- {logOption.map((name, i) => {
- return (
- setIsTabActivbe(i)}
- >
-
- {name}
-
-
- )
- })}
-
-
-
-
setshowLogFullSize(!showLogFullSize)}
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- }
- />
- )
-}
-
-export default ModalTroubleShooting
diff --git a/web/containers/ModelConfigInput/index.test.tsx b/web/containers/ModelConfigInput/index.test.tsx
deleted file mode 100644
index cf9cb9da3..000000000
--- a/web/containers/ModelConfigInput/index.test.tsx
+++ /dev/null
@@ -1,84 +0,0 @@
-import '@testing-library/jest-dom'
-import React from 'react'
-import { render, fireEvent } from '@testing-library/react'
-import ModelConfigInput from './index'
-
-// Mocking the Tooltip component to simplify testing
-jest.mock('@janhq/joi', () => ({
- ...jest.requireActual('@janhq/joi'),
- Tooltip: ({
- trigger,
- content,
- }: {
- trigger: React.ReactNode
- content: string
- }) => (
-
- {trigger}
- {content}
-
- ),
-}))
-
-describe('ModelConfigInput', () => {
- it('renders correctly with given props', () => {
- const { getByText, getByPlaceholderText } = render(
-
- )
-
- // Check if title is rendered
- expect(getByText('Test Title')).toBeInTheDocument()
-
- // Check if the description tooltip content is rendered
- expect(getByText('This is a description.')).toBeInTheDocument()
-
- // Check if the placeholder is rendered
- expect(getByPlaceholderText('Enter text here')).toBeInTheDocument()
- })
-
- it('calls onValueChanged when value changes', () => {
- const onValueChangedMock = jest.fn()
- const { getByPlaceholderText } = render(
-
- )
-
- const textArea = getByPlaceholderText('Enter text here')
-
- // Simulate typing in the textarea
- fireEvent.change(textArea, { target: { value: 'New Value' } })
-
- // Check if onValueChanged was called with the new value
- expect(onValueChangedMock).toHaveBeenCalledWith('New Value')
- })
-
- it('disables the textarea when disabled prop is true', () => {
- const { getByPlaceholderText } = render(
-
- )
-
- const textArea = getByPlaceholderText('Enter text here')
-
- // Check if the textarea is disabled
- expect(textArea).toBeDisabled()
- })
-})
diff --git a/web/containers/ModelConfigInput/index.tsx b/web/containers/ModelConfigInput/index.tsx
deleted file mode 100644
index e67080df2..000000000
--- a/web/containers/ModelConfigInput/index.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-import { TextArea, Tooltip } from '@janhq/joi'
-
-import { InfoIcon } from 'lucide-react'
-
-type Props = {
- title: string
- disabled?: boolean
- name: string
- description: string
- placeholder: string
- value: string
- onValueChanged?: (e: string | number | boolean) => void
-}
-
-const ModelConfigInput = ({
- title,
- disabled = false,
- value,
- description,
- placeholder,
- onValueChanged,
-}: Props) => {
- return (
-
-
-
{title}
-
- }
- content={description}
- />
-
-
- )
-}
-
-export default ModelConfigInput
diff --git a/web/containers/ModelDownloadButton/index.tsx b/web/containers/ModelDownloadButton/index.tsx
deleted file mode 100644
index cd97743c2..000000000
--- a/web/containers/ModelDownloadButton/index.tsx
+++ /dev/null
@@ -1,110 +0,0 @@
-import { useCallback, useMemo } from 'react'
-
-import { Button, Tooltip } from '@janhq/joi'
-import { useAtomValue, useSetAtom } from 'jotai'
-
-import { twMerge } from 'tailwind-merge'
-
-import { MainViewState } from '@/constants/screens'
-
-import { useCreateNewThread } from '@/hooks/useCreateNewThread'
-import useDownloadModel from '@/hooks/useDownloadModel'
-
-import ModalCancelDownload from '../ModalCancelDownload'
-
-import { mainViewStateAtom } from '@/helpers/atoms/App.atom'
-import { assistantsAtom } from '@/helpers/atoms/Assistant.atom'
-
-import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom'
-import {
- downloadedModelsAtom,
- getDownloadingModelAtom,
-} from '@/helpers/atoms/Model.atom'
-
-interface Props {
- id: string
- theme?: 'primary' | 'ghost' | 'icon' | 'destructive' | undefined
- variant?: 'solid' | 'soft' | 'outline' | undefined
- className?: string
- hideProgress?: boolean
-}
-const ModelDownloadButton = ({ id, theme, className, hideProgress }: Props) => {
- const { downloadModel } = useDownloadModel()
- const downloadingModels = useAtomValue(getDownloadingModelAtom)
- const serverEnabled = useAtomValue(serverEnabledAtom)
- const downloadedModels = useAtomValue(downloadedModelsAtom)
- const assistants = useAtomValue(assistantsAtom)
- const setMainViewState = useSetAtom(mainViewStateAtom)
- const { requestCreateNewThread } = useCreateNewThread()
- const isDownloaded = useMemo(
- () => downloadedModels.some((md) => md.id === id),
- [downloadedModels, id]
- )
- const isDownloading = useMemo(
- () => downloadingModels.some((md) => md === id),
- [downloadingModels, id]
- )
-
- const onDownloadClick = useCallback(() => {
- downloadModel(id)
- }, [id, downloadModel])
-
- const onUseModelClick = useCallback(async () => {
- const downloadedModel = downloadedModels.find((e) => e.id === id)
- if (downloadedModel)
- await requestCreateNewThread(assistants[0], downloadedModel)
- setMainViewState(MainViewState.Thread)
- }, [
- assistants,
- downloadedModels,
- setMainViewState,
- requestCreateNewThread,
- id,
- ])
-
- const defaultButton = (
- {
- e.stopPropagation()
- onDownloadClick()
- }}
- >
- Download
-
- )
- const downloadingButton = !hideProgress && (
-
- )
-
- const downloadedButton = (
-
- Use
-
- }
- content="Threads are disabled while the server is running"
- disabled={!serverEnabled}
- />
- )
- return (
- <>
- {isDownloading
- ? downloadingButton
- : isDownloaded
- ? downloadedButton
- : defaultButton}
- >
- )
-}
-
-export default ModelDownloadButton
diff --git a/web/containers/ModelDropdown/index.test.tsx b/web/containers/ModelDropdown/index.test.tsx
deleted file mode 100644
index 8cde2df04..000000000
--- a/web/containers/ModelDropdown/index.test.tsx
+++ /dev/null
@@ -1,113 +0,0 @@
-import { render, screen, waitFor, fireEvent } from '@testing-library/react'
-import { useAtomValue, useAtom } from 'jotai'
-import ModelDropdown from './index'
-import useRecommendedModel from '@/hooks/useRecommendedModel'
-import '@testing-library/jest-dom'
-
-class ResizeObserverMock {
- observe() {}
- unobserve() {}
- disconnect() {}
-}
-
-global.ResizeObserver = ResizeObserverMock
-
-jest.mock('jotai', () => {
- const originalModule = jest.requireActual('jotai')
- return {
- ...originalModule,
- useAtom: jest.fn(),
- useAtomValue: jest.fn(),
- useSetAtom: jest.fn(),
- }
-})
-
-jest.mock('@/containers/ModelLabel')
-jest.mock('@/hooks/useRecommendedModel')
-
-describe('ModelDropdown', () => {
- const remoteModel = {
- metadata: { tags: ['Featured'], size: 100 },
- name: 'Test Model',
- engine: 'openai',
- }
-
- const localModel = {
- metadata: { tags: ['Local'], size: 100 },
- name: 'Local Model',
- engine: 'nitro',
- }
-
- const configuredModels = [remoteModel, localModel, localModel]
-
- const mockConfiguredModel = configuredModels
- const selectedModel = { id: 'selectedModel', name: 'selectedModel' }
- const setSelectedModel = jest.fn()
- const showEngineListModel = ['nitro']
- const showEngineListModelAtom = jest.fn()
-
- beforeEach(() => {
- jest.clearAllMocks()
- ;(useAtom as jest.Mock).mockReturnValue([selectedModel, setSelectedModel])
- ;(useAtom as jest.Mock).mockReturnValue([
- showEngineListModel,
- showEngineListModelAtom,
- ])
- ;(useAtomValue as jest.Mock).mockReturnValue(mockConfiguredModel)
- ;(useRecommendedModel as jest.Mock).mockReturnValue({
- recommendedModel: { id: 'model1', parameters: [], settings: [] },
- downloadedModels: [],
- })
- })
-
- it('renders the ModelDropdown component', async () => {
- render( )
-
- await waitFor(() => {
- expect(screen.getByTestId('model-selector')).toBeInTheDocument()
- })
- })
-
- it('renders the ModelDropdown component as disabled', async () => {
- render( )
-
- await waitFor(() => {
- expect(screen.getByTestId('model-selector')).toBeInTheDocument()
- expect(screen.getByTestId('model-selector')).toHaveClass(
- 'pointer-events-none'
- )
- })
- })
-
- it('renders the ModelDropdown component as badge for chat Input', async () => {
- render( )
-
- await waitFor(() => {
- expect(screen.getByTestId('model-selector')).toBeInTheDocument()
- expect(screen.getByTestId('model-selector-badge')).toBeInTheDocument()
- expect(screen.getByTestId('model-selector-badge')).toHaveClass('badge')
- })
- })
-
- it('renders the Tab correctly', async () => {
- render( )
-
- await waitFor(() => {
- expect(screen.getByTestId('model-selector')).toBeInTheDocument()
- expect(screen.getByText('On-device')).toBeInTheDocument()
- expect(screen.getByText('Cloud')).toBeInTheDocument()
- })
- })
-
- it('filters models correctly', async () => {
- render( )
-
- await waitFor(() => {
- expect(screen.getByTestId('model-selector')).toBeInTheDocument()
- fireEvent.click(screen.getByText('Cloud'))
- fireEvent.change(screen.getByText('Cloud'), {
- target: { value: 'remote' },
- })
- })
- })
-})
diff --git a/web/containers/ModelDropdown/index.tsx b/web/containers/ModelDropdown/index.tsx
deleted file mode 100644
index 1793e78ca..000000000
--- a/web/containers/ModelDropdown/index.tsx
+++ /dev/null
@@ -1,654 +0,0 @@
-import {
- useState,
- useMemo,
- useEffect,
- useCallback,
- useRef,
- Fragment,
-} from 'react'
-
-import Image from 'next/image'
-
-import { EngineConfig, InferenceEngine } from '@janhq/core'
-import {
- badgeVariants,
- Button,
- Input,
- ScrollArea,
- Tabs,
- useClickOutside,
-} from '@janhq/joi'
-
-import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'
-
-import {
- CheckIcon,
- ChevronDownIcon,
- ChevronUpIcon,
- DownloadCloudIcon,
- XIcon,
-} from 'lucide-react'
-import { twMerge } from 'tailwind-merge'
-
-import ProgressCircle from '@/containers/Loader/ProgressCircle'
-
-import ModelLabel from '@/containers/ModelLabel'
-
-import SetupRemoteModel from '@/containers/SetupRemoteModel'
-
-import { useActiveModel } from '@/hooks/useActiveModel'
-
-import { useCreateNewThread } from '@/hooks/useCreateNewThread'
-import useDownloadModel from '@/hooks/useDownloadModel'
-import { modelDownloadStateAtom } from '@/hooks/useDownloadState'
-import { useGetEngines } from '@/hooks/useEngineManagement'
-
-import { useGetFeaturedSources } from '@/hooks/useModelSource'
-import useRecommendedModel from '@/hooks/useRecommendedModel'
-
-import useUpdateModelParameters from '@/hooks/useUpdateModelParameters'
-
-import { formatDownloadPercentage, toGigabytes } from '@/utils/converter'
-
-import { getLogoEngine, getTitleByEngine } from '@/utils/modelEngine'
-
-import { extractModelName } from '@/utils/modelSource'
-
-import { activeAssistantAtom } from '@/helpers/atoms/Assistant.atom'
-import {
- configuredModelsAtom,
- getDownloadingModelAtom,
- selectedModelAtom,
- showEngineListModelAtom,
-} from '@/helpers/atoms/Model.atom'
-
-import { showScrollBarAtom } from '@/helpers/atoms/Setting.atom'
-import {
- activeThreadAtom,
- setThreadModelParamsAtom,
-} from '@/helpers/atoms/Thread.atom'
-
-type Props = {
- chatInputMode?: boolean
- strictedThread?: boolean
- disabled?: boolean
-}
-
-export const modelDropdownStateAtom = atom(false)
-
-const ModelDropdown = ({
- disabled,
- chatInputMode,
- strictedThread = true,
-}: Props) => {
- const { downloadModel } = useDownloadModel()
- const [modelDropdownState, setModelDropdownState] = useAtom(
- modelDropdownStateAtom
- )
- const showScrollBar = useAtomValue(showScrollBarAtom)
-
- const [searchFilter, setSearchFilter] = useState('local')
- const [searchText, setSearchText] = useState('')
- const [open, setOpen] = useState(modelDropdownState)
- const activeThread = useAtomValue(activeThreadAtom)
- const activeAssistant = useAtomValue(activeAssistantAtom)
- const downloadingModels = useAtomValue(getDownloadingModelAtom)
- const [toggle, setToggle] = useState(null)
- const [selectedModel, setSelectedModel] = useAtom(selectedModelAtom)
- const { recommendedModel, downloadedModels } = useRecommendedModel()
- const [dropdownOptions, setDropdownOptions] = useState(
- null
- )
- const { sources: featuredModels } = useGetFeaturedSources()
-
- const { engines } = useGetEngines()
-
- const downloadStates = useAtomValue(modelDownloadStateAtom)
- const setThreadModelParams = useSetAtom(setThreadModelParamsAtom)
- const { updateModelParameter } = useUpdateModelParameters()
- const searchInputRef = useRef(null)
- const configuredModels = useAtomValue(configuredModelsAtom)
- const { stopModel } = useActiveModel()
-
- const { updateThreadMetadata } = useCreateNewThread()
-
- const engineList = useMemo(
- () =>
- Object.entries(engines ?? {}).flatMap((e) => ({
- name: e[0],
- type: e[1][0]?.type === 'remote' ? 'remote' : 'local',
- engine: e[1][0],
- })),
- [engines]
- )
-
- useClickOutside(() => handleChangeStateOpen(false), null, [
- dropdownOptions,
- toggle,
- ])
-
- const [showEngineListModel, setShowEngineListModel] = useAtom(
- showEngineListModelAtom
- )
-
- const handleChangeStateOpen = useCallback(
- (state: boolean) => {
- setOpen(state)
- setModelDropdownState(state)
- },
- [setModelDropdownState]
- )
-
- const filteredDownloadedModels = useMemo(
- () =>
- configuredModels
- .concat(
- downloadedModels.filter(
- (e) => !configuredModels.some((x) => x.id === e.id)
- )
- )
- .filter((e) =>
- e.name.toLowerCase().includes(searchText.toLowerCase().trim())
- )
- .filter((e) => {
- if (searchFilter === 'local') {
- return (
- engineList.find((t) => t.engine?.engine === e.engine)?.type ===
- 'local'
- )
- }
- return true
- })
- .sort((a, b) => a.name.localeCompare(b.name))
- .sort((a, b) => {
- const aInDownloadedModels = downloadedModels.some(
- (item) => item.id === a.id
- )
- const bInDownloadedModels = downloadedModels.some(
- (item) => item.id === b.id
- )
- if (aInDownloadedModels && !bInDownloadedModels) {
- return -1
- } else if (!aInDownloadedModels && bInDownloadedModels) {
- return 1
- } else {
- return 0
- }
- }),
- [configuredModels, searchText, searchFilter, downloadedModels, engineList]
- )
-
- useEffect(() => {
- if (modelDropdownState && chatInputMode) {
- setOpen(modelDropdownState)
- }
- }, [chatInputMode, modelDropdownState])
-
- useEffect(() => {
- if (open && searchInputRef.current) {
- searchInputRef.current.focus()
- }
- }, [open])
-
- useEffect(() => {
- setShowEngineListModel((prev) => [
- ...prev,
- ...engineList
- .filter((x) => (x.engine?.api_key?.length ?? 0) > 0)
- .map((e) => e.name),
- ])
- }, [setShowEngineListModel, engineList])
-
- useEffect(() => {
- if (!activeThread) return
- const modelId = activeAssistant?.model?.id
-
- const model = downloadedModels.find((model) => model.id === modelId)
- if (model) {
- if (
- engines?.[model.engine]?.[0]?.type === 'local' ||
- (engines?.[model.engine]?.[0]?.api_key?.length ?? 0) > 0
- )
- setSelectedModel(model)
- } else {
- setSelectedModel(undefined)
- }
- }, [
- recommendedModel,
- activeThread,
- downloadedModels,
- setSelectedModel,
- activeAssistant?.model?.id,
- engines,
- ])
-
- const isLocalEngine = useCallback(
- (engine?: string) => {
- if (!engine) return false
- return engineList.some((t) => t.name === engine && t.type === 'local')
- },
- [engineList]
- )
-
- const onClickModelItem = useCallback(
- async (modelId: string) => {
- if (!activeAssistant) return
- const model = downloadedModels.find((m) => m.id === modelId)
- setSelectedModel(model)
- setOpen(false)
- stopModel()
-
- if (activeThread) {
- const contextLength = model?.settings.ctx_len
- ? Math.min(8192, model?.settings.ctx_len ?? 8192)
- : undefined
- const overriddenParameters = {
- ctx_len: contextLength,
- max_tokens: contextLength
- ? Math.min(model?.parameters.max_tokens ?? 8192, contextLength)
- : model?.parameters.max_tokens,
- }
-
- const modelParams = {
- ...model?.parameters,
- ...model?.settings,
- ...overriddenParameters,
- }
-
- // Update model parameter to the thread state
- setThreadModelParams(activeThread.id, modelParams)
-
- // Update model parameter to the thread file
- if (model)
- updateModelParameter(
- activeThread,
- {
- params: modelParams,
- modelId: model.id,
- engine: model.engine,
- },
- // Update tools
- [
- {
- type: 'retrieval',
- enabled: model?.engine === InferenceEngine.cortex,
- settings: {
- ...(activeAssistant.tools &&
- activeAssistant.tools[0]?.settings),
- },
- },
- ]
- )
- }
- },
- [
- activeAssistant,
- downloadedModels,
- setSelectedModel,
- activeThread,
- updateThreadMetadata,
- setThreadModelParams,
- updateModelParameter,
- stopModel,
- ]
- )
-
- const isDownloadALocalModel = useMemo(
- () =>
- downloadedModels.some((x) =>
- engineList.some((t) => t.name === x.engine && t.type === 'local')
- ),
- [downloadedModels, engineList]
- )
-
- if (strictedThread && !activeThread) {
- return null
- }
-
- return (
-
-
- {chatInputMode ? (
- handleChangeStateOpen(!open)}
- >
-
- {selectedModel?.name || 'Select a model'}
-
-
- ) : (
-
- }
- onClick={() => setOpen(!open)}
- />
- )}
-
-
-
-
- setSearchFilter(value)}
- />
-
-
- setSearchText(e.target.value)}
- suffixIcon={
- searchText.length > 0 && (
- setSearchText('')}
- />
- )
- }
- />
-
-
- {engineList
- .filter((e) => e.type === searchFilter)
- .filter(
- (e) =>
- e.type === 'remote' ||
- e.name === InferenceEngine.cortex_llamacpp ||
- filteredDownloadedModels.some((e) => e.engine === e.name)
- )
- .map((engine, i) => {
- const isConfigured =
- engine.type === 'local' ||
- ((engine.engine as EngineConfig).api_key?.length ?? 0) > 1
- const engineLogo = getLogoEngine(engine.name as InferenceEngine)
- const showModel = showEngineListModel.includes(engine.name)
- const onClickChevron = () => {
- if (showModel) {
- setShowEngineListModel((prev) =>
- prev.filter((item) => item !== engine.name)
- )
- } else {
- setShowEngineListModel((prev) => [...prev, engine.name])
- }
- }
- return (
-
-
-
-
- {engineLogo && (
-
- )}
-
- {getTitleByEngine(engine.name)}
-
-
-
- {engine.type === 'remote' && (
- 0
- }
- />
- )}
- {!showModel ? (
-
-
-
- ) : (
-
-
-
- )}
-
-
-
- {engine.type === 'local' &&
- !isDownloadALocalModel &&
- showModel &&
- !searchText.length && (
-
- {featuredModels?.map((model) => {
- const isDownloading = downloadingModels.some(
- (md) => md === (model.models[0]?.id ?? model.id)
- )
- return (
-
-
-
- {extractModelName(model.id)}
-
-
-
-
-
- {toGigabytes(model.models[0]?.size)}
-
- {!isDownloading ? (
-
- downloadModel(model.models[0]?.id)
- }
- />
- ) : (
- Object.values(downloadStates)
- .filter(
- (x) =>
- x.modelId ===
- (model.models[0]?.id ?? model.id)
- )
- .map((item) => (
-
- ))
- )}
-
-
- )
- })}
-
- )}
-
-
- {filteredDownloadedModels
- .filter(
- (x) =>
- x.engine === engine.name ||
- (x.engine === InferenceEngine.nitro &&
- engine.name === InferenceEngine.cortex_llamacpp)
- )
- .filter((y) => {
- if (isLocalEngine(y.engine) && !searchText.length) {
- return downloadedModels.find((c) => c.id === y.id)
- } else {
- return y
- }
- })
- .map((model) => {
- if (!showModel) return null
- const isDownloading = downloadingModels.some(
- (md) => md === model.id
- )
- const isDownloaded = downloadedModels.some(
- (c) => c.id === model.id
- )
- return (
-
- {isDownloaded && (
- {
- if (
- !isConfigured &&
- engine.type === 'remote'
- )
- return null
- if (isDownloaded) {
- onClickModelItem(model.id)
- }
- }}
- >
-
-
- {selectedModel?.id === model.id && (
-
- )}
- {!isDownloaded && (
-
- {toGigabytes(model.metadata?.size)}
-
- )}
- {!isDownloading && !isDownloaded ? (
-
- downloadModel(
- model.sources[0].url,
- model.id
- )
- }
- />
- ) : (
- Object.values(downloadStates)
- .filter((x) => x.modelId === model.id)
- .map((item) => (
-
- ))
- )}
-
-
- )}
-
- )
- })}
-
-
-
- )
- })}
-
-
-
-
- )
-}
-
-export default ModelDropdown
diff --git a/web/containers/ModelLabel/ModelLabel.test.tsx b/web/containers/ModelLabel/ModelLabel.test.tsx
deleted file mode 100644
index 545fc30d8..000000000
--- a/web/containers/ModelLabel/ModelLabel.test.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import React from 'react'
-import { render } from '@testing-library/react'
-import { useAtomValue } from 'jotai'
-import { useActiveModel } from '@/hooks/useActiveModel'
-import ModelLabel from '@/containers/ModelLabel'
-
-jest.mock('jotai', () => ({
- useAtomValue: jest.fn(),
- atom: jest.fn(),
-}))
-
-jest.mock('@/hooks/useActiveModel', () => ({
- useActiveModel: jest.fn(),
-}))
-
-describe('ModelLabel', () => {
- const mockUseAtomValue = useAtomValue as jest.Mock
- const mockUseActiveModel = useActiveModel as jest.Mock
-
- const defaultProps: any = {
- metadata: {
- author: 'John Doe', // Add the 'author' property with a value
- tags: ['8B'],
- size: 100,
- },
- compact: false,
- }
-
- beforeEach(() => {
- jest.clearAllMocks()
- })
-
- it('renders nothing when minimumRamModel is less than availableRam', () => {
- mockUseAtomValue
- .mockReturnValueOnce(100)
- .mockReturnValueOnce(50)
- .mockReturnValueOnce(0)
- mockUseActiveModel.mockReturnValue({
- activeModel: { metadata: { size: 0 } },
- })
-
- const props = {
- ...defaultProps,
- metadata: {
- ...defaultProps.metadata,
- size: 10,
- },
- }
-
- const { container } = render( )
- expect(container.firstChild).toBeNull()
- })
-})
diff --git a/web/containers/ModelLabel/NotEnoughMemoryLabel.tsx b/web/containers/ModelLabel/NotEnoughMemoryLabel.tsx
deleted file mode 100644
index 287193183..000000000
--- a/web/containers/ModelLabel/NotEnoughMemoryLabel.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import { Fragment, memo } from 'react'
-
-import { Badge, Tooltip } from '@janhq/joi'
-import { AlertTriangleIcon, InfoIcon } from 'lucide-react'
-
-type Props = {
- compact?: boolean
- unit: string
-}
-
-const tooltipContent = `Your device doesn't have enough RAM to run this model. Consider upgrading your RAM or using a device with more memory capacity.`
-
-const NotEnoughMemoryLabel = ({ unit, compact }: Props) => (
- <>
- {compact ? (
-
-
- }
- content={
-
- Not enough RAM: {tooltipContent}
-
- }
- />
-
- ) : (
-
- Not enough {unit}
-
- }
- content={
-
- Not enough RAM: {tooltipContent}
-
- }
- />
-
- )}
- >
-)
-
-export default memo(NotEnoughMemoryLabel)
diff --git a/web/containers/ModelLabel/RecommendedLabel.tsx b/web/containers/ModelLabel/RecommendedLabel.tsx
deleted file mode 100644
index 8236cf13a..000000000
--- a/web/containers/ModelLabel/RecommendedLabel.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import { memo } from 'react'
-
-import { Badge } from '@janhq/joi'
-
-import { twMerge } from 'tailwind-merge'
-
-type Props = {
- compact?: boolean
-}
-
-const RecommendedLabel = ({ compact }: Props) => (
-
- {!compact && Recommended }
-
-)
-
-export default memo(RecommendedLabel)
diff --git a/web/containers/ModelLabel/SlowOnYourDeviceLabel.tsx b/web/containers/ModelLabel/SlowOnYourDeviceLabel.tsx
deleted file mode 100644
index e8e9bcb4d..000000000
--- a/web/containers/ModelLabel/SlowOnYourDeviceLabel.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import { Fragment, memo } from 'react'
-
-import { Badge, Tooltip } from '@janhq/joi'
-
-import { AlertTriangleIcon, InfoIcon } from 'lucide-react'
-
-type Props = {
- compact?: boolean
-}
-
-const tooltipContent = `Your device may be running low on available RAM, which can affect the speed of this model. Try closing any unnecessary applications to free up system memory.`
-
-const SlowOnYourDeviceLabel = ({ compact }: Props) => (
- <>
- {compact ? (
-
-
- }
- content={
-
- Slow on your device: {tooltipContent}
-
- }
- />
-
- ) : (
-
- Slow on your device
-
- }
- content={
-
- Slow on your device: {tooltipContent}
-
- }
- />
-
- )}
- >
-)
-
-export default memo(SlowOnYourDeviceLabel)
diff --git a/web/containers/ModelLabel/index.tsx b/web/containers/ModelLabel/index.tsx
deleted file mode 100644
index 6c6c3cfda..000000000
--- a/web/containers/ModelLabel/index.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-import React from 'react'
-
-import { useAtomValue } from 'jotai'
-
-import { useActiveModel } from '@/hooks/useActiveModel'
-
-import NotEnoughMemoryLabel from './NotEnoughMemoryLabel'
-
-import SlowOnYourDeviceLabel from './SlowOnYourDeviceLabel'
-
-import {
- availableVramAtom,
- totalRamAtom,
- usedRamAtom,
-} from '@/helpers/atoms/SystemBar.atom'
-
-type Props = {
- size?: number
- compact?: boolean
-}
-
-const ModelLabel = ({ size, compact }: Props) => {
- const { activeModel } = useActiveModel()
- const totalRam = useAtomValue(totalRamAtom)
- const usedRam = useAtomValue(usedRamAtom)
- const availableVram = useAtomValue(availableVramAtom)
-
- const getLabel = (size: number) => {
- const minimumRamModel = (size * 1.25) / (1024 * 1024)
-
- const availableRam =
- availableVram > 0
- ? availableVram * 1000000 // MB to bytes
- : totalRam -
- (usedRam +
- (activeModel?.metadata?.size
- ? (activeModel.metadata.size * 1.25) / (1024 * 1024)
- : 0))
-
- if (minimumRamModel > totalRam) {
- return (
- 0 ? 'VRAM' : 'RAM'}
- compact={compact}
- />
- )
- }
-
- if (minimumRamModel < totalRam && minimumRamModel > availableRam) {
- return
- }
-
- return null
- }
-
- return getLabel(size ?? 0)
-}
-
-export default React.memo(ModelLabel)
diff --git a/web/containers/ModelSearch/index.tsx b/web/containers/ModelSearch/index.tsx
deleted file mode 100644
index 223601cb7..000000000
--- a/web/containers/ModelSearch/index.tsx
+++ /dev/null
@@ -1,98 +0,0 @@
-import React, { ChangeEvent, useCallback, useState, useRef } from 'react'
-
-import { Input } from '@janhq/joi'
-import { SearchIcon } from 'lucide-react'
-
-import { useDebouncedCallback } from 'use-debounce'
-
-import {
- useGetModelSources,
- useModelSourcesMutation,
-} from '@/hooks/useModelSource'
-
-import Spinner from '../Loader/Spinner'
-
-type Props = {
- supportModelImport?: boolean
- onSearchLocal?: (searchText: string) => void
-}
-
-const ModelSearch = ({ supportModelImport, onSearchLocal }: Props) => {
- const [searchText, setSearchText] = useState('')
- const [isSearching, setSearching] = useState(false)
- const { mutate } = useGetModelSources()
- const { addModelSource } = useModelSourcesMutation()
- const inputRef = useRef(null)
- const debounced = useDebouncedCallback(async () => {
- if (searchText.indexOf('/') === -1) {
- // If we don't find / in the text, perform a local search
- onSearchLocal?.(searchText)
- return
- }
- // Attempt to search local
- onSearchLocal?.(searchText)
-
- setSearching(true)
- // Attempt to search model source
- if (supportModelImport)
- addModelSource(searchText)
- .then(() => mutate())
- .then(() => onSearchLocal?.(searchText))
- .catch(console.debug)
- .finally(() => setSearching(false))
- }, 300)
-
- const onSearchChanged = useCallback(
- (e: ChangeEvent) => {
- e.preventDefault()
- e.stopPropagation()
- setSearchText(e.target.value)
- debounced()
- },
- [debounced]
- )
-
- const onClear = useCallback(() => {
- setSearchText('')
- debounced()
- }, [debounced])
-
- const onKeyDown = useCallback(
- (e: React.KeyboardEvent) => {
- if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {
- e.preventDefault()
- debounced()
- }
- },
- [debounced]
- )
-
- return (
-
- ) : (
-
- )
- }
- placeholder={
- supportModelImport
- ? 'Search or enter Hugging Face URL'
- : 'Search models'
- }
- onChange={onSearchChanged}
- onKeyDown={onKeyDown}
- value={searchText}
- clearable={searchText.length > 0}
- onClear={onClear}
- className="bg-[hsla(var(--app-bg))]"
- onClick={() => {
- onSearchLocal?.(inputRef.current?.value ?? '')
- }}
- />
- )
-}
-
-export default ModelSearch
diff --git a/web/containers/ModelSetting/SettingComponent.tsx b/web/containers/ModelSetting/SettingComponent.tsx
deleted file mode 100644
index d892dbe61..000000000
--- a/web/containers/ModelSetting/SettingComponent.tsx
+++ /dev/null
@@ -1,106 +0,0 @@
-import {
- SettingComponentProps,
- InputComponentProps,
- CheckboxComponentProps,
- SliderComponentProps,
-} from '@janhq/core'
-
-import Checkbox from '@/containers/Checkbox'
-import ModelConfigInput from '@/containers/ModelConfigInput'
-import SliderRightPanel from '@/containers/SliderRightPanel'
-import TagInput from '@/containers/TagInput'
-
-type Props = {
- componentProps: SettingComponentProps[]
- disabled?: boolean
- onValueUpdated: (
- key: string,
- value: string | number | boolean | string[]
- ) => void
-}
-
-const SettingComponent: React.FC = ({
- componentProps,
- disabled = false,
- onValueUpdated,
-}) => {
- const components = componentProps.map((data) => {
- switch (data.controllerType) {
- case 'slider': {
- const { min, max, step, value } =
- data.controllerProps as SliderComponentProps
-
- return (
- onValueUpdated(data.key, value)}
- />
- )
- }
-
- case 'input': {
- const { placeholder, value: textValue } =
- data.controllerProps as InputComponentProps
- return (
- onValueUpdated(data.key, value)}
- />
- )
- }
-
- case 'tag': {
- const { placeholder, value: textValue } =
- data.controllerProps as InputComponentProps
- return (
- onValueUpdated(data.key, value)}
- />
- )
- }
-
- case 'checkbox': {
- const { value } = data.controllerProps as CheckboxComponentProps
- return (
- onValueUpdated(data.key, value)}
- />
- )
- }
-
- default:
- return null
- }
- })
-
- return {components}
-}
-
-export default SettingComponent
diff --git a/web/containers/ModelSetting/index.tsx b/web/containers/ModelSetting/index.tsx
deleted file mode 100644
index 9c21bf6ac..000000000
--- a/web/containers/ModelSetting/index.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import React from 'react'
-
-import { SettingComponentProps } from '@janhq/core'
-
-import SettingComponentBuilder from './SettingComponent'
-
-type Props = {
- componentProps: SettingComponentProps[]
- onValueChanged: (
- key: string,
- value: string | number | boolean | string[]
- ) => void
- disabled?: boolean
-}
-
-const ModelSetting = ({
- componentProps,
- onValueChanged,
- disabled = false,
-}: Props) => (
-
-)
-
-export default React.memo(ModelSetting)
diff --git a/web/containers/Providers/AppUpdateListener.tsx b/web/containers/Providers/AppUpdateListener.tsx
deleted file mode 100644
index 39b78aac7..000000000
--- a/web/containers/Providers/AppUpdateListener.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import { Fragment, useEffect } from 'react'
-
-import { AppUpdateInfo } from '@janhq/core'
-import { useSetAtom } from 'jotai'
-
-import {
- appDownloadProgressAtom,
- appUpdateAvailableAtom,
- updateVersionErrorAtom,
- appUpdateNotAvailableAtom,
-} from '@/helpers/atoms/App.atom'
-
-const AppUpdateListener = () => {
- const setProgress = useSetAtom(appDownloadProgressAtom)
- const setUpdateVersionError = useSetAtom(updateVersionErrorAtom)
- const setAppUpdateAvailable = useSetAtom(appUpdateAvailableAtom)
- const setAppUpdateNotvailable = useSetAtom(appUpdateNotAvailableAtom)
-
- useEffect(() => {
- if (window && window.electronAPI) {
- window.electronAPI.onAppUpdateDownloadUpdate(
- (_event: string, appUpdateInfo: AppUpdateInfo) => {
- setProgress(appUpdateInfo.percent)
- console.debug('app update progress:', appUpdateInfo.percent)
- }
- )
-
- window.electronAPI.onAppUpdateDownloadError(
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- (_event: string, error: any) => {
- console.error('Download error: ', error)
- setProgress(-1)
-
- // Can not install update
- // Prompt user to download the update manually
- setUpdateVersionError(error.failedToInstallVersion)
- }
- )
-
- window.electronAPI.onAppUpdateDownloadSuccess(() => {
- setProgress(-1)
- })
-
- window.electronAPI.onAppUpdateAvailable(() => {
- setAppUpdateAvailable(true)
- })
-
- window.electronAPI.onAppUpdateNotAvailable(() => {
- setAppUpdateAvailable(false)
- setAppUpdateNotvailable(true)
- })
- }
- }, [setProgress, setUpdateVersionError, setAppUpdateAvailable])
-
- return
-}
-
-export default AppUpdateListener
diff --git a/web/containers/Providers/ClipboardListener.tsx b/web/containers/Providers/ClipboardListener.tsx
deleted file mode 100644
index d1124794e..000000000
--- a/web/containers/Providers/ClipboardListener.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import { Fragment } from 'react'
-
-import { useSetAtom } from 'jotai'
-
-import { selectedTextAtom } from './Jotai'
-
-const ClipboardListener = () => {
- const setSelectedText = useSetAtom(selectedTextAtom)
-
- if (typeof window !== 'undefined') {
- window?.electronAPI?.onSelectedText((_event: string, text: string) => {
- setSelectedText(text)
- })
- }
-
- return
-}
-
-export default ClipboardListener
diff --git a/web/containers/Providers/CoreConfigurator.tsx b/web/containers/Providers/CoreConfigurator.tsx
deleted file mode 100644
index 54b716309..000000000
--- a/web/containers/Providers/CoreConfigurator.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-'use client'
-
-import { PropsWithChildren, useCallback, useEffect, useState } from 'react'
-
-import Loader from '@/containers/Loader'
-
-import { setupCoreServices } from '@/services/coreService'
-import {
- isCoreExtensionInstalled,
- setupBaseExtensions,
-} from '@/services/extensionService'
-
-import { extensionManager } from '@/extension'
-
-export const CoreConfigurator = ({ children }: PropsWithChildren) => {
- const [setupCore, setSetupCore] = useState(false)
- const [activated, setActivated] = useState(false)
- const [settingUp, setSettingUp] = useState(false)
-
- const setupExtensions = useCallback(async () => {
- // Register all active extensions
- await extensionManager.registerActive()
-
- setTimeout(async () => {
- if (!isCoreExtensionInstalled()) {
- setSettingUp(true)
-
- await new Promise((resolve) => setTimeout(resolve, 500))
- setupBaseExtensions()
- return
- }
-
- extensionManager.load()
- setSettingUp(false)
- setActivated(true)
- }, 500)
- }, [])
-
- // Services Setup
- useEffect(() => {
- setupCoreServices()
- setSetupCore(true)
- return () => {
- extensionManager.unload()
- }
- }, [])
-
- useEffect(() => {
- if (setupCore) {
- // Electron
- if (window && window.core?.api) {
- setupExtensions()
- } else {
- // Host
- setActivated(true)
- }
- }
- }, [setupCore, setupExtensions])
-
- return (
- <>
- {settingUp && }
- {setupCore && activated && <>{children}>}
- >
- )
-}
diff --git a/web/containers/Providers/DataLoader.tsx b/web/containers/Providers/DataLoader.tsx
deleted file mode 100644
index 1174150f4..000000000
--- a/web/containers/Providers/DataLoader.tsx
+++ /dev/null
@@ -1,92 +0,0 @@
-'use client'
-
-import { Fragment, useEffect } from 'react'
-
-import {
- AppConfiguration,
- EngineEvent,
- events,
- getUserHomePath,
-} from '@janhq/core'
-import { useSetAtom } from 'jotai'
-
-import { useDebouncedCallback } from 'use-debounce'
-
-import useAssistants from '@/hooks/useAssistants'
-import { useGetEngines } from '@/hooks/useEngineManagement'
-import useGetSystemResources from '@/hooks/useGetSystemResources'
-import { useGetHardwareInfo } from '@/hooks/useHardwareManagement'
-import useModels from '@/hooks/useModels'
-import useThreads from '@/hooks/useThreads'
-
-import { SettingScreenList } from '@/screens/Settings'
-
-import { defaultJanDataFolderAtom } from '@/helpers/atoms/App.atom'
-import {
- janDataFolderPathAtom,
- quickAskEnabledAtom,
-} from '@/helpers/atoms/AppConfig.atom'
-import { janSettingScreenAtom } from '@/helpers/atoms/Setting.atom'
-
-const DataLoader: React.FC = () => {
- const setJanDataFolderPath = useSetAtom(janDataFolderPathAtom)
- const setQuickAskEnabled = useSetAtom(quickAskEnabledAtom)
- const setJanDefaultDataFolder = useSetAtom(defaultJanDataFolderAtom)
- const setJanSettingScreen = useSetAtom(janSettingScreenAtom)
- const { getData: loadModels } = useModels()
- const { mutate } = useGetEngines()
- const { mutate: getHardwareInfo } = useGetHardwareInfo(false)
-
- useThreads()
- useAssistants()
- useGetSystemResources()
-
- useEffect(() => {
- // Load data once
- loadModels()
- getHardwareInfo()
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [])
- const reloadData = useDebouncedCallback(() => {
- mutate()
- }, 300)
-
- useEffect(() => {
- events.on(EngineEvent.OnEngineUpdate, reloadData)
- return () => {
- // Remove listener on unmount
- events.off(EngineEvent.OnEngineUpdate, reloadData)
- }
- }, [reloadData])
-
- useEffect(() => {
- window.core?.api
- ?.getAppConfigurations()
- ?.then((appConfig: AppConfiguration) => {
- setJanDataFolderPath(appConfig.data_folder)
- setQuickAskEnabled(appConfig.quick_ask)
- })
- }, [setJanDataFolderPath, setQuickAskEnabled])
-
- useEffect(() => {
- async function getDefaultJanDataFolder() {
- const defaultJanDataFolder = await getUserHomePath()
-
- setJanDefaultDataFolder(defaultJanDataFolder)
- }
- getDefaultJanDataFolder()
- }, [setJanDefaultDataFolder])
-
- useEffect(() => {
- const janSettingScreen = SettingScreenList.filter(
- (screen) => window.electronAPI || screen !== 'Extensions'
- )
- setJanSettingScreen(janSettingScreen)
- }, [setJanSettingScreen])
-
- console.debug('Load Data...')
-
- return
-}
-
-export default DataLoader
diff --git a/web/containers/Providers/DeepLinkListener.tsx b/web/containers/Providers/DeepLinkListener.tsx
deleted file mode 100644
index 41a8d9e24..000000000
--- a/web/containers/Providers/DeepLinkListener.tsx
+++ /dev/null
@@ -1,98 +0,0 @@
-import { Fragment } from 'react'
-
-import { useSetAtom } from 'jotai'
-
-import { useDebouncedCallback } from 'use-debounce'
-
-import { MainViewState } from '@/constants/screens'
-
-import {
- useGetModelSources,
- useModelSourcesMutation,
-} from '@/hooks/useModelSource'
-
-import { loadingModalInfoAtom } from '../LoadingModal'
-import { toaster } from '../Toast'
-
-import { mainViewStateAtom } from '@/helpers/atoms/App.atom'
-import { modelDetailAtom } from '@/helpers/atoms/Model.atom'
-
-const DeepLinkListener: React.FC = () => {
- const { addModelSource } = useModelSourcesMutation()
- const setLoadingInfo = useSetAtom(loadingModalInfoAtom)
- const setMainView = useSetAtom(mainViewStateAtom)
- const setModelDetail = useSetAtom(modelDetailAtom)
- const { mutate } = useGetModelSources()
-
- const handleDeepLinkAction = useDebouncedCallback(
- async (deepLinkAction: DeepLinkAction) => {
- if (
- deepLinkAction.action !== 'models' ||
- deepLinkAction.provider !== 'huggingface'
- ) {
- console.error(
- `Invalid deeplink action (${deepLinkAction.action}) or provider (${deepLinkAction.provider})`
- )
- return
- }
-
- try {
- setLoadingInfo({
- title: 'Getting Hugging Face model details',
- message: 'Please wait..',
- })
- await addModelSource(deepLinkAction.resource).then(() => mutate())
- setLoadingInfo(undefined)
- setMainView(MainViewState.Hub)
- setModelDetail(deepLinkAction.resource)
- } catch (err) {
- setLoadingInfo(undefined)
- toaster({
- title: 'Failed to get Hugging Face model details',
- description: err instanceof Error ? err.message : 'Unexpected Error',
- type: 'error',
- })
- console.error(err)
- }
- },
- 300
- )
-
- window.electronAPI?.onDeepLink((_event: string, input: string) => {
- window.core?.api?.ackDeepLink()
-
- const action = deeplinkParser(input)
- if (!action) return
- handleDeepLinkAction(action)
- })
-
- return
-}
-
-type DeepLinkAction = {
- action: string
- provider: string
- resource: string
-}
-
-const deeplinkParser = (
- deepLink: string | undefined
-): DeepLinkAction | undefined => {
- if (!deepLink) return undefined
-
- try {
- const url = new URL(deepLink)
- const params = url.pathname.split('/').filter((str) => str.length > 0)
-
- if (params.length < 3) return undefined
- const action = params[0]
- const provider = params[1]
- const resource = params.slice(2).join('/')
- return { action, provider, resource }
- } catch (err) {
- console.error(err)
- return undefined
- }
-}
-
-export default DeepLinkListener
diff --git a/web/containers/Providers/EventListener.tsx b/web/containers/Providers/EventListener.tsx
deleted file mode 100644
index 27764bcdc..000000000
--- a/web/containers/Providers/EventListener.tsx
+++ /dev/null
@@ -1,131 +0,0 @@
-import { useCallback, useEffect } from 'react'
-
-import React from 'react'
-
-import {
- DownloadEvent,
- DownloadState,
- events,
- ModelEvent,
- ExtensionTypeEnum,
- ModelExtension,
- ModelManager,
- Model,
-} from '@janhq/core'
-
-import { useSetAtom } from 'jotai'
-
-import { setDownloadStateAtom } from '@/hooks/useDownloadState'
-
-import { toaster } from '../Toast'
-
-import AppUpdateListener from './AppUpdateListener'
-import ClipboardListener from './ClipboardListener'
-import ModelHandler from './ModelHandler'
-
-import QuickAskListener from './QuickAskListener'
-
-import { extensionManager } from '@/extension'
-import {
- addDownloadingModelAtom,
- removeDownloadingModelAtom,
-} from '@/helpers/atoms/Model.atom'
-
-const EventListener = () => {
- const setDownloadState = useSetAtom(setDownloadStateAtom)
- const addDownloadingModel = useSetAtom(addDownloadingModelAtom)
- const removeDownloadingModel = useSetAtom(removeDownloadingModelAtom)
-
- const onFileDownloadUpdate = useCallback(
- async (state: DownloadState) => {
- console.debug('onFileDownloadUpdate', state)
- addDownloadingModel(state.modelId)
- setDownloadState(state)
- },
- [addDownloadingModel, setDownloadState]
- )
-
- const onFileDownloadError = useCallback(
- (state: DownloadState) => {
- console.debug('onFileDownloadError', state)
- state.downloadState = 'error'
- setDownloadState(state)
- removeDownloadingModel(state.modelId)
- },
- [setDownloadState, removeDownloadingModel]
- )
-
- const onFileDownloadStopped = useCallback(
- (state: DownloadState) => {
- console.debug('onFileDownloadError', state)
-
- state.downloadState = 'error'
- state.error = 'aborted'
- setDownloadState(state)
- removeDownloadingModel(state.modelId)
- },
- [setDownloadState, removeDownloadingModel]
- )
-
- const onFileDownloadSuccess = useCallback(
- async (state: DownloadState) => {
- console.debug('onFileDownloadSuccess', state)
-
- // Update model metadata accordingly
- const model = ModelManager.instance().models.get(state.modelId)
- if (model) {
- await extensionManager
- .get(ExtensionTypeEnum.Model)
- ?.updateModel({
- id: model.id,
- ...model.settings,
- ...model.parameters,
- } as Partial)
- .catch((e) => console.debug(e))
-
- toaster({
- title: 'Download Completed',
- description: `Download ${state.modelId} completed`,
- type: 'success',
- })
- }
- state.downloadState = 'end'
- setDownloadState(state)
- removeDownloadingModel(state.modelId)
- events.emit(ModelEvent.OnModelsUpdate, { fetch: true })
- },
- [removeDownloadingModel, setDownloadState]
- )
-
- useEffect(() => {
- console.debug('EventListenerWrapper: registering event listeners...')
- events.on(DownloadEvent.onFileDownloadUpdate, onFileDownloadUpdate)
- events.on(DownloadEvent.onFileDownloadError, onFileDownloadError)
- events.on(DownloadEvent.onFileDownloadSuccess, onFileDownloadSuccess)
- events.on(DownloadEvent.onFileDownloadStopped, onFileDownloadStopped)
-
- return () => {
- console.debug('EventListenerWrapper: unregistering event listeners...')
- events.off(DownloadEvent.onFileDownloadUpdate, onFileDownloadUpdate)
- events.off(DownloadEvent.onFileDownloadError, onFileDownloadError)
- events.off(DownloadEvent.onFileDownloadSuccess, onFileDownloadSuccess)
- events.off(DownloadEvent.onFileDownloadStopped, onFileDownloadStopped)
- }
- }, [
- onFileDownloadUpdate,
- onFileDownloadError,
- onFileDownloadSuccess,
- onFileDownloadStopped,
- ])
-
- return (
- <>
-
-
-
-
- >
- )
-}
-
-export default EventListener
diff --git a/web/containers/Providers/Jotai.tsx b/web/containers/Providers/Jotai.tsx
deleted file mode 100644
index 5371097f4..000000000
--- a/web/containers/Providers/Jotai.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-'use client'
-
-import { PropsWithChildren } from 'react'
-
-import { Provider, atom } from 'jotai'
-
-import { FileInfo } from '@/types/file'
-
-export const editPromptAtom = atom('')
-export const currentPromptAtom = atom('')
-export const fileUploadAtom = atom()
-
-export const searchAtom = atom('')
-
-export const selectedTextAtom = atom('')
-
-export default function JotaiWrapper({ children }: PropsWithChildren) {
- return {children}
-}
diff --git a/web/containers/Providers/KeyListener.tsx b/web/containers/Providers/KeyListener.tsx
deleted file mode 100644
index 02a1d4eb5..000000000
--- a/web/containers/Providers/KeyListener.tsx
+++ /dev/null
@@ -1,94 +0,0 @@
-'use client'
-
-import { Fragment, useEffect } from 'react'
-
-import { useAtom, useAtomValue, useSetAtom } from 'jotai'
-
-import { MainViewState } from '@/constants/screens'
-
-import { useCreateNewThread } from '@/hooks/useCreateNewThread'
-
-import { useStarterScreen } from '@/hooks/useStarterScreen'
-
-import {
- mainViewStateAtom,
- showLeftPanelAtom,
- showRightPanelAtom,
-} from '@/helpers/atoms/App.atom'
-import { assistantsAtom } from '@/helpers/atoms/Assistant.atom'
-import {
- activeThreadAtom,
- modalActionThreadAtom,
- ThreadModalAction,
-} from '@/helpers/atoms/Thread.atom'
-
-export default function KeyListener() {
- const setShowLeftPanel = useSetAtom(showLeftPanelAtom)
- const setShowRightPanel = useSetAtom(showRightPanelAtom)
- const [mainViewState, setMainViewState] = useAtom(mainViewStateAtom)
- const { requestCreateNewThread } = useCreateNewThread()
- const assistants = useAtomValue(assistantsAtom)
- const activeThread = useAtomValue(activeThreadAtom)
- const setModalActionThread = useSetAtom(modalActionThreadAtom)
- const { isShowStarterScreen } = useStarterScreen()
-
- useEffect(() => {
- const onKeyDown = (e: KeyboardEvent) => {
- const prefixKey = isMac ? e.metaKey : e.ctrlKey
-
- if (e.code === 'KeyB' && prefixKey && e.shiftKey) {
- setShowRightPanel((showRightideBar) => !showRightideBar)
- return
- }
-
- if (e.code === 'Backspace' && prefixKey && e.shiftKey) {
- if (!activeThread || mainViewState !== MainViewState.Thread) return
- setModalActionThread({
- showModal: ThreadModalAction.Delete,
- thread: activeThread,
- })
- return
- }
-
- if (e.code === 'KeyC' && prefixKey && e.shiftKey) {
- if (!activeThread || mainViewState !== MainViewState.Thread) return
- setModalActionThread({
- showModal: ThreadModalAction.Clean,
- thread: activeThread,
- })
- return
- }
-
- if (e.code === 'KeyN' && prefixKey && !isShowStarterScreen) {
- if (mainViewState !== MainViewState.Thread) return
- requestCreateNewThread(assistants[0])
- setMainViewState(MainViewState.Thread)
- return
- }
-
- if (e.code === 'KeyB' && prefixKey) {
- setShowLeftPanel((showLeftSideBar) => !showLeftSideBar)
- return
- }
-
- if (e.code === 'Comma' && prefixKey) {
- setMainViewState(MainViewState.Settings)
- return
- }
- }
- document.addEventListener('keydown', onKeyDown)
- return () => document.removeEventListener('keydown', onKeyDown)
- }, [
- activeThread,
- assistants,
- isShowStarterScreen,
- mainViewState,
- requestCreateNewThread,
- setMainViewState,
- setModalActionThread,
- setShowLeftPanel,
- setShowRightPanel,
- ])
-
- return
-}
diff --git a/web/containers/Providers/ModelHandler.tsx b/web/containers/Providers/ModelHandler.tsx
deleted file mode 100644
index 1ae36fb57..000000000
--- a/web/containers/Providers/ModelHandler.tsx
+++ /dev/null
@@ -1,373 +0,0 @@
-import { Fragment, useCallback, useEffect, useRef } from 'react'
-
-import {
- ChatCompletionMessage,
- ChatCompletionRole,
- events,
- ThreadMessage,
- ExtensionTypeEnum,
- MessageStatus,
- MessageRequest,
- ConversationalExtension,
- MessageEvent,
- MessageRequestType,
- ModelEvent,
- Thread,
- EngineManager,
- InferenceEngine,
- extractInferenceParams,
-} from '@janhq/core'
-import { useAtom, useAtomValue, useSetAtom } from 'jotai'
-import { ulid } from 'ulidx'
-
-import { activeModelAtom, stateModelAtom } from '@/hooks/useActiveModel'
-
-import { extensionManager } from '@/extension'
-import {
- getCurrentChatMessagesAtom,
- addNewMessageAtom,
- updateMessageAtom,
- tokenSpeedAtom,
- subscribedGeneratingMessageAtom,
-} from '@/helpers/atoms/ChatMessage.atom'
-import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
-import {
- updateThreadWaitingForResponseAtom,
- threadsAtom,
- isGeneratingResponseAtom,
- updateThreadAtom,
- getActiveThreadModelParamsAtom,
- activeThreadAtom,
-} from '@/helpers/atoms/Thread.atom'
-
-const maxWordForThreadTitle = 10
-const defaultThreadTitle = 'New Thread'
-
-export default function ModelHandler() {
- const messages = useAtomValue(getCurrentChatMessagesAtom)
- const addNewMessage = useSetAtom(addNewMessageAtom)
- const updateMessage = useSetAtom(updateMessageAtom)
- const downloadedModels = useAtomValue(downloadedModelsAtom)
- const [activeModel, setActiveModel] = useAtom(activeModelAtom)
- const setStateModel = useSetAtom(stateModelAtom)
- const subscribedGeneratingMessage = useAtomValue(
- subscribedGeneratingMessageAtom
- )
- const activeThread = useAtomValue(activeThreadAtom)
-
- const updateThreadWaiting = useSetAtom(updateThreadWaitingForResponseAtom)
- const threads = useAtomValue(threadsAtom)
- const modelsRef = useRef(downloadedModels)
- const threadsRef = useRef(threads)
- const setIsGeneratingResponse = useSetAtom(isGeneratingResponseAtom)
- const updateThread = useSetAtom(updateThreadAtom)
- const messagesRef = useRef(messages)
- const messageGenerationSubscriber = useRef(subscribedGeneratingMessage)
- const activeModelRef = useRef(activeModel)
- const activeThreadRef = useRef(activeThread)
- const activeModelParams = useAtomValue(getActiveThreadModelParamsAtom)
- const activeModelParamsRef = useRef(activeModelParams)
-
- const [tokenSpeed, setTokenSpeed] = useAtom(tokenSpeedAtom)
- const tokenSpeedRef = useRef(tokenSpeed)
-
- useEffect(() => {
- activeThreadRef.current = activeThread
- }, [activeThread])
-
- useEffect(() => {
- threadsRef.current = threads
- }, [threads])
-
- useEffect(() => {
- modelsRef.current = downloadedModels
- }, [downloadedModels])
-
- useEffect(() => {
- messagesRef.current = messages
- }, [messages])
-
- useEffect(() => {
- activeModelRef.current = activeModel
- }, [activeModel])
-
- useEffect(() => {
- activeModelParamsRef.current = activeModelParams
- }, [activeModelParams])
-
- useEffect(() => {
- messageGenerationSubscriber.current = subscribedGeneratingMessage
- }, [subscribedGeneratingMessage])
-
- useEffect(() => {
- tokenSpeedRef.current = tokenSpeed
- }, [tokenSpeed])
-
- const onNewMessageResponse = useCallback(
- async (message: ThreadMessage) => {
- if (message.type !== MessageRequestType.Summary) {
- addNewMessage(message)
- }
- },
- [addNewMessage]
- )
-
- const onModelStopped = useCallback(() => {
- setActiveModel(undefined)
- setStateModel({ state: 'start', loading: false, model: undefined })
- }, [setActiveModel, setStateModel])
-
- const updateThreadTitle = useCallback(
- (message: ThreadMessage) => {
- // Update only when it's finished
- if (message.status !== MessageStatus.Ready) return
-
- const thread = threadsRef.current?.find((e) => e.id == message.thread_id)
- let messageContent = message.content[0]?.text?.value
- if (!thread || !messageContent) return
-
- // No new line character is presented in the title
- // And non-alphanumeric characters should be removed
- if (messageContent.includes('\n'))
- messageContent = messageContent.replace(/\n/g, ' ')
-
- const match = messageContent.match(/<\/think>(.*)$/)
- if (match) messageContent = match[1]
-
- // Remove non-alphanumeric characters
- const cleanedMessageContent = messageContent
- .replace(/[^\p{L}\s]+/gu, '')
- .trim()
-
- // Do not persist empty message
- if (!cleanedMessageContent.trim().length) return
-
- const updatedThread: Thread = {
- ...thread,
-
- title: cleanedMessageContent,
- metadata: {
- ...thread.metadata,
- title: cleanedMessageContent,
- },
- }
-
- extensionManager
- .get(ExtensionTypeEnum.Conversational)
- ?.modifyThread({
- ...updatedThread,
- })
- .then(() => {
- // Update the Thread title with the response of the inference on the 1st prompt
- updateThread({
- ...updatedThread,
- })
- })
- },
- [updateThread]
- )
-
- const updateThreadMessage = useCallback(
- (message: ThreadMessage) => {
- updateMessage(
- message.id,
- message.thread_id,
- message.content,
- message.metadata,
- message.status
- )
-
- if (message.status === MessageStatus.Pending) {
- if (message.content.length) {
- setIsGeneratingResponse(false)
- }
-
- setTokenSpeed((prev) => {
- const currentTimestamp = new Date().getTime() // Get current time in milliseconds
- if (!prev) {
- // If this is the first update, just set the lastTimestamp and return
- return {
- lastTimestamp: currentTimestamp,
- tokenSpeed: 0,
- tokenCount: 1,
- message: message.id,
- }
- }
-
- const timeDiffInSeconds =
- (currentTimestamp - prev.lastTimestamp) / 1000 // Time difference in seconds
- const totalTokenCount = prev.tokenCount + 1
- const averageTokenSpeed =
- totalTokenCount / (timeDiffInSeconds > 0 ? timeDiffInSeconds : 1) // Calculate average token speed
- return {
- ...prev,
- tokenSpeed: averageTokenSpeed,
- tokenCount: totalTokenCount,
- message: message.id,
- model: activeModelRef.current?.name,
- }
- })
- } else {
- // Mark the thread as not waiting for response
- updateThreadWaiting(message.thread_id, false)
-
- setIsGeneratingResponse(false)
-
- const thread = threadsRef.current?.find(
- (e) => e.id == message.thread_id
- )
- if (!thread) return
-
- const messageContent = message.content[0]?.text?.value
-
- const metadata = {
- ...thread.metadata,
- ...(messageContent && { lastMessage: messageContent }),
- updated_at: Date.now(),
- }
-
- updateThread({
- ...thread,
- metadata,
- })
-
- extensionManager
- .get(ExtensionTypeEnum.Conversational)
- ?.modifyThread({
- ...thread,
- metadata,
- })
-
- // Update message's metadata with token usage
- message.metadata = {
- ...message.metadata,
- token_speed: tokenSpeedRef.current?.tokenSpeed,
- model: activeModelRef.current?.name,
- }
-
- if (message.status === MessageStatus.Error) {
- message.metadata = {
- ...message.metadata,
- error: message.content[0]?.text?.value,
- error_code: message.error_code,
- }
- // Unassign active model if any
- setActiveModel(undefined)
- setStateModel({
- state: 'start',
- loading: false,
- model: undefined,
- })
- }
-
- extensionManager
- .get(ExtensionTypeEnum.Conversational)
- ?.createMessage(message)
-
- // Attempt to generate the title of the Thread when needed
- generateThreadTitle(message, thread)
- }
- },
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [setIsGeneratingResponse, updateMessage, updateThread, updateThreadWaiting]
- )
-
- const onMessageResponseUpdate = useCallback(
- (message: ThreadMessage) => {
- if (message.type === MessageRequestType.Summary)
- updateThreadTitle(message)
- else updateThreadMessage(message)
- },
- [updateThreadMessage, updateThreadTitle]
- )
-
- const generateThreadTitle = (message: ThreadMessage, thread: Thread) => {
- // If this is the first ever prompt in the thread
- if ((thread.title ?? thread.metadata?.title)?.trim() !== defaultThreadTitle)
- return
-
- // Check model engine; we don't want to generate a title when it's not a local engine. remote model using first promp
- if (
- !activeModelRef.current ||
- (activeModelRef.current?.engine !== InferenceEngine.cortex &&
- activeModelRef.current?.engine !== InferenceEngine.cortex_llamacpp)
- ) {
- const updatedThread: Thread = {
- ...thread,
- title: (thread.metadata?.lastMessage as string) || defaultThreadTitle,
- metadata: {
- ...thread.metadata,
- title: (thread.metadata?.lastMessage as string) || defaultThreadTitle,
- },
- }
- return extensionManager
- .get(ExtensionTypeEnum.Conversational)
- ?.modifyThread({
- ...updatedThread,
- })
- .then(() => {
- updateThread({
- ...updatedThread,
- })
- })
- .catch(console.error)
- }
-
- // This is the first time message comes in on a new thread
- // Summarize the first message, and make that the title of the Thread
- // 1. Get the summary of the first prompt using whatever engine user is currently using
- const threadMessages = messagesRef?.current
-
- if (!threadMessages || threadMessages.length === 0) return
-
- const summarizeFirstPrompt = `Summarize in a ${maxWordForThreadTitle}-word Title. Give the title only. Here is the message: "${threadMessages[0]?.content[0]?.text?.value}"`
-
- // Prompt: Given this query from user {query}, return to me the summary in 10 words as the title
- const msgId = ulid()
- const messages: ChatCompletionMessage[] = [
- {
- role: ChatCompletionRole.User,
- content: summarizeFirstPrompt,
- },
- ]
-
- const runtimeParams = extractInferenceParams(activeModelParamsRef.current)
-
- const messageRequest: MessageRequest = {
- id: msgId,
- threadId: message.thread_id,
- type: MessageRequestType.Summary,
- attachments: [],
- messages,
- model: {
- ...activeModelRef.current,
- parameters: {
- ...runtimeParams,
- stream: false,
- },
- },
- }
-
- // 2. Update the title with the result of the inference
- setTimeout(() => {
- const engine = EngineManager.instance().get(InferenceEngine.cortex)
- engine?.inference(messageRequest)
- }, 1000)
- }
-
- useEffect(() => {
- if (window.core?.events) {
- events.on(MessageEvent.OnMessageResponse, onNewMessageResponse)
- events.on(MessageEvent.OnMessageUpdate, onMessageResponseUpdate)
- events.on(ModelEvent.OnModelStopped, onModelStopped)
- }
-
- return () => {
- events.off(MessageEvent.OnMessageResponse, onNewMessageResponse)
- events.off(MessageEvent.OnMessageUpdate, onMessageResponseUpdate)
- events.off(ModelEvent.OnModelStopped, onModelStopped)
- }
- }, [onNewMessageResponse, onMessageResponseUpdate, onModelStopped])
-
- return
-}
diff --git a/web/containers/Providers/QuickAskConfigurator.tsx b/web/containers/Providers/QuickAskConfigurator.tsx
deleted file mode 100644
index 40e5caf8e..000000000
--- a/web/containers/Providers/QuickAskConfigurator.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-'use client'
-
-import { PropsWithChildren, useEffect, useState } from 'react'
-
-import { setupCoreServices } from '@/services/coreService'
-
-export const QuickAskConfigurator = ({ children }: PropsWithChildren) => {
- const [setupCore, setSetupCore] = useState(false)
-
- // Services Setup
- useEffect(() => {
- setupCoreServices()
- setSetupCore(true)
- }, [])
-
- return <>{setupCore && <>{children}>}>
-}
diff --git a/web/containers/Providers/QuickAskListener.tsx b/web/containers/Providers/QuickAskListener.tsx
deleted file mode 100644
index 03d685953..000000000
--- a/web/containers/Providers/QuickAskListener.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import { Fragment } from 'react'
-
-import { useSetAtom } from 'jotai'
-
-import { useDebouncedCallback } from 'use-debounce'
-
-import { MainViewState } from '@/constants/screens'
-
-import useSendChatMessage from '@/hooks/useSendChatMessage'
-
-import { mainViewStateAtom } from '@/helpers/atoms/App.atom'
-
-const QuickAskListener: React.FC = () => {
- const { sendChatMessage } = useSendChatMessage()
- const setMainState = useSetAtom(mainViewStateAtom)
-
- const debounced = useDebouncedCallback((value) => {
- setMainState(MainViewState.Thread)
- sendChatMessage(value)
- }, 300)
-
- window.electronAPI?.onUserSubmitQuickAsk((_event: string, input: string) => {
- debounced(input)
- })
-
- return
-}
-
-export default QuickAskListener
diff --git a/web/containers/Providers/Responsive.test.tsx b/web/containers/Providers/Responsive.test.tsx
deleted file mode 100644
index 5a03acea2..000000000
--- a/web/containers/Providers/Responsive.test.tsx
+++ /dev/null
@@ -1,87 +0,0 @@
-import '@testing-library/jest-dom'
-import React from 'react'
-import { render } from '@testing-library/react'
-import { useAtom } from 'jotai'
-import Responsive from './Responsive'
-import { showLeftPanelAtom, showRightPanelAtom } from '@/helpers/atoms/App.atom'
-
-// Mocking the required atoms
-jest.mock('jotai', () => {
- const originalModule = jest.requireActual('jotai')
- return {
- ...originalModule,
- useAtom: jest.fn(),
- useAtomValue: jest.fn(),
- }
-})
-
-const mockSetShowLeftPanel = jest.fn()
-const mockSetShowRightPanel = jest.fn()
-const mockShowLeftPanel = true
-const mockShowRightPanel = true
-
-beforeEach(() => {
- // Mocking the atom behavior
- ;(useAtom as jest.Mock).mockImplementation((atom) => {
- if (atom === showLeftPanelAtom) {
- return [mockShowLeftPanel, mockSetShowLeftPanel]
- }
- if (atom === showRightPanelAtom) {
- return [mockShowRightPanel, mockSetShowRightPanel]
- }
- return [null, jest.fn()]
- })
-})
-
-describe('Responsive', () => {
- beforeAll(() => {
- // Mocking the window.matchMedia function
- window.matchMedia = jest.fn().mockImplementation((query) => {
- return {
- matches: false, // Set this to true to simulate mobile view
- addListener: jest.fn(),
- removeListener: jest.fn(),
- }
- })
- })
-
- it('hides left and right panels on small screens', () => {
- // Simulate mobile view
- window.matchMedia = jest.fn().mockImplementation((query) => ({
- matches: true, // Change to true to simulate mobile
- addListener: jest.fn(),
- removeListener: jest.fn(),
- }))
-
- render( )
-
- // Check that the left and right panel states were updated to false
- expect(mockSetShowLeftPanel).toHaveBeenCalledWith(false)
- expect(mockSetShowRightPanel).toHaveBeenCalledWith(false)
- })
-
- it('restores the last known panel states on larger screens', () => {
- // Simulate mobile view first
- window.matchMedia = jest.fn().mockImplementation((query) => ({
- matches: true, // Change to true to simulate mobile
- addListener: jest.fn(),
- removeListener: jest.fn(),
- }))
-
- render( )
-
- // Change back to desktop view
- window.matchMedia = jest.fn().mockImplementation((query) => ({
- matches: false, // Change to false to simulate desktop
- addListener: jest.fn(),
- removeListener: jest.fn(),
- }))
-
- // Call the effect manually to simulate the component re-rendering
- const rerender = render( )
-
- // Check that the last known states were restored (which were true initially)
- expect(mockSetShowLeftPanel).toHaveBeenCalledWith(true)
- expect(mockSetShowRightPanel).toHaveBeenCalledWith(true)
- })
-})
diff --git a/web/containers/Providers/Responsive.tsx b/web/containers/Providers/Responsive.tsx
deleted file mode 100644
index f73fdc970..000000000
--- a/web/containers/Providers/Responsive.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import { Fragment, useEffect, useRef } from 'react'
-
-import { useMediaQuery } from '@janhq/joi'
-import { useAtom } from 'jotai'
-
-import { showLeftPanelAtom, showRightPanelAtom } from '@/helpers/atoms/App.atom'
-
-const Responsive = () => {
- const matches = useMediaQuery('(max-width: 880px)')
- const [showLeftPanel, setShowLeftPanel] = useAtom(showLeftPanelAtom)
- const [showRightPanel, setShowRightPanel] = useAtom(showRightPanelAtom)
-
- // Refs to store the last known state of the panels
- const lastLeftPanelState = useRef(showLeftPanel)
- const lastRightPanelState = useRef(showRightPanel)
-
- useEffect(() => {
- if (matches) {
- // Store the last known state before closing the panels
- lastLeftPanelState.current = showLeftPanel
- lastRightPanelState.current = showRightPanel
- setShowLeftPanel(false)
- setShowRightPanel(false)
- } else {
- // Restore the last known state when the screen is resized back
- setShowLeftPanel(lastLeftPanelState.current)
- setShowRightPanel(lastRightPanelState.current)
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [matches, setShowLeftPanel, setShowRightPanel])
-
- return
-}
-
-export default Responsive
diff --git a/web/containers/Providers/SWRConfigProvider.tsx b/web/containers/Providers/SWRConfigProvider.tsx
deleted file mode 100644
index 346385343..000000000
--- a/web/containers/Providers/SWRConfigProvider.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-'use client'
-
-import * as React from 'react'
-
-import { SWRConfig } from 'swr'
-
-function SWRConfigProvider({ children }: { children: React.ReactNode }) {
- // https://swr.vercel.app/docs/advanced/cache#localstorage-based-persistent-cache
- // When initializing, we restore the data from `localStorage` into a map.
-
- const map = React.useMemo(() => new Map(), [])
- React.useEffect(() => {
- const savedCache = JSON.parse(
- window.localStorage.getItem('app-cache') || '[]'
- )
- savedCache.forEach(([key, value]: [string, object]) => {
- map.set(key, value)
- })
-
- // Before unloading the app, we write back all the data into `localStorage`.
- window.addEventListener('beforeunload', () => {
- const appCache = JSON.stringify(Array.from(map.entries()))
- window.localStorage.setItem('app-cache', appCache)
- })
- }, [map])
-
- return map }}>{children}
-}
-
-export default SWRConfigProvider
diff --git a/web/containers/Providers/SettingsHandler.tsx b/web/containers/Providers/SettingsHandler.tsx
deleted file mode 100644
index 0ec34b0d4..000000000
--- a/web/containers/Providers/SettingsHandler.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-'use client'
-
-import { useEffect } from 'react'
-
-import { useConfigurations } from '@/hooks/useConfigurations'
-import { useLoadTheme } from '@/hooks/useLoadTheme'
-
-const SettingsHandler: React.FC = () => {
- useLoadTheme()
-
- const { configurePullOptions } = useConfigurations()
-
- useEffect(() => {
- configurePullOptions()
- }, [configurePullOptions])
-
- return <>>
-}
-
-export default SettingsHandler
diff --git a/web/containers/Providers/Theme.test.tsx b/web/containers/Providers/Theme.test.tsx
deleted file mode 100644
index 552bbecbe..000000000
--- a/web/containers/Providers/Theme.test.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import '@testing-library/jest-dom'
-import React from 'react'
-import { render } from '@testing-library/react'
-import ThemeWrapper from './Theme'
-
-// Mock the ThemeProvider from next-themes
-jest.mock('next-themes', () => ({
- ThemeProvider: ({ children }: { children: React.ReactNode }) => (
- {children}
- ),
-}))
-
-describe('ThemeWrapper', () => {
- it('renders children within ThemeProvider', () => {
- const { getByText } = render(
-
- Child Component
-
- )
-
- // Check if the child component is rendered
- expect(getByText('Child Component')).toBeInTheDocument()
- })
-})
diff --git a/web/containers/Providers/Theme.tsx b/web/containers/Providers/Theme.tsx
deleted file mode 100644
index 93325bc40..000000000
--- a/web/containers/Providers/Theme.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-'use client'
-import React from 'react'
-
-import { PropsWithChildren } from 'react'
-
-import { ThemeProvider } from 'next-themes'
-
-export default function ThemeWrapper({ children }: PropsWithChildren) {
- return (
-
- {children}
-
- )
-}
diff --git a/web/containers/Providers/index.tsx b/web/containers/Providers/index.tsx
deleted file mode 100644
index 78cb76d78..000000000
--- a/web/containers/Providers/index.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-'use client'
-
-import { PropsWithChildren } from 'react'
-
-import { Toaster } from 'react-hot-toast'
-
-import EventListener from '@/containers/Providers/EventListener'
-import JotaiWrapper from '@/containers/Providers/Jotai'
-
-import ThemeWrapper from '@/containers/Providers/Theme'
-
-import { CoreConfigurator } from './CoreConfigurator'
-import DataLoader from './DataLoader'
-
-import DeepLinkListener from './DeepLinkListener'
-import KeyListener from './KeyListener'
-import { QuickAskConfigurator } from './QuickAskConfigurator'
-import Responsive from './Responsive'
-
-import SWRConfigProvider from './SWRConfigProvider'
-import SettingsHandler from './SettingsHandler'
-
-const Providers = ({ children }: PropsWithChildren) => {
- const isQuickAsk =
- typeof window !== 'undefined' && window.electronAPI?.isQuickAsk()
- return (
-
-
-
- {isQuickAsk && (
- <>
- {children}
- >
- )}
- {!isQuickAsk && (
-
- <>
-
-
-
-
-
-
-
-
- {children}
- >
-
- )}
-
-
-
- )
-}
-
-export default Providers
diff --git a/web/containers/RightPanelContainer/index.test.tsx b/web/containers/RightPanelContainer/index.test.tsx
deleted file mode 100644
index b8549254b..000000000
--- a/web/containers/RightPanelContainer/index.test.tsx
+++ /dev/null
@@ -1,131 +0,0 @@
-import '@testing-library/jest-dom'
-import { waitFor } from '@testing-library/react'
-
-import React from 'react'
-import { render, fireEvent } from '@testing-library/react'
-import RightPanelContainer, { rightPanelWidthAtom } from './index'
-import { showRightPanelAtom } from '@/helpers/atoms/App.atom'
-import { reduceTransparentAtom } from '@/helpers/atoms/Setting.atom'
-
-// Mocking ResizeObserver
-class ResizeObserver {
- observe() {}
- unobserve() {}
- disconnect() {}
-}
-
-global.ResizeObserver = ResizeObserver
-
-// Mocking window.matchMedia
-Object.defineProperty(window, 'matchMedia', {
- writable: true,
- value: jest.fn().mockImplementation((query) => ({
- matches: false,
- media: query,
- onchange: null,
- addListener: jest.fn(), // deprecated
- removeListener: jest.fn(), // deprecated
- addEventListener: jest.fn(),
- removeEventListener: jest.fn(),
- dispatchEvent: jest.fn(),
- })),
-})
-
-// Mocking the required atoms
-jest.mock('jotai', () => {
- const originalModule = jest.requireActual('jotai')
- return {
- ...originalModule,
- useAtomValue: jest.fn((atom) => {
- if (atom === reduceTransparentAtom) return false
- if (atom === showRightPanelAtom) return true
- }),
- useAtom: jest.fn((atom) => {
- if (atom === rightPanelWidthAtom) return [280, jest.fn()]
- if (atom === showRightPanelAtom) return [true, mockSetShowRightPanel]
- return [null, jest.fn()]
- }),
- }
-})
-
-const mockSetShowRightPanel = jest.fn()
-
-beforeEach(() => {
- // Setting up the localStorage mock
- localStorage.clear()
- localStorage.setItem('rightPanelWidth', '280') // Setting a default width
-})
-
-describe('RightPanelContainer', () => {
- it('renders correctly with children', () => {
- const { getByText } = render(
-
- Child Content
-
- )
-
- // Check if the child content is rendered
- expect(getByText('Child Content')).toBeInTheDocument()
- })
-
- it('initializes width from localStorage', async () => {
- const { container } = render( )
-
- const rightPanel = container.firstChild as HTMLDivElement
-
- // Wait for the width to be applied
- await waitFor(() => {
- expect(rightPanel.style.width).toBe('280px') // Correct width from localStorage
- })
- })
-
- it('changes width on resizing', () => {
- const { container } = render( )
-
- const rightPanel = container.firstChild as HTMLDivElement
-
- // Simulate mouse down on the resize handle
- const resizeHandle = document.createElement('div')
- resizeHandle.className = 'group/resize'
- rightPanel.appendChild(resizeHandle)
-
- // Simulate mouse down to start resizing
- fireEvent.mouseDown(resizeHandle)
-
- // Simulate mouse move event
- fireEvent.mouseMove(window, { clientX: 100 })
-
- // Simulate mouse up to stop resizing
- fireEvent.mouseUp(window)
-
- // Verify that the right panel's width changes
- // Since we can't get the actual width calculation in this test,
- // you may want to check if the rightPanelWidth is updated in your implementation.
- // Here, just check if the function is called:
- expect(localStorage.getItem('rightPanelWidth')).toBeDefined()
- })
-
- it('hides panel when clicked outside on mobile', () => {
- // Mock useMediaQuery to simulate mobile view
- ;(window.matchMedia as jest.Mock).mockImplementation((query) => ({
- matches: true, // Always return true for mobile
- addListener: jest.fn(),
- removeListener: jest.fn(),
- }))
-
- const { container } = render(
-
- Child Content
-
- )
-
- const rightPanel = container.firstChild as HTMLDivElement
-
- // Simulate a click outside
- fireEvent.mouseDown(document.body)
- fireEvent.mouseUp(document.body) // Ensure the click event is completed
-
- // Verify that setShowRightPanel was called to hide the panel
- expect(mockSetShowRightPanel).toHaveBeenCalledWith(false)
- })
-})
diff --git a/web/containers/RightPanelContainer/index.tsx b/web/containers/RightPanelContainer/index.tsx
deleted file mode 100644
index 6d474d557..000000000
--- a/web/containers/RightPanelContainer/index.tsx
+++ /dev/null
@@ -1,134 +0,0 @@
-import {
- Fragment,
- PropsWithChildren,
- useCallback,
- useEffect,
- useState,
-} from 'react'
-
-import { ScrollArea, useClickOutside, useMediaQuery } from '@janhq/joi'
-import { atom, useAtom, useAtomValue } from 'jotai'
-
-import { twMerge } from 'tailwind-merge'
-
-import { showRightPanelAtom } from '@/helpers/atoms/App.atom'
-import {
- reduceTransparentAtom,
- showScrollBarAtom,
-} from '@/helpers/atoms/Setting.atom'
-
-type Props = PropsWithChildren
-
-const DEFAULT_RIGHT_PANEL_WIDTH = 280
-export const RIGHT_PANEL_WIDTH = 'rightPanelWidth'
-
-export const rightPanelWidthAtom = atom(DEFAULT_RIGHT_PANEL_WIDTH)
-
-const RightPanelContainer = ({ children }: Props) => {
- const [isResizing, setIsResizing] = useState(false)
- const [rightPanelWidth, setRightPanelWidth] = useAtom(rightPanelWidthAtom)
- const [rightPanelRef, setRightPanelRef] = useState(
- null
- )
- const reduceTransparent = useAtomValue(reduceTransparentAtom)
- const showScrollBar = useAtomValue(showScrollBarAtom)
-
- const [showRightPanel, setShowRightPanel] = useAtom(showRightPanelAtom)
- const matches = useMediaQuery('(max-width: 880px)')
-
- useClickOutside(
- () => matches && showRightPanel && setShowRightPanel(false),
- null,
- [rightPanelRef]
- )
-
- const startResizing = useCallback(() => {
- setIsResizing(true)
- document.body.classList.add('select-none')
- }, [])
-
- const stopResizing = useCallback(() => {
- setIsResizing(false)
- document.body.classList.remove('select-none')
- }, [])
-
- const resize = useCallback(
- (mouseMoveEvent: { clientX: number }) => {
- if (isResizing) {
- if (rightPanelRef !== null) {
- if (
- rightPanelRef?.getBoundingClientRect().right -
- mouseMoveEvent.clientX <
- 200
- ) {
- setRightPanelWidth(DEFAULT_RIGHT_PANEL_WIDTH)
- setIsResizing(false)
- localStorage.setItem(
- RIGHT_PANEL_WIDTH,
- String(DEFAULT_RIGHT_PANEL_WIDTH)
- )
- setShowRightPanel(false)
- } else {
- const resized =
- rightPanelRef?.getBoundingClientRect().right -
- mouseMoveEvent.clientX
- localStorage.setItem(RIGHT_PANEL_WIDTH, String(resized))
- setRightPanelWidth(resized)
- }
- }
- }
- },
- [isResizing, rightPanelRef, setRightPanelWidth, setShowRightPanel]
- )
-
- useEffect(() => {
- if (localStorage.getItem(RIGHT_PANEL_WIDTH) === null) {
- setRightPanelWidth(DEFAULT_RIGHT_PANEL_WIDTH)
- localStorage.setItem(RIGHT_PANEL_WIDTH, String(DEFAULT_RIGHT_PANEL_WIDTH))
- }
- window.addEventListener('mousemove', resize)
- window.addEventListener('mouseup', stopResizing)
- return () => {
- window.removeEventListener('mousemove', resize)
- window.removeEventListener('mouseup', stopResizing)
- }
- }, [resize, setRightPanelWidth, stopResizing])
-
- return (
- isResizing && e.preventDefault()}
- >
-
- {children}
- {showRightPanel && !matches && (
-
-
-
- )}
-
-
- )
-}
-
-export default RightPanelContainer
diff --git a/web/containers/ServerLogs/index.tsx b/web/containers/ServerLogs/index.tsx
deleted file mode 100644
index 8b230c424..000000000
--- a/web/containers/ServerLogs/index.tsx
+++ /dev/null
@@ -1,307 +0,0 @@
-/* eslint-disable @typescript-eslint/naming-convention */
-
-import { memo, useCallback, useEffect, useRef, useState } from 'react'
-
-import { Button, ScrollArea, useClipboard } from '@janhq/joi'
-import { useAtomValue } from 'jotai'
-
-import { FolderIcon, CheckIcon, CopyIcon } from 'lucide-react'
-
-import { twMerge } from 'tailwind-merge'
-
-import { useLogs } from '@/hooks/useLogs'
-
-import { usePath } from '@/hooks/usePath'
-
-import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom'
-import { showScrollBarAtom } from '@/helpers/atoms/Setting.atom'
-
-type ServerLogsProps = { limit?: number; withCopy?: boolean }
-
-const ServerLogs = (props: ServerLogsProps) => {
- const { limit = 0, withCopy } = props
- const { getLogs } = useLogs()
- const serverEnabled = useAtomValue(serverEnabledAtom)
- const [logs, setLogs] = useState([])
- const listRef = useRef(null)
- const prevScrollTop = useRef(0)
- const isUserManuallyScrollingUp = useRef(false)
- const showScrollBar = useAtomValue(showScrollBarAtom)
-
- const updateLogs = useCallback(
- () =>
- getLogs('app').then((log) => {
- if (typeof log?.split === 'function') {
- setLogs(
- log.split(/\r?\n|\r|\n/g).filter((e) => e.includes('[SERVER]::'))
- )
- }
- }),
- // eslint-disable-next-line react-hooks/exhaustive-deps
- []
- )
-
- useEffect(() => {
- if (serverEnabled) {
- updateLogs()
- }
- }, [serverEnabled, updateLogs])
-
- useEffect(() => {
- updateLogs()
-
- // Log polling interval
- const intervalId = setInterval(() => {
- updateLogs()
- }, window.core?.api?.pollingInterval ?? 1200)
-
- // clean up interval
- return () => clearInterval(intervalId)
- }, [updateLogs])
-
- const { onRevealInFinder } = usePath()
-
- const clipboard = useClipboard({ timeout: 1000 })
-
- const handleScroll = useCallback((event: React.UIEvent) => {
- const currentScrollTop = event.currentTarget.scrollTop
-
- if (prevScrollTop.current > currentScrollTop) {
- isUserManuallyScrollingUp.current = true
- } else {
- const currentScrollTop = event.currentTarget.scrollTop
- const scrollHeight = event.currentTarget.scrollHeight
- const clientHeight = event.currentTarget.clientHeight
-
- if (currentScrollTop + clientHeight >= scrollHeight) {
- isUserManuallyScrollingUp.current = false
- }
- }
-
- if (isUserManuallyScrollingUp.current === true) {
- event.preventDefault()
- event.stopPropagation()
- }
- prevScrollTop.current = currentScrollTop
- }, [])
-
- useEffect(() => {
- if (isUserManuallyScrollingUp.current === true || !listRef.current) return
- const scrollHeight = listRef.current?.scrollHeight ?? 0
- listRef.current?.scrollTo({
- top: scrollHeight,
- behavior: 'instant',
- })
- }, [listRef.current?.scrollHeight, isUserManuallyScrollingUp, logs])
-
- return (
- <>
-
- {withCopy && (
-
-
-
onRevealInFinder('logs')}
- >
-
- <>
-
- Open
- >
-
-
-
{
- clipboard.copy(logs.slice(-100).join('\n') ?? '')
- }}
- >
-
- {clipboard.copied ? (
- <>
-
- Copying...
- >
- ) : (
- <>
-
- Copy All
- >
- )}
-
-
-
-
- )}
-
-
-
- {logs.length > 0 ? (
-
- {logs.slice(-limit).map((log, i) => {
- return (
-
- {log}
-
- )
- })}
-
- ) : (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Empty logs
-
-
- )}
-
-
- >
- )
-}
-
-export default memo(ServerLogs)
diff --git a/web/containers/SetupRemoteModel/index.tsx b/web/containers/SetupRemoteModel/index.tsx
deleted file mode 100644
index b4b2cfa42..000000000
--- a/web/containers/SetupRemoteModel/index.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import { InferenceEngine } from '@janhq/core'
-
-import { Button } from '@janhq/joi'
-import { useSetAtom } from 'jotai'
-import { SettingsIcon, PlusIcon } from 'lucide-react'
-
-import { MainViewState } from '@/constants/screens'
-
-import { mainViewStateAtom } from '@/helpers/atoms/App.atom'
-import { selectedSettingAtom } from '@/helpers/atoms/Setting.atom'
-
-type Props = {
- engine: InferenceEngine
- isConfigured: boolean
-}
-
-const SetupRemoteModel = ({ engine, isConfigured }: Props) => {
- const setSelectedSetting = useSetAtom(selectedSettingAtom)
- const setMainViewState = useSetAtom(mainViewStateAtom)
-
- const onSetupItemClick = (setting: InferenceEngine) => {
- setSelectedSetting(setting)
- setMainViewState(MainViewState.Settings)
- }
-
- return (
- {
- onSetupItemClick(engine)
- }}
- >
- {isConfigured ? (
-
- ) : (
-
- )}
-
- )
-}
-
-export default SetupRemoteModel
diff --git a/web/containers/Shortcut/index.tsx b/web/containers/Shortcut/index.tsx
deleted file mode 100644
index 88415cdf9..000000000
--- a/web/containers/Shortcut/index.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-export default function ShortCut(props: { menu: string }) {
- const { menu } = props
- const symbol = isMac ? '⌘' : 'Ctrl + '
-
- return (
-
- )
-}
diff --git a/web/containers/SliderRightPanel/index.test.tsx b/web/containers/SliderRightPanel/index.test.tsx
deleted file mode 100644
index 8a0644862..000000000
--- a/web/containers/SliderRightPanel/index.test.tsx
+++ /dev/null
@@ -1,105 +0,0 @@
-import React from 'react'
-import { render } from '@testing-library/react'
-import { fireEvent } from '@testing-library/dom'
-import SliderRightPanel from './index'
-import '@testing-library/jest-dom'
-
-class ResizeObserverMock {
- observe() {}
- unobserve() {}
- disconnect() {}
-}
-
-global.ResizeObserver = ResizeObserverMock
-
-jest.mock('@janhq/joi', () => ({
- ...jest.requireActual('@janhq/joi'),
- Slider: ({ children, onValueChange, ...props }: any) => (
-
-
- onValueChange && onValueChange([parseInt(e.target.value)])
- }
- />
- {children}
-
- ),
-}))
-
-describe('SliderRightPanel', () => {
- const defaultProps = {
- title: 'Test Slider',
- disabled: false,
- min: 0,
- max: 100,
- step: 1,
- description: 'This is a test slider',
- value: 50,
- onValueChanged: jest.fn(),
- }
-
- it('renders correctly with given props', () => {
- const { getByText } = render( )
- expect(getByText('Test Slider')).toBeInTheDocument()
- })
-
- it('calls onValueChanged with correct value when input is changed', () => {
- defaultProps.onValueChanged = jest.fn()
- const { getByRole } = render( )
-
- const input = getByRole('textbox')
- fireEvent.change(input, { target: { value: '75' } })
- expect(defaultProps.onValueChanged).toHaveBeenCalledWith(75)
- })
-
- it('calls onValueChanged with correct value when slider is changed', () => {
- defaultProps.onValueChanged = jest.fn()
- const { getByTestId } = render( )
-
- const input = getByTestId('slider-input')
- fireEvent.change(input, { target: { value: '75' } })
- expect(defaultProps.onValueChanged).toHaveBeenCalledWith(75)
- })
-
- it('calls onValueChanged with max value when input exceeds max', () => {
- defaultProps.onValueChanged = jest.fn()
- const { getByRole } = render( )
- const input = getByRole('textbox') as HTMLInputElement
- fireEvent.change(input, { target: { value: '150' } })
- fireEvent.focusOut(input)
- expect(defaultProps.onValueChanged).toHaveBeenCalledWith(100)
- expect(input.value).toEqual('100')
- })
-
- it('calls onValueChanged with min value when input is below min', () => {
- defaultProps.onValueChanged = jest.fn()
- const { getByRole } = render( )
- const input = getByRole('textbox') as HTMLInputElement
- fireEvent.change(input, { target: { value: '0' } })
- fireEvent.focusOut(input)
- expect(defaultProps.onValueChanged).toHaveBeenCalledWith(0)
- expect(input.value).toEqual('0')
- })
-
- it('calls onValueChanged when input value is empty string', () => {
- defaultProps.onValueChanged = jest.fn()
- const { getByRole } = render( )
- const input = getByRole('textbox') as HTMLInputElement
- fireEvent.change(input, { target: { value: '' } })
- fireEvent.focusOut(input)
- expect(defaultProps.onValueChanged).toHaveBeenCalledWith(0)
- expect(input.value).toEqual('0')
- })
-
- it('does not call onValueChanged when input is invalid', () => {
- defaultProps.onValueChanged = jest.fn()
- const { getByRole } = render( )
- const input = getByRole('textbox') as HTMLInputElement
- fireEvent.change(input, { target: { value: 'invalid' } })
- expect(defaultProps.onValueChanged).not.toHaveBeenCalledWith(0)
- expect(input.value).toEqual('50')
- })
-})
diff --git a/web/containers/SliderRightPanel/index.tsx b/web/containers/SliderRightPanel/index.tsx
deleted file mode 100644
index 5022845c9..000000000
--- a/web/containers/SliderRightPanel/index.tsx
+++ /dev/null
@@ -1,135 +0,0 @@
-import { useEffect, useState } from 'react'
-
-import { Slider, Input, Tooltip } from '@janhq/joi'
-
-import { useClickOutside } from '@janhq/joi'
-import { InfoIcon } from 'lucide-react'
-
-type Props = {
- name?: string
- title: string
- disabled: boolean
- description: string
- min: number
- max: number
- step: number
- value: number
- onValueChanged: (e: string | number | boolean) => void
-}
-
-const SliderRightPanel = ({
- title,
- disabled,
- min,
- max,
- step,
- description,
- value,
- onValueChanged,
-}: Props) => {
- const [showTooltip, setShowTooltip] = useState({ max: false, min: false })
- const [val, setVal] = useState(value.toString())
-
- useClickOutside(() => setShowTooltip({ max: false, min: false }), null, [])
-
- useEffect(() => {
- setVal(value.toString())
- }, [value])
-
- return (
-
-
-
{title}
-
- }
- content={description}
- />
-
-
-
-
{
- onValueChanged?.(Number(e[0]))
- setVal(e[0].toString())
- }}
- min={min}
- max={max}
- step={step}
- disabled={disabled}
- />
-
-
-
{
- if (Number(e.target.value) > Number(max)) {
- onValueChanged?.(Number(max))
- setVal(max.toString())
- setShowTooltip({ max: true, min: false })
- } else if (
- Number(e.target.value) < Number(min) ||
- !e.target.value.length
- ) {
- onValueChanged?.(Number(min))
- setVal(min.toString())
- setShowTooltip({ max: false, min: true })
- } else {
- setVal(Number(e.target.value).toString()) // There is a case .5 but not 0.5
- }
- }}
- onChange={(e) => {
- // TODO: How to support negative number input?
- // Passthru since it validates again onBlur
- if (/^\d*\.?\d*$/.test(e.target.value)) {
- setVal(e.target.value)
- }
-
- // Should not accept invalid value or NaN
- // E.g. anything changes that trigger onValueChanged
- // Which is incorrect
- if (
- Number(e.target.value) > Number(max) ||
- Number(e.target.value) < Number(min) ||
- Number.isNaN(Number(e.target.value))
- ) {
- return
- }
- onValueChanged?.(Number(e.target.value))
- }}
- />
- }
- content={
- <>
- {showTooltip.max && (
- Automatically set to the maximum allowed tokens
- )}
- {showTooltip.min && (
- Automatically set to the minimum allowed tokens
- )}
- >
- }
- />
-
-
- )
-}
-
-export default SliderRightPanel
diff --git a/web/containers/TagInput/index.test.tsx b/web/containers/TagInput/index.test.tsx
deleted file mode 100644
index e3d6ef8cc..000000000
--- a/web/containers/TagInput/index.test.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import React from 'react'
-import { render, fireEvent } from '@testing-library/react'
-import TagInput from './index' // Adjust the import path as necessary
-import '@testing-library/jest-dom'
-
-describe('TagInput Component', () => {
- let props: any
-
- beforeEach(() => {
- props = {
- title: 'Tags',
- name: 'tag-input',
- description: 'Add your tags',
- placeholder: 'Enter a tag',
- value: ['tag1', 'tag2'],
- onValueChanged: jest.fn(),
- }
- })
-
- it('renders correctly', () => {
- const { getByText, getByPlaceholderText } = render( )
- expect(getByText('Tags')).toBeInTheDocument()
- expect(getByText('tag1')).toBeInTheDocument()
- expect(getByText('tag2')).toBeInTheDocument()
- expect(getByPlaceholderText('Enter a tag')).toBeInTheDocument()
- })
-
- it('calls onValueChanged when a new tag is added', () => {
- const { getByPlaceholderText } = render( )
- const input = getByPlaceholderText('Enter a tag')
-
- fireEvent.change(input, { target: { value: 'tag3' } })
- fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' })
-
- expect(props.onValueChanged).toHaveBeenCalledWith(
- expect.arrayContaining(['tag1', 'tag2', 'tag3'])
- )
- })
-
- it('calls onValueChanged when a tag is removed', () => {
- const { getAllByRole } = render( )
- const removeButton = getAllByRole('button')[0] // Click on the first remove button
-
- fireEvent.click(removeButton)
-
- expect(props.onValueChanged).toHaveBeenCalledWith(
- expect.arrayContaining(['tag2'])
- )
- })
-})
diff --git a/web/containers/TagInput/index.tsx b/web/containers/TagInput/index.tsx
deleted file mode 100644
index 160cd19e3..000000000
--- a/web/containers/TagInput/index.tsx
+++ /dev/null
@@ -1,149 +0,0 @@
-import { useEffect, useRef, useState } from 'react'
-
-import { Badge, Input, Tooltip } from '@janhq/joi'
-
-import { InfoIcon, XIcon } from 'lucide-react'
-
-type Props = {
- title: string
- disabled?: boolean
- name: string
- description: string
- placeholder: string
- value: string[]
- onValueChanged?: (e: string | number | boolean | string[]) => void
-}
-
-function TooltipBadge({
- item,
- value,
- onValueChanged,
-}: {
- item: string
- value: string[]
- onValueChanged?: (e: string[]) => void
-}) {
- const textRef = useRef(null)
- const [isEllipsized, setIsEllipsized] = useState(false)
-
- useEffect(() => {
- if (textRef.current) {
- setIsEllipsized(textRef.current.scrollWidth > textRef.current.clientWidth)
- }
- }, [item])
-
- return (
-
- {isEllipsized ? (
-
-
-
- {item}
-
- {
- onValueChanged &&
- onValueChanged(value.filter((i) => i !== item))
- }}
- >
-
-
-
-
- }
- content={item}
- />
- ) : (
-
-
- {item}
-
- {
- onValueChanged && onValueChanged(value.filter((i) => i !== item))
- }}
- >
-
-
-
- )}
-
- )
-}
-
-const TagInput = ({
- title,
- disabled = false,
- value,
- description,
- placeholder,
- onValueChanged,
-}: Props) => {
- const [pendingDataPoint, setPendingDataPoint] = useState('')
-
- const addPendingDataPoint = () => {
- if (pendingDataPoint) {
- const newDataPoints = new Set([...value, pendingDataPoint])
- onValueChanged && onValueChanged(Array.from(newDataPoints))
- setPendingDataPoint('')
- }
- }
-
- return (
-
-
-
{title}
-
- }
- content={description}
- />
-
-
setPendingDataPoint(e.target.value)}
- placeholder={placeholder}
- className="w-full"
- onKeyDown={(e) => {
- if (e.key === 'Enter' || e.key === 'Tab') {
- e.preventDefault()
- addPendingDataPoint()
- }
- }}
- />
- {value.length > 0 && (
-
- {value.map((item, idx) => {
- return (
-
- )
- })}
-
- )}
-
- )
-}
-
-export default TagInput
diff --git a/web/containers/Toast/index.tsx b/web/containers/Toast/index.tsx
deleted file mode 100644
index 8754d9c25..000000000
--- a/web/containers/Toast/index.tsx
+++ /dev/null
@@ -1,101 +0,0 @@
-import toast from 'react-hot-toast'
-
-import {
- XIcon,
- CheckCircleIcon,
- XCircleIcon,
- AlertCircleIcon,
- InfoIcon,
-} from 'lucide-react'
-import { twMerge } from 'tailwind-merge'
-
-type Props = {
- title?: string
- description?: string
- type?: 'default' | 'error' | 'success' | 'warning'
-}
-
-const renderIcon = (type: string) => {
- switch (type) {
- case 'warning':
- return (
-
- )
-
- case 'error':
- return (
-
- )
-
- case 'success':
- return (
-
- )
-
- default:
- return
- }
-}
-
-export function toaster(props: Props) {
- const { title, description, type = 'default' } = props
- return toast.custom(
- (t) => {
- return (
-
-
-
{renderIcon(type)}
-
-
- {title}
-
-
- {description}
-
-
-
toast.dismiss(t.id)}
- />
-
-
- )
- },
- { id: 'toast', duration: 2000, position: 'top-right' }
- )
-}
-
-export function snackbar(props: Props) {
- const { description, type = 'default' } = props
- return toast.custom(
- (t) => {
- return (
-
-
-
{renderIcon(type)}
-
- {description}
-
-
toast.dismiss(t.id)}
- />
-
-
- )
- },
- { id: 'snackbar', duration: 2000, position: 'bottom-center' }
- )
-}
diff --git a/web/containers/ToolCallApprovalModal/index.tsx b/web/containers/ToolCallApprovalModal/index.tsx
deleted file mode 100644
index 9b93f3cbf..000000000
--- a/web/containers/ToolCallApprovalModal/index.tsx
+++ /dev/null
@@ -1,110 +0,0 @@
-import { memo, useCallback, useState } from 'react'
-
-import { Button, Modal, ModalClose } from '@janhq/joi'
-import { useSetAtom } from 'jotai'
-
-import { approvedThreadToolsAtom } from '@/helpers/atoms/Thread.atom'
-
-export function useTollCallPromiseModal() {
- const [isOpen, setIsOpen] = useState(false)
- const setApprovedToolsAtom = useSetAtom(approvedThreadToolsAtom)
- const [modalProps, setModalProps] = useState<{
- toolName: string
- threadId: string
- resolveRef: ((value: unknown) => void) | null
- }>({
- toolName: '',
- threadId: '',
- resolveRef: null,
- })
-
- // Function to open the modal and return a Promise
- const showModal = useCallback((toolName: string, threadId: string) => {
- return new Promise((resolve) => {
- setModalProps({
- toolName,
- threadId,
- resolveRef: resolve,
- })
- setIsOpen(true)
- })
- }, [])
-
- const PromiseModal = useCallback(() => {
- const handleConfirm = () => {
- setIsOpen(false)
- if (modalProps.resolveRef) {
- modalProps.resolveRef(true)
- }
- }
-
- const handleCancel = () => {
- setIsOpen(false)
- if (modalProps.resolveRef) {
- modalProps.resolveRef(false)
- }
- }
-
- return (
- Allow tool from {modalProps.toolName} (local)?}
- open={isOpen}
- onOpenChange={(open) => {
- setIsOpen(!isOpen)
- if (!open) handleCancel()
- }}
- content={
-
-
- Malicious MCP servers or conversation content could potentially
- trick Jan into attempting harmful actions through your installed
- tools. Review each action carefully before approving.
-
-
-
- {
- setApprovedToolsAtom((prev) => {
- const newState = { ...prev }
- if (!newState[modalProps.threadId]) {
- newState[modalProps.threadId] = []
- }
- if (
- !newState[modalProps.threadId].includes(
- modalProps.toolName
- )
- ) {
- newState[modalProps.threadId].push(modalProps.toolName)
- }
- return newState
- })
- handleConfirm()
- }}
- autoFocus
- >
- Allow for this chat
-
-
-
-
- Allow once
-
-
-
- Deny
-
-
-
- }
- />
- )
- }, [isOpen, modalProps])
- return { showModal, PromiseModal }
-}
diff --git a/web/dev.Dockerfile b/web/dev.Dockerfile
deleted file mode 100644
index f68b168cf..000000000
--- a/web/dev.Dockerfile
+++ /dev/null
@@ -1,27 +0,0 @@
-# Dockerfile
-
-# Use node alpine as it's a small node image
-FROM node:alpine
-
-# Create the directory on the node image
-# where our Next.js app will live
-RUN mkdir -p /app
-
-# Set /app as the working directory
-WORKDIR /app
-
-# Copy package.json and package-lock.json
-# to the /app working directory
-COPY package*.json /app
-
-# Install dependencies in /app
-RUN yarn install
-
-# Copy the rest of our Next.js folder into /app
-COPY . /app
-
-# Ensure port 3000 is accessible to our system
-EXPOSE 3000
-
-# Run yarn dev, as we would via the command line
-CMD ["yarn", "dev"]
\ No newline at end of file
diff --git a/web/docker-compose.yml b/web/docker-compose.yml
deleted file mode 100644
index 7662ff6a3..000000000
--- a/web/docker-compose.yml
+++ /dev/null
@@ -1,19 +0,0 @@
-version: '3.8'
-
-services:
- web:
- build:
- context: ./
- dockerfile: dev.Dockerfile
- container_name: jan_web
- restart: always
- env_file:
- - .env
- volumes:
- - .:/app
- - /app/node_modules
- - /app/.next
- ports:
- - '3000:3000'
- environment:
- NODE_ENV: development
diff --git a/web/env-example b/web/env-example
deleted file mode 100644
index 11e735cf6..000000000
--- a/web/env-example
+++ /dev/null
@@ -1,3 +0,0 @@
-NEXT_PUBLIC_ENV=development
-NEXT_PUBLIC_WEB_URL=
-NEXT_PUBLIC_DISCORD_INVITATION_URL=
\ No newline at end of file
diff --git a/web/extension/Extension.test.ts b/web/extension/Extension.test.ts
deleted file mode 100644
index d7b4a1805..000000000
--- a/web/extension/Extension.test.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import Extension from "./Extension";
-
-test('should create an Extension instance with all properties', () => {
- const url = 'https://example.com';
- const name = 'Test Extension';
- const productName = 'Test Product';
- const active = true;
- const description = 'Test Description';
- const version = '1.0.0';
-
- const extension = new Extension(url, name, productName, active, description, version);
-
- expect(extension.url).toBe(url);
- expect(extension.name).toBe(name);
- expect(extension.productName).toBe(productName);
- expect(extension.active).toBe(active);
- expect(extension.description).toBe(description);
- expect(extension.version).toBe(version);
-});
diff --git a/web/extension/Extension.ts b/web/extension/Extension.ts
deleted file mode 100644
index 7dfb72b43..000000000
--- a/web/extension/Extension.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-/**
- * Extension manifest object.
- */
-class Extension {
- /** @type {string} Name of the extension. */
- name: string
-
- /** @type {string} Product name of the extension. */
- productName?: string
-
- /** @type {string} The URL of the extension to load. */
- url: string
-
- /** @type {boolean} Whether the extension is activated or not. */
- active?: boolean
-
- /** @type {string} Extension's description. */
- description?: string
-
- /** @type {string} Extension's version. */
- version?: string
-
- constructor(
- url: string,
- name: string,
- productName?: string,
- active?: boolean,
- description?: string,
- version?: string
- ) {
- this.name = name
- this.productName = productName
- this.url = url
- this.active = active
- this.description = description
- this.version = version
- }
-}
-
-export default Extension
diff --git a/web/extension/ExtensionManager.test.ts b/web/extension/ExtensionManager.test.ts
deleted file mode 100644
index 58f784b07..000000000
--- a/web/extension/ExtensionManager.test.ts
+++ /dev/null
@@ -1,131 +0,0 @@
-// ExtensionManager.test.ts
-import { AIEngine, BaseExtension, ExtensionTypeEnum } from '@janhq/core'
-import { ExtensionManager } from './ExtensionManager'
-import Extension from './Extension'
-
-class TestExtension extends BaseExtension {
- onLoad(): void {}
- onUnload(): void {}
-}
-class TestEngine extends AIEngine {
- provider: string = 'testEngine'
- onUnload(): void {}
-}
-
-describe('ExtensionManager', () => {
- let manager: ExtensionManager
-
- beforeEach(() => {
- manager = new ExtensionManager()
- })
-
- it('should register an extension', () => {
- const extension = new TestExtension('', '')
- manager.register('testExtension', extension)
- expect(manager.getByName('testExtension')).toBe(extension)
- })
-
- it('should register an AI engine', () => {
- const extension = { provider: 'testEngine' } as unknown as BaseExtension
- manager.register('testExtension', extension)
- expect(manager.getEngine('testEngine')).toBe(extension)
- })
-
- it('should retrieve an extension by type', () => {
- const extension = new TestExtension('', '')
- jest.spyOn(extension, 'type').mockReturnValue(ExtensionTypeEnum.Assistant)
- manager.register('testExtension', extension)
- expect(manager.get(ExtensionTypeEnum.Assistant)).toBe(extension)
- })
-
- it('should retrieve an extension by name', () => {
- const extension = new TestExtension('', '')
- manager.register('testExtension', extension)
- expect(manager.getByName('testExtension')).toBe(extension)
- })
-
- it('should retrieve all extensions', () => {
- const extension1 = new TestExtension('', '')
- const extension2 = new TestExtension('', '')
- manager.register('testExtension1', extension1)
- manager.register('testExtension2', extension2)
- expect(manager.getAll()).toEqual([extension1, extension2])
- })
-
- it('should retrieve an engine by name', () => {
- const engine = new TestEngine('', '')
- manager.register('anything', engine)
- expect(manager.getEngine('testEngine')).toBe(engine)
- })
-
- it('should load all extensions', () => {
- const extension = new TestExtension('', '')
- jest.spyOn(extension, 'onLoad')
- manager.register('testExtension', extension)
- manager.load()
- expect(extension.onLoad).toHaveBeenCalled()
- })
-
- it('should unload all extensions', () => {
- const extension = new TestExtension('', '')
- jest.spyOn(extension, 'onUnload')
- manager.register('testExtension', extension)
- manager.unload()
- expect(extension.onUnload).toHaveBeenCalled()
- })
-
- it('should list all extensions', () => {
- const extension1 = new TestExtension('', '')
- const extension2 = new TestExtension('', '')
- manager.register('testExtension1', extension1)
- manager.register('testExtension2', extension2)
- expect(manager.listExtensions()).toEqual([extension1, extension2])
- })
-
- it('should retrieve active extensions', async () => {
- const extension = new Extension(
- 'url',
- 'name',
- 'productName',
- true,
- 'description',
- 'version'
- )
- window.core = {
- api: {
- getActiveExtensions: jest.fn(),
- },
- }
- jest
- .spyOn(window.core.api, 'getActiveExtensions')
- .mockResolvedValue([extension])
- const activeExtensions = await manager.getActive()
- expect(activeExtensions).toEqual([extension])
- })
-
- it('should register all active extensions', async () => {
- const extension = new Extension(
- 'url',
- 'name',
- 'productName',
- true,
- 'description',
- 'version'
- )
- jest.spyOn(manager, 'getActive').mockResolvedValue([extension])
- jest.spyOn(manager, 'activateExtension').mockResolvedValue()
- await manager.registerActive()
- expect(manager.activateExtension).toHaveBeenCalledWith(extension)
- })
-
- it('should uninstall extensions', async () => {
- window.core = {
- api: {
- uninstallExtension: jest.fn(),
- },
- }
- jest.spyOn(window.core.api, 'uninstallExtension').mockResolvedValue(true)
- const result = await manager.uninstall(['testExtension'])
- expect(result).toBe(true)
- })
-})
diff --git a/web/extension/ExtensionManager.ts b/web/extension/ExtensionManager.ts
deleted file mode 100644
index f0e613f0d..000000000
--- a/web/extension/ExtensionManager.ts
+++ /dev/null
@@ -1,201 +0,0 @@
-/* eslint-disable @typescript-eslint/no-explicit-any */
-
-import { AIEngine, BaseExtension, ExtensionTypeEnum } from '@janhq/core'
-
-import { convertFileSrc } from '@tauri-apps/api/core'
-
-import Extension from './Extension'
-/**
- * Manages the registration and retrieval of extensions.
- */
-export class ExtensionManager {
- date = new Date().toISOString()
- // Registered extensions
- private extensions = new Map()
-
- // Registered inference engines
- private engines = new Map()
-
- /**
- * Registers an extension.
- * @param extension - The extension to register.
- */
- register(name: string, extension: T) {
- // Register for naming use
- this.extensions.set(name, extension)
-
- // Register AI Engines
- if ('provider' in extension && typeof extension.provider === 'string') {
- this.engines.set(
- extension.provider as unknown as string,
- extension as unknown as AIEngine
- )
- }
- }
-
- /**
- * Retrieves a extension by its type.
- * @param type - The type of the extension to retrieve.
- * @returns The extension, if found.
- */
- get(type: ExtensionTypeEnum): T | undefined {
- return this.getAll().findLast((e) => e.type() === type) as T | undefined
- }
-
- /**
- * Retrieves a extension by its type.
- * @param type - The type of the extension to retrieve.
- * @returns The extension, if found.
- */
- getByName(name: string): BaseExtension | undefined {
- return this.extensions.get(name) as BaseExtension | undefined
- }
-
- /**
- * Retrieves a extension by its type.
- * @param type - The type of the extension to retrieve.
- * @returns The extension, if found.
- */
- getAll(): BaseExtension[] {
- return Array.from(this.extensions.values())
- }
-
- /**
- * Retrieves a extension by its type.
- * @param engine - The engine name to retrieve.
- * @returns The extension, if found.
- */
- getEngine(engine: string): T | undefined {
- return this.engines.get(engine) as T | undefined
- }
-
- /**
- * Loads all registered extension.
- */
- load() {
- this.listExtensions().forEach((ext) => {
- ext.onLoad()
- })
- }
-
- /**
- * Unloads all registered extensions.
- */
- unload() {
- this.listExtensions().forEach((ext) => {
- ext.onUnload()
- })
- }
-
- /**
- * Retrieves a list of all registered extensions.
- * @returns An array of extensions.
- */
- listExtensions() {
- return [...this.extensions.values()]
- }
-
- /**
- * Retrieves a list of all registered extensions.
- * @returns An array of extensions.
- */
- async getActive(): Promise {
- const res = await window.core?.api?.getActiveExtensions()
- if (!res || !Array.isArray(res)) return []
-
- const extensions: Extension[] = res.map(
- (ext: any) =>
- new Extension(
- ext.url,
- ext.name,
- ext.productName,
- ext.active,
- ext.description,
- ext.version
- )
- )
- return extensions
- }
-
- /**
- * Register a extension with its class.
- * @param {Extension} extension extension object as provided by the main process.
- * @returns {void}
- */
- async activateExtension(extension: Extension) {
- // Import class
- const extensionUrl = extension.url
- await import(
- /* webpackIgnore: true */ IS_TAURI
- ? convertFileSrc(extensionUrl)
- : extensionUrl
- ).then((extensionClass) => {
- // Register class if it has a default export
- if (
- typeof extensionClass.default === 'function' &&
- extensionClass.default.prototype
- ) {
- this.register(
- extension.name,
- new extensionClass.default(
- extension.url,
- extension.name,
- extension.productName,
- extension.active,
- extension.description,
- extension.version
- )
- )
- }
- })
- }
-
- /**
- * Registers all active extensions.
- * @returns {void}
- */
- async registerActive() {
- // Get active extensions
- const activeExtensions = (await this.getActive()) ?? []
- // Activate all
- await Promise.all(
- activeExtensions.map((ext: Extension) => this.activateExtension(ext))
- )
- }
-
- /**
- * Install a new extension.
- * @param {Array.} extensions A list of NPM specifiers, or installation configuration objects.
- * @returns {Promise. | false>} extension as defined by the main process. Has property cancelled set to true if installation was cancelled in the main process.
- */
- async install(extensions: any[]) {
- if (typeof window === 'undefined') {
- return
- }
- const res = await window.core?.api?.installExtension(extensions)
- if (res.cancelled) return false
- return res.map(async (ext: any) => {
- const extension = new Extension(ext.name, ext.url, ext.active)
- await this.activateExtension(extension)
- return extension
- })
- }
-
- /**
- * Uninstall provided extensions
- * @param {Array.} extensions List of names of extensions to uninstall.
- * @param {boolean} reload Whether to reload all renderers after updating the extensions.
- * @returns {Promise.} Whether uninstalling the extensions was successful.
- */
- uninstall(extensions: string[], reload = true) {
- if (typeof window === 'undefined') {
- return
- }
- return window.core?.api?.uninstallExtension(extensions, reload)
- }
-}
-
-/**
- * The singleton instance of the ExtensionManager.
- */
-export const extensionManager = new ExtensionManager()
diff --git a/web/extension/index.test.ts b/web/extension/index.test.ts
deleted file mode 100644
index 50b6b59db..000000000
--- a/web/extension/index.test.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-import { extensionManager } from './index';
-
-describe('index', () => {
- it('should export extensionManager from ExtensionManager', () => {
- expect(extensionManager).toBeDefined();
- });
-});
diff --git a/web/extension/index.ts b/web/extension/index.ts
deleted file mode 100644
index e2fbb5ad5..000000000
--- a/web/extension/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { extensionManager } from './ExtensionManager'
diff --git a/web/helpers/atoms/ApiServer.atom.test.ts b/web/helpers/atoms/ApiServer.atom.test.ts
deleted file mode 100644
index 4c5d7fca4..000000000
--- a/web/helpers/atoms/ApiServer.atom.test.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-
-import { hostOptions } from './ApiServer.atom';
-
-test('hostOptions correct values', () => {
- expect(hostOptions).toEqual([
- { name: '127.0.0.1', value: '127.0.0.1' },
- { name: '0.0.0.0', value: '0.0.0.0' },
- ]);
-});
diff --git a/web/helpers/atoms/ApiServer.atom.ts b/web/helpers/atoms/ApiServer.atom.ts
deleted file mode 100644
index ce37ba4ed..000000000
--- a/web/helpers/atoms/ApiServer.atom.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { atomWithStorage } from 'jotai/utils'
-
-export const hostOptions = [
- { name: '127.0.0.1', value: '127.0.0.1' },
- { name: '0.0.0.0', value: '0.0.0.0' },
-]
-
-export const apiServerPortAtom = atomWithStorage('apiServerPort', '1337')
-export const apiServerHostAtom = atomWithStorage('apiServerHost', '127.0.0.1')
-export const apiServerPrefix = atomWithStorage('apiServerPrefix', '/v1')
-
-export const apiServerCorsEnabledAtom = atomWithStorage(
- 'apiServerCorsEnabled',
- true
-)
-
-export const apiServerVerboseLogEnabledAtom = atomWithStorage(
- 'apiServerVerboseLogEnabled',
- true
-)
diff --git a/web/helpers/atoms/App.atom.test.ts b/web/helpers/atoms/App.atom.test.ts
deleted file mode 100644
index f3d58dfc1..000000000
--- a/web/helpers/atoms/App.atom.test.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-
-import { mainViewStateAtom } from './App.atom';
-import { MainViewState } from '@/constants/screens';
-
-test('mainViewStateAtom initializes with Thread', () => {
- const result = mainViewStateAtom.init;
- expect(result).toBe(MainViewState.Thread);
-});
diff --git a/web/helpers/atoms/App.atom.ts b/web/helpers/atoms/App.atom.ts
deleted file mode 100644
index 1d9b9e31b..000000000
--- a/web/helpers/atoms/App.atom.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-import { atom } from 'jotai'
-
-import { atomWithStorage } from 'jotai/utils'
-
-import { MainViewState } from '@/constants/screens'
-
-export const mainViewStateAtom = atom(MainViewState.Thread)
-
-export const defaultJanDataFolderAtom = atom('')
-
-export const LocalEngineDefaultVariantAtom = atom('')
-
-export const RecommendEngineVariantAtom = atomWithStorage(
- 'recommendEngineVariant',
- '',
- undefined,
- { getOnInit: true }
-)
-
-const SHOW_RIGHT_PANEL = 'showRightPanel'
-
-// Store panel atom
-export const showLeftPanelAtom = atom(true)
-
-export const showRightPanelAtom = atomWithStorage(
- SHOW_RIGHT_PANEL,
- false,
- undefined,
- { getOnInit: true }
-)
-
-export const showSystemMonitorPanelAtom = atom(false)
-export const appDownloadProgressAtom = atom(-1)
-export const updateVersionErrorAtom = atom(undefined)
-export const appUpdateAvailableAtom = atom(false)
-export const appUpdateNotAvailableAtom = atom(false)
-
-const COPY_OVER_INSTRUCTION_ENABLED = 'copy_over_instruction_enabled'
-
-export const copyOverInstructionEnabledAtom = atomWithStorage(
- COPY_OVER_INSTRUCTION_ENABLED,
- false
-)
-
-/**
- * App Banner Hub Atom - storage last banner setting - default undefined
- */
-const appBannerHubStorageAtom = atomWithStorage(
- 'appBannerHub',
- undefined,
- undefined,
- {
- getOnInit: true,
- }
-)
-/**
- * App Hub Banner configured image - Retrieve from appBannerHubStorageAtom - fallback a random banner
- */
-export const getAppBannerHubAtom = atom(
- (get) =>
- get(appBannerHubStorageAtom) ??
- `./images/HubBanner/banner-${Math.floor(Math.random() * 30) + 1}.jpg`
-)
-
-/**
- * Set App Hub Banner - store in appBannerHubStorageAtom
- */
-export const setAppBannerHubAtom = atom(null, (get, set, banner: string) => {
- set(appBannerHubStorageAtom, banner)
-})
diff --git a/web/helpers/atoms/AppConfig.atom.test.ts b/web/helpers/atoms/AppConfig.atom.test.ts
deleted file mode 100644
index 28f085e53..000000000
--- a/web/helpers/atoms/AppConfig.atom.test.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-
-import { hostAtom } from './AppConfig.atom';
-
-test('hostAtom default value', () => {
- const result = hostAtom.init;
- expect(result).toBe('http://localhost:1337/');
-});
diff --git a/web/helpers/atoms/AppConfig.atom.ts b/web/helpers/atoms/AppConfig.atom.ts
deleted file mode 100644
index db4157422..000000000
--- a/web/helpers/atoms/AppConfig.atom.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-import { atom } from 'jotai'
-import { atomWithStorage } from 'jotai/utils'
-
-const EXPERIMENTAL_FEATURE = 'experimentalFeature'
-const PROXY_FEATURE_ENABLED = 'proxyFeatureEnabled'
-const VULKAN_ENABLED = 'vulkanEnabled'
-const IGNORE_SSL = 'ignoreSSLFeature'
-const VERIFY_PROXY_SSL = 'verifyProxySSL'
-const VERIFY_PROXY_HOST_SSL = 'verifyProxyHostSSL'
-const VERIFY_PEER_SSL = 'verifyPeerSSL'
-const VERIFY_HOST_SSL = 'verifyHostSSL'
-const HTTPS_PROXY_FEATURE = 'httpsProxyFeature'
-const PROXY_USERNAME = 'proxyUsername'
-const PROXY_PASSWORD = 'proxyPassword'
-const QUICK_ASK_ENABLED = 'quickAskEnabled'
-const NO_PROXY = 'noProxy'
-
-export const janDataFolderPathAtom = atom('')
-
-export const experimentalFeatureEnabledAtom = atomWithStorage(
- EXPERIMENTAL_FEATURE,
- false,
- undefined,
- { getOnInit: true }
-)
-
-export const proxyEnabledAtom = atomWithStorage(
- PROXY_FEATURE_ENABLED,
- false,
- undefined,
- { getOnInit: true }
-)
-export const proxyAtom = atomWithStorage(HTTPS_PROXY_FEATURE, '', undefined, {
- getOnInit: true,
-})
-
-export const proxyUsernameAtom = atomWithStorage(
- PROXY_USERNAME,
- '',
- undefined,
- { getOnInit: true }
-)
-
-export const proxyPasswordAtom = atomWithStorage(
- PROXY_PASSWORD,
- '',
- undefined,
- { getOnInit: true }
-)
-
-export const ignoreSslAtom = atomWithStorage(IGNORE_SSL, false, undefined, {
- getOnInit: true,
-})
-
-export const noProxyAtom = atomWithStorage(NO_PROXY, '', undefined, {
- getOnInit: false,
-})
-
-export const verifyProxySslAtom = atomWithStorage(
- VERIFY_PROXY_SSL,
- false,
- undefined,
- { getOnInit: true }
-)
-
-export const verifyProxyHostSslAtom = atomWithStorage(
- VERIFY_PROXY_HOST_SSL,
- false,
- undefined,
- { getOnInit: true }
-)
-
-export const verifyPeerSslAtom = atomWithStorage(
- VERIFY_PEER_SSL,
- false,
- undefined,
- { getOnInit: true }
-)
-
-export const verifyHostSslAtom = atomWithStorage(
- VERIFY_HOST_SSL,
- false,
- undefined,
- { getOnInit: true }
-)
-
-export const vulkanEnabledAtom = atomWithStorage(
- VULKAN_ENABLED,
- false,
- undefined,
- { getOnInit: true }
-)
-export const quickAskEnabledAtom = atomWithStorage(
- QUICK_ASK_ENABLED,
- false,
- undefined,
- { getOnInit: true }
-)
-
-export const hostAtom = atom('http://localhost:1337/')
diff --git a/web/helpers/atoms/Assistant.atom.test.ts b/web/helpers/atoms/Assistant.atom.test.ts
deleted file mode 100644
index a5073d293..000000000
--- a/web/helpers/atoms/Assistant.atom.test.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-
-import { assistantsAtom } from './Assistant.atom';
-
-test('assistantsAtom initializes as an empty array', () => {
- const initialValue = assistantsAtom.init;
- expect(Array.isArray(initialValue)).toBe(true);
- expect(initialValue).toHaveLength(0);
-});
diff --git a/web/helpers/atoms/Assistant.atom.ts b/web/helpers/atoms/Assistant.atom.ts
deleted file mode 100644
index cb50a0553..000000000
--- a/web/helpers/atoms/Assistant.atom.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { Assistant, ThreadAssistantInfo } from '@janhq/core'
-import { atom } from 'jotai'
-import { atomWithStorage } from 'jotai/utils'
-
-export const assistantsAtom = atom([])
-
-/**
- * Get the current active assistant
- */
-export const activeAssistantAtom = atomWithStorage<
- ThreadAssistantInfo | undefined
->('activeAssistant', undefined, undefined, { getOnInit: true })
diff --git a/web/helpers/atoms/ChatMessage.atom.test.ts b/web/helpers/atoms/ChatMessage.atom.test.ts
deleted file mode 100644
index 6acf4283e..000000000
--- a/web/helpers/atoms/ChatMessage.atom.test.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-
-import { getCurrentChatMessagesAtom } from './ChatMessage.atom';
-import { setConvoMessagesAtom, chatMessages, readyThreadsMessagesAtom } from './ChatMessage.atom';
-
-test('getCurrentChatMessagesAtom returns empty array when no active thread ID', () => {
- const getMock = jest.fn().mockReturnValue(undefined);
- expect(getCurrentChatMessagesAtom.read(getMock)).toEqual([]);
-});
-
-
-test('getCurrentChatMessagesAtom returns empty array when activeThreadId is undefined', () => {
- const getMock = jest.fn().mockReturnValue({
- activeThreadId: undefined,
- chatMessages: {
- threadId: [{ id: 1, content: 'message' }],
- },
- });
- expect(getCurrentChatMessagesAtom.read(getMock)).toEqual([]);
-});
-
-test('setConvoMessagesAtom updates chatMessages and readyThreadsMessagesAtom', () => {
- const getMock = jest.fn().mockReturnValue({});
- const setMock = jest.fn();
- const threadId = 'thread1';
- const messages = [{ id: '1', content: 'Hello' }];
-
- setConvoMessagesAtom.write(getMock, setMock, threadId, messages);
-
- expect(setMock).toHaveBeenCalledWith(chatMessages, { [threadId]: messages });
- expect(setMock).toHaveBeenCalledWith(readyThreadsMessagesAtom, { [threadId]: true });
-});
-
diff --git a/web/helpers/atoms/ChatMessage.atom.ts b/web/helpers/atoms/ChatMessage.atom.ts
deleted file mode 100644
index faae6e298..000000000
--- a/web/helpers/atoms/ChatMessage.atom.ts
+++ /dev/null
@@ -1,201 +0,0 @@
-import {
- ChatCompletionRole,
- MessageStatus,
- ThreadContent,
- ThreadMessage,
-} from '@janhq/core'
-import { atom } from 'jotai'
-
-import { atomWithStorage } from 'jotai/utils'
-
-import {
- getActiveThreadIdAtom,
- updateThreadStateLastMessageAtom,
-} from './Thread.atom'
-
-import { TokenSpeed } from '@/types/token'
-
-const CHAT_MESSAGE_NAME = 'chatMessages'
-/**
- * Stores all chat messages for all threads
- */
-export const chatMessagesStorage = atomWithStorage<
- Record
->(CHAT_MESSAGE_NAME, {}, undefined, { getOnInit: true })
-
-export const cachedMessages = atom>()
-/**
- * Retrieve chat messages for all threads
- */
-export const chatMessages = atom(
- (get) => get(cachedMessages) ?? get(chatMessagesStorage),
- (_get, set, newValue: Record) => {
- set(cachedMessages, newValue)
- ;(() => set(chatMessagesStorage, newValue))()
- }
-)
-
-/**
- * Store subscribed generating message thread
- */
-export const subscribedGeneratingMessageAtom = atom<{
- thread_id?: string
-}>({})
-
-/**
- * Stores the status of the messages load for each thread
- */
-export const readyThreadsMessagesAtom = atomWithStorage<
- Record
->('currentThreadMessages', {}, undefined, { getOnInit: true })
-
-/**
- * Store the token speed for current message
- */
-export const tokenSpeedAtom = atom(undefined)
-/**
- * Return the chat messages for the current active conversation
- */
-export const getCurrentChatMessagesAtom = atom((get) => {
- const activeThreadId = get(getActiveThreadIdAtom)
- if (!activeThreadId) return []
- const messages = get(chatMessages)[activeThreadId]
- if (!Array.isArray(messages)) return []
- return messages ?? []
-})
-
-export const setConvoMessagesAtom = atom(
- null,
- (get, set, threadId: string, messages: ThreadMessage[]) => {
- const newData: Record = {
- ...get(chatMessages),
- }
- newData[threadId] = messages
- set(chatMessages, newData)
- set(readyThreadsMessagesAtom, {
- ...get(readyThreadsMessagesAtom),
- [threadId]: true,
- })
- }
-)
-
-/**
- * Used for pagination. Add old messages to the current conversation
- */
-export const addOldMessagesAtom = atom(
- null,
- (get, set, newMessages: ThreadMessage[]) => {
- const currentConvoId = get(getActiveThreadIdAtom)
- if (!currentConvoId) return
-
- const currentMessages = get(chatMessages)[currentConvoId] ?? []
- const updatedMessages = [...currentMessages, ...newMessages]
-
- const newData: Record = {
- ...get(chatMessages),
- }
- newData[currentConvoId] = updatedMessages
- set(chatMessages, newData)
- }
-)
-
-export const addNewMessageAtom = atom(
- null,
- (get, set, newMessage: ThreadMessage) => {
- const currentMessages = get(chatMessages)[newMessage.thread_id] ?? []
- const updatedMessages = [...currentMessages, newMessage]
-
- const newData: Record = {
- ...get(chatMessages),
- }
- newData[newMessage.thread_id] = updatedMessages
- set(chatMessages, newData)
-
- // Update thread last message
- if (newMessage.content.length)
- set(
- updateThreadStateLastMessageAtom,
- newMessage.thread_id,
- newMessage.content
- )
- }
-)
-
-export const deleteChatMessageAtom = atom(
- null,
- (get, set, threadId: string) => {
- const newData: Record = {
- ...get(chatMessages),
- }
- newData[threadId] = []
- set(chatMessages, newData)
- }
-)
-
-export const cleanChatMessageAtom = atom(null, (get, set, id: string) => {
- const newData: Record = {
- ...get(chatMessages),
- }
- newData[id] = newData[id]?.filter((e) => e.role === ChatCompletionRole.System)
- set(chatMessages, newData)
-})
-
-export const deleteMessageAtom = atom(null, (get, set, id: string) => {
- const newData: Record = {
- ...get(chatMessages),
- }
- const threadId = get(getActiveThreadIdAtom)
- if (threadId) {
- // Should also delete error messages to clear out the error state
- newData[threadId] = newData[threadId].filter(
- (e) => e.id !== id && !e.metadata?.error
- )
-
- set(chatMessages, newData)
- }
-})
-
-export const editMessageAtom = atom('')
-
-export const updateMessageAtom = atom(
- null,
- (
- get,
- set,
- id: string,
- conversationId: string,
- text: ThreadContent[],
- metadata: Record | undefined,
- status: MessageStatus
- ) => {
- const messages = get(chatMessages)[conversationId] ?? []
- const message = messages.find((e) => e.id === id)
- if (message) {
- message.content = text
- message.status = status
- message.metadata = metadata
- const updatedMessages = [...messages]
-
- const newData: Record = {
- ...get(chatMessages),
- }
- newData[conversationId] = updatedMessages
- set(chatMessages, newData)
- // Update thread last message
- if (text.length)
- set(updateThreadStateLastMessageAtom, conversationId, text)
- } else {
- set(addNewMessageAtom, {
- id,
- thread_id: conversationId,
- content: text,
- status,
- role: ChatCompletionRole.Assistant,
- created_at: Date.now() / 1000,
- completed_at: Date.now() / 1000,
- object: 'thread.message',
- metadata: metadata,
- })
- }
- }
-)
diff --git a/web/helpers/atoms/Extension.atom.test.ts b/web/helpers/atoms/Extension.atom.test.ts
deleted file mode 100644
index d41290eea..000000000
--- a/web/helpers/atoms/Extension.atom.test.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-// Extension.atom.test.ts
-
-import { act, renderHook } from '@testing-library/react'
-import * as ExtensionAtoms from './Extension.atom'
-import { useAtom, useAtomValue, useSetAtom } from 'jotai'
-
-describe('Extension.atom.ts', () => {
- afterEach(() => {
- jest.clearAllMocks()
- })
-
- describe('inActiveEngineProviderAtom', () => {
- it('should initialize as an empty array', () => {
- const { result } = renderHook(() => useAtomValue(ExtensionAtoms.inActiveEngineProviderAtom))
- expect(result.current).toEqual([])
- })
-
- it('should persist value in storage', () => {
- const { result } = renderHook(() => useAtom(ExtensionAtoms.inActiveEngineProviderAtom))
-
- act(() => {
- result.current[1](['provider1', 'provider2'])
- })
-
- // Simulate a re-render to check if the value persists
- const { result: newResult } = renderHook(() => useAtomValue(ExtensionAtoms.inActiveEngineProviderAtom))
- expect(newResult.current).toEqual(['provider1', 'provider2'])
- })
- })
-})
diff --git a/web/helpers/atoms/Extension.atom.ts b/web/helpers/atoms/Extension.atom.ts
deleted file mode 100644
index 7e008df85..000000000
--- a/web/helpers/atoms/Extension.atom.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { atomWithStorage } from 'jotai/utils'
-
-const INACTIVE_ENGINE_PROVIDER = 'inActiveEngineProvider'
-export const inActiveEngineProviderAtom = atomWithStorage(
- INACTIVE_ENGINE_PROVIDER,
- [],
- undefined,
- { getOnInit: true }
-)
-
-const SHOW_SETTING_ACTIVE_LOCAL_ENGINE = 'showSettingActiveLocalEngine'
-export const showSettingActiveLocalEngineAtom = atomWithStorage(
- SHOW_SETTING_ACTIVE_LOCAL_ENGINE,
- [],
- undefined,
- { getOnInit: true }
-)
-
-const SHOW_SETTING_ACTIVE_REMOTE_ENGINE = 'showSettingActiveRemoteEngine'
-export const showSettingActiveRemoteEngineAtom = atomWithStorage(
- SHOW_SETTING_ACTIVE_REMOTE_ENGINE,
- [],
- undefined,
- { getOnInit: true }
-)
diff --git a/web/helpers/atoms/LocalServer.atom.test.ts b/web/helpers/atoms/LocalServer.atom.test.ts
deleted file mode 100644
index b3c53ec07..000000000
--- a/web/helpers/atoms/LocalServer.atom.test.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-
-import { serverEnabledAtom } from './LocalServer.atom';
-
-test('serverEnabledAtom_initialValue', () => {
- const result = serverEnabledAtom.init;
- expect(result).toBe(false);
-});
diff --git a/web/helpers/atoms/LocalServer.atom.ts b/web/helpers/atoms/LocalServer.atom.ts
deleted file mode 100644
index 3ee584eed..000000000
--- a/web/helpers/atoms/LocalServer.atom.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { atom } from 'jotai'
-
-export const serverEnabledAtom = atom(false)
-
-export const LocalAPIserverModelParamsAtom = atom()
diff --git a/web/helpers/atoms/Model.atom.test.ts b/web/helpers/atoms/Model.atom.test.ts
deleted file mode 100644
index b4eb87e7a..000000000
--- a/web/helpers/atoms/Model.atom.test.ts
+++ /dev/null
@@ -1,313 +0,0 @@
-import { act, renderHook } from '@testing-library/react'
-import * as ModelAtoms from './Model.atom'
-import { useAtom, useAtomValue, useSetAtom } from 'jotai'
-
-describe('Model.atom.ts', () => {
- let mockJotaiGet: jest.Mock
- let mockJotaiSet: jest.Mock
-
- beforeEach(() => {
- mockJotaiGet = jest.fn()
- mockJotaiSet = jest.fn()
- })
-
- afterEach(() => {
- jest.clearAllMocks()
- })
-
- describe('stateModel', () => {
- it('should initialize with correct default values', () => {
- expect(ModelAtoms.stateModel.init).toEqual({
- state: 'start',
- loading: false,
- model: '',
- })
- })
- })
-
- describe('selectedModelAtom', () => {
- it('should initialize as undefined', () => {
- expect(ModelAtoms.selectedModelAtom.init).toBeUndefined()
- })
- })
-
- describe('showEngineListModelAtom', () => {
- it('should initialize with local engines', () => {
- expect(ModelAtoms.showEngineListModelAtom.init).toEqual([
- 'nitro',
- 'cortex',
- 'llama-cpp',
- 'onnxruntime',
- 'tensorrt-llm',
- ])
- })
- })
-
- describe('addDownloadingModelAtom', () => {
- it('should add downloading model', async () => {
- const { result: reset } = renderHook(() =>
- useSetAtom(ModelAtoms.downloadingModelsAtom)
- )
- const { result: setAtom } = renderHook(() =>
- useSetAtom(ModelAtoms.addDownloadingModelAtom)
- )
- const { result: getAtom } = renderHook(() =>
- useAtomValue(ModelAtoms.getDownloadingModelAtom)
- )
- act(() => {
- setAtom.current({ id: '1' } as any)
- })
- expect(getAtom.current).toEqual([{ id: '1' }])
- act(() => {
- reset.current([])
- })
- })
- })
-
- describe('removeDownloadingModelAtom', () => {
- it('should remove downloading model', async () => {
- const { result: reset } = renderHook(() =>
- useSetAtom(ModelAtoms.downloadingModelsAtom)
- )
-
- const { result: setAtom } = renderHook(() =>
- useSetAtom(ModelAtoms.addDownloadingModelAtom)
- )
- const { result: removeAtom } = renderHook(() =>
- useSetAtom(ModelAtoms.removeDownloadingModelAtom)
- )
- const { result: getAtom } = renderHook(() =>
- useAtomValue(ModelAtoms.getDownloadingModelAtom)
- )
- expect(getAtom.current).toEqual([])
- act(() => {
- setAtom.current('1')
- removeAtom.current('1')
- })
- expect(getAtom.current).toEqual([])
- act(() => {
- reset.current([])
- })
- })
- })
-
- describe('removeDownloadedModelAtom', () => {
- it('should remove downloaded model', async () => {
- const { result: reset } = renderHook(() =>
- useSetAtom(ModelAtoms.downloadingModelsAtom)
- )
- const { result: setAtom } = renderHook(() =>
- useSetAtom(ModelAtoms.downloadedModelsAtom)
- )
- const { result: removeAtom } = renderHook(() =>
- useSetAtom(ModelAtoms.removeDownloadedModelAtom)
- )
- const { result: getAtom } = renderHook(() =>
- useAtomValue(ModelAtoms.downloadedModelsAtom)
- )
- act(() => {
- setAtom.current([{ id: '1' }] as any)
- })
- expect(getAtom.current).toEqual([
- {
- id: '1',
- },
- ])
- act(() => {
- removeAtom.current('1')
- })
- expect(getAtom.current).toEqual([])
- act(() => {
- reset.current([])
- })
- })
- })
-
- describe('importingModelAtom', () => {
- afterEach(() => {
- jest.resetAllMocks()
- jest.clearAllMocks()
- })
- it('should not update for non-existing import', async () => {
- const { result: importAtom } = renderHook(() =>
- useAtom(ModelAtoms.importingModelsAtom)
- )
- const { result: updateAtom } = renderHook(() =>
- useSetAtom(ModelAtoms.updateImportingModelProgressAtom)
- )
- act(() => {
- importAtom.current[1]([])
- updateAtom.current('2', 50)
- })
- expect(importAtom.current[0]).toEqual([])
- })
- it('should update progress for existing import', async () => {
- const { result: importAtom } = renderHook(() =>
- useAtom(ModelAtoms.importingModelsAtom)
- )
- const { result: updateAtom } = renderHook(() =>
- useSetAtom(ModelAtoms.updateImportingModelProgressAtom)
- )
-
- act(() => {
- importAtom.current[1]([
- { importId: '1', status: 'MODEL_SELECTED' },
- ] as any)
- updateAtom.current('1', 50)
- })
- expect(importAtom.current[0]).toEqual([
- {
- importId: '1',
- status: 'IMPORTING',
- percentage: 50,
- },
- ])
- })
-
- it('should not update with invalid data', async () => {
- const { result: importAtom } = renderHook(() =>
- useAtom(ModelAtoms.importingModelsAtom)
- )
- const { result: updateAtom } = renderHook(() =>
- useSetAtom(ModelAtoms.updateImportingModelProgressAtom)
- )
-
- act(() => {
- importAtom.current[1]([
- { importId: '1', status: 'MODEL_SELECTED' },
- ] as any)
- updateAtom.current('2', 50)
- })
- expect(importAtom.current[0]).toEqual([
- {
- importId: '1',
- status: 'MODEL_SELECTED',
- },
- ])
- })
- it('should update import error', async () => {
- const { result: importAtom } = renderHook(() =>
- useAtom(ModelAtoms.importingModelsAtom)
- )
- const { result: errorAtom } = renderHook(() =>
- useSetAtom(ModelAtoms.setImportingModelErrorAtom)
- )
- act(() => {
- importAtom.current[1]([
- { importId: '1', status: 'IMPORTING', percentage: 50 },
- ] as any)
- errorAtom.current('1', 'unknown')
- })
- expect(importAtom.current[0]).toEqual([
- {
- importId: '1',
- status: 'FAILED',
- percentage: 50,
- },
- ])
- })
- it('should not update import error on invalid import ID', async () => {
- const { result: importAtom } = renderHook(() =>
- useAtom(ModelAtoms.importingModelsAtom)
- )
- const { result: errorAtom } = renderHook(() =>
- useSetAtom(ModelAtoms.setImportingModelErrorAtom)
- )
- act(() => {
- importAtom.current[1]([
- { importId: '1', status: 'IMPORTING', percentage: 50 },
- ] as any)
- errorAtom.current('2', 'unknown')
- })
- expect(importAtom.current[0]).toEqual([
- {
- importId: '1',
- status: 'IMPORTING',
- percentage: 50,
- },
- ])
- })
-
- it('should update import success', async () => {
- const { result: importAtom } = renderHook(() =>
- useAtom(ModelAtoms.importingModelsAtom)
- )
- const { result: successAtom } = renderHook(() =>
- useSetAtom(ModelAtoms.setImportingModelSuccessAtom)
- )
-
- act(() => {
- importAtom.current[1]([{ importId: '1', status: 'IMPORTING' }] as any)
- successAtom.current('1', 'id')
- })
- expect(importAtom.current[0]).toEqual([
- {
- importId: '1',
- status: 'IMPORTED',
- percentage: 1,
- modelId: 'id',
- },
- ])
- })
-
- it('should update with invalid import ID', async () => {
- const { result: importAtom } = renderHook(() =>
- useAtom(ModelAtoms.importingModelsAtom)
- )
- const { result: successAtom } = renderHook(() =>
- useSetAtom(ModelAtoms.setImportingModelSuccessAtom)
- )
-
- act(() => {
- importAtom.current[1]([{ importId: '1', status: 'IMPORTING' }] as any)
- successAtom.current('2', 'id')
- })
- expect(importAtom.current[0]).toEqual([
- {
- importId: '1',
- status: 'IMPORTING',
- },
- ])
- })
- it('should not update with valid data', async () => {
- const { result: importAtom } = renderHook(() =>
- useAtom(ModelAtoms.importingModelsAtom)
- )
- const { result: updateAtom } = renderHook(() =>
- useSetAtom(ModelAtoms.updateImportingModelAtom)
- )
-
- act(() => {
- importAtom.current[1]([
- { importId: '1', status: 'IMPORTING', percentage: 1 },
- ] as any)
- updateAtom.current('1', 'name', 'description', ['tag'])
- })
- expect(importAtom.current[0]).toEqual([
- {
- importId: '1',
- percentage: 1,
- status: 'IMPORTING',
- name: 'name',
- tags: ['tag'],
- description: 'description',
- },
- ])
- })
-
- it('should not update when there is no importing model', async () => {
- const { result: importAtom } = renderHook(() =>
- useAtom(ModelAtoms.importingModelsAtom)
- )
- const { result: updateAtom } = renderHook(() =>
- useSetAtom(ModelAtoms.updateImportingModelAtom)
- )
-
- act(() => {
- importAtom.current[1]([])
- updateAtom.current('1', 'name', 'description', ['tag'])
- })
- expect(importAtom.current[0]).toEqual([])
- })
- })
-})
diff --git a/web/helpers/atoms/Model.atom.ts b/web/helpers/atoms/Model.atom.ts
deleted file mode 100644
index 22f277294..000000000
--- a/web/helpers/atoms/Model.atom.ts
+++ /dev/null
@@ -1,202 +0,0 @@
-import { ImportingModel, InferenceEngine, Model } from '@janhq/core'
-import { atom } from 'jotai'
-import { atomWithStorage } from 'jotai/utils'
-
-/**
- * Enum for the keys used to store models in the local storage.
- */
-enum ModelStorageAtomKeys {
- DownloadedModels = 'downloadedModels',
- AvailableModels = 'availableModels',
-}
-//// Models Atom
-/**
- * Downloaded Models Atom
- * This atom stores the list of models that have been downloaded.
- */
-export const downloadedModelsAtom = atomWithStorage(
- ModelStorageAtomKeys.DownloadedModels,
- [],
- undefined,
- { getOnInit: true }
-)
-
-/**
- * Configured Models Atom
- * This atom stores the list of models that have been configured and available to download
- */
-export const configuredModelsAtom = atomWithStorage(
- ModelStorageAtomKeys.AvailableModels,
- [],
- undefined,
- { getOnInit: true }
-)
-
-export const removeDownloadedModelAtom = atom(
- null,
- (get, set, modelId: string) => {
- const downloadedModels = get(downloadedModelsAtom)
-
- set(
- downloadedModelsAtom,
- downloadedModels.filter((e) => e.id !== modelId)
- )
- }
-)
-
-/**
- * Atom to store the selected model (from ModelDropdown)
- */
-export const selectedModelAtom = atom(undefined)
-
-/**
- * Atom to store the expanded engine sections (from ModelDropdown)
- */
-export const showEngineListModelAtom = atom([
- InferenceEngine.nitro,
- InferenceEngine.cortex,
- InferenceEngine.cortex_llamacpp,
- InferenceEngine.cortex_onnx,
- InferenceEngine.cortex_tensorrtllm,
-])
-
-/**
- * Atom to store the current model detail page of a certain model id
- */
-export const modelDetailAtom = atom(undefined)
-
-/// End Models Atom
-/// Model Download Atom
-
-export const stateModel = atom({ state: 'start', loading: false, model: '' })
-
-/**
- * Stores the list of models which are being downloaded.
- */
-export const downloadingModelsAtom = atom([])
-
-export const getDownloadingModelAtom = atom((get) => get(downloadingModelsAtom))
-
-export const addDownloadingModelAtom = atom(null, (get, set, model: string) => {
- const downloadingModels = get(downloadingModelsAtom)
- if (!downloadingModels.includes(model)) {
- set(downloadingModelsAtom, [...downloadingModels, model])
- }
-})
-
-export const removeDownloadingModelAtom = atom(
- null,
- (get, set, modelId: string) => {
- const downloadingModels = get(downloadingModelsAtom)
-
- set(
- downloadingModelsAtom,
- downloadingModels.filter((e) => e !== modelId)
- )
- }
-)
-
-/// End Model Download Atom
-/// Model Import Atom
-
-/// TODO: move this part to another atom
-// store the paths of the models that are being imported
-export const importingModelsAtom = atom([])
-
-/**
- * Importing progress Atom
- */
-export const updateImportingModelProgressAtom = atom(
- null,
- (get, set, importId: string, percentage: number) => {
- const model = get(importingModelsAtom).find((x) => x.importId === importId)
- if (!model) return
- const newModel: ImportingModel = {
- ...model,
- status: 'IMPORTING',
- percentage,
- }
- const newList = get(importingModelsAtom).map((x) =>
- x.importId === importId ? newModel : x
- )
- set(importingModelsAtom, newList)
- }
-)
-
-/**
- * Importing error Atom
- */
-export const setImportingModelErrorAtom = atom(
- null,
- (get, set, importId: string, error: string) => {
- const model = get(importingModelsAtom).find((x) => x.importId === importId)
- if (!model) return
- const newModel: ImportingModel = {
- ...model,
- status: 'FAILED',
- }
-
- console.error(`Importing model ${model} failed`, error)
- const newList = get(importingModelsAtom).map((m) =>
- m.importId === importId ? newModel : m
- )
- set(importingModelsAtom, newList)
- }
-)
-
-/**
- * Importing success Atom
- */
-export const setImportingModelSuccessAtom = atom(
- null,
- (get, set, importId: string, modelId: string) => {
- const model = get(importingModelsAtom).find((x) => x.importId === importId)
- if (!model) return
- const newModel: ImportingModel = {
- ...model,
- modelId,
- status: 'IMPORTED',
- percentage: 1,
- }
- const newList = get(importingModelsAtom).map((x) =>
- x.importId === importId ? newModel : x
- )
- set(importingModelsAtom, newList)
- }
-)
-
-/**
- * Update importing model metadata Atom
- */
-export const updateImportingModelAtom = atom(
- null,
- (
- get,
- set,
- importId: string,
- name: string,
- description: string,
- tags: string[]
- ) => {
- const model = get(importingModelsAtom).find((x) => x.importId === importId)
- if (!model) return
- const newModel: ImportingModel = {
- ...model,
- name,
- importId,
- description,
- tags,
- }
- const newList = get(importingModelsAtom).map((x) =>
- x.importId === importId ? newModel : x
- )
- set(importingModelsAtom, newList)
- }
-)
-
-/// End Model Import Atom
-
-/// ModelDropdown States Atom
-export const isDownloadALocalModelAtom = atom(false)
-export const isAnyRemoteModelConfiguredAtom = atom(false)
-/// End ModelDropdown States Atom
diff --git a/web/helpers/atoms/Setting.atom.test.ts b/web/helpers/atoms/Setting.atom.test.ts
deleted file mode 100644
index 7c5d7ce94..000000000
--- a/web/helpers/atoms/Setting.atom.test.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-
-import { selectedSettingAtom } from './Setting.atom';
-
-test('selectedSettingAtom has correct initial value', () => {
- const result = selectedSettingAtom.init;
- expect(result).toBe('My Models');
-});
diff --git a/web/helpers/atoms/Setting.atom.ts b/web/helpers/atoms/Setting.atom.ts
deleted file mode 100644
index 914e7ddd1..000000000
--- a/web/helpers/atoms/Setting.atom.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-import { atom } from 'jotai'
-
-import { atomWithStorage } from 'jotai/utils'
-
-import { SettingScreen } from '@/screens/Settings'
-
-export const selectedSettingAtom = atom('My Models')
-
-export const janSettingScreenAtom = atom([])
-
-export const THEME = 'themeAppearance'
-export const REDUCE_TRANSPARENT = 'reduceTransparent'
-export const SPELL_CHECKING = 'spellChecking'
-export const SCROLL_BAR = 'scrollBar'
-export const PRODUCT_ANALYTIC = 'productAnalytic'
-export const PRODUCT_ANALYTIC_PROMPT = 'productAnalyticPrompt'
-export const THEME_DATA = 'themeData'
-export const THEME_OPTIONS = 'themeOptions'
-export const THEME_PATH = 'themePath'
-export const CHAT_WIDTH = 'chatWidth'
-export const themesOptionsAtom = atomWithStorage<
- { name: string; value: string }[]
->(THEME_OPTIONS, [], undefined, { getOnInit: true })
-
-export const selectedThemeIdAtom = atomWithStorage(
- THEME,
- 'joi-light',
- undefined,
- { getOnInit: true }
-)
-export const themeDataAtom = atomWithStorage(
- THEME_DATA,
- undefined,
- undefined,
- { getOnInit: true }
-)
-export const reduceTransparentAtom = atomWithStorage(
- REDUCE_TRANSPARENT,
- true,
- undefined,
- { getOnInit: true }
-)
-export const spellCheckAtom = atomWithStorage(
- SPELL_CHECKING,
- false,
- undefined,
- { getOnInit: true }
-)
-export const showScrollBarAtom = atomWithStorage(
- SCROLL_BAR,
- false,
- undefined,
- { getOnInit: true }
-)
-export const productAnalyticAtom = atomWithStorage(
- PRODUCT_ANALYTIC,
- false,
- undefined,
- { getOnInit: true }
-)
-export const productAnalyticPromptAtom = atomWithStorage(
- PRODUCT_ANALYTIC_PROMPT,
- true,
- undefined,
- { getOnInit: true }
-)
-export const chatWidthAtom = atomWithStorage(
- CHAT_WIDTH,
- 'full',
- undefined,
- { getOnInit: true }
-)
diff --git a/web/helpers/atoms/SystemBar.atom.test.ts b/web/helpers/atoms/SystemBar.atom.test.ts
deleted file mode 100644
index 57a7c2ada..000000000
--- a/web/helpers/atoms/SystemBar.atom.test.ts
+++ /dev/null
@@ -1,146 +0,0 @@
-import { renderHook, act } from '@testing-library/react'
-import { useAtom } from 'jotai'
-import * as SystemBarAtoms from './SystemBar.atom'
-
-describe('SystemBar.atom.ts', () => {
- afterEach(() => {
- jest.clearAllMocks()
- })
-
- describe('totalRamAtom', () => {
- it('should initialize as 0', () => {
- const { result } = renderHook(() => useAtom(SystemBarAtoms.totalRamAtom))
- expect(result.current[0]).toBe(0)
- })
-
- it('should update correctly', () => {
- const { result } = renderHook(() => useAtom(SystemBarAtoms.totalRamAtom))
- act(() => {
- result.current[1](16384)
- })
- expect(result.current[0]).toBe(16384)
- })
- })
-
- describe('usedRamAtom', () => {
- it('should initialize as 0', () => {
- const { result } = renderHook(() => useAtom(SystemBarAtoms.usedRamAtom))
- expect(result.current[0]).toBe(0)
- })
-
- it('should update correctly', () => {
- const { result } = renderHook(() => useAtom(SystemBarAtoms.usedRamAtom))
- act(() => {
- result.current[1](8192)
- })
- expect(result.current[0]).toBe(8192)
- })
- })
-
- describe('cpuUsageAtom', () => {
- it('should initialize as 0', () => {
- const { result } = renderHook(() => useAtom(SystemBarAtoms.cpuUsageAtom))
- expect(result.current[0]).toBe(0)
- })
-
- it('should update correctly', () => {
- const { result } = renderHook(() => useAtom(SystemBarAtoms.cpuUsageAtom))
- act(() => {
- result.current[1](50)
- })
- expect(result.current[0]).toBe(50)
- })
- })
-
- describe('ramUtilitizedAtom', () => {
- it('should initialize as 0', () => {
- const { result } = renderHook(() =>
- useAtom(SystemBarAtoms.ramUtilitizedAtom)
- )
- expect(result.current[0]).toBe(0)
- })
-
- it('should update correctly', () => {
- const { result } = renderHook(() =>
- useAtom(SystemBarAtoms.ramUtilitizedAtom)
- )
- act(() => {
- result.current[1](75)
- })
- expect(result.current[0]).toBe(75)
- })
- })
-
- describe('gpusAtom', () => {
- it('should initialize as an empty array', () => {
- const { result } = renderHook(() => useAtom(SystemBarAtoms.gpusAtom))
- expect(result.current[0]).toEqual([])
- })
-
- it('should update correctly', () => {
- const { result } = renderHook(() => useAtom(SystemBarAtoms.gpusAtom))
- const gpus = [{ id: 'gpu1' }, { id: 'gpu2' }]
- act(() => {
- result.current[1](gpus as any)
- })
- expect(result.current[0]).toEqual(gpus)
- })
- })
-
- describe('nvidiaTotalVramAtom', () => {
- it('should initialize as 0', () => {
- const { result } = renderHook(() =>
- useAtom(SystemBarAtoms.nvidiaTotalVramAtom)
- )
- expect(result.current[0]).toBe(0)
- })
-
- it('should update correctly', () => {
- const { result } = renderHook(() =>
- useAtom(SystemBarAtoms.nvidiaTotalVramAtom)
- )
- act(() => {
- result.current[1](8192)
- })
- expect(result.current[0]).toBe(8192)
- })
- })
-
- describe('availableVramAtom', () => {
- it('should initialize as 0', () => {
- const { result } = renderHook(() =>
- useAtom(SystemBarAtoms.availableVramAtom)
- )
- expect(result.current[0]).toBe(0)
- })
-
- it('should update correctly', () => {
- const { result } = renderHook(() =>
- useAtom(SystemBarAtoms.availableVramAtom)
- )
- act(() => {
- result.current[1](4096)
- })
- expect(result.current[0]).toBe(4096)
- })
- })
-
- describe('systemMonitorCollapseAtom', () => {
- it('should initialize as false', () => {
- const { result } = renderHook(() =>
- useAtom(SystemBarAtoms.systemMonitorCollapseAtom)
- )
- expect(result.current[0]).toBe(false)
- })
-
- it('should update correctly', () => {
- const { result } = renderHook(() =>
- useAtom(SystemBarAtoms.systemMonitorCollapseAtom)
- )
- act(() => {
- result.current[1](true)
- })
- expect(result.current[0]).toBe(true)
- })
- })
-})
diff --git a/web/helpers/atoms/SystemBar.atom.ts b/web/helpers/atoms/SystemBar.atom.ts
deleted file mode 100644
index ba91364ba..000000000
--- a/web/helpers/atoms/SystemBar.atom.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { atom } from 'jotai'
-
-export const totalRamAtom = atom(0)
-export const usedRamAtom = atom(0)
-
-export const cpuUsageAtom = atom(0)
-export const ramUtilitizedAtom = atom(0)
-
-export const gpusAtom = atom[]>([])
-
-export const nvidiaTotalVramAtom = atom(0)
-export const availableVramAtom = atom(0)
-export const systemMonitorCollapseAtom = atom(false)
diff --git a/web/helpers/atoms/Thread.atom.test.ts b/web/helpers/atoms/Thread.atom.test.ts
deleted file mode 100644
index cc88dd66e..000000000
--- a/web/helpers/atoms/Thread.atom.test.ts
+++ /dev/null
@@ -1,187 +0,0 @@
-// Thread.atom.test.ts
-
-import { act, renderHook } from '@testing-library/react'
-import * as ThreadAtoms from './Thread.atom'
-import { useAtom, useAtomValue, useSetAtom } from 'jotai'
-
-describe('Thread.atom.ts', () => {
- afterEach(() => {
- jest.clearAllMocks()
- })
-
- describe('threadStatesAtom', () => {
- it('should initialize as an empty object', () => {
- const { result: threadStatesAtom } = renderHook(() =>
- useAtom(ThreadAtoms.threadsAtom)
- )
- expect(threadStatesAtom.current[0]).toEqual([])
- })
- })
-
- describe('threadsAtom', () => {
- it('should initialize as an empty array', () => {
- const { result: threadsAtom } = renderHook(() =>
- useAtom(ThreadAtoms.threadsAtom)
- )
- expect(threadsAtom.current[0]).toEqual([])
- })
- })
-
- describe('threadDataReadyAtom', () => {
- it('should initialize as false', () => {
- const { result: threadDataReadyAtom } = renderHook(() =>
- useAtom(ThreadAtoms.threadsAtom)
- )
- expect(threadDataReadyAtom.current[0]).toEqual([])
- })
- })
-
- describe('activeThreadIdAtom', () => {
- it('should set and get active thread id', () => {
- const { result: getAtom } = renderHook(() =>
- useAtomValue(ThreadAtoms.getActiveThreadIdAtom)
- )
- const { result: setAtom } = renderHook(() =>
- useSetAtom(ThreadAtoms.setActiveThreadIdAtom)
- )
-
- expect(getAtom.current).toBeUndefined()
-
- act(() => {
- setAtom.current('thread-1')
- })
-
- expect(getAtom.current).toBe('thread-1')
- })
- })
-
- describe('activeThreadAtom', () => {
- it('should return the active thread', () => {
- const { result: threadsAtom } = renderHook(() =>
- useAtom(ThreadAtoms.threadsAtom)
- )
- const { result: setActiveThreadId } = renderHook(() =>
- useSetAtom(ThreadAtoms.setActiveThreadIdAtom)
- )
- const { result: activeThread } = renderHook(() =>
- useAtomValue(ThreadAtoms.activeThreadAtom)
- )
-
- act(() => {
- threadsAtom.current[1]([
- { id: 'thread-1', title: 'Test Thread' },
- ] as any)
- setActiveThreadId.current('thread-1')
- })
-
- expect(activeThread.current).toEqual({
- id: 'thread-1',
- title: 'Test Thread',
- })
- })
- })
-
- describe('updateThreadAtom', () => {
- it('should update an existing thread', () => {
- const { result: threadsAtom } = renderHook(() =>
- useAtom(ThreadAtoms.threadsAtom)
- )
- const { result: updateThread } = renderHook(() =>
- useSetAtom(ThreadAtoms.updateThreadAtom)
- )
-
- act(() => {
- threadsAtom.current[1]([
- {
- id: 'thread-1',
- title: 'Old Title',
- updated: new Date('2023-01-01').toISOString(),
- },
- {
- id: 'thread-2',
- title: 'Thread 2',
- updated: new Date('2023-01-02').toISOString(),
- },
- ] as any)
- })
-
- act(() => {
- updateThread.current({
- id: 'thread-1',
- title: 'New Title',
- updated: new Date('2023-01-03').toISOString(),
- } as any)
- })
-
- expect(threadsAtom.current[0]).toEqual([
- {
- id: 'thread-1',
- title: 'New Title',
- updated: new Date('2023-01-03').toISOString(),
- },
- {
- id: 'thread-2',
- title: 'Thread 2',
- updated: new Date('2023-01-02').toISOString(),
- },
- ])
- })
- })
-
- describe('setThreadModelParamsAtom', () => {
- it('should set thread model params', () => {
- const { result: paramsAtom } = renderHook(() =>
- useAtom(ThreadAtoms.threadModelParamsAtom)
- )
- const { result: setParams } = renderHook(() =>
- useSetAtom(ThreadAtoms.setThreadModelParamsAtom)
- )
-
- act(() => {
- setParams.current('thread-1', { modelName: 'gpt-3' } as any)
- })
-
- expect(paramsAtom.current[0]).toEqual({
- 'thread-1': { modelName: 'gpt-3' },
- })
- })
- })
-
- describe('deleteThreadStateAtom', () => {
- it('should delete a thread state', () => {
- const { result: statesAtom } = renderHook(() =>
- useAtom(ThreadAtoms.threadStatesAtom)
- )
- const { result: deleteState } = renderHook(() =>
- useSetAtom(ThreadAtoms.deleteThreadStateAtom)
- )
-
- act(() => {
- statesAtom.current[1]({
- 'thread-1': { lastMessage: 'Hello' },
- 'thread-2': { lastMessage: 'Hi' },
- } as any)
- })
-
- act(() => {
- deleteState.current('thread-1')
- })
-
- expect(statesAtom.current[0]).toEqual({
- 'thread-2': { lastMessage: 'Hi' },
- })
- })
- })
-
- describe('modalActionThreadAtom', () => {
- it('should initialize with undefined values', () => {
- const { result } = renderHook(() =>
- useAtomValue(ThreadAtoms.modalActionThreadAtom)
- )
- expect(result.current).toEqual({
- showModal: undefined,
- thread: undefined,
- })
- })
- })
-})
diff --git a/web/helpers/atoms/Thread.atom.ts b/web/helpers/atoms/Thread.atom.ts
deleted file mode 100644
index 578ec6ac6..000000000
--- a/web/helpers/atoms/Thread.atom.ts
+++ /dev/null
@@ -1,300 +0,0 @@
-import { Thread, ThreadContent, ThreadState } from '@janhq/core'
-
-import { atom } from 'jotai'
-import { atomWithStorage, selectAtom } from 'jotai/utils'
-
-import { ModelParams } from '@/types/model'
-
-/**
- * Thread Modal Action Enum
- */
-export enum ThreadModalAction {
- Clean = 'clean',
- Delete = 'delete',
- DeleteAll = 'deleteAll',
- EditTitle = 'edit-title',
-}
-
-const ACTIVE_SETTING_INPUT_BOX = 'activeSettingInputBox'
-
-/**
- * Enum for the keys used to store models in the local storage.
- */
-enum ThreadStorageAtomKeys {
- ThreadStates = 'threadStates',
- ThreadList = 'threadList',
- ThreadListReady = 'threadListReady',
- DisabledTools = 'disabledTools',
-}
-
-//// Threads Atom
-/**
- * Stores all thread states for the current user
- */
-export const threadStatesAtom = atomWithStorage>(
- ThreadStorageAtomKeys.ThreadStates,
- {}
-)
-
-/**
- * Returns whether there is a thread waiting for response or not
- */
-const isWaitingForResponseAtom = selectAtom(threadStatesAtom, (threads) =>
- Object.values(threads).some((t) => t.waitingForResponse)
-)
-
-/**
- * Combine 2 states to reduce rerender
- * 1. isWaitingForResponse
- * 2. isGenerating
- */
-export const isBlockingSendAtom = atom(
- (get) => get(isWaitingForResponseAtom) || get(isGeneratingResponseAtom)
-)
-
-/**
- * Stores all threads for the current user
- */
-export const threadsAtom = atomWithStorage(
- ThreadStorageAtomKeys.ThreadList,
- []
-)
-
-/**
- * Whether thread data is ready or not
- * */
-export const threadDataReadyAtom = atomWithStorage(
- ThreadStorageAtomKeys.ThreadListReady,
- false
-)
-
-/**
- * Store model params at thread level settings
- */
-export const threadModelParamsAtom = atom>({})
-
-/**
- * Store the tool call approval for thread id
- */
-export const approvedThreadToolsAtom = atom>({})
-
-/**
- * Store the tool call disabled for thread id
- */
-export const disabledThreadToolsAtom = atomWithStorage(
- ThreadStorageAtomKeys.DisabledTools,
- []
-)
-
-//// End Thread Atom
-
-/// Active Thread Atom
-/**
- * Stores the current active thread id.
- */
-const activeThreadIdAtom = atom(undefined)
-
-/**
- * Get the active thread id
- */
-export const getActiveThreadIdAtom = atom((get) => get(activeThreadIdAtom))
-
-/**
- * Set the active thread id
- */
-export const setActiveThreadIdAtom = atom(
- null,
- (_get, set, threadId: string | undefined) => set(activeThreadIdAtom, threadId)
-)
-
-/**
- * Get the current active thread metadata
- */
-export const activeThreadAtom = atom((get) =>
- get(threadsAtom).find((c) => c.id === get(getActiveThreadIdAtom))
-)
-
-/**
- * Get the active thread state
- */
-export const activeThreadStateAtom = atom((get) => {
- const threadId = get(activeThreadIdAtom)
- if (!threadId) {
- console.debug('Active thread id is undefined')
- return undefined
- }
-
- return get(threadStatesAtom)[threadId]
-})
-
-/**
- * Get the active thread model params
- */
-export const getActiveThreadModelParamsAtom = atom(
- (get) => {
- const threadId = get(activeThreadIdAtom)
- if (!threadId) {
- console.debug('Active thread id is undefined')
- return undefined
- }
-
- return get(threadModelParamsAtom)[threadId]
- }
-)
-/// End Active Thread Atom
-
-/// Threads State Atom
-export const engineParamsUpdateAtom = atom(false)
-
-/**
- * Whether the thread is waiting to send a message
- */
-export const waitingToSendMessage = atom(undefined)
-
-/**
- * Whether the thread is generating a response
- */
-export const isGeneratingResponseAtom = atom(undefined)
-
-/**
- * Create a new thread and add it to the thread list
- */
-export const createNewThreadAtom = atom(null, (get, set, newThread: Thread) => {
- // create thread state for this new thread
- const currentState = { ...get(threadStatesAtom) }
-
- const threadState: ThreadState = {
- hasMore: false,
- waitingForResponse: false,
- lastMessage: undefined,
- }
- currentState[newThread.id] = threadState
- set(threadStatesAtom, currentState)
-
- // add the new thread on top of the thread list to the state
- const threads = get(threadsAtom)
- set(threadsAtom, [newThread, ...threads])
-})
-
-/**
- * Remove a thread state from the atom
- */
-export const deleteThreadStateAtom = atom(
- null,
- (get, set, threadId: string) => {
- const currentState = { ...get(threadStatesAtom) }
- delete currentState[threadId]
- set(threadStatesAtom, currentState)
- }
-)
-
-/**
- * Update the thread state with the new state
- */
-export const updateThreadWaitingForResponseAtom = atom(
- null,
- (get, set, threadId: string, waitingForResponse: boolean) => {
- const currentState = { ...get(threadStatesAtom) }
- currentState[threadId] = {
- ...currentState[threadId],
- waitingForResponse,
- error: undefined,
- }
- set(threadStatesAtom, currentState)
- }
-)
-
-/**
- * Reset the thread waiting for response state
- */
-export const resetThreadWaitingForResponseAtom = atom(null, (get, set) => {
- const currentState = { ...get(threadStatesAtom) }
- Object.keys(currentState).forEach((threadId) => {
- currentState[threadId] = {
- ...currentState[threadId],
- waitingForResponse: false,
- error: undefined,
- }
- })
- set(threadStatesAtom, currentState)
-})
-
-/**
- * Reset all generating states
- **/
-export const resetGeneratingResponseAtom = atom(null, (get, set) => {
- set(resetThreadWaitingForResponseAtom)
- set(isGeneratingResponseAtom, false)
-})
-
-/**
- * Update the thread last message
- */
-export const updateThreadStateLastMessageAtom = atom(
- null,
- (get, set, threadId: string, lastContent?: ThreadContent[]) => {
- const currentState = { ...get(threadStatesAtom) }
- const lastMessage = lastContent?.[0]?.text?.value ?? ''
- currentState[threadId] = {
- ...currentState[threadId],
- lastMessage,
- }
- set(threadStatesAtom, currentState)
- }
-)
-
-/**
- * Update a thread with the new thread metadata
- */
-export const updateThreadAtom = atom(
- null,
- (get, set, updatedThread: Thread) => {
- const threads: Thread[] = get(threadsAtom).map((c) =>
- c.id === updatedThread.id ? updatedThread : c
- )
-
- // sort new threads based on updated at
- threads.sort((a, b) => {
- return ((a.metadata?.updated_at as number) ?? 0) >
- ((b.metadata?.updated_at as number) ?? 0)
- ? -1
- : 1
- })
- set(threadsAtom, threads)
- }
-)
-
-/**
- * Update the thread model params
- */
-export const setThreadModelParamsAtom = atom(
- null,
- (get, set, threadId: string, params: ModelParams) => {
- const currentState = { ...get(threadModelParamsAtom) }
- currentState[threadId] = params
- set(threadModelParamsAtom, currentState)
- }
-)
-
-/**
- * Settings input box active state
- */
-export const activeSettingInputBoxAtom = atomWithStorage(
- ACTIVE_SETTING_INPUT_BOX,
- true,
- undefined,
- { getOnInit: true }
-)
-
-/**
- * Whether thread is presenting a Modal or not
- */
-export const modalActionThreadAtom = atom<{
- showModal: ThreadModalAction | undefined
- thread: Thread | undefined
-}>({
- showModal: undefined,
- thread: undefined,
-})
-
-/// Ebd Threads State Atom
diff --git a/web/helpers/atoms/ThreadRightPanel.atom.test.ts b/web/helpers/atoms/ThreadRightPanel.atom.test.ts
deleted file mode 100644
index 162b059fd..000000000
--- a/web/helpers/atoms/ThreadRightPanel.atom.test.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-
-import { activeTabThreadRightPanelAtom } from './ThreadRightPanel.atom';
-
-test('activeTabThreadRightPanelAtom can be imported', () => {
- expect(activeTabThreadRightPanelAtom).toBeDefined();
-});
diff --git a/web/helpers/atoms/ThreadRightPanel.atom.ts b/web/helpers/atoms/ThreadRightPanel.atom.ts
deleted file mode 100644
index 904e08552..000000000
--- a/web/helpers/atoms/ThreadRightPanel.atom.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { atom } from 'jotai'
-
-// Store tabs menu active state
-export const activeTabThreadRightPanelAtom = atom(
- 'assistant'
-)
diff --git a/web/hooks/useActiveModel.ts b/web/hooks/useActiveModel.ts
deleted file mode 100644
index 0031c13aa..000000000
--- a/web/hooks/useActiveModel.ts
+++ /dev/null
@@ -1,170 +0,0 @@
-import { useCallback, useEffect, useRef } from 'react'
-
-import { EngineManager, InferenceEngine, Model } from '@janhq/core'
-import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'
-
-import { toaster } from '@/containers/Toast'
-
-import { LAST_USED_MODEL_ID } from './useRecommendedModel'
-
-import { vulkanEnabledAtom } from '@/helpers/atoms/AppConfig.atom'
-import { activeAssistantAtom } from '@/helpers/atoms/Assistant.atom'
-import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
-
-export const activeModelAtom = atom(undefined)
-export const loadModelErrorAtom = atom(undefined)
-
-type ModelState = {
- state: string
- loading: boolean
- model?: Model
-}
-
-export const stateModelAtom = atom({
- state: 'start',
- loading: false,
- model: undefined,
-})
-
-export function useActiveModel() {
- const [activeModel, setActiveModel] = useAtom(activeModelAtom)
- const [stateModel, setStateModel] = useAtom(stateModelAtom)
- const downloadedModels = useAtomValue(downloadedModelsAtom)
- const setLoadModelError = useSetAtom(loadModelErrorAtom)
- const pendingModelLoad = useRef(false)
- const isVulkanEnabled = useAtomValue(vulkanEnabledAtom)
- const activeAssistant = useAtomValue(activeAssistantAtom)
-
- const downloadedModelsRef = useRef([])
-
- useEffect(() => {
- downloadedModelsRef.current = downloadedModels
- }, [downloadedModels])
-
- const startModel = async (modelId: string, abortable: boolean = true) => {
- if (
- (activeModel && activeModel.id === modelId) ||
- (stateModel.model?.id === modelId && stateModel.loading)
- ) {
- console.debug(`Model ${modelId} is already initialized. Ignore..`)
- return Promise.resolve()
- }
-
- if (activeModel) {
- await stopModel(activeModel)
- }
- pendingModelLoad.current = true
-
- let model = downloadedModelsRef?.current.find((e) => e.id === modelId)
-
- setLoadModelError(undefined)
-
- setActiveModel(undefined)
-
- setStateModel({ state: 'start', loading: true, model })
-
- if (!model) {
- toaster({
- title: `Model ${modelId} not found!`,
- description: `Please download the model first.`,
- type: 'warning',
- })
- setStateModel(() => ({
- state: 'start',
- loading: false,
- model: undefined,
- }))
-
- return Promise.reject(`Model ${modelId} not found!`)
- }
-
- /// Apply thread model settings
- if (activeAssistant?.model.id === modelId) {
- model = {
- ...model,
- settings: {
- ...model.settings,
- ...activeAssistant?.model.settings,
- },
- }
- }
-
- if (isVulkanEnabled) {
- // @ts-expect-error flash_attn is newly added and will be migrate to cortex in the future
- model.settings['flash_attn'] = false
- }
-
- localStorage.setItem(LAST_USED_MODEL_ID, model.id)
- const engine = EngineManager.instance().get(InferenceEngine.cortex)
- return engine
- ?.loadModel(model)
- .then(() => {
- setActiveModel(model)
- setStateModel(() => ({
- state: 'stop',
- loading: false,
- model,
- }))
- toaster({
- title: 'Success!',
- description: `Model ${model.id} has been started.`,
- type: 'success',
- })
- })
- .catch((error) => {
- setStateModel(() => ({
- state: 'start',
- loading: false,
- undefined,
- }))
-
- if (!pendingModelLoad.current && abortable) {
- return Promise.reject(new Error('aborted'))
- }
-
- toaster({
- title: 'Failed!',
- description: `Model ${model.id} failed to start. ${error.message ?? ''}`,
- type: 'error',
- })
- setLoadModelError(error.message ?? error)
- return Promise.reject(error)
- })
- }
-
- const stopModel = useCallback(
- async (model?: Model) => {
- const stoppingModel = model ?? activeModel ?? stateModel.model
- if (!stoppingModel || (stateModel.state === 'stop' && stateModel.loading))
- return
-
- const engine = EngineManager.instance().get(InferenceEngine.cortex)
- return engine
- ?.unloadModel(stoppingModel)
- .catch((e) => console.error(e))
- .then(() => {
- setActiveModel(undefined)
- setStateModel({ state: 'start', loading: false, model: undefined })
- pendingModelLoad.current = false
- })
- },
- [activeModel, setStateModel, setActiveModel, stateModel]
- )
-
- const stopInference = useCallback(async () => {
- // Loading model
- if (stateModel.loading) {
- stopModel()
- return
- }
- // if (!activeModel) return
-
- // const engine = EngineManager.instance().get(InferenceEngine.cortex)
- // engine?.stopInference()
- // NOTE: this only works correctly if there is only 1 concurrent request
- // at any point in time, which is a reasonable assumption to have.
- EngineManager.instance().controller?.abort()
- }, [activeModel, stateModel, stopModel])
-
- return { activeModel, startModel, stopModel, stopInference, stateModel }
-}
diff --git a/web/hooks/useApp.ts b/web/hooks/useApp.ts
deleted file mode 100644
index f30b9e3c5..000000000
--- a/web/hooks/useApp.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { extensionManager } from '@/extension'
-
-export function useApp() {
- async function relaunch() {
- const extensions = extensionManager.getAll()
- await Promise.all(extensions.map((e) => e.onUnload()))
- window.core?.api?.relaunch()
- }
- return { relaunch }
-}
diff --git a/web/hooks/useAssistant.test.ts b/web/hooks/useAssistant.test.ts
deleted file mode 100644
index e029bb7f6..000000000
--- a/web/hooks/useAssistant.test.ts
+++ /dev/null
@@ -1,95 +0,0 @@
-import { renderHook, act } from '@testing-library/react'
-import { useSetAtom } from 'jotai'
-import { events, AssistantEvent, ExtensionTypeEnum } from '@janhq/core'
-
-// Mock dependencies
-jest.mock('jotai', () => ({
- useAtomValue: jest.fn(),
- useSetAtom: jest.fn(),
- useAtom: jest.fn(),
- atom: jest.fn(),
-}))
-jest.mock('@janhq/core')
-jest.mock('@/extension')
-
-import useAssistants from './useAssistants'
-import { extensionManager } from '@/extension'
-
-// Mock data
-const mockAssistants = [
- { id: 'assistant-1', name: 'Assistant 1' },
- { id: 'assistant-2', name: 'Assistant 2' },
-]
-
-const mockAssistantExtension = {
- getAssistants: jest.fn().mockResolvedValue(mockAssistants),
-} as any
-
-describe('useAssistants', () => {
- beforeEach(() => {
- jest.clearAllMocks()
- jest.spyOn(extensionManager, 'get').mockReturnValue(mockAssistantExtension)
- })
-
- it('should fetch and set assistants on mount', async () => {
- const mockSetAssistants = jest.fn()
- ;(useSetAtom as jest.Mock).mockReturnValue(mockSetAssistants)
-
- renderHook(() => useAssistants())
-
- // Wait for useEffect to complete
- await act(async () => {})
-
- expect(mockAssistantExtension.getAssistants).toHaveBeenCalled()
- expect(mockSetAssistants).toHaveBeenCalledWith(mockAssistants)
- })
-
- it('should update assistants when AssistantEvent.OnAssistantsUpdate is emitted', async () => {
- const mockSetAssistants = jest.fn()
- ;(useSetAtom as jest.Mock).mockReturnValue(mockSetAssistants)
-
- renderHook(() => useAssistants())
-
- // Wait for initial useEffect to complete
- await act(async () => {})
-
- // Clear previous calls
- mockSetAssistants.mockClear()
-
- // Simulate AssistantEvent.OnAssistantsUpdate event
- await act(async () => {
- events.emit(AssistantEvent.OnAssistantsUpdate, '')
- })
-
- expect(mockAssistantExtension.getAssistants).toHaveBeenCalledTimes(1)
- })
-
- it('should unsubscribe from events on unmount', async () => {
- const { unmount } = renderHook(() => useAssistants())
-
- // Wait for useEffect to complete
- await act(async () => {})
-
- const offSpy = jest.spyOn(events, 'off')
-
- unmount()
-
- expect(offSpy).toHaveBeenCalledWith(
- AssistantEvent.OnAssistantsUpdate,
- expect.any(Function)
- )
- })
-
- it('should handle case when AssistantExtension is not available', async () => {
- const mockSetAssistants = jest.fn()
- ;(useSetAtom as jest.Mock).mockReturnValue(mockSetAssistants)
- ;(extensionManager.get as jest.Mock).mockReturnValue(undefined)
-
- renderHook(() => useAssistants())
-
- // Wait for useEffect to complete
- await act(async () => {})
-
- expect(mockSetAssistants).toHaveBeenCalledWith([])
- })
-})
diff --git a/web/hooks/useAssistants.ts b/web/hooks/useAssistants.ts
deleted file mode 100644
index 61679bce5..000000000
--- a/web/hooks/useAssistants.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import { useCallback, useEffect } from 'react'
-
-import {
- Assistant,
- AssistantEvent,
- AssistantExtension,
- ExtensionTypeEnum,
- events,
-} from '@janhq/core'
-
-import { useSetAtom } from 'jotai'
-
-import { extensionManager } from '@/extension'
-import { assistantsAtom } from '@/helpers/atoms/Assistant.atom'
-
-const useAssistants = () => {
- const setAssistants = useSetAtom(assistantsAtom)
-
- const getData = useCallback(async () => {
- const assistants = await getLocalAssistants()
- setAssistants(assistants)
- }, [setAssistants])
-
- useEffect(() => {
- getData()
-
- events.on(AssistantEvent.OnAssistantsUpdate, () => getData())
- return () => {
- events.off(AssistantEvent.OnAssistantsUpdate, () => getData())
- }
- }, [getData])
-}
-
-const getLocalAssistants = async (): Promise =>
- extensionManager
- .get(ExtensionTypeEnum.Assistant)
- ?.getAssistants() ?? []
-
-export default useAssistants
diff --git a/web/hooks/useClipboard.test.ts b/web/hooks/useClipboard.test.ts
deleted file mode 100644
index a79f8132b..000000000
--- a/web/hooks/useClipboard.test.ts
+++ /dev/null
@@ -1,105 +0,0 @@
-import { renderHook, act } from '@testing-library/react'
-import { useClipboard } from './useClipboard'
-
-describe('useClipboard', () => {
- let originalClipboard: any
-
- beforeAll(() => {
- originalClipboard = { ...global.navigator.clipboard }
- const mockClipboard = {
- writeText: jest.fn(() => Promise.resolve()),
- }
- // @ts-ignore
- global.navigator.clipboard = mockClipboard
- })
-
- afterAll(() => {
- // @ts-ignore
- global.navigator.clipboard = originalClipboard
- })
-
- beforeEach(() => {
- jest.useFakeTimers()
- })
-
- afterEach(() => {
- jest.clearAllTimers()
- jest.useRealTimers()
- })
-
- it('should copy text to clipboard', async () => {
- const { result } = renderHook(() => useClipboard())
-
- await act(async () => {
- result.current.copy('Test text')
- })
-
- expect(navigator.clipboard.writeText).toHaveBeenCalledWith('Test text')
- expect(result.current.copied).toBe(true)
- expect(result.current.error).toBe(null)
- })
-
- it('should set copied to false after timeout', async () => {
- const { result } = renderHook(() => useClipboard({ timeout: 1000 }))
-
- await act(async () => {
- result.current.copy('Test text')
- })
-
- expect(result.current.copied).toBe(true)
-
- act(() => {
- jest.advanceTimersByTime(1000)
- })
-
- expect(result.current.copied).toBe(false)
- })
-
- it('should handle clipboard errors', async () => {
- const mockError = new Error('Clipboard error')
- // @ts-ignore
- navigator.clipboard.writeText.mockRejectedValueOnce(mockError)
-
- const { result } = renderHook(() => useClipboard())
-
- await act(async () => {
- result.current.copy('Test text')
- })
-
- expect(result.current.error).toEqual(mockError)
- expect(result.current.copied).toBe(false)
- })
-
- it('should reset state', async () => {
- const { result } = renderHook(() => useClipboard())
-
- await act(async () => {
- result.current.copy('Test text')
- })
-
- expect(result.current.copied).toBe(true)
-
- act(() => {
- result.current.reset()
- })
-
- expect(result.current.copied).toBe(false)
- expect(result.current.error).toBe(null)
- })
-
- it('should handle missing clipboard API', () => {
- // @ts-ignore
- delete global.navigator.clipboard
-
- const { result } = renderHook(() => useClipboard())
-
- act(() => {
- result.current.copy('Test text')
- })
-
- expect(result.current.error).toEqual(
- new Error('useClipboard: navigator.clipboard is not supported')
- )
- expect(result.current.copied).toBe(false)
- })
-})
diff --git a/web/hooks/useClipboard.ts b/web/hooks/useClipboard.ts
deleted file mode 100644
index 919fe9183..000000000
--- a/web/hooks/useClipboard.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-/* eslint-disable @typescript-eslint/no-explicit-any */
-import { useState } from 'react'
-
-export function useClipboard({ timeout = 2000 } = {}) {
- const [error, setError] = useState(null)
- const [copied, setCopied] = useState(false)
- const [copyTimeout, setCopyTimeout] = useState(null)
-
- const handleCopyResult = (value: boolean) => {
- window.clearTimeout(copyTimeout!)
- setCopyTimeout(window.setTimeout(() => setCopied(false), timeout))
- setCopied(value)
- }
-
- const copy = (valueToCopy: any) => {
- if ('clipboard' in navigator) {
- navigator.clipboard
- .writeText(valueToCopy)
- .then(() => handleCopyResult(true))
- .catch((err) => setError(err))
- } else {
- setError(new Error('useClipboard: navigator.clipboard is not supported'))
- }
- }
-
- const reset = () => {
- setCopied(false)
- setError(null)
- window.clearTimeout(copyTimeout!)
- }
-
- return { copy, reset, error, copied }
-}
diff --git a/web/hooks/useConfigurations.test.ts b/web/hooks/useConfigurations.test.ts
deleted file mode 100644
index 1fab89f59..000000000
--- a/web/hooks/useConfigurations.test.ts
+++ /dev/null
@@ -1,137 +0,0 @@
-import { renderHook, act } from '@testing-library/react'
-import { useConfigurations } from './useConfigurations'
-import { useAtomValue } from 'jotai'
-import { extensionManager } from '@/extension'
-
-// Mock dependencies
-jest.mock('jotai', () => {
- const originalModule = jest.requireActual('jotai')
- return {
- ...originalModule,
- useAtomValue: jest.fn(),
- }
-})
-
-jest.mock('@/extension', () => ({
- extensionManager: {
- get: jest.fn(),
- },
-}))
-
-describe('useConfigurations', () => {
- beforeEach(() => {
- jest.clearAllMocks()
- })
-
- it('should call configurePullOptions with correct proxy settings when proxy is enabled', () => {
- // Explicitly set mock return values for each call
- (useAtomValue as jest.Mock)
- .mockReturnValueOnce(true) // proxyEnabled
- .mockReturnValueOnce('http://proxy.example.com') // proxyUrl
- .mockReturnValueOnce('') // proxyIgnoreSSL
- .mockReturnValueOnce(true) // verifyProxySSL
- .mockReturnValueOnce(true) // verifyProxyHostSSL
- .mockReturnValueOnce(true) // verifyPeerSSL
- .mockReturnValueOnce(true) // verifyHostSSL
- .mockReturnValueOnce('') // noProxy
- .mockReturnValueOnce('username') // proxyUsername
- .mockReturnValueOnce('password') // proxyPassword
-
-
- const mockConfigurePullOptions = jest.fn()
- ;(extensionManager.get as jest.Mock).mockReturnValue({
- configurePullOptions: mockConfigurePullOptions,
- })
-
- const { result } = renderHook(() => useConfigurations())
-
- act(() => {
- result.current.configurePullOptions()
- })
-
- expect(mockConfigurePullOptions).toHaveBeenCalledWith({
- proxy_username: 'username',
- proxy_password: 'password',
- proxy_url: 'http://proxy.example.com',
- verify_proxy_ssl: true,
- verify_proxy_host_ssl: true,
- verify_peer_ssl: true,
- verify_host_ssl: true,
- no_proxy: '',
- })
- })
-
- it('should call configurePullOptions with empty proxy settings when proxy is disabled', () => {
- // Mock atom values
- ;(useAtomValue as jest.Mock)
- .mockReturnValueOnce(false) // proxyEnabled
- .mockReturnValueOnce('') // proxyUrl
- .mockReturnValueOnce(false) // proxyIgnoreSSL
- .mockReturnValueOnce('') // noProxy
- .mockReturnValueOnce('') // proxyUsername
- .mockReturnValueOnce('') // proxyPassword
- .mockReturnValueOnce(false) // verifyProxySSL
- .mockReturnValueOnce(false) // verifyProxyHostSSL
- .mockReturnValueOnce(false) // verifyPeerSSL
- .mockReturnValueOnce(false) // verifyHostSSL
-
- const mockConfigurePullOptions = jest.fn()
- ;(extensionManager.get as jest.Mock).mockReturnValue({
- configurePullOptions: mockConfigurePullOptions,
- })
-
- const { result } = renderHook(() => useConfigurations())
-
- act(() => {
- result.current.configurePullOptions()
- })
-
- expect(mockConfigurePullOptions).toHaveBeenCalledWith({
- proxy_username: '',
- proxy_password: '',
- proxy_url: '',
- verify_proxy_ssl: false,
- verify_proxy_host_ssl: false,
- verify_peer_ssl: false,
- verify_host_ssl: false,
- no_proxy: '',
- })
- })
-
- it('should set all verify SSL to false when proxyIgnoreSSL is true', () => {
- // Mock atom values
- ;(useAtomValue as jest.Mock)
- .mockReturnValueOnce(true) // proxyEnabled
- .mockReturnValueOnce('http://proxy.example.com') // proxyUrl
- .mockReturnValueOnce(true) // proxyIgnoreSSL
- .mockReturnValueOnce(true) // verifyProxySSL
- .mockReturnValueOnce(true) // verifyProxyHostSSL
- .mockReturnValueOnce(true) // verifyPeerSSL
- .mockReturnValueOnce(true) // verifyHostSSL
- .mockReturnValueOnce('') // noProxy
- .mockReturnValueOnce('username') // proxyUsername
- .mockReturnValueOnce('password') // proxyPassword
-
- const mockConfigurePullOptions = jest.fn()
- ;(extensionManager.get as jest.Mock).mockReturnValue({
- configurePullOptions: mockConfigurePullOptions,
- })
-
- const { result } = renderHook(() => useConfigurations())
-
- act(() => {
- result.current.configurePullOptions()
- })
-
- expect(mockConfigurePullOptions).toHaveBeenCalledWith({
- proxy_username: 'username',
- proxy_password: 'password',
- proxy_url: 'http://proxy.example.com',
- verify_proxy_ssl: false,
- verify_proxy_host_ssl: false,
- verify_peer_ssl: false,
- verify_host_ssl: false,
- no_proxy: '',
- })
- })
-})
\ No newline at end of file
diff --git a/web/hooks/useConfigurations.ts b/web/hooks/useConfigurations.ts
deleted file mode 100644
index 0bd96e760..000000000
--- a/web/hooks/useConfigurations.ts
+++ /dev/null
@@ -1,81 +0,0 @@
-import { useCallback, useEffect } from 'react'
-
-import { ExtensionTypeEnum, ModelExtension } from '@janhq/core'
-import { useAtomValue } from 'jotai'
-
-import { extensionManager } from '@/extension'
-import {
- ignoreSslAtom,
- noProxyAtom,
- proxyAtom,
- proxyEnabledAtom,
- proxyPasswordAtom,
- proxyUsernameAtom,
- verifyHostSslAtom,
- verifyPeerSslAtom,
- verifyProxyHostSslAtom,
- verifyProxySslAtom,
-} from '@/helpers/atoms/AppConfig.atom'
-
-export const useConfigurations = () => {
- const proxyEnabled = useAtomValue(proxyEnabledAtom)
- const proxyUrl = useAtomValue(proxyAtom)
- const proxyIgnoreSSL = useAtomValue(ignoreSslAtom)
- const verifyProxySSL = useAtomValue(verifyProxySslAtom)
- const verifyProxyHostSSL = useAtomValue(verifyProxyHostSslAtom)
- const verifyPeerSSL = useAtomValue(verifyPeerSslAtom)
- const verifyHostSSL = useAtomValue(verifyHostSslAtom)
- const noProxy = useAtomValue(noProxyAtom)
- const proxyUsername = useAtomValue(proxyUsernameAtom)
- const proxyPassword = useAtomValue(proxyPasswordAtom)
-
- const configurePullOptions = useCallback(() => {
- extensionManager
- .get(ExtensionTypeEnum.Model)
- ?.configurePullOptions(
- proxyEnabled
- ? {
- proxy_username: proxyUsername,
- proxy_password: proxyPassword,
- proxy_url: proxyUrl,
- verify_proxy_ssl: proxyIgnoreSSL ? false : verifyProxySSL,
- verify_proxy_host_ssl: proxyIgnoreSSL
- ? false
- : verifyProxyHostSSL,
- verify_peer_ssl: proxyIgnoreSSL ? false : verifyPeerSSL,
- verify_host_ssl: proxyIgnoreSSL ? false : verifyHostSSL,
- no_proxy: noProxy,
- }
- : {
- proxy_username: '',
- proxy_password: '',
- proxy_url: '',
- verify_proxy_ssl: false,
- verify_proxy_host_ssl: false,
- verify_peer_ssl: false,
- verify_host_ssl: false,
- no_proxy: '',
- }
- )
- }, [
- proxyEnabled,
- proxyUrl,
- proxyIgnoreSSL,
- noProxy,
- proxyUsername,
- proxyPassword,
- verifyProxySSL,
- verifyProxyHostSSL,
- verifyPeerSSL,
- verifyHostSSL,
- ])
-
- useEffect(() => {
- configurePullOptions()
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [configurePullOptions])
-
- return {
- configurePullOptions,
- }
-}
diff --git a/web/hooks/useCreateNewThread.test.ts b/web/hooks/useCreateNewThread.test.ts
deleted file mode 100644
index 0ef8ef195..000000000
--- a/web/hooks/useCreateNewThread.test.ts
+++ /dev/null
@@ -1,204 +0,0 @@
-// useCreateNewThread.test.ts
-import { renderHook, act } from '@testing-library/react'
-import { useCreateNewThread } from './useCreateNewThread'
-import { useAtomValue, useSetAtom } from 'jotai'
-import { useActiveModel } from './useActiveModel'
-import useRecommendedModel from './useRecommendedModel'
-import useSetActiveThread from './useSetActiveThread'
-import { extensionManager } from '@/extension'
-import { toaster } from '@/containers/Toast'
-
-// Mock the dependencies
-jest.mock('jotai', () => {
- const originalModule = jest.requireActual('jotai')
- return {
- ...originalModule,
- useAtomValue: jest.fn(),
- useSetAtom: jest.fn(),
- }
-})
-jest.mock('./useActiveModel')
-jest.mock('./useRecommendedModel')
-jest.mock('./useSetActiveThread')
-jest.mock('@/extension')
-jest.mock('@/containers/Toast')
-
-describe('useCreateNewThread', () => {
- beforeEach(() => {
- jest.clearAllMocks()
- })
-
- it('should create a new thread', async () => {
- const mockSetAtom = jest.fn()
- ;(useSetAtom as jest.Mock).mockReturnValue(mockSetAtom)
- ;(useAtomValue as jest.Mock).mockReturnValue({
- metadata: {},
- assistants: [
- {
- id: 'assistant1',
- name: 'Assistant 1',
- instructions: undefined,
- },
- ],
- })
- ;(useActiveModel as jest.Mock).mockReturnValue({ stopInference: jest.fn() })
- ;(useRecommendedModel as jest.Mock).mockReturnValue({
- recommendedModel: { id: 'model1', parameters: [], settings: [] },
- downloadedModels: [],
- })
- ;(useSetActiveThread as jest.Mock).mockReturnValue({
- setActiveThread: jest.fn(),
- })
- ;(extensionManager.get as jest.Mock).mockReturnValue({
- saveThread: jest.fn(),
- })
-
- const { result } = renderHook(() => useCreateNewThread())
-
- await act(async () => {
- await result.current.requestCreateNewThread({
- id: 'assistant1',
- name: 'Assistant 1',
- model: {
- id: 'model1',
- parameters: [],
- settings: [],
- },
- } as any)
- })
-
- expect(extensionManager.get).toHaveBeenCalled()
- })
-
- it('should create a new thread with instructions', async () => {
- const mockSetAtom = jest.fn()
- ;(useSetAtom as jest.Mock).mockReturnValue(mockSetAtom)
- ;(useAtomValue as jest.Mock).mockReturnValueOnce(false)
- ;(useAtomValue as jest.Mock).mockReturnValue({
- metadata: {},
- assistants: [
- {
- id: 'assistant1',
- name: 'Assistant 1',
- instructions: 'Hello Jan',
- },
- ],
- })
- ;(useAtomValue as jest.Mock).mockReturnValueOnce(false)
- ;(useActiveModel as jest.Mock).mockReturnValue({ stopInference: jest.fn() })
- ;(useRecommendedModel as jest.Mock).mockReturnValue({
- recommendedModel: { id: 'model1', parameters: [], settings: [] },
- downloadedModels: [],
- })
- ;(useSetActiveThread as jest.Mock).mockReturnValue({
- setActiveThread: jest.fn(),
- })
- ;(extensionManager.get as jest.Mock).mockReturnValue({
- saveThread: jest.fn(),
- })
-
- const { result } = renderHook(() => useCreateNewThread())
-
- await act(async () => {
- await result.current.requestCreateNewThread({
- id: 'assistant1',
- name: 'Assistant 1',
- instructions: 'Hello Jan Assistant',
- model: {
- id: 'model1',
- parameters: [],
- settings: [],
- },
- } as any)
- })
-
- expect(extensionManager.get).toHaveBeenCalled()
- })
-
- it('should create a new thread with previous instructions', async () => {
- const mockSetAtom = jest.fn()
- ;(useSetAtom as jest.Mock).mockReturnValue(mockSetAtom)
- ;(useAtomValue as jest.Mock).mockReturnValueOnce(true)
- ;(useAtomValue as jest.Mock).mockReturnValueOnce({
- metadata: {},
- assistants: [
- {
- id: 'assistant1',
- name: 'Assistant 1',
- instructions: 'Hello Jan',
- },
- ],
- })
- ;(useAtomValue as jest.Mock).mockReturnValueOnce(true)
- ;(useActiveModel as jest.Mock).mockReturnValue({ stopInference: jest.fn() })
- ;(useRecommendedModel as jest.Mock).mockReturnValue({
- recommendedModel: { id: 'model1', parameters: [], settings: [] },
- downloadedModels: [],
- })
- ;(useSetActiveThread as jest.Mock).mockReturnValue({
- setActiveThread: jest.fn(),
- })
- ;(extensionManager.get as jest.Mock).mockReturnValue({
- saveThread: jest.fn(),
- })
-
- const { result } = renderHook(() => useCreateNewThread())
-
- await act(async () => {
- await result.current.requestCreateNewThread({
- id: 'assistant1',
- name: 'Assistant 1',
- model: {
- id: 'model1',
- parameters: [],
- settings: [],
- },
- } as any)
- })
-
- expect(extensionManager.get).toHaveBeenCalled()
- })
-
- it('should show a warning toast if trying to create an empty thread', async () => {
- ;(useAtomValue as jest.Mock).mockReturnValue([{ metadata: {} }]) // Mock an empty thread
- ;(useRecommendedModel as jest.Mock).mockReturnValue({
- recommendedModel: null,
- downloadedModels: [],
- })
-
- const { result } = renderHook(() => useCreateNewThread())
-
- await act(async () => {
- await result.current.requestCreateNewThread({
- id: 'assistant1',
- name: 'Assistant 1',
- tools: [],
- } as any)
- })
-
- expect(toaster).toHaveBeenCalledWith(
- expect.objectContaining({
- title: 'No new thread created.',
- type: 'warning',
- })
- )
- })
-
- it('should update thread metadata', async () => {
- const mockUpdateThread = jest.fn()
- ;(useSetAtom as jest.Mock).mockReturnValue(mockUpdateThread)
- ;(extensionManager.get as jest.Mock).mockReturnValue({
- saveThread: jest.fn(),
- })
-
- const { result } = renderHook(() => useCreateNewThread())
-
- const mockThread = { id: 'thread1', title: 'Test Thread', assistants: [{}] }
-
- await act(async () => {
- await result.current.updateThreadMetadata(mockThread as any)
- })
-
- expect(mockUpdateThread).toHaveBeenCalledWith(mockThread)
- })
-})
diff --git a/web/hooks/useCreateNewThread.ts b/web/hooks/useCreateNewThread.ts
deleted file mode 100644
index 423ce4151..000000000
--- a/web/hooks/useCreateNewThread.ts
+++ /dev/null
@@ -1,200 +0,0 @@
-import { useCallback } from 'react'
-
-import {
- ConversationalExtension,
- ExtensionTypeEnum,
- Thread,
- ThreadAssistantInfo,
- AssistantTool,
- Model,
- Assistant,
-} from '@janhq/core'
-import { useAtom, useAtomValue, useSetAtom } from 'jotai'
-
-import { useDebouncedCallback } from 'use-debounce'
-
-import { fileUploadAtom } from '@/containers/Providers/Jotai'
-
-import { toaster } from '@/containers/Toast'
-
-import useRecommendedModel from './useRecommendedModel'
-import useSetActiveThread from './useSetActiveThread'
-
-import { extensionManager } from '@/extension'
-import { copyOverInstructionEnabledAtom } from '@/helpers/atoms/App.atom'
-
-import { experimentalFeatureEnabledAtom } from '@/helpers/atoms/AppConfig.atom'
-import { activeAssistantAtom } from '@/helpers/atoms/Assistant.atom'
-import { selectedModelAtom } from '@/helpers/atoms/Model.atom'
-import {
- threadsAtom,
- updateThreadAtom,
- setThreadModelParamsAtom,
- createNewThreadAtom,
-} from '@/helpers/atoms/Thread.atom'
-
-export const useCreateNewThread = () => {
- const createNewThread = useSetAtom(createNewThreadAtom)
- const { setActiveThread } = useSetActiveThread()
- const updateThread = useSetAtom(updateThreadAtom)
- const setFileUpload = useSetAtom(fileUploadAtom)
- const setSelectedModel = useSetAtom(selectedModelAtom)
- const setThreadModelParams = useSetAtom(setThreadModelParamsAtom)
- const copyOverInstructionEnabled = useAtomValue(
- copyOverInstructionEnabledAtom
- )
- const [activeAssistant, setActiveAssistant] = useAtom(activeAssistantAtom)
-
- const experimentalEnabled = useAtomValue(experimentalFeatureEnabledAtom)
-
- const threads = useAtomValue(threadsAtom)
-
- const { recommendedModel } = useRecommendedModel()
-
- const selectedModel = useAtomValue(selectedModelAtom)
-
- const requestCreateNewThread = async (
- assistant: (ThreadAssistantInfo & { id: string; name: string }) | Assistant,
- model?: Model | undefined
- ) => {
- const defaultModel = model || selectedModel || recommendedModel
-
- // modify assistant tools when experimental on, retieval toggle enabled in default
- const assistantTools: AssistantTool = {
- type: 'retrieval',
- enabled: true,
- settings: assistant.tools && assistant.tools[0].settings,
- }
-
- // Default context length is 8192
- const contextLength = defaultModel?.settings?.ctx_len
- ? Math.min(8192, defaultModel?.settings?.ctx_len)
- : undefined
-
- const overriddenSettings = {
- ctx_len: contextLength,
- }
-
- // Use ctx length by default
- const overriddenParameters = {
- max_tokens: contextLength
- ? Math.min(defaultModel?.parameters?.max_tokens ?? 8192, contextLength)
- : defaultModel?.parameters?.max_tokens,
- }
-
- const createdAt = Date.now()
- let instructions: string | undefined = assistant.instructions
- if (copyOverInstructionEnabled) {
- instructions = activeAssistant?.instructions ?? undefined
- }
- const assistantInfo: ThreadAssistantInfo = {
- assistant_id: assistant.id,
- assistant_name: assistant.name,
- tools: experimentalEnabled ? [assistantTools] : assistant.tools,
- model: {
- id: defaultModel?.id ?? '*',
- settings: { ...defaultModel?.settings, ...overriddenSettings },
- parameters: { ...defaultModel?.parameters, ...overriddenParameters },
- engine: defaultModel?.engine,
- },
- instructions,
- }
-
- const thread: Partial = {
- object: 'thread',
- title: 'New Thread',
- assistants: [assistantInfo],
- created: createdAt,
- updated: createdAt,
- metadata: {
- title: 'New Thread',
- updated_at: Date.now(),
- },
- }
-
- // add the new thread on top of the thread list to the state
- try {
- const createdThread = await persistNewThread(thread, assistantInfo)
- if (!createdThread) throw 'Thread created failed.'
- createNewThread(createdThread)
-
- setSelectedModel(defaultModel)
- setThreadModelParams(createdThread.id, {
- ...defaultModel?.settings,
- ...defaultModel?.parameters,
- ...overriddenSettings,
- })
-
- // Delete the file upload state
- setFileUpload(undefined)
- setActiveThread(createdThread)
- } catch (ex) {
- return toaster({
- title: 'Thread created failed.',
- description: `Could not create a new thread. Please try again.`,
- type: 'error',
- })
- }
- }
-
- const updateThreadExtension = (thread: Thread) => {
- return extensionManager
- .get(ExtensionTypeEnum.Conversational)
- ?.modifyThread(thread)
- }
-
- const updateAssistantExtension = (
- threadId: string,
- assistant: ThreadAssistantInfo
- ) => {
- return extensionManager
- .get(ExtensionTypeEnum.Conversational)
- ?.modifyThreadAssistant(threadId, assistant)
- }
-
- const updateThreadCallback = useDebouncedCallback(updateThreadExtension, 300)
- const updateAssistantCallback = useDebouncedCallback(
- updateAssistantExtension,
- 300
- )
-
- const updateThreadMetadata = useCallback(
- async (thread: Thread) => {
- updateThread(thread)
-
- updateThreadCallback(thread)
- if (thread.assistants && thread.assistants?.length > 0) {
- setActiveAssistant(thread.assistants[0])
- return updateAssistantCallback(thread.id, thread.assistants[0])
- }
- },
- [
- updateThread,
- setActiveAssistant,
- updateThreadCallback,
- updateAssistantCallback,
- ]
- )
-
- const persistNewThread = async (
- thread: Partial,
- assistantInfo: ThreadAssistantInfo
- ): Promise => {
- return await extensionManager
- .get(ExtensionTypeEnum.Conversational)
- ?.createThread(thread)
- .then(async (thread) => {
- await extensionManager
- .get(ExtensionTypeEnum.Conversational)
- ?.createThreadAssistant(thread.id, assistantInfo)
- .catch(console.error)
- return thread
- })
- .catch(() => undefined)
- }
-
- return {
- requestCreateNewThread,
- updateThreadMetadata,
- }
-}
diff --git a/web/hooks/useDeleteModel.test.ts b/web/hooks/useDeleteModel.test.ts
deleted file mode 100644
index 3ee0926f9..000000000
--- a/web/hooks/useDeleteModel.test.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import { renderHook, act } from '@testing-library/react'
-import { extensionManager } from '@/extension/ExtensionManager'
-import useDeleteModel from './useDeleteModel'
-import { toaster } from '@/containers/Toast'
-import { useSetAtom } from 'jotai'
-
-// Mock the dependencies
-jest.mock('@/extension/ExtensionManager')
-jest.mock('@/containers/Toast')
-jest.mock('jotai', () => ({
- useSetAtom: jest.fn(() => jest.fn()),
- atom: jest.fn(),
-}))
-
-describe('useDeleteModel', () => {
- const mockModel: any = {
- id: 'test-model',
- name: 'Test Model',
- // Add other required properties of Model
- }
-
- const mockDeleteModel = jest.fn()
-
- beforeEach(() => {
- jest.clearAllMocks()
- ;(extensionManager.get as jest.Mock).mockReturnValue({
- deleteModel: mockDeleteModel,
- })
- })
-
- it('should delete a model successfully', async () => {
- const { result } = renderHook(() => useDeleteModel())
-
- await act(async () => {
- await result.current.deleteModel(mockModel)
- })
-
- expect(mockDeleteModel).toHaveBeenCalledWith('test-model')
- expect(toaster).toHaveBeenCalledWith({
- title: 'Model Deletion Successful',
- description: `Model ${mockModel.name} has been successfully deleted.`,
- type: 'success',
- })
- })
-
- it('should call removeDownloadedModel with the model id', async () => {
- const { result } = renderHook(() => useDeleteModel())
-
- await act(async () => {
- await result.current.deleteModel(mockModel)
- })
-
- // Assuming useSetAtom returns a mock function
- ;(useSetAtom as jest.Mock).mockReturnValue(jest.fn())
- expect(useSetAtom).toHaveBeenCalled()
- })
-
- it('should handle errors during model deletion', async () => {
- const error = new Error('Deletion failed')
- mockDeleteModel.mockRejectedValue(error)
-
- const { result } = renderHook(() => useDeleteModel())
-
- await act(async () => {
- await expect(result.current.deleteModel(mockModel)).rejects.toThrow(
- 'Deletion failed'
- )
- })
-
- expect(mockDeleteModel).toHaveBeenCalledWith("test-model")
- expect(toaster).not.toHaveBeenCalled()
- })
-})
diff --git a/web/hooks/useDeleteModel.ts b/web/hooks/useDeleteModel.ts
deleted file mode 100644
index 5621a78b8..000000000
--- a/web/hooks/useDeleteModel.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import { useCallback } from 'react'
-
-import { ExtensionTypeEnum, ModelExtension, Model } from '@janhq/core'
-
-import { useSetAtom } from 'jotai'
-
-import { toaster } from '@/containers/Toast'
-
-import { extensionManager } from '@/extension/ExtensionManager'
-import { removeDownloadedModelAtom } from '@/helpers/atoms/Model.atom'
-
-export default function useDeleteModel() {
- const removeDownloadedModel = useSetAtom(removeDownloadedModelAtom)
-
- const deleteModel = useCallback(
- async (model: Model) => {
- await localDeleteModel(model.id)
- removeDownloadedModel(model.id)
- toaster({
- title: 'Model Deletion Successful',
- description: `Model ${model.name} has been successfully deleted.`,
- type: 'success',
- })
- },
- [removeDownloadedModel]
- )
-
- return { deleteModel }
-}
-
-const localDeleteModel = async (model: string) =>
- extensionManager
- .get(ExtensionTypeEnum.Model)
- ?.deleteModel(model)
diff --git a/web/hooks/useDeleteThread.test.ts b/web/hooks/useDeleteThread.test.ts
deleted file mode 100644
index bf53589ea..000000000
--- a/web/hooks/useDeleteThread.test.ts
+++ /dev/null
@@ -1,163 +0,0 @@
-/**
- * @jest-environment jsdom
- */
-
-import { renderHook, act } from '@testing-library/react'
-import { useAtom, useAtomValue, useSetAtom } from 'jotai'
-import useDeleteThread from './useDeleteThread'
-import { extensionManager } from '@/extension/ExtensionManager'
-import { useCreateNewThread } from './useCreateNewThread'
-import { Thread } from '@janhq/core/dist/types/types'
-import { currentPromptAtom } from '@/containers/Providers/Jotai'
-// Mock the necessary dependencies
-// Mock dependencies
-jest.mock('jotai', () => ({
- useAtomValue: jest.fn(),
- useSetAtom: jest.fn(),
- useAtom: jest.fn(),
- atom: jest.fn(),
-}))
-jest.mock('./useCreateNewThread')
-jest.mock('@/extension/ExtensionManager')
-jest.mock('@/containers/Toast')
-
-describe('useDeleteThread', () => {
- beforeEach(() => {
- jest.clearAllMocks()
- })
-
- it('should delete a thread successfully', async () => {
- const mockThreads = [
- { id: 'thread1', title: 'Thread 1' },
- { id: 'thread2', title: 'Thread 2' },
- ]
- const mockSetThreads = jest.fn()
- ;(useAtom as jest.Mock).mockReturnValue([mockThreads, mockSetThreads])
- ;(useSetAtom as jest.Mock).mockReturnValue(() => {})
- ;(useCreateNewThread as jest.Mock).mockReturnValue({})
-
- const mockDeleteThread = jest.fn().mockImplementation(() => ({
- catch: () => jest.fn,
- }))
-
- extensionManager.get = jest.fn().mockReturnValue({
- deleteThread: mockDeleteThread,
- getThreadAssistant: jest.fn().mockResolvedValue({}),
- })
-
- const { result } = renderHook(() => useDeleteThread())
-
- await act(async () => {
- await result.current.deleteThread('thread1')
- })
-
- expect(mockDeleteThread).toHaveBeenCalledWith('thread1')
- expect(mockSetThreads).toHaveBeenCalledWith([mockThreads[1]])
- })
-
- it('should clean a thread successfully', async () => {
- const mockThreads = [{ id: 'thread1', title: 'Thread 1', metadata: {} }]
- const mockSetThreads = jest.fn()
- ;(useAtom as jest.Mock).mockReturnValue([mockThreads, mockSetThreads])
- const mockCleanMessages = jest.fn()
- ;(useSetAtom as jest.Mock).mockReturnValue(() => mockCleanMessages)
- ;(useAtomValue as jest.Mock).mockReturnValue(['thread 1'])
-
- const mockSaveThread = jest.fn()
- const mockDeleteMessage = jest.fn().mockResolvedValue({})
- const mockModifyThread = jest.fn().mockResolvedValue({})
- extensionManager.get = jest.fn().mockReturnValue({
- saveThread: mockSaveThread,
- getThreadAssistant: jest.fn().mockResolvedValue({}),
- listMessages: jest.fn().mockResolvedValue([
- {
- id: 'message1',
- text: 'Message 1',
- },
- ]),
- deleteMessage: mockDeleteMessage,
- modifyThread: mockModifyThread,
- })
-
- const { result } = renderHook(() => useDeleteThread())
-
- await act(async () => {
- await result.current.cleanThread('thread1')
- })
-
- expect(mockDeleteMessage).toHaveBeenCalled()
- expect(mockModifyThread).toHaveBeenCalled()
- })
-
- it('should handle errors when deleting a thread', async () => {
- const mockThreads = [{ id: 'thread1', title: 'Thread 1' }]
- const mockSetThreads = jest.fn()
- ;(useAtom as jest.Mock).mockReturnValue([mockThreads, mockSetThreads])
- const mockCreateNewThread = jest.fn()
- ;(useCreateNewThread as jest.Mock).mockReturnValue({
- requestCreateNewThread: mockCreateNewThread,
- })
-
- const mockDeleteThread = jest
- .fn()
- .mockRejectedValue(new Error('Delete error'))
- extensionManager.get = jest.fn().mockReturnValue({
- deleteThread: mockDeleteThread,
- })
-
- const consoleErrorSpy = jest
- .spyOn(console, 'error')
- .mockImplementation(() => {})
-
- const { result } = renderHook(() => useDeleteThread())
-
- await act(async () => {
- await result.current.deleteThread('thread1')
- })
-
- expect(mockDeleteThread).toHaveBeenCalledWith('thread1')
- expect(consoleErrorSpy).toHaveBeenCalledWith(expect.any(Error))
-
- consoleErrorSpy.mockRestore()
- })
-
- it('should delete all threads successfully', async () => {
- const mockThreads = [
- { id: 'thread1', title: 'Thread 1' },
- { id: 'thread2', title: 'Thread 2' },
- ]
- const mockSetThreads = jest.fn()
- ;(useAtom as jest.Mock).mockReturnValue([mockThreads, mockSetThreads])
-
- // create mock functions
- const mockSetCurrentPrompt = jest.fn()
-
- // mock useSetAtom for each atom
- let currentAtom: any
- ;(useSetAtom as jest.Mock).mockImplementation((atom) => {
- currentAtom = atom
- if (currentAtom === currentPromptAtom) return mockSetCurrentPrompt
- return jest.fn()
- })
-
- const mockDeleteThread = jest.fn().mockImplementation(() => ({
- catch: () => jest.fn,
- }))
-
- extensionManager.get = jest.fn().mockReturnValue({
- deleteThread: mockDeleteThread,
- })
-
- const { result } = renderHook(() => useDeleteThread())
-
- await act(async () => {
- await result.current.deleteAllThreads(mockThreads as Thread[])
- })
-
- expect(mockDeleteThread).toHaveBeenCalledTimes(2)
- expect(mockDeleteThread).toHaveBeenCalledWith('thread1')
- expect(mockDeleteThread).toHaveBeenCalledWith('thread2')
- expect(mockSetThreads).toHaveBeenCalledWith([])
- expect(mockSetCurrentPrompt).toHaveBeenCalledWith('')
- })
-})
diff --git a/web/hooks/useDeleteThread.ts b/web/hooks/useDeleteThread.ts
deleted file mode 100644
index bdb82a268..000000000
--- a/web/hooks/useDeleteThread.ts
+++ /dev/null
@@ -1,125 +0,0 @@
-import { useCallback } from 'react'
-
-import { ExtensionTypeEnum, ConversationalExtension, Thread } from '@janhq/core'
-
-import { useAtom, useSetAtom } from 'jotai'
-
-import { currentPromptAtom } from '@/containers/Providers/Jotai'
-
-import { toaster } from '@/containers/Toast'
-
-import useSetActiveThread from './useSetActiveThread'
-
-import { extensionManager } from '@/extension/ExtensionManager'
-
-import { deleteChatMessageAtom as deleteChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
-import {
- threadsAtom,
- setActiveThreadIdAtom,
- deleteThreadStateAtom,
- updateThreadAtom,
-} from '@/helpers/atoms/Thread.atom'
-
-export default function useDeleteThread() {
- const [threads, setThreads] = useAtom(threadsAtom)
- const updateThread = useSetAtom(updateThreadAtom)
-
- const setCurrentPrompt = useSetAtom(currentPromptAtom)
- const setActiveThreadId = useSetAtom(setActiveThreadIdAtom)
- const deleteMessages = useSetAtom(deleteChatMessagesAtom)
-
- const deleteThreadState = useSetAtom(deleteThreadStateAtom)
- const { setActiveThread } = useSetActiveThread()
-
- const cleanThread = useCallback(
- async (threadId: string) => {
- const messages = await extensionManager
- .get(ExtensionTypeEnum.Conversational)
- ?.listMessages(threadId)
- .catch(console.error)
- if (messages) {
- for (const message of messages) {
- await extensionManager
- .get(ExtensionTypeEnum.Conversational)
- ?.deleteMessage(threadId, message.id)
- .catch(console.error)
- }
-
- const thread = threads.find((e) => e.id === threadId)
- if (thread) {
- const updatedThread = {
- ...thread,
- title: 'New Thread',
- metadata: {
- ...thread.metadata,
- title: 'New Thread',
- lastMessage: '',
- },
- }
- extensionManager
- .get(ExtensionTypeEnum.Conversational)
- ?.modifyThread(updatedThread)
- .catch(console.error)
- updateThread(updatedThread)
- }
- }
- deleteMessages(threadId)
- },
- [deleteMessages, threads, updateThread]
- )
-
- const deleteThread = async (threadId: string) => {
- if (!threadId) {
- alert('No active thread')
- return
- }
- await extensionManager
- .get(ExtensionTypeEnum.Conversational)
- ?.deleteThread(threadId)
- .catch(console.error)
- const availableThreads = threads.filter((c) => c.id !== threadId)
- setThreads(availableThreads)
-
- // delete the thread state
- deleteThreadState(threadId)
-
- deleteMessages(threadId)
- setCurrentPrompt('')
- toaster({
- title: 'Thread successfully deleted.',
- description: `Thread ${threadId} has been successfully deleted.`,
- type: 'success',
- })
- if (availableThreads.length > 0) {
- setActiveThread(availableThreads[0])
- } else {
- setActiveThreadId(undefined)
- }
- }
-
- const deleteAllThreads = async (threads: Thread[]) => {
- for (const thread of threads) {
- await extensionManager
- .get(ExtensionTypeEnum.Conversational)
- ?.deleteThread(thread.id as string)
- .catch(console.error)
- deleteThreadState(thread.id as string)
- deleteMessages(thread.id as string)
- }
-
- setThreads([])
- setCurrentPrompt('')
- setActiveThreadId(undefined)
- toaster({
- title: 'All threads successfully deleted.',
- description: `All thread data has been successfully deleted.`,
- type: 'success',
- })
- }
-
- return {
- cleanThread,
- deleteThread,
- deleteAllThreads,
- }
-}
diff --git a/web/hooks/useDownloadModel.test.ts b/web/hooks/useDownloadModel.test.ts
deleted file mode 100644
index 7e9d7b518..000000000
--- a/web/hooks/useDownloadModel.test.ts
+++ /dev/null
@@ -1,95 +0,0 @@
-import { renderHook, act } from '@testing-library/react'
-import { useAtom, useSetAtom } from 'jotai'
-import useDownloadModel from './useDownloadModel'
-import * as core from '@janhq/core'
-import { extensionManager } from '@/extension/ExtensionManager'
-
-// Mock the necessary dependencies
-jest.mock('jotai', () => ({
- useAtomValue: jest.fn(),
- useSetAtom: jest.fn(),
- useAtom: jest.fn(),
- atom: jest.fn(),
-}))
-jest.mock('@janhq/core')
-jest.mock('@/extension/ExtensionManager')
-
-describe('useDownloadModel', () => {
- beforeEach(() => {
- jest.clearAllMocks()
- ;(useAtom as jest.Mock).mockReturnValue([false, jest.fn()])
- })
-
- it('should download a model', async () => {
- const mockModel: core.Model = {
- id: 'test-model',
- sources: [{ filename: 'test.bin', url: 'https://fake.url' }],
- } as core.Model
-
- const mockExtension = {
- pullModel: jest.fn().mockResolvedValue(undefined),
- }
- ;(useSetAtom as jest.Mock).mockReturnValue(() => undefined)
- ;(extensionManager.get as jest.Mock).mockReturnValue(mockExtension)
-
- const { result } = renderHook(() => useDownloadModel())
-
- act(() => {
- result.current.downloadModel(mockModel.sources[0].url, mockModel.id)
- })
-
- expect(mockExtension.pullModel).toHaveBeenCalledWith(
- mockModel.sources[0].url,
- mockModel.id,
- undefined
- )
- })
-
- it('should abort model download', async () => {
- const mockModel: core.Model = {
- id: 'test-model',
- sources: [{ filename: 'test.bin' }],
- } as core.Model
-
- ;(core.joinPath as jest.Mock).mockResolvedValue('/path/to/model/test.bin')
- const mockExtension = {
- cancelModelPull: jest.fn().mockResolvedValue(undefined),
- }
- ;(useSetAtom as jest.Mock).mockReturnValue(() => undefined)
- ;(extensionManager.get as jest.Mock).mockReturnValue(mockExtension)
- const { result } = renderHook(() => useDownloadModel())
-
- act(() => {
- result.current.abortModelDownload(mockModel.id)
- })
-
- expect(mockExtension.cancelModelPull).toHaveBeenCalledWith('test-model')
- })
-
- it('should handle proxy settings', async () => {
- const mockModel: core.Model = {
- id: 'test-model',
- sources: [{ filename: 'test.bin' }],
- } as core.Model
-
- const mockExtension = {
- pullModel: jest.fn().mockResolvedValue(undefined),
- }
- ;(useSetAtom as jest.Mock).mockReturnValue(() => undefined)
- ;(extensionManager.get as jest.Mock).mockReturnValue(mockExtension)
- ;(useAtom as jest.Mock).mockReturnValueOnce([true, jest.fn()]) // proxyEnabled
- ;(useAtom as jest.Mock).mockReturnValueOnce(['http://proxy.com', jest.fn()]) // proxy
-
- const { result } = renderHook(() => useDownloadModel())
-
- act(() => {
- result.current.downloadModel(mockModel.sources[0].url, mockModel.id)
- })
-
- expect(mockExtension.pullModel).toHaveBeenCalledWith(
- mockModel.sources[0].url,
- mockModel.id,
- undefined
- )
- })
-})
diff --git a/web/hooks/useDownloadModel.ts b/web/hooks/useDownloadModel.ts
deleted file mode 100644
index c616f8769..000000000
--- a/web/hooks/useDownloadModel.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-import { useCallback } from 'react'
-
-import { ExtensionTypeEnum, ModelExtension } from '@janhq/core'
-
-import { useSetAtom } from 'jotai'
-
-import { toaster } from '@/containers/Toast'
-
-import { setDownloadStateAtom } from './useDownloadState'
-
-import { extensionManager } from '@/extension/ExtensionManager'
-
-import {
- addDownloadingModelAtom,
- removeDownloadingModelAtom,
-} from '@/helpers/atoms/Model.atom'
-
-export default function useDownloadModel() {
- const removeDownloadingModel = useSetAtom(removeDownloadingModelAtom)
- const addDownloadingModel = useSetAtom(addDownloadingModelAtom)
- const setDownloadStates = useSetAtom(setDownloadStateAtom)
-
- const downloadModel = useCallback(
- async (model: string, id?: string, name?: string) => {
- addDownloadingModel(id ?? model)
- setDownloadStates({
- modelId: id ?? model,
- downloadState: 'downloading',
- fileName: id ?? model,
- size: {
- total: 0,
- transferred: 0,
- },
- percent: 0,
- })
- downloadLocalModel(model, id, name).catch((error) => {
- if (error.message) {
- toaster({
- title: 'Download failed',
- description: error.message,
- type: 'error',
- })
- }
-
- removeDownloadingModel(model)
- })
- },
- [removeDownloadingModel, addDownloadingModel, setDownloadStates]
- )
-
- const abortModelDownload = useCallback(async (model: string) => {
- await cancelModelDownload(model)
- }, [])
-
- return {
- downloadModel,
- abortModelDownload,
- }
-}
-
-const downloadLocalModel = async (model: string, id?: string, name?: string) =>
- extensionManager
- .get(ExtensionTypeEnum.Model)
- ?.pullModel(model, id, name)
-
-const cancelModelDownload = async (model: string) =>
- extensionManager
- .get(ExtensionTypeEnum.Model)
- ?.cancelModelPull(model)
diff --git a/web/hooks/useDownloadState.test.ts b/web/hooks/useDownloadState.test.ts
deleted file mode 100644
index 893649e26..000000000
--- a/web/hooks/useDownloadState.test.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-import {
- setDownloadStateAtom,
- modelDownloadStateAtom,
-} from './useDownloadState'
-
-// Mock dependencies
-jest.mock('jotai', () => ({
- atom: jest.fn(),
- useAtom: jest.fn(),
-}))
-jest.mock('@/containers/Toast', () => ({
- toaster: jest.fn(),
-}))
-jest.mock('@/helpers/atoms/Model.atom', () => ({
- configuredModelsAtom: jest.fn(),
- downloadedModelsAtom: jest.fn(),
- removeDownloadingModelAtom: jest.fn(),
-}))
-
-describe('setDownloadStateAtom', () => {
- let get: jest.Mock
- let set: jest.Mock
-
- beforeEach(() => {
- get = jest.fn()
- set = jest.fn()
- })
-
- it('should handle download completion', () => {
- const state = {
- downloadState: 'end',
- modelId: 'model1',
- fileName: 'file1',
- children: [],
- }
- const currentState = {
- model1: {
- children: [state],
- },
- }
- get.mockReturnValueOnce(currentState)
- get.mockReturnValueOnce([{ id: 'model1' }])
-
- set(setDownloadStateAtom, state)
-
- expect(set).toHaveBeenCalledWith(
- undefined,
- expect.objectContaining({ modelId: expect.stringContaining('model1') })
- )
- })
-
- it('should handle download error', () => {
- const state = {
- downloadState: 'error',
- modelId: 'model1',
- error: 'some error',
- }
- const currentState = {
- model1: {},
- }
- get.mockReturnValueOnce(currentState)
-
- set(setDownloadStateAtom, state)
-
- expect(set).toHaveBeenCalledWith(
- undefined,
- expect.objectContaining({ modelId: 'model1' })
- )
- })
-
- it('should handle download error with certificate issue', () => {
- const state = {
- downloadState: 'error',
- modelId: 'model1',
- error: 'certificate error',
- }
- const currentState = {
- model1: {},
- }
- get.mockReturnValueOnce(currentState)
-
- set(setDownloadStateAtom, state)
-
- expect(set).toHaveBeenCalledWith(
- undefined,
- expect.objectContaining({ modelId: 'model1' })
- )
- })
-
- it('should handle download in progress', () => {
- const state = {
- downloadState: 'progress',
- modelId: 'model1',
- fileName: 'file1',
- size: { total: 100, transferred: 50 },
- }
- const currentState = {
- model1: {
- children: [],
- size: { total: 0, transferred: 0 },
- },
- }
- get.mockReturnValueOnce(currentState)
-
- set(setDownloadStateAtom, state)
-
- expect(set).toHaveBeenCalledWith(modelDownloadStateAtom, expect.any(Object))
- })
-})
diff --git a/web/hooks/useDownloadState.ts b/web/hooks/useDownloadState.ts
deleted file mode 100644
index 632ca31a8..000000000
--- a/web/hooks/useDownloadState.ts
+++ /dev/null
@@ -1,147 +0,0 @@
-import { DownloadState } from '@janhq/core'
-import { atom } from 'jotai'
-
-import { toaster } from '@/containers/Toast'
-
-import {
- configuredModelsAtom,
- downloadedModelsAtom,
- removeDownloadingModelAtom,
-} from '@/helpers/atoms/Model.atom'
-
-// download states
-
-export const modelDownloadStateAtom = atom>({})
-
-/**
- * Remove a download state for a particular model.
- */
-export const removeDownloadStateAtom = atom(null, (get, set, id: string) => {
- const currentState = { ...get(modelDownloadStateAtom) }
- delete currentState[id]
- set(modelDownloadStateAtom, currentState)
- set(removeDownloadingModelAtom, id)
-})
-/**
- * Used to set the download state for a particular model.
- */
-export const setDownloadStateAtom = atom(
- null,
- (get, set, state: DownloadState) => {
- try {
- const currentState = { ...get(modelDownloadStateAtom) }
-
- if (state.downloadState === 'end') {
- const modelDownloadState = currentState[state.modelId]
-
- const updatedChildren: DownloadState[] = (
- modelDownloadState.children ?? []
- ).filter((m) => m.fileName !== state.fileName)
- updatedChildren.push(state)
- modelDownloadState.children = updatedChildren
- currentState[state.modelId] = modelDownloadState
-
- const isAllChildrenDownloadEnd = modelDownloadState.children?.every(
- (m) => m.downloadState === 'end'
- )
-
- if (isAllChildrenDownloadEnd) {
- // download successfully
- delete currentState[state.modelId]
- set(removeDownloadingModelAtom, state.modelId)
-
- const model = get(configuredModelsAtom).find(
- (e) => e.id === state.modelId
- )
- if (model) set(downloadedModelsAtom, (prev) => [...prev, model])
- }
- } else if (state.downloadState === 'error') {
- // download error
- delete currentState[state.modelId]
- set(removeDownloadingModelAtom, state.modelId)
- if (state.error === 'aborted') {
- toaster({
- title: 'Cancel Download',
- description: `Model ${state.modelId} download cancelled`,
- type: 'warning',
- })
- } else {
- let error = state.error
- if (
- typeof error?.includes === 'function' &&
- state.error?.includes('certificate')
- ) {
- error +=
- '. To fix enable "Ignore SSL Certificates" in Advanced settings.'
- }
- toaster({
- title: 'Download Failed',
- description: `Model ${state.modelId} download failed: ${error}`,
- type: 'error',
- })
- }
- } else {
- // download in progress
- if (state.size.total === 0 || !currentState[state.modelId]) {
- // this is initial state, just set the state
- currentState[state.modelId] = state
- set(modelDownloadStateAtom, currentState)
- return
- }
-
- const modelDownloadState = currentState[state.modelId]
- if (!modelDownloadState) {
- console.debug('setDownloadStateAtom: modelDownloadState not found')
- return
- }
-
- // delete the children if the filename is matched and replace the new state
- const updatedChildren: DownloadState[] = (
- modelDownloadState.children ?? []
- ).filter((m) => m.fileName !== state.fileName)
-
- updatedChildren.push(state)
-
- // re-calculate the overall progress if we have all the children download data
- const isAnyChildDownloadNotReady = updatedChildren.some(
- (m) =>
- m.size.total === 0 &&
- !modelDownloadState.children?.some(
- (e) => e.fileName === m.fileName && e.downloadState === 'end'
- ) &&
- modelDownloadState.children?.some((e) => e.fileName === m.fileName)
- )
-
- modelDownloadState.children = updatedChildren
-
- if (isAnyChildDownloadNotReady) {
- // just update the children
- currentState[state.modelId] = modelDownloadState
- set(modelDownloadStateAtom, currentState)
- return
- }
-
- const parentTotalSize = updatedChildren.reduce(
- (acc, m) => acc + m.size.total,
- 0
- )
- // calculate the total transferred size by sum all children transferred size
- const transferredSize = updatedChildren.reduce(
- (acc, m) => acc + m.size.transferred,
- 0
- )
- modelDownloadState.size.total = parentTotalSize
- modelDownloadState.size.transferred = transferredSize
-
- modelDownloadState.percent =
- parentTotalSize === 0 ? 0 : transferredSize / parentTotalSize
- currentState[state.modelId] = modelDownloadState
- }
-
- set(modelDownloadStateAtom, currentState)
- } catch (e) {
- console.debug('setDownloadStateAtom: state', state)
- console.debug('setDownloadStateAtom: error', e)
- }
- }
-)
diff --git a/web/hooks/useDropModelBinaries.test.ts b/web/hooks/useDropModelBinaries.test.ts
deleted file mode 100644
index 7ca5a479e..000000000
--- a/web/hooks/useDropModelBinaries.test.ts
+++ /dev/null
@@ -1,133 +0,0 @@
-/**
- * @jest-environment jsdom
- */
-// useDropModelBinaries.test.ts
-
-import { renderHook, act } from '@testing-library/react'
-import { useSetAtom } from 'jotai'
-import { v4 as uuidv4 } from 'uuid'
-import useDropModelBinaries from './useDropModelBinaries'
-import { getFileInfoFromFile } from '@/utils/file'
-import { snackbar } from '@/containers/Toast'
-
-// Mock dependencies
-// Mock the necessary dependencies
-jest.mock('jotai', () => ({
- useAtomValue: jest.fn(),
- useSetAtom: jest.fn(),
- useAtom: jest.fn(),
- atom: jest.fn(),
-}))
-jest.mock('uuid')
-jest.mock('@/utils/file')
-jest.mock('@/containers/Toast')
-jest.mock("@uppy/core")
-
-describe('useDropModelBinaries', () => {
- const mockSetImportingModels = jest.fn()
- const mockSetImportModelStage = jest.fn()
-
- beforeEach(() => {
- jest.clearAllMocks()
- ;(useSetAtom as jest.Mock).mockReturnValueOnce(mockSetImportingModels)
- ;(useSetAtom as jest.Mock).mockReturnValueOnce(mockSetImportModelStage)
- ;(uuidv4 as jest.Mock).mockReturnValue('mock-uuid')
- ;(getFileInfoFromFile as jest.Mock).mockResolvedValue([])
- })
-
- it('should handle dropping supported files', async () => {
- const { result } = renderHook(() => useDropModelBinaries())
-
- const mockFiles = [
- { name: 'model1.gguf', path: '/path/to/model1.gguf', size: 1000 },
- { name: 'model2.gguf', path: '/path/to/model2.gguf', size: 2000 },
- ]
-
- ;(getFileInfoFromFile as jest.Mock).mockResolvedValue(mockFiles)
-
- await act(async () => {
- await result.current.onDropModels([])
- })
-
- expect(mockSetImportingModels).toHaveBeenCalledWith([
- {
- importId: 'mock-uuid',
- modelId: undefined,
- name: 'model1',
- description: '',
- path: '/path/to/model1.gguf',
- tags: [],
- size: 1000,
- status: 'PREPARING',
- format: 'gguf',
- },
- {
- importId: 'mock-uuid',
- modelId: undefined,
- name: 'model2',
- description: '',
- path: '/path/to/model2.gguf',
- tags: [],
- size: 2000,
- status: 'PREPARING',
- format: 'gguf',
- },
- ])
- expect(mockSetImportModelStage).toHaveBeenCalledWith('MODEL_SELECTED')
- })
-
- it('should handle dropping unsupported files', async () => {
- const { result } = renderHook(() => useDropModelBinaries())
-
- const mockFiles = [
- { name: 'unsupported.txt', path: '/path/to/unsupported.txt', size: 500 },
- ]
-
- ;(getFileInfoFromFile as jest.Mock).mockResolvedValue(mockFiles)
-
- await act(async () => {
- await result.current.onDropModels([])
- })
-
- expect(snackbar).toHaveBeenCalledWith({
- description: 'Only files with .gguf extension can be imported.',
- type: 'error',
- })
- expect(mockSetImportingModels).not.toHaveBeenCalled()
- expect(mockSetImportModelStage).not.toHaveBeenCalled()
- })
-
- it('should handle dropping both supported and unsupported files', async () => {
- const { result } = renderHook(() => useDropModelBinaries())
-
- const mockFiles = [
- { name: 'model.gguf', path: '/path/to/model.gguf', size: 1000 },
- { name: 'unsupported.txt', path: '/path/to/unsupported.txt', size: 500 },
- ]
-
- ;(getFileInfoFromFile as jest.Mock).mockResolvedValue(mockFiles)
-
- await act(async () => {
- await result.current.onDropModels([])
- })
-
- expect(snackbar).toHaveBeenCalledWith({
- description: 'Only files with .gguf extension can be imported.',
- type: 'error',
- })
- expect(mockSetImportingModels).toHaveBeenCalledWith([
- {
- importId: 'mock-uuid',
- modelId: undefined,
- name: 'model',
- description: '',
- path: '/path/to/model.gguf',
- tags: [],
- size: 1000,
- status: 'PREPARING',
- format: 'gguf',
- },
- ])
- expect(mockSetImportModelStage).toHaveBeenCalledWith('MODEL_SELECTED')
- })
-})
diff --git a/web/hooks/useDropModelBinaries.ts b/web/hooks/useDropModelBinaries.ts
deleted file mode 100644
index 7c87355f7..000000000
--- a/web/hooks/useDropModelBinaries.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import { useCallback } from 'react'
-
-import { ImportingModel } from '@janhq/core'
-import { useSetAtom } from 'jotai'
-
-import { v4 as uuidv4 } from 'uuid'
-
-import { snackbar } from '@/containers/Toast'
-
-import { getFileInfoFromFile } from '@/utils/file'
-
-import { setImportModelStageAtom } from './useImportModel'
-
-import { importingModelsAtom } from '@/helpers/atoms/Model.atom'
-
-export default function useDropModelBinaries() {
- const setImportingModels = useSetAtom(importingModelsAtom)
- const setImportModelStage = useSetAtom(setImportModelStageAtom)
-
- const onDropModels = useCallback(
- async (acceptedFiles: File[]) => {
- const files = await getFileInfoFromFile(acceptedFiles)
-
- const unsupportedFiles = files.filter(
- (file) => !file.path.endsWith('.gguf')
- )
- const supportedFiles = files.filter((file) => file.path.endsWith('.gguf'))
-
- const importingModels: ImportingModel[] = supportedFiles.map((file) => ({
- importId: uuidv4(),
- modelId: undefined,
- name: file.name.replace(/ /g, '').replace('.gguf', ''),
- description: '',
- path: file.path,
- tags: [],
- size: file.size,
- status: 'PREPARING',
- format: 'gguf',
- }))
- if (unsupportedFiles.length > 0) {
- snackbar({
- description: `Only files with .gguf extension can be imported.`,
- type: 'error',
- })
- }
- if (importingModels.length === 0) return
-
- setImportingModels(importingModels)
- setImportModelStage('MODEL_SELECTED')
- },
- [setImportModelStage, setImportingModels]
- )
-
- return { onDropModels }
-}
diff --git a/web/hooks/useEngineManagement.ts b/web/hooks/useEngineManagement.ts
deleted file mode 100644
index 8c19737ac..000000000
--- a/web/hooks/useEngineManagement.ts
+++ /dev/null
@@ -1,574 +0,0 @@
-import 'openai/shims/web'
-import { useCallback, useMemo, useState } from 'react'
-
-import {
- ExtensionTypeEnum,
- EngineManagementExtension,
- InferenceEngine,
- EngineReleased,
- EngineConfig,
- events,
- EngineEvent,
- Model,
- ModelEvent,
- ModelSource,
- ModelSibling,
- ModelExtension,
-} from '@janhq/core'
-import { useAtom, useAtomValue } from 'jotai'
-import { atomWithStorage } from 'jotai/utils'
-import useSWR from 'swr'
-
-import { models, TokenJS } from 'token.js'
-import { LLMProvider } from 'token.js/dist/chat'
-
-import { getDescriptionByEngine, getTitleByEngine } from '@/utils/modelEngine'
-
-import { extensionManager } from '@/extension/ExtensionManager'
-import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
-
-export const builtInEngines = [
- 'openai',
- 'ai21',
- 'anthropic',
- 'gemini',
- 'cohere',
- 'bedrock',
- 'mistral',
- 'groq',
- 'perplexity',
- 'openrouter',
- 'openai-compatible',
-]
-
-export const convertBuiltInEngine = (engine?: string): LLMProvider => {
- const engineName = normalizeBuiltInEngineName(engine) ?? ''
- return (
- builtInEngines.includes(engineName) ? engineName : 'openai-compatible'
- ) as LLMProvider
-}
-
-export const normalizeBuiltInEngineName = (
- engine?: string
-): string | undefined => {
- return engine === ('google_gemini' as InferenceEngine) ? 'gemini' : engine
-}
-
-export const extendBuiltInEngineModels = (
- tokenJS: TokenJS,
- provider: LLMProvider,
- model?: string
-) => {
- if (provider !== 'openrouter' && provider !== 'openai-compatible' && model) {
- if (
- provider in Object.keys(models) &&
- (models[provider].models as unknown as string[]).includes(model)
- ) {
- return
- }
-
- try {
- // @ts-expect-error Unknown extendModelList provider type
- tokenJS.extendModelList(provider, model, {
- streaming: true,
- toolCalls: true,
- })
- } catch (error) {
- console.error('Failed to extend model list:', error)
- }
- }
-}
-
-export const releasedEnginesCacheAtom = atomWithStorage<{
- data: EngineReleased[]
- timestamp: number
-} | null>('releasedEnginesCache', null, undefined, { getOnInit: true })
-
-export const releasedEnginesLatestCacheAtom = atomWithStorage<{
- data: EngineReleased[]
- timestamp: number
-} | null>('releasedEnginesLatestCache', null, undefined, { getOnInit: true })
-
-export interface RemoteModelList {
- data?: {
- id?: string
- name?: string
- }[]
-}
-
-// fetcher function
-async function fetchExtensionData(
- extension: EngineManagementExtension | null,
- method: (extension: EngineManagementExtension) => Promise
-): Promise {
- if (!extension) {
- throw new Error('Extension not found')
- }
- return method(extension)
-}
-
-/**
- * @returns A Promise that resolves to an object of list engines.
- */
-export function useGetEngines() {
- const extension = useMemo(
- () =>
- extensionManager.get(
- ExtensionTypeEnum.Engine
- ) ?? null,
- []
- )
-
- const {
- data: engines,
- error,
- mutate,
- } = useSWR(
- extension ? 'engines' : null,
- () => fetchExtensionData(extension, (ext) => ext.getEngines()),
- {
- revalidateOnFocus: false,
- revalidateOnReconnect: true,
- }
- )
-
- return { engines, error, mutate }
-}
-
-/**
- * @returns A Promise that resolves to an object of remote models.
- */
-export function useGetRemoteModels(name: string) {
- const extension = useMemo(
- () =>
- extensionManager.get(
- ExtensionTypeEnum.Engine
- ) ?? null,
- []
- )
-
- const {
- data: remoteModels,
- error,
- mutate,
- } = useSWR(
- extension ? `remoteModels_${name}` : null,
- () =>
- fetchExtensionData(
- extension,
- (ext) => ext.getRemoteModels(name) as Promise
- ),
- {
- revalidateOnFocus: false,
- revalidateOnReconnect: true,
- }
- )
-
- return { remoteModels, error, mutate }
-}
-
-/**
- * @param name - Inference engine name.
- * @returns A Promise that resolves to an array of installed engine.
- */
-export function useGetInstalledEngines(name: InferenceEngine) {
- const extension = useMemo(
- () =>
- extensionManager.get(
- ExtensionTypeEnum.Engine
- ) ?? null,
- []
- )
-
- const {
- data: installedEngines,
- error,
- mutate,
- } = useSWR(
- extension ? 'installedEngines' : null,
- () => fetchExtensionData(extension, (ext) => ext.getInstalledEngines(name)),
- {
- revalidateOnFocus: false,
- revalidateOnReconnect: true,
- }
- )
-
- return { installedEngines, error, mutate }
-}
-
-/**
- * @param name - Inference engine name.
- * @param version - Version of the engine.
- * @param platform - Optional to sort by operating system. macOS, linux, windows.
- * @returns A Promise that resolves to an array of latest released engine by version.
- */
-export function useGetReleasedEnginesByVersion(
- engine: InferenceEngine,
- version: string | undefined,
- platform: string
-) {
- const extension = useMemo(
- () =>
- extensionManager.get(
- ExtensionTypeEnum.Engine
- ) ?? null,
- []
- )
-
- const [cache, setCache] = useAtom(releasedEnginesCacheAtom)
-
- const shouldFetch = Boolean(extension && version)
-
- const fetcher = async () => {
- const now = Date.now()
- const fifteenMinutes = 15 * 60 * 1000
- if (cache && cache.timestamp + fifteenMinutes > now) {
- return cache.data // Use cached data
- }
-
- const newData = await fetchExtensionData(extension, (ext) =>
- ext.getReleasedEnginesByVersion(engine, version!, platform)
- )
-
- setCache({ data: newData, timestamp: now })
- return newData
- }
-
- const { data, error, mutate } = useSWR(
- shouldFetch
- ? `releasedEnginesByVersion-${engine}-${version}-${platform}`
- : null,
- fetcher,
- {
- revalidateOnFocus: false,
- revalidateOnReconnect: true,
- }
- )
-
- return {
- releasedEnginesByVersion: data,
- error,
- mutate,
- }
-}
-
-/**
- * @param name - Inference engine name.
- * @param platform - Optional to sort by operating system. macOS, linux, windows.
- * @returns A Promise that resolves to an array of latest released engine.
- */
-
-export function useGetLatestReleasedEngine(
- engine: InferenceEngine,
- platform: string
-) {
- const extension = useMemo(
- () =>
- extensionManager.get(
- ExtensionTypeEnum.Engine
- ) ?? null,
- []
- )
-
- const [cache, setCache] = useAtom(releasedEnginesLatestCacheAtom)
-
- const fetcher = async () => {
- const now = Date.now()
- const fifteenMinutes = 15 * 60 * 1000
-
- if (cache && cache.timestamp + fifteenMinutes > now) {
- return cache.data // Use cached data
- }
-
- const newData = await fetchExtensionData(extension, (ext) =>
- ext.getLatestReleasedEngine(engine, platform)
- )
-
- setCache({ data: newData, timestamp: now })
- return newData
- }
-
- const { data, error, mutate } = useSWR(
- extension ? 'latestReleasedEngine' : null,
- fetcher,
- {
- revalidateOnFocus: false,
- revalidateOnReconnect: true,
- }
- )
-
- return {
- latestReleasedEngine: data,
- error,
- mutate,
- }
-}
-
-/**
- * @param name - Inference engine name.
- * @returns A Promise that resolves to an object of default engine.
- */
-export function useGetDefaultEngineVariant(name: InferenceEngine) {
- const extension = useMemo(
- () =>
- extensionManager.get(ExtensionTypeEnum.Engine),
- []
- )
-
- const {
- data: defaultEngineVariant,
- error,
- mutate,
- } = useSWR(
- extension ? 'defaultEngineVariant' : null,
- () =>
- fetchExtensionData(extension ?? null, (ext) =>
- ext.getDefaultEngineVariant(name)
- ),
- {
- revalidateOnFocus: false,
- revalidateOnReconnect: true,
- }
- )
-
- return { defaultEngineVariant, error, mutate }
-}
-
-const getExtension = () =>
- extensionManager.get(ExtensionTypeEnum.Engine) ??
- null
-
-/**
- * @body variant - string
- * @body version - string
- * @returns A Promise that resolves to set default engine.
- */
-export const setDefaultEngineVariant = async (
- name: InferenceEngine,
- engineConfig: { variant: string; version: string }
-) => {
- const extension = getExtension()
-
- if (!extension) {
- throw new Error('Extension is not available')
- }
-
- try {
- // Call the extension's method
- const response = await extension.setDefaultEngineVariant(name, engineConfig)
- return response
- } catch (error) {
- console.error('Failed to set default engine variant:', error)
- throw error
- }
-}
-
-/**
- * @body variant - string
- * @body version - string
- * @returns A Promise that resolves to set default engine.
- */
-export const updateEngine = async (
- name: InferenceEngine,
- engineConfig?: EngineConfig
-) => {
- const extension = getExtension()
-
- if (!extension) {
- throw new Error('Extension is not available')
- }
-
- try {
- // Call the extension's method
- const response = await extension.updateEngine(name, engineConfig)
- events.emit(EngineEvent.OnEngineUpdate, {})
- return response
- } catch (error) {
- console.error('Failed to set default engine variant:', error)
- throw error
- }
-}
-
-/**
- * @param name - Inference engine name.
- * @returns A Promise that resolves to intall of engine.
- */
-export const installEngine = async (
- name: string,
- engineConfig: EngineConfig
-) => {
- const extension = getExtension()
-
- if (!extension) {
- throw new Error('Extension is not available')
- }
-
- try {
- // Call the extension's method
- const response = await extension.installEngine(name, engineConfig)
- events.emit(EngineEvent.OnEngineUpdate, {})
- return response
- } catch (error) {
- console.error('Failed to install engine variant:', error)
- throw error
- }
-}
-
-/**
- * Add a new remote engine
- * @returns A Promise that resolves to intall of engine.
- */
-export const addRemoteEngine = async (engineConfig: EngineConfig) => {
- const extension = getExtension()
-
- if (!extension) {
- throw new Error('Extension is not available')
- }
-
- try {
- // Call the extension's method
- const response = await extension.addRemoteEngine(engineConfig)
- events.emit(EngineEvent.OnEngineUpdate, {})
- return response
- } catch (error) {
- console.error('Failed to install engine variant:', error)
- throw error
- }
-}
-
-/**
- * @param name - Inference engine name.
- * @returns A Promise that resolves to unintall of engine.
- */
-export const uninstallEngine = async (
- name: InferenceEngine,
- engineConfig: EngineConfig
-) => {
- const extension = getExtension()
-
- if (!extension) {
- throw new Error('Extension is not available')
- }
-
- try {
- // Call the extension's method
- const response = await extension.uninstallEngine(name, engineConfig)
- events.emit(EngineEvent.OnEngineUpdate, {})
- return response
- } catch (error) {
- console.error('Failed to install engine variant:', error)
- throw error
- }
-}
-
-/**
- * Add a new remote engine model
- * @param name
- * @param engine
- * @returns
- */
-export const addRemoteEngineModel = async (name: string, engine: string) => {
- const extension = getExtension()
-
- if (!extension) {
- throw new Error('Extension is not available')
- }
-
- try {
- // Call the extension's method
- const response = await extension.addRemoteModel({
- id: name,
- model: name,
- engine: engine as InferenceEngine,
- } as unknown as Model)
- events.emit(ModelEvent.OnModelsUpdate, { fetch: true })
- return response
- } catch (error) {
- console.error('Failed to install engine variant:', error)
- throw error
- }
-}
-
-/**
- * Remote model sources
- * @returns A Promise that resolves to an object of model sources.
- */
-export const useGetEngineModelSources = () => {
- const { engines } = useGetEngines()
- const downloadedModels = useAtomValue(downloadedModelsAtom)
-
- return {
- sources: Object.entries(engines ?? {})
- ?.filter((e) => e?.[1]?.[0]?.type === 'remote')
- .map(
- ([key, values]) =>
- ({
- id: key,
- models: (
- downloadedModels.filter((e) => e.engine === values[0]?.engine) ??
- []
- ).map(
- (e) =>
- ({
- id: e.id,
- size: e.metadata?.size,
- }) as unknown as ModelSibling
- ),
- metadata: {
- id: getTitleByEngine(key as InferenceEngine),
- description: getDescriptionByEngine(key as InferenceEngine),
- apiKey: values[0]?.api_key,
- },
- type: 'cloud',
- }) as unknown as ModelSource
- ),
- }
-}
-
-/**
- * Refresh model list
- * @param engine
- * @returns
- */
-export const useRefreshModelList = (engine: string) => {
- const [refreshingModels, setRefreshingModels] = useState(false)
- const { mutate: fetchRemoteModels } = useGetRemoteModels(engine)
-
- const refreshModels = useCallback(
- (engine: string) => {
- setRefreshingModels(true)
- fetchRemoteModels()
- .then((remoteModelList) =>
- Promise.all(
- remoteModelList?.data?.map((model: { id?: string }) =>
- model?.id
- ? addRemoteEngineModel(model.id, engine).catch(() => {})
- : {}
- ) ?? []
- )
- )
- .finally(() => setRefreshingModels(false))
- },
- [fetchRemoteModels]
- )
-
- return { refreshingModels, refreshModels }
-}
-
-export const useFetchModelsHub = () => {
- const extension = useMemo(
- () => extensionManager.get(ExtensionTypeEnum.Model) ?? null,
- []
- )
-
- const { data, error, mutate } = useSWR(
- extension ? 'fetchModelsHub' : null,
- () => extension?.fetchModelsHub(),
- {
- revalidateOnFocus: false,
- revalidateOnReconnect: true,
- }
- )
-
- return { modelsHub: data, error, mutate }
-}
diff --git a/web/hooks/useFactoryReset.test.ts b/web/hooks/useFactoryReset.test.ts
deleted file mode 100644
index c66ecce20..000000000
--- a/web/hooks/useFactoryReset.test.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-import { renderHook, act } from '@testing-library/react'
-import { useAtomValue, useSetAtom } from 'jotai'
-import useFactoryReset, { FactoryResetState } from './useFactoryReset'
-import { useActiveModel } from './useActiveModel'
-import { fs } from '@janhq/core'
-
-// Mock the dependencies
-jest.mock('jotai', () => ({
- atom: jest.fn(),
- useAtomValue: jest.fn(),
- useSetAtom: jest.fn(),
-}))
-jest.mock('./useActiveModel', () => ({
- useActiveModel: jest.fn(),
-}))
-jest.mock('@janhq/core', () => ({
- fs: {
- rm: jest.fn(),
- },
- EngineManager: {
- instance: jest.fn().mockReturnValue({
- get: jest.fn(),
- engines: {},
- }),
- },
-}))
-
-describe('useFactoryReset', () => {
- const mockStopModel = jest.fn()
- const mockSetFactoryResetState = jest.fn()
- const mockGetAppConfigurations = jest.fn()
- const mockUpdateAppConfiguration = jest.fn()
- const mockRelaunch = jest.fn()
-
- beforeEach(() => {
- jest.clearAllMocks()
- ;(useAtomValue as jest.Mock).mockReturnValue('/default/jan/data/folder')
- ;(useSetAtom as jest.Mock).mockReturnValue(mockSetFactoryResetState)
- ;(useActiveModel as jest.Mock).mockReturnValue({ stopModel: mockStopModel })
- global.window ??= Object.create(window)
- global.window.core = {
- api: {
- getAppConfigurations: mockGetAppConfigurations,
- updateAppConfiguration: mockUpdateAppConfiguration,
- relaunch: mockRelaunch,
- factoryReset: jest.fn(),
- },
- }
- mockGetAppConfigurations.mockResolvedValue({
- data_folder: '/current/jan/data/folder',
- quick_ask: false,
- })
- // @ts-ignore
- jest.spyOn(global, 'setTimeout').mockImplementation((cb) => cb())
- })
-
- it('should reset all correctly', async () => {
- const { result } = renderHook(() => useFactoryReset())
-
- await act(async () => {
- await result.current.resetAll()
- })
-
- expect(mockSetFactoryResetState).toHaveBeenCalledWith(
- FactoryResetState.Starting
- )
- expect(mockSetFactoryResetState).toHaveBeenCalledWith(
- FactoryResetState.StoppingModel
- )
- expect(mockStopModel).toHaveBeenCalled()
- expect(mockSetFactoryResetState).toHaveBeenCalledWith(
- FactoryResetState.DeletingData
- )
- expect(fs.rm).toHaveBeenCalledWith({ args: ['/current/jan/data/folder'] })
- expect(mockSetFactoryResetState).toHaveBeenCalledWith(
- FactoryResetState.ClearLocalStorage
- )
- })
-
- it('should keep current folder when specified', async () => {
- const { result } = renderHook(() => useFactoryReset())
-
- await act(async () => {
- await result.current.resetAll(true)
- })
-
- expect(mockUpdateAppConfiguration).not.toHaveBeenCalled()
- })
-})
diff --git a/web/hooks/useFactoryReset.ts b/web/hooks/useFactoryReset.ts
deleted file mode 100644
index c582cf685..000000000
--- a/web/hooks/useFactoryReset.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import { useCallback } from 'react'
-
-import { fs, AppConfiguration, EngineManager } from '@janhq/core'
-import { atom, useAtomValue, useSetAtom } from 'jotai'
-
-import { useActiveModel } from './useActiveModel'
-
-import { defaultJanDataFolderAtom } from '@/helpers/atoms/App.atom'
-
-export enum FactoryResetState {
- Idle = 'idle',
- Starting = 'starting',
- StoppingModel = 'stopping_model',
- DeletingData = 'deleting_data',
- ClearLocalStorage = 'clear_local_storage',
-}
-
-export const factoryResetStateAtom = atom(FactoryResetState.Idle)
-
-export default function useFactoryReset() {
- const defaultJanDataFolder = useAtomValue(defaultJanDataFolderAtom)
- const { stopModel } = useActiveModel()
- const setFactoryResetState = useSetAtom(factoryResetStateAtom)
-
- const resetAll = useCallback(
- async (keepCurrentFolder?: boolean) => {
- setFactoryResetState(FactoryResetState.Starting)
- // read the place of jan data folder
- const appConfiguration: AppConfiguration | undefined =
- await window.core?.api?.getAppConfigurations()
-
- if (!appConfiguration) {
- console.debug('Failed to get app configuration')
- }
-
- const janDataFolderPath = appConfiguration!.data_folder
- // 1: Stop running model
- setFactoryResetState(FactoryResetState.StoppingModel)
- await stopModel()
-
- await Promise.all(
- Object.values(EngineManager.instance().engines).map(async (engine) => {
- await engine.onUnload()
- })
- )
-
- await new Promise((resolve) => setTimeout(resolve, 4000))
-
- // 2: Delete the old jan data folder
- setFactoryResetState(FactoryResetState.DeletingData)
- await fs.rm(janDataFolderPath)
-
- // 3: Set the default jan data folder
- if (!keepCurrentFolder) {
- // set the default jan data folder to user's home directory
- const configuration: AppConfiguration = {
- data_folder: defaultJanDataFolder,
- quick_ask: appConfiguration?.quick_ask ?? false,
- distinct_id: appConfiguration?.distinct_id,
- }
- await window.core?.api?.updateAppConfiguration({ configuration })
- }
-
- await window.core?.api?.installExtensions()
-
- // Perform factory reset
- // await window.core?.api?.factoryReset()
-
- // 4: Clear app local storage
- setFactoryResetState(FactoryResetState.ClearLocalStorage)
- // reset the localStorage
- localStorage.clear()
-
- // 5: Relaunch the app
- window.core.api.relaunch()
- },
- [defaultJanDataFolder, stopModel, setFactoryResetState]
- )
-
- return {
- resetAll,
- }
-}
diff --git a/web/hooks/useGetHFRepoData.test.ts b/web/hooks/useGetHFRepoData.test.ts
deleted file mode 100644
index 01055612d..000000000
--- a/web/hooks/useGetHFRepoData.test.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-/**
- * @jest-environment jsdom
- */
-import { renderHook, act } from '@testing-library/react'
-import { useGetHFRepoData } from './useGetHFRepoData'
-import { extensionManager } from '@/extension'
-import * as hf from '@/utils/huggingface'
-
-jest.mock('@/extension', () => ({
- extensionManager: {
- get: jest.fn(),
- },
-}))
-
-jest.mock('@/utils/huggingface')
-
-describe('useGetHFRepoData', () => {
- beforeEach(() => {
- jest.clearAllMocks()
- })
-
- it('should fetch HF repo data successfully', async () => {
- const mockData = { name: 'Test Repo', stars: 100 }
- ;(hf.fetchHuggingFaceRepoData as jest.Mock).mockReturnValue(mockData)
-
- const { result } = renderHook(() => useGetHFRepoData())
-
- expect(result.current.loading).toBe(false)
- expect(result.current.error).toBeUndefined()
-
- let data
- act(() => {
- data = result.current.getHfRepoData('test-repo')
- })
-
- expect(result.current.loading).toBe(true)
-
- expect(result.current.error).toBeUndefined()
- expect(await data).toEqual(mockData)
- })
-})
diff --git a/web/hooks/useGetHFRepoData.ts b/web/hooks/useGetHFRepoData.ts
deleted file mode 100644
index 6f2ec2b57..000000000
--- a/web/hooks/useGetHFRepoData.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { useCallback, useState } from 'react'
-
-import { HuggingFaceRepoData } from '@janhq/core'
-
-import { fetchHuggingFaceRepoData } from '@/utils/huggingface'
-
-export const useGetHFRepoData = () => {
- const [error, setError] = useState(undefined)
- const [loading, setLoading] = useState(false)
-
- const getHfRepoData = useCallback(async (repoId: string) => {
- try {
- setError(undefined)
- setLoading(true)
- const data = await extensionGetHfRepoData(repoId)
- return data
- } catch (err) {
- console.error(err)
- if (err instanceof Error) {
- setError(err.message)
- }
- throw err
- } finally {
- setLoading(false)
- }
- }, [])
-
- return { loading, error, getHfRepoData }
-}
-
-const extensionGetHfRepoData = async (
- repoId: string
-): Promise => {
- return fetchHuggingFaceRepoData(repoId)
-}
diff --git a/web/hooks/useGetLatestRelease.ts b/web/hooks/useGetLatestRelease.ts
deleted file mode 100644
index 58de2ddb8..000000000
--- a/web/hooks/useGetLatestRelease.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-/* eslint-disable @typescript-eslint/no-explicit-any */
-import useSWR from 'swr'
-
-const fetchLatestRelease = async (includeBeta: boolean) => {
- const res = await fetch(
- 'https://api.github.com/repos/menloresearch/jan/releases'
- )
- if (!res.ok) throw new Error('Failed to fetch releases')
-
- const releases = await res.json()
-
- // Filter stable and beta releases
- const stableRelease = releases.find(
- (release: { prerelease: any; draft: any }) =>
- !release.prerelease && !release.draft
- )
- const betaRelease = releases.find(
- (release: { prerelease: any }) => release.prerelease
- )
-
- return includeBeta ? (betaRelease ?? stableRelease) : stableRelease
-}
-
-export function useGetLatestRelease(includeBeta = false) {
- const { data, error, mutate } = useSWR(
- ['latestRelease', includeBeta],
- () => fetchLatestRelease(includeBeta),
- {
- revalidateOnFocus: false,
- revalidateOnReconnect: true,
- }
- )
-
- return { release: data, error, mutate }
-}
diff --git a/web/hooks/useGetSystemResources.test.ts b/web/hooks/useGetSystemResources.test.ts
deleted file mode 100644
index 78392b612..000000000
--- a/web/hooks/useGetSystemResources.test.ts
+++ /dev/null
@@ -1,103 +0,0 @@
-// useGetSystemResources.test.ts
-
-import { renderHook, act } from '@testing-library/react'
-import useGetSystemResources from './useGetSystemResources'
-import { extensionManager } from '@/extension/ExtensionManager'
-
-// Mock the extensionManager
-jest.mock('@/extension/ExtensionManager', () => ({
- extensionManager: {
- get: jest.fn(),
- },
-}))
-
-// Mock the necessary dependencies
-jest.mock('jotai', () => ({
- useAtomValue: jest.fn(),
- useSetAtom: () => jest.fn(),
- useAtom: jest.fn(),
- atom: jest.fn(),
-}))
-
-describe('useGetSystemResources', () => {
- const mockMonitoringExtension = {
- getHardware: jest.fn(),
- getCurrentLoad: jest.fn(),
- }
-
- beforeEach(() => {
- jest.useFakeTimers()
- ;(extensionManager.get as jest.Mock).mockReturnValue(
- mockMonitoringExtension
- )
- })
-
- afterEach(() => {
- jest.clearAllMocks()
- jest.useRealTimers()
- })
-
- it('should fetch system resources on initial render', async () => {
- mockMonitoringExtension.getHardware.mockResolvedValue({
- cpu: { usage: 50 },
- ram: { available: 4000, total: 8000 },
- })
- mockMonitoringExtension.getCurrentLoad.mockResolvedValue({
- gpu: [],
- })
-
- const { result } = renderHook(() => useGetSystemResources())
-
- expect(mockMonitoringExtension.getHardware).toHaveBeenCalledTimes(1)
- })
-
- it('should start watching system resources when watch is called', () => {
- const { result } = renderHook(() => useGetSystemResources())
-
- act(() => {
- result.current.watch()
- })
-
- expect(mockMonitoringExtension.getHardware).toHaveBeenCalled()
-
- // Fast-forward time by 2 seconds
- act(() => {
- jest.advanceTimersByTime(2000)
- })
-
- expect(mockMonitoringExtension.getHardware).toHaveBeenCalled()
- })
-
- it('should stop watching when stopWatching is called', () => {
- const { result } = renderHook(() => useGetSystemResources())
-
- act(() => {
- result.current.watch()
- })
-
- act(() => {
- result.current.stopWatching()
- })
-
- // Fast-forward time by 2 seconds
- act(() => {
- jest.advanceTimersByTime(2000)
- })
-
- // Expect no additional calls after stopping
- expect(mockMonitoringExtension.getHardware).toHaveBeenCalled()
- })
-
- it('should not fetch resources if monitoring extension is not available', async () => {
- ;(extensionManager.get as jest.Mock).mockReturnValue(null)
-
- const { result } = renderHook(() => useGetSystemResources())
-
- await act(async () => {
- result.current.getSystemResources()
- })
-
- expect(mockMonitoringExtension.getHardware).not.toHaveBeenCalled()
- expect(mockMonitoringExtension.getCurrentLoad).not.toHaveBeenCalled()
- })
-})
diff --git a/web/hooks/useGetSystemResources.ts b/web/hooks/useGetSystemResources.ts
deleted file mode 100644
index e40100a55..000000000
--- a/web/hooks/useGetSystemResources.ts
+++ /dev/null
@@ -1,127 +0,0 @@
-/* eslint-disable @typescript-eslint/no-explicit-any */
-import { useCallback, useEffect, useState } from 'react'
-
-import { ExtensionTypeEnum, HardwareManagementExtension } from '@janhq/core'
-
-import { useSetAtom } from 'jotai'
-
-import { extensionManager } from '@/extension/ExtensionManager'
-import {
- cpuUsageAtom,
- totalRamAtom,
- usedRamAtom,
- nvidiaTotalVramAtom,
- gpusAtom,
- ramUtilitizedAtom,
- availableVramAtom,
-} from '@/helpers/atoms/SystemBar.atom'
-
-export default function useGetSystemResources() {
- const [intervalId, setIntervalId] = useState<
- NodeJS.Timeout | number | undefined
- >(undefined)
-
- const setGpus = useSetAtom(gpusAtom)
- const setCpuUsage = useSetAtom(cpuUsageAtom)
- const setTotalNvidiaVram = useSetAtom(nvidiaTotalVramAtom)
- const setAvailableVram = useSetAtom(availableVramAtom)
- const setUsedRam = useSetAtom(usedRamAtom)
- const setTotalRam = useSetAtom(totalRamAtom)
- const setRamUtilitized = useSetAtom(ramUtilitizedAtom)
-
- const getSystemResources = useCallback(async () => {
- if (
- !extensionManager.get(
- ExtensionTypeEnum.Hardware
- )
- ) {
- return
- }
-
- const hardwareExtension = extensionManager.get(
- ExtensionTypeEnum.Hardware
- )
-
- const hardwareInfo = await hardwareExtension?.getHardware()
-
- const usedMemory =
- Number(hardwareInfo?.ram.total) - Number(hardwareInfo?.ram.available)
-
- if (hardwareInfo?.ram?.total && hardwareInfo?.ram?.available)
- setUsedRam(Number(usedMemory))
-
- if (hardwareInfo?.ram?.total) setTotalRam(hardwareInfo.ram.total)
-
- const ramUtilitized =
- ((Number(usedMemory) ?? 0) / (hardwareInfo?.ram.total ?? 1)) * 100
-
- setRamUtilitized(Math.round(ramUtilitized))
-
- setCpuUsage(Math.round(hardwareInfo?.cpu.usage ?? 0))
-
- const gpus = hardwareInfo?.gpus ?? []
- setGpus(gpus as any)
-
- let totalNvidiaVram = 0
- if (gpus.length > 0) {
- totalNvidiaVram = gpus.reduce(
- (total: number, gpu: { total_vram: number }) =>
- total + Number(gpu.total_vram),
- 0
- )
- }
-
- setTotalNvidiaVram(totalNvidiaVram)
-
- setAvailableVram(
- gpus.reduce((total, gpu) => {
- return total + Number(gpu.free_vram || 0)
- }, 0)
- )
- }, [
- setUsedRam,
- setTotalRam,
- setRamUtilitized,
- setCpuUsage,
- setGpus,
- setTotalNvidiaVram,
- setAvailableVram,
- ])
-
- const watch = () => {
- getSystemResources()
-
- // Fetch interval - every 2s
- const itv = setInterval(() => {
- getSystemResources()
- }, 2000)
- setIntervalId(itv)
- }
- const stopWatching = useCallback(() => {
- if (intervalId) clearInterval(intervalId)
- }, [intervalId])
-
- useEffect(() => {
- getSystemResources()
- // Component did unmount
- // Stop watching if any
- return () => {
- stopWatching()
- }
- }, [getSystemResources, stopWatching])
-
- return {
- /**
- * Fetch resource information once
- */
- getSystemResources,
- /**
- * Fetch & watch for resource update
- */
- watch,
- /**
- * Stop watching
- */
- stopWatching,
- }
-}
diff --git a/web/hooks/useHardwareManagement.ts b/web/hooks/useHardwareManagement.ts
deleted file mode 100644
index a7103e50b..000000000
--- a/web/hooks/useHardwareManagement.ts
+++ /dev/null
@@ -1,95 +0,0 @@
-import { useMemo } from 'react'
-
-import { ExtensionTypeEnum, HardwareManagementExtension } from '@janhq/core'
-
-import { useSetAtom } from 'jotai'
-import useSWR from 'swr'
-
-import { extensionManager } from '@/extension/ExtensionManager'
-import {
- cpuUsageAtom,
- ramUtilitizedAtom,
- totalRamAtom,
- usedRamAtom,
-} from '@/helpers/atoms/SystemBar.atom'
-
-// fetcher function
-async function fetchExtensionData(
- extension: HardwareManagementExtension | null,
- method: (extension: HardwareManagementExtension) => Promise
-): Promise {
- if (!extension) {
- throw new Error('Extension not found')
- }
- return method(extension)
-}
-
-const getExtension = () =>
- extensionManager.get(
- ExtensionTypeEnum.Hardware
- ) ?? null
-
-/**
- * @returns A Promise that resolves to an object of list engines.
- */
-export function useGetHardwareInfo(updatePeriodically: boolean = true) {
- const setCpuUsage = useSetAtom(cpuUsageAtom)
- const setUsedRam = useSetAtom(usedRamAtom)
- const setTotalRam = useSetAtom(totalRamAtom)
- const setRamUtilitized = useSetAtom(ramUtilitizedAtom)
-
- const extension = useMemo(
- () =>
- extensionManager.get(
- ExtensionTypeEnum.Hardware
- ) ?? null,
- []
- )
-
- const {
- data: hardware,
- error,
- mutate,
- } = useSWR(
- extension ? 'hardware' : null,
- () => fetchExtensionData(extension, (ext) => ext.getHardware()),
- {
- revalidateOnFocus: false,
- revalidateOnReconnect: false,
- refreshInterval: updatePeriodically ? 2000 : undefined,
- onSuccess(data) {
- const usedMemory = data.ram.total - data.ram.available
- setUsedRam(usedMemory)
-
- setTotalRam(data.ram.total)
-
- const ramUtilitized = (usedMemory / data.ram.total) * 100
- setRamUtilitized(Math.round(ramUtilitized))
-
- setCpuUsage(Math.round(data.cpu.usage))
- },
- }
- )
-
- return { hardware, error, mutate }
-}
-
-/**
- * set gpus activate
- * @returns A Promise that resolves set gpus activate.
- */
-export const setActiveGpus = async (data: { gpus: number[] }) => {
- const extension = getExtension()
-
- if (!extension) {
- throw new Error('Extension is not available')
- }
-
- try {
- const response = await extension.setAvtiveGpu(data)
- return response
- } catch (error) {
- console.error('Failed to install engine variant:', error)
- throw error
- }
-}
diff --git a/web/hooks/useImportModel.test.ts b/web/hooks/useImportModel.test.ts
deleted file mode 100644
index 571947903..000000000
--- a/web/hooks/useImportModel.test.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-// useImportModel.test.ts
-
-import { renderHook, act } from '@testing-library/react'
-import { extensionManager } from '@/extension'
-import useImportModel from './useImportModel'
-
-// Mock dependencies
-jest.mock('@janhq/core')
-jest.mock('@/extension')
-jest.mock('@/containers/Toast')
-jest.mock('uuid', () => ({ v4: () => 'mocked-uuid' }))
-
-describe('useImportModel', () => {
- beforeEach(() => {
- jest.clearAllMocks()
- })
-
- it('should import models successfully', async () => {
- const mockImportModels = jest.fn().mockResolvedValue(undefined)
- const mockExtension = {
- importModel: mockImportModels,
- } as any
-
- jest.spyOn(extensionManager, 'get').mockReturnValue(mockExtension)
-
- const { result } = renderHook(() => useImportModel())
-
- const models = [
- { modelId: '1', path: '/path/to/model1' },
- { modelId: '2', path: '/path/to/model2' },
- ] as any
-
- await act(async () => {
- await result.current.importModels(models, 'copy')
- })
-
- expect(mockImportModels).toHaveBeenCalledWith('1', '/path/to/model1', undefined,'copy')
- expect(mockImportModels).toHaveBeenCalledWith('2', '/path/to/model2', undefined, 'copy')
- })
-
- it('should update model info successfully', async () => {
- const mockUpdateModelInfo = jest
- .fn()
- .mockResolvedValue({ id: 'model-1', name: 'Updated Model' })
- const mockExtension = {
- updateModel: mockUpdateModelInfo,
- } as any
-
- jest.spyOn(extensionManager, 'get').mockReturnValue(mockExtension)
-
- const { result } = renderHook(() => useImportModel())
-
- const modelInfo = { id: 'model-1', name: 'Updated Model' }
-
- await act(async () => {
- await result.current.updateModelInfo(modelInfo)
- })
-
- expect(mockUpdateModelInfo).toHaveBeenCalledWith(modelInfo)
- })
-
- it('should handle empty file paths', async () => {
- const { result } = renderHook(() => useImportModel())
-
- await act(async () => {
- await result.current.sanitizeFilePaths([])
- })
-
- // Expect no state changes or side effects
- })
-})
diff --git a/web/hooks/useImportModel.ts b/web/hooks/useImportModel.ts
deleted file mode 100644
index 94d11b455..000000000
--- a/web/hooks/useImportModel.ts
+++ /dev/null
@@ -1,150 +0,0 @@
-import { useCallback } from 'react'
-
-import {
- ExtensionTypeEnum,
- ImportingModel,
- Model,
- ModelEvent,
- ModelExtension,
- OptionType,
- events,
- fs,
-} from '@janhq/core'
-
-import { atom, useAtomValue, useSetAtom } from 'jotai'
-
-import { v4 as uuidv4 } from 'uuid'
-
-import { snackbar } from '@/containers/Toast'
-
-import { FilePathWithSize } from '@/utils/file'
-
-import { extensionManager } from '@/extension'
-import {
- addDownloadingModelAtom,
- downloadedModelsAtom,
- importingModelsAtom,
- removeDownloadingModelAtom,
- setImportingModelSuccessAtom,
-} from '@/helpers/atoms/Model.atom'
-
-export type ImportModelStage =
- | 'NONE'
- | 'SELECTING_MODEL'
- | 'CHOOSE_WHAT_TO_IMPORT'
- | 'MODEL_SELECTED'
- | 'IMPORTING_MODEL'
- | 'EDIT_MODEL_INFO'
- | 'CONFIRM_CANCEL'
-
-const importModelStageAtom = atom('NONE')
-
-export const getImportModelStageAtom = atom((get) => get(importModelStageAtom))
-
-export const setImportModelStageAtom = atom(
- null,
- (_get, set, stage: ImportModelStage) => {
- set(importModelStageAtom, stage)
- }
-)
-
-export type ModelUpdate = {
- name: string
- description: string
- tags: string[]
-}
-
-const useImportModel = () => {
- const setImportModelStage = useSetAtom(setImportModelStageAtom)
- const setImportingModels = useSetAtom(importingModelsAtom)
- const addDownloadingModel = useSetAtom(addDownloadingModelAtom)
- const removeDownloadingModel = useSetAtom(removeDownloadingModelAtom)
- const downloadedModels = useAtomValue(downloadedModelsAtom)
- const setImportingModelSuccess = useSetAtom(setImportingModelSuccessAtom)
-
- const incrementalModelName = useCallback(
- (name: string, startIndex: number = 0): string => {
- const newModelName = startIndex ? `${name}-${startIndex}` : name
- if (downloadedModels.some((model) => model.id === newModelName)) {
- return incrementalModelName(name, startIndex + 1)
- } else {
- return newModelName
- }
- },
- [downloadedModels]
- )
-
- const importModels = useCallback(
- (models: ImportingModel[], optionType: OptionType) => {
- models.map(async (model) => {
- const modelId = model.modelId ?? incrementalModelName(model.name)
- if (modelId) {
- addDownloadingModel(modelId)
- extensionManager
- .get(ExtensionTypeEnum.Model)
- ?.importModel(modelId, model.path, model.name, optionType)
- .finally(() => {
- removeDownloadingModel(modelId)
-
- events.emit(ModelEvent.OnModelsUpdate, { fetch: true })
- setImportingModelSuccess(model.importId, modelId)
- })
- }
- })
- },
- [addDownloadingModel, incrementalModelName, removeDownloadingModel]
- )
-
- const updateModelInfo = useCallback(
- async (modelInfo: Partial) => localUpdateModelInfo(modelInfo),
- []
- )
-
- const sanitizeFilePaths = useCallback(
- async (filePaths: string[]) => {
- if (!filePaths || filePaths.length === 0) return
- const { unsupportedFiles, supportedFiles } = (await fs.getGgufFiles(
- filePaths
- )) as unknown as {
- unsupportedFiles: FilePathWithSize[]
- supportedFiles: FilePathWithSize[]
- }
-
- const importingModels: ImportingModel[] = supportedFiles.map(
- ({ path, name, size }: FilePathWithSize) => ({
- importId: uuidv4(),
- modelId: undefined,
- name: name.replace(/ /g, '').replace('.gguf', ''),
- description: '',
- path: path,
- tags: [],
- size: size,
- status: 'PREPARING',
- format: 'gguf',
- })
- )
- if (unsupportedFiles.length > 0) {
- snackbar({
- description: `Only files with .gguf extension can be imported.`,
- type: 'error',
- })
- }
- if (importingModels.length === 0) return
-
- setImportingModels(importingModels)
- setImportModelStage('MODEL_SELECTED')
- },
- [setImportModelStage, setImportingModels]
- )
-
- return { importModels, updateModelInfo, sanitizeFilePaths }
-}
-
-const localUpdateModelInfo = async (
- modelInfo: Partial
-): Promise =>
- extensionManager
- .get(ExtensionTypeEnum.Model)
- ?.updateModel(modelInfo)
-
-export default useImportModel
diff --git a/web/hooks/useLoadTheme.test.ts b/web/hooks/useLoadTheme.test.ts
deleted file mode 100644
index c378fb2e6..000000000
--- a/web/hooks/useLoadTheme.test.ts
+++ /dev/null
@@ -1,118 +0,0 @@
-import { renderHook, act } from '@testing-library/react'
-import { useTheme } from 'next-themes'
-import { fs, joinPath } from '@janhq/core'
-import { useAtom, useAtomValue, useSetAtom } from 'jotai'
-
-import { useLoadTheme } from './useLoadTheme'
-import {
- selectedThemeIdAtom,
- themeDataAtom,
-} from '@/helpers/atoms/Setting.atom'
-
-// Mock dependencies
-jest.mock('next-themes')
-jest.mock('@janhq/core')
-
-// Mock dependencies
-jest.mock('jotai', () => ({
- useAtomValue: jest.fn(),
- useSetAtom: jest.fn(),
- useAtom: jest.fn(),
- atom: jest.fn(),
-}))
-
-describe('useLoadTheme', () => {
- beforeEach(() => {
- jest.clearAllMocks()
- })
-
- const mockSelectedThemeId = 'joi-light'
- const mockThemeData = {
- id: 'joi-light',
- displayName: 'Joi Light',
- nativeTheme: 'light',
- variables: {
- '--primary-color': '#007bff',
- },
- }
-
- it('should load theme and set variables', async () => {
- const readTheme = jest.fn().mockResolvedValue("{}")
-
- global.window.core = {
- api: {
- getThemes: () => ['joi-light', 'joi-dark'],
- readTheme,
- },
- }
- // Mock Jotai hooks
- ;(useAtomValue as jest.Mock).mockImplementation((atom) => {
- switch (atom) {
- default:
- return undefined
- }
- })
- ;(useSetAtom as jest.Mock).mockReturnValue(jest.fn())
- ;(useAtom as jest.Mock).mockImplementation((atom) => {
- switch (atom) {
- case selectedThemeIdAtom:
- return [mockSelectedThemeId, jest.fn()]
- case themeDataAtom:
- return [mockThemeData, jest.fn()]
- default:
- return [undefined, jest.fn()]
- }
- })
-
- // Mock fs and joinPath
- ;(fs.readdirSync as jest.Mock).mockResolvedValue(['joi-light', 'joi-dark'])
- ;(fs.readFileSync as jest.Mock).mockResolvedValue(
- JSON.stringify(mockThemeData)
- )
- ;(joinPath as jest.Mock).mockImplementation((paths) => paths.join('/'))
-
- // Mock setTheme from next-themes
- const mockSetTheme = jest.fn()
- ;(useTheme as jest.Mock).mockReturnValue({ setTheme: mockSetTheme })
-
- const { result } = renderHook(() => useLoadTheme())
-
- await act(async () => {
- await result.current
- })
-
- // Assertions
- expect(readTheme).toHaveBeenLastCalledWith({ themeName: 'joi-light' })
- })
-
- it('should set default theme if no selected theme', async () => {
- // Mock Jotai hooks with empty selected theme
- ;(useSetAtom as jest.Mock).mockReturnValue(jest.fn())
- ;(useAtom as jest.Mock).mockReturnValue(['', jest.fn()])
- ;(useAtom as jest.Mock).mockReturnValue([{}, jest.fn()])
-
- const mockSetSelectedThemeId = jest.fn()
- ;(useAtom as jest.Mock).mockReturnValue(['', mockSetSelectedThemeId])
-
- const { result } = renderHook(() => useLoadTheme())
-
- await act(async () => {
- await result.current
- })
-
- expect(mockSetSelectedThemeId).toHaveBeenCalledWith('joi-light')
- })
-
- it('should handle missing janDataFolderPath', async () => {
- // Mock Jotai hooks with empty janDataFolderPath
- ;(useAtomValue as jest.Mock).mockReturnValue('')
-
- const { result } = renderHook(() => useLoadTheme())
-
- await act(async () => {
- await result.current
- })
-
- expect(fs.readdirSync).not.toHaveBeenCalled()
- })
-})
diff --git a/web/hooks/useLoadTheme.ts b/web/hooks/useLoadTheme.ts
deleted file mode 100644
index 7631ee235..000000000
--- a/web/hooks/useLoadTheme.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-import { useCallback, useEffect } from 'react'
-
-import { useTheme } from 'next-themes'
-
-import { useAtom } from 'jotai'
-
-import cssVars from '@/utils/jsonToCssVariables'
-
-import {
- selectedThemeIdAtom,
- themeDataAtom,
- themesOptionsAtom,
-} from '@/helpers/atoms/Setting.atom'
-
-type NativeThemeProps = 'light' | 'dark'
-
-export const useLoadTheme = () => {
- const [themeOptions, setThemeOptions] = useAtom(themesOptionsAtom)
- const [themeData, setThemeData] = useAtom(themeDataAtom)
- const [selectedIdTheme, setSelectedIdTheme] = useAtom(selectedThemeIdAtom)
- const { setTheme } = useTheme()
-
- const setNativeTheme = useCallback(
- (nativeTheme: NativeThemeProps) => {
- if (!window.electronAPI) return
-
- if (nativeTheme === 'dark') {
- window?.core?.api?.setNativeThemeDark()
- setTheme('dark')
- localStorage.setItem('nativeTheme', 'dark')
- } else {
- window?.core?.api?.setNativeThemeLight()
- setTheme('light')
- localStorage.setItem('nativeTheme', 'light')
- }
- },
- [setTheme]
- )
-
- const applyTheme = (theme: Theme) => {
- if (!theme.variables) return
- const variables = cssVars(theme.variables)
- const headTag = document.getElementsByTagName('head')[0]
- const styleTag = document.createElement('style')
- styleTag.innerHTML = `:root {${variables}}`
- headTag.appendChild(styleTag)
- }
-
- const getThemes = useCallback(async () => {
- const installedThemes = await window.core.api.getThemes()
-
- const themesOptions: { name: string; value: string }[] =
- installedThemes.map((x: string) => ({
- name: x
- .replace(/-/g, ' ')
- .replace(/\b\w/g, (char) => char.toUpperCase()),
- value: x,
- }))
- setThemeOptions(themesOptions)
-
- if (!selectedIdTheme.length) return setSelectedIdTheme('joi-light')
- const theme: Theme = JSON.parse(
- await window.core.api.readTheme({
- themeName: selectedIdTheme,
- })
- )
-
- setThemeData(theme)
- setNativeTheme(theme.nativeTheme)
- applyTheme(theme)
- }, [selectedIdTheme])
-
- const configureTheme = useCallback(async () => {
- if (!themeData || !themeOptions) {
- getThemes()
- } else {
- applyTheme(themeData)
- }
- setNativeTheme(themeData?.nativeTheme as NativeThemeProps)
- }, [themeData, themeOptions, getThemes, setNativeTheme])
-
- useEffect(() => {
- configureTheme()
- }, [themeData])
-
- useEffect(() => {
- getThemes()
- }, [])
-}
diff --git a/web/hooks/useLogs.test.ts b/web/hooks/useLogs.test.ts
deleted file mode 100644
index a7a055bbd..000000000
--- a/web/hooks/useLogs.test.ts
+++ /dev/null
@@ -1,103 +0,0 @@
-// useLogs.test.ts
-
-import { renderHook, act } from '@testing-library/react'
-import { useAtomValue } from 'jotai'
-import { fs, joinPath, openFileExplorer } from '@janhq/core'
-
-import { useLogs } from './useLogs'
-
-// Mock dependencies
-jest.mock('jotai', () => ({
- useAtomValue: jest.fn(),
- atom: jest.fn(),
-}))
-
-jest.mock('@janhq/core', () => ({
- fs: {
- existsSync: jest.fn(),
- readFileSync: jest.fn(),
- writeFileSync: jest.fn(),
- },
- joinPath: jest.fn(),
- openFileExplorer: jest.fn(),
-}))
-
-describe('useLogs', () => {
- beforeEach(() => {
- jest.clearAllMocks()
- ;(useAtomValue as jest.Mock).mockReturnValue('/mock/jan/data/folder')
- })
-
- it('should get logs and sanitize them', async () => {
- const mockLogs = '/mock/jan/data/folder/some/log/content'
- const expectedSanitizedLogs = 'jan-data-folder/some/log/content'
-
- ;(joinPath as jest.Mock).mockResolvedValue('file://logs/test.log')
- ;(fs.existsSync as jest.Mock).mockResolvedValue(true)
- ;(fs.readFileSync as jest.Mock).mockResolvedValue(mockLogs)
-
- const { result } = renderHook(() => useLogs())
-
- await act(async () => {
- const logs = await result.current.getLogs('test')
- expect(logs).toBe(expectedSanitizedLogs)
- })
-
- expect(joinPath).toHaveBeenCalledWith(['file://logs', 'test.log'])
- expect(fs.existsSync).toHaveBeenCalledWith('file://logs/test.log')
- expect(fs.readFileSync).toHaveBeenCalledWith(
- 'file://logs/test.log',
- 'utf-8'
- )
- })
-
- it('should return empty string if log file does not exist', async () => {
- ;(joinPath as jest.Mock).mockResolvedValue('file://logs/nonexistent.log')
- ;(fs.existsSync as jest.Mock).mockResolvedValue(false)
-
- const { result } = renderHook(() => useLogs())
-
- await act(async () => {
- const logs = await result.current.getLogs('nonexistent')
- expect(logs).toBe('')
- })
-
- expect(fs.readFileSync).not.toHaveBeenCalled()
- })
-
- it('should open server log', async () => {
- ;(joinPath as jest.Mock).mockResolvedValue(
- '/mock/jan/data/folder/logs/app.log'
- )
- ;(openFileExplorer as jest.Mock).mockResolvedValue(undefined)
-
- const { result } = renderHook(() => useLogs())
-
- await act(async () => {
- await result.current.openServerLog()
- })
-
- expect(joinPath).toHaveBeenCalledWith([
- '/mock/jan/data/folder',
- 'logs',
- 'app.log',
- ])
- expect(openFileExplorer).toHaveBeenCalledWith(
- '/mock/jan/data/folder/logs/app.log'
- )
- })
-
- it('should clear server log', async () => {
- ;(joinPath as jest.Mock).mockResolvedValue('file://logs/app.log')
- ;(fs.writeFileSync as jest.Mock).mockResolvedValue(undefined)
-
- const { result } = renderHook(() => useLogs())
-
- await act(async () => {
- await result.current.clearServerLog()
- })
-
- expect(joinPath).toHaveBeenCalledWith(['file://logs', 'app.log'])
- expect(fs.writeFileSync).toHaveBeenCalledWith('file://logs/app.log', '')
- })
-})
diff --git a/web/hooks/useLogs.ts b/web/hooks/useLogs.ts
deleted file mode 100644
index a391a2278..000000000
--- a/web/hooks/useLogs.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { useCallback } from 'react'
-
-import { fs, joinPath, openFileExplorer } from '@janhq/core'
-import { useAtomValue } from 'jotai'
-
-import { janDataFolderPathAtom } from '@/helpers/atoms/AppConfig.atom'
-
-export const useLogs = () => {
- const janDataFolderPath = useAtomValue(janDataFolderPathAtom)
-
- const getLogs = useCallback(
- async (file: string): Promise => {
- const path = await joinPath(['file://logs', `${file}.log`])
- if (!(await fs.existsSync(path))) return ''
- const logs = await fs.readFileSync(path, 'utf-8')
-
- const sanitizedLogs = logs.replace(
- new RegExp(`${janDataFolderPath}\\/`, 'g'),
- 'jan-data-folder/'
- )
-
- return sanitizedLogs
- },
- [janDataFolderPath]
- )
-
- const openServerLog = useCallback(async () => {
- const fullPath = await joinPath([janDataFolderPath, 'logs', 'app.log'])
- return openFileExplorer(fullPath)
- }, [janDataFolderPath])
-
- const clearServerLog = useCallback(async () => {
- await fs.writeFileSync(await joinPath(['file://logs', 'app.log']), '')
- }, [])
-
- return { getLogs, openServerLog, clearServerLog }
-}
diff --git a/web/hooks/useModelSource.ts b/web/hooks/useModelSource.ts
deleted file mode 100644
index f9e01802a..000000000
--- a/web/hooks/useModelSource.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-import { useMemo } from 'react'
-
-import { ExtensionTypeEnum, ModelExtension } from '@janhq/core'
-import useSWR from 'swr'
-
-import { extensionManager } from '@/extension/ExtensionManager'
-
-/**
- * @returns A Promise that resolves to an object of model sources.
- */
-export function useGetModelSources() {
- const extension = useMemo(
- () => extensionManager.get(ExtensionTypeEnum.Model),
- []
- )
-
- const {
- data: sources,
- error,
- mutate,
- } = useSWR(
- extension ? 'getSources' : null,
- () =>
- extension?.getSources().then((e) =>
- e.map((m) => ({
- ...m,
- models: m.models.sort((a, b) => a.size - b.size),
- }))
- ),
- {
- revalidateOnFocus: false,
- revalidateOnReconnect: true,
- }
- )
-
- return { sources, error, mutate }
-}
-
-/**
- * @returns A Promise that resolves to featured model sources.
- */
-export function useGetFeaturedSources() {
- const { sources, error, mutate } = useGetModelSources()
-
- return {
- sources: sources?.filter((e) => e.metadata?.tags?.includes('featured')),
- error,
- mutate,
- }
-}
-
-/**
- * @returns A Promise that resolves to model source mutation.
- */
-export const useModelSourcesMutation = () => {
- const extension = useMemo(
- () => extensionManager.get(ExtensionTypeEnum.Model),
- []
- )
- /**
- * Add a new model source
- * @returns A Promise that resolves to intall of engine.
- */
- const addModelSource = async (source: string) => {
- try {
- // Call the extension's method
- return await extension?.addSource(source)
- } catch (error) {
- console.error('Failed to install engine variant:', error)
- throw error
- }
- }
-
- /**
- * Delete a new model source
- * @returns A Promise that resolves to intall of engine.
- */
- const deleteModelSource = async (source: string) => {
- try {
- // Call the extension's method
- return await extension?.deleteSource(source)
- } catch (error) {
- console.error('Failed to install engine variant:', error)
- throw error
- }
- }
- return { addModelSource, deleteModelSource }
-}
diff --git a/web/hooks/useModels.test.ts b/web/hooks/useModels.test.ts
deleted file mode 100644
index 331dfd67b..000000000
--- a/web/hooks/useModels.test.ts
+++ /dev/null
@@ -1,143 +0,0 @@
-// useModels.test.ts
-import { renderHook, act, waitFor } from '@testing-library/react'
-import { events, ModelEvent, ModelManager } from '@janhq/core'
-import { extensionManager } from '@/extension'
-
-// Mock dependencies
-jest.mock('@janhq/core')
-jest.mock('@/extension')
-jest.mock('use-debounce', () => ({
- useDebouncedCallback: jest.fn().mockImplementation((fn) => fn),
-}))
-
-import useModels from './useModels'
-
-// Mock data
-const models = [
- { id: 'model-1', name: 'Model 1' },
- { id: 'model-2', name: 'Model 2' },
-]
-
-describe('useModels', () => {
- beforeEach(() => {
- jest.clearAllMocks()
- })
-
- it('should fetch and set models on mount', async () => {
- const mockModelExtension = {
- getModels: jest.fn().mockResolvedValue(models),
- } as any
- ;(ModelManager.instance as jest.Mock).mockReturnValue({
- models: {
- values: () => ({
- toArray: () => ({
- filter: () => models,
- }),
- }),
- get: () => undefined,
- has: () => true,
- },
- })
-
- jest.spyOn(extensionManager, 'get').mockReturnValue(mockModelExtension)
-
- const { result } = renderHook(() => useModels())
- await act(() => {
- result.current?.getData()
- })
-
- expect(mockModelExtension.getModels).toHaveBeenCalled()
- })
-
- it('should return empty on error', async () => {
- const mockModelExtension = {
- getModels: jest.fn().mockRejectedValue(new Error('Error')),
- } as any
- ;(ModelManager.instance as jest.Mock).mockReturnValue({
- models: {
- values: () => ({
- toArray: () => ({
- filter: () => models,
- }),
- }),
- get: () => undefined,
- has: () => true,
- },
- })
-
- jest.spyOn(extensionManager, 'get').mockReturnValue(mockModelExtension)
-
- const { result } = renderHook(() => useModels())
-
- await act(() => {
- result.current?.getData()
- })
-
- expect(mockModelExtension.getModels()).rejects.toThrow()
- })
-
- it('should update states on models update', async () => {
- const mockModelExtension = {
- getModels: jest.fn().mockResolvedValue(models),
- } as any
-
- ;(ModelManager.instance as jest.Mock).mockReturnValue({
- models: {
- values: () => ({
- toArray: () => ({
- filter: () => models,
- }),
- }),
- get: () => undefined,
- has: () => true,
- },
- })
-
- jest.spyOn(extensionManager, 'get').mockReturnValue(mockModelExtension)
- jest.spyOn(events, 'on').mockImplementationOnce((event, cb) => {
- cb({ fetch: false })
- })
- renderHook(() => useModels())
-
- expect(mockModelExtension.getModels).not.toHaveBeenCalled()
- })
-
- it('should update states on models update', async () => {
- const mockModelExtension = {
- getModels: jest.fn().mockResolvedValue(models),
- } as any
-
- ;(ModelManager.instance as jest.Mock).mockReturnValue({
- models: {
- values: () => ({
- toArray: () => ({
- filter: () => models,
- }),
- }),
- get: () => undefined,
- has: () => true,
- },
- })
-
- jest.spyOn(extensionManager, 'get').mockReturnValue(mockModelExtension)
- jest.spyOn(events, 'on').mockImplementationOnce((event, cb) => {
- cb({ fetch: true })
- })
- renderHook(() => useModels())
-
- expect(mockModelExtension.getModels).toHaveBeenCalled()
- })
-
- it('should remove event listener on unmount', async () => {
- const removeListenerSpy = jest.spyOn(events, 'off')
-
- const { unmount } = renderHook(() => useModels())
-
- unmount()
-
- expect(removeListenerSpy).toHaveBeenCalledWith(
- ModelEvent.OnModelsUpdate,
- expect.any(Function)
- )
- })
-})
diff --git a/web/hooks/useModels.ts b/web/hooks/useModels.ts
deleted file mode 100644
index 3f11ba58c..000000000
--- a/web/hooks/useModels.ts
+++ /dev/null
@@ -1,119 +0,0 @@
-import { useCallback, useEffect } from 'react'
-
-import {
- ExtensionTypeEnum,
- Model,
- ModelEvent,
- ModelExtension,
- events,
- ModelManager,
- InferenceEngine,
-} from '@janhq/core'
-
-import { useSetAtom } from 'jotai'
-
-import { useDebouncedCallback } from 'use-debounce'
-
-import { extensionManager } from '@/extension'
-
-import {
- configuredModelsAtom,
- downloadedModelsAtom,
-} from '@/helpers/atoms/Model.atom'
-
-/**
- * useModels hook - Handles the state of models
- * It fetches the downloaded models, configured models and default model from Model Extension
- * and updates the atoms accordingly.
- */
-const useModels = () => {
- const setDownloadedModels = useSetAtom(downloadedModelsAtom)
- const setExtensionModels = useSetAtom(configuredModelsAtom)
-
- const getData = useCallback(() => {
- const getDownloadedModels = async () => {
- const localModels = (await getModels())
- .map((e) => ({
- ...e,
- name:
- ModelManager.instance().models.get(e.id)?.name ?? e.name ?? e.id,
- metadata:
- ModelManager.instance().models.get(e.id)?.metadata ?? e.metadata,
- }))
- .filter((e) => !('status' in e) || e.status !== 'downloadable')
-
- const remoteModels = Array.from(
- ModelManager.instance().models.values()
- ).filter((e) => e.engine !== InferenceEngine.cortex_llamacpp)
- const toUpdate = [
- ...localModels,
- ...remoteModels.filter(
- (e: Model) => !localModels.some((g: Model) => g.id === e.id)
- ),
- ]
-
- setDownloadedModels(toUpdate)
-
- let isUpdated = false
-
- toUpdate.forEach((model) => {
- if (!ModelManager.instance().models.has(model.id)) {
- ModelManager.instance().models.set(model.id, model)
- // eslint-disable-next-line react-hooks/exhaustive-deps
- isUpdated = true
- }
- })
- if (isUpdated) {
- getExtensionModels()
- }
- }
-
- const getExtensionModels = () => {
- const models = Array.from(ModelManager.instance().models.values())
- setExtensionModels(models)
- }
- // Fetch all data
- getExtensionModels()
- getDownloadedModels()
- }, [setDownloadedModels, setExtensionModels])
-
- const reloadData = useDebouncedCallback(() => getData(), 300)
-
- const updateStates = useCallback(() => {
- const cachedModels = Array.from(ModelManager.instance().models.values())
- setDownloadedModels((downloadedModels) => [
- ...downloadedModels,
- ...cachedModels.filter(
- (e) =>
- e.engine !== InferenceEngine.cortex_llamacpp &&
- !downloadedModels.some((g: Model) => g.id === e.id)
- ),
- ])
-
- setExtensionModels(cachedModels)
- }, [setDownloadedModels, setExtensionModels])
-
- const getModels = async (): Promise =>
- extensionManager
- .get(ExtensionTypeEnum.Model)
- ?.getModels()
- .catch(() => []) ?? []
-
- useEffect(() => {
- // Listen for model updates
- events.on(ModelEvent.OnModelsUpdate, async (data: { fetch?: boolean }) => {
- if (data.fetch) reloadData()
- else updateStates()
- })
- return () => {
- // Remove listener on unmount
- events.off(ModelEvent.OnModelsUpdate, async () => {})
- }
- }, [reloadData, updateStates])
-
- return {
- getData,
- }
-}
-
-export default useModels
diff --git a/web/hooks/usePath.ts b/web/hooks/usePath.ts
deleted file mode 100644
index 6ff1a73bd..000000000
--- a/web/hooks/usePath.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import { openFileExplorer, joinPath, baseName } from '@janhq/core'
-import { useAtomValue } from 'jotai'
-
-import { getFileInfo } from '@/utils/file'
-
-import { janDataFolderPathAtom } from '@/helpers/atoms/AppConfig.atom'
-import { activeThreadAtom } from '@/helpers/atoms/Thread.atom'
-
-export const usePath = () => {
- const janDataFolderPath = useAtomValue(janDataFolderPathAtom)
- const activeThread = useAtomValue(activeThreadAtom)
-
- const onRevealInFinder = async (path: string) => {
- const fullPath = await joinPath([janDataFolderPath, path])
- openFileExplorer(fullPath)
- }
-
- const onViewFile = async (id: string) => {
- if (!activeThread) return
-
- id = await baseName(id)
-
- // New ID System
- if (!id.startsWith('file-')) {
- const threadFilePath = await joinPath([
- janDataFolderPath,
- 'threads',
- `${activeThread.id}/files`,
- id,
- ])
- openFileExplorer(threadFilePath)
- } else {
- id = id.split('.')[0]
- const fileName = (await getFileInfo(id)).filename
- const filesPath = await joinPath([janDataFolderPath, 'files', fileName])
- openFileExplorer(filesPath)
- }
- }
-
- const onViewFileContainer = async () => {
- if (!activeThread) return
-
- let filePath = undefined
- filePath = await joinPath(['threads', `${activeThread.id}/files`])
- if (!filePath) return
- const fullPath = await joinPath([janDataFolderPath, filePath])
- openFileExplorer(fullPath)
- }
-
- return {
- onRevealInFinder,
- onViewFile,
- onViewFileContainer,
- }
-}
diff --git a/web/hooks/useRecommendedModel.ts b/web/hooks/useRecommendedModel.ts
deleted file mode 100644
index 4107087f8..000000000
--- a/web/hooks/useRecommendedModel.ts
+++ /dev/null
@@ -1,114 +0,0 @@
-import { useCallback, useEffect, useState } from 'react'
-
-import { Model, InferenceEngine } from '@janhq/core'
-
-import { atom, useAtomValue } from 'jotai'
-
-import { activeModelAtom } from './useActiveModel'
-
-import { useGetEngines } from './useEngineManagement'
-
-import { activeAssistantAtom } from '@/helpers/atoms/Assistant.atom'
-import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
-import { activeThreadAtom } from '@/helpers/atoms/Thread.atom'
-
-export const lastUsedModel = atom(undefined)
-
-export const LAST_USED_MODEL_ID = 'last-used-model-id'
-
-/**
- * A hook that return the recommended model when user
- * wants to create a new thread.
- *
- * The precedence is as follows:
- * 1. Active model
- * 2. If no active model(s), then the last used model
- * 3. If no active or last used model, then the 1st model on the list
- */
-export default function useRecommendedModel() {
- const activeModel = useAtomValue(activeModelAtom)
- const [sortedModels, setSortedModels] = useState([])
- const [recommendedModel, setRecommendedModel] = useState()
- const activeThread = useAtomValue(activeThreadAtom)
- const downloadedModels = useAtomValue(downloadedModelsAtom)
- const activeAssistant = useAtomValue(activeAssistantAtom)
- const { engines } = useGetEngines()
-
- const getAndSortDownloadedModels = useCallback(async (): Promise => {
- const models = downloadedModels.sort((a, b) =>
- a.engine !== InferenceEngine.cortex_llamacpp &&
- b.engine === InferenceEngine.cortex_llamacpp
- ? 1
- : -1
- )
- setSortedModels(models)
- return models
- }, [downloadedModels])
-
- const getRecommendedModel = useCallback(async (): Promise<
- Model | undefined
- > => {
- const models = (await getAndSortDownloadedModels()).filter((e: Model) =>
- engines?.[e.engine]?.[0].type === 'local' ||
- (engines?.[e.engine]?.[0].api_key?.length ?? 0) > 0
- ? true
- : false
- )
-
- if (!activeThread || !activeAssistant) return
- const modelId = activeAssistant.model.id
- const model = models.find((model) => model.id === modelId)
-
- if (model) {
- setRecommendedModel(model)
- }
-
- if (activeModel) {
- // if we have active model alr, then we can just use that
- console.debug(`Using active model ${activeModel.id}`)
- setRecommendedModel(activeModel)
- return
- }
-
- // sort the model, for display purpose
- if (models.length === 0) {
- // if we have no downloaded models, then can't recommend anything
- setRecommendedModel(undefined)
- return
- }
-
- // otherwise, get the last used model id
- const lastUsedModelId = localStorage.getItem(LAST_USED_MODEL_ID)
-
- // if we don't have [lastUsedModelId], then we can just use the first model
- // in the downloaded list
- if (!lastUsedModelId) {
- setRecommendedModel(models[0])
- return
- }
-
- const lastUsedModel = models.find((model) => model.id === lastUsedModelId)
- if (!lastUsedModel) {
- // if we can't find the last used model, then we can just use the first model
- // in the downloaded list
- console.debug(
- `Last used model ${lastUsedModelId} not found, using first model in list ${models[0].id}}`
- )
- setRecommendedModel(models[0])
- return
- }
-
- setRecommendedModel(lastUsedModel)
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [getAndSortDownloadedModels, activeThread, engines])
-
- useEffect(() => {
- getRecommendedModel()
- }, [getRecommendedModel])
-
- return {
- recommendedModel,
- downloadedModels: sortedModels,
- setRecommendedModel,
- }
-}
diff --git a/web/hooks/useSendChatMessage.ts b/web/hooks/useSendChatMessage.ts
deleted file mode 100644
index 3e6131fac..000000000
--- a/web/hooks/useSendChatMessage.ts
+++ /dev/null
@@ -1,578 +0,0 @@
-import 'openai/shims/web'
-
-import { useEffect, useRef } from 'react'
-
-import {
- MessageRequestType,
- ExtensionTypeEnum,
- Thread,
- ThreadMessage,
- Model,
- ConversationalExtension,
- ThreadAssistantInfo,
- events,
- MessageEvent,
- EngineManager,
- InferenceEngine,
- MessageStatus,
-} from '@janhq/core'
-import { extractInferenceParams, extractModelLoadParams } from '@janhq/core'
-import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'
-
-import {
- ChatCompletionMessageParam,
- ChatCompletionTool,
- ChatCompletionMessageToolCall,
-} from 'openai/resources/chat'
-import { CompletionResponse, StreamCompletionResponse, TokenJS } from 'token.js'
-import { ulid } from 'ulidx'
-
-import { modelDropdownStateAtom } from '@/containers/ModelDropdown'
-import {
- currentPromptAtom,
- editPromptAtom,
- fileUploadAtom,
-} from '@/containers/Providers/Jotai'
-
-import { compressImage, getBase64 } from '@/utils/base64'
-import {
- createMessage,
- createMessageContent,
- emptyMessageContent,
-} from '@/utils/createMessage'
-import { MessageRequestBuilder } from '@/utils/messageRequestBuilder'
-
-import { ThreadMessageBuilder } from '@/utils/threadMessageBuilder'
-
-import { useActiveModel } from './useActiveModel'
-
-import {
- convertBuiltInEngine,
- extendBuiltInEngineModels,
- useGetEngines,
-} from './useEngineManagement'
-
-import { extensionManager } from '@/extension/ExtensionManager'
-import { activeAssistantAtom } from '@/helpers/atoms/Assistant.atom'
-import {
- addNewMessageAtom,
- deleteMessageAtom,
- getCurrentChatMessagesAtom,
- tokenSpeedAtom,
-} from '@/helpers/atoms/ChatMessage.atom'
-import { selectedModelAtom } from '@/helpers/atoms/Model.atom'
-import {
- activeThreadAtom,
- approvedThreadToolsAtom,
- disabledThreadToolsAtom,
- engineParamsUpdateAtom,
- getActiveThreadModelParamsAtom,
- isGeneratingResponseAtom,
- updateThreadAtom,
- updateThreadWaitingForResponseAtom,
-} from '@/helpers/atoms/Thread.atom'
-import { ModelTool } from '@/types/model'
-
-export const reloadModelAtom = atom(false)
-
-export default function useSendChatMessage(
- showModal?: (toolName: string, threadId: string) => Promise
-) {
- const activeThread = useAtomValue(activeThreadAtom)
- const activeAssistant = useAtomValue(activeAssistantAtom)
- const addNewMessage = useSetAtom(addNewMessageAtom)
- const updateThread = useSetAtom(updateThreadAtom)
- const updateThreadWaiting = useSetAtom(updateThreadWaitingForResponseAtom)
- const setCurrentPrompt = useSetAtom(currentPromptAtom)
- const deleteMessage = useSetAtom(deleteMessageAtom)
- const setEditPrompt = useSetAtom(editPromptAtom)
- const approvedTools = useAtomValue(approvedThreadToolsAtom)
- const disabledTools = useAtomValue(disabledThreadToolsAtom)
-
- const currentMessages = useAtomValue(getCurrentChatMessagesAtom)
- const selectedModel = useAtomValue(selectedModelAtom)
- const { activeModel, startModel } = useActiveModel()
-
- const modelRef = useRef()
- const activeModelParams = useAtomValue(getActiveThreadModelParamsAtom)
- const engineParamsUpdate = useAtomValue(engineParamsUpdateAtom)
-
- const setEngineParamsUpdate = useSetAtom(engineParamsUpdateAtom)
- const setReloadModel = useSetAtom(reloadModelAtom)
- const [fileUpload, setFileUpload] = useAtom(fileUploadAtom)
- const setIsGeneratingResponse = useSetAtom(isGeneratingResponseAtom)
- const activeThreadRef = useRef()
- const activeAssistantRef = useRef()
- const setTokenSpeed = useSetAtom(tokenSpeedAtom)
- const setModelDropdownState = useSetAtom(modelDropdownStateAtom)
-
- const selectedModelRef = useRef()
-
- const { engines } = useGetEngines()
-
- useEffect(() => {
- modelRef.current = activeModel
- }, [activeModel])
-
- useEffect(() => {
- activeThreadRef.current = activeThread
- }, [activeThread])
-
- useEffect(() => {
- selectedModelRef.current = selectedModel
- }, [selectedModel])
-
- useEffect(() => {
- activeAssistantRef.current = activeAssistant
- }, [activeAssistant])
-
- const resendChatMessage = async () => {
- // Delete last response before regenerating
- const newConvoData = Array.from(currentMessages)
- let toSendMessage = newConvoData.pop()
-
- while (toSendMessage && toSendMessage?.role !== 'user') {
- await extensionManager
- .get(ExtensionTypeEnum.Conversational)
- ?.deleteMessage(toSendMessage.thread_id, toSendMessage.id)
- .catch(console.error)
- deleteMessage(toSendMessage.id ?? '')
- toSendMessage = newConvoData.pop()
- }
-
- if (toSendMessage?.content[0]?.text?.value)
- sendChatMessage(toSendMessage.content[0].text.value, true, newConvoData)
- }
-
- const sendChatMessage = async (
- message: string,
- isResend: boolean = false,
- messages?: ThreadMessage[]
- ) => {
- if (!message || message.trim().length === 0) return
-
- const activeThread = activeThreadRef.current
- const activeAssistant = activeAssistantRef.current
- const currentModel = selectedModelRef.current
-
- if (!activeThread || !activeAssistant) {
- console.error('No active thread or assistant')
- return
- }
-
- if (!currentModel?.id) {
- setModelDropdownState(true)
- return
- }
-
- if (engineParamsUpdate) setReloadModel(true)
- setTokenSpeed(undefined)
-
- const runtimeParams = extractInferenceParams(activeModelParams)
- const settingParams = extractModelLoadParams(activeModelParams)
-
- const prompt = message.trim()
-
- updateThreadWaiting(activeThread.id, true)
- setCurrentPrompt('')
- setEditPrompt('')
-
- try {
- let base64Blob = fileUpload ? await getBase64(fileUpload.file) : undefined
-
- if (base64Blob && fileUpload?.type === 'image') {
- // Compress image
- base64Blob = await compressImage(base64Blob, 512)
- }
-
- const modelRequest = selectedModel ?? activeAssistant.model
-
- // Fallback support for previous broken threads
- if (activeAssistant.model?.id === '*') {
- activeAssistant.model = {
- id: currentModel.id,
- settings: currentModel.settings,
- parameters: currentModel.parameters,
- }
- }
- if (runtimeParams.stream == null) {
- runtimeParams.stream = true
- }
-
- // Build Message Request
- // TODO: detect if model supports tools
- const tools = (await window.core.api.getTools())
- ?.filter((tool: ModelTool) => !disabledTools.includes(tool.name))
- .map((tool: ModelTool) => ({
- type: 'function' as const,
- function: {
- name: tool.name,
- description: tool.description?.slice(0, 1024),
- parameters: tool.inputSchema,
- strict: false,
- },
- }))
- const requestBuilder = new MessageRequestBuilder(
- MessageRequestType.Thread,
- {
- ...modelRequest,
- settings: settingParams,
- parameters: runtimeParams,
- },
- activeThread,
- messages ?? currentMessages,
- tools && tools.length ? tools : undefined
- ).addSystemMessage(activeAssistant.instructions)
-
- requestBuilder.pushMessage(prompt, base64Blob, fileUpload)
-
- // Build Thread Message to persist
- const threadMessageBuilder = new ThreadMessageBuilder(
- requestBuilder
- ).pushMessage(prompt, base64Blob, fileUpload)
-
- const newMessage = threadMessageBuilder.build()
-
- // Update thread state
- const updatedThread: Thread = {
- ...activeThread,
- updated: newMessage.created_at,
- metadata: {
- ...activeThread.metadata,
- lastMessage: prompt,
- },
- }
- updateThread(updatedThread)
-
- if (
- !isResend &&
- (newMessage.content.length || newMessage.attachments?.length)
- ) {
- // Add message
- const createdMessage = await extensionManager
- .get(ExtensionTypeEnum.Conversational)
- ?.createMessage(newMessage)
- .catch(() => undefined)
-
- if (!createdMessage) return
-
- // Push to states
- addNewMessage(createdMessage)
- }
-
- // Start Model if not started
- const isCortex =
- modelRequest.engine == InferenceEngine.cortex ||
- modelRequest.engine == InferenceEngine.cortex_llamacpp
- const modelId = selectedModel?.id ?? activeAssistantRef.current?.model.id
-
- if (base64Blob) {
- setFileUpload(undefined)
- }
-
- if (modelRef.current?.id !== modelId && modelId && isCortex) {
- const error = await startModel(modelId).catch((error: Error) => error)
- if (error) {
- updateThreadWaiting(activeThread.id, false)
- return
- }
- }
- setIsGeneratingResponse(true)
-
- let isDone = false
-
- const engine =
- engines?.[requestBuilder.model.engine as InferenceEngine]?.[0]
- const apiKey = engine?.api_key
- const provider = convertBuiltInEngine(engine?.engine)
-
- const tokenJS = new TokenJS({
- apiKey: apiKey ?? (await window.core.api.appToken()),
- baseURL: apiKey ? undefined : `${API_BASE_URL}/v1`,
- })
-
- extendBuiltInEngineModels(tokenJS, provider, modelId)
-
- // llama.cpp currently does not support streaming when tools are used.
- const useStream =
- requestBuilder.tools && isCortex
- ? false
- : modelRequest.parameters?.stream
-
- let parentMessageId: string | undefined
- while (!isDone) {
- let messageId = ulid()
- if (!parentMessageId) {
- parentMessageId = ulid()
- messageId = parentMessageId
- }
- const data = requestBuilder.build()
- const message: ThreadMessage = createMessage({
- id: messageId,
- thread_id: activeThread.id,
- assistant_id: activeAssistant.assistant_id,
- metadata: {
- ...(messageId !== parentMessageId
- ? { parent_id: parentMessageId }
- : {}),
- },
- })
- events.emit(MessageEvent.OnMessageResponse, message)
-
- // we need to separate into 2 cases to appease linter
- const controller = new AbortController()
- EngineManager.instance().controller = controller
- if (useStream) {
- const response = await tokenJS.chat.completions.create(
- {
- stream: true,
- provider,
- messages: requestBuilder.messages as ChatCompletionMessageParam[],
- model: data.model?.id ?? '',
- tools: data.tools as ChatCompletionTool[],
- tool_choice: data.tools ? 'auto' : undefined,
- },
- {
- signal: controller.signal,
- }
- )
- // Variables to track and accumulate streaming content
- if (!message.content.length) {
- message.content = emptyMessageContent
- }
- isDone = await processStreamingResponse(
- response,
- requestBuilder,
- message
- )
- } else {
- const response = await tokenJS.chat.completions.create(
- {
- stream: false,
- provider,
- messages: requestBuilder.messages as ChatCompletionMessageParam[],
- model: data.model?.id ?? '',
- tools: data.tools as ChatCompletionTool[],
- tool_choice: data.tools ? 'auto' : undefined,
- },
- {
- signal: controller.signal,
- }
- )
- // Variables to track and accumulate streaming content
- if (!message.content.length) {
- message.content = emptyMessageContent
- }
- isDone = await processNonStreamingResponse(
- response,
- requestBuilder,
- message
- )
- }
-
- message.status = MessageStatus.Ready
- events.emit(MessageEvent.OnMessageUpdate, message)
- }
- } catch (error) {
- setIsGeneratingResponse(false)
- updateThreadWaiting(activeThread.id, false)
- const errorMessage: ThreadMessage = createMessage({
- thread_id: activeThread.id,
- assistant_id: activeAssistant.assistant_id,
- content: createMessageContent(
- typeof error === 'object' && error && 'message' in error
- ? (error as { message: string }).message
- : JSON.stringify(error)
- ),
- })
- events.emit(MessageEvent.OnMessageResponse, errorMessage)
-
- errorMessage.status = MessageStatus.Error
- events.emit(MessageEvent.OnMessageUpdate, errorMessage)
- }
-
- // Reset states
- setReloadModel(false)
- setEngineParamsUpdate(false)
- }
-
- const processNonStreamingResponse = async (
- response: CompletionResponse,
- requestBuilder: MessageRequestBuilder,
- message: ThreadMessage
- ): Promise => {
- // Handle tool calls in the response
- const toolCalls: ChatCompletionMessageToolCall[] =
- response.choices[0]?.message?.tool_calls ?? []
- const content = response.choices[0].message?.content
- message.content = createMessageContent(content ?? '')
- events.emit(MessageEvent.OnMessageUpdate, message)
- await postMessageProcessing(
- toolCalls ?? [],
- requestBuilder,
- message,
- content ?? ''
- )
- return !toolCalls || !toolCalls.length
- }
-
- const processStreamingResponse = async (
- response: StreamCompletionResponse,
- requestBuilder: MessageRequestBuilder,
- message: ThreadMessage
- ): Promise => {
- // Variables to track and accumulate streaming content
- let currentToolCall: {
- id: string
- function: { name: string; arguments: string }
- } | null = null
- let accumulatedContent = ''
- const toolCalls: ChatCompletionMessageToolCall[] = []
- // Process the streaming chunks
- for await (const chunk of response) {
- // Handle tool calls in the chunk
- if (chunk.choices[0]?.delta?.tool_calls) {
- const deltaToolCalls = chunk.choices[0].delta.tool_calls
-
- // Handle the beginning of a new tool call
- if (
- deltaToolCalls[0]?.index !== undefined &&
- deltaToolCalls[0]?.function
- ) {
- const index = deltaToolCalls[0].index
-
- // Create new tool call if this is the first chunk for it
- if (!toolCalls[index]) {
- toolCalls[index] = {
- id: deltaToolCalls[0]?.id || '',
- function: {
- name: deltaToolCalls[0]?.function?.name || '',
- arguments: deltaToolCalls[0]?.function?.arguments || '',
- },
- type: 'function',
- }
- currentToolCall = toolCalls[index]
- } else {
- // Continuation of existing tool call
- currentToolCall = toolCalls[index]
-
- // Append to function name or arguments if they exist in this chunk
- if (deltaToolCalls[0]?.function?.name) {
- currentToolCall!.function.name += deltaToolCalls[0].function.name
- }
-
- if (deltaToolCalls[0]?.function?.arguments) {
- currentToolCall!.function.arguments +=
- deltaToolCalls[0].function.arguments
- }
- }
- }
- }
-
- // Handle regular content in the chunk
- if (chunk.choices[0]?.delta?.content) {
- const content = chunk.choices[0].delta.content
- accumulatedContent += content
-
- message.content = createMessageContent(accumulatedContent)
- events.emit(MessageEvent.OnMessageUpdate, message)
- }
- }
-
- await postMessageProcessing(
- toolCalls ?? [],
- requestBuilder,
- message,
- accumulatedContent ?? ''
- )
- return !toolCalls || !toolCalls.length
- }
-
- const postMessageProcessing = async (
- toolCalls: ChatCompletionMessageToolCall[],
- requestBuilder: MessageRequestBuilder,
- message: ThreadMessage,
- content: string
- ) => {
- requestBuilder.pushAssistantMessage({
- content,
- role: 'assistant',
- refusal: null,
- tool_calls: toolCalls,
- })
-
- // Handle completed tool calls
- if (toolCalls.length > 0) {
- for (const toolCall of toolCalls) {
- const toolId = ulid()
- const toolCallsMetadata =
- message.metadata?.tool_calls &&
- Array.isArray(message.metadata?.tool_calls)
- ? message.metadata?.tool_calls
- : []
- message.metadata = {
- ...(message.metadata ?? {}),
- tool_calls: [
- ...toolCallsMetadata,
- {
- tool: {
- ...toolCall,
- id: toolId,
- },
- response: undefined,
- state: 'pending',
- },
- ],
- }
- events.emit(MessageEvent.OnMessageUpdate, message)
-
- const approved =
- approvedTools[message.thread_id]?.includes(toolCall.function.name) ||
- (showModal
- ? await showModal(toolCall.function.name, message.thread_id)
- : true)
-
- const result = approved
- ? await window.core.api.callTool({
- toolName: toolCall.function.name,
- arguments: JSON.parse(toolCall.function.arguments),
- })
- : {
- content: [
- {
- type: 'text',
- text: 'The user has chosen to disallow the tool call.',
- },
- ],
- }
- if (result.error) break
-
- message.metadata = {
- ...(message.metadata ?? {}),
- tool_calls: [
- ...toolCallsMetadata,
- {
- tool: {
- ...toolCall,
- id: toolId,
- },
- response: result,
- state: 'ready',
- },
- ],
- }
-
- requestBuilder.pushToolMessage(
- result.content[0]?.text ?? '',
- toolCall.id
- )
- events.emit(MessageEvent.OnMessageUpdate, message)
- }
- }
- }
-
- return {
- sendChatMessage,
- resendChatMessage,
- }
-}
diff --git a/web/hooks/useSetActiveThread.ts b/web/hooks/useSetActiveThread.ts
deleted file mode 100644
index 62baa4a87..000000000
--- a/web/hooks/useSetActiveThread.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import { ExtensionTypeEnum, Thread, ConversationalExtension } from '@janhq/core'
-
-import { useAtom, useAtomValue, useSetAtom } from 'jotai'
-
-import { extensionManager } from '@/extension'
-import { activeAssistantAtom } from '@/helpers/atoms/Assistant.atom'
-import {
- setConvoMessagesAtom,
- subscribedGeneratingMessageAtom,
-} from '@/helpers/atoms/ChatMessage.atom'
-import {
- getActiveThreadIdAtom,
- setActiveThreadIdAtom,
- setThreadModelParamsAtom,
-} from '@/helpers/atoms/Thread.atom'
-import { ModelParams } from '@/types/model'
-
-export default function useSetActiveThread() {
- const setActiveThreadId = useSetAtom(setActiveThreadIdAtom)
- const activeThreadId = useAtomValue(getActiveThreadIdAtom)
- const setThreadMessages = useSetAtom(setConvoMessagesAtom)
- const setThreadModelParams = useSetAtom(setThreadModelParamsAtom)
- const setActiveAssistant = useSetAtom(activeAssistantAtom)
- const [messageSubscriber, setMessageSubscriber] = useAtom(
- subscribedGeneratingMessageAtom
- )
-
- const setActiveThread = async (thread: Thread) => {
- if (!thread?.id || activeThreadId === thread.id) return
-
- setActiveThreadId(thread.id)
-
- try {
- const assistantInfo = await getThreadAssistant(thread.id)
- setActiveAssistant(assistantInfo)
- // Load local messages only if there are no messages in the state
- const messages = await getLocalThreadMessage(thread.id).catch(() => [])
- const modelParams: ModelParams = {
- ...assistantInfo?.model?.parameters,
- ...assistantInfo?.model?.settings,
- }
- setThreadModelParams(thread?.id, modelParams)
- setThreadMessages(thread.id, messages)
- if (messageSubscriber.thread_id !== thread.id) setMessageSubscriber({})
- } catch (e) {
- console.error(e)
- }
- }
-
- return { setActiveThread }
-}
-
-const getLocalThreadMessage = async (threadId: string) =>
- extensionManager
- .get(ExtensionTypeEnum.Conversational)
- ?.listMessages(threadId) ?? []
-
-const getThreadAssistant = async (threadId: string) =>
- extensionManager
- .get(ExtensionTypeEnum.Conversational)
- ?.getThreadAssistant(threadId)
diff --git a/web/hooks/useStarterScreen.ts b/web/hooks/useStarterScreen.ts
deleted file mode 100644
index f0bc7eeda..000000000
--- a/web/hooks/useStarterScreen.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-/* eslint-disable @typescript-eslint/no-unused-vars */
-import { useMemo } from 'react'
-
-import { InferenceEngine, EngineConfig } from '@janhq/core'
-import { useAtomValue } from 'jotai'
-
-import { isLocalEngine } from '@/utils/modelEngine'
-
-import { useGetEngines } from './useEngineManagement'
-
-import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
-import { threadsAtom } from '@/helpers/atoms/Thread.atom'
-
-export function useStarterScreen() {
- const downloadedModels = useAtomValue(downloadedModelsAtom)
- const threads = useAtomValue(threadsAtom)
-
- const { engines } = useGetEngines()
-
- const remoteEngines =
- engines &&
- Object.entries(engines)
- .filter(([key]) => !isLocalEngine(engines, key as InferenceEngine))
- .flatMap(([_, engineArray]) => engineArray as EngineConfig)
-
- const isDownloadALocalModel = useMemo(
- () =>
- downloadedModels.some((x) => engines && isLocalEngine(engines, x.engine)),
- [engines, downloadedModels]
- )
-
- const isAnyRemoteModelConfigured = useMemo(
- () => (remoteEngines ?? []).some((x) => x.api_key && x.api_key.length > 0),
- [remoteEngines]
- )
-
- const isShowStarterScreen = useMemo(
- () =>
- !isAnyRemoteModelConfigured && !isDownloadALocalModel && !threads.length,
- [isAnyRemoteModelConfigured, isDownloadALocalModel, threads]
- )
-
- return {
- isShowStarterScreen,
- }
-}
diff --git a/web/hooks/useThread.test.ts b/web/hooks/useThread.test.ts
deleted file mode 100644
index 4db7f87ac..000000000
--- a/web/hooks/useThread.test.ts
+++ /dev/null
@@ -1,192 +0,0 @@
-// useThreads.test.ts
-
-import { renderHook, act } from '@testing-library/react'
-import { useSetAtom } from 'jotai'
-import { ExtensionTypeEnum } from '@janhq/core'
-import { extensionManager } from '@/extension/ExtensionManager'
-import useThreads from './useThreads'
-import {
- threadDataReadyAtom,
- threadModelParamsAtom,
- threadsAtom,
- threadStatesAtom,
-} from '@/helpers/atoms/Thread.atom'
-
-// Mock the necessary dependencies
-jest.mock('jotai', () => ({
- useAtomValue: jest.fn(),
- useSetAtom: jest.fn(),
- useAtom: jest.fn(),
- atom: jest.fn(),
-}))
-jest.mock('@/extension/ExtensionManager')
-
-describe('useThreads', () => {
- beforeEach(() => {
- jest.clearAllMocks()
- })
-
- const mockThreads = [
- {
- id: 'thread1',
- metadata: { lastMessage: 'Hello' },
- assistants: [
- {
- model: {
- parameters: { param1: 'value1' },
- settings: { setting1: 'value1' },
- },
- },
- ],
- },
- {
- id: 'thread2',
- metadata: { lastMessage: 'Hi there' },
- assistants: [
- {
- model: {
- parameters: { param2: 'value2' },
- settings: { setting2: 'value2' },
- },
- },
- ],
- },
- ]
-
- it('should fetch and set threads data', async () => {
- // Mock Jotai hooks
- const mockSetThreadStates = jest.fn()
- const mockSetThreads = jest.fn()
- const mockSetThreadModelRuntimeParams = jest.fn()
- const mockSetThreadDataReady = jest.fn()
-
- ;(useSetAtom as jest.Mock).mockImplementation((atom) => {
- switch (atom) {
- case threadStatesAtom:
- return mockSetThreadStates
- case threadsAtom:
- return mockSetThreads
- case threadModelParamsAtom:
- return mockSetThreadModelRuntimeParams
- case threadDataReadyAtom:
- return mockSetThreadDataReady
- default:
- return jest.fn()
- }
- })
-
- // Mock extensionManager
- const mockGetThreads = jest.fn().mockResolvedValue(mockThreads)
- ;(extensionManager.get as jest.Mock).mockReturnValue({
- listThreads: mockGetThreads,
- })
-
- const { result } = renderHook(() => useThreads())
-
- await act(async () => {
- // Wait for useEffect to complete
- })
-
- // Assertions
- expect(extensionManager.get).toHaveBeenCalledWith(
- ExtensionTypeEnum.Conversational
- )
- expect(mockGetThreads).toHaveBeenCalled()
-
- expect(mockSetThreadStates).toHaveBeenCalledWith({
- thread1: {
- hasMore: false,
- waitingForResponse: false,
- lastMessage: 'Hello',
- },
- thread2: {
- hasMore: false,
- waitingForResponse: false,
- lastMessage: 'Hi there',
- },
- })
-
- expect(mockSetThreads).toHaveBeenCalledWith(mockThreads)
-
- expect(mockSetThreadModelRuntimeParams).toHaveBeenCalledWith({
- thread1: { param1: 'value1', setting1: 'value1' },
- thread2: { param2: 'value2', setting2: 'value2' },
- })
-
- expect(mockSetThreadDataReady).toHaveBeenCalledWith(true)
- })
-
- it('should handle empty threads', async () => {
- // Mock empty threads
- ;(extensionManager.get as jest.Mock).mockReturnValue({
- listThreads: jest.fn().mockResolvedValue([]),
- })
-
- const mockSetThreadStates = jest.fn()
- const mockSetThreads = jest.fn()
- const mockSetThreadModelRuntimeParams = jest.fn()
- const mockSetThreadDataReady = jest.fn()
-
- ;(useSetAtom as jest.Mock).mockImplementation((atom) => {
- switch (atom) {
- case threadStatesAtom:
- return mockSetThreadStates
- case threadsAtom:
- return mockSetThreads
- case threadModelParamsAtom:
- return mockSetThreadModelRuntimeParams
- case threadDataReadyAtom:
- return mockSetThreadDataReady
- default:
- return jest.fn()
- }
- })
-
- const { result } = renderHook(() => useThreads())
-
- await act(async () => {
- // Wait for useEffect to complete
- })
-
- expect(mockSetThreadStates).toHaveBeenCalledWith({})
- expect(mockSetThreads).toHaveBeenCalledWith([])
- expect(mockSetThreadModelRuntimeParams).toHaveBeenCalledWith({})
- expect(mockSetThreadDataReady).toHaveBeenCalledWith(true)
- })
-
- it('should handle missing ConversationalExtension', async () => {
- // Mock missing ConversationalExtension
- ;(extensionManager.get as jest.Mock).mockReturnValue(null)
-
- const mockSetThreadStates = jest.fn()
- const mockSetThreads = jest.fn()
- const mockSetThreadModelRuntimeParams = jest.fn()
- const mockSetThreadDataReady = jest.fn()
-
- ;(useSetAtom as jest.Mock).mockImplementation((atom) => {
- switch (atom) {
- case threadStatesAtom:
- return mockSetThreadStates
- case threadsAtom:
- return mockSetThreads
- case threadModelParamsAtom:
- return mockSetThreadModelRuntimeParams
- case threadDataReadyAtom:
- return mockSetThreadDataReady
- default:
- return jest.fn()
- }
- })
-
- const { result } = renderHook(() => useThreads())
-
- await act(async () => {
- // Wait for useEffect to complete
- })
-
- expect(mockSetThreadStates).toHaveBeenCalledWith({})
- expect(mockSetThreads).toHaveBeenCalledWith([])
- expect(mockSetThreadModelRuntimeParams).toHaveBeenCalledWith({})
- expect(mockSetThreadDataReady).toHaveBeenCalledWith(true)
- })
-})
diff --git a/web/hooks/useThreads.ts b/web/hooks/useThreads.ts
deleted file mode 100644
index 00ad35457..000000000
--- a/web/hooks/useThreads.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-import { useEffect } from 'react'
-
-import {
- ExtensionTypeEnum,
- Thread,
- ThreadState,
- ConversationalExtension,
-} from '@janhq/core'
-
-import { useSetAtom } from 'jotai'
-
-import { extensionManager } from '@/extension/ExtensionManager'
-import {
- threadDataReadyAtom,
- threadModelParamsAtom,
- threadStatesAtom,
- threadsAtom,
-} from '@/helpers/atoms/Thread.atom'
-import { ModelParams } from '@/types/model'
-
-const useThreads = () => {
- const setThreadStates = useSetAtom(threadStatesAtom)
- const setThreads = useSetAtom(threadsAtom)
- const setThreadModelRuntimeParams = useSetAtom(threadModelParamsAtom)
- const setThreadDataReady = useSetAtom(threadDataReadyAtom)
-
- useEffect(() => {
- const getThreads = async () => {
- const localThreads = (await getLocalThreads()).sort((a, b) => {
- return ((a.metadata?.updated_at as number) ?? 0) >
- ((b.metadata?.updated_at as number) ?? 0)
- ? -1
- : 1
- })
- const localThreadStates: Record = {}
- const threadModelParams: Record = {}
-
- localThreads.forEach((thread) => {
- if (thread.id != null) {
- const lastMessage = (thread.metadata?.lastMessage as string) ?? ''
-
- localThreadStates[thread.id] = {
- hasMore: false,
- waitingForResponse: false,
- lastMessage,
- }
-
- const modelParams = thread.assistants?.[0]?.model?.parameters
- const engineParams = thread.assistants?.[0]?.model?.settings
- threadModelParams[thread.id] = {
- ...modelParams,
- ...engineParams,
- }
- }
- })
-
- // updating app states
- setThreadStates(localThreadStates)
- setThreads(localThreads)
- setThreadModelRuntimeParams(threadModelParams)
- setThreadDataReady(true)
- }
-
- getThreads()
- }, [
- setThreadModelRuntimeParams,
- setThreadStates,
- setThreads,
- setThreadDataReady,
- ])
-}
-
-const getLocalThreads = async (): Promise =>
- (await extensionManager
- .get(ExtensionTypeEnum.Conversational)
- ?.listThreads()) ?? []
-
-export default useThreads
diff --git a/web/hooks/useUpdateModelParameters.test.ts b/web/hooks/useUpdateModelParameters.test.ts
deleted file mode 100644
index 6c7ceb8b0..000000000
--- a/web/hooks/useUpdateModelParameters.test.ts
+++ /dev/null
@@ -1,286 +0,0 @@
-import { renderHook, act } from '@testing-library/react'
-import { useAtom } from 'jotai'
-// Mock dependencies
-jest.mock('ulidx')
-jest.mock('@/extension')
-jest.mock('jotai', () => ({
- ...jest.requireActual('jotai'),
- useAtom: jest.fn(),
-}))
-
-import useUpdateModelParameters from './useUpdateModelParameters'
-import { extensionManager } from '@/extension'
-
-// Mock data
-let model: any = {
- id: 'model-1',
- engine: 'nitro',
-}
-
-let extension: any = {
- modifyThread: jest.fn(),
- modifyThreadAssistant: jest.fn(),
-}
-
-const mockThread: any = {
- id: 'thread-1',
- assistants: [
- {
- model: {
- parameters: {},
- settings: {},
- },
- },
- ],
- object: 'thread',
- title: 'New Thread',
- created: 0,
- updated: 0,
-}
-
-describe('useUpdateModelParameters', () => {
- beforeAll(() => {
- jest.clearAllMocks()
- jest.useFakeTimers()
- jest.mock('./useRecommendedModel', () => ({
- useRecommendedModel: () => ({
- recommendedModel: model,
- setRecommendedModel: jest.fn(),
- downloadedModels: [],
- }),
- }))
- })
-
- it('should update model parameters and save thread when params are valid', async () => {
- ;(useAtom as jest.Mock).mockReturnValue([
- {
- id: 'assistant-1',
- },
- jest.fn(),
- ])
- const mockValidParameters: any = {
- params: {
- // Inference
- stop: ['', ''],
- temperature: 0.5,
- token_limit: 1000,
- top_k: 0.7,
- top_p: 0.1,
- stream: true,
- max_tokens: 1000,
- frequency_penalty: 0.3,
- presence_penalty: 0.2,
-
- // Load model
- ctx_len: 1024,
- ngl: 12,
- embedding: true,
- n_parallel: 2,
- cpu_threads: 4,
- prompt_template: 'template',
- llama_model_path: 'path',
- mmproj: 'mmproj',
- vision_model: 'vision',
- text_model: 'text',
- },
- modelId: 'model-1',
- engine: 'nitro',
- }
-
- // Spy functions
- jest.spyOn(extensionManager, 'get').mockReturnValue(extension)
- jest.spyOn(extension, 'modifyThread').mockReturnValue({})
- jest.spyOn(extension, 'modifyThreadAssistant').mockReturnValue({})
-
- const { result } = renderHook(() => useUpdateModelParameters())
-
- await act(async () => {
- await result.current.updateModelParameter(mockThread, mockValidParameters)
- })
-
- jest.runAllTimers()
-
- // Check if the model parameters are valid before persisting
- expect(extension.modifyThreadAssistant).toHaveBeenCalledWith('thread-1', {
- id: 'assistant-1',
- model: {
- parameters: {
- stop: ['', ''],
- temperature: 0.5,
- token_limit: 1000,
- top_k: 0.7,
- top_p: 0.1,
- stream: true,
- max_tokens: 1000,
- frequency_penalty: 0.3,
- presence_penalty: 0.2,
- },
- settings: {
- ctx_len: 1024,
- ngl: 12,
- embedding: true,
- n_parallel: 2,
- cpu_threads: 4,
- prompt_template: 'template',
- llama_model_path: 'path',
- mmproj: 'mmproj',
- },
- id: 'model-1',
- engine: 'nitro',
- },
- })
- })
-
- it('should not update invalid model parameters', async () => {
- ;(useAtom as jest.Mock).mockReturnValue([
- {
- id: 'assistant-1',
- },
- jest.fn(),
- ])
- const mockInvalidParameters: any = {
- params: {
- // Inference
- stop: [1, ''],
- temperature: '0.5',
- token_limit: '1000',
- top_k: '0.7',
- top_p: '0.1',
- stream: 'true',
- max_tokens: '1000',
- frequency_penalty: '0.3',
- presence_penalty: '0.2',
-
- // Load model
- ctx_len: '1024',
- ngl: '12',
- embedding: 'true',
- n_parallel: '2',
- cpu_threads: '4',
- prompt_template: 'template',
- llama_model_path: 'path',
- mmproj: 'mmproj',
- vision_model: 'vision',
- text_model: 'text',
- },
- modelId: 'model-1',
- engine: 'nitro',
- }
-
- // Spy functions
- jest.spyOn(extensionManager, 'get').mockReturnValue(extension)
- jest.spyOn(extension, 'modifyThread').mockReturnValue({})
- jest.spyOn(extension, 'modifyThreadAssistant').mockReturnValue({})
-
- const { result } = renderHook(() => useUpdateModelParameters())
-
- await act(async () => {
- await result.current.updateModelParameter(
- mockThread,
- mockInvalidParameters
- )
- })
-
- jest.runAllTimers()
-
- // Check if the model parameters are valid before persisting
- expect(extension.modifyThreadAssistant).toHaveBeenCalledWith('thread-1', {
- id: 'assistant-1',
- model: {
- engine: 'nitro',
- id: 'model-1',
- parameters: {
- token_limit: 1000,
- max_tokens: 1000,
- },
- settings: {
- cpu_threads: 4,
- ctx_len: 1024,
- prompt_template: 'template',
- llama_model_path: 'path',
- mmproj: 'mmproj',
- n_parallel: 2,
- ngl: 12,
- },
- },
- })
- })
-
- it('should update valid model parameters only', async () => {
- ;(useAtom as jest.Mock).mockReturnValue([
- {
- id: 'assistant-1',
- },
- jest.fn(),
- ])
- const mockInvalidParameters: any = {
- params: {
- // Inference
- stop: [''],
- temperature: -0.5,
- token_limit: 100.2,
- top_k: 0.7,
- top_p: 0.1,
- stream: true,
- max_tokens: 1000,
- frequency_penalty: 1.2,
- presence_penalty: 0.2,
-
- // Load model
- ctx_len: 1024,
- ngl: 0,
- embedding: 'true',
- n_parallel: 2,
- cpu_threads: 4,
- prompt_template: 'template',
- llama_model_path: 'path',
- mmproj: 'mmproj',
- vision_model: 'vision',
- text_model: 'text',
- },
- modelId: 'model-1',
- engine: 'nitro',
- }
-
- // Spy functions
- jest.spyOn(extensionManager, 'get').mockReturnValue(extension)
- jest.spyOn(extension, 'modifyThread').mockReturnValue({})
- jest.spyOn(extension, 'modifyThreadAssistant').mockReturnValue({})
- const { result } = renderHook(() => useUpdateModelParameters())
-
- await act(async () => {
- await result.current.updateModelParameter(
- mockThread,
- mockInvalidParameters
- )
- })
- jest.runAllTimers()
-
- // Check if the model parameters are valid before persisting
- expect(extension.modifyThreadAssistant).toHaveBeenCalledWith('thread-1', {
- id: 'assistant-1',
- model: {
- engine: 'nitro',
- id: 'model-1',
- parameters: {
- stop: [''],
- top_k: 0.7,
- top_p: 0.1,
- stream: true,
- token_limit: 100,
- max_tokens: 1000,
- presence_penalty: 0.2,
- },
- settings: {
- ctx_len: 1024,
- ngl: 0,
- n_parallel: 2,
- cpu_threads: 4,
- prompt_template: 'template',
- llama_model_path: 'path',
- mmproj: 'mmproj',
- },
- },
- })
- })
-})
diff --git a/web/hooks/useUpdateModelParameters.ts b/web/hooks/useUpdateModelParameters.ts
deleted file mode 100644
index 8bab0c357..000000000
--- a/web/hooks/useUpdateModelParameters.ts
+++ /dev/null
@@ -1,118 +0,0 @@
-import { useCallback } from 'react'
-
-import {
- AssistantTool,
- ConversationalExtension,
- ExtensionTypeEnum,
- InferenceEngine,
- Thread,
- ThreadAssistantInfo,
- extractInferenceParams,
- extractModelLoadParams,
-} from '@janhq/core'
-
-import { useAtom, useAtomValue, useSetAtom } from 'jotai'
-
-import { useDebouncedCallback } from 'use-debounce'
-
-import { extensionManager } from '@/extension'
-import { activeAssistantAtom } from '@/helpers/atoms/Assistant.atom'
-import { selectedModelAtom } from '@/helpers/atoms/Model.atom'
-import {
- getActiveThreadModelParamsAtom,
- setThreadModelParamsAtom,
-} from '@/helpers/atoms/Thread.atom'
-import { ModelParams } from '@/types/model'
-
-export type UpdateModelParameter = {
- params?: ModelParams
- modelId?: string
- modelPath?: string
- engine?: InferenceEngine
-}
-
-export default function useUpdateModelParameters() {
- const activeModelParams = useAtomValue(getActiveThreadModelParamsAtom)
- const [activeAssistant, setActiveAssistant] = useAtom(activeAssistantAtom)
- const [selectedModel] = useAtom(selectedModelAtom)
- const setThreadModelParams = useSetAtom(setThreadModelParamsAtom)
-
- const updateAssistantExtension = (
- threadId: string,
- assistant: ThreadAssistantInfo
- ) => {
- return extensionManager
- .get(ExtensionTypeEnum.Conversational)
- ?.modifyThreadAssistant(threadId, assistant)
- }
-
- const updateAssistantCallback = useDebouncedCallback(
- updateAssistantExtension,
- 300
- )
-
- const updateModelParameter = useCallback(
- async (
- thread: Thread,
- settings: UpdateModelParameter,
- tools?: AssistantTool[]
- ) => {
- if (!activeAssistant) return
-
- const toUpdateSettings = processStopWords(settings.params ?? {})
- const updatedModelParams = settings.modelId
- ? toUpdateSettings
- : {
- ...selectedModel?.parameters,
- ...selectedModel?.settings,
- ...activeModelParams,
- ...toUpdateSettings,
- }
-
- // update the state
- setThreadModelParams(thread.id, updatedModelParams)
- const runtimeParams = extractInferenceParams(updatedModelParams)
- const settingParams = extractModelLoadParams(updatedModelParams)
- const assistantInfo = {
- ...activeAssistant,
- tools: tools ?? activeAssistant.tools,
- model: {
- ...activeAssistant?.model,
- parameters: runtimeParams,
- settings: settingParams,
- id: settings.modelId ?? selectedModel?.id ?? activeAssistant.model.id,
- engine:
- settings.engine ??
- selectedModel?.engine ??
- activeAssistant.model.engine,
- },
- }
- setActiveAssistant(assistantInfo)
-
- updateAssistantCallback(thread.id, assistantInfo)
- },
- [
- activeAssistant,
- selectedModel?.parameters,
- selectedModel?.settings,
- selectedModel?.id,
- selectedModel?.engine,
- activeModelParams,
- setThreadModelParams,
- setActiveAssistant,
- updateAssistantCallback,
- ]
- )
-
- const processStopWords = (params: ModelParams): ModelParams => {
- if ('stop' in params && typeof params['stop'] === 'string') {
- // Input as string but stop words accept an array of strings (space as separator)
- params['stop'] = (params['stop'] as string)
- .split(' ')
- .filter((e) => e.trim().length)
- }
- return params
- }
-
- return { updateModelParameter }
-}
diff --git a/web/jest.config.js b/web/jest.config.js
deleted file mode 100644
index dea8827cd..000000000
--- a/web/jest.config.js
+++ /dev/null
@@ -1,44 +0,0 @@
-const nextJest = require('next/jest')
-
-/** @type {import('jest').Config} */
-const createJestConfig = nextJest({})
-
-// Add any custom config to be passed to Jest
-const config = {
- testEnvironment: 'jsdom',
- transform: {
- '^.+\\.(ts|tsx)$': 'ts-jest',
- '^.+\\.(js|jsx)$': 'babel-jest',
- },
- moduleNameMapper: {
- // ...
- '^@/(.*)$': '/$1',
- 'react-markdown': '/mock/empty-mock.tsx',
- 'rehype-highlight': '/mock/empty-mock.tsx',
- 'rehype-katex': '/mock/empty-mock.tsx',
- 'rehype-raw': '/mock/empty-mock.tsx',
- 'remark-math': '/mock/empty-mock.tsx',
- '^react$': '/node_modules/react',
- '^react/jsx-runtime$': '/node_modules/react/jsx-runtime',
- '^react-dom$': '/node_modules/react-dom',
- },
- // Add more setup options before each test is run
- // setupFilesAfterEnv: ['/jest.setup.ts'],
- runner: './testRunner.js',
- collectCoverageFrom: ['./**/*.{ts,tsx}'],
- transform: {
- '^.+\\.tsx?$': [
- 'ts-jest',
- {
- diagnostics: false,
- },
- ],
- },
-}
-
-// https://stackoverflow.com/a/72926763/5078746
-// module.exports = createJestConfig(config)
-module.exports = async () => ({
- ...(await createJestConfig(config)()),
- transformIgnorePatterns: ['/node_modules/(?!((.*))/)'],
-})
diff --git a/web/mock/empty-mock.tsx b/web/mock/empty-mock.tsx
deleted file mode 100644
index dd7f322f2..000000000
--- a/web/mock/empty-mock.tsx
+++ /dev/null
@@ -1,2 +0,0 @@
-const EmptyMock = {}
-export default EmptyMock
diff --git a/web/next.config.js b/web/next.config.js
deleted file mode 100644
index c36eae42a..000000000
--- a/web/next.config.js
+++ /dev/null
@@ -1,52 +0,0 @@
-/* eslint-disable @typescript-eslint/no-var-requires */
-/** @type {import('next').NextConfig} */
-
-const webpack = require('webpack')
-
-const packageJson = require('./package.json')
-
-const nextConfig = {
- eslint: {
- // Warning: This allows production builds to successfully complete even if
- // your project has ESLint errors.
- ignoreDuringBuilds: true,
- },
- output: 'export',
- assetPrefix: '.',
- images: {
- unoptimized: true,
- remotePatterns: [
- {
- protocol: 'https',
- hostname: '**',
- },
- ],
- },
- webpack: (config) => {
- // do some stuff here
- config.optimization.minimize = false
- config.optimization.minimizer = []
- config.plugins = [
- ...config.plugins,
- new webpack.DefinePlugin({
- VERSION: JSON.stringify(packageJson.version),
- ANALYTICS_ID: JSON.stringify(process.env.ANALYTICS_ID),
- POSTHOG_KEY: JSON.stringify(process.env.POSTHOG_KEY),
- POSTHOG_HOST: JSON.stringify(process.env.POSTHOG_HOST),
- ANALYTICS_HOST: JSON.stringify(process.env.ANALYTICS_HOST),
- API_BASE_URL: JSON.stringify(
- process.env.API_BASE_URL ??
- `http://127.0.0.1:${process.env.CORTEX_API_PORT ?? '39291'}`
- ),
- isMac: process.platform === 'darwin',
- isWindows: process.platform === 'win32',
- isLinux: process.platform === 'linux',
- PLATFORM: JSON.stringify(process.platform),
- IS_TAURI: process.env.IS_TAURI === 'true',
- }),
- ]
- return config
- },
-}
-
-module.exports = nextConfig
diff --git a/web/package.json b/web/package.json
deleted file mode 100644
index dd02f3de5..000000000
--- a/web/package.json
+++ /dev/null
@@ -1,112 +0,0 @@
-{
- "name": "@janhq/web",
- "version": "0.5.15",
- "private": true,
- "homepage": "./",
- "scripts": {
- "dev": "next dev",
- "build": "next build",
- "start": "next start",
- "lint": "eslint .",
- "lint:fix": "eslint . --fix",
- "format": "prettier --write \"**/*.{js,jsx,ts,tsx}\"",
- "compile": "tsc --noEmit -p . --pretty",
- "test": "jest"
- },
- "dependencies": {
- "@hello-pangea/dnd": "17.0.0",
- "@hookform/resolvers": "^3.9.1",
- "@janhq/core": "link:../core",
- "@janhq/joi": "link:../joi",
- "@radix-ui/react-icons": "^1.3.2",
- "@tanstack/react-virtual": "^3.10.9",
- "@tauri-apps/api": "^2.4.0",
- "@tauri-apps/plugin-http": "^2.4.2",
- "@tauri-apps/plugin-updater": "~2",
- "@uppy/core": "^4.3.0",
- "@uppy/react": "^4.0.4",
- "@uppy/xhr-upload": "^4.2.3",
- "autoprefixer": "10.4.16",
- "class-variance-authority": "^0.7.0",
- "framer-motion": "^10.16.4",
- "highlight.js": "^11.10.0",
- "jotai": "^2.6.0",
- "katex": "^0.16.10",
- "lodash": "^4.17.21",
- "lucide-react": "^0.311.0",
- "marked": "^9.1.2",
- "next": "14.2.3",
- "next-themes": "^0.2.1",
- "npx-scope-finder": "^1.3.0",
- "openai": "^4.90.0",
- "postcss": "8.4.31",
- "postcss-url": "10.1.3",
- "posthog-js": "^1.194.6",
- "react": "18.2.0",
- "react-circular-progressbar": "^2.1.0",
- "react-dom": "18.2.0",
- "react-dropzone": "14.2.3",
- "react-hook-form": "^7.54.2",
- "react-hot-toast": "^2.4.1",
- "react-icons": "^4.12.0",
- "react-markdown": "^9.0.1",
- "react-toastify": "^9.1.3",
- "rehype-highlight": "^7.0.1",
- "rehype-highlight-code-lines": "^1.0.4",
- "rehype-katex": "^7.0.1",
- "rehype-raw": "^7.0.0",
- "remark-gfm": "^4.0.0",
- "remark-math": "^6.0.0",
- "sass": "^1.69.4",
- "slate": "latest",
- "slate-dom": "0.111.0",
- "slate-history": "0.110.3",
- "slate-react": "0.110.3",
- "swr": "^2.2.5",
- "tailwind-merge": "^2.0.0",
- "tailwindcss": "3.4.17",
- "token.js": "npm:token.js-fork@0.7.6",
- "ulidx": "^2.3.0",
- "use-debounce": "^10.0.0",
- "uuid": "^9.0.1",
- "zod": "^3.24.1"
- },
- "devDependencies": {
- "@next/eslint-plugin-next": "^14.0.1",
- "@testing-library/dom": "^10.4.0",
- "@testing-library/jest-dom": "^6.6.3",
- "@testing-library/react": "^16.0.1",
- "@testing-library/user-event": "^14.5.2",
- "@types/jest": "^29.5.12",
- "@types/lodash": "^4.14.200",
- "@types/node": "20.8.10",
- "@types/react": "18.2.34",
- "@types/react-dom": "18.2.14",
- "@types/react-icons": "^3.0.0",
- "@types/react-syntax-highlighter": "^15.5.13",
- "@types/uuid": "^9.0.6",
- "@typescript-eslint/eslint-plugin": "^6.8.0",
- "@typescript-eslint/parser": "^6.8.0",
- "babel-loader": "^10.0.0",
- "encoding": "^0.1.13",
- "eslint": "8.52.0",
- "eslint-config-next": "14.0.1",
- "eslint-config-prettier": "^9.0.0",
- "eslint-config-turbo": "^1.10.15",
- "eslint-import-resolver-typescript": "^3.6.1",
- "eslint-plugin-import": "^2.28.1",
- "eslint-plugin-prettier": "^5.0.1",
- "eslint-plugin-react": "^7.34.0",
- "jest-environment-jsdom": "^29.7.0",
- "jest-runner": "^29.7.0",
- "prettier": "^3.0.3",
- "prettier-plugin-tailwindcss": "^0.5.6",
- "rimraf": "^5.0.5",
- "ts-jest": "^29.2.5",
- "typescript": "^5.3.3"
- },
- "resolutions": {
- "highlight.js": "11.10.0"
- },
- "packageManager": "yarn@4.5.3"
-}
diff --git a/web/postcss.config.js b/web/postcss.config.js
deleted file mode 100644
index fa30f9c8c..000000000
--- a/web/postcss.config.js
+++ /dev/null
@@ -1,10 +0,0 @@
-module.exports = {
- plugins: {
- // eslint-disable-next-line @typescript-eslint/naming-convention
- 'postcss-url': {
- url: 'inline',
- },
- 'tailwindcss': {},
- 'autoprefixer': {},
- },
-}
diff --git a/web/public/icons/Jan.svg b/web/public/icons/Jan.svg
deleted file mode 100644
index 619487743..000000000
--- a/web/public/icons/Jan.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
diff --git a/web/public/icons/app_icon.svg b/web/public/icons/app_icon.svg
deleted file mode 100644
index 188c822f6..000000000
--- a/web/public/icons/app_icon.svg
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/web/public/icons/discord.svg b/web/public/icons/discord.svg
deleted file mode 100644
index 0c3eb1a9b..000000000
--- a/web/public/icons/discord.svg
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/web/public/icons/huggingFace.svg b/web/public/icons/huggingFace.svg
deleted file mode 100644
index 8570b2049..000000000
--- a/web/public/icons/huggingFace.svg
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/web/public/images/HubBanner/banner-1.jpg b/web/public/images/HubBanner/banner-1.jpg
deleted file mode 100644
index 230920d16..000000000
Binary files a/web/public/images/HubBanner/banner-1.jpg and /dev/null differ
diff --git a/web/public/images/HubBanner/banner-10.jpg b/web/public/images/HubBanner/banner-10.jpg
deleted file mode 100644
index 8ce5b5ec5..000000000
Binary files a/web/public/images/HubBanner/banner-10.jpg and /dev/null differ
diff --git a/web/public/images/HubBanner/banner-11.jpg b/web/public/images/HubBanner/banner-11.jpg
deleted file mode 100644
index 66748dba6..000000000
Binary files a/web/public/images/HubBanner/banner-11.jpg and /dev/null differ
diff --git a/web/public/images/HubBanner/banner-12.jpg b/web/public/images/HubBanner/banner-12.jpg
deleted file mode 100644
index d59699a89..000000000
Binary files a/web/public/images/HubBanner/banner-12.jpg and /dev/null differ
diff --git a/web/public/images/HubBanner/banner-13.jpg b/web/public/images/HubBanner/banner-13.jpg
deleted file mode 100644
index 6e8ccf690..000000000
Binary files a/web/public/images/HubBanner/banner-13.jpg and /dev/null differ
diff --git a/web/public/images/HubBanner/banner-14.jpg b/web/public/images/HubBanner/banner-14.jpg
deleted file mode 100644
index b6afaa6c2..000000000
Binary files a/web/public/images/HubBanner/banner-14.jpg and /dev/null differ
diff --git a/web/public/images/HubBanner/banner-15.jpg b/web/public/images/HubBanner/banner-15.jpg
deleted file mode 100644
index 30237060f..000000000
Binary files a/web/public/images/HubBanner/banner-15.jpg and /dev/null differ
diff --git a/web/public/images/HubBanner/banner-16.jpg b/web/public/images/HubBanner/banner-16.jpg
deleted file mode 100644
index dbd11da2c..000000000
Binary files a/web/public/images/HubBanner/banner-16.jpg and /dev/null differ
diff --git a/web/public/images/HubBanner/banner-17.jpg b/web/public/images/HubBanner/banner-17.jpg
deleted file mode 100644
index 828c0df67..000000000
Binary files a/web/public/images/HubBanner/banner-17.jpg and /dev/null differ
diff --git a/web/public/images/HubBanner/banner-18.jpg b/web/public/images/HubBanner/banner-18.jpg
deleted file mode 100644
index 4f8f4a9fb..000000000
Binary files a/web/public/images/HubBanner/banner-18.jpg and /dev/null differ
diff --git a/web/public/images/HubBanner/banner-19.jpg b/web/public/images/HubBanner/banner-19.jpg
deleted file mode 100644
index 9da970f7a..000000000
Binary files a/web/public/images/HubBanner/banner-19.jpg and /dev/null differ
diff --git a/web/public/images/HubBanner/banner-2.jpg b/web/public/images/HubBanner/banner-2.jpg
deleted file mode 100644
index 7241f7ee0..000000000
Binary files a/web/public/images/HubBanner/banner-2.jpg and /dev/null differ
diff --git a/web/public/images/HubBanner/banner-20.jpg b/web/public/images/HubBanner/banner-20.jpg
deleted file mode 100644
index 58348eea7..000000000
Binary files a/web/public/images/HubBanner/banner-20.jpg and /dev/null differ
diff --git a/web/public/images/HubBanner/banner-21.jpg b/web/public/images/HubBanner/banner-21.jpg
deleted file mode 100644
index 3f4f3cd59..000000000
Binary files a/web/public/images/HubBanner/banner-21.jpg and /dev/null differ
diff --git a/web/public/images/HubBanner/banner-22.jpg b/web/public/images/HubBanner/banner-22.jpg
deleted file mode 100644
index 54aa38834..000000000
Binary files a/web/public/images/HubBanner/banner-22.jpg and /dev/null differ
diff --git a/web/public/images/HubBanner/banner-23.jpg b/web/public/images/HubBanner/banner-23.jpg
deleted file mode 100644
index fbac14352..000000000
Binary files a/web/public/images/HubBanner/banner-23.jpg and /dev/null differ
diff --git a/web/public/images/HubBanner/banner-24.jpg b/web/public/images/HubBanner/banner-24.jpg
deleted file mode 100644
index 0d1ac23c9..000000000
Binary files a/web/public/images/HubBanner/banner-24.jpg and /dev/null differ
diff --git a/web/public/images/HubBanner/banner-25.jpg b/web/public/images/HubBanner/banner-25.jpg
deleted file mode 100644
index a3a7e7ead..000000000
Binary files a/web/public/images/HubBanner/banner-25.jpg and /dev/null differ
diff --git a/web/public/images/HubBanner/banner-26.jpg b/web/public/images/HubBanner/banner-26.jpg
deleted file mode 100644
index 41ecc74a4..000000000
Binary files a/web/public/images/HubBanner/banner-26.jpg and /dev/null differ
diff --git a/web/public/images/HubBanner/banner-27.jpg b/web/public/images/HubBanner/banner-27.jpg
deleted file mode 100644
index d1d05325a..000000000
Binary files a/web/public/images/HubBanner/banner-27.jpg and /dev/null differ
diff --git a/web/public/images/HubBanner/banner-28.jpg b/web/public/images/HubBanner/banner-28.jpg
deleted file mode 100644
index fa48c2559..000000000
Binary files a/web/public/images/HubBanner/banner-28.jpg and /dev/null differ
diff --git a/web/public/images/HubBanner/banner-29.jpg b/web/public/images/HubBanner/banner-29.jpg
deleted file mode 100644
index 2e3ccaa85..000000000
Binary files a/web/public/images/HubBanner/banner-29.jpg and /dev/null differ
diff --git a/web/public/images/HubBanner/banner-3.jpg b/web/public/images/HubBanner/banner-3.jpg
deleted file mode 100644
index 91dc1e499..000000000
Binary files a/web/public/images/HubBanner/banner-3.jpg and /dev/null differ
diff --git a/web/public/images/HubBanner/banner-30.jpg b/web/public/images/HubBanner/banner-30.jpg
deleted file mode 100644
index 464f8225c..000000000
Binary files a/web/public/images/HubBanner/banner-30.jpg and /dev/null differ
diff --git a/web/public/images/HubBanner/banner-4.jpg b/web/public/images/HubBanner/banner-4.jpg
deleted file mode 100644
index 582daf089..000000000
Binary files a/web/public/images/HubBanner/banner-4.jpg and /dev/null differ
diff --git a/web/public/images/HubBanner/banner-5.jpg b/web/public/images/HubBanner/banner-5.jpg
deleted file mode 100644
index 8e732a2f0..000000000
Binary files a/web/public/images/HubBanner/banner-5.jpg and /dev/null differ
diff --git a/web/public/images/HubBanner/banner-6.jpg b/web/public/images/HubBanner/banner-6.jpg
deleted file mode 100644
index c2c807cba..000000000
Binary files a/web/public/images/HubBanner/banner-6.jpg and /dev/null differ
diff --git a/web/public/images/HubBanner/banner-7.jpg b/web/public/images/HubBanner/banner-7.jpg
deleted file mode 100644
index 9453023ed..000000000
Binary files a/web/public/images/HubBanner/banner-7.jpg and /dev/null differ
diff --git a/web/public/images/HubBanner/banner-8.jpg b/web/public/images/HubBanner/banner-8.jpg
deleted file mode 100644
index e056b50d5..000000000
Binary files a/web/public/images/HubBanner/banner-8.jpg and /dev/null differ
diff --git a/web/public/images/HubBanner/banner-9.jpg b/web/public/images/HubBanner/banner-9.jpg
deleted file mode 100644
index 6ff9b9190..000000000
Binary files a/web/public/images/HubBanner/banner-9.jpg and /dev/null differ
diff --git a/web/public/images/ModelProvider/anthropic.svg b/web/public/images/ModelProvider/anthropic.svg
deleted file mode 100644
index 1f3f18dcf..000000000
--- a/web/public/images/ModelProvider/anthropic.svg
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/web/public/images/ModelProvider/cohere.svg b/web/public/images/ModelProvider/cohere.svg
deleted file mode 100644
index 0ff4f0029..000000000
--- a/web/public/images/ModelProvider/cohere.svg
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/web/public/images/ModelProvider/cortex.svg b/web/public/images/ModelProvider/cortex.svg
deleted file mode 100644
index c0ebd58bf..000000000
--- a/web/public/images/ModelProvider/cortex.svg
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/web/public/images/ModelProvider/deepseek.svg b/web/public/images/ModelProvider/deepseek.svg
deleted file mode 100644
index 6f4b775d3..000000000
--- a/web/public/images/ModelProvider/deepseek.svg
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
-Created with Pixso.
-
-
diff --git a/web/public/images/ModelProvider/dot.svg b/web/public/images/ModelProvider/dot.svg
deleted file mode 100644
index f667c20b1..000000000
--- a/web/public/images/ModelProvider/dot.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/web/public/images/ModelProvider/google-gemini.svg b/web/public/images/ModelProvider/google-gemini.svg
deleted file mode 100644
index 787c83710..000000000
--- a/web/public/images/ModelProvider/google-gemini.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/web/public/images/ModelProvider/google.svg b/web/public/images/ModelProvider/google.svg
deleted file mode 100644
index 1c44dd330..000000000
--- a/web/public/images/ModelProvider/google.svg
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/web/public/images/ModelProvider/groq.svg b/web/public/images/ModelProvider/groq.svg
deleted file mode 100644
index 9c2e0a34a..000000000
--- a/web/public/images/ModelProvider/groq.svg
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/web/public/images/ModelProvider/hugging-face.svg b/web/public/images/ModelProvider/hugging-face.svg
deleted file mode 100644
index 9ac72080a..000000000
--- a/web/public/images/ModelProvider/hugging-face.svg
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/web/public/images/ModelProvider/martian.svg b/web/public/images/ModelProvider/martian.svg
deleted file mode 100644
index b5ceacdf8..000000000
--- a/web/public/images/ModelProvider/martian.svg
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/web/public/images/ModelProvider/meta.svg b/web/public/images/ModelProvider/meta.svg
deleted file mode 100644
index 91bdf9783..000000000
--- a/web/public/images/ModelProvider/meta.svg
+++ /dev/null
@@ -1,72 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/web/public/images/ModelProvider/mistral.svg b/web/public/images/ModelProvider/mistral.svg
deleted file mode 100644
index 22233c55c..000000000
--- a/web/public/images/ModelProvider/mistral.svg
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/web/public/images/ModelProvider/nitro.svg b/web/public/images/ModelProvider/nitro.svg
deleted file mode 100644
index 775517a75..000000000
--- a/web/public/images/ModelProvider/nitro.svg
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
diff --git a/web/public/images/ModelProvider/nvidia.svg b/web/public/images/ModelProvider/nvidia.svg
deleted file mode 100644
index 09c2194ec..000000000
--- a/web/public/images/ModelProvider/nvidia.svg
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/web/public/images/ModelProvider/openRouter.svg b/web/public/images/ModelProvider/openRouter.svg
deleted file mode 100644
index 62ff2b424..000000000
--- a/web/public/images/ModelProvider/openRouter.svg
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/web/public/images/ModelProvider/openai.svg b/web/public/images/ModelProvider/openai.svg
deleted file mode 100644
index 8f0785415..000000000
--- a/web/public/images/ModelProvider/openai.svg
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/web/public/images/ModelProvider/send.svg b/web/public/images/ModelProvider/send.svg
deleted file mode 100644
index 28d30299f..000000000
--- a/web/public/images/ModelProvider/send.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/web/public/images/compact-width-dark.png b/web/public/images/compact-width-dark.png
deleted file mode 100644
index 12c4cfa24..000000000
Binary files a/web/public/images/compact-width-dark.png and /dev/null differ
diff --git a/web/public/images/compact-width.png b/web/public/images/compact-width.png
deleted file mode 100644
index 2404a34c6..000000000
Binary files a/web/public/images/compact-width.png and /dev/null differ
diff --git a/web/public/images/full-width-dark.png b/web/public/images/full-width-dark.png
deleted file mode 100644
index 93e3df9c4..000000000
Binary files a/web/public/images/full-width-dark.png and /dev/null differ
diff --git a/web/public/images/full-width.png b/web/public/images/full-width.png
deleted file mode 100644
index 47d151132..000000000
Binary files a/web/public/images/full-width.png and /dev/null differ
diff --git a/web/screens/Hub/ModelFilter/ContextLength/index.tsx b/web/screens/Hub/ModelFilter/ContextLength/index.tsx
deleted file mode 100644
index a0886e247..000000000
--- a/web/screens/Hub/ModelFilter/ContextLength/index.tsx
+++ /dev/null
@@ -1,84 +0,0 @@
-import { useState } from 'react'
-
-import { Slider, Input, Tooltip } from '@janhq/joi'
-
-import { atom, useAtom } from 'jotai'
-import { InfoIcon } from 'lucide-react'
-
-export const hubCtxLenAtom = atom(0)
-
-export default function ContextLengthFilter() {
- const [value, setValue] = useAtom(hubCtxLenAtom)
- const [inputingValue, setInputingValue] = useState(false)
-
- const normalizeTextValue = (value: number) => {
- return value === 100 ? '1M' : value === 0 ? 0 : `${value}K`
- }
-
- return (
-
-
-
Context length
-
- }
- content="Controls how much text the model can consider at once. Longer context allows the model to handle more input but uses more memory and runs slower."
- />
-
-
-
-
{
- setValue(Number(e[0]))
- }}
- min={0}
- max={100}
- step={1}
- />
-
-
-
-
setInputingValue(true)}
- onBlur={(e) => {
- setInputingValue(false)
- const numericValue = e.target.value.replace(/\D/g, '')
- const value = Number(numericValue)
- setValue(value > 100 ? 100 : value)
- }}
- onChange={(e) => {
- // Passthru since it validates again onBlur
- if (/^\d*\.?\d*$/.test(e.target.value)) {
- setValue(Number(e.target.value))
- }
-
- // Should not accept invalid value or NaN
- // E.g. anything changes that trigger onValueChanged
- // Which is incorrect
- if (
- Number(e.target.value) > 100 ||
- Number(e.target.value) < 0 ||
- Number.isNaN(Number(e.target.value))
- )
- return
- setValue(Number(e.target.value))
- }}
- />
-
-
- )
-}
diff --git a/web/screens/Hub/ModelFilter/ModelSize/index.tsx b/web/screens/Hub/ModelFilter/ModelSize/index.tsx
deleted file mode 100644
index a8d411e33..000000000
--- a/web/screens/Hub/ModelFilter/ModelSize/index.tsx
+++ /dev/null
@@ -1,117 +0,0 @@
-import { useState } from 'react'
-
-import { Slider, Input } from '@janhq/joi'
-
-import { atom, useAtom } from 'jotai'
-
-export const hubModelSizeMinAtom = atom(0)
-export const hubModelSizeMaxAtom = atom(100)
-
-export default function ModelSizeFilter({ max }: { max: number }) {
- const [value, setValue] = useAtom(hubModelSizeMinAtom)
- const [valueMax, setValueMax] = useAtom(hubModelSizeMaxAtom)
- const [inputingMinValue, setInputingMinValue] = useState(false)
- const [inputingMaxValue, setInputingMaxValue] = useState(false)
-
- const normalizeTextValue = (value: number) => {
- return value === 0 ? 0 : `${value}GB`
- }
-
- return (
-
-
-
-
- {
- setValue(Number(e[0]))
- setValueMax(Number(e[1]))
- }}
- min={0}
- max={max}
- step={1}
- />
-
-
-
-
-
-
from
-
-
setInputingMinValue(true)}
- onBlur={(e) => {
- setInputingMinValue(false)
- const numericValue = e.target.value.replace(/\D/g, '')
- const value = Number(numericValue)
- setValue(value > valueMax ? valueMax : value)
- }}
- onChange={(e) => {
- // Passthru since it validates again onBlur
- if (/^\d*\.?\d*$/.test(e.target.value)) {
- setValue(Number(e.target.value))
- }
-
- // Should not accept invalid value or NaN
- // E.g. anything changes that trigger onValueChanged
- // Which is incorrect
- if (
- Number(e.target.value) > max ||
- Number(e.target.value) < 0 ||
- Number.isNaN(Number(e.target.value))
- )
- return
- setValue(Number(e.target.value))
- }}
- />
-
-
-
to
-
-
setInputingMaxValue(true)}
- onBlur={(e) => {
- setInputingMaxValue(false)
- const numericValue = e.target.value.replace(/\D/g, '')
- const value = Number(numericValue)
- setValueMax(value > max ? max : value)
- }}
- onChange={(e) => {
- // Passthru since it validates again onBlur
- if (/^\d*\.?\d*$/.test(e.target.value)) {
- setValueMax(Number(e.target.value))
- }
-
- // Should not accept invalid value or NaN
- // E.g. anything changes that trigger onValueChanged
- // Which is incorrect
- if (
- Number(e.target.value) > max ||
- Number(e.target.value) < 0 ||
- Number.isNaN(Number(e.target.value))
- )
- return
- setValueMax(Number(e.target.value))
- }}
- />
-
-
-
-
- )
-}
diff --git a/web/screens/Hub/ModelList/ModelHeader/index.tsx b/web/screens/Hub/ModelList/ModelHeader/index.tsx
deleted file mode 100644
index 5fe0993ff..000000000
--- a/web/screens/Hub/ModelList/ModelHeader/index.tsx
+++ /dev/null
@@ -1,210 +0,0 @@
-import { useCallback, useMemo } from 'react'
-
-import Image from 'next/image'
-
-import { InferenceEngine, ModelSource } from '@janhq/core'
-
-import { Button, Tooltip, Dropdown, Badge } from '@janhq/joi'
-
-import { useAtomValue, useSetAtom } from 'jotai'
-import { ChevronDownIcon } from 'lucide-react'
-
-import { twMerge } from 'tailwind-merge'
-
-import ModalCancelDownload from '@/containers/ModalCancelDownload'
-
-import { MainViewState } from '@/constants/screens'
-
-import { useCreateNewThread } from '@/hooks/useCreateNewThread'
-import useDownloadModel from '@/hooks/useDownloadModel'
-
-import { toGigabytes } from '@/utils/converter'
-
-import { getLogoEngine } from '@/utils/modelEngine'
-import { extractModelName } from '@/utils/modelSource'
-
-import { fuzzySearch } from '@/utils/search'
-
-import { mainViewStateAtom } from '@/helpers/atoms/App.atom'
-import { assistantsAtom } from '@/helpers/atoms/Assistant.atom'
-import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom'
-
-import {
- downloadedModelsAtom,
- getDownloadingModelAtom,
-} from '@/helpers/atoms/Model.atom'
-import { selectedSettingAtom } from '@/helpers/atoms/Setting.atom'
-
-type Props = {
- model: ModelSource
- onSelectedModel: () => void
-}
-
-const ModelItemHeader = ({ model, onSelectedModel }: Props) => {
- const { downloadModel } = useDownloadModel()
- const downloadingModels = useAtomValue(getDownloadingModelAtom)
- const downloadedModels = useAtomValue(downloadedModelsAtom)
- const setSelectedSetting = useSetAtom(selectedSettingAtom)
- const { requestCreateNewThread } = useCreateNewThread()
-
- const setMainViewState = useSetAtom(mainViewStateAtom)
-
- const serverEnabled = useAtomValue(serverEnabledAtom)
- const assistants = useAtomValue(assistantsAtom)
-
- const onDownloadClick = useCallback(() => {
- downloadModel(model.models?.[0].id)
- }, [model, downloadModel])
-
- const isDownloaded = downloadedModels.some((md) =>
- model.models.some((m) => m.id === md.id)
- )
- const defaultModel = useMemo(() => {
- return model.models?.find(
- (e) => e.id.includes('q4-km') || fuzzySearch('q4km', e.id)
- )
- }, [model])
-
- let downloadButton = (
-
-
- Download
-
-
({
- name: (
-
-
- {e.id}
-
- {e.id === defaultModel?.id && (
-
- Default
-
- )}
-
- ),
- value: e.id,
- suffix: toGigabytes(e.size),
- }))}
- onValueChanged={(e) => downloadModel(e)}
- >
-
-
-
-
-
- )
-
- const isDownloading = downloadingModels.some((md) =>
- model.models.some((m) => m.id === md)
- )
-
- const onUseModelClick = useCallback(async () => {
- const downloadedModel = downloadedModels.find((e) =>
- model.models.some((m) => m.id === e.id)
- )
- if (downloadedModel) {
- await requestCreateNewThread(assistants[0], downloadedModel)
- setMainViewState(MainViewState.Thread)
- }
- }, [
- assistants,
- model,
- requestCreateNewThread,
- setMainViewState,
- downloadedModels,
- ])
-
- if (isDownloaded) {
- downloadButton = (
-
- Use
-
- }
- disabled={!serverEnabled}
- content="Threads are disabled while the server is running"
- />
- )
- } else if (isDownloading) {
- downloadButton = (
- model.models.some((m) => m.id === e)) ??
- model.id
- }
- />
- )
- }
-
- return (
-
-
-
-
- {model.type === 'cloud' && (
- <>
-
- >
- )}
- {extractModelName(model.metadata?.id)}
-
-
-
-
-
- {toGigabytes(model.models?.[0]?.size)}
-
-
- {model.type !== 'cloud' ? (
- downloadButton
- ) : (
- <>
- {!model.metadata?.apiKey?.length && (
-
{
- setSelectedSetting(model.id)
- setMainViewState(MainViewState.Settings)
- }}
- >
- Set Up
-
- )}
- >
- )}
-
-
-
- )
-}
-
-export default ModelItemHeader
diff --git a/web/screens/Hub/ModelList/ModelItem/index.tsx b/web/screens/Hub/ModelList/ModelItem/index.tsx
deleted file mode 100644
index e96021c20..000000000
--- a/web/screens/Hub/ModelList/ModelItem/index.tsx
+++ /dev/null
@@ -1,85 +0,0 @@
-import Markdown from 'react-markdown'
-
-import Image from 'next/image'
-
-import { ModelSource } from '@janhq/core'
-
-import { DownloadIcon, FileJson } from 'lucide-react'
-import rehypeRaw from 'rehype-raw'
-
-import ModelLabel from '@/containers/ModelLabel'
-
-import ModelItemHeader from '@/screens/Hub/ModelList/ModelHeader'
-
-import { markdownComponents } from '@/screens/Thread/ThreadCenterPanel/TextMessage/MarkdownUtils'
-
-import { toGigabytes } from '@/utils/converter'
-import { extractDescription } from '@/utils/modelSource'
-import '@/styles/components/model.scss'
-
-type Props = {
- model: ModelSource
- onSelectedModel: () => void
-}
-
-const ModelItem: React.FC = ({ model, onSelectedModel }) => {
- return (
-
-
-
-
-
-
- {toGigabytes(model.models?.[0]?.size)}
-
-
-
-
- {extractDescription(model.metadata?.description) || '-'}
-
-
-
- {(model?.author ?? model?.metadata?.author) && (
-
- {model.id?.includes('huggingface.co') && (
- <>
- {' '}
- >
- )}{' '}
- {model?.author ?? model?.metadata?.author}
-
- )}
- {model.models?.length > 0 && (
-
-
- {model.models?.length}{' '}
- {model.type === 'cloud' ? 'models' : 'versions'}
-
- )}
- {model.metadata?.downloads > 0 && (
-
-
- {model.metadata?.downloads}
-
- )}
-
-
-
-
- )
-}
-
-export default ModelItem
diff --git a/web/screens/Hub/ModelList/index.tsx b/web/screens/Hub/ModelList/index.tsx
deleted file mode 100644
index b8bafa61a..000000000
--- a/web/screens/Hub/ModelList/index.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import { ModelSource } from '@janhq/core'
-
-import ModelItem from '@/screens/Hub/ModelList/ModelItem'
-
-type Props = {
- models: ModelSource[]
- onSelectedModel: (model: ModelSource) => void
- filterOption?: string
-}
-
-const ModelList = ({ models, onSelectedModel, filterOption }: Props) => {
- return (
-
- {models.length === 0 && filterOption === 'on-device' ? (
-
-
- No results found
-
-
- ) : (
- <>
- {models.map((model) => (
-
onSelectedModel(model)}
- />
- ))}
- >
- )}
-
- )
-}
-
-export default ModelList
diff --git a/web/screens/Hub/ModelPage/RemoteModelRefresh.tsx b/web/screens/Hub/ModelPage/RemoteModelRefresh.tsx
deleted file mode 100644
index d091b97b5..000000000
--- a/web/screens/Hub/ModelPage/RemoteModelRefresh.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import { Button } from '@janhq/joi'
-
-import { RefreshCwIcon } from 'lucide-react'
-
-import Spinner from '@/containers/Loader/Spinner'
-
-import { useRefreshModelList } from '@/hooks/useEngineManagement'
-
-function RemoteModelRefresh({ engine }: { engine: string }) {
- const { refreshingModels, refreshModels } = useRefreshModelList(engine)
-
- return (
- refreshModels(engine)}
- >
- {refreshingModels ? (
-
- ) : (
-
- )}
- Refresh
-
- )
-}
-
-export default RemoteModelRefresh
diff --git a/web/screens/Hub/ModelPage/index.tsx b/web/screens/Hub/ModelPage/index.tsx
deleted file mode 100644
index dcd0c833b..000000000
--- a/web/screens/Hub/ModelPage/index.tsx
+++ /dev/null
@@ -1,232 +0,0 @@
-import Image from 'next/image'
-
-import { ModelSource } from '@janhq/core'
-import { Badge, Button, ScrollArea } from '@janhq/joi'
-import { useAtomValue, useSetAtom } from 'jotai'
-import {
- ArrowLeftIcon,
- DownloadIcon,
- FileJson,
- SettingsIcon,
-} from 'lucide-react'
-
-import ModelDownloadButton from '@/containers/ModelDownloadButton'
-
-import ModelLabel from '@/containers/ModelLabel'
-
-import { MainViewState } from '@/constants/screens'
-
-import { MarkdownTextMessage } from '@/screens/Thread/ThreadCenterPanel/TextMessage/MarkdownTextMessage'
-
-import { toGigabytes } from '@/utils/converter'
-import { extractModelName, removeYamlFrontMatter } from '@/utils/modelSource'
-
-import RemoteModelRefresh from './RemoteModelRefresh'
-
-import { mainViewStateAtom } from '@/helpers/atoms/App.atom'
-import {
- selectedSettingAtom,
- showScrollBarAtom,
-} from '@/helpers/atoms/Setting.atom'
-
-type Props = {
- model: ModelSource
- onGoBack: () => void
-}
-
-const ModelPage = ({ model, onGoBack }: Props) => {
- const setSelectedSetting = useSetAtom(selectedSettingAtom)
- const setMainViewState = useSetAtom(mainViewStateAtom)
- const showScrollBar = useAtomValue(showScrollBarAtom)
-
- return (
-
-
-
-
-
- {/* Header */}
-
-
- {model.type !== 'cloud'
- ? extractModelName(model.metadata.id)
- : model.metadata.id}
-
-
- {model.type === 'cloud' && (
- <>
- {!model.metadata?.apiKey?.length ? (
- {
- setSelectedSetting(model.id)
- setMainViewState(MainViewState.Settings)
- }}
- >
- Set Up
-
- ) : (
- {
- setSelectedSetting(model.id)
- setMainViewState(MainViewState.Settings)
- }}
- >
-
-
- )}
- >
- )}
-
-
-
- {(model?.author ?? model?.metadata?.author) && (
-
- {model.id?.includes('huggingface.co') && (
- <>
- {' '}
- >
- )}
- {model?.author ?? model?.metadata?.author}
-
- )}
- {model.models?.length > 0 && (
-
-
- {model.models?.length}{' '}
- {model.type === 'cloud' ? 'models' : 'versions'}
-
- )}
- {model.metadata?.downloads > 0 && (
-
-
- {model.metadata?.downloads}
-
- )}
-
- {/* Table of versions */}
-
-
-
-
-
-
- {model.type !== 'cloud' ? 'Version' : 'Models'}
-
- {model.type !== 'cloud' && (
- <>
-
-
- Format
-
-
- Size
-
- >
- )}
-
- {model.type === 'cloud' && (
-
- )}
-
-
-
-
- {model.models?.map((item, i) => {
- return (
-
-
-
- {model.type === 'cloud'
- ? item.id
- : item.id?.split(':')?.pop()}
-
- {i === 0 && model.type !== 'cloud' && (
-
- Default
-
- )}
-
- {model.type !== 'cloud' && (
- <>
-
-
-
-
- GGUF
-
-
- {toGigabytes(item.size)}
-
- >
- )}
-
- {(model.type !== 'cloud' ||
- (model.metadata?.apiKey?.length ?? 0) > 0) && (
-
- )}
-
-
- )
- })}
-
-
-
-
- {/* README */}
-
-
-
-
-
-
-
- )
-}
-
-export default ModelPage
diff --git a/web/screens/Hub/index.tsx b/web/screens/Hub/index.tsx
deleted file mode 100644
index e0f8936bc..000000000
--- a/web/screens/Hub/index.tsx
+++ /dev/null
@@ -1,612 +0,0 @@
-/* eslint-disable @typescript-eslint/naming-convention */
-import { useCallback, useMemo, useRef, useState, useEffect } from 'react'
-
-import { useDropzone } from 'react-dropzone'
-
-import Image from 'next/image'
-
-import { ModelSource } from '@janhq/core'
-
-import {
- ScrollArea,
- Button,
- Select,
- Tabs,
- useClickOutside,
- Switch,
-} from '@janhq/joi'
-import { motion as m } from 'framer-motion'
-
-import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'
-import { ImagePlusIcon, UploadCloudIcon, UploadIcon } from 'lucide-react'
-
-import { twMerge } from 'tailwind-merge'
-
-import CenterPanelContainer from '@/containers/CenterPanelContainer'
-import ModelSearch from '@/containers/ModelSearch'
-
-import {
- useFetchModelsHub,
- useGetEngineModelSources,
-} from '@/hooks/useEngineManagement'
-import { setImportModelStageAtom } from '@/hooks/useImportModel'
-
-import {
- useGetModelSources,
- useModelSourcesMutation,
-} from '@/hooks/useModelSource'
-
-import ModelList from '@/screens/Hub/ModelList'
-
-import { toGigabytes } from '@/utils/converter'
-import { extractModelRepo } from '@/utils/modelSource'
-import { fuzzySearch } from '@/utils/search'
-
-import ContextLengthFilter, { hubCtxLenAtom } from './ModelFilter/ContextLength'
-import ModelSizeFilter, {
- hubModelSizeMaxAtom,
- hubModelSizeMinAtom,
-} from './ModelFilter/ModelSize'
-import ModelPage from './ModelPage'
-
-import {
- getAppBannerHubAtom,
- setAppBannerHubAtom,
-} from '@/helpers/atoms/App.atom'
-import { modelDetailAtom } from '@/helpers/atoms/Model.atom'
-
-import { showScrollBarAtom } from '@/helpers/atoms/Setting.atom'
-import { totalRamAtom } from '@/helpers/atoms/SystemBar.atom'
-
-const sortMenus = [
- {
- name: 'Most downloaded',
- value: 'most-downloaded',
- },
- {
- name: 'Newest',
- value: 'newest',
- },
-]
-const filterOptions = [
- {
- name: 'All',
- value: 'all',
- },
- {
- name: 'On-device',
- value: 'on-device',
- },
- {
- name: 'Cloud',
- value: 'cloud',
- },
-]
-
-const hubCompatibleAtom = atom(false)
-
-const HubScreen = () => {
- const { sources } = useGetModelSources()
- const { sources: remoteModelSources } = useGetEngineModelSources()
- const { mutate: fetchModelsHub } = useFetchModelsHub()
- const { addModelSource } = useModelSourcesMutation()
- const [searchValue, setSearchValue] = useState('')
- const [sortSelected, setSortSelected] = useState('newest')
- const [filterOption, setFilterOption] = useState('all')
- const [hubBannerOption, setHubBannerOption] = useState('gallery')
- const [showHubBannerSetting, setShowHubBannerSetting] = useState(false)
- const appBannerHub = useAtomValue(getAppBannerHubAtom)
- const setAppBannerHub = useSetAtom(setAppBannerHubAtom)
- const [selectedModel, setSelectedModel] = useState(
- undefined
- )
- const showScrollBar = useAtomValue(showScrollBarAtom)
- const [modelDetail, setModelDetail] = useAtom(modelDetailAtom)
- const setImportModelStage = useSetAtom(setImportModelStageAtom)
- const dropdownRef = useRef(null)
- const imageInputRef = useRef(null)
- const hubBannerSettingRef = useRef(null)
-
- const [compatible, setCompatible] = useAtom(hubCompatibleAtom)
- const totalRam = useAtomValue(totalRamAtom)
- const [ctxLenFilter, setCtxLenFilter] = useAtom(hubCtxLenAtom)
- const [minModelSizeFilter, setMinModelSizeFilter] =
- useAtom(hubModelSizeMinAtom)
- const [maxModelSizeFilter, setMaxModelSizeFilter] =
- useAtom(hubModelSizeMaxAtom)
-
- const largestModel =
- sources &&
- sources
- .flatMap((model) => model.models)
- .reduce((max, model) => (model.size > max.size ? model : max), {
- size: 0,
- })
-
- const searchedModels = useMemo(
- () =>
- searchValue.length
- ? (sources?.filter((e) =>
- fuzzySearch(
- searchValue.replaceAll(' ', '').toLowerCase(),
- e.id.toLowerCase()
- )
- ) ?? [])
- : [],
- [sources, searchValue]
- )
-
- const filteredModels = useMemo(() => {
- return (sources ?? []).filter((model) => {
- const isCompatible =
- !compatible ||
- model.models?.some((e) => e.size * 1.5 < totalRam * (1 << 20))
- const matchesCtxLen =
- !ctxLenFilter ||
- model.metadata?.gguf?.context_length > ctxLenFilter * 1000
- const matchesMinSize =
- !minModelSizeFilter ||
- model.models.some((e) => e.size >= minModelSizeFilter * (1 << 30))
- const matchesMaxSize =
- maxModelSizeFilter === largestModel?.size ||
- model.models.some((e) => e.size <= maxModelSizeFilter * (1 << 30))
-
- return isCompatible && matchesCtxLen && matchesMinSize && matchesMaxSize
- })
- }, [
- sources,
- compatible,
- ctxLenFilter,
- minModelSizeFilter,
- maxModelSizeFilter,
- totalRam,
- ])
-
- const sortedModels = useMemo(() => {
- return filteredModels.sort((a, b) => {
- if (sortSelected === 'most-downloaded') {
- return b.metadata.downloads - a.metadata.downloads
- } else {
- return (
- new Date(b.metadata.createdAt).getTime() -
- new Date(a.metadata.createdAt).getTime()
- )
- }
- })
- }, [sortSelected, filteredModels])
-
- useEffect(() => {
- if (modelDetail) {
- setSelectedModel(sources?.find((e) => e.id === modelDetail))
- setModelDetail(undefined)
- }
- }, [modelDetail, sources, setModelDetail, addModelSource])
-
- useEffect(() => {
- if (largestModel) {
- setMaxModelSizeFilter(
- Number(
- toGigabytes(Number(largestModel?.size), {
- hideUnit: true,
- toFixed: 0,
- })
- )
- )
- }
- }, [largestModel])
-
- useEffect(() => {
- if (selectedModel) {
- // Try add the model source again to update it's data
- addModelSource(selectedModel.id).catch(console.debug)
- }
- }, [sources, selectedModel, addModelSource, setSelectedModel])
-
- useClickOutside(
- () => {
- setSearchValue('')
- },
- null,
- [dropdownRef.current]
- )
-
- useClickOutside(
- () => {
- setShowHubBannerSetting(false)
- },
- null,
- [hubBannerSettingRef.current]
- )
-
- const onImportModelClick = useCallback(() => {
- setImportModelStage('SELECTING_MODEL')
- }, [setImportModelStage])
-
- const onSearchUpdate = useCallback((input: string) => {
- setSearchValue(input)
- }, [])
-
- const setBannerHubImage = (image: string) => {
- setShowHubBannerSetting(false)
- setAppBannerHub(image)
- }
-
- /**
- * Handles the change event of the extension file input element by setting the file name state.
- * Its to be used to display the extension file name of the selected file.
- * @param event - The change event object.
- */
- const handleFileChange = (event: React.ChangeEvent) => {
- const file = event.target.files?.[0]
- if (!file) return
-
- const fileType = file.type
- if (!fileType.startsWith('image/')) {
- alert('Please upload an image file.')
- return
- }
-
- const reader = new FileReader()
- reader.onload = () => {
- // FileReader result is already in a valid Base64 format
- setBannerHubImage(reader.result as string)
- }
- reader.readAsDataURL(file)
- }
-
- const { isDragActive, getRootProps } = useDropzone({
- noClick: true,
- multiple: true,
- accept: {
- 'image/jpeg': ['.jpeg'],
- 'image/png': ['.png'],
- 'image/jpg': ['.jpg'],
- },
- onDrop: (files) => {
- const reader = new FileReader()
- reader.onload = () => {
- // FileReader result is already in a valid Base64 format
- setBannerHubImage(reader.result as string)
- }
- reader.readAsDataURL(files[0])
- },
- })
-
- useEffect(() => {
- fetchModelsHub()
- }, [])
-
- return (
-
-
- {!selectedModel && (
-
- <>
-
-
-
-
-
- setShowHubBannerSetting(!showHubBannerSetting)
- }
- >
-
-
-
-
-
- setHubBannerOption(value)}
- />
-
- {hubBannerOption === 'gallery' && (
-
- {Array.from({ length: 30 }, (_, i) => i + 1).map(
- (e) => {
- return (
-
- setBannerHubImage(
- `./images/HubBanner/banner-${e}.jpg`
- )
- }
- >
-
-
- )
- }
- )}
-
- )}
- {hubBannerOption === 'upload' && (
-
{
- imageInputRef.current?.click()
- }}
- >
-
-
-
-
-
- {!isDragActive && (
- <>
-
- Click to upload
-
-
- or drag and drop
-
-
- Image size: 920x200
-
- >
- )}
- {isDragActive && (
-
- Drop here
-
- )}
-
-
-
-
- )}
-
-
-
-
-
-
-
-
-
0 && 'visible'
- )}
- >
- {searchedModels.length === 0 ? (
-
-
- No results found
-
-
- ) : (
-
- {searchedModels.map((model) => (
-
{
- setSelectedModel(model)
- e.stopPropagation()
- }}
- >
-
- {searchValue.includes('huggingface.co') && (
- <>
- {' '}
- >
- )}
- {extractModelRepo(model.id)}
-
-
- ))}
-
- )}
-
-
-
-
-
-
-
- Import
-
-
-
- {/* Filters and Model List */}
-
- {/* Filters */}
-
-
- Filters
- {
- setCtxLenFilter(0)
- setMinModelSizeFilter(0)
- setMaxModelSizeFilter(
- Number(
- toGigabytes(Number(largestModel?.size), {
- hideUnit: true,
- toFixed: 0,
- })
- )
- )
- setCompatible(true)
- }}
- >
- Reset
-
-
-
- setCompatible(!compatible)}
- className="w-9"
- />
- Compatible with my device
-
-
-
-
-
-
-
-
-
- {/* Model List */}
-
- <>
-
-
-
- {filterOptions.map((e) => (
-
- setFilterOption(e.value)}
- >
- {e.name}
-
-
- ))}
-
-
-
- {
- setSortSelected(value)
- }}
- options={sortMenus}
- />
-
-
- {(filterOption === 'on-device' ||
- filterOption === 'all') && (
-
setSelectedModel(model)}
- filterOption={filterOption}
- />
- )}
- {(filterOption === 'cloud' || filterOption === 'all') && (
- setSelectedModel(model)}
- />
- )}
- >
-
-
- >
-
- )}
- {selectedModel && (
- {
- setSearchValue('')
- setSelectedModel(undefined)
- }}
- />
- )}
-
-
- )
-}
-
-export default HubScreen
diff --git a/web/screens/LocalServer/LocalServerCenterPanel/index.tsx b/web/screens/LocalServer/LocalServerCenterPanel/index.tsx
deleted file mode 100644
index 455314b40..000000000
--- a/web/screens/LocalServer/LocalServerCenterPanel/index.tsx
+++ /dev/null
@@ -1,82 +0,0 @@
-import { useEffect, useState } from 'react'
-
-import { Button } from '@janhq/joi'
-import { CodeIcon, Paintbrush } from 'lucide-react'
-
-import { InfoIcon } from 'lucide-react'
-
-import CenterPanelContainer from '@/containers/CenterPanelContainer'
-import ServerLogs from '@/containers/ServerLogs'
-
-import { useLogs } from '@/hooks/useLogs'
-
-const FIRST_TIME_VISIT_API_SERVER = 'firstTimeVisitAPIServer'
-
-const LocalServerCenterPanel = () => {
- const { openServerLog, clearServerLog } = useLogs()
-
- const [firstTimeVisitAPIServer, setFirstTimeVisitAPIServer] =
- useState(false)
-
- useEffect(() => {
- if (localStorage.getItem(FIRST_TIME_VISIT_API_SERVER) === null) {
- setFirstTimeVisitAPIServer(true)
- }
- }, [firstTimeVisitAPIServer])
-
- return (
-
-
-
-
Server Logs
-
-
openServerLog()}
- >
-
- Open Logs
-
-
clearServerLog()}
- >
-
- Clear
-
-
-
- {firstTimeVisitAPIServer ? (
-
-
-
-
-
-
- Once you start the server, you cannot chat with your
- assistant.
-
- {
- localStorage.setItem(FIRST_TIME_VISIT_API_SERVER, 'false')
- setFirstTimeVisitAPIServer(false)
- }}
- >
- Got it
-
-
-
-
-
- ) : (
-
- )}
-
-
- )
-}
-
-export default LocalServerCenterPanel
diff --git a/web/screens/LocalServer/LocalServerLeftPanel/index.tsx b/web/screens/LocalServer/LocalServerLeftPanel/index.tsx
deleted file mode 100644
index 660ff305b..000000000
--- a/web/screens/LocalServer/LocalServerLeftPanel/index.tsx
+++ /dev/null
@@ -1,288 +0,0 @@
-import { Fragment, useCallback, useState } from 'react'
-
-import {
- EngineManager,
- InferenceEngine,
- Model,
- ModelSettingParams,
-} from '@janhq/core'
-import { Button, Tooltip, Select, Input, Checkbox } from '@janhq/joi'
-
-import { useAtom, useAtomValue, useSetAtom } from 'jotai'
-import { ExternalLinkIcon, InfoIcon } from 'lucide-react'
-
-import { twMerge } from 'tailwind-merge'
-
-import LeftPanelContainer from '@/containers/LeftPanelContainer'
-
-import { toaster } from '@/containers/Toast'
-
-import { useActiveModel, loadModelErrorAtom } from '@/hooks/useActiveModel'
-
-import {
- apiServerCorsEnabledAtom,
- apiServerHostAtom,
- apiServerPortAtom,
- apiServerPrefix,
- apiServerVerboseLogEnabledAtom,
- hostOptions,
-} from '@/helpers/atoms/ApiServer.atom'
-
-import {
- LocalAPIserverModelParamsAtom,
- serverEnabledAtom,
-} from '@/helpers/atoms/LocalServer.atom'
-import { selectedModelAtom } from '@/helpers/atoms/Model.atom'
-
-const LocalServerLeftPanel = () => {
- const [errorRangePort, setErrorRangePort] = useState(false)
- const [errorPrefix, setErrorPrefix] = useState(false)
- const [serverEnabled, setServerEnabled] = useAtom(serverEnabledAtom)
- const [isLoading, setIsLoading] = useState(false)
-
- const { stateModel } = useActiveModel()
- const selectedModel = useAtomValue(selectedModelAtom)
-
- const [isCorsEnabled, setIsCorsEnabled] = useAtom(apiServerCorsEnabledAtom)
- const [isVerboseEnabled, setIsVerboseEnabled] = useAtom(
- apiServerVerboseLogEnabledAtom
- )
- const [host, setHost] = useAtom(apiServerHostAtom)
- const [port, setPort] = useAtom(apiServerPortAtom)
- const [prefix, setPrefix] = useAtom(apiServerPrefix)
- const setLoadModelError = useSetAtom(loadModelErrorAtom)
- const localAPIserverModelParams = useAtomValue(LocalAPIserverModelParamsAtom)
- const FIRST_TIME_VISIT_API_SERVER = 'firstTimeVisitAPIServer'
-
- const model: Model | undefined = selectedModel
- ? {
- ...selectedModel,
- object: selectedModel.object || '',
- settings: (typeof localAPIserverModelParams === 'object'
- ? { ...(localAPIserverModelParams as ModelSettingParams) }
- : { ...selectedModel.settings }) as ModelSettingParams,
- }
- : undefined
-
- const [firstTimeVisitAPIServer, setFirstTimeVisitAPIServer] =
- useState(false)
-
- const handleChangePort = useCallback(
- (value: string) => {
- setErrorRangePort(Number(value) <= 0 || Number(value) >= 65536)
- setPort(value)
- },
- [setPort]
- )
-
- const handleChangePrefix = useCallback(
- (value: string) => {
- setErrorPrefix(!value.length || !value.startsWith('/'))
- setPrefix(value)
- },
- [setPrefix]
- )
-
- const onStartServerClick = async () => {
- if (selectedModel == null) return
- try {
- setIsLoading(true)
- const isStarted = await window.core?.api?.startServer({
- host,
- port: parseInt(port),
- prefix,
- isCorsEnabled,
- isVerboseEnabled,
- })
- if (isStarted) setServerEnabled(true)
- if (firstTimeVisitAPIServer) {
- localStorage.setItem(FIRST_TIME_VISIT_API_SERVER, 'false')
- setFirstTimeVisitAPIServer(false)
- }
- const engine = EngineManager.instance().get(InferenceEngine.cortex)
- engine?.loadModel(model as Model)
- // startModel(selectedModel.id, false).catch((e) => console.error(e))
- setIsLoading(false)
- } catch (e) {
- console.error(e)
- setIsLoading(false)
- toaster({
- title: `Failed to start server!`,
- description: 'Please check Server Logs for more details.',
- type: 'error',
- })
- }
- }
-
- const onStopServerClick = async () => {
- window.core?.api?.stopServer()
- setServerEnabled(false)
- setLoadModelError(undefined)
- setIsLoading(false)
- }
-
- const onToggleServer = async () => {
- if (serverEnabled) {
- await onStopServerClick()
- } else {
- await onStartServerClick()
- }
- }
-
- return (
-
-
-
-
- Start an OpenAI-compatible local HTTP server.
-
-
-
-
-
- {isLoading
- ? 'Starting...'
- : serverEnabled
- ? 'Stop Server'
- : 'Start Server'}
-
- {serverEnabled && (
-
-
- API Playground {' '}
-
-
-
- )}
-
-
-
-
-
Server Options
-
-
- setHost(e)}
- disabled={serverEnabled}
- options={hostOptions}
- block
- />
-
-
-
- {
- handleChangePort(e.target.value)
- }}
- maxLength={5}
- disabled={serverEnabled}
- />
-
-
- {errorRangePort && (
-
{`The port range should be from 0 to 65536`}
- )}
-
-
-
-
-
- API Prefix
-
-
- {
- handleChangePrefix(e.target.value)
- }}
- disabled={serverEnabled}
- />
-
- {errorPrefix && (
-
{`Prefix should start with /`}
- )}
-
-
-
-
- Cross-Origin-Resource-Sharing (CORS)
-
- }
- content="CORS (Cross-Origin Resource Sharing) manages resource access on this server from external domains. Enable for secure inter-website communication, regulating data sharing to bolster overall security."
- />
-
- }
- checked={isCorsEnabled}
- onChange={(e) => setIsCorsEnabled(e.target.checked)}
- name="cors"
- disabled={serverEnabled}
- />
-
-
-
-
-
- Verbose Server Logs
-
- }
- content="Verbose Server Logs provide extensive details about server activities. Enable to capture thorough records, aiding in troubleshooting and monitoring server performance effectively."
- />
-
- }
- checked={isVerboseEnabled}
- onChange={(e) => setIsVerboseEnabled(e.target.checked)}
- name="verbose"
- disabled={serverEnabled}
- />
-
-
-
-
-
- )
-}
-
-export default LocalServerLeftPanel
diff --git a/web/screens/LocalServer/LocalServerRightPanel/index.tsx b/web/screens/LocalServer/LocalServerRightPanel/index.tsx
deleted file mode 100644
index 02e51868d..000000000
--- a/web/screens/LocalServer/LocalServerRightPanel/index.tsx
+++ /dev/null
@@ -1,193 +0,0 @@
-import { useCallback, useEffect, useMemo, useState } from 'react'
-
-import { extractInferenceParams, extractModelLoadParams } from '@janhq/core'
-import { Accordion, AccordionItem, Input } from '@janhq/joi'
-import { useAtomValue, useSetAtom } from 'jotai'
-import { AlertTriangleIcon, CheckIcon, CopyIcon, InfoIcon } from 'lucide-react'
-
-import EngineSetting from '@/containers/EngineSetting'
-import { modalTroubleShootingAtom } from '@/containers/ModalTroubleShoot'
-import ModelDropdown from '@/containers/ModelDropdown'
-import ModelSetting from '@/containers/ModelSetting'
-import RightPanelContainer from '@/containers/RightPanelContainer'
-
-import { loadModelErrorAtom } from '@/hooks/useActiveModel'
-
-import { useClipboard } from '@/hooks/useClipboard'
-
-import { getConfigurationsData } from '@/utils/componentSettings'
-
-import {
- LocalAPIserverModelParamsAtom,
- serverEnabledAtom,
-} from '@/helpers/atoms/LocalServer.atom'
-import { selectedModelAtom } from '@/helpers/atoms/Model.atom'
-
-const LocalServerRightPanel = () => {
- const loadModelError = useAtomValue(loadModelErrorAtom)
- const setLocalAPIserverModelParams = useSetAtom(LocalAPIserverModelParamsAtom)
- const serverEnabled = useAtomValue(serverEnabledAtom)
- const setModalTroubleShooting = useSetAtom(modalTroubleShootingAtom)
-
- const selectedModel = useAtomValue(selectedModelAtom)
-
- const clipboard = useClipboard({ timeout: 1000 })
-
- const [currentModelSettingParams, setCurrentModelSettingParams] = useState(
- extractModelLoadParams(selectedModel?.settings)
- )
-
- const overriddenSettings =
- selectedModel?.settings.ctx_len && selectedModel.settings.ctx_len > 2048
- ? { ctx_len: 4096 }
- : {}
-
- useEffect(() => {
- if (selectedModel) {
- setCurrentModelSettingParams({
- ...selectedModel?.settings,
- ...overriddenSettings,
- })
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [selectedModel])
-
- const modelRuntimeParams = extractInferenceParams(selectedModel?.settings)
-
- const componentDataRuntimeSetting = getConfigurationsData(
- modelRuntimeParams,
- selectedModel
- )
-
- const componentDataEngineSetting = getConfigurationsData(
- currentModelSettingParams
- )
-
- const engineSettings = useMemo(
- () =>
- componentDataEngineSetting.filter(
- (x) => x.key !== 'prompt_template' && x.key !== 'embedding'
- ),
-
- [componentDataEngineSetting]
- )
-
- const modelSettings = useMemo(() => {
- return componentDataRuntimeSetting.filter(
- (x) => x.key !== 'prompt_template'
- )
- }, [componentDataRuntimeSetting])
-
- const onUpdateParams = useCallback(() => {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- setLocalAPIserverModelParams(() => {
- return { ...currentModelSettingParams }
- })
- }, [currentModelSettingParams, setLocalAPIserverModelParams])
-
- const onValueChanged = useCallback(
- (key: string, value: string | number | boolean | string[]) => {
- setCurrentModelSettingParams((prevParams) => ({
- ...prevParams,
- [key]: value,
- }))
- },
- []
- )
-
- useEffect(() => {
- onUpdateParams()
- }, [currentModelSettingParams, onUpdateParams])
-
- return (
-
-
-
-
-
- You can concurrently send requests to one active local model and
- multiple remote models.
-
-
-
-
-
-
- {
- clipboard.copy(selectedModel?.id)
- }}
- suffixIcon={
- selectedModel ? (
- clipboard.copied ? (
-
- ) : (
-
- )
- ) : (
- <>>
- )
- }
- />
-
-
- {loadModelError && serverEnabled && (
-
-
-
- Model failed to start. Access{' '}
- setModalTroubleShooting(true)}
- >
- troubleshooting assistance
-
-
-
- )}
-
-
-
- {modelSettings.length !== 0 && (
-
-
-
- )}
-
- {engineSettings.length !== 0 && (
-
-
-
- )}
-
-
- )
-}
-
-export default LocalServerRightPanel
diff --git a/web/screens/LocalServer/index.tsx b/web/screens/LocalServer/index.tsx
deleted file mode 100644
index 529c799db..000000000
--- a/web/screens/LocalServer/index.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-'use client'
-
-import ModalTroubleShooting from '@/containers/ModalTroubleShoot'
-
-import LocalServerCenterPanel from './LocalServerCenterPanel'
-import LocalServerLeftPanel from './LocalServerLeftPanel'
-import LocalServerRightPanel from './LocalServerRightPanel'
-
-const LocalServerScreen = () => {
- return (
-
-
-
-
-
-
- )
-}
-
-export default LocalServerScreen
diff --git a/web/screens/Settings/Advanced/DataFolder/ModalChangeDirectory.tsx b/web/screens/Settings/Advanced/DataFolder/ModalChangeDirectory.tsx
deleted file mode 100644
index 96cb0f57d..000000000
--- a/web/screens/Settings/Advanced/DataFolder/ModalChangeDirectory.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import React from 'react'
-
-import { Modal, ModalClose, Button } from '@janhq/joi'
-
-import { atom, useAtom } from 'jotai'
-
-export const showDirectoryConfirmModalAtom = atom(false)
-
-type Props = {
- destinationPath: string
- onUserConfirmed: () => void
-}
-
-const ModalChangeDirectory: React.FC = ({
- destinationPath,
- onUserConfirmed,
-}) => {
- const [show, setShow] = useAtom(showDirectoryConfirmModalAtom)
-
- return (
-
-
- Are you sure you want to relocate Jan data folder to{' '}
- {destinationPath}
- ? A restart is required afterward, and the original folder
- remains intact.
-
- {isWindows && (
-
- Note that Jan will not erase the new Jan data folder upon future
- uninstallation.
-
- )}
-
-
- setShow(false)}>
- Cancel
-
-
-
- Yes, Proceed
-
-
-
-
- }
- />
- )
-}
-
-export default ModalChangeDirectory
diff --git a/web/screens/Settings/Advanced/DataFolder/ModalConfirmDestNotEmpty.tsx b/web/screens/Settings/Advanced/DataFolder/ModalConfirmDestNotEmpty.tsx
deleted file mode 100644
index 1459606d1..000000000
--- a/web/screens/Settings/Advanced/DataFolder/ModalConfirmDestNotEmpty.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import React from 'react'
-
-import { Modal, ModalClose, Button } from '@janhq/joi'
-
-import { atom, useAtom } from 'jotai'
-
-export const showDestNotEmptyConfirmAtom = atom(false)
-
-type Props = {
- onUserConfirmed: () => void
-}
-
-const ModalChangeDestNotEmpty = ({ onUserConfirmed }: Props) => {
- const [show, setShow] = useAtom(showDestNotEmptyConfirmAtom)
-
- return (
-
-
- You may accidentally delete your other personal data when
- uninstalling the app in the future. Are you sure you want to proceed
- with this folder? Please review your selection carefully.
-
-
- setShow(false)}>
- Cancel
-
-
-
- Yes, Proceed
-
-
-
-
- }
- />
- )
-}
-
-export default ModalChangeDestNotEmpty
diff --git a/web/screens/Settings/Advanced/DataFolder/ModalErrorSetDestGlobal.tsx b/web/screens/Settings/Advanced/DataFolder/ModalErrorSetDestGlobal.tsx
deleted file mode 100644
index 0305432a9..000000000
--- a/web/screens/Settings/Advanced/DataFolder/ModalErrorSetDestGlobal.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import React from 'react'
-
-import { Modal, ModalClose, Button } from '@janhq/joi'
-
-import { atom, useAtom } from 'jotai'
-
-export const showChangeFolderErrorAtom = atom(false)
-
-const ModalErrorSetDestGlobal = () => {
- const [show, setShow] = useAtom(showChangeFolderErrorAtom)
- return (
-
-
- Oops! Something went wrong. Jan data folder remains the same. Please
- try again.
-
-
- setShow(false)}>
-
- Got it
-
-
-
-
- }
- />
- )
-}
-
-export default ModalErrorSetDestGlobal
diff --git a/web/screens/Settings/Advanced/DataFolder/ModalSameDirectory.tsx b/web/screens/Settings/Advanced/DataFolder/ModalSameDirectory.tsx
deleted file mode 100644
index 13d35b7f8..000000000
--- a/web/screens/Settings/Advanced/DataFolder/ModalSameDirectory.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import React from 'react'
-
-import { Modal, ModalClose, Button } from '@janhq/joi'
-
-import { atom, useAtom } from 'jotai'
-
-export const showSamePathModalAtom = atom(false)
-
-type Props = {
- onChangeFolderClick: () => void
-}
-
-const ModalSameDirectory = ({ onChangeFolderClick }: Props) => {
- const [show, setShow] = useAtom(showSamePathModalAtom)
-
- return (
-
- {`It seems like the folder you've chosen same with current directory`}
-
- setShow(false)}>
- Cancel
-
-
- {
- setShow(false)
- onChangeFolderClick()
- }}
- autoFocus
- >
- Choose a different folder
-
-
-
-
- }
- />
- )
-}
-
-export default ModalSameDirectory
diff --git a/web/screens/Settings/Advanced/DataFolder/index.tsx b/web/screens/Settings/Advanced/DataFolder/index.tsx
deleted file mode 100644
index f37d6e814..000000000
--- a/web/screens/Settings/Advanced/DataFolder/index.tsx
+++ /dev/null
@@ -1,140 +0,0 @@
-import { Fragment, useCallback, useState } from 'react'
-
-import { AppConfiguration, isSubdirectory } from '@janhq/core'
-import { Button, Input } from '@janhq/joi'
-import { useAtomValue, useSetAtom } from 'jotai'
-import { PencilIcon, FolderOpenIcon } from 'lucide-react'
-
-import Loader from '@/containers/Loader'
-
-export const SUCCESS_SET_NEW_DESTINATION = 'successSetNewDestination'
-
-import { useApp } from '@/hooks/useApp'
-
-import ModalChangeDirectory, {
- showDirectoryConfirmModalAtom,
-} from './ModalChangeDirectory'
-import ModalChangeDestNotEmpty, {
- showDestNotEmptyConfirmAtom,
-} from './ModalConfirmDestNotEmpty'
-import ModalErrorSetDestGlobal, {
- showChangeFolderErrorAtom,
-} from './ModalErrorSetDestGlobal'
-
-import ModalSameDirectory, { showSamePathModalAtom } from './ModalSameDirectory'
-
-import { janDataFolderPathAtom } from '@/helpers/atoms/AppConfig.atom'
-
-const DataFolder = () => {
- const [showLoader, setShowLoader] = useState(false)
- const setShowDirectoryConfirm = useSetAtom(showDirectoryConfirmModalAtom)
- const setShowSameDirectory = useSetAtom(showSamePathModalAtom)
- const setShowChangeFolderError = useSetAtom(showChangeFolderErrorAtom)
- const showDestNotEmptyConfirm = useSetAtom(showDestNotEmptyConfirmAtom)
-
- const [destinationPath, setDestinationPath] = useState(undefined)
- const janDataFolderPath = useAtomValue(janDataFolderPathAtom)
- const { relaunch } = useApp()
-
- const onChangeFolderClick = useCallback(async () => {
- const destFolder = await window.core?.api?.selectDirectory()
- if (!destFolder) return
-
- if (destFolder === janDataFolderPath) {
- setShowSameDirectory(true)
- return
- }
-
- const appConfiguration: AppConfiguration =
- await window.core?.api?.getAppConfigurations()
- const currentJanDataFolder = appConfiguration.data_folder
-
- if (await isSubdirectory(currentJanDataFolder, destFolder)) {
- setShowSameDirectory(true)
- return
- }
-
- const isEmpty: boolean =
- await window.core?.api?.isDirectoryEmpty(destFolder)
-
- if (!isEmpty) {
- setDestinationPath(destFolder)
- showDestNotEmptyConfirm(true)
- return
- }
-
- setDestinationPath(destFolder)
- setShowDirectoryConfirm(true)
- }, [
- janDataFolderPath,
- setShowDirectoryConfirm,
- setShowSameDirectory,
- showDestNotEmptyConfirm,
- ])
-
- const onUserConfirmed = useCallback(async () => {
- if (!destinationPath) return
- try {
- setShowLoader(true)
- await window.core?.api?.changeDataFolder(destinationPath)
- localStorage.setItem(SUCCESS_SET_NEW_DESTINATION, 'true')
- setTimeout(() => {
- setShowLoader(false)
- }, 1200)
- await relaunch()
- } catch (e) {
- console.error(e)
- setShowLoader(false)
- setShowChangeFolderError(true)
- }
- }, [destinationPath, setShowChangeFolderError])
-
- return (
-
-
-
-
-
Jan Data Folder
-
-
- Default location for messages and other user data.
-
-
-
-
-
- window.core?.api?.openAppDirectory()}
- />
-
-
-
-
-
-
-
-
-
-
- {showLoader && }
-
- )
-}
-
-export default DataFolder
diff --git a/web/screens/Settings/Advanced/FactoryReset/ModalConfirmReset.tsx b/web/screens/Settings/Advanced/FactoryReset/ModalConfirmReset.tsx
deleted file mode 100644
index 207a11ee6..000000000
--- a/web/screens/Settings/Advanced/FactoryReset/ModalConfirmReset.tsx
+++ /dev/null
@@ -1,81 +0,0 @@
-import React, { useCallback, useState } from 'react'
-
-import { Modal, ModalClose, Button, Input, Checkbox } from '@janhq/joi'
-
-import { atom, useAtom, useAtomValue } from 'jotai'
-
-import useFactoryReset from '@/hooks/useFactoryReset'
-
-import { defaultJanDataFolderAtom } from '@/helpers/atoms/App.atom'
-
-export const modalValidationAtom = atom(false)
-
-const ModalConfirmReset = () => {
- const [modalValidation, setModalValidation] = useAtom(modalValidationAtom)
- const defaultJanDataFolder = useAtomValue(defaultJanDataFolderAtom)
- const { resetAll } = useFactoryReset()
- const [inputValue, setInputValue] = useState('')
- const [currentDirectoryChecked, setCurrentDirectoryChecked] = useState(true)
-
- const onFactoryResetClick = useCallback(() => {
- setModalValidation(false)
- resetAll(currentDirectoryChecked)
- }, [currentDirectoryChecked, resetAll, setModalValidation])
-
- return (
- setModalValidation(false)}
- title="Are you sure you want to reset to default settings?"
- content={
-
-
- Restore application to its initial state, erasing all models and
- chat history. This action is irreversible and recommended only if
- the application is corrupted.
-
-
-
-
{`To confirm, please enter the word "RESET" below:`}
-
setInputValue(e.target.value)}
- />
-
-
-
setCurrentDirectoryChecked(e.target.checked)}
- label="Keep the current app data location"
- helperDescription={
-
- Otherwise it will reset back to its original location at:{' '}
- {defaultJanDataFolder}
-
- }
- />
-
-
-
- setModalValidation(false)}>
- Cancel
-
-
-
- Reset Now
-
-
-
-
- }
- />
- )
-}
-
-export default ModalConfirmReset
diff --git a/web/screens/Settings/Advanced/FactoryReset/ResettingModal.tsx b/web/screens/Settings/Advanced/FactoryReset/ResettingModal.tsx
deleted file mode 100644
index 6b24d0b70..000000000
--- a/web/screens/Settings/Advanced/FactoryReset/ResettingModal.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import { Modal } from '@janhq/joi'
-import { atom, useAtomValue } from 'jotai'
-
-import {
- FactoryResetState,
- factoryResetStateAtom,
-} from '@/hooks/useFactoryReset'
-
-const resetModalVisibilityAtom = atom((get) => {
- const visible = get(factoryResetStateAtom) !== FactoryResetState.Idle
- return visible
-})
-
-const ResettingModal = () => {
- const visibility = useAtomValue(resetModalVisibilityAtom)
-
- return (
- >}
- />
- )
-}
-
-export default ResettingModal
diff --git a/web/screens/Settings/Advanced/FactoryReset/index.tsx b/web/screens/Settings/Advanced/FactoryReset/index.tsx
deleted file mode 100644
index 72642fafe..000000000
--- a/web/screens/Settings/Advanced/FactoryReset/index.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import { Button } from '@janhq/joi'
-
-import { useSetAtom } from 'jotai'
-
-import ModalValidation, { modalValidationAtom } from './ModalConfirmReset'
-import ResettingModal from './ResettingModal'
-
-const FactoryReset = () => {
- const setModalValidation = useSetAtom(modalValidationAtom)
-
- return (
-
-
-
-
- Reset to Factory Settings
-
-
-
- Restore application to its initial state, erasing all models and chat
- history. This action is irreversible and recommended only if the
- application is corrupted.
-
-
-
setModalValidation(true)}
- >
- Reset
-
-
-
-
- )
-}
-
-export default FactoryReset
diff --git a/web/screens/Settings/Advanced/ProxySettings/index.test.tsx b/web/screens/Settings/Advanced/ProxySettings/index.test.tsx
deleted file mode 100644
index 1cd4d645f..000000000
--- a/web/screens/Settings/Advanced/ProxySettings/index.test.tsx
+++ /dev/null
@@ -1,147 +0,0 @@
-/**
- * @jest-environment jsdom
- */
-
-import React from 'react'
-import { render, screen, fireEvent, waitFor } from '@testing-library/react'
-import '@testing-library/jest-dom'
-import ProxySettings from '.'
-
-// Mock ResizeObserver
-class ResizeObserverMock {
- observe() {}
- unobserve() {}
- disconnect() {}
-}
-
-global.ResizeObserver = ResizeObserverMock as any
-
-// Mock global window.core
-global.window.core = {
- api: {
- getAppConfigurations: () => jest.fn(),
- updateAppConfiguration: () => jest.fn(),
- relaunch: () => jest.fn(),
- },
-}
-
-// Mock dependencies
-jest.mock('@/hooks/useConfigurations', () => ({
- useConfigurations: () => ({
- configurePullOptions: jest.fn(),
- }),
-}))
-
-jest.mock('jotai', () => {
- const originalModule = jest.requireActual('jotai')
- return {
- ...originalModule,
- useAtom: jest.fn().mockImplementation((atom) => {
- switch (atom) {
- case 'proxyEnabledAtom':
- return [true, jest.fn()]
- case 'proxyAtom':
- return ['', jest.fn()]
- case 'proxyUsernameAtom':
- return ['', jest.fn()]
- case 'proxyPasswordAtom':
- return ['', jest.fn()]
- case 'ignoreSslAtom':
- return [false, jest.fn()]
- case 'verifyProxySslAtom':
- return [true, jest.fn()]
- case 'verifyProxyHostSslAtom':
- return [true, jest.fn()]
- case 'verifyPeerSslAtom':
- return [true, jest.fn()]
- case 'verifyHostSslAtom':
- return [true, jest.fn()]
- case 'noProxyAtom':
- return ['localhost', jest.fn()]
- default:
- return [null, jest.fn()]
- }
- }),
- }
-})
-
-describe('ProxySettings', () => {
- const mockOnBack = jest.fn()
-
- beforeEach(() => {
- jest.clearAllMocks()
- })
-
- it('renders the component', async () => {
- render( )
-
- await waitFor(() => {
- expect(screen.getByText('HTTPS Proxy')).toBeInTheDocument()
- expect(screen.getByText('Proxy URL')).toBeInTheDocument()
- expect(screen.getByText('Authentication')).toBeInTheDocument()
- expect(screen.getByText('No Proxy')).toBeInTheDocument()
- expect(screen.getByText('SSL Verification')).toBeInTheDocument()
- })
- })
-
- it('handles back navigation', async () => {
- render( )
-
- const backButton = screen.getByText('Advanced Settings')
- fireEvent.click(backButton)
-
- expect(mockOnBack).toHaveBeenCalled()
- })
-
- it('toggles password visibility', () => {
- render( )
-
- const passwordVisibilityToggle = screen.getByTestId('password-visibility-toggle')
- const passwordInput = screen.getByTestId('proxy-password')
-
- expect(passwordInput).toHaveAttribute('type', 'password')
-
- fireEvent.click(passwordVisibilityToggle)
- expect(passwordInput).toHaveAttribute('type', 'text')
-
- fireEvent.click(passwordVisibilityToggle)
- expect(passwordInput).toHaveAttribute('type', 'password')
- })
-
- it('allows clearing input fields', async () => {
- render( )
-
- // Test clearing proxy URL
- const proxyInput = screen.getByTestId('proxy-input')
- fireEvent.change(proxyInput, { target: { value: 'http://test.proxy' } })
-
- const clearProxyButton = screen.getByTestId('clear-proxy-button')
- fireEvent.click(clearProxyButton)
- expect(proxyInput).toHaveValue('')
-
- // Test clearing username
- const usernameInput = screen.getByTestId('proxy-username')
- fireEvent.change(usernameInput, { target: { value: 'testuser' } })
-
- // Test clearing password
- const passwordInput = screen.getByTestId('proxy-password')
- fireEvent.change(passwordInput, { target: { value: 'testpassword' } })
- })
-
- it('renders SSL verification switches', async () => {
- render( )
-
- const sslSwitches = [
- 'Ignore SSL certificates',
- 'Verify Proxy SSL',
- 'Verify Proxy Host SSL',
- 'Verify Peer SSL',
- 'Verify Host SSL'
- ]
-
- sslSwitches.forEach(switchText => {
- const switchElement = screen.getByText(switchText)
- expect(switchElement).toBeInTheDocument()
- })
- })
-})
\ No newline at end of file
diff --git a/web/screens/Settings/Advanced/ProxySettings/index.tsx b/web/screens/Settings/Advanced/ProxySettings/index.tsx
deleted file mode 100644
index 7d5723ca8..000000000
--- a/web/screens/Settings/Advanced/ProxySettings/index.tsx
+++ /dev/null
@@ -1,397 +0,0 @@
-import { useCallback, useState } from 'react'
-
-import { Input, ScrollArea, Switch } from '@janhq/joi'
-import { useAtom, useAtomValue } from 'jotai'
-import { EyeIcon, EyeOffIcon, XIcon, ArrowLeftIcon } from 'lucide-react'
-import { useDebouncedCallback } from 'use-debounce'
-
-import { useConfigurations } from '@/hooks/useConfigurations'
-
-import {
- ignoreSslAtom,
- proxyAtom,
- verifyProxySslAtom,
- verifyProxyHostSslAtom,
- verifyPeerSslAtom,
- verifyHostSslAtom,
- noProxyAtom,
- proxyUsernameAtom,
- proxyPasswordAtom,
-} from '@/helpers/atoms/AppConfig.atom'
-import { showScrollBarAtom } from '@/helpers/atoms/Setting.atom'
-
-const ProxySettings = ({ onBack }: { onBack: () => void }) => {
- const [proxy, setProxy] = useAtom(proxyAtom)
- const [noProxy, setNoProxy] = useAtom(noProxyAtom)
- const [partialProxy, setPartialProxy] = useState(proxy)
- const [proxyUsername, setProxyUsername] = useAtom(proxyUsernameAtom)
- const [proxyPassword, setProxyPassword] = useAtom(proxyPasswordAtom)
- const [proxyPartialPassword, setProxyPartialPassword] =
- useState(proxyPassword)
- const [proxyPartialUsername, setProxyPartialUsername] =
- useState(proxyUsername)
- const { configurePullOptions } = useConfigurations()
- const [ignoreSSL, setIgnoreSSL] = useAtom(ignoreSslAtom)
- const [verifyProxySSL, setVerifyProxySSL] = useAtom(verifyProxySslAtom)
- const [verifyProxyHostSSL, setVerifyProxyHostSSL] = useAtom(
- verifyProxyHostSslAtom
- )
- const [verifyPeerSSL, setVerifyPeerSSL] = useAtom(verifyPeerSslAtom)
- const [verifyHostSSL, setVerifyHostSSL] = useAtom(verifyHostSslAtom)
- const [showPassword, setShowPassword] = useState(false)
- const showScrollBar = useAtomValue(showScrollBarAtom)
-
- const updatePullOptions = useDebouncedCallback(
- () => configurePullOptions(),
- 1000
- )
-
- const onProxyChange = useDebouncedCallback((value: string) => {
- if (value.trim().startsWith('http')) {
- setProxy(value.trim())
- updatePullOptions()
- } else {
- setProxy('')
- updatePullOptions()
- }
- }, 1000)
-
- const onProxyUsernameChange = useDebouncedCallback((value: string) => {
- setProxyUsername(value)
- updatePullOptions()
- }, 1000)
-
- const onProxyPasswordChange = useDebouncedCallback((value: string) => {
- setProxyPassword(value)
- updatePullOptions()
- }, 1000)
-
- const handleProxyInputChange = useCallback(
- (e: React.ChangeEvent) => {
- const value = e.target.value || ''
- setPartialProxy(value)
- onProxyChange(value)
- },
- [setPartialProxy, onProxyChange]
- )
-
- const handleProxyUsernameInputChange = useCallback(
- (e: React.ChangeEvent) => {
- const value = e.target.value || ''
- setProxyPartialUsername(value)
- onProxyUsernameChange(value)
- },
- [setProxyPartialUsername, onProxyUsernameChange]
- )
-
- const handleProxyPasswordInputChange = useCallback(
- (e: React.ChangeEvent) => {
- const value = e.target.value || ''
- setProxyPartialPassword(value)
- onProxyPasswordChange(value)
- },
- [setProxyPartialPassword, onProxyPasswordChange]
- )
-
- const onNoProxyChange = useCallback(
- (e: React.ChangeEvent) => {
- const listNoProxy = e.target.value || ''
- const listNoProxyTrim = listNoProxy.split(',').map((item) => item.trim())
- setNoProxy(listNoProxyTrim.join(','))
- updatePullOptions()
- },
- [setNoProxy, updatePullOptions]
- )
-
- return (
-
- {/* Header */}
-
-
-
-
- Advanced Settings
-
-
/
-
- HTTPS Proxy
-
-
-
-
- {/* Content */}
-
-
-
-
Proxy Configuration
-
-
-
-
-
-
-
Proxy URL
-
- URL and port of your proxy server.
-
-
-
-
-
-
- {partialProxy && (
- {
- setPartialProxy('')
- setProxy('')
- }}
- className="p-1 hover:text-[hsla(var(--text-primary))]"
- >
-