diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index a6fa42e2db..0000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve Jan -title: 'bug: [DESCRIPTION]' -labels: 'type: bug' -assignees: '' - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**Steps to reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your issue. - -**Environment details** -- Operating System: [Specify your OS. e.g., MacOS Sonoma 14.2.1, Windows 11, Ubuntu 22, etc] -- Jan Version: [e.g., 0.4.xxx nightly or manual] -- Processor: [e.g., Apple M1, Intel Core i7, AMD Ryzen 5, etc] -- RAM: [e.g., 8GB, 16GB] -- Any additional relevant hardware specifics: [e.g., Graphics card, SSD/HDD] - -**Logs** -If the cause of the error is not clear, kindly provide your usage logs: https://jan.ai/docs/troubleshooting#how-to-get-error-logs - -**Additional context** -Add any other context or information that could be helpful in diagnosing the problem. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000000..419643e51b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,82 @@ +name: "\U0001F41B Bug Report" +description: "If something isn't working as expected \U0001F914" +labels: [ "type: bug" ] +title: 'bug: [DESCRIPTION]' + +body: + - type: markdown + attributes: + value: "Thanks for taking the time to fill out this bug report!" + + - type: checkboxes + attributes: + label: "#" + description: "Please search [here](./?q=is%3Aissue) to see if an issue already exists for the bug you encountered" + options: + - label: "I have searched the existing issues" + required: false + + - type: textarea + validations: + required: true + attributes: + label: "Current behavior" + description: "A clear and concise description of what the bug is" + + - type: textarea + validations: + required: true + attributes: + label: "Minimum reproduction step" + description: | + Please list out steps to reproduce the behavior + placeholder: | + 1. Go to '...' + 2. Click on '....' + 3. Scroll down to '....' + 4. See error + + - type: textarea + validations: + required: true + attributes: + label: "Expected behavior" + description: "A clear and concise description of what you expected to happen" + + - type: textarea + validations: + required: true + attributes: + label: "Screenshots / Logs" + description: | + Kindly provide your screenshots / [usage logs](https://jan.ai/docs/troubleshooting#how-to-get-error-logs) that could be helpful in diagnosing the problem + **Tip:** You can attach images, recordings or log files by clicking this area to highlight it and then dragging files in + - type: markdown + attributes: + value: | + --- + + - type: input + validations: + required: true + attributes: + label: "Jan version" + description: "**Tip:** The version is located in the lower right conner of the Jan app" + placeholder: "e.g. 0.5.x-xxx nightly or stable" + + - type: checkboxes + attributes: + label: "In which operating systems have you tested?" + options: + - label: macOS + - label: Windows + - label: Linux + + - type: textarea + attributes: + label: "Environment details" + description: | + - Operating System: [Specify your OS details: e.g., MacOS Sonoma 14.2.1, Windows 11, Ubuntu 22, etc] + - Processor: [e.g., Apple M1, Intel Core i7, AMD Ryzen 5, etc] + - RAM: [e.g., 8GB, 16GB] + - Any additional relevant hardware specifics: [e.g., Graphics card, SSD/HDD] \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..e0c100daf5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,7 @@ +## To encourage contributors to use issue templates, we don't allow blank issues +blank_issues_enabled: true + +contact_links: + - name: "\u2753 Our GitHub Discussions page" + url: "https://github.com/orgs/janhq/discussions/categories/q-a" + about: "Please ask and answer questions here!" \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/discussion-thread.md b/.github/ISSUE_TEMPLATE/discussion-thread.md deleted file mode 100644 index d15921191d..0000000000 --- a/.github/ISSUE_TEMPLATE/discussion-thread.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -name: Discussion thread -about: Start an open ended discussion -title: 'Discussion: [TOPIC HERE]' -labels: 'type: discussion' -assignees: '' - ---- - -**Motivation** - -**Discussion** - -**Alternatives** - -**Resources** diff --git a/.github/ISSUE_TEMPLATE/documentation-request.md b/.github/ISSUE_TEMPLATE/documentation-request.md index 2f2b44f124..4d4dcdb0ec 100644 --- a/.github/ISSUE_TEMPLATE/documentation-request.md +++ b/.github/ISSUE_TEMPLATE/documentation-request.md @@ -1,5 +1,5 @@ --- -name: Documentation request +name: "📖 Documentation request" about: Documentation requests title: 'docs: TITLE' labels: 'type: documentation' diff --git a/.github/ISSUE_TEMPLATE/epic-request.md b/.github/ISSUE_TEMPLATE/epic-request.md index 51941c412d..f86f379fa6 100644 --- a/.github/ISSUE_TEMPLATE/epic-request.md +++ b/.github/ISSUE_TEMPLATE/epic-request.md @@ -1,5 +1,5 @@ --- -name: Epic request +name: "💥 Epic request" about: Suggest an idea for this project title: 'epic: [DESCRIPTION]' labels: 'type: epic' diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 26f586bd0d..0000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: 'feat: [DESCRIPTION]' -labels: 'type: feature request' -assignees: '' - ---- - -**Problem** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Success Criteria** -A clear and concise description of what you want to happen. - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000000..0f7f59f6ca --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,44 @@ +name: "\U0001F680 Feature Request" +description: "Suggest an idea for this project \U0001F63B!" +title: 'feat: [DESCRIPTION]' +labels: 'type: feature request' +body: + - type: markdown + attributes: + value: "Thanks for taking the time to fill out this form!" + + - type: checkboxes + attributes: + label: "#" + description: "Please search [here](./?q=is%3Aissue) to see if an issue already exists for the feature you are requesting" + options: + - label: "I have searched the existing issues" + required: false + + - type: textarea + validations: + required: true + attributes: + label: "Is your feature request related to a problem? Please describe it" + description: "A clear and concise description of what the problem is" + placeholder: | + I'm always frustrated when ... + + - type: textarea + validations: + required: true + attributes: + label: "Describe the solution" + description: "Description of what you want to happen. Add any considered drawbacks" + + - type: textarea + attributes: + label: "Teachability, documentation, adoption, migration strategy" + description: "Explain how users will be able to use this and possibly write out something for the docs. Maybe a screenshot or design?" + + - type: textarea + validations: + required: true + attributes: + label: "What is the motivation / use case for changing the behavior?" + description: "Describe the motivation or the concrete use case" diff --git a/.github/workflows/jan-openai-api-test.yml b/.github/workflows/jan-openai-api-test.yml deleted file mode 100644 index b7e2717b3f..0000000000 --- a/.github/workflows/jan-openai-api-test.yml +++ /dev/null @@ -1,101 +0,0 @@ -name: Test - OpenAI API Pytest collection -on: - workflow_dispatch: - inputs: - endpoints: - description: 'comma-separated list (see available at endpoints_mapping.json e.g. GET /users,POST /transform)' - required: false - default: all - type: string - - push: - branches: - - main - - dev - - release/** - paths: - - "docs/**" - - pull_request: - branches: - - main - - dev - - release/** - paths: - - "docs/**" - -jobs: - openai-python-tests: - runs-on: [self-hosted, Linux, ubuntu-desktop] - if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) || github.event_name == 'push' || github.event_name == 'workflow_dispatch' - steps: - - name: Getting the repo - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - name: Installing node - uses: actions/setup-node@v3 - with: - node-version: 20 - - - name: "Cleanup cache" - continue-on-error: true - run: | - rm -rf ~/jan - make clean - - - name: Install dependencies - run: | - npm install -g @stoplight/prism-cli - - - name: Create python virtual environment and run test - run: | - python3 -m venv /tmp/jan - source /tmp/jan/bin/activate - # Clone openai-api-python repo - OPENAI_API_PYTHON_TAG=$(cat docs/openapi/version.txt) - git clone https://github.com/openai/openai-python.git - cd openai-python - git checkout $OPENAI_API_PYTHON_TAG - - python3 -m venv /tmp/jan - source /tmp/jan/bin/activate - pip install -r requirements-dev.lock - pip install pytest-reportportal pytest-html - - # Create pytest.ini file with content - cat ../docs/tests/pytest.ini >> pytest.ini - echo "rp_api_key = ${{ secrets.REPORT_PORTAL_API_KEY }}" >> pytest.ini - echo "rp_endpoint = ${{ secrets.REPORT_PORTAL_URL_PYTEST }}" >> pytest.ini - cat pytest.ini - - # Append to conftest.py - cat ../docs/tests/conftest.py >> tests/conftest.py - cat ../docs/tests/endpoints_mapping.json >> tests/endpoints_mapping.json - - # start mock server and run test then stop mock server - prism mock ../docs/openapi/jan.yaml > prism.log & prism_pid=$! && - pytest --endpoint "$ENDPOINTS" --reportportal --html=report.html && kill $prism_pid - deactivate - env: - ENDPOINTS: ${{ github.event.inputs.endpoints }} - - - name: Upload Artifact - uses: actions/upload-artifact@v2 - with: - name: report - path: | - openai-python/report.html - openai-python/assets - openai-python/prism.log - - - name: Clean up - if: always() - run: | - rm -rf /tmp/jan - rm -rf openai-python - rm -rf report.html - rm -rf report.zip - - diff --git a/.github/workflows/jan-server-build-nightly.yml b/.github/workflows/jan-server-build-nightly.yml index 3e394be7ec..29e13804ee 100644 --- a/.github/workflows/jan-server-build-nightly.yml +++ b/.github/workflows/jan-server-build-nightly.yml @@ -9,7 +9,7 @@ on: - 'README.md' - 'docs/**' schedule: - - cron: '0 20 * * 1,2,3' # At 8 PM UTC on Monday, Tuesday, and Wednesday which is 3 AM UTC+7 Tuesday, Wednesday, and Thursday + - cron: '0 21 * * 1,2,3' # At 8 PM UTC on Monday, Tuesday, and Wednesday which is 4 AM UTC+7 Tuesday, Wednesday, and Thursday workflow_dispatch: jobs: diff --git a/.github/workflows/nightly-integrate-cortex-cpp.yml b/.github/workflows/nightly-integrate-cortex-cpp.yml new file mode 100644 index 0000000000..e0b48bc46f --- /dev/null +++ b/.github/workflows/nightly-integrate-cortex-cpp.yml @@ -0,0 +1,127 @@ +name: Nightly Update cortex cpp + +on: + schedule: + - cron: '30 19 * * 1-5' # At 01:30 on every day-of-week from Monday through Friday UTC +7 + workflow_dispatch: + +jobs: + update-submodule: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + actions: write + + outputs: + pr_number: ${{ steps.check-update.outputs.pr_number }} + pr_created: ${{ steps.check-update.outputs.pr_created }} + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + submodules: recursive + ref: dev + fetch-depth: 0 + token: ${{ secrets.PAT_SERVICE_ACCOUNT }} + + - name: Configure Git + run: | + git config --global user.name 'github-actions[bot]' + git config --global user.email 'github-actions[bot]@users.noreply.github.com' + + - name: Update submodule to latest release + id: check-update + env: + GITHUB_TOKEN: ${{ secrets.PAT_SERVICE_ACCOUNT }} + run: | + curl -s https://api.github.com/repos/janhq/cortex/releases > /tmp/github_api_releases.json + latest_prerelease_name=$(cat /tmp/github_api_releases.json | jq -r '.[] | select(.prerelease) | .name' | head -n 1) + + get_asset_count() { + local version_name=$1 + cat /tmp/github_api_releases.json | jq -r --arg version_name "$version_name" '.[] | select(.name == $version_name) | .assets | length' + } + + cortex_cpp_version_file_path="extensions/inference-nitro-extension/bin/version.txt" + current_version_name=$(cat "$cortex_cpp_version_file_path" | head -n 1) + + current_version_asset_count=$(get_asset_count "$current_version_name") + latest_prerelease_asset_count=$(get_asset_count "$latest_prerelease_name") + + if [ "$current_version_name" = "$latest_prerelease_name" ]; then + echo "cortex cpp remote repo doesn't have update today, skip update cortex-cpp for today nightly build" + echo "::set-output name=pr_created::false" + exit 0 + fi + + if [ "$current_version_asset_count" != "$latest_prerelease_asset_count" ]; then + echo "Latest prerelease version has different number of assets, somethink went wrong, skip update cortex-cpp for today nightly build" + echo "::set-output name=pr_created::false" + exit 1 + fi + + echo $latest_prerelease_name > $cortex_cpp_version_file_path + echo "Updated version from $current_version_name to $latest_prerelease_name." + echo "::set-output name=pr_created::true" + + git add -f $cortex_cpp_version_file_path + git commit -m "Update cortex cpp nightly to version $latest_prerelease_name" + branch_name="update-nightly-$(date +'%Y-%m-%d-%H-%M')" + git checkout -b $branch_name + git push origin $branch_name + + pr_title="Update cortex cpp nightly to version $latest_prerelease_name" + pr_body="This PR updates the Update cortex cpp nightly to version $latest_prerelease_name" + + gh pr create --title "$pr_title" --body "$pr_body" --head $branch_name --base dev --reviewer Van-QA + + pr_number=$(gh pr list --head $branch_name --json number --jq '.[0].number') + echo "::set-output name=pr_number::$pr_number" + + check-and-merge-pr: + needs: update-submodule + if: needs.update-submodule.outputs.pr_created == 'true' + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + submodules: recursive + fetch-depth: 0 + token: ${{ secrets.PAT_SERVICE_ACCOUNT }} + + - name: Wait for CI to pass + env: + GITHUB_TOKEN: ${{ secrets.PAT_SERVICE_ACCOUNT }} + run: | + pr_number=${{ needs.update-submodule.outputs.pr_number }} + while true; do + ci_completed=$(gh pr checks $pr_number --json completedAt --jq '.[].completedAt') + if echo "$ci_completed" | grep -q "0001-01-01T00:00:00Z"; then + echo "CI is still running, waiting..." + sleep 60 + else + echo "CI has completed, checking states..." + ci_states=$(gh pr checks $pr_number --json state --jq '.[].state') + if echo "$ci_states" | grep -vqE "SUCCESS|SKIPPED"; then + echo "CI failed, exiting..." + exit 1 + else + echo "CI passed, merging PR..." + break + fi + fi + done + + - name: Merge the PR + env: + GITHUB_TOKEN: ${{ secrets.PAT_SERVICE_ACCOUNT }} + run: | + pr_number=${{ needs.update-submodule.outputs.pr_number }} + gh pr merge $pr_number --merge --admin diff --git a/Makefile b/Makefile index 204e1698f9..1687f8bbe8 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ install-and-build: build-joi ifeq ($(OS),Windows_NT) yarn config set network-timeout 300000 endif - yarn global add turbo + yarn global add turbo@1.13.2 yarn build:core yarn build:server yarn install diff --git a/electron/handlers/native.ts b/electron/handlers/native.ts index 1bc815b41e..869b9fd58e 100644 --- a/electron/handlers/native.ts +++ b/electron/handlers/native.ts @@ -1,4 +1,4 @@ -import { app, ipcMain, dialog, shell, nativeTheme, screen } from 'electron' +import { app, ipcMain, dialog, shell, nativeTheme } from 'electron' import { join } from 'path' import { windowManager } from '../managers/window' import { @@ -41,12 +41,9 @@ export function handleAppIPCs() { windowManager.mainWindow?.minimize() }) - ipcMain.handle(NativeRoute.setMaximizeApp, async () => { + ipcMain.handle(NativeRoute.setMaximizeApp, async (_event) => { if (windowManager.mainWindow?.isMaximized()) { - // const bounds = await getBounds() - // windowManager.mainWindow?.setSize(bounds.width, bounds.height) - // windowManager.mainWindow?.setPosition(Number(bounds.x), Number(bounds.y)) - windowManager.mainWindow.restore() + windowManager.mainWindow.unmaximize() } else { windowManager.mainWindow?.maximize() } diff --git a/electron/managers/mainWindowConfig.ts b/electron/managers/mainWindowConfig.ts index 25f0635f7a..c3f9c01bda 100644 --- a/electron/managers/mainWindowConfig.ts +++ b/electron/managers/mainWindowConfig.ts @@ -4,11 +4,12 @@ export const mainWindowConfig: Electron.BrowserWindowConstructorOptions = { skipTaskbar: false, minWidth: DEFAULT_MIN_WIDTH, show: true, + transparent: true, + frame: false, titleBarStyle: 'hidden', vibrancy: 'fullscreen-ui', visualEffectState: 'active', backgroundMaterial: 'acrylic', - maximizable: false, autoHideMenuBar: true, trafficLightPosition: { x: 16, diff --git a/extensions/inference-nitro-extension/bin/version.txt b/extensions/inference-nitro-extension/bin/version.txt index 5f749c1366..1f77169993 100644 --- a/extensions/inference-nitro-extension/bin/version.txt +++ b/extensions/inference-nitro-extension/bin/version.txt @@ -1 +1 @@ -0.4.11 +0.4.13 diff --git a/extensions/inference-nitro-extension/package.json b/extensions/inference-nitro-extension/package.json index ce19734d28..23c3ec6137 100644 --- a/extensions/inference-nitro-extension/package.json +++ b/extensions/inference-nitro-extension/package.json @@ -1,7 +1,7 @@ { "name": "@janhq/inference-cortex-extension", "productName": "Cortex Inference Engine", - "version": "1.0.11", + "version": "1.0.12", "description": "This extension embeds cortex.cpp, a lightweight inference engine written in C++. See https://nitro.jan.ai.\nAdditional dependencies could be installed to run without Cuda Toolkit installation.", "main": "dist/index.js", "node": "dist/node/index.cjs.js", diff --git a/extensions/inference-nitro-extension/resources/models/qwen2-7b/model.json b/extensions/inference-nitro-extension/resources/models/qwen2-7b/model.json new file mode 100644 index 0000000000..39343575c7 --- /dev/null +++ b/extensions/inference-nitro-extension/resources/models/qwen2-7b/model.json @@ -0,0 +1,36 @@ +{ + "sources": [ + { + "filename": "Qwen2-7B-Instruct-Q4_K_M.gguf", + "url": "https://huggingface.co/bartowski/Qwen2-7B-Instruct-GGUF/resolve/main/Qwen2-7B-Instruct-Q4_K_M.gguf" + } + ], + "id": "qwen2-7b", + "object": "model", + "name": "Qwen 2 Instruct 7B Q4", + "version": "1.0", + "description": "Qwen is optimized at Chinese, ideal for everyday tasks.", + "format": "gguf", + "settings": { + "ctx_len": 32768, + "prompt_template": "<|im_start|>system\n{system_message}<|im_end|>\n<|im_start|>user\n{prompt}<|im_end|>\n<|im_start|>assistant", + "llama_model_path": "Qwen2-7B-Instruct-Q4_K_M.gguf", + "ngl": 28 + }, + "parameters": { + "temperature": 0.7, + "top_p": 0.95, + "stream": true, + "max_tokens": 32768, + "stop": [], + "frequency_penalty": 0, + "presence_penalty": 0 + }, + "metadata": { + "author": "Alibaba", + "tags": ["7B", "Finetuned"], + "size": 4680000000 + }, + "engine": "nitro" + } + \ No newline at end of file diff --git a/extensions/inference-nitro-extension/rollup.config.ts b/extensions/inference-nitro-extension/rollup.config.ts index 3a790b5016..71712a4d67 100644 --- a/extensions/inference-nitro-extension/rollup.config.ts +++ b/extensions/inference-nitro-extension/rollup.config.ts @@ -39,6 +39,8 @@ const aya8bJson = require('./resources/models/aya-23-8b/model.json') const aya35bJson = require('./resources/models/aya-23-35b/model.json') const phimediumJson = require('./resources/models/phi3-medium/model.json') const codestralJson = require('./resources/models/codestral-22b/model.json') +const qwen2Json = require('./resources/models/qwen2-7b/model.json') + export default [ { @@ -84,7 +86,8 @@ export default [ phimediumJson, aya8bJson, aya35bJson, - codestralJson + codestralJson, + qwen2Json ]), NODE: JSON.stringify(`${packageJson.name}/${packageJson.node}`), DEFAULT_SETTINGS: JSON.stringify(defaultSettingJson), diff --git a/extensions/inference-nvidia-extension/README.md b/extensions/inference-nvidia-extension/README.md new file mode 100644 index 0000000000..65a1b2b593 --- /dev/null +++ b/extensions/inference-nvidia-extension/README.md @@ -0,0 +1,79 @@ +# Nvidia Engine Extension + +Created using Jan extension example + +# Create a Jan Extension using Typescript + +Use this template to bootstrap the creation of a TypeScript Jan extension. 🚀 + +## Create Your Own Extension + +To create your own extension, you can use this repository as a template! Just follow the below instructions: + +1. Click the Use this template button at the top of the repository +2. Select Create a new repository +3. Select an owner and name for your new repository +4. Click Create repository +5. Clone your new repository + +## Initial Setup + +After you've cloned the repository to your local machine or codespace, you'll need to perform some initial setup steps before you can develop your extension. + +> [!NOTE] +> +> You'll need to have a reasonably modern version of +> [Node.js](https://nodejs.org) handy. If you are using a version manager like +> [`nodenv`](https://github.com/nodenv/nodenv) or +> [`nvm`](https://github.com/nvm-sh/nvm), you can run `nodenv install` in the +> root of your repository to install the version specified in +> [`package.json`](./package.json). Otherwise, 20.x or later should work! + +1. :hammer_and_wrench: Install the dependencies + + ```bash + npm install + ``` + +1. :building_construction: Package the TypeScript for distribution + + ```bash + npm run bundle + ``` + +1. :white_check_mark: Check your artifact + + There will be a tgz file in your extension directory now + +## Update the Extension Metadata + +The [`package.json`](package.json) file defines metadata about your extension, such as +extension name, main entry, description and version. + +When you copy this repository, update `package.json` with the name, description for your extension. + +## Update the Extension Code + +The [`src/`](./src/) directory is the heart of your extension! This contains the +source code that will be run when your extension functions are invoked. You can replace the +contents of this directory with your own code. + +There are a few things to keep in mind when writing your extension code: + +- Most Jan Extension functions are processed asynchronously. + In `index.ts`, you will see that the extension function will return a `Promise`. + + ```typescript + import { events, MessageEvent, MessageRequest } from '@janhq/core' + + function onStart(): Promise { + return events.on(MessageEvent.OnMessageSent, (data: MessageRequest) => + this.inference(data) + ) + } + ``` + + For more information about the Jan Extension Core module, see the + [documentation](https://github.com/janhq/jan/blob/main/core/README.md). + +So, what are you waiting for? Go ahead and start customizing your extension! diff --git a/extensions/inference-nvidia-extension/package.json b/extensions/inference-nvidia-extension/package.json new file mode 100644 index 0000000000..8bd7708bc8 --- /dev/null +++ b/extensions/inference-nvidia-extension/package.json @@ -0,0 +1,43 @@ +{ + "name": "@janhq/inference-nvidia-extension", + "productName": "NVIDIA NIM Inference Engine", + "version": "1.0.1", + "description": "This extension enables NVIDIA chat completion API calls", + "main": "dist/index.js", + "module": "dist/module.js", + "engine": "nvidia", + "author": "Jan ", + "license": "AGPL-3.0", + "scripts": { + "build": "tsc -b . && webpack --config webpack.config.js", + "build:publish": "rimraf *.tgz --glob && yarn build && npm pack && cpx *.tgz ../../pre-install" + }, + "exports": { + ".": "./dist/index.js", + "./main": "./dist/module.js" + }, + "devDependencies": { + "cpx": "^1.5.0", + "rimraf": "^3.0.2", + "webpack": "^5.88.2", + "webpack-cli": "^5.1.4", + "ts-loader": "^9.5.0" + }, + "dependencies": { + "@janhq/core": "file:../../core", + "fetch-retry": "^5.0.6", + "path-browserify": "^1.0.1", + "ulidx": "^2.3.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "files": [ + "dist/*", + "package.json", + "README.md" + ], + "bundleDependencies": [ + "fetch-retry" + ] +} diff --git a/extensions/inference-nvidia-extension/resources/models.json b/extensions/inference-nvidia-extension/resources/models.json new file mode 100644 index 0000000000..b97644fc99 --- /dev/null +++ b/extensions/inference-nvidia-extension/resources/models.json @@ -0,0 +1,31 @@ +[ + { + "sources": [ + { + "url": "https://integrate.api.nvidia.com/v1/chat/completions" + } + ], + "id": "mistralai/mistral-7b-instruct-v0.2", + "object": "model", + "name": "Mistral 7B", + "version": "1.1", + "description": "Mistral 7B with NVIDIA", + "format": "api", + "settings": {}, + "parameters": { + "max_tokens": 1024, + "temperature": 0.3, + "top_p": 1, + "stream": false, + "frequency_penalty": 0, + "presence_penalty": 0, + "stop": null, + "seed": null + }, + "metadata": { + "author": "NVIDIA", + "tags": ["General"] + }, + "engine": "nvidia" + } +] diff --git a/extensions/inference-nvidia-extension/resources/settings.json b/extensions/inference-nvidia-extension/resources/settings.json new file mode 100644 index 0000000000..e7647b5621 --- /dev/null +++ b/extensions/inference-nvidia-extension/resources/settings.json @@ -0,0 +1,24 @@ +[ + { + "key": "chat-completions-endpoint", + "title": "Chat Completions Endpoint", + "description": "The endpoint to use for chat completions. See the [NVIDIA API documentation](https://www.nvidia.com/en-us/ai/) for more information.", + "controllerType": "input", + "controllerProps": { + "placeholder": "https://integrate.api.nvidia.com/v1/chat/completions", + "value": "https://integrate.api.nvidia.com/v1/chat/completions" + } + }, + { + "key": "nvidia-api-key", + "title": "API Key", + "description": "The NVIDIA API uses API keys for authentication. Visit your [API Keys](https://org.ngc.nvidia.com/setup/personal-keys) page to retrieve the API key you'll use in your requests..", + "controllerType": "input", + "controllerProps": { + "placeholder": "nvapi-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "value": "", + "type": "password", + "inputActions": ["unobscure", "copy"] + } + } +] diff --git a/extensions/inference-nvidia-extension/src/index.ts b/extensions/inference-nvidia-extension/src/index.ts new file mode 100644 index 0000000000..9af27d90c7 --- /dev/null +++ b/extensions/inference-nvidia-extension/src/index.ts @@ -0,0 +1,66 @@ +/** + * @file This file exports a class that implements the InferenceExtension interface from the @janhq/core package. + * The class provides methods for initializing and stopping a model, and for making inference requests. + * It also subscribes to events emitted by the @janhq/core package and handles new message requests. + * @version 1.0.0 + * @module inference-mistral-extension/src/index + */ + +import { RemoteOAIEngine } from '@janhq/core' + +declare const SETTINGS: Array +declare const MODELS: Array + +enum Settings { + apiKey = 'nvidia-api-key', + chatCompletionsEndPoint = 'chat-completions-endpoint', +} +/** + * A class that implements the InferenceExtension interface from the @janhq/core package. + * The class provides methods for initializing and stopping a model, and for making inference requests. + * It also subscribes to events emitted by the @janhq/core package and handles new message requests. + */ +export default class JanNVIDIANIMInferenceEngine extends RemoteOAIEngine { + inferenceUrl: string = '' + provider: string = 'nvidia' + + override async onLoad(): Promise { + super.onLoad() + + // Register Settings + this.registerSettings(SETTINGS) + this.registerModels(MODELS) + + this.apiKey = await this.getSetting(Settings.apiKey, '') + this.inferenceUrl = await this.getSetting( + Settings.chatCompletionsEndPoint, + '' + ) + + if (this.inferenceUrl.length === 0) { + SETTINGS.forEach((setting) => { + if (setting.key === Settings.chatCompletionsEndPoint) { + this.inferenceUrl = setting.controllerProps.value as string + } + }) + } + } + + onSettingUpdate(key: string, value: T): void { + if (key === Settings.apiKey) { + this.apiKey = value as string + } else if (key === Settings.chatCompletionsEndPoint) { + if (typeof value !== 'string') return + + if (value.trim().length === 0) { + SETTINGS.forEach((setting) => { + if (setting.key === Settings.chatCompletionsEndPoint) { + this.inferenceUrl = setting.controllerProps.value as string + } + }) + } else { + this.inferenceUrl = value + } + } + } +} diff --git a/extensions/inference-nvidia-extension/tsconfig.json b/extensions/inference-nvidia-extension/tsconfig.json new file mode 100644 index 0000000000..2477d58ce5 --- /dev/null +++ b/extensions/inference-nvidia-extension/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "es2016", + "module": "ES6", + "moduleResolution": "node", + "outDir": "./dist", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": false, + "skipLibCheck": true, + "rootDir": "./src" + }, + "include": ["./src"] +} diff --git a/extensions/inference-nvidia-extension/webpack.config.js b/extensions/inference-nvidia-extension/webpack.config.js new file mode 100644 index 0000000000..0e35fc227b --- /dev/null +++ b/extensions/inference-nvidia-extension/webpack.config.js @@ -0,0 +1,42 @@ +const path = require('path') +const webpack = require('webpack') +const packageJson = require('./package.json') +const settingJson = require('./resources/settings.json') +const modelsJson = require('./resources/models.json') + +module.exports = { + experiments: { outputModule: true }, + entry: './src/index.ts', // Adjust the entry point to match your project's main file + mode: 'production', + module: { + rules: [ + { + test: /\.tsx?$/, + use: 'ts-loader', + exclude: /node_modules/, + }, + ], + }, + plugins: [ + new webpack.DefinePlugin({ + SETTINGS: JSON.stringify(settingJson), + ENGINE: JSON.stringify(packageJson.engine), + MODELS: JSON.stringify(modelsJson), + }), + ], + output: { + filename: 'index.js', // Adjust the output file name as needed + path: path.resolve(__dirname, 'dist'), + library: { type: 'module' }, // Specify ESM output format + }, + resolve: { + extensions: ['.ts', '.js'], + fallback: { + path: require.resolve('path-browserify'), + }, + }, + optimization: { + minimize: false, + }, + // Add loaders and other configuration as needed for your project +} diff --git a/extensions/model-extension/package.json b/extensions/model-extension/package.json index 908dc6deeb..c0ca949bd1 100644 --- a/extensions/model-extension/package.json +++ b/extensions/model-extension/package.json @@ -1,7 +1,7 @@ { "name": "@janhq/model-extension", "productName": "Model Management", - "version": "1.0.31", + "version": "1.0.32", "description": "Model Management Extension provides model exploration and seamless downloads", "main": "dist/index.js", "node": "dist/node/index.cjs.js", diff --git a/extensions/model-extension/src/index.ts b/extensions/model-extension/src/index.ts index 887ce7474d..aa8f6603bf 100644 --- a/extensions/model-extension/src/index.ts +++ b/extensions/model-extension/src/index.ts @@ -417,6 +417,30 @@ export default class JanModelExtension extends ModelExtension { ) } + private async getModelJsonPath( + folderFullPath: string + ): Promise { + // try to find model.json recursively inside each folder + if (!(await fs.existsSync(folderFullPath))) return undefined + const files: string[] = await fs.readdirSync(folderFullPath) + if (files.length === 0) return undefined + if (files.includes(JanModelExtension._modelMetadataFileName)) { + return joinPath([ + folderFullPath, + JanModelExtension._modelMetadataFileName, + ]) + } + // continue recursive + for (const file of files) { + const path = await joinPath([folderFullPath, file]) + const fileStats = await fs.fileStat(path) + if (fileStats.isDirectory) { + const result = await this.getModelJsonPath(path) + if (result) return result + } + } + } + private async getModelsMetadata( selector?: (path: string, model: Model) => Promise ): Promise { @@ -438,11 +462,11 @@ export default class JanModelExtension extends ModelExtension { const readJsonPromises = allDirectories.map(async (dirName) => { // filter out directories that don't match the selector // read model.json - const jsonPath = await joinPath([ + const folderFullPath = await joinPath([ JanModelExtension._homeDir, dirName, - JanModelExtension._modelMetadataFileName, ]) + const jsonPath = await this.getModelJsonPath(folderFullPath) if (await fs.existsSync(jsonPath)) { // if we have the model.json file, read it diff --git a/web/containers/Layout/TopPanel/index.tsx b/web/containers/Layout/TopPanel/index.tsx index e2966e4dab..6dd9ba8a5d 100644 --- a/web/containers/Layout/TopPanel/index.tsx +++ b/web/containers/Layout/TopPanel/index.tsx @@ -9,6 +9,7 @@ import { PanelRightCloseIcon, MinusIcon, MenuIcon, + SquareIcon, PaletteIcon, XIcon, } from 'lucide-react' @@ -51,7 +52,7 @@ const TopPanel = () => { - {isWindows && ( + {!isMac && (
+ diff --git a/web/containers/Loader/ProgressCircle.tsx b/web/containers/Loader/ProgressCircle.tsx new file mode 100644 index 0000000000..e104341136 --- /dev/null +++ b/web/containers/Loader/ProgressCircle.tsx @@ -0,0 +1,50 @@ +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/ModelDropdown/index.tsx b/web/containers/ModelDropdown/index.tsx index a57525698f..b0fed7e66f 100644 --- a/web/containers/ModelDropdown/index.tsx +++ b/web/containers/ModelDropdown/index.tsx @@ -8,16 +8,19 @@ import { useAtom, useAtomValue, useSetAtom } from 'jotai' import { ChevronDownIcon, 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 useDownloadModel from '@/hooks/useDownloadModel' +import { modelDownloadStateAtom } from '@/hooks/useDownloadState' import useRecommendedModel from '@/hooks/useRecommendedModel' import useUpdateModelParameters from '@/hooks/useUpdateModelParameters' -import { toGibibytes } from '@/utils/converter' +import { formatDownloadPercentage, toGibibytes } from '@/utils/converter' import { extensionManager } from '@/extension' @@ -64,6 +67,7 @@ const ModelDropdown = ({ const [dropdownOptions, setDropdownOptions] = useState( null ) + const downloadStates = useAtomValue(modelDownloadStateAtom) const setThreadModelParams = useSetAtom(setThreadModelParamsAtom) const { updateModelParameter } = useUpdateModelParameters() @@ -277,8 +281,8 @@ const ModelDropdown = ({ className="h-6 gap-1 px-2" options={[ { name: 'All', value: 'all' }, - { name: 'Local', value: 'local' }, - { name: 'Remote', value: 'remote' }, + { name: 'On-device', value: 'local' }, + { name: 'Cloud', value: 'remote' }, ]} onValueChange={(value) => setSearchFilter(value)} onOpenChange={(open) => setFilterOptionsOpen(open)} @@ -351,12 +355,29 @@ const ModelDropdown = ({ {toGibibytes(model.metadata.size)} - {!isDownloading && ( + {!isDownloading ? ( downloadModel(model)} /> + ) : ( + Object.values(downloadStates) + .filter((x) => x.modelId === model.id) + .map((item) => ( + + )) )}
@@ -397,12 +418,29 @@ const ModelDropdown = ({ {toGibibytes(model.metadata.size)} - {!isDownloading && ( + {!isDownloading ? ( downloadModel(model)} /> + ) : ( + Object.values(downloadStates) + .filter((x) => x.modelId === model.id) + .map((item) => ( + + )) )} diff --git a/web/containers/ModelLabel/NotEnoughMemoryLabel.tsx b/web/containers/ModelLabel/NotEnoughMemoryLabel.tsx index fb2b7bde5c..287193183c 100644 --- a/web/containers/ModelLabel/NotEnoughMemoryLabel.tsx +++ b/web/containers/ModelLabel/NotEnoughMemoryLabel.tsx @@ -1,32 +1,49 @@ -import { memo } from 'react' +import { Fragment, memo } from 'react' import { Badge, Tooltip } from '@janhq/joi' -import { InfoIcon } from 'lucide-react' -import { twMerge } from 'tailwind-merge' +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 && Not enough {unit}} - - ) : ( - - ) - } - content="This tag signals insufficient RAM for optimal model performance. It's dynamic and may change with your system's RAM availability." - /> - + <> + {compact ? ( +
+ + } + content={ + + Not enough RAM: {tooltipContent} + + } + /> +
+ ) : ( + + Not enough {unit} + + } + content={ + + Not enough RAM: {tooltipContent} + + } + /> + + )} + ) export default memo(NotEnoughMemoryLabel) diff --git a/web/containers/ModelLabel/SlowOnYourDeviceLabel.tsx b/web/containers/ModelLabel/SlowOnYourDeviceLabel.tsx index d89fd25050..e8e9bcb4d6 100644 --- a/web/containers/ModelLabel/SlowOnYourDeviceLabel.tsx +++ b/web/containers/ModelLabel/SlowOnYourDeviceLabel.tsx @@ -1,32 +1,49 @@ -import { memo } from 'react' +import { Fragment, memo } from 'react' import { Badge, Tooltip } from '@janhq/joi' -import { InfoIcon } from 'lucide-react' -import { twMerge } from 'tailwind-merge' +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 && Slow on your device} - - ) : ( - - ) - } - content="This tag indicates that your current RAM performance may affect model speed. It can change based on other active apps. To improve, consider closing unnecessary applications to free up RAM." - /> - + <> + {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/Providers/KeyListener.tsx b/web/containers/Providers/KeyListener.tsx index 36d5130169..2731846df2 100644 --- a/web/containers/Providers/KeyListener.tsx +++ b/web/containers/Providers/KeyListener.tsx @@ -8,7 +8,11 @@ import { MainViewState } from '@/constants/screens' import { useCreateNewThread } from '@/hooks/useCreateNewThread' -import { mainViewStateAtom, showLeftPanelAtom } from '@/helpers/atoms/App.atom' +import { + mainViewStateAtom, + showLeftPanelAtom, + showRightPanelAtom, +} from '@/helpers/atoms/App.atom' import { assistantsAtom } from '@/helpers/atoms/Assistant.atom' type Props = { @@ -17,6 +21,7 @@ type Props = { export default function KeyListener({ children }: Props) { const setShowLeftPanel = useSetAtom(showLeftPanelAtom) + const setShowRightPanel = useSetAtom(showRightPanelAtom) const setMainViewState = useSetAtom(mainViewStateAtom) const { requestCreateNewThread } = useCreateNewThread() const assistants = useAtomValue(assistantsAtom) @@ -25,6 +30,11 @@ export default function KeyListener({ children }: Props) { const onKeyDown = (e: KeyboardEvent) => { const prefixKey = isMac ? e.metaKey : e.ctrlKey + if (e.key === 'b' && prefixKey && e.shiftKey) { + setShowRightPanel((showRightideBar) => !showRightideBar) + return + } + if (e.key === 'n' && prefixKey) { requestCreateNewThread(assistants[0]) setMainViewState(MainViewState.Thread) @@ -43,7 +53,13 @@ export default function KeyListener({ children }: Props) { } document.addEventListener('keydown', onKeyDown) return () => document.removeEventListener('keydown', onKeyDown) - }, [assistants, requestCreateNewThread, setMainViewState, setShowLeftPanel]) + }, [ + assistants, + requestCreateNewThread, + setMainViewState, + setShowLeftPanel, + setShowRightPanel, + ]) return {children} } diff --git a/web/hooks/useLoadTheme.ts b/web/hooks/useLoadTheme.ts index dfa8d263de..8afba27c46 100644 --- a/web/hooks/useLoadTheme.ts +++ b/web/hooks/useLoadTheme.ts @@ -29,11 +29,11 @@ export const useLoadTheme = async () => { const setNativeTheme = useCallback( (nativeTheme: NativeThemeProps) => { if (nativeTheme === 'dark') { - window?.electronAPI.setNativeThemeDark() + window?.electronAPI?.setNativeThemeDark() setTheme('dark') localStorage.setItem('nativeTheme', 'dark') } else { - window?.electronAPI.setNativeThemeLight() + window?.electronAPI?.setNativeThemeLight() setTheme('light') localStorage.setItem('nativeTheme', 'light') } diff --git a/web/hooks/useSendChatMessage.ts b/web/hooks/useSendChatMessage.ts index 59282aa9f2..8c6013505b 100644 --- a/web/hooks/useSendChatMessage.ts +++ b/web/hooks/useSendChatMessage.ts @@ -10,6 +10,7 @@ import { ConversationalExtension, EngineManager, ToolManager, + ChatCompletionMessage, } from '@janhq/core' import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai' @@ -19,6 +20,7 @@ import { fileUploadAtom, } from '@/containers/Providers/Jotai' +import { Stack } from '@/utils/Stack' import { compressImage, getBase64 } from '@/utils/base64' import { MessageRequestBuilder } from '@/utils/messageRequestBuilder' import { toRuntimeParams, toSettingParams } from '@/utils/modelParam' @@ -90,6 +92,33 @@ export default function useSendChatMessage() { selectedModelRef.current = selectedModel }, [selectedModel]) + const normalizeMessages = ( + messages: ChatCompletionMessage[] + ): ChatCompletionMessage[] => { + const stack = new Stack() + for (const message of messages) { + if (stack.isEmpty()) { + stack.push(message) + continue + } + const topMessage = stack.peek() + + if (message.role === topMessage.role) { + // add an empty message + stack.push({ + role: + topMessage.role === ChatCompletionRole.User + ? ChatCompletionRole.Assistant + : ChatCompletionRole.User, + content: '.', // some model requires not empty message + }) + } + stack.push(message) + } + + return stack.reverseOutput() + } + const resendChatMessage = async (currentMessage: ThreadMessage) => { if (!activeThreadRef.current) { console.error('No active thread') @@ -140,6 +169,8 @@ export default function useSendChatMessage() { ) ?? [] ) + request.messages = normalizeMessages(request.messages ?? []) + const engine = requestBuilder.model?.engine ?? selectedModelRef.current?.engine ?? '' @@ -258,6 +289,7 @@ export default function useSendChatMessage() { (assistant) => assistant.tools ?? [] ) ?? [] ) + request.messages = normalizeMessages(request.messages ?? []) // Request for inference EngineManager.instance() diff --git a/web/screens/Settings/Hotkeys/index.tsx b/web/screens/Settings/Hotkeys/index.tsx index 6434ba295d..382efad2e5 100644 --- a/web/screens/Settings/Hotkeys/index.tsx +++ b/web/screens/Settings/Hotkeys/index.tsx @@ -11,6 +11,11 @@ const availableHotkeys = [ modifierKeys: [isMac ? '⌘' : 'Ctrl'], description: 'Toggle collapsible left panel', }, + { + combination: 'Shift B', + modifierKeys: [isMac ? '⌘' : 'Ctrl'], + description: 'Toggle collapsible right panel', + }, { combination: ',', modifierKeys: [isMac ? '⌘' : 'Ctrl'], @@ -21,7 +26,7 @@ const availableHotkeys = [ description: 'Send a message', }, { - combination: 'Shift + Enter', + combination: 'Shift Enter', description: 'Insert new line in input box', }, { diff --git a/web/screens/Thread/ThreadCenterPanel/SimpleTextMessage/index.tsx b/web/screens/Thread/ThreadCenterPanel/SimpleTextMessage/index.tsx index 509610bd5b..43d2f02c9e 100644 --- a/web/screens/Thread/ThreadCenterPanel/SimpleTextMessage/index.tsx +++ b/web/screens/Thread/ThreadCenterPanel/SimpleTextMessage/index.tsx @@ -268,7 +268,7 @@ const SimpleTextMessage: React.FC = (props) => { ) : (
@@ -279,7 +279,7 @@ const SimpleTextMessage: React.FC = (props) => { ) : (
{ + private array: T[] = [] + + pop(): T | undefined { + if (this.isEmpty()) throw new Error() + + return this.array.pop() + } + + push(data: T): void { + this.array.push(data) + } + + peek(): T { + if (this.isEmpty()) throw new Error() + + return this.array[this.array.length - 1] + } + + isEmpty(): boolean { + return this.array.length === 0 + } + + size(): number { + return this.array.length + } + + reverseOutput(): T[] { + return [...this.array] + } +}