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 uses: unfor19/install-aws-cli-action@v1
- name: Delete cloudflare-r2 folder using awscli s3api - name: Delete cloudflare-r2 folder using awscli s3api
if: github.ref == 'refs/heads/main'
continue-on-error: true continue-on-error: true
run: | run: |
# Get the list of objects in the 'latest' folder # Get the list of objects in the 'latest' folder
@ -34,25 +35,18 @@ jobs:
AWS_DEFAULT_REGION: auto AWS_DEFAULT_REGION: auto
AWS_EC2_METADATA_DISABLED: "true" AWS_EC2_METADATA_DISABLED: "true"
build-macos: # Job create Update app version based on latest release tag with build number and save to output
runs-on: macos-latest get-update-version:
runs-on: ubuntu-latest
needs: delete-cloudflare-r2-folder needs: delete-cloudflare-r2-folder
environment: production environment: production
permissions: outputs:
contents: write new_version: ${{ steps.version_update.outputs.new_version }}
steps: steps:
- name: Getting the repo
uses: actions/checkout@v3
- name: Installing node
uses: actions/setup-node@v1
with:
node-version: 20
- name: Install jq - name: Install jq
uses: dcarbone/install-jq-action@v2.0.1 uses: dcarbone/install-jq-action@v2.0.1
- name: Update app version based on latest release tag with build number - name: Update app version based on latest release tag with build number
id: version_update id: version_update
run: | run: |
@ -82,11 +76,35 @@ jobs:
# Remove the 'v' and append the build number to the version # Remove the 'v' and append the build number to the version
NEW_VERSION="${LATEST_TAG#v}-${GITHUB_RUN_NUMBER}" NEW_VERSION="${LATEST_TAG#v}-${GITHUB_RUN_NUMBER}"
echo "New version: $NEW_VERSION" 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" 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 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 mv /tmp/package.json electron/package.json
@ -119,26 +137,27 @@ jobs:
- name: Upload Artifact - name: Upload Artifact
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
with: with:
name: jan-mac-x64-${{ steps.version_update.outputs.new_version }} name: jan-mac-x64-${{ needs.get-update-version.outputs.new_version }}
path: ./electron/dist/jan-mac-x64-${{ steps.version_update.outputs.new_version }}.dmg path: ./electron/dist/jan-mac-x64-${{ needs.get-update-version.outputs.new_version }}.dmg
- name: Upload Artifact - name: Upload Artifact
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
with: with:
name: jan-mac-arm64-${{ steps.version_update.outputs.new_version }} name: jan-mac-arm64-${{ needs.get-update-version.outputs.new_version }}
path: ./electron/dist/jan-mac-arm64-${{ steps.version_update.outputs.new_version }}.dmg path: ./electron/dist/jan-mac-arm64-${{ needs.get-update-version.outputs.new_version }}.dmg
- name: put-object using awscli s3api - name: put-object using awscli s3api
if: github.ref == 'refs/heads/main'
continue-on-error: true continue-on-error: true
run: | run: |
ls -al ./electron/dist 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-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-${{ 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-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-${{ 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-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-${{ 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 "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 "${{ 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 "${{ 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 "${{ 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 "${{ 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 "${{ 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 "${{ 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" 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: env:
AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }}
@ -148,7 +167,7 @@ jobs:
build-windows-x64: build-windows-x64:
runs-on: windows-latest runs-on: windows-latest
needs: delete-cloudflare-r2-folder needs: [delete-cloudflare-r2-folder, get-update-version]
permissions: permissions:
contents: write contents: write
steps: steps:
@ -167,38 +186,12 @@ jobs:
id: version_update id: version_update
shell: bash shell: bash
run: | 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 # 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 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 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 mv /tmp/package.json electron/package.json
@ -223,18 +216,19 @@ jobs:
- name: Upload Artifact - name: Upload Artifact
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
with: 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 path: ./electron/dist/*.exe
- name: put-object using awscli s3api - name: put-object using awscli s3api
if: github.ref == 'refs/heads/main'
shell: bash shell: bash
run: | run: |
ls -al ./electron/dist 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-${{ 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-${{ 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 "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 "${{ 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 "${{ 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 "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: env:
AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }}
@ -243,7 +237,7 @@ jobs:
build-linux-x64: build-linux-x64:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: delete-cloudflare-r2-folder needs: [delete-cloudflare-r2-folder, get-update-version]
environment: production environment: production
env: env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_TOKEN }} SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_TOKEN }}
@ -264,37 +258,11 @@ jobs:
- name: Update app version base on tag - name: Update app version base on tag
id: version_update id: version_update
run: | 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 # 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 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 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 mv /tmp/package.json electron/package.json
@ -307,16 +275,17 @@ jobs:
- name: Upload Artifact - name: Upload Artifact
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
with: 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 path: ./electron/dist/*.deb
- name: put-object using awscli s3api - name: put-object using awscli s3api
if: github.ref == 'refs/heads/main'
run: | run: |
ls -al ./electron/dist 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 "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 "${{ 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 "${{ 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 "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: env:
AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }}
@ -324,7 +293,7 @@ jobs:
AWS_EC2_METADATA_DISABLED: "true" AWS_EC2_METADATA_DISABLED: "true"
noti-discord-nightly-and-update-url-readme: 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 environment: production
if: github.event_name == 'schedule' if: github.event_name == 'schedule'
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -335,16 +304,29 @@ jobs:
fetch-depth: "0" fetch-depth: "0"
token: ${{ secrets.PAT_SERVICE_ACCOUNT }} 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 - name: Notify Discord
uses: Ilshidur/action-discord@master uses: Ilshidur/action-discord@master
with: 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: env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
- name: Update README.md with artifact URL - name: Update README.md with artifact URL
run: | 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.email "service@jan.ai"
git config --global user.name "Service Account" git config --global user.name "Service Account"
git add README.md git add README.md
@ -354,9 +336,9 @@ jobs:
GITHUB_RUN_ID: ${{ github.run_id }} GITHUB_RUN_ID: ${{ github.run_id }}
noti-discord-manual-and-update-url-readme: 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 environment: production
if: github.event_name == 'workflow_dispatch' if: github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
@ -364,11 +346,21 @@ jobs:
with: with:
fetch-depth: "0" fetch-depth: "0"
token: ${{ secrets.PAT_SERVICE_ACCOUNT }} 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 - name: Notify Discord
uses: Ilshidur/action-discord@master uses: Ilshidur/action-discord@master
with: 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: env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
@ -376,7 +368,10 @@ jobs:
- name: Update README.md with artifact URL - name: Update README.md with artifact URL
if: github.ref == 'refs/heads/main' if: github.ref == 'refs/heads/main'
run: | 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.email "service@jan.ai"
git config --global user.name "Service Account" git config --global user.name "Service Account"
git add README.md git add README.md

View File

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

1
.gitignore vendored
View File

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

View File

@ -52,21 +52,17 @@ build: check-file-counts
clean: clean:
ifeq ($(OS),Windows_NT) ifeq ($(OS),Windows_NT)
powershell -Command "Get-ChildItem -Path . -Include node_modules, .next, dist -Recurse -Directory | Remove-Item -Recurse -Force" 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%\jan\extensions"
rmdir /s /q "%USERPROFILE%\AppData\Roaming\jan-electron"
rmdir /s /q "%USERPROFILE%\AppData\Local\jan*"
else ifeq ($(shell uname -s),Linux) else ifeq ($(shell uname -s),Linux)
find . -name "node_modules" -type d -prune -exec rm -rf '{}' + find . -name "node_modules" -type d -prune -exec rm -rf '{}' +
find . -name ".next" -type d -exec rm -rf '{}' + find . -name ".next" -type d -exec rm -rf '{}' +
find . -name "dist" -type d -exec rm -rf '{}' + find . -name "dist" -type d -exec rm -rf '{}' +
rm -rf "~/.config/jan" rm -rf "~/jan/extensions"
rm -rf "~/.config/jan-electron"
rm -rf "~/.cache/jan*" rm -rf "~/.cache/jan*"
else else
find . -name "node_modules" -type d -prune -exec rm -rf '{}' + find . -name "node_modules" -type d -prune -exec rm -rf '{}' +
find . -name ".next" -type d -exec rm -rf '{}' + find . -name ".next" -type d -exec rm -rf '{}' +
find . -name "dist" -type d -exec rm -rf '{}' + find . -name "dist" -type d -exec rm -rf '{}' +
rm -rf ~/Library/Application\ Support/jan rm -rf ~/jan/extensions
rm -rf ~/Library/Application\ Support/jan-electron
rm -rf ~/Library/Caches/jan* rm -rf ~/Library/Caches/jan*
endif 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 ## Download
<table> <table>
<tr> <tr style="text-align:center">
<td style="text-align:center"><b>Version Type</b></td> <td style="text-align:center"><b>Version Type</b></td>
<td style="text-align:center"><b>Windows</b></td> <td style="text-align:center"><b>Windows</b></td>
<td colspan="2" style="text-align:center"><b>MacOS</b></td> <td colspan="2" style="text-align:center"><b>MacOS</b></td>
<td style="text-align:center"><b>Linux</b></td> <td style="text-align:center"><b>Linux</b></td>
</tr> </tr>
<tr> <tr style="text-align:center">
<td style="text-align:center"><b>Stable (Recommended)</b></td> <td style="text-align:center"><b>Stable (Recommended)</b></td>
<td style="text-align:center"> <td style="text-align:center">
<a href='https://github.com/janhq/jan/releases/download/v0.4.3/jan-win-x64-0.4.3.exe'> <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> </a>
</td> </td>
</tr> </tr>
<tr style="text-align: center"> <tr style="text-align:center">
<td style="text-align:center"><b>Experimental (Nighlty Build)</b></td> <td style="text-align:center"><b>Experimental (Nightly Build)</b></td>
<td style="text-align:center" colspan="4"> <td style="text-align:center">
<a href='https://github.com/janhq/jan/actions/runs/7372465396'> <a href='https://delta.jan.ai/0.4.3-118/jan-win-x64-0.4.3-118.exe'>
<b>Github action artifactory</b> <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> </a>
</td> </td>
</tr> </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 website](https://jan.ai/)
- [Jan Github](https://github.com/janhq/jan) - [Jan Github](https://github.com/janhq/jan)
- [User Guides](https://jan.ai/docs) - [User Guides](https://jan.ai/guides/)
- [Developer docs](https://jan.ai/docs/extensions/) - [Developer docs](https://jan.ai/developer/)
- [API reference](https://jan.ai/api-reference/) - [API reference](https://jan.ai/api-reference/)
- [Specs](https://jan.ai/specs/) - [Specs](https://jan.ai/docs/)
#### Nitro #### Nitro
@ -111,18 +130,7 @@ As Jan is in development mode, you might get stuck on a broken build.
To reset your installation: To reset your installation:
1. **Remove Jan from your Applications folder and Cache folder** 1. Use the following commands to remove any dangling backend processes:
```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:
```sh ```sh
ps aux | grep nitro ps aux | grep nitro
@ -134,6 +142,18 @@ To reset your installation:
kill -9 <PID> 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 ## Contributing
Contributions are welcome! Please read the [CONTRIBUTING.md](CONTRIBUTING.md) file 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:** 1. **Clone the repository and prepare:**
```bash ```bash
git clone https://github.com/janhq/jan git clone https://github.com/janhq/jan
cd jan cd jan
git checkout -b DESIRED_BRANCH git checkout -b DESIRED_BRANCH
``` ```
2. **Run development and use Jan Desktop** 2. **Run development and use Jan Desktop**
``` ```bash
make dev 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 ### 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. 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 ## Acknowledgements
Jan builds on top of other open-source projects: Jan builds on top of other open-source projects:

View File

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

View File

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

View File

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

View File

@ -5,3 +5,4 @@ export * from './extension/store'
export * from './download' export * from './download'
export * from './module' export * from './module'
export * from './api' 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 { export class ModuleManager {
public requiredModules: Record<string, any> = {} public requiredModules: Record<string, any> = {}
public cleaningResource = false
public static instance: ModuleManager = new ModuleManager() public static instance: ModuleManager = new ModuleManager()

View File

@ -1,9 +1,23 @@
--- ---
title: Your First Assistant 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 :::caution
This is currently under development. This is currently under development.
::: :::
A quickstart on how to build an assistant

View File

@ -1,9 +1,22 @@
--- ---
title: Anatomy of an Assistant 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 :::caution
This is currently under development. This is currently under development.
::: :::
An overview of assistant.json

View File

@ -1,9 +1,22 @@
--- ---
title: Package your Assistant 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 :::caution
This is currently under development. 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 :::caution
This is currently under development. 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 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 :::caution
This is currently under development. 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 :::caution
This is currently under development. 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 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 :::caution
This is currently under development. This is currently under development.
::: :::
A quickstart on tensorrt-llm impl

View File

@ -1,9 +1,22 @@
--- ---
title: Anatomy of an Extension 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 :::caution
This is currently under development. 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 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. 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) import DocCardList from "@theme/DocCardList";
- [Upload docs](upload-docs)
- [Upload images](upload-images) <DocCardList />
- [Manage chat history](manage-chat-history)

View File

@ -1,6 +1,6 @@
--- ---
title: Install Models from the Hub 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: keywords:
[ [
Jan AI, Jan AI,

View File

@ -1,7 +1,7 @@
--- ---
title: Import Models Manually title: Import Models Manually
slug: /guides/using-models/import-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: keywords:
[ [
Jan AI, Jan AI,
@ -13,6 +13,7 @@ keywords:
no-subscription fee, no-subscription fee,
large language model, large language model,
import-models-manually, import-models-manually,
local model,
] ]
--- ---
@ -24,16 +25,12 @@ This is currently under development.
import Tabs from "@theme/Tabs"; import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem"; 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 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.
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.
> We are fast shipping a UI to make this easier, but it's a bit manual for now. Apologies. > 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 ### 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`. 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 filename must be `model.json`.
- Ensure the `id` property matches the folder name you created. - Ensure the `id` property matches the folder name you created.
- Ensure the GGUF filename should match the `id` property exactly. - 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 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`. - Ensure the `state` property is set to `ready`.
```js ```js
{ {
// highlight-start // highlight-start
"source_url": "https://huggingface.co/janhq/trinity-v1-GGUF/resolve/main/trinity-v1.Q4_K_M.gguf", "source_url": "https://huggingface.co/janhq/trinity-v1-GGUF/resolve/main/trinity-v1.Q4_K_M.gguf",
"id": "trinity-v1-7b", "id": "trinity-v1-7b",
// highlight-end // highlight-end
"object": "model", "object": "model",
"name": "Trinity-v1 7B Q4", "name": "Trinity-v1 7B Q4",
"version": "1.0", "version": "1.0",
"description": "Trinity is an experimental model merge of GreenNodeLM & LeoScorpius using the Slerp method. Recommended for daily assistance purposes.", "description": "Trinity is an experimental model merge of GreenNodeLM & LeoScorpius using the Slerp method. Recommended for daily assistance purposes.",
"format": "gguf", "format": "gguf",
"settings": { "settings": {
"ctx_len": 4096, "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-next-line // highlight-next-line
"state": "ready", "prompt_template": "{system_message}\n### Instruction:\n{prompt}\n### Response:"
"engine": "nitro" },
} "parameters": {
"max_tokens": 4096
},
"metadata": {
"author": "Jan",
"tags": ["7B", "Merged"],
"size": 4370000000
},
"engine": "nitro",
// highlight-next-line
"state": "ready"
}
``` ```
### 3. Download the Model ### 3. Download the Model
Restart Jan and navigate to the Hub. Locate your model and click the `Download` button to download the model binary. 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. 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 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: keywords:
[ [
Jan AI, 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
``` 2. Go to http://localhost:1337/docs for API docs.
curl http://localhost:1337/v1/chat/completions
``` 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 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 title: Extension Settings
--- slug: /guides/using-extensions/extension-settings/
description: Configure settings for extensions.
TODO: how to 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 title: Stuck on a Broken Build
slug: /troubleshooting/stuck-on-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: keywords:
[ [
Jan AI, 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 operate on the basis of trust.
- We expect you to be available and communicative during scheduled meetings or work hours. - We expect you to be available and communicative during scheduled meetings or work hours.
- Turning on video during meetings is encouraged. - 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. - 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. - 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. - 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`. 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 ## 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 - `Roadmap Labels` tag large, long-term, & strategic projects that can span multiple teams and multiple sprints
- Example label: `project: Jan has Mobile` - Example label: `roadmap: Jan has Mobile`
- `Projects` contain `epics` - `Roadmaps` contain `epics`
[`Epics`](https://github.com/janhq/jan/issues?q=is%3Aissue+is%3Aopen+label%3A%22type%3A+epic%22) [`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 - `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` contain `tasks`
- `Epics` should always have 1 owner - `Epics` should always have 1 owner
[`Milestones`](https://github.com/janhq/jan/milestones) [`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` track release versions. We use [semantic versioning](https://semver.org/)
- `Milestones` span 1-2 weeks and have deadlines - `Milestones` span ~2 weeks and have deadlines
- `Milestones` usually fit within 2 week sprint cycles
[`Tasks`](https://github.com/janhq/jan/issues) [`Tasks`](https://github.com/janhq/jan/issues)
- Tasks are individual issues (feats, bugs, chores) that can be completed within a few days - 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 are usually named per [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/#summary)
- Tasks should always have 1 owner - 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 - `no status`: issues that need to be triaged (needs an owner, ETA)
- `todo`: issues you plan to tackle within this week - `icebox`: issues you don't plan to tackle yet
- `planned`: issues you plan to tackle this week
- `in-progress`: in progress - `in-progress`: in progress
- `in-review`: pending PR or blocked by something - `in-review`: pending PR or blocked by something
- `done`: done - `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; font-weight: bold;
margin-bottom: 16px; margin-bottom: 16px;
margin-top: -20px; margin-top: -20px;
} }

View File

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

View File

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

View File

View File

@ -1,20 +1,12 @@
import { app, ipcMain, shell, nativeTheme } from 'electron' import { app, ipcMain, shell, nativeTheme } from 'electron'
import { join, basename } from 'path' import { join, basename } from 'path'
import { WindowManager } from './../managers/window' import { WindowManager } from './../managers/window'
import { userSpacePath } from './../utils/path' import { getResourcePath, userSpacePath } from './../utils/path'
import { AppRoute } from '@janhq/core' import { AppRoute } from '@janhq/core'
import { ExtensionManager, ModuleManager } from '@janhq/core/node' import { ExtensionManager, ModuleManager } from '@janhq/core/node'
import { startServer, stopServer } from '@janhq/server'
export function handleAppIPCs() { 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. * 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. * 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) 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. * Relaunches the app in production - reload window in development.
* @param _event - The IPC event object. * @param _event - The IPC event object.

View File

@ -34,8 +34,18 @@ export function handleDownloaderIPCs() {
*/ */
ipcMain.handle(DownloadRoute.abortDownload, async (_event, fileName) => { ipcMain.handle(DownloadRoute.abortDownload, async (_event, fileName) => {
const rq = DownloadManager.instance.networkRequests[fileName] const rq = DownloadManager.instance.networkRequests[fileName]
DownloadManager.instance.networkRequests[fileName] = undefined if (rq) {
rq?.abort() 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 destination = resolve(userDataPath, fileName)
const rq = request(url) 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` const downloadingTempFile = `${destination}.download`
progress(rq, {}) progress(rq, {})
@ -93,13 +107,11 @@ export function handleDownloaderIPCs() {
DownloadEvent.onFileDownloadError, DownloadEvent.onFileDownloadError,
{ {
fileName, fileName,
err: 'Download cancelled', err: { message: 'aborted' },
} }
) )
} }
}) })
.pipe(createWriteStream(downloadingTempFile)) .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 { handleAppUpdates } from './handlers/update'
import { handleFsIPCs } from './handlers/fs' import { handleFsIPCs } from './handlers/fs'
import { migrateExtensions } from './utils/migration' import { migrateExtensions } from './utils/migration'
import { dispose } from './utils/disposable'
/**
* Server
*/
import { startServer } from '@janhq/server'
app app
.whenReady() .whenReady()
@ -34,7 +30,6 @@ app
.then(handleIPCs) .then(handleIPCs)
.then(handleAppUpdates) .then(handleAppUpdates)
.then(createMainWindow) .then(createMainWindow)
.then(startServer)
.then(() => { .then(() => {
app.on('activate', () => { app.on('activate', () => {
if (!BrowserWindow.getAllWindows().length) { if (!BrowserWindow.getAllWindows().length) {
@ -43,14 +38,12 @@ app
}) })
}) })
app.on('window-all-closed', () => { app.once('window-all-closed', () => {
ModuleManager.instance.clearImportedModules() cleanUpAndQuit()
app.quit()
}) })
app.on('quit', () => { app.once('quit', () => {
ModuleManager.instance.clearImportedModules() cleanUpAndQuit()
app.quit()
}) })
function createMainWindow() { function createMainWindow() {
@ -75,6 +68,12 @@ function createMainWindow() {
if (process.platform !== 'darwin') app.quit() 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 */ /* Enable dev tools for development */
if (!app.isPackaged) mainWindow.webContents.openDevTools() if (!app.isPackaged) mainWindow.webContents.openDevTools()
} }
@ -89,3 +88,13 @@ function handleIPCs() {
handleAppIPCs() handleAppIPCs()
handleFileMangerIPCs() 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}",
"build/**/*.{js,map}", "build/**/*.{js,map}",
"pre-install", "pre-install",
"models/**/*" "models/**/*",
"docs/**/*"
], ],
"asarUnpack": [ "asarUnpack": [
"pre-install", "pre-install",
"models" "models",
"docs"
], ],
"publish": [ "publish": [
{ {

View File

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

View File

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

View File

@ -17,7 +17,7 @@
}, },
"dependencies": { "dependencies": {
"@janhq/core": "file:../../core", "@janhq/core": "file:../../core",
"systeminformation": "^5.21.8", "node-os-utils": "^1.3.7",
"ts-loader": "^9.5.0" "ts-loader": "^9.5.0"
}, },
"files": [ "files": [
@ -26,6 +26,6 @@
"README.md" "README.md"
], ],
"bundleDependencies": [ "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 () => const getResourcesInfo = () =>
new Promise(async (resolve) => { new Promise((resolve) => {
const cpu = await si.cpu(); nodeOsUtils.mem.used()
const mem = await si.mem(); .then(ramUsedInfo => {
// const gpu = await si.graphics(); const totalMemory = ramUsedInfo.totalMemMb * 1024 * 1024;
const response = { const usedMemory = ramUsedInfo.usedMemMb * 1024 * 1024;
cpu, const response = {
mem, mem: {
// gpu, totalMemory,
}; usedMemory,
resolve(response); },
};
resolve(response);
})
}); });
const getCurrentLoad = async () => const getCurrentLoad = () =>
new Promise(async (resolve) => { new Promise((resolve) => {
const currentLoad = await si.currentLoad(); nodeOsUtils.cpu.usage().then(cpuPercentage =>{
resolve(currentLoad); const response = {
cpu: {
usage: cpuPercentage,
},
};
resolve(response);
});
}); });
module.exports = { module.exports = {

View File

@ -25,7 +25,8 @@
"scripts": { "scripts": {
"lint": "yarn workspace jan lint && yarn workspace jan-web lint", "lint": "yarn workspace jan lint && yarn workspace jan-web lint",
"test": "yarn workspace jan test:e2e", "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:web": "yarn workspace jan-web dev",
"dev": "concurrently --kill-others \"yarn dev:web\" \"wait-on http://localhost:3000 && yarn dev:electron\"", "dev": "concurrently --kill-others \"yarn dev:web\" \"wait-on http://localhost:3000 && yarn dev:electron\"",
"test-local": "yarn lint && yarn build:test && yarn test", "test-local": "yarn lint && yarn build:test && yarn test",
@ -34,15 +35,15 @@
"build:server": "cd server && yarn install && yarn run build", "build:server": "cd server && yarn install && yarn run build",
"build:core": "cd core && 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: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: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: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: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: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: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": "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": { "devDependencies": {
"concurrently": "^8.2.1", "concurrently": "^8.2.1",

View File

@ -1,60 +1,78 @@
import fastify from "fastify"; import fastify from "fastify";
import dotenv from "dotenv"; import dotenv from "dotenv";
import { v1Router } from "@janhq/core/node"; import { log, v1Router } from "@janhq/core/node";
import path from "path"; import path from "path";
import os from "os";
dotenv.config(); 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 JAN_API_PORT = Number.parseInt(process.env.JAN_API_PORT || "1337");
const serverLogPath = path.join(os.homedir(), "jan", "server.log");
const server = fastify(); let server: any | undefined = undefined;
server.register(require("@fastify/cors"), {});
server.register(require("@fastify/swagger"), { export const startServer = async (schemaPath?: string, baseDir?: string) => {
mode: "static", try {
specification: { server = fastify({
path: "./../docs/openapi/jan.yaml", logger: {
baseDir: "./../docs/openapi", level: "info",
}, file: serverLogPath,
}); },
server.register(require("@fastify/swagger-ui"), { });
routePrefix: "/docs", await server.register(require("@fastify/cors"), {});
baseDir: path.join(__dirname, "../..", "./docs/openapi"),
uiConfig: { await server.register(require("@fastify/swagger"), {
docExpansion: "full", mode: "static",
deepLinking: false, specification: {
}, path: schemaPath ?? "./../docs/openapi/jan.yaml",
staticCSP: true, baseDir: baseDir ?? "./../docs/openapi",
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,
}); });
done(); await server.register(require("@fastify/swagger-ui"), {
}, routePrefix: "/",
{ prefix: "extensions" } baseDir: baseDir ?? path.join(__dirname, "../..", "./docs/openapi"),
); uiConfig: {
server.register(v1Router, { prefix: "/v1" }); docExpansion: "full",
deepLinking: false,
export const startServer = () => { },
server staticCSP: false,
.listen({ transformSpecificationClone: true,
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(
(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 = () => { export const stopServer = async () => {
server.close(); try {
await server.close();
} catch (e) {
log(e);
}
}; };

View File

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

View File

@ -1,31 +1,40 @@
import { ReactNode, useState } from 'react' import { ReactNode, useState } from 'react'
import { useAtomValue } from 'jotai'
import { import {
ChevronDownIcon, ChevronDownIcon,
MoreVerticalIcon, MoreVerticalIcon,
FolderOpenIcon, FolderOpenIcon,
Code2Icon, Code2Icon,
PencilIcon,
} from 'lucide-react' } from 'lucide-react'
import { twMerge } from 'tailwind-merge' import { twMerge } from 'tailwind-merge'
import { useActiveModel } from '@/hooks/useActiveModel'
import { useClickOutside } from '@/hooks/useClickOutside' import { useClickOutside } from '@/hooks/useClickOutside'
import { activeThreadAtom } from '@/helpers/atoms/Thread.atom'
interface Props { interface Props {
children: ReactNode children: ReactNode
title: string title: string
onRevealInFinderClick: (type: string) => void onRevealInFinderClick?: (type: string) => void
onViewJsonClick: (type: string) => void onViewJsonClick?: (type: string) => void
asChild?: boolean
} }
export default function CardSidebar({ export default function CardSidebar({
children, children,
title, title,
onRevealInFinderClick, onRevealInFinderClick,
onViewJsonClick, onViewJsonClick,
asChild,
}: Props) { }: Props) {
const [show, setShow] = useState(true) const [show, setShow] = useState(true)
const [more, setMore] = useState(false) const [more, setMore] = useState(false)
const [menu, setMenu] = useState<HTMLDivElement | null>(null) const [menu, setMenu] = useState<HTMLDivElement | null>(null)
const [toggle, setToggle] = useState<HTMLDivElement | null>(null) const [toggle, setToggle] = useState<HTMLDivElement | null>(null)
const { activeModel } = useActiveModel()
const activeThread = useAtomValue(activeThreadAtom)
useClickOutside(() => setMore(false), null, [menu, toggle]) useClickOutside(() => setMore(false), null, [menu, toggle])
@ -39,68 +48,127 @@ export default function CardSidebar({
return ( return (
<div <div
className={twMerge( 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 <div
className={twMerge( className={twMerge(
'relative flex items-center rounded-t-md ', 'relative flex items-center justify-between pl-4',
show && 'border-b border-border' show && 'border-b border-border'
)} )}
> >
<button <span className="font-bold">{title}</span>
onClick={() => setShow(!show)} <div className="flex">
className="flex w-full flex-1 items-center space-x-2 bg-zinc-200 px-3 py-2 dark:bg-zinc-600/10" {!asChild && (
> <div
<ChevronDownIcon ref={setToggle}
className={twMerge( className="cursor-pointer rounded-lg bg-zinc-100 p-2 pr-0 dark:bg-zinc-900"
'h-5 w-5 flex-none text-gray-400', onClick={() => setMore(!more)}
show && 'rotate-180' >
)} <MoreVerticalIcon className="h-5 w-5" />
/> </div>
<span className="font-bold">{title}</span> )}
</button> <button
<div onClick={() => setShow(!show)}
ref={setToggle} className="flex w-full flex-1 items-center space-x-2 rounded-lg bg-zinc-100 px-3 py-2 dark:bg-zinc-900"
className="cursor-pointer rounded-md bg-zinc-200 p-2 dark:bg-zinc-600/10" >
onClick={() => setMore(!more)} <ChevronDownIcon
> className={twMerge(
<MoreVerticalIcon className="h-5 w-5" /> 'h-5 w-5 flex-none text-gray-400',
show && 'rotate-180'
)}
/>
</button>
</div> </div>
{more && ( {more && (
<div <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} ref={setMenu}
> >
<div <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={() => { onClick={() => {
onRevealInFinderClick(title) onRevealInFinderClick && onRevealInFinderClick(title)
setMore(false) setMore(false)
}} }}
> >
<FolderOpenIcon size={16} className="text-muted-foreground" /> <FolderOpenIcon
<span className="text-bold text-black dark:text-muted-foreground"> size={16}
{openFolderTitle} className={twMerge(
</span> '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>
<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={() => { onClick={() => {
onViewJsonClick(title) onViewJsonClick && onViewJsonClick(title)
setMore(false) setMore(false)
}} }}
> >
<Code2Icon size={16} className="text-muted-foreground" /> <PencilIcon
<span className="text-bold text-black dark:text-muted-foreground"> size={16}
View as JSON className="mt-0.5 flex-shrink-0 text-muted-foreground"
</span> />
<>
<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> </div>
)} )}
</div> </div>
{show && ( {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} {children}
</div> </div>
)} )}

View File

@ -1,32 +1,79 @@
import React from 'react' 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 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 = { type Props = {
name: string name: string
title: string title: string
description: string
checked: boolean checked: boolean
} }
const Checkbox: React.FC<Props> = ({ name, title, checked }) => { const Checkbox: React.FC<Props> = ({ name, title, checked, description }) => {
const { updateModelParameter } = useUpdateModelParameters() const { updateModelParameter } = useUpdateModelParameters()
const threadId = useAtomValue(getActiveThreadIdAtom) 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) => { const onCheckedChange = (checked: boolean) => {
if (!threadId) return if (!threadId) return
if (engineParams.some((x) => x.name.includes(name))) {
setEngineParamsUpdate(true)
stopModel()
} else {
setEngineParamsUpdate(false)
}
updateModelParameter(threadId, name, checked) updateModelParameter(threadId, name, checked)
} }
return ( return (
<div className="flex justify-between"> <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} /> <Switch checked={checked} onCheckedChange={onCheckedChange} />
</div> </div>
) )

View File

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

View File

@ -9,7 +9,7 @@ export default function SystemItem({ name, value }: Props) {
return ( return (
<div className="flex items-center gap-x-1"> <div className="flex items-center gap-x-1">
<p className="text-xs">{name}</p> <p className="text-xs">{name}</p>
<span className="text-xs">{value}</span> <span className="text-xs font-bold">{value}</span>
</div> </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 { useAtomValue } from 'jotai'
import { FaGithub, FaDiscord } from 'react-icons/fa'
import DownloadingState from '@/containers/Layout/BottomBar/DownloadingState' import DownloadingState from '@/containers/Layout/BottomBar/DownloadingState'
import SystemItem from '@/containers/Layout/BottomBar/SystemItem' import SystemItem from '@/containers/Layout/BottomBar/SystemItem'
@ -15,7 +24,6 @@ import { MainViewState } from '@/constants/screens'
import { useActiveModel } from '@/hooks/useActiveModel' import { useActiveModel } from '@/hooks/useActiveModel'
import { useDownloadState } from '@/hooks/useDownloadState' import { useDownloadState } from '@/hooks/useDownloadState'
import { useGetAppVersion } from '@/hooks/useGetAppVersion'
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels' import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
import useGetSystemResources from '@/hooks/useGetSystemResources' import useGetSystemResources from '@/hooks/useGetSystemResources'
import { useMainViewState } from '@/hooks/useMainViewState' import { useMainViewState } from '@/hooks/useMainViewState'
@ -24,11 +32,23 @@ const BottomBar = () => {
const { activeModel, stateModel } = useActiveModel() const { activeModel, stateModel } = useActiveModel()
const { ram, cpu } = useGetSystemResources() const { ram, cpu } = useGetSystemResources()
const progress = useAtomValue(appDownloadProgress) const progress = useAtomValue(appDownloadProgress)
const appVersion = useGetAppVersion()
const { downloadedModels } = useGetDownloadedModels() const { downloadedModels } = useGetDownloadedModels()
const { setMainViewState } = useMainViewState() const { setMainViewState } = useMainViewState()
const { downloadStates } = useDownloadState() 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 ( 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="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"> <div className="flex flex-shrink-0 items-center gap-x-2">
@ -71,12 +91,39 @@ const BottomBar = () => {
<DownloadingState /> <DownloadingState />
</div> </div>
<div className="flex gap-x-2"> <div className="flex items-center gap-x-4">
<SystemItem name="CPU:" value={`${cpu}%`} /> <div className="flex items-center gap-x-2">
<SystemItem name="Mem:" value={`${ram}%`} /> <SystemItem name="CPU:" value={`${cpu}%`} />
<span className="text-xs font-semibold "> <SystemItem name="Mem:" value={`${ram}%`} />
Jan v{appVersion?.version ?? ''} </div>
</span> {/* 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>
</div> </div>
) )

View File

@ -74,6 +74,7 @@ export default function RibbonNav() {
state: MainViewState.Settings, state: MainViewState.Settings,
}, },
] ]
return ( 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="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"> <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) => { const onModelActionClick = (modelId: string) => {
if (activeModel && activeModel.id === modelId) { if (activeModel && activeModel.id === modelId) {
stopModel(modelId) stopModel()
} else { } else {
startModel(modelId) 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 { 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 CommandListDownloadedModel from '@/containers/Layout/TopBar/CommandListDownloadedModel'
import CommandSearch from '@/containers/Layout/TopBar/CommandSearch' import CommandSearch from '@/containers/Layout/TopBar/CommandSearch'
import { MainViewState } from '@/constants/screens' import { MainViewState } from '@/constants/screens'
import { useClickOutside } from '@/hooks/useClickOutside'
import { useCreateNewThread } from '@/hooks/useCreateNewThread' import { useCreateNewThread } from '@/hooks/useCreateNewThread'
import useGetAssistants, { getAssistants } from '@/hooks/useGetAssistants' import useGetAssistants, { getAssistants } from '@/hooks/useGetAssistants'
import { useMainViewState } from '@/hooks/useMainViewState' import { useMainViewState } from '@/hooks/useMainViewState'
import { showRightSideBarAtom } from '@/screens/Chat/Sidebar' import { showRightSideBarAtom } from '@/screens/Chat/Sidebar'
import { activeThreadAtom } from '@/helpers/atoms/Thread.atom' import { activeThreadAtom, threadStatesAtom } from '@/helpers/atoms/Thread.atom'
const TopBar = () => { const TopBar = () => {
const activeThread = useAtomValue(activeThreadAtom) const activeThread = useAtomValue(activeThreadAtom)
@ -20,6 +33,13 @@ const TopBar = () => {
const { requestCreateNewThread } = useCreateNewThread() const { requestCreateNewThread } = useCreateNewThread()
const { assistants } = useGetAssistants() const { assistants } = useGetAssistants()
const setShowRightSideBar = useSetAtom(showRightSideBarAtom) 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) => { const titleScreen = (viewStateName: MainViewState) => {
switch (viewStateName) { 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 ( 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"> <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 && ( {mainViewState === MainViewState.Thread && (
<div className="absolute left-16 h-full w-60 border-r border-border" /> <div className="relative w-full">
)} <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"> <div className="flex h-full w-full items-center justify-between">
{mainViewState === MainViewState.Thread ? (
<div className="unset-drag flex space-x-8">
<div className="flex w-52 justify-between">
<div className="cursor-pointer"> <div className="cursor-pointer">
<PanelLeftIcon <PanelLeftIcon
size={20} size={20}
@ -63,34 +113,107 @@ const TopBar = () => {
/> />
</div> </div>
<div <div
className="cursor-pointer pr-2" className="unset-drag cursor-pointer pr-4"
onClick={onCreateConversationClick} onClick={onCreateConversationClick}
> >
<PenSquareIcon size={20} className="text-muted-foreground" /> <PenSquareIcon size={20} className="text-muted-foreground" />
</div> </div>
</div> </div>
<span className="text-sm font-bold"> </div>
{titleScreen(mainViewState)} <div className="absolute left-80 h-full">
</span> <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 && ( {activeThread && (
<div <div className="flex h-full w-52 items-center justify-between px-4">
className="unset-drag absolute right-4 cursor-pointer" {showing && (
onClick={() => setShowRightSideBar((show) => !show)} <div className="relative flex h-full items-center">
> <span className="mr-2 text-sm font-bold">
<PanelRightIcon size={20} className="text-muted-foreground" /> 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> </div>
) : ( </div>
<div> )}
<span className="text-sm font-bold">
{titleScreen(mainViewState)} {mainViewState !== MainViewState.Thread && (
</span> <div className="relative left-16 flex w-[calc(100%-64px)] items-center justify-between space-x-4 pl-6 pr-2">
</div> <span className="text-sm font-bold">
)} {titleScreen(mainViewState)}
<CommandSearch /> </span>
<CommandListDownloadedModel /> </div>
</div> )}
<CommandSearch />
<CommandListDownloadedModel />
</div> </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 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 = { type Props = {
title: string title: string
name: string name: string
description: string
placeholder: string placeholder: string
value: string value: string
} }
@ -17,20 +36,51 @@ const ModelConfigInput: React.FC<Props> = ({
title, title,
name, name,
value, value,
description,
placeholder, placeholder,
}) => { }) => {
const { updateModelParameter } = useUpdateModelParameters() const { updateModelParameter } = useUpdateModelParameters()
const threadId = useAtomValue(getActiveThreadIdAtom) 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>) => { const onValueChanged = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
if (!threadId) return if (!threadId) return
if (engineParams.some((x) => x.name.includes(name))) {
setEngineParamsUpdate(true)
stopModel()
} else {
setEngineParamsUpdate(false)
}
updateModelParameter(threadId, name, e.target.value) updateModelParameter(threadId, name, e.target.value)
} }
return ( return (
<div className="flex flex-col"> <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 <Textarea
placeholder={placeholder} placeholder={placeholder}
onChange={onValueChanged} onChange={onValueChanged}

View File

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

View File

@ -52,7 +52,8 @@ export default function EventListenerWrapper({ children }: PropsWithChildren) {
window.electronAPI.onFileDownloadError( window.electronAPI.onFileDownloadError(
async (_event: string, state: any) => { 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 modelName = await baseName(state.fileName)
const model = modelsRef.current.find( const model = modelsRef.current.find(
(model) => modelBinFileName(model) === modelName (model) => modelBinFileName(model) === modelName
@ -66,7 +67,7 @@ export default function EventListenerWrapper({ children }: PropsWithChildren) {
if (state && state.fileName) { if (state && state.fileName) {
const modelName = await baseName(state.fileName) const modelName = await baseName(state.fileName)
const model = modelsRef.current.find( const model = modelsRef.current.find(
async (model) => modelBinFileName(model) === modelName (model) => modelBinFileName(model) === modelName
) )
if (model) { if (model) {
setDownloadStateSuccess(model.id) setDownloadStateSuccess(model.id)

View File

@ -1,15 +1,34 @@
import React from 'react' import React from 'react'
import { Slider, Input } from '@janhq/uikit' import {
import { useAtomValue } from 'jotai' 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 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 = { type Props = {
name: string name: string
title: string title: string
description: string
min: number min: number
max: number max: number
step: number step: number
@ -22,20 +41,51 @@ const SliderRightPanel: React.FC<Props> = ({
min, min,
max, max,
step, step,
description,
value, value,
}) => { }) => {
const { updateModelParameter } = useUpdateModelParameters() const { updateModelParameter } = useUpdateModelParameters()
const threadId = useAtomValue(getActiveThreadIdAtom) 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[]) => { const onValueChanged = (e: number[]) => {
if (!threadId) return if (!threadId) return
if (engineParams.some((x) => x.name.includes(name))) {
setEngineParamsUpdate(true)
stopModel()
} else {
setEngineParamsUpdate(false)
}
updateModelParameter(threadId, name, e[0]) updateModelParameter(threadId, name, e[0])
} }
return ( return (
<div className="flex flex-col"> <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="flex items-center gap-x-4">
<div className="relative w-full"> <div className="relative w-full">
<Slider <Slider
@ -58,7 +108,13 @@ const SliderRightPanel: React.FC<Props> = ({
min={min} min={min}
max={max} max={max}
value={String(value)} 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>
</div> </div>

View File

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

View File

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

View File

@ -33,7 +33,7 @@ const setDownloadStateFailedAtom = atom(null, (get, set, modelId: string) => {
const currentState = { ...get(modelDownloadStateAtom) } const currentState = { ...get(modelDownloadStateAtom) }
const state = currentState[modelId] const state = currentState[modelId]
if (!state) { if (!state) {
console.error(`Cannot find download state for ${modelId}`) console.debug(`Cannot find download state for ${modelId}`)
toaster({ toaster({
title: 'Cancel Download', title: 'Cancel Download',
description: `Model ${modelId} 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 currentLoadInfor = await monitoring?.getCurrentLoad()
const ram = const ram =
(resourceInfor?.mem?.active ?? 0) / (resourceInfor?.mem?.total ?? 1) (resourceInfor?.mem?.usedMemory ?? 0) /
if (resourceInfor?.mem?.active) setUsedRam(resourceInfor.mem.active) (resourceInfor?.mem?.totalMemory ?? 1)
if (resourceInfor?.mem?.total) setTotalRam(resourceInfor.mem.total) if (resourceInfor?.mem?.usedMemory) setUsedRam(resourceInfor.mem.usedMemory)
if (resourceInfor?.mem?.totalMemory)
setTotalRam(resourceInfor.mem.totalMemory)
setRam(Math.round(ram * 100)) setRam(Math.round(ram * 100))
setCPU(Math.round(currentLoadInfor?.currentLoad ?? 0)) setCPU(Math.round(currentLoadInfor?.cpu?.usage ?? 0))
setCpuUsage(Math.round(currentLoadInfor?.currentLoad ?? 0)) setCpuUsage(Math.round(currentLoadInfor?.cpu?.usage ?? 0))
} }
useEffect(() => { useEffect(() => {
getSystemResources() getSystemResources()
// Fetch interval - every 5s // Fetch interval - every 0.5s
// TODO: Will we really need this? // TODO: Will we really need this?
// There is a possibility that this will be removed and replaced by the process event hook? // There is a possibility that this will be removed and replaced by the process event hook?
const intervalId = setInterval(() => { const intervalId = setInterval(() => {
getSystemResources() getSystemResources()
}, 5000) }, 500)
// clean up interval // clean up interval
return () => clearInterval(intervalId) return () => clearInterval(intervalId)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -21,6 +21,7 @@ export type InputData = {
export type SliderData = { export type SliderData = {
min: number min: number
max: number max: number
step: number step: number
value: number value: number
} }
@ -29,48 +30,58 @@ type CheckboxData = {
checked: boolean checked: boolean
} }
const settingComponentBuilder = (componentData: SettingComponentData[]) => { const settingComponentBuilder = (
const components = componentData.map((data) => { componentData: SettingComponentData[],
switch (data.controllerType) { onlyPrompt?: boolean
case 'slider': ) => {
const { min, max, step, value } = data.controllerData as SliderData const components = componentData
return ( .filter((x) =>
<Slider onlyPrompt ? x.name === 'prompt_template' : x.name !== 'prompt_template'
key={data.name} )
title={data.title} .map((data) => {
min={min} switch (data.controllerType) {
max={max} case 'slider':
step={step} const { min, max, step, value } = data.controllerData as SliderData
value={value} return (
name={data.name} <Slider
/> key={data.name}
) title={data.title}
case 'input': description={data.description}
const { placeholder, value: textValue } = min={min}
data.controllerData as InputData max={max}
return ( step={step}
<ModelConfigInput value={value}
title={data.title} name={data.name}
key={data.name} />
name={data.name} )
placeholder={placeholder} case 'input':
value={textValue} const { placeholder, value: textValue } =
/> data.controllerData as InputData
) return (
case 'checkbox': <ModelConfigInput
const { checked } = data.controllerData as CheckboxData title={data.title}
return ( key={data.name}
<Checkbox name={data.name}
key={data.name} description={data.description}
name={data.name} placeholder={placeholder}
title={data.title} value={textValue}
checked={checked} />
/> )
) case 'checkbox':
default: const { checked } = data.controllerData as CheckboxData
return null 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> 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' import { getUserSpace, openFileExplorer, joinPath } from '@janhq/core'
@ -10,19 +11,21 @@ import { twMerge } from 'tailwind-merge'
import LogoMark from '@/containers/Brand/Logo/Mark' import LogoMark from '@/containers/Brand/Logo/Mark'
import CardSidebar from '@/containers/CardSidebar' import CardSidebar from '@/containers/CardSidebar'
import DropdownListSidebar, { import DropdownListSidebar, {
selectedModelAtom, selectedModelAtom,
} from '@/containers/DropdownListSidebar' } from '@/containers/DropdownListSidebar'
import { FeatureToggleContext } from '@/context/FeatureToggle'
import { useCreateNewThread } from '@/hooks/useCreateNewThread' import { useCreateNewThread } from '@/hooks/useCreateNewThread'
import { getConfigurationsData } from '@/utils/componentSettings'
import { toSettingParams } from '@/utils/model_param' import { toSettingParams } from '@/utils/model_param'
import EngineSetting from '../EngineSetting' import EngineSetting from '../EngineSetting'
import ModelSetting from '../ModelSetting' import ModelSetting from '../ModelSetting'
import settingComponentBuilder from '../ModelSetting/settingComponentBuilder'
import { import {
activeThreadAtom, activeThreadAtom,
getActiveThreadModelParamsAtom, getActiveThreadModelParamsAtom,
@ -34,13 +37,14 @@ export const showRightSideBarAtom = atom<boolean>(true)
const Sidebar: React.FC = () => { const Sidebar: React.FC = () => {
const showing = useAtomValue(showRightSideBarAtom) const showing = useAtomValue(showRightSideBarAtom)
const activeThread = useAtomValue(activeThreadAtom) const activeThread = useAtomValue(activeThreadAtom)
const activeModelParams = useAtomValue(getActiveThreadModelParamsAtom)
const selectedModel = useAtomValue(selectedModelAtom) const selectedModel = useAtomValue(selectedModelAtom)
const { updateThreadMetadata } = useCreateNewThread() const { updateThreadMetadata } = useCreateNewThread()
const threadStates = useAtomValue(threadStatesAtom)
const { experimentalFeatureEnabed } = useContext(FeatureToggleContext)
const activeModelParams = useAtomValue(getActiveThreadModelParamsAtom) const threadStates = useAtomValue(threadStatesAtom)
const modelSettingParams = toSettingParams(activeModelParams)
const modelEngineParams = toSettingParams(activeModelParams)
const componentDataEngineSetting = getConfigurationsData(modelEngineParams)
const onReviewInFinderClick = async (type: string) => { const onReviewInFinderClick = async (type: string) => {
if (!activeThread) return if (!activeThread) return
@ -119,48 +123,43 @@ const Sidebar: React.FC = () => {
> >
<div <div
className={twMerge( 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' showing ? 'animate-enter opacity-100' : 'opacity-0'
)} )}
> >
<CardSidebar <div className="flex flex-col space-y-4 p-4">
title="Thread" <div>
onRevealInFinderClick={onReviewInFinderClick} <label
onViewJsonClick={onViewJsonClick} id="thread-title"
> className="mb-2 inline-block font-bold text-gray-600 dark:text-gray-300"
<div className="flex flex-col space-y-4 p-2"> >
<div> Title
<label </label>
id="thread-title" <Input
className="mb-2 inline-block font-bold text-gray-600 dark:text-gray-300" id="thread-title"
> value={activeThread?.title}
Title onChange={(e) => {
</label> if (activeThread)
<Input updateThreadMetadata({
id="thread-title" ...activeThread,
value={activeThread?.title} title: e.target.value || '',
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> </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 <CardSidebar
title="Assistant" title="Assistant"
onRevealInFinderClick={onReviewInFinderClick} onRevealInFinderClick={onReviewInFinderClick}
@ -176,7 +175,7 @@ const Sidebar: React.FC = () => {
<div> <div>
<label <label
id="thread-title" 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 Instructions
</label> </label>
@ -198,28 +197,60 @@ const Sidebar: React.FC = () => {
}} }}
/> />
</div> </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> </div>
</CardSidebar> </CardSidebar>
{experimentalFeatureEnabed && Object.keys(modelSettingParams).length ? (
<CardSidebar
title="Engine"
onRevealInFinderClick={onReviewInFinderClick}
onViewJsonClick={onViewJsonClick}
>
<div className="p-2">
<EngineSetting />
</div>
</CardSidebar>
) : null}
<CardSidebar <CardSidebar
title="Model" title="Model"
onRevealInFinderClick={onReviewInFinderClick} onRevealInFinderClick={onReviewInFinderClick}
onViewJsonClick={onViewJsonClick} onViewJsonClick={onViewJsonClick}
> >
<div className="p-2"> <div className="px-2">
<DropdownListSidebar />
<div className="mt-4"> <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>
</div> </div>
</CardSidebar> </CardSidebar>

View File

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

View File

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

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