Merge pull request #3051 from janhq/dev

chore: release 0.5.1 to main
This commit is contained in:
Van Pham 2024-06-17 15:28:38 +07:00 committed by GitHub
commit 25d92c3677
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 906 additions and 244 deletions

View File

@ -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.

82
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@ -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]

7
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -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!"

View File

@ -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**

View File

@ -1,5 +1,5 @@
---
name: Documentation request
name: "📖 Documentation request"
about: Documentation requests
title: 'docs: TITLE'
labels: 'type: documentation'

View File

@ -1,5 +1,5 @@
---
name: Epic request
name: "💥 Epic request"
about: Suggest an idea for this project
title: 'epic: [DESCRIPTION]'
labels: 'type: epic'

View File

@ -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.

View File

@ -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"

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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()
}

View File

@ -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,

View File

@ -1 +1 @@
0.4.11
0.4.13

View File

@ -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",

View File

@ -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"
}

View File

@ -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),

View File

@ -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<any>`.
```typescript
import { events, MessageEvent, MessageRequest } from '@janhq/core'
function onStart(): Promise<any> {
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!

View File

@ -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 <service@jan.ai>",
"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"
]
}

View File

@ -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"
}
]

View File

@ -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"]
}
}
]

View File

@ -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<any>
declare const MODELS: Array<any>
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<void> {
super.onLoad()
// Register Settings
this.registerSettings(SETTINGS)
this.registerModels(MODELS)
this.apiKey = await this.getSetting<string>(Settings.apiKey, '')
this.inferenceUrl = await this.getSetting<string>(
Settings.chatCompletionsEndPoint,
''
)
if (this.inferenceUrl.length === 0) {
SETTINGS.forEach((setting) => {
if (setting.key === Settings.chatCompletionsEndPoint) {
this.inferenceUrl = setting.controllerProps.value as string
}
})
}
}
onSettingUpdate<T>(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
}
}
}
}

View File

@ -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"]
}

View File

@ -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
}

View File

@ -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",

View File

