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
|
about: Documentation requests
|
||||||
title: 'docs: TITLE'
|
title: 'docs: TITLE'
|
||||||
labels: 'type: documentation'
|
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
|
about: Suggest an idea for this project
|
||||||
title: 'epic: [DESCRIPTION]'
|
title: 'epic: [DESCRIPTION]'
|
||||||
labels: 'type: epic'
|
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'
|
- 'README.md'
|
||||||
- 'docs/**'
|
- 'docs/**'
|
||||||
schedule:
|
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:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
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)
|
ifeq ($(OS),Windows_NT)
|
||||||
yarn config set network-timeout 300000
|
yarn config set network-timeout 300000
|
||||||
endif
|
endif
|
||||||
yarn global add turbo
|
yarn global add turbo@1.13.2
|
||||||
yarn build:core
|
yarn build:core
|
||||||
yarn build:server
|
yarn build:server
|
||||||
yarn install
|
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 { join } from 'path'
|
||||||
import { windowManager } from '../managers/window'
|
import { windowManager } from '../managers/window'
|
||||||
import {
|
import {
|
||||||
@ -41,12 +41,9 @@ export function handleAppIPCs() {
|
|||||||
windowManager.mainWindow?.minimize()
|
windowManager.mainWindow?.minimize()
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle(NativeRoute.setMaximizeApp, async () => {
|
ipcMain.handle(NativeRoute.setMaximizeApp, async (_event) => {
|
||||||
if (windowManager.mainWindow?.isMaximized()) {
|
if (windowManager.mainWindow?.isMaximized()) {
|
||||||
// const bounds = await getBounds()
|
windowManager.mainWindow.unmaximize()
|
||||||
// windowManager.mainWindow?.setSize(bounds.width, bounds.height)
|
|
||||||
// windowManager.mainWindow?.setPosition(Number(bounds.x), Number(bounds.y))
|
|
||||||
windowManager.mainWindow.restore()
|
|
||||||
} else {
|
} else {
|
||||||
windowManager.mainWindow?.maximize()
|
windowManager.mainWindow?.maximize()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,11 +4,12 @@ export const mainWindowConfig: Electron.BrowserWindowConstructorOptions = {
|
|||||||
skipTaskbar: false,
|
skipTaskbar: false,
|
||||||
minWidth: DEFAULT_MIN_WIDTH,
|
minWidth: DEFAULT_MIN_WIDTH,
|
||||||
show: true,
|
show: true,
|
||||||
|
transparent: true,
|
||||||
|
frame: false,
|
||||||
titleBarStyle: 'hidden',
|
titleBarStyle: 'hidden',
|
||||||
vibrancy: 'fullscreen-ui',
|
vibrancy: 'fullscreen-ui',
|
||||||
visualEffectState: 'active',
|
visualEffectState: 'active',
|
||||||
backgroundMaterial: 'acrylic',
|
backgroundMaterial: 'acrylic',
|
||||||
maximizable: false,
|
|
||||||
autoHideMenuBar: true,
|
autoHideMenuBar: true,
|
||||||
trafficLightPosition: {
|
trafficLightPosition: {
|
||||||
x: 16,
|
x: 16,
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
0.4.11
|
0.4.13
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@janhq/inference-cortex-extension",
|
"name": "@janhq/inference-cortex-extension",
|
||||||
"productName": "Cortex Inference Engine",
|
"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.",
|
"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",
|
"main": "dist/index.js",
|
||||||
"node": "dist/node/index.cjs.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 aya35bJson = require('./resources/models/aya-23-35b/model.json')
|
||||||
const phimediumJson = require('./resources/models/phi3-medium/model.json')
|
const phimediumJson = require('./resources/models/phi3-medium/model.json')
|
||||||
const codestralJson = require('./resources/models/codestral-22b/model.json')
|
const codestralJson = require('./resources/models/codestral-22b/model.json')
|
||||||
|
const qwen2Json = require('./resources/models/qwen2-7b/model.json')
|
||||||
|
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
@ -84,7 +86,8 @@ export default [
|
|||||||
phimediumJson,
|
phimediumJson,
|
||||||
aya8bJson,
|
aya8bJson,
|
||||||
aya35bJson,
|
aya35bJson,
|
||||||
codestralJson
|
codestralJson,
|
||||||
|
qwen2Json
|
||||||
]),
|
]),
|
||||||
NODE: JSON.stringify(`${packageJson.name}/${packageJson.node}`),
|
NODE: JSON.stringify(`${packageJson.name}/${packageJson.node}`),
|
||||||
DEFAULT_SETTINGS: JSON.stringify(defaultSettingJson),
|
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",
|
"name": "@janhq/model-extension",
|
||||||
"productName": "Model Management",
|
"productName": "Model Management",
|
||||||
"version": "1.0.31",
|
"version": "1.0.32",
|
||||||
"description": "Model Management Extension provides model exploration and seamless downloads",
|
"description": "Model Management Extension provides model exploration and seamless downloads",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"node": "dist/node/index.cjs.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(
|
private async getModelsMetadata(
|
||||||
selector?: (path: string, model: Model) => Promise<boolean>
|
selector?: (path: string, model: Model) => Promise<boolean>
|
||||||
): Promise<Model[]> {
|
): Promise<Model[]> {
|
||||||
@ -438,11 +462,11 @@ export default class JanModelExtension extends ModelExtension {
|
|||||||
const readJsonPromises = allDirectories.map(async (dirName) => {
|
const readJsonPromises = allDirectories.map(async (dirName) => {
|
||||||
// filter out directories that don't match the selector
|
// filter out directories that don't match the selector
|
||||||
// read model.json
|
// read model.json
|
||||||
const jsonPath = await joinPath([
|
const folderFullPath = await joinPath([
|
||||||
JanModelExtension._homeDir,
|
JanModelExtension._homeDir,
|
||||||
dirName,
|
dirName,
|
||||||
JanModelExtension._modelMetadataFileName,
|
|
||||||
])
|
])
|
||||||
|
const jsonPath = await this.getModelJsonPath(folderFullPath)
|
||||||
|
|
||||||
if (await fs.existsSync(jsonPath)) {
|
if (await fs.existsSync(jsonPath)) {
|
||||||
// if we have the model.json file, read it
|
// if we have the model.json file, read it
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import {
|
|||||||
PanelRightCloseIcon,
|
PanelRightCloseIcon,
|
||||||
MinusIcon,
|
MinusIcon,
|
||||||
MenuIcon,
|
MenuIcon,
|
||||||
|
SquareIcon,
|
||||||
PaletteIcon,
|
PaletteIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
@ -51,7 +52,7 @@ const TopPanel = () => {
|
|||||||
<Button
|
<Button
|
||||||
theme="icon"
|
theme="icon"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
window?.electronAPI.showOpenMenu(100, 100)
|
window?.electronAPI?.showOpenMenu(100, 100)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MenuIcon size={16} />
|
<MenuIcon size={16} />
|
||||||
@ -96,17 +97,23 @@ const TopPanel = () => {
|
|||||||
<PaletteIcon size={16} className="cursor-pointer" />
|
<PaletteIcon size={16} className="cursor-pointer" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{isWindows && (
|
{!isMac && (
|
||||||
<div className="flex items-center gap-x-2">
|
<div className="flex items-center gap-x-2">
|
||||||
<Button
|
<Button
|
||||||
theme="icon"
|
theme="icon"
|
||||||
onClick={() => window?.electronAPI.setMinimizeApp()}
|
onClick={() => window?.electronAPI?.setMinimizeApp()}
|
||||||
>
|
>
|
||||||
<MinusIcon size={16} />
|
<MinusIcon size={16} />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
theme="icon"
|
theme="icon"
|
||||||
onClick={() => window?.electronAPI.setCloseApp()}
|
onClick={() => window?.electronAPI?.setMaximizeApp()}
|
||||||
|
>
|
||||||
|
<SquareIcon size={14} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
theme="icon"
|
||||||
|
onClick={() => window?.electronAPI?.setCloseApp()}
|
||||||
>
|
>
|
||||||
<XIcon size={16} />
|
<XIcon size={16} />
|
||||||
</Button>
|
</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 { ChevronDownIcon, DownloadCloudIcon, XIcon } from 'lucide-react'
|
||||||
import { twMerge } from 'tailwind-merge'
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
|
import ProgressCircle from '@/containers/Loader/ProgressCircle'
|
||||||
|
|
||||||
import ModelLabel from '@/containers/ModelLabel'
|
import ModelLabel from '@/containers/ModelLabel'
|
||||||
|
|
||||||
import SetupRemoteModel from '@/containers/SetupRemoteModel'
|
import SetupRemoteModel from '@/containers/SetupRemoteModel'
|
||||||
|
|
||||||
import useDownloadModel from '@/hooks/useDownloadModel'
|
import useDownloadModel from '@/hooks/useDownloadModel'
|
||||||
|
import { modelDownloadStateAtom } from '@/hooks/useDownloadState'
|
||||||
import useRecommendedModel from '@/hooks/useRecommendedModel'
|
import useRecommendedModel from '@/hooks/useRecommendedModel'
|
||||||
|
|
||||||
import useUpdateModelParameters from '@/hooks/useUpdateModelParameters'
|
import useUpdateModelParameters from '@/hooks/useUpdateModelParameters'
|
||||||
|
|
||||||
import { toGibibytes } from '@/utils/converter'
|
import { formatDownloadPercentage, toGibibytes } from '@/utils/converter'
|
||||||
|
|
||||||
import { extensionManager } from '@/extension'
|
import { extensionManager } from '@/extension'
|
||||||
|
|
||||||
@ -64,6 +67,7 @@ const ModelDropdown = ({
|
|||||||
const [dropdownOptions, setDropdownOptions] = useState<HTMLDivElement | null>(
|
const [dropdownOptions, setDropdownOptions] = useState<HTMLDivElement | null>(
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
|
const downloadStates = useAtomValue(modelDownloadStateAtom)
|
||||||
const setThreadModelParams = useSetAtom(setThreadModelParamsAtom)
|
const setThreadModelParams = useSetAtom(setThreadModelParamsAtom)
|
||||||
const { updateModelParameter } = useUpdateModelParameters()
|
const { updateModelParameter } = useUpdateModelParameters()
|
||||||
|
|
||||||
@ -277,8 +281,8 @@ const ModelDropdown = ({
|
|||||||
className="h-6 gap-1 px-2"
|
className="h-6 gap-1 px-2"
|
||||||
options={[
|
options={[
|
||||||
{ name: 'All', value: 'all' },
|
{ name: 'All', value: 'all' },
|
||||||
{ name: 'Local', value: 'local' },
|
{ name: 'On-device', value: 'local' },
|
||||||
{ name: 'Remote', value: 'remote' },
|
{ name: 'Cloud', value: 'remote' },
|
||||||
]}
|
]}
|
||||||
onValueChange={(value) => setSearchFilter(value)}
|
onValueChange={(value) => setSearchFilter(value)}
|
||||||
onOpenChange={(open) => setFilterOptionsOpen(open)}
|
onOpenChange={(open) => setFilterOptionsOpen(open)}
|
||||||
@ -351,12 +355,29 @@ const ModelDropdown = ({
|
|||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{toGibibytes(model.metadata.size)}
|
{toGibibytes(model.metadata.size)}
|
||||||
</span>
|
</span>
|
||||||
{!isDownloading && (
|
{!isDownloading ? (
|
||||||
<DownloadCloudIcon
|
<DownloadCloudIcon
|
||||||
size={18}
|
size={18}
|
||||||
className="cursor-pointer text-[hsla(var(--app-link))]"
|
className="cursor-pointer text-[hsla(var(--app-link))]"
|
||||||
onClick={() => downloadModel(model)}
|
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>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
@ -397,12 +418,29 @@ const ModelDropdown = ({
|
|||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{toGibibytes(model.metadata.size)}
|
{toGibibytes(model.metadata.size)}
|
||||||
</span>
|
</span>
|
||||||
{!isDownloading && (
|
{!isDownloading ? (
|
||||||
<DownloadCloudIcon
|
<DownloadCloudIcon
|
||||||
size={18}
|
size={18}
|
||||||
className="cursor-pointer text-[hsla(var(--app-link))]"
|
className="cursor-pointer text-[hsla(var(--app-link))]"
|
||||||
onClick={() => downloadModel(model)}
|
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>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@ -1,32 +1,49 @@
|
|||||||
import { memo } from 'react'
|
import { Fragment, memo } from 'react'
|
||||||
|
|
||||||
import { Badge, Tooltip } from '@janhq/joi'
|
import { Badge, Tooltip } from '@janhq/joi'
|
||||||
import { InfoIcon } from 'lucide-react'
|
import { AlertTriangleIcon, InfoIcon } from 'lucide-react'
|
||||||
import { twMerge } from 'tailwind-merge'
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
compact?: boolean
|
compact?: boolean
|
||||||
unit: string
|
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) => (
|
const NotEnoughMemoryLabel = ({ unit, compact }: Props) => (
|
||||||
<Badge
|
<>
|
||||||
theme="destructive"
|
{compact ? (
|
||||||
variant="soft"
|
<div className="flex h-5 w-5 items-center">
|
||||||
className={twMerge(compact && 'h-5 w-5 p-1')}
|
|
||||||
>
|
|
||||||
{!compact && <span className="line-clamp-1">Not enough {unit}</span>}
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
trigger={
|
trigger={
|
||||||
compact ? (
|
<AlertTriangleIcon
|
||||||
<div className="h-2 w-2 cursor-pointer rounded-full bg-[hsla(var(--destructive-bg))]" />
|
size={14}
|
||||||
) : (
|
className="cursor-pointer text-[hsla(var(--destructive-bg))]"
|
||||||
<InfoIcon size={14} className="ml-2 flex-shrink-0 cursor-pointer" />
|
/>
|
||||||
)
|
}
|
||||||
|
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>
|
||||||
}
|
}
|
||||||
content="This tag signals insufficient RAM for optimal model performance. It's dynamic and may change with your system's RAM availability."
|
|
||||||
/>
|
/>
|
||||||
</Badge>
|
</Badge>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
|
|
||||||
export default memo(NotEnoughMemoryLabel)
|
export default memo(NotEnoughMemoryLabel)
|
||||||
|
|||||||
@ -1,32 +1,49 @@
|
|||||||
import { memo } from 'react'
|
import { Fragment, memo } from 'react'
|
||||||
|
|
||||||
import { Badge, Tooltip } from '@janhq/joi'
|
import { Badge, Tooltip } from '@janhq/joi'
|
||||||
|
|
||||||
import { InfoIcon } from 'lucide-react'
|
import { AlertTriangleIcon, InfoIcon } from 'lucide-react'
|
||||||
import { twMerge } from 'tailwind-merge'
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
compact?: boolean
|
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) => (
|
const SlowOnYourDeviceLabel = ({ compact }: Props) => (
|
||||||
<Badge
|
<>
|
||||||
theme="warning"
|
{compact ? (
|
||||||
variant="soft"
|
<div className="flex h-5 w-5 items-center">
|
||||||
className={twMerge(compact && 'h-5 w-5 p-1')}
|
|
||||||
>
|
|
||||||
{!compact && <span className="line-clamp-1">Slow on your device</span>}
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
trigger={
|
trigger={
|
||||||
compact ? (
|
<AlertTriangleIcon
|
||||||
<div className="h-2 w-2 cursor-pointer rounded-full bg-[hsla(var(--warning-bg))] p-0" />
|
size={14}
|
||||||
) : (
|
className="cursor-pointer text-[hsla(var(--warning-bg))]"
|
||||||
<InfoIcon size={14} className="ml-2 flex-shrink-0 cursor-pointer" />
|
/>
|
||||||
)
|
}
|
||||||
|
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>
|
||||||
}
|
}
|
||||||
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>
|
</Badge>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
|
|
||||||
export default memo(SlowOnYourDeviceLabel)
|
export default memo(SlowOnYourDeviceLabel)
|
||||||
|
|||||||
@ -8,7 +8,11 @@ import { MainViewState } from '@/constants/screens'
|
|||||||
|
|
||||||
import { useCreateNewThread } from '@/hooks/useCreateNewThread'
|
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'
|
import { assistantsAtom } from '@/helpers/atoms/Assistant.atom'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -17,6 +21,7 @@ type Props = {
|
|||||||
|
|
||||||
export default function KeyListener({ children }: Props) {
|
export default function KeyListener({ children }: Props) {
|
||||||
const setShowLeftPanel = useSetAtom(showLeftPanelAtom)
|
const setShowLeftPanel = useSetAtom(showLeftPanelAtom)
|
||||||
|
const setShowRightPanel = useSetAtom(showRightPanelAtom)
|
||||||
const setMainViewState = useSetAtom(mainViewStateAtom)
|
const setMainViewState = useSetAtom(mainViewStateAtom)
|
||||||
const { requestCreateNewThread } = useCreateNewThread()
|
const { requestCreateNewThread } = useCreateNewThread()
|
||||||
const assistants = useAtomValue(assistantsAtom)
|
const assistants = useAtomValue(assistantsAtom)
|
||||||
@ -25,6 +30,11 @@ export default function KeyListener({ children }: Props) {
|
|||||||
const onKeyDown = (e: KeyboardEvent) => {
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
const prefixKey = isMac ? e.metaKey : e.ctrlKey
|
const prefixKey = isMac ? e.metaKey : e.ctrlKey
|
||||||
|
|
||||||
|
if (e.key === 'b' && prefixKey && e.shiftKey) {
|
||||||
|
setShowRightPanel((showRightideBar) => !showRightideBar)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (e.key === 'n' && prefixKey) {
|
if (e.key === 'n' && prefixKey) {
|
||||||
requestCreateNewThread(assistants[0])
|
requestCreateNewThread(assistants[0])
|
||||||
setMainViewState(MainViewState.Thread)
|
setMainViewState(MainViewState.Thread)
|
||||||
@ -43,7 +53,13 @@ export default function KeyListener({ children }: Props) {
|
|||||||
}
|
}
|
||||||
document.addEventListener('keydown', onKeyDown)
|
document.addEventListener('keydown', onKeyDown)
|
||||||
return () => document.removeEventListener('keydown', onKeyDown)
|
return () => document.removeEventListener('keydown', onKeyDown)
|
||||||
}, [assistants, requestCreateNewThread, setMainViewState, setShowLeftPanel])
|
}, [
|
||||||
|
assistants,
|
||||||
|
requestCreateNewThread,
|
||||||
|
setMainViewState,
|
||||||
|
setShowLeftPanel,
|
||||||
|
setShowRightPanel,
|
||||||
|
])
|
||||||
|
|
||||||
return <Fragment>{children}</Fragment>
|
return <Fragment>{children}</Fragment>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,11 +29,11 @@ export const useLoadTheme = async () => {
|
|||||||
const setNativeTheme = useCallback(
|
const setNativeTheme = useCallback(
|
||||||
(nativeTheme: NativeThemeProps) => {
|
(nativeTheme: NativeThemeProps) => {
|
||||||
if (nativeTheme === 'dark') {
|
if (nativeTheme === 'dark') {
|
||||||
window?.electronAPI.setNativeThemeDark()
|
window?.electronAPI?.setNativeThemeDark()
|
||||||
setTheme('dark')
|
setTheme('dark')
|
||||||
localStorage.setItem('nativeTheme', 'dark')
|
localStorage.setItem('nativeTheme', 'dark')
|
||||||
} else {
|
} else {
|
||||||
window?.electronAPI.setNativeThemeLight()
|
window?.electronAPI?.setNativeThemeLight()
|
||||||
setTheme('light')
|
setTheme('light')
|
||||||
localStorage.setItem('nativeTheme', 'light')
|
localStorage.setItem('nativeTheme', 'light')
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import {
|
|||||||
ConversationalExtension,
|
ConversationalExtension,
|
||||||
EngineManager,
|
EngineManager,
|
||||||
ToolManager,
|
ToolManager,
|
||||||
|
ChatCompletionMessage,
|
||||||
} from '@janhq/core'
|
} from '@janhq/core'
|
||||||
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'
|
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||||
|
|
||||||
@ -19,6 +20,7 @@ import {
|
|||||||
fileUploadAtom,
|
fileUploadAtom,
|
||||||
} from '@/containers/Providers/Jotai'
|
} from '@/containers/Providers/Jotai'
|
||||||
|
|
||||||
|
import { Stack } from '@/utils/Stack'
|
||||||
import { compressImage, getBase64 } from '@/utils/base64'
|
import { compressImage, getBase64 } from '@/utils/base64'
|
||||||
import { MessageRequestBuilder } from '@/utils/messageRequestBuilder'
|
import { MessageRequestBuilder } from '@/utils/messageRequestBuilder'
|
||||||
import { toRuntimeParams, toSettingParams } from '@/utils/modelParam'
|
import { toRuntimeParams, toSettingParams } from '@/utils/modelParam'
|
||||||
@ -90,6 +92,33 @@ export default function useSendChatMessage() {
|
|||||||
selectedModelRef.current = selectedModel
|
selectedModelRef.current = selectedModel
|
||||||
}, [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) => {
|
const resendChatMessage = async (currentMessage: ThreadMessage) => {
|
||||||
if (!activeThreadRef.current) {
|
if (!activeThreadRef.current) {
|
||||||
console.error('No active thread')
|
console.error('No active thread')
|
||||||
@ -140,6 +169,8 @@ export default function useSendChatMessage() {
|
|||||||
) ?? []
|
) ?? []
|
||||||
)
|
)
|
||||||
|
|
||||||
|
request.messages = normalizeMessages(request.messages ?? [])
|
||||||
|
|
||||||
const engine =
|
const engine =
|
||||||
requestBuilder.model?.engine ?? selectedModelRef.current?.engine ?? ''
|
requestBuilder.model?.engine ?? selectedModelRef.current?.engine ?? ''
|
||||||
|
|
||||||
@ -258,6 +289,7 @@ export default function useSendChatMessage() {
|
|||||||
(assistant) => assistant.tools ?? []
|
(assistant) => assistant.tools ?? []
|
||||||
) ?? []
|
) ?? []
|
||||||
)
|
)
|
||||||
|
request.messages = normalizeMessages(request.messages ?? [])
|
||||||
|
|
||||||
// Request for inference
|
// Request for inference
|
||||||
EngineManager.instance()
|
EngineManager.instance()
|
||||||
|
|||||||
@ -11,6 +11,11 @@ const availableHotkeys = [
|
|||||||
modifierKeys: [isMac ? '⌘' : 'Ctrl'],
|
modifierKeys: [isMac ? '⌘' : 'Ctrl'],
|
||||||
description: 'Toggle collapsible left panel',
|
description: 'Toggle collapsible left panel',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
combination: 'Shift B',
|
||||||
|
modifierKeys: [isMac ? '⌘' : 'Ctrl'],
|
||||||
|
description: 'Toggle collapsible right panel',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
combination: ',',
|
combination: ',',
|
||||||
modifierKeys: [isMac ? '⌘' : 'Ctrl'],
|
modifierKeys: [isMac ? '⌘' : 'Ctrl'],
|
||||||
@ -21,7 +26,7 @@ const availableHotkeys = [
|
|||||||
description: 'Send a message',
|
description: 'Send a message',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
combination: 'Shift + Enter',
|
combination: 'Shift Enter',
|
||||||
description: 'Insert new line in input box',
|
description: 'Insert new line in input box',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -268,7 +268,7 @@ const SimpleTextMessage: React.FC<ThreadMessage> = (props) => {
|
|||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
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'
|
isUser ? 'whitespace-pre-wrap break-words' : 'p-4'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@ -279,7 +279,7 @@ const SimpleTextMessage: React.FC<ThreadMessage> = (props) => {
|
|||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
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'
|
isUser && 'whitespace-pre-wrap break-words'
|
||||||
)}
|
)}
|
||||||
dangerouslySetInnerHTML={{ __html: parsedText }}
|
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