commit
25d92c3677
37
.github/ISSUE_TEMPLATE/bug_report.md
vendored
37
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -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
82
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal 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
7
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal 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!"
|
||||
16
.github/ISSUE_TEMPLATE/discussion-thread.md
vendored
16
.github/ISSUE_TEMPLATE/discussion-thread.md
vendored
@ -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**
|
||||
@ -1,5 +1,5 @@
|
||||
---
|
||||
name: Documentation request
|
||||
name: "📖 Documentation request"
|
||||
about: Documentation requests
|
||||
title: 'docs: TITLE'
|
||||
labels: 'type: documentation'
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/epic-request.md
vendored
2
.github/ISSUE_TEMPLATE/epic-request.md
vendored
@ -1,5 +1,5 @@
|
||||
---
|
||||
name: Epic request
|
||||
name: "💥 Epic request"
|
||||
about: Suggest an idea for this project
|
||||
title: 'epic: [DESCRIPTION]'
|
||||
labels: 'type: epic'
|
||||
|
||||
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -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.
|
||||
44
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
44
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal 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"
|
||||
101
.github/workflows/jan-openai-api-test.yml
vendored
101
.github/workflows/jan-openai-api-test.yml
vendored
@ -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
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
127
.github/workflows/nightly-integrate-cortex-cpp.yml
vendored
Normal file
127
.github/workflows/nightly-integrate-cortex-cpp.yml
vendored
Normal 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
|
||||
2
Makefile
2
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
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -1 +1 @@
|
||||
0.4.11
|
||||
0.4.13
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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),
|
||||
|
||||
79
extensions/inference-nvidia-extension/README.md
Normal file
79
extensions/inference-nvidia-extension/README.md
Normal 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!
|
||||
43
extensions/inference-nvidia-extension/package.json
Normal file
43
extensions/inference-nvidia-extension/package.json
Normal 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"
|
||||
]
|
||||
}
|
||||
31
extensions/inference-nvidia-extension/resources/models.json
Normal file
31
extensions/inference-nvidia-extension/resources/models.json
Normal 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"
|
||||
}
|
||||
]
|
||||
@ -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"]
|
||||
}
|
||||
}
|
||||
]
|
||||
66
extensions/inference-nvidia-extension/src/index.ts
Normal file
66
extensions/inference-nvidia-extension/src/index.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
14
extensions/inference-nvidia-extension/tsconfig.json
Normal file
14
extensions/inference-nvidia-extension/tsconfig.json
Normal 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"]
|
||||
}
|
||||
42
extensions/inference-nvidia-extension/webpack.config.js
Normal file
42
extensions/inference-nvidia-extension/webpack.config.js
Normal 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
|
||||
}
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
50
web/containers/Loader/ProgressCircle.tsx
Normal file
50
web/containers/Loader/ProgressCircle.tsx
Normal 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
|
||||
@ -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>
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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>
|
||||
}
|
||||
|
||||
@ -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')
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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',
|
||||
},
|
||||
{
|
||||
|
||||
@ -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
31
web/utils/Stack.ts
Normal 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]
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user