Merge branch 'main' into docs/add-guides

This commit is contained in:
Ho Duc Hieu 2024-01-04 15:05:43 +07:00
commit 30bb911628
108 changed files with 1875 additions and 712 deletions

13
.github/pull_request_template.md vendored Normal file
View File

@ -0,0 +1,13 @@
## Describe Your Changes
-
## Fixes Issues
-
## Self Checklist
- [ ] Added relevant comments, esp in complex areas
- [ ] Updated docs (for bug fixes / features)
- [ ] Created issues for follow-up changes or refactoring needed

View File

@ -15,6 +15,7 @@ jobs:
uses: unfor19/install-aws-cli-action@v1
- name: Delete cloudflare-r2 folder using awscli s3api
if: github.ref == 'refs/heads/main'
continue-on-error: true
run: |
# Get the list of objects in the 'latest' folder
@ -34,25 +35,18 @@ jobs:
AWS_DEFAULT_REGION: auto
AWS_EC2_METADATA_DISABLED: "true"
build-macos:
runs-on: macos-latest
# Job create Update app version based on latest release tag with build number and save to output
get-update-version:
runs-on: ubuntu-latest
needs: delete-cloudflare-r2-folder
environment: production
permissions:
contents: write
outputs:
new_version: ${{ steps.version_update.outputs.new_version }}
steps:
- name: Getting the repo
uses: actions/checkout@v3
- name: Installing node
uses: actions/setup-node@v1
with:
node-version: 20
- name: Install jq
uses: dcarbone/install-jq-action@v2.0.1
- name: Update app version based on latest release tag with build number
id: version_update
run: |
@ -82,11 +76,35 @@ jobs:
# Remove the 'v' and append the build number to the version
NEW_VERSION="${LATEST_TAG#v}-${GITHUB_RUN_NUMBER}"
echo "New version: $NEW_VERSION"
# Update the version in electron/package.json
jq --arg version "$NEW_VERSION" '.version = $version' electron/package.json > /tmp/package.json
mv /tmp/package.json electron/package.json
echo "::set-output name=new_version::$NEW_VERSION"
build-macos:
runs-on: macos-latest
needs: [delete-cloudflare-r2-folder, get-update-version]
environment: production
permissions:
contents: write
steps:
- name: Getting the repo
uses: actions/checkout@v3
- name: Installing node
uses: actions/setup-node@v1
with:
node-version: 20
- name: Install jq
uses: dcarbone/install-jq-action@v2.0.1
- name: Update app version based on latest release tag with build number
id: version_update
run: |
# Update the version in electron/package.json
jq --arg version "${{ needs.get-update-version.outputs.new_version }}" '.version = $version' electron/package.json > /tmp/package.json
mv /tmp/package.json electron/package.json
jq --arg version "${{ needs.get-update-version.outputs.new_version }}" '.version = $version' web/package.json > /tmp/package.json
mv /tmp/package.json web/package.json
jq '.build.publish = [{"provider": "generic", "url": "${{ secrets.CLOUDFLARE_R2_PUBLIC_URL }}", "channel": "latest"}]' electron/package.json > /tmp/package.json
mv /tmp/package.json electron/package.json
@ -119,26 +137,27 @@ jobs:
- name: Upload Artifact
uses: actions/upload-artifact@v2
with:
name: jan-mac-x64-${{ steps.version_update.outputs.new_version }}
path: ./electron/dist/jan-mac-x64-${{ steps.version_update.outputs.new_version }}.dmg
name: jan-mac-x64-${{ needs.get-update-version.outputs.new_version }}
path: ./electron/dist/jan-mac-x64-${{ needs.get-update-version.outputs.new_version }}.dmg
- name: Upload Artifact
uses: actions/upload-artifact@v2
with:
name: jan-mac-arm64-${{ steps.version_update.outputs.new_version }}
path: ./electron/dist/jan-mac-arm64-${{ steps.version_update.outputs.new_version }}.dmg
name: jan-mac-arm64-${{ needs.get-update-version.outputs.new_version }}
path: ./electron/dist/jan-mac-arm64-${{ needs.get-update-version.outputs.new_version }}.dmg
- name: put-object using awscli s3api
if: github.ref == 'refs/heads/main'
continue-on-error: true
run: |
ls -al ./electron/dist
aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "latest/jan-mac-x64-${{ steps.version_update.outputs.new_version }}.dmg" --body "./electron/dist/jan-mac-x64-${{ steps.version_update.outputs.new_version }}.dmg" --content-type "application/octet-stream"
aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "latest/jan-mac-arm64-${{ steps.version_update.outputs.new_version }}.dmg" --body "./electron/dist/jan-mac-arm64-${{ steps.version_update.outputs.new_version }}.dmg" --content-type "application/octet-stream"
aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "latest/jan-mac-x64-${{ steps.version_update.outputs.new_version }}.zip" --body "./electron/dist/jan-mac-x64-${{ steps.version_update.outputs.new_version }}.zip" --content-type "application/zip"
aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "latest/jan-mac-arm64-${{ steps.version_update.outputs.new_version }}.zip" --body "./electron/dist/jan-mac-arm64-${{ steps.version_update.outputs.new_version }}.zip" --content-type "application/zip"
aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "${{ steps.version_update.outputs.new_version }}/jan-mac-x64-${{ steps.version_update.outputs.new_version }}.dmg" --body "./electron/dist/jan-mac-x64-${{ steps.version_update.outputs.new_version }}.dmg" --content-type "application/octet-stream"
aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "${{ steps.version_update.outputs.new_version }}/jan-mac-arm64-${{ steps.version_update.outputs.new_version }}.dmg" --body "./electron/dist/jan-mac-arm64-${{ steps.version_update.outputs.new_version }}.dmg" --content-type "application/octet-stream"
aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "${{ steps.version_update.outputs.new_version }}/latest-mac.yml" --body "./electron/dist/latest-mac.yml" --content-type "text/yaml"
aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "latest/jan-mac-x64-${{ needs.get-update-version.outputs.new_version }}.dmg" --body "./electron/dist/jan-mac-x64-${{ needs.get-update-version.outputs.new_version }}.dmg" --content-type "application/octet-stream"
aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "latest/jan-mac-arm64-${{ needs.get-update-version.outputs.new_version }}.dmg" --body "./electron/dist/jan-mac-arm64-${{ needs.get-update-version.outputs.new_version }}.dmg" --content-type "application/octet-stream"
aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "latest/jan-mac-x64-${{ needs.get-update-version.outputs.new_version }}.zip" --body "./electron/dist/jan-mac-x64-${{ needs.get-update-version.outputs.new_version }}.zip" --content-type "application/zip"
aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "latest/jan-mac-arm64-${{ needs.get-update-version.outputs.new_version }}.zip" --body "./electron/dist/jan-mac-arm64-${{ needs.get-update-version.outputs.new_version }}.zip" --content-type "application/zip"
aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "${{ needs.get-update-version.outputs.new_version }}/jan-mac-x64-${{ needs.get-update-version.outputs.new_version }}.dmg" --body "./electron/dist/jan-mac-x64-${{ needs.get-update-version.outputs.new_version }}.dmg" --content-type "application/octet-stream"
aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "${{ needs.get-update-version.outputs.new_version }}/jan-mac-arm64-${{ needs.get-update-version.outputs.new_version }}.dmg" --body "./electron/dist/jan-mac-arm64-${{ needs.get-update-version.outputs.new_version }}.dmg" --content-type "application/octet-stream"
aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "${{ needs.get-update-version.outputs.new_version }}/latest-mac.yml" --body "./electron/dist/latest-mac.yml" --content-type "text/yaml"
aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "latest/latest-mac.yml" --body "./electron/dist/latest-mac.yml" --content-type "text/yaml"
env:
AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }}
@ -148,7 +167,7 @@ jobs:
build-windows-x64:
runs-on: windows-latest
needs: delete-cloudflare-r2-folder
needs: [delete-cloudflare-r2-folder, get-update-version]
permissions:
contents: write
steps:
@ -167,38 +186,12 @@ jobs:
id: version_update
shell: bash
run: |
# Function to get the latest release tag
get_latest_tag() {
local retries=0
local max_retries=3
local tag
while [ $retries -lt $max_retries ]; do
tag=$(curl -s https://api.github.com/repos/janhq/jan/releases/latest | jq -r .tag_name)
if [ -n "$tag" ] && [ "$tag" != "null" ]; then
echo $tag
return
else
let retries++
echo "Retrying... ($retries/$max_retries)"
sleep 2
fi
done
echo "Failed to fetch latest tag after $max_retries attempts."
exit 1
}
# Get the latest release tag from GitHub API
LATEST_TAG=$(get_latest_tag)
# Remove the 'v' and append the build number to the version
NEW_VERSION="${LATEST_TAG#v}-${GITHUB_RUN_NUMBER}"
echo "New version: $NEW_VERSION"
# Update the version in electron/package.json
jq --arg version "$NEW_VERSION" '.version = $version' electron/package.json > /tmp/package.json
jq --arg version "${{ needs.get-update-version.outputs.new_version }}" '.version = $version' electron/package.json > /tmp/package.json
mv /tmp/package.json electron/package.json
echo "::set-output name=new_version::$NEW_VERSION"
jq --arg version "${{ needs.get-update-version.outputs.new_version }}" '.version = $version' web/package.json > /tmp/package.json
mv /tmp/package.json web/package.json
jq '.build.publish = [{"provider": "generic", "url": "${{ secrets.CLOUDFLARE_R2_PUBLIC_URL }}", "channel": "latest"}]' electron/package.json > /tmp/package.json
mv /tmp/package.json electron/package.json
@ -223,18 +216,19 @@ jobs:
- name: Upload Artifact
uses: actions/upload-artifact@v2
with:
name: jan-win-x64-${{ steps.version_update.outputs.new_version }}
name: jan-win-x64-${{ needs.get-update-version.outputs.new_version }}
path: ./electron/dist/*.exe
- name: put-object using awscli s3api
if: github.ref == 'refs/heads/main'
shell: bash
run: |
ls -al ./electron/dist
aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "latest/jan-win-x64-${{ steps.version_update.outputs.new_version }}.exe" --body "./electron/dist/jan-win-x64-${{ steps.version_update.outputs.new_version }}.exe"
aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "latest/jan-win-x64-${{ steps.version_update.outputs.new_version }}.exe.blockmap" --body "./electron/dist/jan-win-x64-${{ steps.version_update.outputs.new_version }}.exe.blockmap"
aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "${{ steps.version_update.outputs.new_version }}/jan-win-x64-${{ steps.version_update.outputs.new_version }}.exe" --body "./electron/dist/jan-win-x64-${{ steps.version_update.outputs.new_version }}.exe"
aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "latest/jan-win-x64-${{ needs.get-update-version.outputs.new_version }}.exe" --body "./electron/dist/jan-win-x64-${{ needs.get-update-version.outputs.new_version }}.exe"
aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "latest/jan-win-x64-${{ needs.get-update-version.outputs.new_version }}.exe.blockmap" --body "./electron/dist/jan-win-x64-${{ needs.get-update-version.outputs.new_version }}.exe.blockmap"
aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "${{ needs.get-update-version.outputs.new_version }}/jan-win-x64-${{ needs.get-update-version.outputs.new_version }}.exe" --body "./electron/dist/jan-win-x64-${{ needs.get-update-version.outputs.new_version }}.exe"
aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "latest/latest.yml" --body "./electron/dist/latest.yml"
aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "${{ steps.version_update.outputs.new_version }}/latest.yml" --body "./electron/dist/latest.yml"
aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "${{ needs.get-update-version.outputs.new_version }}/latest.yml" --body "./electron/dist/latest.yml"
env:
AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }}
@ -243,7 +237,7 @@ jobs:
build-linux-x64:
runs-on: ubuntu-latest
needs: delete-cloudflare-r2-folder
needs: [delete-cloudflare-r2-folder, get-update-version]
environment: production
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_TOKEN }}
@ -264,37 +258,11 @@ jobs:
- name: Update app version base on tag
id: version_update
run: |
# Function to get the latest release tag
get_latest_tag() {
local retries=0
local max_retries=3
local tag
while [ $retries -lt $max_retries ]; do
tag=$(curl -s https://api.github.com/repos/janhq/jan/releases/latest | jq -r .tag_name)
if [ -n "$tag" ] && [ "$tag" != "null" ]; then
echo $tag
return
else
let retries++
echo "Retrying... ($retries/$max_retries)"
sleep 2
fi
done
echo "Failed to fetch latest tag after $max_retries attempts."
exit 1
}
# Get the latest release tag from GitHub API
LATEST_TAG=$(get_latest_tag)
# Remove the 'v' and append the build number to the version
NEW_VERSION="${LATEST_TAG#v}-${GITHUB_RUN_NUMBER}"
echo "New version: $NEW_VERSION"
# Update the version in electron/package.json
jq --arg version "$NEW_VERSION" '.version = $version' electron/package.json > /tmp/package.json
jq --arg version "${{ needs.get-update-version.outputs.new_version }}" '.version = $version' electron/package.json > /tmp/package.json
mv /tmp/package.json electron/package.json
echo "::set-output name=new_version::$NEW_VERSION"
jq --arg version "${{ needs.get-update-version.outputs.new_version }}" '.version = $version' web/package.json > /tmp/package.json
mv /tmp/package.json web/package.json
jq '.build.publish = [{"provider": "generic", "url": "${{ secrets.CLOUDFLARE_R2_PUBLIC_URL }}", "channel": "latest"}]' electron/package.json > /tmp/package.json
mv /tmp/package.json electron/package.json
@ -307,16 +275,17 @@ jobs:
- name: Upload Artifact
uses: actions/upload-artifact@v2
with:
name: jan-linux-amd64-${{ steps.version_update.outputs.new_version }}
name: jan-linux-amd64-${{ needs.get-update-version.outputs.new_version }}
path: ./electron/dist/*.deb
- name: put-object using awscli s3api
if: github.ref == 'refs/heads/main'
run: |
ls -al ./electron/dist
aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "latest/jan-linux-amd64-${{ steps.version_update.outputs.new_version }}.deb" --body "./electron/dist/jan-linux-amd64-${{ steps.version_update.outputs.new_version }}.deb" --content-type "application/octet-stream"
aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "${{ steps.version_update.outputs.new_version }}/jan-linux-amd64-${{ steps.version_update.outputs.new_version }}.deb" --body "./electron/dist/jan-linux-amd64-${{ steps.version_update.outputs.new_version }}.deb" --content-type "application/octet-stream"
aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "latest/jan-linux-amd64-${{ needs.get-update-version.outputs.new_version }}.deb" --body "./electron/dist/jan-linux-amd64-${{ needs.get-update-version.outputs.new_version }}.deb" --content-type "application/octet-stream"
aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "${{ needs.get-update-version.outputs.new_version }}/jan-linux-amd64-${{ needs.get-update-version.outputs.new_version }}.deb" --body "./electron/dist/jan-linux-amd64-${{ needs.get-update-version.outputs.new_version }}.deb" --content-type "application/octet-stream"
aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "latest/latest-linux.yml" --body "./electron/dist/latest-linux.yml" --content-type "text/yaml"
aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "${{ steps.version_update.outputs.new_version }}/latest-linux.yml" --body "./electron/dist/latest-linux.yml" --content-type "text/yaml"
aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "${{ needs.get-update-version.outputs.new_version }}/latest-linux.yml" --body "./electron/dist/latest-linux.yml" --content-type "text/yaml"
env:
AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }}
@ -324,7 +293,7 @@ jobs:
AWS_EC2_METADATA_DISABLED: "true"
noti-discord-nightly-and-update-url-readme:
needs: [build-macos, build-windows-x64, build-linux-x64, delete-cloudflare-r2-folder]
needs: [build-macos, build-windows-x64, build-linux-x64, delete-cloudflare-r2-folder, get-update-version]
environment: production
if: github.event_name == 'schedule'
runs-on: ubuntu-latest
@ -335,16 +304,29 @@ jobs:
fetch-depth: "0"
token: ${{ secrets.PAT_SERVICE_ACCOUNT }}
- name: Set version to environment variable
run: |
echo "VERSION=${{ needs.get-update-version.outputs.new_version }}" >> $GITHUB_ENV
- name: Notify Discord
uses: Ilshidur/action-discord@master
with:
args: "Nightly build artifact: https://github.com/janhq/jan/actions/runs/{{ GITHUB_RUN_ID }}"
args: |
Jan App Nightly build artifact version {{ VERSION }}:
- Windows: https://delta.jan.ai/{{ VERSION }}/jan-win-x64-{{ VERSION }}.exe
- macOS Intel: https://delta.jan.ai/{{ VERSION }}/jan-mac-x64-{{ VERSION }}.dmg
- macOS Apple Silicon: https://delta.jan.ai/{{ VERSION }}/jan-mac-arm64-{{ VERSION }}.dmg
- Linux: https://delta.jan.ai/{{ VERSION }}/jan-linux-amd64-{{ VERSION }}.deb
- Github action run: https://github.com/janhq/jan/actions/runs/{{ GITHUB_RUN_ID }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
- name: Update README.md with artifact URL
run: |
sed -i "s|<a href='https://github.com/janhq/jan/actions/runs/.*'>|<a href='https://github.com/janhq/jan/actions/runs/${GITHUB_RUN_ID}'>|" README.md
sed -i "s|<a href='https://delta.jan.ai/.*/jan-win-x64-.*'>|<a href='https://delta.jan.ai/${{ needs.get-update-version.outputs.new_version }}/jan-win-x64-${{ needs.get-update-version.outputs.new_version }}.exe'>|" README.md
sed -i "s|<a href='https://delta.jan.ai/.*/jan-mac-x64-.*'>|<a href='https://delta.jan.ai/${{ needs.get-update-version.outputs.new_version }}/jan-mac-x64-${{ needs.get-update-version.outputs.new_version }}.dmg'>|" README.md
sed -i "s|<a href='https://delta.jan.ai/.*/jan-mac-arm64-.*'>|<a href='https://delta.jan.ai/${{ needs.get-update-version.outputs.new_version }}/jan-mac-arm64-${{ needs.get-update-version.outputs.new_version }}.dmg'>|" README.md
sed -i "s|<a href='https://delta.jan.ai/.*/jan-linux-amd64-.*'>|<a href='https://delta.jan.ai/${{ needs.get-update-version.outputs.new_version }}/jan-linux-amd64-${{ needs.get-update-version.outputs.new_version }}.deb'>|" README.md
git config --global user.email "service@jan.ai"
git config --global user.name "Service Account"
git add README.md
@ -354,9 +336,9 @@ jobs:
GITHUB_RUN_ID: ${{ github.run_id }}
noti-discord-manual-and-update-url-readme:
needs: [build-macos, build-windows-x64, build-linux-x64, delete-cloudflare-r2-folder]
needs: [build-macos, build-windows-x64, build-linux-x64, delete-cloudflare-r2-folder, get-update-version]
environment: production
if: github.event_name == 'workflow_dispatch'
if: github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Checkout code
@ -364,11 +346,21 @@ jobs:
with:
fetch-depth: "0"
token: ${{ secrets.PAT_SERVICE_ACCOUNT }}
- name: Set version to environment variable
run: |
echo "VERSION=${{ needs.get-update-version.outputs.new_version }}" >> $GITHUB_ENV
- name: Notify Discord
uses: Ilshidur/action-discord@master
with:
args: "Manual build artifact: https://github.com/janhq/jan/actions/runs/{{ GITHUB_RUN_ID }}"
args: |
Jan App Manual build artifact version {{ VERSION }}:
- Windows: https://delta.jan.ai/{{ VERSION }}/jan-win-x64-{{ VERSION }}.exe
- macOS Intel: https://delta.jan.ai/{{ VERSION }}/jan-mac-x64-{{ VERSION }}.dmg
- macOS Apple Silicon: https://delta.jan.ai/{{ VERSION }}/jan-mac-arm64-{{ VERSION }}.dmg
- Linux: https://delta.jan.ai/{{ VERSION }}/jan-linux-amd64-{{ VERSION }}.deb
- Github action run: https://github.com/janhq/jan/actions/runs/{{ GITHUB_RUN_ID }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
@ -376,7 +368,10 @@ jobs:
- name: Update README.md with artifact URL
if: github.ref == 'refs/heads/main'
run: |
sed -i "s|<a href='https://github.com/janhq/jan/actions/runs/.*'>|<a href='https://github.com/janhq/jan/actions/runs/${GITHUB_RUN_ID}'>|" README.md
sed -i "s|<a href='https://delta.jan.ai/.*/jan-win-x64-.*'>|<a href='https://delta.jan.ai/${{ needs.get-update-version.outputs.new_version }}/jan-win-x64-${{ needs.get-update-version.outputs.new_version }}.exe'>|" README.md
sed -i "s|<a href='https://delta.jan.ai/.*/jan-mac-x64-.*'>|<a href='https://delta.jan.ai/${{ needs.get-update-version.outputs.new_version }}/jan-mac-x64-${{ needs.get-update-version.outputs.new_version }}.dmg'>|" README.md
sed -i "s|<a href='https://delta.jan.ai/.*/jan-mac-arm64-.*'>|<a href='https://delta.jan.ai/${{ needs.get-update-version.outputs.new_version }}/jan-mac-arm64-${{ needs.get-update-version.outputs.new_version }}.dmg'>|" README.md
sed -i "s|<a href='https://delta.jan.ai/.*/jan-linux-amd64-.*'>|<a href='https://delta.jan.ai/${{ needs.get-update-version.outputs.new_version }}/jan-linux-amd64-${{ needs.get-update-version.outputs.new_version }}.deb'>|" README.md
git config --global user.email "service@jan.ai"
git config --global user.name "Service Account"
git add README.md

View File

@ -60,6 +60,8 @@ jobs:
fi
jq --arg version "${VERSION_TAG#v}" '.version = $version' electron/package.json > /tmp/package.json
mv /tmp/package.json electron/package.json
jq --arg version "${VERSION_TAG#v}" '.version = $version' web/package.json > /tmp/package.json
mv /tmp/package.json web/package.json
env:
VERSION_TAG: ${{ steps.tag.outputs.tag }}
@ -120,6 +122,8 @@ jobs:
fi
jq --arg version "${VERSION_TAG#v}" '.version = $version' electron/package.json > /tmp/package.json
mv /tmp/package.json electron/package.json
jq --arg version "${VERSION_TAG#v}" '.version = $version' web/package.json > /tmp/package.json
mv /tmp/package.json web/package.json
jq '.build.win.sign = "./sign.js"' electron/package.json > /tmp/package.json
mv /tmp/package.json electron/package.json
env:
@ -203,6 +207,8 @@ jobs:
fi
jq --arg version "${VERSION_TAG#v}" '.version = $version' electron/package.json > /tmp/package.json
mv /tmp/package.json electron/package.json
jq --arg version "${VERSION_TAG#v}" '.version = $version' web/package.json > /tmp/package.json
mv /tmp/package.json web/package.json
env:
VERSION_TAG: ${{ steps.tag.outputs.tag }}

1
.gitignore vendored
View File

@ -11,6 +11,7 @@ build
.DS_Store
electron/renderer
electron/models
electron/docs
package-lock.json
*.log

View File

@ -52,21 +52,17 @@ build: check-file-counts
clean:
ifeq ($(OS),Windows_NT)
powershell -Command "Get-ChildItem -Path . -Include node_modules, .next, dist -Recurse -Directory | Remove-Item -Recurse -Force"
rmdir /s /q "%USERPROFILE%\AppData\Roaming\jan"
rmdir /s /q "%USERPROFILE%\AppData\Roaming\jan-electron"
rmdir /s /q "%USERPROFILE%\AppData\Local\jan*"
rmdir /s /q "%USERPROFILE%\jan\extensions"
else ifeq ($(shell uname -s),Linux)
find . -name "node_modules" -type d -prune -exec rm -rf '{}' +
find . -name ".next" -type d -exec rm -rf '{}' +
find . -name "dist" -type d -exec rm -rf '{}' +
rm -rf "~/.config/jan"
rm -rf "~/.config/jan-electron"
rm -rf "~/jan/extensions"
rm -rf "~/.cache/jan*"
else
find . -name "node_modules" -type d -prune -exec rm -rf '{}' +
find . -name ".next" -type d -exec rm -rf '{}' +
find . -name "dist" -type d -exec rm -rf '{}' +
rm -rf ~/Library/Application\ Support/jan
rm -rf ~/Library/Application\ Support/jan-electron
rm -rf ~/jan/extensions
rm -rf ~/Library/Caches/jan*
endif

101
README.md
View File

@ -34,13 +34,13 @@ Jan is an open-source ChatGPT alternative that runs 100% offline on your compute
## Download
<table>
<tr>
<tr style="text-align:center">
<td style="text-align:center"><b>Version Type</b></td>
<td style="text-align:center"><b>Windows</b></td>
<td colspan="2" style="text-align:center"><b>MacOS</b></td>
<td style="text-align:center"><b>Linux</b></td>
</tr>
<tr>
<tr style="text-align:center">
<td style="text-align:center"><b>Stable (Recommended)</b></td>
<td style="text-align:center">
<a href='https://github.com/janhq/jan/releases/download/v0.4.3/jan-win-x64-0.4.3.exe'>
@ -67,11 +67,30 @@ Jan is an open-source ChatGPT alternative that runs 100% offline on your compute
</a>
</td>
</tr>
<tr style="text-align: center">
<td style="text-align:center"><b>Experimental (Nighlty Build)</b></td>
<td style="text-align:center" colspan="4">
<a href='https://github.com/janhq/jan/actions/runs/7372465396'>
<b>Github action artifactory</b>
<tr style="text-align:center">
<td style="text-align:center"><b>Experimental (Nightly Build)</b></td>
<td style="text-align:center">
<a href='https://delta.jan.ai/0.4.3-118/jan-win-x64-0.4.3-118.exe'>
<img src='./docs/static/img/windows.png' style="height:14px; width: 14px" />
<b>jan.exe</b>
</a>
</td>
<td style="text-align:center">
<a href='https://delta.jan.ai/0.4.3-118/jan-mac-x64-0.4.3-118.dmg'>
<img src='./docs/static/img/mac.png' style="height:15px; width: 15px" />
<b>Intel</b>
</a>
</td>
<td style="text-align:center">
<a href='https://delta.jan.ai/0.4.3-118/jan-mac-arm64-0.4.3-118.dmg'>
<img src='./docs/static/img/mac.png' style="height:15px; width: 15px" />
<b>M1/M2</b>
</a>
</td>
<td style="text-align:center">
<a href='https://delta.jan.ai/0.4.3-118/jan-linux-amd64-0.4.3-118.deb'>
<img src='./docs/static/img/linux.png' style="height:14px; width: 14px" />
<b>jan.deb</b>
</a>
</td>
</tr>
@ -91,10 +110,10 @@ _Realtime Video: Jan v0.4.3-nightly on a Mac M1, 16GB Sonoma 14_
- [Jan website](https://jan.ai/)
- [Jan Github](https://github.com/janhq/jan)
- [User Guides](https://jan.ai/docs)
- [Developer docs](https://jan.ai/docs/extensions/)
- [User Guides](https://jan.ai/guides/)
- [Developer docs](https://jan.ai/developer/)
- [API reference](https://jan.ai/api-reference/)
- [Specs](https://jan.ai/specs/)
- [Specs](https://jan.ai/docs/)
#### Nitro
@ -111,18 +130,7 @@ As Jan is in development mode, you might get stuck on a broken build.
To reset your installation:
1. **Remove Jan from your Applications folder and Cache folder**
```bash
make clean
```
This will remove all build artifacts and cached files:
- Delete Jan from your `/Applications` folder
- Clear Application cache in `/Users/$(whoami)/Library/Caches/jan`
2. Use the following commands to remove any dangling backend processes:
1. Use the following commands to remove any dangling backend processes:
```sh
ps aux | grep nitro
@ -134,6 +142,18 @@ To reset your installation:
kill -9 <PID>
```
2. **Remove Jan from your Applications folder and Cache folder**
```bash
make clean
```
This will remove all build artifacts and cached files:
- Delete Jan extension from your `~/jan/extensions` folder
- Delete all `node_modules` in current folder
- Clear Application cache in `~/Library/Caches/jan`
## Contributing
Contributions are welcome! Please read the [CONTRIBUTING.md](CONTRIBUTING.md) file
@ -148,19 +168,19 @@ Contributions are welcome! Please read the [CONTRIBUTING.md](CONTRIBUTING.md) fi
1. **Clone the repository and prepare:**
```bash
git clone https://github.com/janhq/jan
cd jan
git checkout -b DESIRED_BRANCH
```
```bash
git clone https://github.com/janhq/jan
cd jan
git checkout -b DESIRED_BRANCH
```
2. **Run development and use Jan Desktop**
```
make dev
```
```bash
make dev
```
This will start the development server and open the desktop app.
This will start the development server and open the desktop app.
### For production build
@ -172,25 +192,6 @@ make build
This will build the app MacOS m1/m2 for production (with code signing already done) and put the result in `dist` folder.
## Nightly Build
Our nightly build process for this project is defined in [`.github/workflows/jan-electron-build-nightly.yml`](.github/workflows/jan-electron-build-nightly.yml)
The nightly build is triggered at 2:00 AM UTC every day.
Getting on Nightly:
1. Join our Discord server [here](https://discord.gg/FTk2MvZwJH) and go to channel [github-jan](https://discordapp.com/channels/1107178041848909847/1148534730359308298).
2. Download the build artifacts from the channel.
3. Subsequently, to get the latest nightly, just quit and restart the app.
4. Upon app restart, you will be automatically prompted to update to the latest nightly build.
## Manual Build
Stable releases are triggered by manual builds. This is usually done for new features or a bug fixes.
The process for this project is defined in [`.github/workflows/jan-electron-build-nightly.yml`](.github/workflows/jan-electron-build-nightly.yml)
## Acknowledgements
Jan builds on top of other open-source projects:

View File

@ -4,13 +4,14 @@
*/
export enum AppRoute {
appDataPath = 'appDataPath',
appVersion = 'appVersion',
openExternalUrl = 'openExternalUrl',
openAppDirectory = 'openAppDirectory',
openFileExplore = 'openFileExplorer',
relaunch = 'relaunch',
joinPath = 'joinPath',
baseName = 'baseName',
startServer = 'startServer',
stopServer = 'stopServer',
}
export enum AppEvent {

View File

@ -47,7 +47,7 @@ export class ExtensionManager {
const extensionsJson = join(extDir, "extensions.json");
if (!existsSync(extensionsJson))
writeFileSync(extensionsJson, "{}", "utf8");
writeFileSync(extensionsJson, "{}");
this.extensionsPath = extDir;
} catch (error) {

View File

@ -84,7 +84,6 @@ export function persistExtensions() {
writeFileSync(
ExtensionManager.instance.getExtensionsFile(),
JSON.stringify(persistData),
"utf8"
);
}

View File

@ -5,3 +5,4 @@ export * from './extension/store'
export * from './download'
export * from './module'
export * from './api'
export * from './log'

18
core/src/node/log.ts Normal file
View File

@ -0,0 +1,18 @@
import fs from 'fs'
import util from 'util'
import path from 'path'
import os from 'os'
const appDir = path.join(os.homedir(), 'jan')
export const logPath = path.join(appDir, 'app.log')
export const log = function (d: any) {
if (fs.existsSync(appDir)) {
var log_file = fs.createWriteStream(logPath, {
flags: 'a',
})
log_file.write(util.format(d) + '\n')
log_file.close()
}
}

View File

@ -3,6 +3,7 @@
*/
export class ModuleManager {
public requiredModules: Record<string, any> = {}
public cleaningResource = false
public static instance: ModuleManager = new ModuleManager()

View File

@ -1,9 +1,23 @@
---
title: Your First Assistant
slug: /developer/build-assistant/your-first-assistant/
description: A quick start on how to build an assistant.
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
quick start,
build assistant,
]
---
:::caution
This is currently under development.
:::
A quickstart on how to build an assistant

View File

@ -1,9 +1,22 @@
---
title: Anatomy of an Assistant
slug: /developer/build-assistant/assistant-anatomy/
description: An overview of assistant.json
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
build assistant,
assistant anatomy,
]
---
:::caution
This is currently under development.
:::
An overview of assistant.json

View File

@ -1,9 +1,22 @@
---
title: Package your Assistant
slug: /developer/build-assistant/package-your-assistant/
description: Package your assistant for sharing and publishing.
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
quick start,
build assistant,
]
---
:::caution
This is currently under development.
:::
Packaging, exporting, sharing, publishing an assistant to Hub

View File

@ -1,12 +0,0 @@
---
title: Build an Assistant
---
:::caution
This is currently under development.
:::
In this tutorial you will learn:
-
-

View File

@ -0,0 +1,25 @@
---
title: Build an Assistant
slug: /developer/build-assistant
description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server.
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
build assistant,
]
---
:::caution
This is currently under development.
:::
import DocCardList from "@theme/DocCardList";
<DocCardList />

View File

@ -1,9 +1,24 @@
---
title: Your First Assistant
title: Your First Engine
slug: /developer/build-engine/your-first-engine/
description: A quick start on how to build your first engine
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
quick start,
build engine,
]
---
:::caution
This is currently under development.
:::
A quickstart on how to integrate tensorrt llm
A quickstart on how to integrate tensorrt llm

View File

@ -1,9 +1,22 @@
---
title: Anatomy of an Engine
slug: /developer/build-engine/engine-anatomy
description: An overview of engine.json
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
build engine,
engine anatomy,
]
---
:::caution
This is currently under development.
:::
An overview of engine.json

View File

@ -1,9 +1,22 @@
---
title: Package your Extension
title: Package your Engine
slug: /developer/build-engine/package-your-engine/
description: Package your engine for sharing and publishing.
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
build engine,
engine anatomy,
]
---
:::caution
This is currently under development.
:::
Packaging, exporting, sharing, publishing an engine config to Hub

View File

@ -1,12 +0,0 @@
---
title: Build an Inference Engine
---
:::caution
This is currently under development.
:::
In this tutorial you will learn:
-
-

View File

@ -0,0 +1,25 @@
---
title: Build an Inference Engine
slug: /developer/build-engine/
description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server.
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
build assistant,
]
---
:::caution
This is currently under development.
:::
import DocCardList from "@theme/DocCardList";
<DocCardList />

View File

@ -1,9 +1,22 @@
---
title: Your First Extension
slug: /developer/build-extension/your-first-extension/
description: A quick start on how to build your first extension
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
quick start,
build extension,
]
---
:::caution
This is currently under development.
:::
A quickstart on tensorrt-llm impl

View File

@ -1,9 +1,22 @@
---
title: Anatomy of an Extension
slug: /developer/build-extension/extension-anatomy
description: An overview of extensions.json
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
build extension,
extension anatomy,
]
---
:::caution
This is currently under development.
:::
An overview of engine.json

View File

@ -1,9 +0,0 @@
---
title: Package your Extension
---
:::caution
This is currently under development.
:::
Packaging, exporting, sharing, publishing an extension to Hub

View File

@ -0,0 +1,22 @@
---
title: Package your Engine
slug: /developer/build-extension/package-your-extension/
description: Package your extension for sharing and publishing.
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
build extension,
extension anatomy,
]
---
:::caution
This is currently under development.
:::

View File

@ -1,9 +0,0 @@
---
title: Build an Extension
---
# Overview
:::caution
This is currently under development.
:::

View File

@ -0,0 +1,25 @@
---
title: Build an Extension
slug: /developer/build-extension/
description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server.
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
build extension,
]
---
:::caution
This is currently under development.
:::
import DocCardList from "@theme/DocCardList";
<DocCardList />

View File

@ -3,4 +3,69 @@ title: Overview
slug: /docs
---
Hello world
The following low-level docs are aimed at core contributors and cover how to contribute to the Core SDK.
:::tip
If you are interested to **build on top of the SDK**, like creating assistants or adding app level extensions, please refer to [developer docs](/developer) instead.
:::
## Core SDK
At its Core, Jan is a cross-platform, local-first and AI native framework that can be used to build anything. In fact, current features are all implemented as 3rd party extensions on top of this Core SDK.
Ultimately, we aim for a VSCode or Obsidian like framework that allows devs to build and customize complex AI applications for their specific needs, in less than 15 minutes.
### Cross Platform
Jan follows [Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) to the best of our ability. Though leaky abstractions remain (we're a fast moving, open source codebase), we do our best to build an SDK that allows devs to **build once, deploy everywhere.**
Currently, Jan supports:
- `Node Native Runtime`, good for server side apps
- `Electron Chromium`, good for Desktop Native apps
- `Capacitor`, good for Mobile apps (planned, not built yet)
- `Python Runtime`, good for MLOps workflows (planned, not built yet)
Currently, Jan works across:
- Mac Intel & Silicon
- Windows
- Ubuntu
- Nvidia GPUs
Read more:
- [Code Entrypoint](https://github.com/janhq/jan/tree/main/core)
- [Dependency Inversion](https://en.wikipedia.org/wiki/Dependency_inversion_principle)
### Local First
Jan's data persistence happens on the user's local filesystem.
We implemented abstractions on top of `fs` and other core modules in an opinionated way, s.t. user data is saved in a folder-based framework that lets users easily package, export, and manage their data.
Read more:
- [Folder-based fs wrapper](https://github.com/janhq/jan/blob/main/core/src/fs.ts)
- [Piping Node modules across infrastructures](https://github.com/janhq/jan/tree/main/core/src/node)
### AI Native
All software applications can be natively supercharged with an embedded AI server and AI abstractions.
Including:
- OpenAI Compatible AI [types](https://github.com/janhq/jan/tree/main/core/src/types) and [core extensions](https://github.com/janhq/jan/tree/main/core/src/extensions) to support common functionality like making an inference call.
- A lightweight, embedded C++ [inference engine](https://github.com/janhq/jan/tree/main/extensions/inference-nitro-extension) that's immediately callable from code.
- [Code Entrypoint](https://github.com/janhq/jan/tree/main/core/src/api)
## Fun Project Ideas
Beyond the current Jan client and UX, the Core SDK can be used to build many other AI-powered and privacy preserving applications.
- `Game engine`: For AI enabled character games, procedural generation games
- `Health app`: For a personal healthcare app that improves habits
- Got ideas? Make a PR into this docs page!
If you are interested to tackle these issues, or have suggestions for integrations and other OSS tools we can use, please hit us up in [Discord](https://discord.gg/5rQ2zTv3be).

View File

@ -1,5 +0,0 @@
---
title: Engineering Specs
---
Talk about CoreSDK here

View File

@ -0,0 +1,24 @@
---
title: Engineering Specs
slug: /docs/engineering
description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server.
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
spec,
engineering,
]
---
import DocCardList from "@theme/DocCardList";
<DocCardList className="DocCardList--no-description" />
Talk about CoreSDK here

View File

@ -1,3 +0,0 @@
---
title: Product Specs
---

View File

@ -0,0 +1,22 @@
---
title: Product Specs
slug: /docs/product
description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server.
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
spec,
product,
]
---
import DocCardList from "@theme/DocCardList";
<DocCardList className="DocCardList--no-description" />

View File

@ -18,7 +18,6 @@ keywords:
This guide is designed to help you maximize your experience with Jan, covering everything from starting engaging threads to managing your chat history effectively.
- [Start a thread](start-thread)
- [Upload docs](upload-docs)
- [Upload images](upload-images)
- [Manage chat history](manage-chat-history)
import DocCardList from "@theme/DocCardList";
<DocCardList />

View File

@ -1,6 +1,6 @@
---
title: Install Models from the Hub
description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server.
description: Guide to install models from the Hub.
keywords:
[
Jan AI,

View File

@ -1,7 +1,7 @@
---
title: Import Models Manually
slug: /guides/using-models/import-manually
description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server.
description: Guide to manually import a local model into Jan.
keywords:
[
Jan AI,
@ -13,6 +13,7 @@ keywords:
no-subscription fee,
large language model,
import-models-manually,
local model,
]
---
@ -24,16 +25,12 @@ This is currently under development.
import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem";
Jan is compatible with all GGUF models.
## Steps to Manually Import a Local Model
If you can not find the model you want in the Hub or have a custom model you want to use, you can import it manually.
In this guide, we will show you how to import a GGUF model from [HuggingFace](https://huggingface.co/), using our lastest model, [Trinity](https://huggingface.co/janhq/trinity-v1-GGUF), as an example.
In this section, we will show you how to import a GGUF model from [HuggingFace](https://huggingface.co/), using our latest model, [Trinity](https://huggingface.co/janhq/trinity-v1-GGUF), as an example.
> We are fast shipping a UI to make this easier, but it's a bit manual for now. Apologies.
## Steps to Manually Import a Model
### 1. Create a Model Folder
Navigate to the `~/jan/models` folder. You can find this folder by going to `App Settings` > `Advanced` > `Open App Directory`.
@ -126,45 +123,45 @@ Edit `model.json` and include the following configurations:
- Ensure the filename must be `model.json`.
- Ensure the `id` property matches the folder name you created.
- Ensure the GGUF filename should match the `id` property exactly.
- Ensure the `source_url` property is the direct binary download link ending in `.gguf`. In HuggingFace, you can find the direct links in `Files and versions` tab.
- Ensure the `source_url` property is the direct binary download link ending in `.gguf`. In HuggingFace, you can find the direct links in the `Files and versions` tab.
- Ensure you are using the correct `prompt_template`. This is usually provided in the HuggingFace model's description page.
- Ensure the `state` property is set to `ready`.
```js
{
// highlight-start
"source_url": "https://huggingface.co/janhq/trinity-v1-GGUF/resolve/main/trinity-v1.Q4_K_M.gguf",
"id": "trinity-v1-7b",
// highlight-end
"object": "model",
"name": "Trinity-v1 7B Q4",
"version": "1.0",
"description": "Trinity is an experimental model merge of GreenNodeLM & LeoScorpius using the Slerp method. Recommended for daily assistance purposes.",
"format": "gguf",
"settings": {
"ctx_len": 4096,
// highlight-next-line
"prompt_template": "{system_message}\n### Instruction:\n{prompt}\n### Response:"
},
"parameters": {
"max_tokens": 4096
},
"metadata": {
"author": "Jan",
"tags": ["7B", "Merged"],
"size": 4370000000
},
// highlight-start
"source_url": "https://huggingface.co/janhq/trinity-v1-GGUF/resolve/main/trinity-v1.Q4_K_M.gguf",
"id": "trinity-v1-7b",
// highlight-end
"object": "model",
"name": "Trinity-v1 7B Q4",
"version": "1.0",
"description": "Trinity is an experimental model merge of GreenNodeLM & LeoScorpius using the Slerp method. Recommended for daily assistance purposes.",
"format": "gguf",
"settings": {
"ctx_len": 4096,
// highlight-next-line
"state": "ready",
"engine": "nitro"
}
"prompt_template": "{system_message}\n### Instruction:\n{prompt}\n### Response:"
},
"parameters": {
"max_tokens": 4096
},
"metadata": {
"author": "Jan",
"tags": ["7B", "Merged"],
"size": 4370000000
},
"engine": "nitro",
// highlight-next-line
"state": "ready"
}
```
### 3. Download the Model
Restart Jan and navigate to the Hub. Locate your model and click the `Download` button to download the model binary.
![image](assets/download-model.png)
![image-01](assets/02-manually-import-local-model.png)
Your model is now ready to use in Jan.

View File

@ -0,0 +1,148 @@
---
title: Integrate With a Remote Server
slug: /guides/using-models/integrate-with-remote-server
description: Guide to integrate with a remote server.
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
import-models-manually,
remote server,
OAI compatible,
]
---
:::caution
This is currently under development.
:::
In this guide, we will show you how to configure Jan as a client and point it to any remote & local (self-hosted) API server.
## OpenAI Platform Configuration
In this section, we will show you how to configure with OpenAI Platform, using the OpenAI GPT 3.5 Turbo 16k model as an example.
### 1. Create a Model JSON
Navigate to the `~/jan/models` folder. Create a folder named `gpt-3.5-turbo-16k` and create a `model.json` file inside the folder including the following configurations:
- Ensure the filename must be `model.json`.
- Ensure the `id` property matches the folder name you created.
- Ensure the `format` property is set to `api`.
- Ensure the `engine` property is set to `openai`.
- Ensure the `state` property is set to `ready`.
```js
{
"source_url": "https://openai.com",
// highlight-next-line
"id": "gpt-3.5-turbo-16k",
"object": "model",
"name": "OpenAI GPT 3.5 Turbo 16k",
"version": "1.0",
"description": "OpenAI GPT 3.5 Turbo 16k model is extremely good",
// highlight-start
"format": "api",
"settings": {},
"parameters": {},
"metadata": {
"author": "OpenAI",
"tags": ["General", "Big Context Length"]
},
"engine": "openai",
"state": "ready"
// highlight-end
}
```
### 2. Configure OpenAI API Keys
You can find your API keys in the [OpenAI Platform](https://platform.openai.com/api-keys) and set the OpenAI API keys in `~/jan/engines/openai.json` file.
```js
{
"full_url": "https://api.openai.com/v1/chat/completions",
// highlight-next-line
"api_key": "sk-<your key here>"
}
```
### 3. Start the Model
Restart Jan and navigate to the Hub. Then, select your configured model and start the model.
![image-01](assets/03-openai-platform-configuration.png)
## Engines with OAI Compatible Configuration
In this section, we will show you how to configure a client connection to a remote/local server, using Jan's API server that is running model `mistral-ins-7b-q4` as an example.
### 1. Configure a Client Connection
Navigate to the `~/jan/engines` folder and modify the `openai.json` file. Please note that at the moment the code that supports any openai compatible endpoint only reads `engine/openai.json` file, thus, it will not search any other files in this directory.
Configure `full_url` properties with the endpoint server that you want to connect. For example, if you want to connect to Jan's API server, you can configure it as follows:
```js
{
// highlight-start
// "full_url": "https://<server-ip-address>:<port>/v1/chat/completions"
"full_url": "https://<server-ip-address>:1337/v1/chat/completions",
// highlight-end
// Skip api_key if your local server does not require authentication
// "api_key": "sk-<your key here>"
}
```
### 2. Create a Model JSON
Navigate to the `~/jan/models` folder. Create a folder named `mistral-ins-7b-q4` and create a `model.json` file inside the folder including the following configurations:
- Ensure the filename must be `model.json`.
- Ensure the `id` property matches the folder name you created.
- Ensure the `format` property is set to `api`.
- Ensure the `engine` property is set to `openai`.
- Ensure the `state` property is set to `ready`.
```js
{
"source_url": "https://jan.ai",
// highlight-next-line
"id": "mistral-ins-7b-q4",
"object": "model",
"name": "Mistral Instruct 7B Q4 on Jan API Server",
"version": "1.0",
"description": "Jan integration with remote Jan API server",
// highlight-next-line
"format": "api",
"settings": {},
"parameters": {},
"metadata": {
"author": "MistralAI, The Bloke",
"tags": [
"remote",
"awesome"
]
},
// highlight-start
"engine": "openai",
"state": "ready"
// highlight-end
}
```
### 3. Start the Model
Restart Jan and navigate to the Hub. Locate your model and click the Use button.
![image-02](assets/03-oai-compatible-configuration.png)
## Assistance and Support
If you have questions or are looking for more preconfigured GGUF models, please feel free to join our [Discord community](https://discord.gg/Dt7MxDyNNZ) for support, updates, and discussions.

View File

@ -1,3 +0,0 @@
---
title: Using Models
---

View File

@ -0,0 +1,21 @@
---
title: Using Models
slug: /guides/using-models/
description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server.
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
using-models,
]
---
import DocCardList from "@theme/DocCardList";
<DocCardList className="DocCardList" />

View File

Before

Width:  |  Height:  |  Size: 378 KiB

After

Width:  |  Height:  |  Size: 378 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 KiB

View File

@ -1,6 +1,6 @@
---
title: Connect to Server
description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server.
description: Connect to Jan's built-in API server.
keywords:
[
Jan AI,
@ -20,13 +20,14 @@ This page is under construction.
:::
Jan ships with a built-in API server, that can be used as a drop-in replacement for OpenAI's API.
Jan ships with a built-in API server, that can be used as a drop-in, local replacement for OpenAI's API.
Jan runs on port `1337` by default, but this can be changed in Settings.
Jan runs on port `1337` by default, but this can (soon) be changed in Settings.
Check out the [API Reference](/api-reference) for more information on the API endpoints.
1. Go to Settings > Advanced > Enable API Server
```
curl http://localhost:1337/v1/chat/completions
2. Go to http://localhost:1337/docs for API docs.
```
3. In terminal, simply CURL...
Note: Some UI states may be broken when in Server Mode.

View File

@ -1,3 +0,0 @@
---
title: Using the Local Server
---

View File

@ -0,0 +1,21 @@
---
title: Using the Local Server
slug: /guides/using-server/
description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server.
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
using-server,
]
---
import DocCardList from "@theme/DocCardList";
<DocCardList />

View File

@ -1,3 +1,17 @@
---
title: Import Extensions
---
slug: /guides/using-extensions/import-extensions/
description: Import extensions into Jan.
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
using-models,
]
---

View File

@ -1,5 +1,17 @@
---
title: Extension Settings
---
TODO: how to configure settings for extensions
slug: /guides/using-extensions/extension-settings/
description: Configure settings for extensions.
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
using-models,
]
---

View File

@ -1,3 +0,0 @@
---
title: Using Extensions
---

View File

@ -0,0 +1,21 @@
---
title: Using Extensions
slug: /guides/using-extensions/
description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server.
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
using-extensions,
]
---
import DocCardList from "@theme/DocCardList";
<DocCardList />

View File

@ -1,7 +1,7 @@
---
title: Stuck on a Broken Build
slug: /troubleshooting/stuck-on-broken-build
description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server.
description: Troubleshooting steps to resolve issues related to broken builds.
keywords:
[
Jan AI,

View File

@ -1,3 +0,0 @@
---
title: Troubleshooting
---

View File

@ -0,0 +1,21 @@
---
title: Troubleshooting
slug: /guides/troubleshooting/
description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server.
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
troubleshooting,
]
---
import DocCardList from "@theme/DocCardList";
<DocCardList />

View File

@ -32,7 +32,7 @@ Welcome to Jan! Were really excited to bring you onboard.
- We operate on the basis of trust.
- We expect you to be available and communicative during scheduled meetings or work hours.
- Turning on video during meetings is encouraged.
- Casual dress during meetings is acceptable; however, use discretion (No naked top, pajamas, etc.)
- Casual dress during meetings is acceptable; however, use discretion (No nudity, pajamas, etc.)
- While its natural for people to disagree at times, disagreement is no excuse for poor behavior and poor manners. We cannot allow that frustration to turn into a personal attack.
- Respect other people's cultures. Especially since we are working in a diverse working culture.
- Sexual harassment is a specific type of prohibited conduct. Sexual harassment is any unwelcome conduct of a sexual nature that might reasonably be expected or be perceived to cause offense or humiliation. Sexual harassment may involve any conduct of a verbal, nonverbal, or physical nature, including written and electronic communications, and may occur between persons of the same or different genders.

View File

@ -6,41 +6,54 @@ We use the [Jan Monorepo Project](https://github.com/orgs/janhq/projects/5) in G
As much as possible, everyone owns their respective `epics` and `tasks`.
> We aim for a `loosely coupled, but tightly aligned` autonomous culture.
:::tip
We aim for a `loosely coupled, but tightly aligned` autonomous culture.
:::
## Quicklinks
- [High-level roadmap](https://github.com/orgs/janhq/projects/5/views/16): view used at at strategic level, for team wide alignment. Start & end dates reflect engineering implementation cycles. Typically product & design work preceeds these timelines.
- [Standup Kanban](https://github.com/orgs/janhq/projects/5/views/25): view used during daily standup. Sprints should be up to date.
## Organization
[`Project Labels`](https://github.com/janhq/jan/issues/labels)
[`Roadmap Labels`](https://github.com/janhq/jan/labels?q=roadmap)
- `Project Labels` tag large, long-term, & strategic projects that can span multiple teams and multiple sprints
- Example label: `project: Jan has Mobile`
- `Projects` contain `epics`
- `Roadmap Labels` tag large, long-term, & strategic projects that can span multiple teams and multiple sprints
- Example label: `roadmap: Jan has Mobile`
- `Roadmaps` contain `epics`
[`Epics`](https://github.com/janhq/jan/issues?q=is%3Aissue+is%3Aopen+label%3A%22type%3A+epic%22)
- `Epics` track large stories that span 1-2 weeks, and it outlines specs, architecture decisions, designs
- Each `epic` corresponds with a `milestone`
- `Epics` contain `tasks`
- `Epics` should always have 1 owner
[`Milestones`](https://github.com/janhq/jan/milestones)
- `Milestones` correspond 1:1 to `epics` and are used to filter [Roadmap Views](https://github.com/orgs/janhq/projects/5/views/16)
- `Milestones` span 1-2 weeks and have deadlines
- `Milestones` track release versions. We use [semantic versioning](https://semver.org/)
- `Milestones` span ~2 weeks and have deadlines
- `Milestones` usually fit within 2 week sprint cycles
[`Tasks`](https://github.com/janhq/jan/issues)
- Tasks are individual issues (feats, bugs, chores) that can be completed within a few days
- Tasks under `In-progress` and `Todo` should always belong to a `milestone`
- Tasks, except for critical bugs, should always belong to an `epic` (and thus fit into our roadmap)
- Tasks are usually named per [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/#summary)
- Tasks should always have 1 owner
We aim to always work on `tasks` that belong to a `milestones`.
We aim to always sprint on `tasks` that are a part of the [current roadmap](https://github.com/orgs/janhq/projects/5/views/16).
## Task Status
## Kanban
- `triaged`: issues that have been assigned
- `todo`: issues you plan to tackle within this week
- `no status`: issues that need to be triaged (needs an owner, ETA)
- `icebox`: issues you don't plan to tackle yet
- `planned`: issues you plan to tackle this week
- `in-progress`: in progress
- `in-review`: pending PR or blocked by something
- `done`: done
## Triage SOP
- `Urgent bugs`: assign to an owner (or @engineers if you are not sure) && tag the current `sprint` & `milestone`
- `All else`: assign the correct roadmap `label(s)` and owner (if any)

3
docs/src/css/custom.css Normal file
View File

@ -0,0 +1,3 @@
.DocCardList--no-description .card p {
display: none;
}

View File

@ -2,4 +2,4 @@
font-weight: bold;
margin-bottom: 16px;
margin-top: -20px;
}
}

View File

@ -11,3 +11,5 @@
@import "./tweaks/markdown.scss";
@import "./tweaks/redocusaurus.scss";
@import "./tweaks/sidebar.scss";
@import "../css/custom.css";

View File

@ -1,6 +1,6 @@
.theme-doc-markdown {
a {
@apply text-blue-600 dark:text-blue-400 underline;
@apply text-blue-600 dark:text-blue-400;
}
ul {
list-style: revert;

View File

View File

@ -1,20 +1,12 @@
import { app, ipcMain, shell, nativeTheme } from 'electron'
import { join, basename } from 'path'
import { WindowManager } from './../managers/window'
import { userSpacePath } from './../utils/path'
import { getResourcePath, userSpacePath } from './../utils/path'
import { AppRoute } from '@janhq/core'
import { ExtensionManager, ModuleManager } from '@janhq/core/node'
import { startServer, stopServer } from '@janhq/server'
export function handleAppIPCs() {
/**
* Returns the version of the app.
* @param _event - The IPC event object.
* @returns The version of the app.
*/
ipcMain.handle(AppRoute.appVersion, async (_event) => {
return app.getVersion()
})
/**
* Handles the "openAppDirectory" IPC message by opening the app's user data directory.
* The `shell.openPath` method is used to open the directory in the user's default file explorer.
@ -56,6 +48,23 @@ export function handleAppIPCs() {
basename(path)
)
/**
* Start Jan API Server.
*/
ipcMain.handle(AppRoute.startServer, async (_event) =>
startServer(
app.isPackaged
? join(getResourcePath(), 'docs', 'openapi', 'jan.yaml')
: undefined,
app.isPackaged ? join(getResourcePath(), 'docs', 'openapi') : undefined
)
)
/**
* Stop Jan API Server.
*/
ipcMain.handle(AppRoute.stopServer, async (_event) => stopServer())
/**
* Relaunches the app in production - reload window in development.
* @param _event - The IPC event object.

View File

@ -34,8 +34,18 @@ export function handleDownloaderIPCs() {
*/
ipcMain.handle(DownloadRoute.abortDownload, async (_event, fileName) => {
const rq = DownloadManager.instance.networkRequests[fileName]
DownloadManager.instance.networkRequests[fileName] = undefined
rq?.abort()
if (rq) {
DownloadManager.instance.networkRequests[fileName] = undefined
rq?.abort()
} else {
WindowManager?.instance.currentWindow?.webContents.send(
DownloadEvent.onFileDownloadError,
{
fileName,
err: { message: 'aborted' },
}
)
}
})
/**
@ -54,7 +64,11 @@ export function handleDownloaderIPCs() {
}
const destination = resolve(userDataPath, fileName)
const rq = request(url)
// downloading file to a temp file first
// Put request to download manager instance
DownloadManager.instance.setRequest(fileName, rq)
// Downloading file to a temp file first
const downloadingTempFile = `${destination}.download`
progress(rq, {})
@ -93,13 +107,11 @@ export function handleDownloaderIPCs() {
DownloadEvent.onFileDownloadError,
{
fileName,
err: 'Download cancelled',
err: { message: 'aborted' },
}
)
}
})
.pipe(createWriteStream(downloadingTempFile))
DownloadManager.instance.setRequest(fileName, rq)
})
}

View File

@ -19,11 +19,7 @@ import { handleAppIPCs } from './handlers/app'
import { handleAppUpdates } from './handlers/update'
import { handleFsIPCs } from './handlers/fs'
import { migrateExtensions } from './utils/migration'
/**
* Server
*/
import { startServer } from '@janhq/server'
import { dispose } from './utils/disposable'
app
.whenReady()
@ -34,7 +30,6 @@ app
.then(handleIPCs)
.then(handleAppUpdates)
.then(createMainWindow)
.then(startServer)
.then(() => {
app.on('activate', () => {
if (!BrowserWindow.getAllWindows().length) {
@ -43,14 +38,12 @@ app
})
})
app.on('window-all-closed', () => {
ModuleManager.instance.clearImportedModules()
app.quit()
app.once('window-all-closed', () => {
cleanUpAndQuit()
})
app.on('quit', () => {
ModuleManager.instance.clearImportedModules()
app.quit()
app.once('quit', () => {
cleanUpAndQuit()
})
function createMainWindow() {
@ -75,6 +68,12 @@ function createMainWindow() {
if (process.platform !== 'darwin') app.quit()
})
/* Open external links in the default browser */
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
require('electron').shell.openExternal(url)
return { action: 'deny' }
})
/* Enable dev tools for development */
if (!app.isPackaged) mainWindow.webContents.openDevTools()
}
@ -89,3 +88,13 @@ function handleIPCs() {
handleAppIPCs()
handleFileMangerIPCs()
}
function cleanUpAndQuit() {
if (!ModuleManager.instance.cleaningResource) {
ModuleManager.instance.cleaningResource = true
WindowManager.instance.currentWindow?.destroy()
dispose(ModuleManager.instance.requiredModules)
ModuleManager.instance.clearImportedModules()
app.quit()
}
}

View File

@ -14,11 +14,13 @@
"build/*.{js,map}",
"build/**/*.{js,map}",
"pre-install",
"models/**/*"
"models/**/*",
"docs/**/*"
],
"asarUnpack": [
"pre-install",
"models"
"models",
"docs"
],
"publish": [
{

View File

@ -32,9 +32,9 @@
"@janhq/core": "file:../../core",
"download-cli": "^1.1.1",
"fetch-retry": "^5.0.6",
"os-utils": "^0.0.14",
"path-browserify": "^1.0.1",
"rxjs": "^7.8.1",
"systeminformation": "^5.21.20",
"tcp-port-used": "^1.0.2",
"ts-loader": "^9.5.0",
"ulid": "^2.3.0"
@ -50,6 +50,6 @@
"bundleDependencies": [
"tcp-port-used",
"fetch-retry",
"systeminformation"
"os-utils"
]
}

View File

@ -4,7 +4,7 @@ const path = require("path");
const { exec, spawn } = require("child_process");
const tcpPortUsed = require("tcp-port-used");
const fetchRetry = require("fetch-retry")(global.fetch);
const si = require("systeminformation");
const osUtils = require("os-utils");
const { readFileSync, writeFileSync, existsSync } = require("fs");
// The PORT to use for the Nitro subprocess
@ -61,7 +61,7 @@ async function updateNvidiaDriverInfo(): Promise<void> {
(error, stdout) => {
let data;
try {
data = JSON.parse(readFileSync(NVIDIA_INFO_FILE, "utf8"));
data = JSON.parse(readFileSync(NVIDIA_INFO_FILE, "utf-8"));
} catch (error) {
data = DEFALT_SETTINGS;
}
@ -109,7 +109,7 @@ function updateCudaExistence() {
let data;
try {
data = JSON.parse(readFileSync(NVIDIA_INFO_FILE, "utf8"));
data = JSON.parse(readFileSync(NVIDIA_INFO_FILE, "utf-8"));
} catch (error) {
data = DEFALT_SETTINGS;
}
@ -127,7 +127,7 @@ async function updateGpuInfo(): Promise<void> {
(error, stdout) => {
let data;
try {
data = JSON.parse(readFileSync(NVIDIA_INFO_FILE, "utf8"));
data = JSON.parse(readFileSync(NVIDIA_INFO_FILE, "utf-8"));
} catch (error) {
data = DEFALT_SETTINGS;
}
@ -376,7 +376,7 @@ function spawnNitroProcess(nitroResourceProbe: any): Promise<any> {
let cudaVisibleDevices = "";
let binaryName;
if (process.platform === "win32") {
let nvida_info = JSON.parse(readFileSync(NVIDIA_INFO_FILE, "utf8"));
let nvida_info = JSON.parse(readFileSync(NVIDIA_INFO_FILE, "utf-8"));
if (nvida_info["run_mode"] === "cpu") {
binaryFolder = path.join(binaryFolder, "win-cpu");
} else {
@ -392,7 +392,7 @@ function spawnNitroProcess(nitroResourceProbe: any): Promise<any> {
}
binaryName = "nitro";
} else {
let nvida_info = JSON.parse(readFileSync(NVIDIA_INFO_FILE, "utf8"));
let nvida_info = JSON.parse(readFileSync(NVIDIA_INFO_FILE, "utf-8"));
if (nvida_info["run_mode"] === "cpu") {
binaryFolder = path.join(binaryFolder, "linux-cpu");
} else {
@ -440,11 +440,10 @@ function spawnNitroProcess(nitroResourceProbe: any): Promise<any> {
*/
function getResourcesInfo(): Promise<ResourcesInfo> {
return new Promise(async (resolve) => {
const cpu = await si.cpu();
// const mem = await si.mem();
const cpu = await osUtils.cpuCount();
console.log("cpu: ", cpu);
const response: ResourcesInfo = {
numCpuPhysicalCore: cpu.physicalCores,
numCpuPhysicalCore: cpu,
memAvailable: 0,
};
resolve(response);

View File

@ -17,7 +17,7 @@
},
"dependencies": {
"@janhq/core": "file:../../core",
"systeminformation": "^5.21.8",
"node-os-utils": "^1.3.7",
"ts-loader": "^9.5.0"
},
"files": [
@ -26,6 +26,6 @@
"README.md"
],
"bundleDependencies": [
"systeminformation"
"node-os-utils"
]
}

View File

@ -1,22 +1,32 @@
const si = require("systeminformation");
const os = require("os");
const nodeOsUtils = require("node-os-utils");
const getResourcesInfo = async () =>
new Promise(async (resolve) => {
const cpu = await si.cpu();
const mem = await si.mem();
// const gpu = await si.graphics();
const response = {
cpu,
mem,
// gpu,
};
resolve(response);
const getResourcesInfo = () =>
new Promise((resolve) => {
nodeOsUtils.mem.used()
.then(ramUsedInfo => {
const totalMemory = ramUsedInfo.totalMemMb * 1024 * 1024;
const usedMemory = ramUsedInfo.usedMemMb * 1024 * 1024;
const response = {
mem: {
totalMemory,
usedMemory,
},
};
resolve(response);
})
});
const getCurrentLoad = async () =>
new Promise(async (resolve) => {
const currentLoad = await si.currentLoad();
resolve(currentLoad);
const getCurrentLoad = () =>
new Promise((resolve) => {
nodeOsUtils.cpu.usage().then(cpuPercentage =>{
const response = {
cpu: {
usage: cpuPercentage,
},
};
resolve(response);
});
});
module.exports = {

View File

@ -25,7 +25,8 @@
"scripts": {
"lint": "yarn workspace jan lint && yarn workspace jan-web lint",
"test": "yarn workspace jan test:e2e",
"dev:electron": "cpx \"models/**\" \"electron/models/\" && yarn workspace jan dev",
"copy:assets": "cpx \"models/**\" \"electron/models/\" && cpx \"docs/openapi/**\" \"electron/docs/openapi\"",
"dev:electron": "yarn copy:assets && yarn workspace jan dev",
"dev:web": "yarn workspace jan-web dev",
"dev": "concurrently --kill-others \"yarn dev:web\" \"wait-on http://localhost:3000 && yarn dev:electron\"",
"test-local": "yarn lint && yarn build:test && yarn test",
@ -34,15 +35,15 @@
"build:server": "cd server && yarn install && yarn run build",
"build:core": "cd core && yarn install && yarn run build",
"build:web": "yarn workspace jan-web build && cpx \"web/out/**\" \"electron/renderer/\"",
"build:electron": "cpx \"models/**\" \"electron/models/\" && yarn workspace jan build",
"build:electron": "yarn copy:assets && yarn workspace jan build",
"build:electron:test": "yarn workspace jan build:test",
"build:extensions:windows": "rimraf ./electron/pre-install/*.tgz && powershell -command \"$jobs = Get-ChildItem -Path './extensions' -Directory | ForEach-Object { Start-Job -Name ($_.Name) -ScriptBlock { param($_dir); try { Set-Location $_dir; npm install; npm run build:publish; Write-Output 'Build successful in ' + $_dir } catch { Write-Error 'Error in ' + $_dir; throw } } -ArgumentList $_.FullName }; $jobs | Wait-Job; $jobs | ForEach-Object { Receive-Job -Job $_ -Keep } | ForEach-Object { Write-Host $_ }; $failed = $jobs | Where-Object { $_.State -ne 'Completed' -or $_.ChildJobs[0].JobStateInfo.State -ne 'Completed' }; if ($failed) { Exit 1 }\"",
"build:extensions:linux": "rimraf ./electron/pre-install/*.tgz && find ./extensions -mindepth 1 -maxdepth 1 -type d -print0 | xargs -0 -n 1 -P 4 -I {} sh -c 'cd {} && npm install && npm run build:publish'",
"build:extensions:darwin": "rimraf ./electron/pre-install/*.tgz && find ./extensions -mindepth 1 -maxdepth 1 -type d -print0 | xargs -0 -n 1 -P 4 -I {} sh -c 'cd {} && npm install && npm run build:publish'",
"build:extensions": "run-script-os",
"build:test": "yarn build:web && yarn workspace jan build:test",
"build:test": "yarn copy:assets && yarn build:web && yarn workspace jan build:test",
"build": "yarn build:web && yarn build:electron",
"build:publish": "cpx \"models/**\" \"electron/models/\" && yarn build:web && yarn workspace jan build:publish"
"build:publish": "yarn copy:assets && yarn build:web && yarn workspace jan build:publish"
},
"devDependencies": {
"concurrently": "^8.2.1",

View File

@ -1,60 +1,78 @@
import fastify from "fastify";
import dotenv from "dotenv";
import { v1Router } from "@janhq/core/node";
import { log, v1Router } from "@janhq/core/node";
import path from "path";
import os from "os";
dotenv.config();
const JAN_API_HOST = process.env.JAN_API_HOST || "0.0.0.0";
const JAN_API_HOST = process.env.JAN_API_HOST || "127.0.0.1";
const JAN_API_PORT = Number.parseInt(process.env.JAN_API_PORT || "1337");
const serverLogPath = path.join(os.homedir(), "jan", "server.log");
const server = fastify();
server.register(require("@fastify/cors"), {});
server.register(require("@fastify/swagger"), {
mode: "static",
specification: {
path: "./../docs/openapi/jan.yaml",
baseDir: "./../docs/openapi",
},
});
server.register(require("@fastify/swagger-ui"), {
routePrefix: "/docs",
baseDir: path.join(__dirname, "../..", "./docs/openapi"),
uiConfig: {
docExpansion: "full",
deepLinking: false,
},
staticCSP: true,
transformSpecificationClone: true,
});
server.register(
(childContext, _, done) => {
childContext.register(require("@fastify/static"), {
root:
process.env.EXTENSION_ROOT ||
path.join(require("os").homedir(), "jan", "extensions"),
wildcard: false,
let server: any | undefined = undefined;
export const startServer = async (schemaPath?: string, baseDir?: string) => {
try {
server = fastify({
logger: {
level: "info",
file: serverLogPath,
},
});
await server.register(require("@fastify/cors"), {});
await server.register(require("@fastify/swagger"), {
mode: "static",
specification: {
path: schemaPath ?? "./../docs/openapi/jan.yaml",
baseDir: baseDir ?? "./../docs/openapi",
},
});
done();
},
{ prefix: "extensions" }
);
server.register(v1Router, { prefix: "/v1" });
export const startServer = () => {
server
.listen({
port: JAN_API_PORT,
host: JAN_API_HOST,
})
.then(() => {
console.log(
`JAN API listening at: http://${JAN_API_HOST}:${JAN_API_PORT}`
);
await server.register(require("@fastify/swagger-ui"), {
routePrefix: "/",
baseDir: baseDir ?? path.join(__dirname, "../..", "./docs/openapi"),
uiConfig: {
docExpansion: "full",
deepLinking: false,
},
staticCSP: false,
transformSpecificationClone: true,
});
await server.register(
(childContext: any, _: any, done: any) => {
childContext.register(require("@fastify/static"), {
root:
process.env.EXTENSION_ROOT ||
path.join(require("os").homedir(), "jan", "extensions"),
wildcard: false,
});
done();
},
{ prefix: "extensions" }
);
await server.register(v1Router, { prefix: "/v1" });
await server
.listen({
port: JAN_API_PORT,
host: JAN_API_HOST,
})
.then(() => {
log(`JAN API listening at: http://${JAN_API_HOST}:${JAN_API_PORT}`);
});
} catch (e) {
log(e);
}
};
export const stopServer = () => {
server.close();
export const stopServer = async () => {
try {
await server.close();
} catch (e) {
log(e);
}
};

View File

@ -8,6 +8,8 @@ const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root
const TooltipPortal = TooltipPrimitive.Portal
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
@ -37,4 +39,5 @@ export {
TooltipContent,
TooltipProvider,
TooltipArrow,
TooltipPortal,
}

View File

@ -1,31 +1,40 @@
import { ReactNode, useState } from 'react'
import { useAtomValue } from 'jotai'
import {
ChevronDownIcon,
MoreVerticalIcon,
FolderOpenIcon,
Code2Icon,
PencilIcon,
} from 'lucide-react'
import { twMerge } from 'tailwind-merge'
import { useActiveModel } from '@/hooks/useActiveModel'
import { useClickOutside } from '@/hooks/useClickOutside'
import { activeThreadAtom } from '@/helpers/atoms/Thread.atom'
interface Props {
children: ReactNode
title: string
onRevealInFinderClick: (type: string) => void
onViewJsonClick: (type: string) => void
onRevealInFinderClick?: (type: string) => void
onViewJsonClick?: (type: string) => void
asChild?: boolean
}
export default function CardSidebar({
children,
title,
onRevealInFinderClick,
onViewJsonClick,
asChild,
}: Props) {
const [show, setShow] = useState(true)
const [more, setMore] = useState(false)
const [menu, setMenu] = useState<HTMLDivElement | null>(null)
const [toggle, setToggle] = useState<HTMLDivElement | null>(null)
const { activeModel } = useActiveModel()
const activeThread = useAtomValue(activeThreadAtom)
useClickOutside(() => setMore(false), null, [menu, toggle])
@ -39,68 +48,127 @@ export default function CardSidebar({
return (
<div
className={twMerge(
'flex w-full flex-col overflow-hidden rounded-md border border-border bg-zinc-200 dark:bg-zinc-600/10'
'flex w-full flex-col border-t border-border bg-zinc-100 dark:bg-zinc-900',
asChild ? 'rounded-lg border' : 'border-t'
)}
>
<div
className={twMerge(
'relative flex items-center rounded-t-md ',
'relative flex items-center justify-between pl-4',
show && 'border-b border-border'
)}
>
<button
onClick={() => setShow(!show)}
className="flex w-full flex-1 items-center space-x-2 bg-zinc-200 px-3 py-2 dark:bg-zinc-600/10"
>
<ChevronDownIcon
className={twMerge(
'h-5 w-5 flex-none text-gray-400',
show && 'rotate-180'
)}
/>
<span className="font-bold">{title}</span>
</button>
<div
ref={setToggle}
className="cursor-pointer rounded-md bg-zinc-200 p-2 dark:bg-zinc-600/10"
onClick={() => setMore(!more)}
>
<MoreVerticalIcon className="h-5 w-5" />
<span className="font-bold">{title}</span>
<div className="flex">
{!asChild && (
<div
ref={setToggle}
className="cursor-pointer rounded-lg bg-zinc-100 p-2 pr-0 dark:bg-zinc-900"
onClick={() => setMore(!more)}
>
<MoreVerticalIcon className="h-5 w-5" />
</div>
)}
<button
onClick={() => setShow(!show)}
className="flex w-full flex-1 items-center space-x-2 rounded-lg bg-zinc-100 px-3 py-2 dark:bg-zinc-900"
>
<ChevronDownIcon
className={twMerge(
'h-5 w-5 flex-none text-gray-400',
show && 'rotate-180'
)}
/>
</button>
</div>
{more && (
<div
className="absolute right-0 top-8 z-20 w-52 overflow-hidden rounded-lg border border-border bg-background shadow-lg"
className="absolute right-4 top-8 z-20 w-72 rounded-lg border border-border bg-background shadow-lg"
ref={setMenu}
>
<div
className="flex cursor-pointer items-center space-x-2 px-4 py-2 hover:bg-secondary"
className={twMerge(
'flex cursor-pointer space-x-2 px-4 py-2 hover:bg-secondary',
title === 'Model' ? 'items-start' : 'items-center'
)}
onClick={() => {
onRevealInFinderClick(title)
onRevealInFinderClick && onRevealInFinderClick(title)
setMore(false)
}}
>
<FolderOpenIcon size={16} className="text-muted-foreground" />
<span className="text-bold text-black dark:text-muted-foreground">
{openFolderTitle}
</span>
<FolderOpenIcon
size={16}
className={twMerge(
'flex-shrink-0 text-muted-foreground',
title === 'Model' && 'mt-1'
)}
/>
<>
{title === 'Model' ? (
<div className="flex flex-col">
<span className="font-medium text-black dark:text-muted-foreground">
Show in Finder
</span>
<span className="mt-1 text-muted-foreground">
Opens thread.json. Changes affect this thread only.
</span>
</div>
) : (
<span className="text-bold text-black dark:text-muted-foreground">
Show in Finder
</span>
)}
</>
</div>
<div
className="flex cursor-pointer items-center space-x-2 px-4 py-2 hover:bg-secondary"
className="flex cursor-pointer items-start space-x-2 px-4 py-2 hover:bg-secondary"
onClick={() => {
onViewJsonClick(title)
onViewJsonClick && onViewJsonClick(title)
setMore(false)
}}
>
<Code2Icon size={16} className="text-muted-foreground" />
<span className="text-bold text-black dark:text-muted-foreground">
View as JSON
</span>
<PencilIcon
size={16}
className="mt-0.5 flex-shrink-0 text-muted-foreground"
/>
<>
<div className="flex flex-col">
<span className="line-clamp-1 font-medium text-black dark:text-muted-foreground">
Edit Global Defaults for{' '}
<span
className="font-bold"
title={activeThread?.assistants[0].model.id}
>
{activeThread?.assistants[0].model.id}
</span>
</span>
<span className="mt-1 text-muted-foreground">
{title === 'Model' ? (
<>
Opens <span className="lowercase">{title}.json.</span>
&nbsp;Changes affect all new assistants and threads.
</>
) : (
<>
Opens <span className="lowercase">{title}.json.</span>
&nbsp;Changes affect all new threads.
</>
)}
</span>
</div>
</>
</div>
</div>
)}
</div>
{show && (
<div className="flex flex-col gap-2 bg-white p-2 dark:bg-background">
<div
className={twMerge(
'flex flex-col gap-2 bg-white px-2 dark:bg-background',
asChild && 'rounded-b-lg'
)}
>
{children}
</div>
)}

View File

@ -1,32 +1,79 @@
import React from 'react'
import { Switch } from '@janhq/uikit'
import {
Switch,
Tooltip,
TooltipArrow,
TooltipContent,
TooltipPortal,
TooltipTrigger,
} from '@janhq/uikit'
import { useAtomValue } from 'jotai'
import { useAtomValue, useSetAtom } from 'jotai'
import { InfoIcon } from 'lucide-react'
import { useActiveModel } from '@/hooks/useActiveModel'
import useUpdateModelParameters from '@/hooks/useUpdateModelParameters'
import { getActiveThreadIdAtom } from '@/helpers/atoms/Thread.atom'
import { getConfigurationsData } from '@/utils/componentSettings'
import { toSettingParams } from '@/utils/model_param'
import {
engineParamsUpdateAtom,
getActiveThreadIdAtom,
getActiveThreadModelParamsAtom,
} from '@/helpers/atoms/Thread.atom'
type Props = {
name: string
title: string
description: string
checked: boolean
}
const Checkbox: React.FC<Props> = ({ name, title, checked }) => {
const Checkbox: React.FC<Props> = ({ name, title, checked, description }) => {
const { updateModelParameter } = useUpdateModelParameters()
const threadId = useAtomValue(getActiveThreadIdAtom)
const activeModelParams = useAtomValue(getActiveThreadModelParamsAtom)
const modelSettingParams = toSettingParams(activeModelParams)
const engineParams = getConfigurationsData(modelSettingParams)
const setEngineParamsUpdate = useSetAtom(engineParamsUpdateAtom)
const { stopModel } = useActiveModel()
const onCheckedChange = (checked: boolean) => {
if (!threadId) return
if (engineParams.some((x) => x.name.includes(name))) {
setEngineParamsUpdate(true)
stopModel()
} else {
setEngineParamsUpdate(false)
}
updateModelParameter(threadId, name, checked)
}
return (
<div className="flex justify-between">
<p className="mb-2 text-sm font-semibold text-gray-600">{title}</p>
<div className="mb-1 flex items-center gap-x-2">
<p className="text-sm font-semibold text-zinc-500 dark:text-gray-300">
{title}
</p>
<Tooltip>
<TooltipTrigger asChild>
<InfoIcon size={16} className="flex-shrink-0 dark:text-gray-500" />
</TooltipTrigger>
<TooltipPortal>
<TooltipContent side="top" className="max-w-[240px]">
<span>{description}</span>
<TooltipArrow />
</TooltipContent>
</TooltipPortal>
</Tooltip>
</div>
<Switch checked={checked} onCheckedChange={onCheckedChange} />
</div>
)

View File

@ -26,7 +26,7 @@ import { useMainViewState } from '@/hooks/useMainViewState'
import useRecommendedModel from '@/hooks/useRecommendedModel'
import { toGigabytes } from '@/utils/converter'
import { toGibibytes } from '@/utils/converter'
import {
activeThreadAtom,
@ -130,7 +130,7 @@ export default function DropdownListSidebar() {
<div className="flex w-full justify-between">
<span className="line-clamp-1 block">{x.name}</span>
<span className="font-bold text-muted-foreground">
{toGigabytes(x.metadata.size)}
{toGibibytes(x.metadata.size)}
</span>
</div>
</SelectItem>

View File

@ -9,7 +9,7 @@ export default function SystemItem({ name, value }: Props) {
return (
<div className="flex items-center gap-x-1">
<p className="text-xs">{name}</p>
<span className="text-xs">{value}</span>
<span className="text-xs font-bold">{value}</span>
</div>
)
}

View File

@ -1,6 +1,15 @@
import { Badge, Button } from '@janhq/uikit'
import {
Badge,
Button,
Tooltip,
TooltipArrow,
TooltipContent,
TooltipTrigger,
} from '@janhq/uikit'
import { useAtomValue } from 'jotai'
import { FaGithub, FaDiscord } from 'react-icons/fa'
import DownloadingState from '@/containers/Layout/BottomBar/DownloadingState'
import SystemItem from '@/containers/Layout/BottomBar/SystemItem'
@ -15,7 +24,6 @@ import { MainViewState } from '@/constants/screens'
import { useActiveModel } from '@/hooks/useActiveModel'
import { useDownloadState } from '@/hooks/useDownloadState'
import { useGetAppVersion } from '@/hooks/useGetAppVersion'
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
import useGetSystemResources from '@/hooks/useGetSystemResources'
import { useMainViewState } from '@/hooks/useMainViewState'
@ -24,11 +32,23 @@ const BottomBar = () => {
const { activeModel, stateModel } = useActiveModel()
const { ram, cpu } = useGetSystemResources()
const progress = useAtomValue(appDownloadProgress)
const appVersion = useGetAppVersion()
const { downloadedModels } = useGetDownloadedModels()
const { setMainViewState } = useMainViewState()
const { downloadStates } = useDownloadState()
const linksMenu = [
{
name: 'Discord',
icon: <FaDiscord size={20} className="flex-shrink-0" />,
link: 'https://discord.gg/FTk2MvZwJH',
},
{
name: 'Github',
icon: <FaGithub size={16} className="flex-shrink-0" />,
link: 'https://github.com/janhq/jan',
},
]
return (
<div className="fixed bottom-0 left-16 z-20 flex h-12 w-[calc(100%-64px)] items-center justify-between border-t border-border bg-background/80 px-3">
<div className="flex flex-shrink-0 items-center gap-x-2">
@ -71,12 +91,39 @@ const BottomBar = () => {
<DownloadingState />
</div>
<div className="flex gap-x-2">
<SystemItem name="CPU:" value={`${cpu}%`} />
<SystemItem name="Mem:" value={`${ram}%`} />
<span className="text-xs font-semibold ">
Jan v{appVersion?.version ?? ''}
</span>
<div className="flex items-center gap-x-4">
<div className="flex items-center gap-x-2">
<SystemItem name="CPU:" value={`${cpu}%`} />
<SystemItem name="Mem:" value={`${ram}%`} />
</div>
{/* VERSION is defined by webpack, please see next.config.js */}
<span className="text-xs">Jan v{VERSION ?? ''}</span>
<div className="mt-1 flex items-center gap-x-2">
{linksMenu
.filter((link) => !!link)
.map((link, i) => {
return (
<div className="relative" key={i}>
<Tooltip>
<TooltipTrigger>
<a
href={link.link}
target="_blank"
rel="noopener noreferrer"
className="relative flex w-full flex-shrink-0 cursor-pointer items-center justify-center"
>
{link.icon}
</a>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={10}>
<span>{link.name}</span>
<TooltipArrow />
</TooltipContent>
</Tooltip>
</div>
)
})}
</div>
</div>
</div>
)

View File

@ -74,6 +74,7 @@ export default function RibbonNav() {
state: MainViewState.Settings,
},
]
return (
<div className="relative top-12 flex h-[calc(100%-48px)] w-16 flex-shrink-0 flex-col border-r border-border bg-background py-4">
<div className="mt-2 flex h-full w-full flex-col items-center justify-between">

View File

@ -26,7 +26,7 @@ export default function CommandListDownloadedModel() {
const onModelActionClick = (modelId: string) => {
if (activeModel && activeModel.id === modelId) {
stopModel(modelId)
stopModel()
} else {
startModel(modelId)
}

View File

@ -1,18 +1,31 @@
import { useState } from 'react'
import { getUserSpace, joinPath, openFileExplorer } from '@janhq/core'
import { useAtomValue, useSetAtom } from 'jotai'
import { PanelLeftIcon, PenSquareIcon, PanelRightIcon } from 'lucide-react'
import {
PanelLeftIcon,
PenSquareIcon,
PanelRightIcon,
MoreVerticalIcon,
FolderOpenIcon,
Code2Icon,
} from 'lucide-react'
import { twMerge } from 'tailwind-merge'
import CommandListDownloadedModel from '@/containers/Layout/TopBar/CommandListDownloadedModel'
import CommandSearch from '@/containers/Layout/TopBar/CommandSearch'
import { MainViewState } from '@/constants/screens'
import { useClickOutside } from '@/hooks/useClickOutside'
import { useCreateNewThread } from '@/hooks/useCreateNewThread'
import useGetAssistants, { getAssistants } from '@/hooks/useGetAssistants'
import { useMainViewState } from '@/hooks/useMainViewState'
import { showRightSideBarAtom } from '@/screens/Chat/Sidebar'
import { activeThreadAtom } from '@/helpers/atoms/Thread.atom'
import { activeThreadAtom, threadStatesAtom } from '@/helpers/atoms/Thread.atom'
const TopBar = () => {
const activeThread = useAtomValue(activeThreadAtom)
@ -20,6 +33,13 @@ const TopBar = () => {
const { requestCreateNewThread } = useCreateNewThread()
const { assistants } = useGetAssistants()
const setShowRightSideBar = useSetAtom(showRightSideBarAtom)
const showing = useAtomValue(showRightSideBarAtom)
const threadStates = useAtomValue(threadStatesAtom)
const [more, setMore] = useState(false)
const [menu, setMenu] = useState<HTMLDivElement | null>(null)
const [toggle, setToggle] = useState<HTMLDivElement | null>(null)
useClickOutside(() => setMore(false), null, [menu, toggle])
const titleScreen = (viewStateName: MainViewState) => {
switch (viewStateName) {
@ -47,15 +67,45 @@ const TopBar = () => {
}
}
const onReviewInFinderClick = async () => {
if (!activeThread) return
const activeThreadState = threadStates[activeThread.id]
if (!activeThreadState.isFinishInit) {
alert('Thread is not started yet')
return
}
const userSpace = await getUserSpace()
let filePath = undefined
filePath = await joinPath(['threads', activeThread.id])
if (!filePath) return
const fullPath = await joinPath([userSpace, filePath])
openFileExplorer(fullPath)
}
const onViewJsonClick = async () => {
if (!activeThread) return
const activeThreadState = threadStates[activeThread.id]
if (!activeThreadState.isFinishInit) {
alert('Thread is not started yet')
return
}
const userSpace = await getUserSpace()
let filePath = undefined
filePath = await joinPath(['threads', activeThread.id, 'thread.json'])
if (!filePath) return
const fullPath = await joinPath([userSpace, filePath])
openFileExplorer(fullPath)
}
return (
<div className="fixed left-0 top-0 z-50 flex h-12 w-full border-b border-border bg-background/80 backdrop-blur-md">
{mainViewState === MainViewState.Thread && (
<div className="absolute left-16 h-full w-60 border-r border-border" />
)}
<div className="relative left-16 flex w-[calc(100%-64px)] items-center justify-between space-x-4 pl-6 pr-2">
{mainViewState === MainViewState.Thread ? (
<div className="unset-drag flex space-x-8">
<div className="flex w-52 justify-between">
<div className="relative w-full">
<div className="absolute left-16 h-full w-60 border-r border-border">
<div className="flex h-full w-full items-center justify-between">
<div className="cursor-pointer">
<PanelLeftIcon
size={20}
@ -63,34 +113,107 @@ const TopBar = () => {
/>
</div>
<div
className="cursor-pointer pr-2"
className="unset-drag cursor-pointer pr-4"
onClick={onCreateConversationClick}
>
<PenSquareIcon size={20} className="text-muted-foreground" />
</div>
</div>
<span className="text-sm font-bold">
{titleScreen(mainViewState)}
</span>
</div>
<div className="absolute left-80 h-full">
<div className="flex h-full items-center">
<span className="text-sm font-bold">
{titleScreen(mainViewState)}
</span>
</div>
</div>
<div
className={twMerge(
'absolute right-0 h-full w-80',
showing && 'border-l border-border'
)}
>
{activeThread && (
<div
className="unset-drag absolute right-4 cursor-pointer"
onClick={() => setShowRightSideBar((show) => !show)}
>
<PanelRightIcon size={20} className="text-muted-foreground" />
<div className="flex h-full w-52 items-center justify-between px-4">
{showing && (
<div className="relative flex h-full items-center">
<span className="mr-2 text-sm font-bold">
Threads Settings
</span>
<div
ref={setToggle}
className="unset-drag cursor-pointer rounded-md p-2"
onClick={() => setMore(!more)}
>
<MoreVerticalIcon className="h-5 w-5" />
</div>
{more && (
<div
className="absolute right-0 top-11 z-20 w-64 overflow-hidden rounded-lg border border-border bg-background shadow-lg"
ref={setMenu}
>
<div
className="flex cursor-pointer items-center space-x-2 px-4 py-2 hover:bg-secondary"
onClick={() => {
onReviewInFinderClick()
setMore(false)
}}
>
<FolderOpenIcon
size={16}
className="text-muted-foreground"
/>
<span className="font-medium text-black dark:text-muted-foreground">
Show in Finder
</span>
</div>
<div
className="flex cursor-pointer items-start space-x-2 px-4 py-2 hover:bg-secondary"
onClick={() => {
onViewJsonClick()
setMore(false)
}}
>
<Code2Icon
size={16}
className="mt-0.5 flex-shrink-0 text-muted-foreground"
/>
<div className="flex flex-col">
<span className="font-medium text-black dark:text-muted-foreground">
Edit Threads Settings
</span>
<span className="mt-1 text-muted-foreground">
Opens thread.json. Changes affect this thread
only.
</span>
</div>
</div>
</div>
)}
</div>
)}
<div
className="unset-drag absolute right-4 cursor-pointer"
onClick={() => setShowRightSideBar((show) => !show)}
>
<PanelRightIcon size={20} className="text-muted-foreground" />
</div>
</div>
)}
</div>
) : (
<div>
<span className="text-sm font-bold">
{titleScreen(mainViewState)}
</span>
</div>
)}
<CommandSearch />
<CommandListDownloadedModel />
</div>
</div>
)}
{mainViewState !== MainViewState.Thread && (
<div className="relative left-16 flex w-[calc(100%-64px)] items-center justify-between space-x-4 pl-6 pr-2">
<span className="text-sm font-bold">
{titleScreen(mainViewState)}
</span>
</div>
)}
<CommandSearch />
<CommandListDownloadedModel />
</div>
)
}

View File

@ -0,0 +1,49 @@
import React, { useEffect, useState } from 'react'
import { useActiveModel } from '@/hooks/useActiveModel'
export default function ModelReload() {
const { stateModel } = useActiveModel()
const [loader, setLoader] = useState(0)
// This is fake loader please fix this when we have realtime percentage when load model
useEffect(() => {
if (stateModel.loading) {
if (loader === 24) {
setTimeout(() => {
setLoader(loader + 1)
}, 250)
} else if (loader === 50) {
setTimeout(() => {
setLoader(loader + 1)
}, 250)
} else if (loader === 78) {
setTimeout(() => {
setLoader(loader + 1)
}, 250)
} else if (loader === 99) {
setLoader(99)
} else {
setLoader(loader + 1)
}
} else {
setLoader(0)
}
}, [stateModel.loading, loader])
if (!stateModel.loading) return null
return (
<div className=" mb-1 mt-2 py-2 text-center">
<div className="relative inline-block overflow-hidden rounded-lg border border-neutral-50 bg-blue-50 px-4 py-2 font-semibold text-blue-600 shadow-lg">
<div
className="absolute left-0 top-0 h-full bg-blue-200"
style={{ width: `${loader}%` }}
/>
<span className="relative z-10">
Reloading model {stateModel.model}
</span>
</div>
</div>
)
}

View File

@ -1,14 +1,33 @@
import { Textarea } from '@janhq/uikit'
import {
Textarea,
Tooltip,
TooltipArrow,
TooltipContent,
TooltipPortal,
TooltipTrigger,
} from '@janhq/uikit'
import { useAtomValue } from 'jotai'
import { useAtomValue, useSetAtom } from 'jotai'
import { InfoIcon } from 'lucide-react'
import { useActiveModel } from '@/hooks/useActiveModel'
import useUpdateModelParameters from '@/hooks/useUpdateModelParameters'
import { getActiveThreadIdAtom } from '@/helpers/atoms/Thread.atom'
import { getConfigurationsData } from '@/utils/componentSettings'
import { toSettingParams } from '@/utils/model_param'
import {
engineParamsUpdateAtom,
getActiveThreadIdAtom,
getActiveThreadModelParamsAtom,
} from '@/helpers/atoms/Thread.atom'
type Props = {
title: string
name: string
description: string
placeholder: string
value: string
}
@ -17,20 +36,51 @@ const ModelConfigInput: React.FC<Props> = ({
title,
name,
value,
description,
placeholder,
}) => {
const { updateModelParameter } = useUpdateModelParameters()
const threadId = useAtomValue(getActiveThreadIdAtom)
const activeModelParams = useAtomValue(getActiveThreadModelParamsAtom)
const modelSettingParams = toSettingParams(activeModelParams)
const engineParams = getConfigurationsData(modelSettingParams)
const setEngineParamsUpdate = useSetAtom(engineParamsUpdateAtom)
const { stopModel } = useActiveModel()
const onValueChanged = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
if (!threadId) return
if (engineParams.some((x) => x.name.includes(name))) {
setEngineParamsUpdate(true)
stopModel()
} else {
setEngineParamsUpdate(false)
}
updateModelParameter(threadId, name, e.target.value)
}
return (
<div className="flex flex-col">
<p className="mb-2 text-sm font-semibold text-gray-600">{title}</p>
<div className="mb-2 flex items-center gap-x-2">
<p className="text-sm font-semibold text-zinc-500 dark:text-gray-300">
{title}
</p>
<Tooltip>
<TooltipTrigger asChild>
<InfoIcon size={16} className="flex-shrink-0 dark:text-gray-500" />
</TooltipTrigger>
<TooltipPortal>
<TooltipContent side="top" className="max-w-[240px]">
<span>{description}</span>
<TooltipArrow />
</TooltipContent>
</TooltipPortal>
</Tooltip>
</div>
<Textarea
placeholder={placeholder}
onChange={onValueChanged}

View File

@ -68,10 +68,10 @@ export default function EventHandler({ children }: { children: ReactNode }) {
setTimeout(async () => {
setActiveModel(undefined)
setStateModel({ state: 'start', loading: false, model: '' })
toaster({
title: 'Success!',
description: `Model ${model.id} has been stopped.`,
})
// toaster({
// title: 'Success!',
// description: `Model ${model.id} has been stopped.`,
// })
}, 500)
}

View File

@ -52,7 +52,8 @@ export default function EventListenerWrapper({ children }: PropsWithChildren) {
window.electronAPI.onFileDownloadError(
async (_event: string, state: any) => {
console.error('Download error', state)
if (state.err?.message !== 'aborted')
console.error('Download error', state)
const modelName = await baseName(state.fileName)
const model = modelsRef.current.find(
(model) => modelBinFileName(model) === modelName
@ -66,7 +67,7 @@ export default function EventListenerWrapper({ children }: PropsWithChildren) {
if (state && state.fileName) {
const modelName = await baseName(state.fileName)
const model = modelsRef.current.find(
async (model) => modelBinFileName(model) === modelName
(model) => modelBinFileName(model) === modelName
)
if (model) {
setDownloadStateSuccess(model.id)

View File

@ -1,15 +1,34 @@
import React from 'react'
import { Slider, Input } from '@janhq/uikit'
import { useAtomValue } from 'jotai'
import {
Slider,
Input,
Tooltip,
TooltipArrow,
TooltipContent,
TooltipPortal,
TooltipTrigger,
} from '@janhq/uikit'
import { useAtomValue, useSetAtom } from 'jotai'
import { InfoIcon } from 'lucide-react'
import { useActiveModel } from '@/hooks/useActiveModel'
import useUpdateModelParameters from '@/hooks/useUpdateModelParameters'
import { getActiveThreadIdAtom } from '@/helpers/atoms/Thread.atom'
import { getConfigurationsData } from '@/utils/componentSettings'
import { toSettingParams } from '@/utils/model_param'
import {
engineParamsUpdateAtom,
getActiveThreadIdAtom,
getActiveThreadModelParamsAtom,
} from '@/helpers/atoms/Thread.atom'
type Props = {
name: string
title: string
description: string
min: number
max: number
step: number
@ -22,20 +41,51 @@ const SliderRightPanel: React.FC<Props> = ({
min,
max,
step,
description,
value,
}) => {
const { updateModelParameter } = useUpdateModelParameters()
const threadId = useAtomValue(getActiveThreadIdAtom)
const activeModelParams = useAtomValue(getActiveThreadModelParamsAtom)
const modelSettingParams = toSettingParams(activeModelParams)
const engineParams = getConfigurationsData(modelSettingParams)
const setEngineParamsUpdate = useSetAtom(engineParamsUpdateAtom)
const { stopModel } = useActiveModel()
const onValueChanged = (e: number[]) => {
if (!threadId) return
if (engineParams.some((x) => x.name.includes(name))) {
setEngineParamsUpdate(true)
stopModel()
} else {
setEngineParamsUpdate(false)
}
updateModelParameter(threadId, name, e[0])
}
return (
<div className="flex flex-col">
<p className="mb-2 text-sm font-semibold text-gray-600">{title}</p>
<div className="mb-3 flex items-center gap-x-2">
<p className="text-sm font-semibold text-zinc-500 dark:text-gray-300">
{title}
</p>
<Tooltip>
<TooltipTrigger asChild>
<InfoIcon size={16} className="flex-shrink-0 dark:text-gray-500" />
</TooltipTrigger>
<TooltipPortal>
<TooltipContent side="top" className="max-w-[240px]">
<span>{description}</span>
<TooltipArrow />
</TooltipContent>
</TooltipPortal>
</Tooltip>
</div>
<div className="flex items-center gap-x-4">
<div className="relative w-full">
<Slider
@ -58,7 +108,13 @@ const SliderRightPanel: React.FC<Props> = ({
min={min}
max={max}
value={String(value)}
onChange={(e) => onValueChanged([Number(e.target.value)])}
onChange={(e) => {
if (Number(e.target.value) >= max) {
onValueChanged([Number(max)])
} else {
onValueChanged([Number(e.target.value)])
}
}}
/>
</div>
</div>

View File

@ -7,6 +7,8 @@ import {
} from '@janhq/core'
import { atom } from 'jotai'
export const engineParamsUpdateAtom = atom<boolean>(false)
/**
* Stores the current active thread id.
*/

View File

@ -53,10 +53,12 @@ export function useActiveModel() {
events.emit(EventName.OnModelInit, model)
}
const stopModel = async (modelId: string) => {
const model = downloadedModels.find((e) => e.id === modelId)
setStateModel({ state: 'stop', loading: true, model: modelId })
events.emit(EventName.OnModelStop, model)
const stopModel = async () => {
if (activeModel) {
setActiveModel(undefined)
setStateModel({ state: 'stop', loading: true, model: activeModel.id })
events.emit(EventName.OnModelStop, activeModel)
}
}
return { activeModel, startModel, stopModel, stateModel }

View File

@ -33,7 +33,7 @@ const setDownloadStateFailedAtom = atom(null, (get, set, modelId: string) => {
const currentState = { ...get(modelDownloadStateAtom) }
const state = currentState[modelId]
if (!state) {
console.error(`Cannot find download state for ${modelId}`)
console.debug(`Cannot find download state for ${modelId}`)
toaster({
title: 'Cancel Download',
description: `Model ${modelId} cancel download`,

View File

@ -1,17 +0,0 @@
import { useEffect, useState } from 'react'
export function useGetAppVersion() {
const [version, setVersion] = useState<string>('')
useEffect(() => {
getAppVersion()
}, [])
const getAppVersion = () => {
window.core?.api?.appVersion().then((version: string | undefined) => {
setVersion(version ?? '')
})
}
return { version }
}

View File

@ -32,24 +32,26 @@ export default function useGetSystemResources() {
const currentLoadInfor = await monitoring?.getCurrentLoad()
const ram =
(resourceInfor?.mem?.active ?? 0) / (resourceInfor?.mem?.total ?? 1)
if (resourceInfor?.mem?.active) setUsedRam(resourceInfor.mem.active)
if (resourceInfor?.mem?.total) setTotalRam(resourceInfor.mem.total)
(resourceInfor?.mem?.usedMemory ?? 0) /
(resourceInfor?.mem?.totalMemory ?? 1)
if (resourceInfor?.mem?.usedMemory) setUsedRam(resourceInfor.mem.usedMemory)
if (resourceInfor?.mem?.totalMemory)
setTotalRam(resourceInfor.mem.totalMemory)
setRam(Math.round(ram * 100))
setCPU(Math.round(currentLoadInfor?.currentLoad ?? 0))
setCpuUsage(Math.round(currentLoadInfor?.currentLoad ?? 0))
setCPU(Math.round(currentLoadInfor?.cpu?.usage ?? 0))
setCpuUsage(Math.round(currentLoadInfor?.cpu?.usage ?? 0))
}
useEffect(() => {
getSystemResources()
// Fetch interval - every 5s
// Fetch interval - every 0.5s
// TODO: Will we really need this?
// There is a possibility that this will be removed and replaced by the process event hook?
const intervalId = setInterval(() => {
getSystemResources()
}, 5000)
}, 500)
// clean up interval
return () => clearInterval(intervalId)

View File

@ -34,6 +34,7 @@ import {
} from '@/helpers/atoms/ChatMessage.atom'
import {
activeThreadAtom,
engineParamsUpdateAtom,
getActiveThreadModelParamsAtom,
threadStatesAtom,
updateThreadAtom,
@ -59,6 +60,11 @@ export default function useSendChatMessage() {
const updateThreadInitSuccess = useSetAtom(updateThreadInitSuccessAtom)
const activeModelParams = useAtomValue(getActiveThreadModelParamsAtom)
const engineParamsUpdate = useAtomValue(engineParamsUpdateAtom)
const setEngineParamsUpdate = useSetAtom(engineParamsUpdateAtom)
const [reloadModel, setReloadModel] = useState(false)
useEffect(() => {
modelRef.current = activeModel
}, [activeModel])
@ -135,8 +141,10 @@ export default function useSendChatMessage() {
console.error('No active thread')
return
}
const activeThreadState = threadStates[activeThread.id]
if (engineParamsUpdate) setReloadModel(true)
const activeThreadState = threadStates[activeThread.id]
const runtimeParams = toRuntimeParams(activeModelParams)
const settingParams = toSettingParams(activeModelParams)
@ -256,10 +264,15 @@ export default function useSendChatMessage() {
await WaitForModelStarting(modelId)
setQueuedMessage(false)
}
events.emit(EventName.OnMessageSent, messageRequest)
setReloadModel(false)
setEngineParamsUpdate(false)
}
return {
reloadModel,
sendChatMessage,
resendChatMessage,
queuedMessage,

View File

@ -34,6 +34,7 @@
"react-dom": "18.2.0",
"react-hook-form": "^7.47.0",
"react-hot-toast": "^2.4.1",
"react-icons": "^4.12.0",
"react-scroll-to-bottom": "^4.2.0",
"react-toastify": "^9.1.3",
"sass": "^1.69.4",
@ -49,6 +50,7 @@
"@types/node": "20.8.10",
"@types/react": "18.2.34",
"@types/react-dom": "18.2.14",
"@types/react-icons": "^3.0.0",
"@types/react-scroll-to-bottom": "^4.2.4",
"@types/uuid": "^9.0.6",
"@typescript-eslint/eslint-plugin": "^6.8.0",

View File

@ -118,7 +118,6 @@ const ChatBody: React.FC = () => {
{messages.map((message, index) => (
<div key={message.id}>
<ChatItem {...message} key={message.id} />
{message.status === MessageStatus.Error &&
index === messages.length - 1 && (
<div

View File

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useAtomValue } from 'jotai'
import { selectedModelAtom } from '@/containers/DropdownListSidebar'
@ -9,7 +10,7 @@ import settingComponentBuilder from '../ModelSetting/settingComponentBuilder'
import { getActiveThreadModelParamsAtom } from '@/helpers/atoms/Thread.atom'
const EngineSetting: React.FC = () => {
const EngineSetting = () => {
const activeModelParams = useAtomValue(getActiveThreadModelParamsAtom)
const selectedModel = useAtomValue(selectedModelAtom)
@ -22,9 +23,9 @@ const EngineSetting: React.FC = () => {
componentData.sort((a, b) => a.title.localeCompare(b.title))
return (
<form className="flex flex-col">
<div className="flex flex-col">
{settingComponentBuilder(componentData)}
</form>
</div>
)
}

View File

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import React from 'react'
import { useAtomValue } from 'jotai'
@ -11,7 +12,7 @@ import settingComponentBuilder from './settingComponentBuilder'
import { getActiveThreadModelParamsAtom } from '@/helpers/atoms/Thread.atom'
const ModelSetting: React.FC = () => {
const ModelSetting = () => {
const activeModelParams = useAtomValue(getActiveThreadModelParamsAtom)
const selectedModel = useAtomValue(selectedModelAtom)
@ -24,9 +25,9 @@ const ModelSetting: React.FC = () => {
componentData.sort((a, b) => a.title.localeCompare(b.title))
return (
<form className="flex flex-col">
<div className="flex flex-col">
{settingComponentBuilder(componentData)}
</form>
</div>
)
}

View File

@ -4,7 +4,7 @@ export const presetConfiguration: Record<string, SettingComponentData> = {
prompt_template: {
name: 'prompt_template',
title: 'Prompt template',
description: 'Prompt template',
description: 'The prompt to use for internal configuration.',
controllerType: 'input',
controllerData: {
placeholder: 'Prompt template',
@ -14,7 +14,8 @@ export const presetConfiguration: Record<string, SettingComponentData> = {
stop: {
name: 'stop',
title: 'Stop',
description: 'Stop',
description:
'Defines specific tokens or phrases at which the model will stop generating further output. ',
controllerType: 'input',
controllerData: {
placeholder: 'Stop',
@ -24,7 +25,8 @@ export const presetConfiguration: Record<string, SettingComponentData> = {
ctx_len: {
name: 'ctx_len',
title: 'Context Length',
description: 'Context Length',
description:
'The context length for model operations varies; the maximum depends on the specific model used.',
controllerType: 'slider',
controllerData: {
min: 0,
@ -40,7 +42,7 @@ export const presetConfiguration: Record<string, SettingComponentData> = {
'The maximum number of tokens the model will generate in a single response.',
controllerType: 'slider',
controllerData: {
min: 0,
min: 128,
max: 4096,
step: 128,
value: 2048,
@ -48,8 +50,8 @@ export const presetConfiguration: Record<string, SettingComponentData> = {
},
ngl: {
name: 'ngl',
title: 'NGL',
description: 'Number of layers in the neural network.',
title: 'Number of GPU layers (ngl)',
description: 'The number of layers to load onto the GPU for acceleration.',
controllerType: 'slider',
controllerData: {
min: 1,
@ -61,7 +63,7 @@ export const presetConfiguration: Record<string, SettingComponentData> = {
embedding: {
name: 'embedding',
title: 'Embedding',
description: 'Indicates if embedding layers are used.',
description: 'Whether to enable embedding.',
controllerType: 'checkbox',
controllerData: {
checked: true,
@ -79,8 +81,7 @@ export const presetConfiguration: Record<string, SettingComponentData> = {
temperature: {
name: 'temperature',
title: 'Temperature',
description:
"Controls randomness in model's responses. Higher values lead to more random responses.",
description: 'Controls the randomness of the models output.',
controllerType: 'slider',
controllerData: {
min: 0,
@ -92,7 +93,8 @@ export const presetConfiguration: Record<string, SettingComponentData> = {
frequency_penalty: {
name: 'frequency_penalty',
title: 'Frequency Penalty',
description: 'Frequency Penalty',
description:
'Adjusts the likelihood of the model repeating words or phrases in its output. ',
controllerType: 'slider',
controllerData: {
min: 0,
@ -104,7 +106,8 @@ export const presetConfiguration: Record<string, SettingComponentData> = {
presence_penalty: {
name: 'presence_penalty',
title: 'Presence Penalty',
description: 'Presence Penalty',
description:
'Influences the generation of new and varied concepts in the models output. ',
controllerType: 'slider',
controllerData: {
min: 0,
@ -116,7 +119,7 @@ export const presetConfiguration: Record<string, SettingComponentData> = {
top_p: {
name: 'top_p',
title: 'Top P',
description: 'Top P',
description: 'Set probability threshold for more relevant outputs.',
controllerType: 'slider',
controllerData: {
min: 0,
@ -128,10 +131,11 @@ export const presetConfiguration: Record<string, SettingComponentData> = {
n_parallel: {
name: 'n_parallel',
title: 'N Parallel',
description: 'N Parallel',
description:
'The number of parallel operations. Only set when enable continuous batching. ',
controllerType: 'slider',
controllerData: {
min: 1,
min: 0,
max: 4,
step: 1,
value: 1,

View File

@ -21,6 +21,7 @@ export type InputData = {
export type SliderData = {
min: number
max: number
step: number
value: number
}
@ -29,48 +30,58 @@ type CheckboxData = {
checked: boolean
}
const settingComponentBuilder = (componentData: SettingComponentData[]) => {
const components = componentData.map((data) => {
switch (data.controllerType) {
case 'slider':
const { min, max, step, value } = data.controllerData as SliderData
return (
<Slider
key={data.name}
title={data.title}
min={min}
max={max}
step={step}
value={value}
name={data.name}
/>
)
case 'input':
const { placeholder, value: textValue } =
data.controllerData as InputData
return (
<ModelConfigInput
title={data.title}
key={data.name}
name={data.name}
placeholder={placeholder}
value={textValue}
/>
)
case 'checkbox':
const { checked } = data.controllerData as CheckboxData
return (
<Checkbox
key={data.name}
name={data.name}
title={data.title}
checked={checked}
/>
)
default:
return null
}
})
const settingComponentBuilder = (
componentData: SettingComponentData[],
onlyPrompt?: boolean
) => {
const components = componentData
.filter((x) =>
onlyPrompt ? x.name === 'prompt_template' : x.name !== 'prompt_template'
)
.map((data) => {
switch (data.controllerType) {
case 'slider':
const { min, max, step, value } = data.controllerData as SliderData
return (
<Slider
key={data.name}
title={data.title}
description={data.description}
min={min}
max={max}
step={step}
value={value}
name={data.name}
/>
)
case 'input':
const { placeholder, value: textValue } =
data.controllerData as InputData
return (
<ModelConfigInput
title={data.title}
key={data.name}
name={data.name}
description={data.description}
placeholder={placeholder}
value={textValue}
/>
)
case 'checkbox':
const { checked } = data.controllerData as CheckboxData
return (
<Checkbox
key={data.name}
name={data.name}
description={data.description}
title={data.title}
checked={checked}
/>
)
default:
return null
}
})
return <div className="flex flex-col gap-y-4">{components}</div>
}

View File

@ -1,4 +1,5 @@
import React, { useContext } from 'react'
/* eslint-disable @typescript-eslint/no-explicit-any */
import React from 'react'
import { getUserSpace, openFileExplorer, joinPath } from '@janhq/core'
@ -10,19 +11,21 @@ import { twMerge } from 'tailwind-merge'
import LogoMark from '@/containers/Brand/Logo/Mark'
import CardSidebar from '@/containers/CardSidebar'
import DropdownListSidebar, {
selectedModelAtom,
} from '@/containers/DropdownListSidebar'
import { FeatureToggleContext } from '@/context/FeatureToggle'
import { useCreateNewThread } from '@/hooks/useCreateNewThread'
import { getConfigurationsData } from '@/utils/componentSettings'
import { toSettingParams } from '@/utils/model_param'
import EngineSetting from '../EngineSetting'
import ModelSetting from '../ModelSetting'
import settingComponentBuilder from '../ModelSetting/settingComponentBuilder'
import {
activeThreadAtom,
getActiveThreadModelParamsAtom,
@ -34,13 +37,14 @@ export const showRightSideBarAtom = atom<boolean>(true)
const Sidebar: React.FC = () => {
const showing = useAtomValue(showRightSideBarAtom)
const activeThread = useAtomValue(activeThreadAtom)
const activeModelParams = useAtomValue(getActiveThreadModelParamsAtom)
const selectedModel = useAtomValue(selectedModelAtom)
const { updateThreadMetadata } = useCreateNewThread()
const threadStates = useAtomValue(threadStatesAtom)
const { experimentalFeatureEnabed } = useContext(FeatureToggleContext)
const activeModelParams = useAtomValue(getActiveThreadModelParamsAtom)
const modelSettingParams = toSettingParams(activeModelParams)
const threadStates = useAtomValue(threadStatesAtom)
const modelEngineParams = toSettingParams(activeModelParams)
const componentDataEngineSetting = getConfigurationsData(modelEngineParams)
const onReviewInFinderClick = async (type: string) => {
if (!activeThread) return
@ -119,48 +123,43 @@ const Sidebar: React.FC = () => {
>
<div
className={twMerge(
'flex flex-col gap-4 p-4 delay-200',
'flex flex-col gap-1 delay-200',
showing ? 'animate-enter opacity-100' : 'opacity-0'
)}
>
<CardSidebar
title="Thread"
onRevealInFinderClick={onReviewInFinderClick}
onViewJsonClick={onViewJsonClick}
>
<div className="flex flex-col space-y-4 p-2">
<div>
<label
id="thread-title"
className="mb-2 inline-block font-bold text-gray-600 dark:text-gray-300"
>
Title
</label>
<Input
id="thread-title"
value={activeThread?.title}
onChange={(e) => {
if (activeThread)
updateThreadMetadata({
...activeThread,
title: e.target.value || '',
})
}}
/>
</div>
<div className="flex flex-col">
<label
id="thread-title"
className="mb-2 inline-block font-bold text-gray-600 dark:text-gray-300"
>
Threads ID
</label>
<span className="text-xs text-muted-foreground">
{activeThread?.id || '-'}
</span>
</div>
<div className="flex flex-col space-y-4 p-4">
<div>
<label
id="thread-title"
className="mb-2 inline-block font-bold text-gray-600 dark:text-gray-300"
>
Title
</label>
<Input
id="thread-title"
value={activeThread?.title}
onChange={(e) => {
if (activeThread)
updateThreadMetadata({
...activeThread,
title: e.target.value || '',
})
}}
/>
</div>
</CardSidebar>
<div className="flex flex-col">
<label
id="thread-title"
className="mb-2 inline-block font-bold text-zinc-500 dark:text-gray-300"
>
Threads ID
</label>
<span className="text-xs text-muted-foreground">
{activeThread?.id || '-'}
</span>
</div>
</div>
<CardSidebar
title="Assistant"
onRevealInFinderClick={onReviewInFinderClick}
@ -176,7 +175,7 @@ const Sidebar: React.FC = () => {
<div>
<label
id="thread-title"
className="mb-2 inline-block font-bold text-gray-600 dark:text-gray-300"
className="mb-2 inline-block font-bold text-zinc-500 dark:text-gray-300"
>
Instructions
</label>
@ -198,28 +197,60 @@ const Sidebar: React.FC = () => {
}}
/>
</div>
{/* Temporary disabled */}
{/* <div>
<label
id="tool-title"
className="mb-2 inline-block font-bold text-zinc-500 dark:text-gray-300"
>
Tools
</label>
<div className="flex items-center justify-between">
<label className="font-medium text-zinc-500 dark:text-gray-300">
Retrieval
</label>
<Switch name="retrieval" />
</div>
</div> */}
</div>
</CardSidebar>
{experimentalFeatureEnabed && Object.keys(modelSettingParams).length ? (
<CardSidebar
title="Engine"
onRevealInFinderClick={onReviewInFinderClick}
onViewJsonClick={onViewJsonClick}
>
<div className="p-2">
<EngineSetting />
</div>
</CardSidebar>
) : null}
<CardSidebar
title="Model"
onRevealInFinderClick={onReviewInFinderClick}
onViewJsonClick={onViewJsonClick}
>
<div className="p-2">
<DropdownListSidebar />
<div className="px-2">
<div className="mt-4">
<ModelSetting />
<DropdownListSidebar />
</div>
<div className="mt-6">
<CardSidebar title="Inference Parameters" asChild>
<div className="px-2 py-4">
<ModelSetting />
</div>
</CardSidebar>
</div>
<div className="mt-4">
<CardSidebar title="Model Parameters" asChild>
<div className="px-2 py-4">
{settingComponentBuilder(componentDataEngineSetting, true)}
</div>
</CardSidebar>
</div>
<div className="my-4">
<CardSidebar
title="Engine Parameters"
onRevealInFinderClick={onReviewInFinderClick}
onViewJsonClick={onViewJsonClick}
asChild
>
<div className="px-2 py-4">
<EngineSetting />
</div>
</CardSidebar>
</div>
</div>
</CardSidebar>

View File

@ -11,6 +11,7 @@ import { twMerge } from 'tailwind-merge'
import LogoMark from '@/containers/Brand/Logo/Mark'
import ModelReload from '@/containers/Loader/ModelReload'
import ModelStart from '@/containers/Loader/ModelStart'
import { currentPromptAtom } from '@/containers/Providers/Jotai'
@ -30,8 +31,10 @@ import ThreadList from '@/screens/Chat/ThreadList'
import Sidebar, { showRightSideBarAtom } from './Sidebar'
import { getCurrentChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
import {
activeThreadAtom,
engineParamsUpdateAtom,
getActiveThreadIdAtom,
waitingToSendMessage,
} from '@/helpers/atoms/Thread.atom'
@ -48,9 +51,10 @@ const ChatScreen = () => {
const [currentPrompt, setCurrentPrompt] = useAtom(currentPromptAtom)
const activeThreadState = useAtomValue(activeThreadStateAtom)
const { sendChatMessage, queuedMessage } = useSendChatMessage()
const { sendChatMessage, queuedMessage, reloadModel } = useSendChatMessage()
const isWaitingForResponse = activeThreadState?.waitingForResponse ?? false
const disabled = currentPrompt.trim().length === 0 || isWaitingForResponse
const isDisabledChatbox =
currentPrompt.trim().length === 0 || isWaitingForResponse
const activeThreadId = useAtomValue(getActiveThreadIdAtom)
const [isWaitingToSend, setIsWaitingToSend] = useAtom(waitingToSendMessage)
@ -59,6 +63,7 @@ const ChatScreen = () => {
const textareaRef = useRef<HTMLTextAreaElement>(null)
const modelRef = useRef(activeModel)
const engineParamsUpdate = useAtomValue(engineParamsUpdateAtom)
useEffect(() => {
modelRef.current = activeModel
@ -144,18 +149,30 @@ const ChatScreen = () => {
</div>
)}
<ModelStart />
{!engineParamsUpdate && <ModelStart />}
{queuedMessage && (
<div className="my-2 py-2 text-center">
<span className="rounded-lg border border-border px-4 py-2 shadow-lg">
{reloadModel && (
<>
<ModelReload />
<div className="mb-2 text-center">
<span className="text-muted-foreground">
Model is reloading to apply new changes.
</span>
</div>
</>
)}
{queuedMessage && !reloadModel && (
<div className="mb-2 text-center">
<span className="text-muted-foreground">
Message queued. It can be sent once the model has started
</span>
</div>
)}
<div className="mx-auto flex w-full flex-shrink-0 items-end justify-center space-x-4 px-8 py-4">
<Textarea
className="max-h-[400px] resize-none overflow-y-hidden pr-20"
className="max-h-[400px] resize-none overflow-y-auto pr-20"
style={{ height: '40px' }}
ref={textareaRef}
onKeyDown={(e: KeyboardEvent<HTMLTextAreaElement>) =>
@ -171,7 +188,9 @@ const ChatScreen = () => {
{messages[messages.length - 1]?.status !== MessageStatus.Pending ? (
<Button
size="lg"
disabled={disabled || stateModel.loading || !activeThread}
disabled={
isDisabledChatbox || stateModel.loading || !activeThread
}
themes="primary"
className="min-w-[100px]"
onClick={sendChatMessage}

View File

@ -21,7 +21,7 @@ import { getAssistants } from '@/hooks/useGetAssistants'
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
import { useMainViewState } from '@/hooks/useMainViewState'
import { toGigabytes } from '@/utils/converter'
import { toGibibytes } from '@/utils/converter'
type Props = {
model: Model
@ -99,7 +99,7 @@ const ExploreModelItemHeader: React.FC<Props> = ({ model, onClick, open }) => {
</div>
<div className="inline-flex items-center space-x-2">
<span className="mr-4 font-semibold text-muted-foreground">
{toGigabytes(model.metadata.size)}
{toGibibytes(model.metadata.size)}
</span>
{downloadButton}
<ChevronDownIcon

Some files were not shown because too many files have changed in this diff Show More