@ -417,6 +417,30 @@ export default class JanModelExtension extends ModelExtension {
)
}
private async getModelJsonPath(
folderFullPath: string
): Promise<string | undefined> {
// 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<boolean>
): Promise<Model[]> {
@ -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

View File

@ -9,6 +9,7 @@ import {
PanelRightCloseIcon,
MinusIcon,
MenuIcon,
SquareIcon,
PaletteIcon,
XIcon,
} from 'lucide-react'
@ -51,7 +52,7 @@ const TopPanel = () => {
<Button
theme="icon"
onClick={() => {
window?.electronAPI.showOpenMenu(100, 100)
window?.electronAPI?.showOpenMenu(100, 100)
}}
>
<MenuIcon size={16} />
@ -96,17 +97,23 @@ const TopPanel = () => {
<PaletteIcon size={16} className="cursor-pointer" />
</Button>
{isWindows && (
{!isMac && (
<div className="flex items-center gap-x-2">
<Button
theme="icon"
onClick={() => window?.electronAPI.setMinimizeApp()}
onClick={() => window?.electronAPI?.setMinimizeApp()}
>
<MinusIcon size={16} />
</Button>
<Button
theme="icon"
onClick={() => window?.electronAPI.setCloseApp()}
onClick={() => window?.electronAPI?.setMaximizeApp()}
>
<SquareIcon size={14} />
</Button>
<Button
theme="icon"
onClick={() => window?.electronAPI?.setCloseApp()}
>
<XIcon size={16} />
</Button>

View File

@ -0,0 +1,50 @@
import React from 'react'
interface ProgressCircleProps {
percentage: number
size?: number
strokeWidth?: number
}
const ProgressCircle: React.FC<ProgressCircleProps> = ({
percentage,
size = 100,
strokeWidth = 14,
}) => {
const radius = (size - strokeWidth) / 2
const circumference = 2 * Math.PI * radius
const offset = circumference - (percentage / 100) * circumference
return (
<svg
className="ml-0.5 h-4 w-4 rotate-[-90deg] transform text-[hsla(var(--primary-bg))]"
height={size}
width={size}
xmlns="http://www.w3.org/2000/svg"
viewBox={`0 0 ${size} ${size}`}
>
<circle
className="opacity-25"
cx={size / 2}
cy={size / 2}
r={radius}
stroke="currentColor"
strokeWidth={strokeWidth}
fill="none"
></circle>
<circle
className="transition-stroke-dashoffset duration-300"
cx={size / 2}
cy={size / 2}
r={radius}
stroke="currentColor"
strokeWidth={strokeWidth}
fill="none"
strokeDasharray={circumference}
strokeDashoffset={offset}
></circle>
</svg>
)
}
export default ProgressCircle

View File

@ -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<HTMLDivElement | null>(
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 = ({
<span className="font-medium">
{toGibibytes(model.metadata.size)}
</span>
{!isDownloading && (
{!isDownloading ? (
<DownloadCloudIcon
size={18}
className="cursor-pointer text-[hsla(var(--app-link))]"
onClick={() => downloadModel(model)}
/>
) : (
Object.values(downloadStates)
.filter((x) => x.modelId === model.id)
.map((item) => (
<ProgressCircle
key={item.modelId}
percentage={
formatDownloadPercentage(
item?.percent,
{
hidePercentage: true,
}
) as number
}
size={100}
/>
))
)}
</div>
</li>
@ -397,12 +418,29 @@ const ModelDropdown = ({
<span className="font-medium">
{toGibibytes(model.metadata.size)}
</span>
{!isDownloading && (
{!isDownloading ? (
<DownloadCloudIcon
size={18}
className="cursor-pointer text-[hsla(var(--app-link))]"
onClick={() => downloadModel(model)}
/>
) : (
Object.values(downloadStates)
.filter((x) => x.modelId === model.id)
.map((item) => (
<ProgressCircle
key={item.modelId}
percentage={
formatDownloadPercentage(
item?.percent,
{
hidePercentage: true,
}
) as number
}
size={100}
/>
))
)}
</div>
</li>

View File

@ -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) => (
<Badge
theme="destructive"
variant="soft"
className={twMerge(compact && 'h-5 w-5 p-1')}
>
{!compact && <span className="line-clamp-1">Not enough {unit}</span>}
<Tooltip
trigger={
compact ? (
<div className="h-2 w-2 cursor-pointer rounded-full bg-[hsla(var(--destructive-bg))]" />
) : (
<InfoIcon size={14} className="ml-2 flex-shrink-0 cursor-pointer" />
)
}
content="This tag signals insufficient RAM for optimal model performance. It's dynamic and may change with your system's RAM availability."
/>
</Badge>
<>
{compact ? (
<div className="flex h-5 w-5 items-center">
<Tooltip
trigger={
<AlertTriangleIcon
size={14}
className="cursor-pointer text-[hsla(var(--destructive-bg))]"
/>
}
content={
<Fragment>
<b>Not enough RAM:</b> <span>{tooltipContent}</span>
</Fragment>
}
/>
</div>
) : (
<Badge theme="destructive" variant="soft">
<span className="line-clamp-1">Not enough {unit}</span>
<Tooltip
trigger={
<InfoIcon size={14} className="ml-2 flex-shrink-0 cursor-pointer" />
}
content={
<Fragment>
<b>Not enough RAM:</b> <span>{tooltipContent}</span>
</Fragment>
}
/>
</Badge>
)}
</>
)
export default memo(NotEnoughMemoryLabel)

View File

@ -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) => (
<Badge
theme="warning"
variant="soft"
className={twMerge(compact && 'h-5 w-5 p-1')}
>
{!compact && <span className="line-clamp-1">Slow on your device</span>}
<Tooltip
trigger={
compact ? (
<div className="h-2 w-2 cursor-pointer rounded-full bg-[hsla(var(--warning-bg))] p-0" />
) : (
<InfoIcon size={14} className="ml-2 flex-shrink-0 cursor-pointer" />
)
}
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."
/>
</Badge>
<>
{compact ? (
<div className="flex h-5 w-5 items-center">
<Tooltip
trigger={
<AlertTriangleIcon
size={14}
className="cursor-pointer text-[hsla(var(--warning-bg))]"
/>
}
content={
<Fragment>
<b>Slow on your device:</b> <span>{tooltipContent}</span>
</Fragment>
}
/>
</div>
) : (
<Badge theme="warning" variant="soft">
<span className="line-clamp-1">Slow on your device</span>
<Tooltip
trigger={
<InfoIcon size={14} className="ml-2 flex-shrink-0 cursor-pointer" />
}
content={
<Fragment>
<b>Slow on your device:</b> <span>{tooltipContent}</span>
</Fragment>
}
/>
</Badge>
)}
</>
)
export default memo(SlowOnYourDeviceLabel)

View File

@ -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 <Fragment>{children}</Fragment>
}

View File

@ -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')
}

View File

@ -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<ChatCompletionMessage>()
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()

View File

@ -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',
},
{

View File

@ -268,7 +268,7 @@ const SimpleTextMessage: React.FC<ThreadMessage> = (props) => {
) : (
<div
className={twMerge(
'message flex flex-col gap-y-2 font-medium leading-relaxed',
'message flex flex-col gap-y-2 leading-relaxed',
isUser ? 'whitespace-pre-wrap break-words' : 'p-4'
)}
>
@ -279,7 +279,7 @@ const SimpleTextMessage: React.FC<ThreadMessage> = (props) => {
) : (
<div
className={twMerge(
'message max-width-[100%] flex flex-col gap-y-2 overflow-auto font-medium leading-relaxed',
'message max-width-[100%] flex flex-col gap-y-2 overflow-auto leading-relaxed',
isUser && 'whitespace-pre-wrap break-words'
)}
dangerouslySetInnerHTML={{ __html: parsedText }}

31
web/utils/Stack.ts Normal file
View File

@ -0,0 +1,31 @@
export class Stack<T> {
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]
}
}