diff --git a/.github/scripts/rename-app-beta.sh b/.github/scripts/rename-app-beta.sh new file mode 100644 index 0000000000..a12d1d635a --- /dev/null +++ b/.github/scripts/rename-app-beta.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +# Check if the correct number of arguments is provided +if [ "$#" -ne 1 ]; then + echo "Usage: $0 " + exit 1 +fi + +INPUT_JSON_FILE="$1" + +# Check if the input file exists +if [ ! -f "$INPUT_JSON_FILE" ]; then + echo "Input file not found: $INPUT_JSON_FILE" + exit 1 +fi + +# Use jq to transform the content +jq ' + .name = "jan-beta" | + .productName = "Jan-beta" | + .build.appId = "jan-beta.ai.app" | + .build.productName = "Jan-beta" | + .build.appId = "jan-beta.ai.app" | + .build.protocols[0].name = "Jan-beta" | + .build.protocols[0].schemes = ["jan-beta"] | + .build.artifactName = "jan-beta-${os}-${arch}-${version}.${ext}" | + .build.publish[0].channel = "beta" +' "$INPUT_JSON_FILE" > ./package.json.tmp + +cat ./package.json.tmp + +rm $INPUT_JSON_FILE +mv ./package.json.tmp $INPUT_JSON_FILE + +# Update the layout file +LAYOUT_FILE_PATH="web/app/layout.tsx" + +if [ ! -f "$LAYOUT_FILE_PATH" ]; then + echo "File does not exist: $LAYOUT_FILE_PATH" + exit 1 +fi + +# Perform the replacements +sed -i -e "s#Jan#Jan-beta#g" "$LAYOUT_FILE_PATH" + +# Notify completion +echo "File has been updated: $LAYOUT_FILE_PATH" \ No newline at end of file diff --git a/.github/scripts/rename-uninstaller-beta.sh b/.github/scripts/rename-uninstaller-beta.sh new file mode 100644 index 0000000000..c322825dab --- /dev/null +++ b/.github/scripts/rename-uninstaller-beta.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# File path to be modified +FILE_PATH="electron/scripts/uninstaller.nsh" + +# Check if the file exists +if [ ! -f "$FILE_PATH" ]; then + echo "File does not exist: $FILE_PATH" + exit 1 +fi + +# Perform the replacements +sed -i -e "s#jan#jan-beta#g" "$FILE_PATH" + +# Notify completion +echo "File has been updated: $FILE_PATH" \ No newline at end of file diff --git a/.github/scripts/rename-workspace-beta.sh b/.github/scripts/rename-workspace-beta.sh new file mode 100644 index 0000000000..6286d18898 --- /dev/null +++ b/.github/scripts/rename-workspace-beta.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# File path to be modified +FILE_PATH="$1" + +# Check if the file exists +if [ ! -f "$FILE_PATH" ]; then + echo "File does not exist: $FILE_PATH" + exit 1 +fi + +# Perform the replacements +sed -i -e 's/yarn workspace jan/yarn workspace jan-beta/g' "$FILE_PATH" + +# Notify completion +echo "File has been updated: $FILE_PATH" \ No newline at end of file diff --git a/.github/workflows/jan-docs.yml b/.github/workflows/jan-docs.yml index ada038e83f..9b5fd97f15 100644 --- a/.github/workflows/jan-docs.yml +++ b/.github/workflows/jan-docs.yml @@ -83,6 +83,6 @@ jobs: accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} projectName: ${{ env.CLOUDFLARE_PROJECT_NAME }} directory: ./docs/out - branch: dev + branch: main # Optional: Enable this if you want to have GitHub Deployments triggered gitHubToken: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/jan-electron-build-beta.yml b/.github/workflows/jan-electron-build-beta.yml new file mode 100644 index 0000000000..4f26252668 --- /dev/null +++ b/.github/workflows/jan-electron-build-beta.yml @@ -0,0 +1,136 @@ +name: Electron Builder - Beta Build + +on: + push: + tags: ["v[0-9]+.[0-9]+.[0-9]+-rc[0-9]+-beta"] + +jobs: + # Job create Update app version based on latest release tag with build number and save to output + get-update-version: + uses: ./.github/workflows/template-get-update-version.yml + + create-draft-release: + runs-on: ubuntu-latest + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') + outputs: + upload_url: ${{ steps.create_release.outputs.upload_url }} + version: ${{ steps.get_version.outputs.version }} + permissions: + contents: write + steps: + - name: Extract tag name without v prefix + id: get_version + run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV && echo "::set-output name=version::${GITHUB_REF#refs/tags/v}" + env: + GITHUB_REF: ${{ github.ref }} + - name: Create Draft Release + id: create_release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.ref_name }} + token: ${{ secrets.GITHUB_TOKEN }} + name: "${{ env.VERSION }}" + draft: true + prerelease: false + generate_release_notes: true + + build-macos-x64: + uses: ./.github/workflows/template-build-macos-x64.yml + secrets: inherit + needs: [get-update-version] + with: + ref: ${{ github.ref }} + public_provider: github + new_version: ${{ needs.get-update-version.outputs.new_version }} + beta: true + + build-macos-arm64: + uses: ./.github/workflows/template-build-macos-arm64.yml + secrets: inherit + needs: [get-update-version] + with: + ref: ${{ github.ref }} + public_provider: github + new_version: ${{ needs.get-update-version.outputs.new_version }} + beta: true + + build-windows-x64: + uses: ./.github/workflows/template-build-windows-x64.yml + secrets: inherit + needs: [get-update-version] + with: + ref: ${{ github.ref }} + public_provider: github + new_version: ${{ needs.get-update-version.outputs.new_version }} + beta: true + + build-linux-x64: + uses: ./.github/workflows/template-build-linux-x64.yml + secrets: inherit + needs: [get-update-version] + with: + ref: ${{ github.ref }} + public_provider: github + new_version: ${{ needs.get-update-version.outputs.new_version }} + beta: true + + combine-beta-mac-yml: + needs: [build-macos-x64, build-macos-arm64, create-draft-release, build-windows-x64, build-linux-x64] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Getting the repo + uses: actions/checkout@v3 + + - name: Download mac-x64 artifacts + uses: actions/download-artifact@v4 + with: + name: beta-mac-x64 + path: ./beta-mac-x64 + - name: Download mac-arm artifacts + uses: actions/download-artifact@v4 + with: + name: beta-mac-arm64 + path: ./beta-mac-arm64 + + - name: 'Merge beta-mac.yml' + # unfortunately electron-builder doesn't understand that we have two different releases for mac-x64 and mac-arm, so we need to manually merge the latest files + # see https://github.com/electron-userland/electron-builder/issues/5592 + run: | + ls -la . + ls -la ./beta-mac-x64 + ls -la ./beta-mac-arm64 + ls -la ./electron + cp ./electron/merge-latest-ymls.js /tmp/merge-beta-ymls.js + npm install js-yaml --prefix /tmp + node /tmp/merge-beta-ymls.js ./beta-mac-x64/beta-mac.yml ./beta-mac-arm64/beta-mac.yml ./beta-mac.yml + cat ./beta-mac.yml + + - name: Yet Another Upload Release Asset Action + uses: shogo82148/actions-upload-release-asset@v1.7.2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ needs.create-draft-release.outputs.upload_url }} + asset_path: ./beta-mac.yml + asset_name: beta-mac.yml + asset_content_type: text/yaml + overwrite: true + + - name: Upload beta-mac.yml + run: | + aws s3 cp ./beta-mac.yml "s3://${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}/temp-beta/beta-mac.yml" + # sync temp-beta to beta by copy files that are different or new + aws s3 sync "s3://${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}/temp-beta/" "s3://${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}/beta/" + env: + AWS_ACCESS_KEY_ID: ${{ secrets.DELTA_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.DELTA_AWS_SECRET_ACCESS_KEY }} + AWS_DEFAULT_REGION: ${{ secrets.DELTA_AWS_REGION }} + AWS_EC2_METADATA_DISABLED: "true" + + - name: set release to prerelease + run: | + gh release edit v${{ needs.create-draft-release.outputs.version }} --draft=false --prerelease + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/jan-electron-build-nightly.yml b/.github/workflows/jan-electron-build-nightly.yml index af1a2baa95..d790809904 100644 --- a/.github/workflows/jan-electron-build-nightly.yml +++ b/.github/workflows/jan-electron-build-nightly.yml @@ -80,7 +80,7 @@ jobs: new_version: ${{ needs.get-update-version.outputs.new_version }} combine-latest-mac-yml: - needs: [set-public-provider, build-macos-x64, build-macos-arm64] + needs: [set-public-provider, build-macos-x64, build-macos-arm64, build-windows-x64, build-linux-x64] runs-on: ubuntu-latest steps: - name: Getting the repo @@ -114,7 +114,8 @@ jobs: - name: Upload latest-mac.yml if: ${{ needs.set-public-provider.outputs.public_provider == 'aws-s3' }} run: | - aws s3 cp ./latest-mac.yml "s3://${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}/latest/latest-mac.yml" + aws s3 cp ./latest-mac.yml "s3://${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}/temp-latest/latest-mac.yml" + aws s3 sync s3://${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}/temp-latest/ s3://${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}/latest/ env: AWS_ACCESS_KEY_ID: ${{ secrets.DELTA_AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.DELTA_AWS_SECRET_ACCESS_KEY }} diff --git a/.github/workflows/jan-electron-linter-and-test.yml b/.github/workflows/jan-electron-linter-and-test.yml index 93d015ea8f..4e20d6c5f8 100644 --- a/.github/workflows/jan-electron-linter-and-test.yml +++ b/.github/workflows/jan-electron-linter-and-test.yml @@ -66,11 +66,7 @@ jobs: test-on-macos: if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) || github.event_name == 'push' || github.event_name == 'workflow_dispatch' - runs-on: ${{ matrix.runs-on }} - strategy: - fail-fast: false - matrix: - runs-on: ['macos-latest', 'macos-13'] + runs-on: [self-hosted, macOS, macos-desktop] steps: - name: Getting the repo uses: actions/checkout@v3 @@ -82,19 +78,40 @@ jobs: with: node-version: 20 + - name: 'Cleanup cache' + continue-on-error: true + run: | + rm -rf ~/jan + make clean + + - name: Get Commit Message for PR + if: github.event_name == 'pull_request' + run: | + echo "REPORT_PORTAL_DESCRIPTION=${{github.event.after}})" >> $GITHUB_ENV + + - name: Get Commit Message for push event + if: github.event_name == 'push' + run: | + echo "REPORT_PORTAL_DESCRIPTION=${{github.sha}})" >> $GITHUB_ENV + + - name: 'Config report portal' + run: | + make update-playwright-config REPORT_PORTAL_URL=${{ secrets.REPORT_PORTAL_URL }} REPORT_PORTAL_API_KEY=${{ secrets.REPORT_PORTAL_API_KEY }} REPORT_PORTAL_PROJECT_NAME=${{ secrets.REPORT_PORTAL_PROJECT_NAME }} REPORT_PORTAL_LAUNCH_NAME="Jan App macos" REPORT_PORTAL_DESCRIPTION="${{env.REPORT_PORTAL_DESCRIPTION}}" + - name: Linter and test run: | + npm config set registry ${{ secrets.NPM_PROXY }} --global + yarn config set registry ${{ secrets.NPM_PROXY }} --global make test env: CSC_IDENTITY_AUTO_DISCOVERY: 'false' + # TURBO_API: '${{ secrets.TURBO_API }}' + # TURBO_TEAM: 'macos' + # TURBO_TOKEN: '${{ secrets.TURBO_TOKEN }}' test-on-macos-pr-target: if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository - runs-on: ${{ matrix.runs-on }} - strategy: - fail-fast: false - matrix: - runs-on: ['macos-latest', 'macos-13'] + runs-on: [self-hosted, macOS, macos-desktop] steps: - name: Getting the repo uses: actions/checkout@v3 @@ -106,8 +123,16 @@ jobs: with: node-version: 20 + - name: 'Cleanup cache' + continue-on-error: true + run: | + rm -rf ~/jan + make clean + - name: Linter and test run: | + npm config set registry https://registry.npmjs.org --global + yarn config set registry https://registry.npmjs.org --global make test env: CSC_IDENTITY_AUTO_DISCOVERY: 'false' @@ -143,16 +168,30 @@ jobs: } make clean + - name: Get Commit Message for push event + if: github.event_name == 'push' + shell: bash + run: | + echo "REPORT_PORTAL_DESCRIPTION=${{github.sha}}" >> $GITHUB_ENV + + - name: 'Config report portal' + shell: bash + run: | + make update-playwright-config REPORT_PORTAL_URL=${{ secrets.REPORT_PORTAL_URL }} REPORT_PORTAL_API_KEY=${{ secrets.REPORT_PORTAL_API_KEY }} REPORT_PORTAL_PROJECT_NAME=${{ secrets.REPORT_PORTAL_PROJECT_NAME }} REPORT_PORTAL_LAUNCH_NAME="Jan App Windows ${{ matrix.antivirus-tools }}" REPORT_PORTAL_DESCRIPTION="${{env.REPORT_PORTAL_DESCRIPTION}}" + - name: Linter and test shell: powershell run: | npm config set registry ${{ secrets.NPM_PROXY }} --global yarn config set registry ${{ secrets.NPM_PROXY }} --global make test - + # env: + # TURBO_API: '${{ secrets.TURBO_API }}' + # TURBO_TEAM: 'windows' + # TURBO_TOKEN: '${{ secrets.TURBO_TOKEN }}' test-on-windows-pr: - if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) || github.event_name == 'push' || github.event_name == 'workflow_dispatch' - runs-on: windows-latest + if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) + runs-on: windows-desktop-default-windows-security steps: - name: Getting the repo uses: actions/checkout@v3 @@ -164,14 +203,44 @@ jobs: with: node-version: 20 + # Clean cache, continue on error + - name: 'Cleanup cache' + shell: powershell + continue-on-error: true + run: | + $path = "$Env:APPDATA\jan" + if (Test-Path $path) { + Remove-Item "\\?\$path" -Recurse -Force + } else { + Write-Output "Folder does not exist." + } + make clean + + - name: Get Commit Message for PR + if: github.event_name == 'pull_request' + shell: bash + run: | + echo "REPORT_PORTAL_DESCRIPTION=${{github.event.after}}" >> $GITHUB_ENV + + - name: 'Config report portal' + shell: bash + run: | + make update-playwright-config REPORT_PORTAL_URL=${{ secrets.REPORT_PORTAL_URL }} REPORT_PORTAL_API_KEY=${{ secrets.REPORT_PORTAL_API_KEY }} REPORT_PORTAL_PROJECT_NAME=${{ secrets.REPORT_PORTAL_PROJECT_NAME }} REPORT_PORTAL_LAUNCH_NAME="Jan App Windows" REPORT_PORTAL_DESCRIPTION="${{env.REPORT_PORTAL_DESCRIPTION}}" + - name: Linter and test shell: powershell run: | + npm config set registry ${{ secrets.NPM_PROXY }} --global + yarn config set registry ${{ secrets.NPM_PROXY }} --global make test + # env: + # TURBO_API: '${{ secrets.TURBO_API }}' + # TURBO_TEAM: 'windows' + # TURBO_TOKEN: '${{ secrets.TURBO_TOKEN }}' test-on-windows-pr-target: if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository - runs-on: windows-latest + runs-on: windows-desktop-default-windows-security steps: - name: Getting the repo uses: actions/checkout@v3 @@ -183,13 +252,28 @@ jobs: with: node-version: 20 + # Clean cache, continue on error + - name: 'Cleanup cache' + shell: powershell + continue-on-error: true + run: | + $path = "$Env:APPDATA\jan" + if (Test-Path $path) { + Remove-Item "\\?\$path" -Recurse -Force + } else { + Write-Output "Folder does not exist." + } + make clean + - name: Linter and test shell: powershell run: | + npm config set registry https://registry.npmjs.org --global + yarn config set registry https://registry.npmjs.org --global make test test-on-ubuntu: - runs-on: ubuntu-latest + runs-on: [self-hosted, Linux, ubuntu-desktop] if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) || github.event_name == 'push' || github.event_name == 'workflow_dispatch' steps: - name: Getting the repo @@ -202,12 +286,41 @@ jobs: with: node-version: 20 + - name: 'Cleanup cache' + continue-on-error: true + run: | + rm -rf ~/jan + make clean + + - name: Get Commit Message for PR + if: github.event_name == 'pull_request' + run: | + echo "REPORT_PORTAL_DESCRIPTION=${{github.event.after}}" >> $GITHUB_ENV + + - name: Get Commit Message for push event + if: github.event_name == 'push' + run: | + echo "REPORT_PORTAL_DESCRIPTION=${{github.sha}}" >> $GITHUB_ENV + + - name: 'Config report portal' + shell: bash + run: | + make update-playwright-config REPORT_PORTAL_URL=${{ secrets.REPORT_PORTAL_URL }} REPORT_PORTAL_API_KEY=${{ secrets.REPORT_PORTAL_API_KEY }} REPORT_PORTAL_PROJECT_NAME=${{ secrets.REPORT_PORTAL_PROJECT_NAME }} REPORT_PORTAL_LAUNCH_NAME="Jan App Linux" REPORT_PORTAL_DESCRIPTION="${{env.REPORT_PORTAL_DESCRIPTION}}" + - name: Linter and test run: | + export DISPLAY=$(w -h | awk 'NR==1 {print $2}') + echo -e "Display ID: $DISPLAY" + npm config set registry ${{ secrets.NPM_PROXY }} --global + yarn config set registry ${{ secrets.NPM_PROXY }} --global make test + # env: + # TURBO_API: '${{ secrets.TURBO_API }}' + # TURBO_TEAM: 'linux' + # TURBO_TOKEN: '${{ secrets.TURBO_TOKEN }}' coverage-check: - runs-on: ubuntu-latest + runs-on: [self-hosted, Linux, ubuntu-desktop] needs: base_branch_cov if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) || github.event_name == 'push' || github.event_name == 'workflow_dispatch' steps: @@ -221,6 +334,12 @@ jobs: with: node-version: 20 + - name: 'Cleanup cache' + continue-on-error: true + run: | + rm -rf ~/jan + make clean + - name: Download code coverage report from base branch uses: actions/download-artifact@v4 with: @@ -228,9 +347,17 @@ jobs: - name: Linter and test coverage run: | + export DISPLAY=$(w -h | awk 'NR==1 {print $2}') + echo -e "Display ID: $DISPLAY" + npm config set registry ${{ secrets.NPM_PROXY }} --global + yarn config set registry ${{ secrets.NPM_PROXY }} --global make lint yarn build:test yarn test:coverage + # env: + # TURBO_API: '${{ secrets.TURBO_API }}' + # TURBO_TEAM: 'linux' + # TURBO_TOKEN: '${{ secrets.TURBO_TOKEN }}' - name: Generate Code Coverage report id: code-coverage @@ -243,7 +370,7 @@ jobs: show-annotations: 'warning' test-on-ubuntu-pr-target: - runs-on: ubuntu-latest + runs-on: [self-hosted, Linux, ubuntu-desktop] if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository steps: - name: Getting the repo @@ -256,6 +383,16 @@ jobs: with: node-version: 20 + - name: 'Cleanup cache' + continue-on-error: true + run: | + rm -rf ~/jan + make clean + - name: Linter and test run: | + export DISPLAY=$(w -h | awk 'NR==1 {print $2}') + echo -e "Display ID: $DISPLAY" + npm config set registry https://registry.npmjs.org --global + yarn config set registry https://registry.npmjs.org --global make test diff --git a/.github/workflows/template-build-linux-x64.yml b/.github/workflows/template-build-linux-x64.yml index 386a0ee4f6..496d153aee 100644 --- a/.github/workflows/template-build-linux-x64.yml +++ b/.github/workflows/template-build-linux-x64.yml @@ -19,6 +19,10 @@ on: required: false type: string default: '/latest/' + beta: + required: false + type: boolean + default: false secrets: DELTA_AWS_S3_BUCKET_NAME: required: false @@ -56,17 +60,29 @@ jobs: mv /tmp/package.json electron/package.json jq --arg version "${{ inputs.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"}, {"provider": "s3", "acl": null, "bucket": "${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}", "region": "${{ secrets.DELTA_AWS_REGION}}", "path": "${{ inputs.aws_s3_prefix }}", "channel": "latest"}]' electron/package.json > /tmp/package.json + jq '.build.publish = [{"provider": "generic", "url": "${{ secrets.CLOUDFLARE_R2_PUBLIC_URL }}", "channel": "latest"}, {"provider": "s3", "acl": null, "bucket": "${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}", "region": "${{ secrets.DELTA_AWS_REGION}}", "path": "temp-latest", "channel": "latest"}]' electron/package.json > /tmp/package.json + mv /tmp/package.json electron/package.json + cat electron/package.json + + - name: Change App Name for beta version + if: inputs.beta == true + shell: bash + run: | + chmod +x .github/scripts/rename-app-beta.sh + .github/scripts/rename-app-beta.sh ./electron/package.json + chmod +x .github/scripts/rename-workspace-beta.sh + .github/scripts/rename-workspace-beta.sh ./package.json + echo "------------------------" + cat ./electron/package.json + echo "------------------------" + cat ./package.json + jq '.build.publish = [{"provider": "generic", "url": "https://delta.jan.ai/beta", "channel": "beta"}, {"provider": "github", "owner": "janhq", "repo": "jan", "channel": "beta"}, {"provider": "s3", "acl": null, "bucket": "${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}", "region": "${{ secrets.DELTA_AWS_REGION}}", "path": "temp-beta", "channel": "beta"}]' electron/package.json > /tmp/package.json mv /tmp/package.json electron/package.json cat electron/package.json - name: Update app version base on tag if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github' run: | - if [[ ! "${VERSION_TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "Error: Tag is not valid!" - exit 1 - fi jq --arg version "${VERSION_TAG#v}" '.version = $version' electron/package.json > /tmp/package.json mv /tmp/package.json electron/package.json jq --arg version "${VERSION_TAG#v}" '.version = $version' web/package.json > /tmp/package.json @@ -92,7 +108,7 @@ jobs: AWS_MAX_ATTEMPTS: "5" - name: Build and publish app to github - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github' + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github' && inputs.beta == false run: | make build-and-publish env: @@ -100,6 +116,17 @@ jobs: ANALYTICS_ID: ${{ secrets.JAN_APP_UMAMI_PROJECT_API_KEY }} ANALYTICS_HOST: ${{ secrets.JAN_APP_UMAMI_URL }} + - name: Build and publish app to github + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github' && inputs.beta == true + run: | + make build-and-publish + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + AWS_ACCESS_KEY_ID: ${{ secrets.DELTA_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.DELTA_AWS_SECRET_ACCESS_KEY }} + AWS_EC2_METADATA_DISABLED: "true" + AWS_MAX_ATTEMPTS: "5" + - name: Upload Artifact .deb file if: inputs.public_provider != 'github' uses: actions/upload-artifact@v4 diff --git a/.github/workflows/template-build-macos-arm64.yml b/.github/workflows/template-build-macos-arm64.yml index 40d40164f2..40cdda627d 100644 --- a/.github/workflows/template-build-macos-arm64.yml +++ b/.github/workflows/template-build-macos-arm64.yml @@ -19,6 +19,10 @@ on: required: false type: string default: '/latest/' + beta: + required: false + type: boolean + default: false secrets: DELTA_AWS_S3_BUCKET_NAME: required: false @@ -68,7 +72,7 @@ jobs: jq --arg version "${{ inputs.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"}, {"provider": "s3", "acl": null, "bucket": "${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}", "region": "${{ secrets.DELTA_AWS_REGION}}", "path": "${{ inputs.aws_s3_prefix }}", "channel": "latest"}]' electron/package.json > /tmp/package.json + jq '.build.publish = [{"provider": "generic", "url": "${{ secrets.CLOUDFLARE_R2_PUBLIC_URL }}", "channel": "latest"}, {"provider": "s3", "acl": null, "bucket": "${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}", "region": "${{ secrets.DELTA_AWS_REGION}}", "path": "temp-latest", "channel": "latest"}]' electron/package.json > /tmp/package.json mv /tmp/package.json electron/package.json jq --arg teamid "${{ secrets.APPLE_TEAM_ID }}" '.build.mac.notarize.teamId = $teamid' electron/package.json > /tmp/package.json @@ -76,13 +80,25 @@ jobs: cat electron/package.json + - name: Change App Name for beta version + if: inputs.beta == true + shell: bash + run: | + chmod +x .github/scripts/rename-app-beta.sh + .github/scripts/rename-app-beta.sh ./electron/package.json + chmod +x .github/scripts/rename-workspace-beta.sh + .github/scripts/rename-workspace-beta.sh ./package.json + echo "------------------------" + cat ./electron/package.json + echo "------------------------" + cat ./package.json + jq '.build.publish = [{"provider": "generic", "url": "https://delta.jan.ai/beta", "channel": "beta"}, {"provider": "github", "owner": "janhq", "repo": "jan", "channel": "beta"}, {"provider": "s3", "acl": null, "bucket": "${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}", "region": "${{ secrets.DELTA_AWS_REGION}}", "path": "temp-beta", "channel": "beta"}]' electron/package.json > /tmp/package.json + mv /tmp/package.json electron/package.json + cat electron/package.json + - name: Update app version base on tag if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github' run: | - if [[ ! "${VERSION_TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "Error: Tag is not valid!" - exit 1 - fi jq --arg version "${VERSION_TAG#v}" '.version = $version' electron/package.json > /tmp/package.json mv /tmp/package.json electron/package.json jq --arg version "${VERSION_TAG#v}" '.version = $version' web/package.json > /tmp/package.json @@ -131,7 +147,7 @@ jobs: AWS_MAX_ATTEMPTS: "5" - name: Build and publish app to github - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github' + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github' && inputs.beta == false run: | make build-and-publish env: @@ -146,6 +162,25 @@ jobs: ANALYTICS_ID: ${{ secrets.JAN_APP_UMAMI_PROJECT_API_KEY }} ANALYTICS_HOST: ${{ secrets.JAN_APP_UMAMI_URL }} + - name: Build and publish app to github + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github' && inputs.beta == true + run: | + make build-and-publish + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CSC_LINK: "/tmp/codesign.p12" + CSC_KEY_PASSWORD: ${{ secrets.CODE_SIGN_P12_PASSWORD }} + CSC_IDENTITY_AUTO_DISCOVERY: "true" + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} + APP_PATH: "." + DEVELOPER_ID: ${{ secrets.DEVELOPER_ID }} + AWS_ACCESS_KEY_ID: ${{ secrets.DELTA_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.DELTA_AWS_SECRET_ACCESS_KEY }} + AWS_DEFAULT_REGION: auto + AWS_EC2_METADATA_DISABLED: "true" + AWS_MAX_ATTEMPTS: "5" + - name: Upload Artifact if: inputs.public_provider != 'github' uses: actions/upload-artifact@v4 @@ -154,7 +189,15 @@ jobs: path: ./electron/dist/jan-mac-arm64-${{ inputs.new_version }}.dmg - name: Upload Artifact + if: inputs.beta == false uses: actions/upload-artifact@v4 with: name: latest-mac-arm64 - path: ./electron/dist/latest-mac.yml \ No newline at end of file + path: ./electron/dist/latest-mac.yml + + - name: Upload Artifact + if: inputs.beta == true + uses: actions/upload-artifact@v4 + with: + name: beta-mac-arm64 + path: ./electron/dist/beta-mac.yml \ No newline at end of file diff --git a/.github/workflows/template-build-macos-x64.yml b/.github/workflows/template-build-macos-x64.yml index b7103fd065..f139797af0 100644 --- a/.github/workflows/template-build-macos-x64.yml +++ b/.github/workflows/template-build-macos-x64.yml @@ -19,6 +19,10 @@ on: required: false type: string default: '/latest/' + beta: + required: false + type: boolean + default: false secrets: DELTA_AWS_S3_BUCKET_NAME: required: false @@ -68,7 +72,7 @@ jobs: jq --arg version "${{ inputs.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"}, {"provider": "s3", "acl": null, "bucket": "${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}", "region": "${{ secrets.DELTA_AWS_REGION}}", "path": "${{ inputs.aws_s3_prefix }}", "channel": "latest"}]' electron/package.json > /tmp/package.json + jq '.build.publish = [{"provider": "generic", "url": "${{ secrets.CLOUDFLARE_R2_PUBLIC_URL }}", "channel": "latest"}, {"provider": "s3", "acl": null, "bucket": "${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}", "region": "${{ secrets.DELTA_AWS_REGION}}", "path": "temp-latest", "channel": "latest"}]' electron/package.json > /tmp/package.json mv /tmp/package.json electron/package.json jq --arg teamid "${{ secrets.APPLE_TEAM_ID }}" '.build.mac.notarize.teamId = $teamid' electron/package.json > /tmp/package.json @@ -76,13 +80,25 @@ jobs: cat electron/package.json + - name: Change App Name for beta version + if: inputs.beta == true + shell: bash + run: | + chmod +x .github/scripts/rename-app-beta.sh + .github/scripts/rename-app-beta.sh ./electron/package.json + chmod +x .github/scripts/rename-workspace-beta.sh + .github/scripts/rename-workspace-beta.sh ./package.json + echo "------------------------" + cat ./electron/package.json + echo "------------------------" + cat ./package.json + jq '.build.publish = [{"provider": "generic", "url": "https://delta.jan.ai/beta", "channel": "beta"}, {"provider": "github", "owner": "janhq", "repo": "jan", "channel": "beta"}, {"provider": "s3", "acl": null, "bucket": "${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}", "region": "${{ secrets.DELTA_AWS_REGION}}", "path": "temp-beta", "channel": "beta"}]' electron/package.json > /tmp/package.json + mv /tmp/package.json electron/package.json + cat electron/package.json + - name: Update app version base on tag if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github' run: | - if [[ ! "${VERSION_TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "Error: Tag is not valid!" - exit 1 - fi jq --arg version "${VERSION_TAG#v}" '.version = $version' electron/package.json > /tmp/package.json mv /tmp/package.json electron/package.json jq --arg version "${VERSION_TAG#v}" '.version = $version' web/package.json > /tmp/package.json @@ -131,7 +147,7 @@ jobs: AWS_MAX_ATTEMPTS: "5" - name: Build and publish app to github - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github' + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github' && inputs.beta == false run: | make build-and-publish env: @@ -146,6 +162,25 @@ jobs: ANALYTICS_ID: ${{ secrets.JAN_APP_UMAMI_PROJECT_API_KEY }} ANALYTICS_HOST: ${{ secrets.JAN_APP_UMAMI_URL }} + - name: Build and publish app to github + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github' && inputs.beta == true + run: | + make build-and-publish + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CSC_LINK: "/tmp/codesign.p12" + CSC_KEY_PASSWORD: ${{ secrets.CODE_SIGN_P12_PASSWORD }} + CSC_IDENTITY_AUTO_DISCOVERY: "true" + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} + APP_PATH: "." + DEVELOPER_ID: ${{ secrets.DEVELOPER_ID }} + AWS_ACCESS_KEY_ID: ${{ secrets.DELTA_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.DELTA_AWS_SECRET_ACCESS_KEY }} + AWS_DEFAULT_REGION: auto + AWS_EC2_METADATA_DISABLED: "true" + AWS_MAX_ATTEMPTS: "5" + - name: Upload Artifact if: inputs.public_provider != 'github' uses: actions/upload-artifact@v4 @@ -154,7 +189,15 @@ jobs: path: ./electron/dist/jan-mac-x64-${{ inputs.new_version }}.dmg - name: Upload Artifact + if: inputs.beta == false uses: actions/upload-artifact@v4 with: name: latest-mac-x64 - path: ./electron/dist/latest-mac.yml \ No newline at end of file + path: ./electron/dist/latest-mac.yml + + - name: Upload Artifact + if: inputs.beta == true + uses: actions/upload-artifact@v4 + with: + name: beta-mac-x64 + path: ./electron/dist/beta-mac.yml \ No newline at end of file diff --git a/.github/workflows/template-build-windows-x64.yml b/.github/workflows/template-build-windows-x64.yml index b1110e3160..ffe94fecc0 100644 --- a/.github/workflows/template-build-windows-x64.yml +++ b/.github/workflows/template-build-windows-x64.yml @@ -19,6 +19,10 @@ on: required: false type: string default: '/latest/' + beta: + required: false + type: boolean + default: false secrets: DELTA_AWS_S3_BUCKET_NAME: required: false @@ -69,21 +73,37 @@ jobs: jq --arg version "${{ inputs.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"}, {"provider": "s3", "acl": null, "bucket": "${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}", "region": "${{ secrets.DELTA_AWS_REGION}}", "path": "${{ inputs.aws_s3_prefix }}", "channel": "latest"}]' electron/package.json > /tmp/package.json + jq '.build.publish = [{"provider": "generic", "url": "${{ secrets.CLOUDFLARE_R2_PUBLIC_URL }}", "channel": "latest"}, {"provider": "s3", "acl": null, "bucket": "${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}", "region": "${{ secrets.DELTA_AWS_REGION}}", "path": "temp-latest", "channel": "latest"}]' electron/package.json > /tmp/package.json mv /tmp/package.json electron/package.json jq '.build.win.sign = "./sign.js"' electron/package.json > /tmp/package.json mv /tmp/package.json electron/package.json cat electron/package.json + - name: Change App Name for beta version + if: inputs.beta == true + shell: bash + run: | + chmod +x .github/scripts/rename-app-beta.sh + .github/scripts/rename-app-beta.sh ./electron/package.json + chmod +x .github/scripts/rename-workspace-beta.sh + .github/scripts/rename-workspace-beta.sh ./package.json + chmod +x .github/scripts/rename-uninstaller-beta.sh + .github/scripts/rename-uninstaller-beta.sh + echo "------------------------" + cat ./electron/package.json + echo "------------------------" + cat ./package.json + echo "------------------------" + cat ./electron/scripts/uninstaller.nsh + jq '.build.publish = [{"provider": "generic", "url": "https://delta.jan.ai/beta", "channel": "beta"}, {"provider": "github", "owner": "janhq", "repo": "jan", "channel": "beta"}, {"provider": "s3", "acl": null, "bucket": "${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}", "region": "${{ secrets.DELTA_AWS_REGION}}", "path": "temp-beta", "channel": "beta"}]' electron/package.json > /tmp/package.json + mv /tmp/package.json electron/package.json + cat electron/package.json + - name: Update app version base on tag if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github' shell: bash run: | - if [[ ! "${VERSION_TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "Error: Tag is not valid!" - exit 1 - fi jq --arg version "${VERSION_TAG#v}" '.version = $version' electron/package.json > /tmp/package.json mv /tmp/package.json electron/package.json jq --arg version "${VERSION_TAG#v}" '.version = $version' web/package.json > /tmp/package.json @@ -113,7 +133,7 @@ jobs: AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} - AZURE_CERT_NAME: ${{ secrets.AZURE_CERT_NAME }} + AZURE_CERT_NAME: homebrewltd AWS_ACCESS_KEY_ID: ${{ secrets.DELTA_AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.DELTA_AWS_SECRET_ACCESS_KEY }} AWS_DEFAULT_REGION: auto @@ -121,7 +141,7 @@ jobs: AWS_MAX_ATTEMPTS: "5" - name: Build app and publish app to github - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github' + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github' && inputs.beta == false run: | make build-and-publish env: @@ -132,7 +152,25 @@ jobs: AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} - AZURE_CERT_NAME: ${{ secrets.AZURE_CERT_NAME }} + AZURE_CERT_NAME: homebrewltd + + - name: Build app and publish app to github + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github' && inputs.beta == true + run: | + make build-and-publish + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + AWS_ACCESS_KEY_ID: ${{ secrets.DELTA_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.DELTA_AWS_SECRET_ACCESS_KEY }} + AWS_DEFAULT_REGION: auto + AWS_EC2_METADATA_DISABLED: "true" + AWS_MAX_ATTEMPTS: "5" + AZURE_KEY_VAULT_URI: ${{ secrets.AZURE_KEY_VAULT_URI }} + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} + # AZURE_CERT_NAME: ${{ secrets.AZURE_CERT_NAME }} + AZURE_CERT_NAME: homebrewltd - name: Upload Artifact if: inputs.public_provider != 'github' diff --git a/electron/package.json b/electron/package.json index ffec404270..feaee5e16d 100644 --- a/electron/package.json +++ b/electron/package.json @@ -77,7 +77,7 @@ }, "scripts": { "lint": "eslint . --ext \".js,.jsx,.ts,.tsx\"", - "test:e2e": "xvfb-maybe -- playwright test --workers=1", + "test:e2e": "playwright test --workers=1", "copy:assets": "rimraf --glob \"./pre-install/*.tgz\" && cpx \"../pre-install/*.tgz\" \"./pre-install\"", "dev": "yarn copy:assets && tsc -p . && electron .", "compile": "tsc -p .", @@ -128,8 +128,7 @@ "rimraf": "^5.0.5", "run-script-os": "^1.1.6", "typescript": "^5.3.3", - "@reportportal/agent-js-playwright": "^5.1.7", - "xvfb-maybe": "^0.2.1" + "@reportportal/agent-js-playwright": "^5.1.7" }, "installConfig": { "hoistingLimits": "workspaces" diff --git a/extensions/inference-martian-extension/resources/settings.json b/extensions/inference-martian-extension/resources/settings.json index 2341ad6cd7..6825099f5e 100644 --- a/extensions/inference-martian-extension/resources/settings.json +++ b/extensions/inference-martian-extension/resources/settings.json @@ -14,7 +14,7 @@ { "key": "chat-completions-endpoint", "title": "Chat Completions Endpoint", - "description": "The endpoint to use for chat completions. See the [Martian API documentation](https://docs.withmartian.com/martian-model-router/api-reference/get-chat-completions) for more information.", + "description": "The endpoint to use for chat completions. See the [Martian API documentation](https://docs.withmartian.com/martian-model-router/getting-started/quickstart-integrating-martian-into-your-codebase) for more information.", "controllerType": "input", "controllerProps": { "placeholder": "https://withmartian.com/api/openai/v1/chat/completions", diff --git a/extensions/inference-nitro-extension/package.json b/extensions/inference-nitro-extension/package.json index 42c31938e8..15ceaf5662 100644 --- a/extensions/inference-nitro-extension/package.json +++ b/extensions/inference-nitro-extension/package.json @@ -1,7 +1,7 @@ { "name": "@janhq/inference-cortex-extension", "productName": "Cortex Inference Engine", - "version": "1.0.19", + "version": "1.0.20", "description": "This extension embeds cortex.cpp, a lightweight inference engine written in C++. See https://jan.ai.\nAdditional dependencies could be installed to run without Cuda Toolkit installation.", "main": "dist/index.js", "node": "dist/node/index.cjs.js", diff --git a/extensions/inference-nitro-extension/resources/models/llava-13b/model.json b/extensions/inference-nitro-extension/resources/models/llava-13b/model.json index caca33b7e0..6d94fd2724 100644 --- a/extensions/inference-nitro-extension/resources/models/llava-13b/model.json +++ b/extensions/inference-nitro-extension/resources/models/llava-13b/model.json @@ -12,7 +12,7 @@ "id": "llava-13b", "object": "model", "name": "LlaVa 13B Q4", - "version": "1.1", + "version": "1.2", "description": "LlaVa can bring vision understanding to Jan", "format": "gguf", "settings": { @@ -24,7 +24,8 @@ "mmproj": "mmproj-model-f16.gguf" }, "parameters": { - "max_tokens": 4096 + "max_tokens": 4096, + "stop": [""] }, "metadata": { "author": "liuhaotian", diff --git a/extensions/inference-nitro-extension/resources/models/llava-7b/model.json b/extensions/inference-nitro-extension/resources/models/llava-7b/model.json index b61ec38c2c..1fdd75247b 100644 --- a/extensions/inference-nitro-extension/resources/models/llava-7b/model.json +++ b/extensions/inference-nitro-extension/resources/models/llava-7b/model.json @@ -12,7 +12,7 @@ "id": "llava-7b", "object": "model", "name": "LlaVa 7B", - "version": "1.1", + "version": "1.2", "description": "LlaVa can bring vision understanding to Jan", "format": "gguf", "settings": { @@ -24,7 +24,8 @@ "mmproj": "mmproj-model-f16.gguf" }, "parameters": { - "max_tokens": 4096 + "max_tokens": 4096, + "stop": [""] }, "metadata": { "author": "liuhaotian", diff --git a/extensions/model-extension/package.json b/extensions/model-extension/package.json index 9a406dcf42..3a694e5a02 100644 --- a/extensions/model-extension/package.json +++ b/extensions/model-extension/package.json @@ -1,7 +1,7 @@ { "name": "@janhq/model-extension", "productName": "Model Management", - "version": "1.0.33", + "version": "1.0.34", "description": "Model Management Extension provides model exploration and seamless downloads", "main": "dist/index.js", "node": "dist/node/index.cjs.js", diff --git a/extensions/model-extension/src/index.ts b/extensions/model-extension/src/index.ts index 6d26d576c7..7e7c12469a 100644 --- a/extensions/model-extension/src/index.ts +++ b/extensions/model-extension/src/index.ts @@ -411,7 +411,8 @@ export default class JanModelExtension extends ModelExtension { .toLowerCase() .includes(JanModelExtension._tensorRtEngineFormat) ) - })?.length > 0 // TODO: find better way (can use basename to check the file name with source url) + // Check if the number of matched files equals the number of sources + })?.length >= model.sources.length ) }) diff --git a/joi/src/core/Tabs/styles.scss b/joi/src/core/Tabs/styles.scss index ce3df013b2..932b8431af 100644 --- a/joi/src/core/Tabs/styles.scss +++ b/joi/src/core/Tabs/styles.scss @@ -35,6 +35,7 @@ flex: 1; height: 38px; display: flex; + white-space: nowrap; color: hsla(var(--text-secondary)); align-items: center; justify-content: center; diff --git a/web/containers/ModelDropdown/index.tsx b/web/containers/ModelDropdown/index.tsx index 0ac016a609..59f19586ae 100644 --- a/web/containers/ModelDropdown/index.tsx +++ b/web/containers/ModelDropdown/index.tsx @@ -306,7 +306,7 @@ const ModelDropdown = ({ className={twMerge('relative', disabled && 'pointer-events-none')} data-testid="model-selector" > -
+
{chatInputMode ? ( = ({ case 'slider': { const { min, max, step, value } = data.controllerProps as SliderComponentProps + return ( setShowTooltip({ max: false, min: false }), null, []) + useEffect(() => { + setVal(value.toString()) + }, [value]) + return (
diff --git a/web/package.json b/web/package.json index 1cf5aa752f..bed94aed42 100644 --- a/web/package.json +++ b/web/package.json @@ -50,7 +50,10 @@ "ulidx": "^2.3.0", "use-debounce": "^10.0.0", "uuid": "^9.0.1", - "zod": "^3.22.4" + "zod": "^3.22.4", + "slate": "latest", + "slate-react": "latest", + "slate-history": "latest" }, "devDependencies": { "@next/eslint-plugin-next": "^14.0.1", diff --git a/web/screens/Hub/index.tsx b/web/screens/Hub/index.tsx index 37adb717c7..8148a6bb5c 100644 --- a/web/screens/Hub/index.tsx +++ b/web/screens/Hub/index.tsx @@ -96,17 +96,19 @@ const HubScreen = () => { {!filteredModels.length ? ( ) : ( -
- { + setSortSelected(value) + }} + options={sortMenus} + /> +
+ + )} -
diff --git a/web/screens/LocalServer/LocalServerLeftPanel/index.tsx b/web/screens/LocalServer/LocalServerLeftPanel/index.tsx index ef2c2d76c5..6f5de80ecd 100644 --- a/web/screens/LocalServer/LocalServerLeftPanel/index.tsx +++ b/web/screens/LocalServer/LocalServerLeftPanel/index.tsx @@ -127,9 +127,10 @@ const LocalServerLeftPanel = () => { {serverEnabled ? 'Stop' : 'Start'} Server {serverEnabled && ( - )} diff --git a/web/screens/Settings/Advanced/index.tsx b/web/screens/Settings/Advanced/index.tsx index 2c444371a8..5b78194ab7 100644 --- a/web/screens/Settings/Advanced/index.tsx +++ b/web/screens/Settings/Advanced/index.tsx @@ -211,9 +211,6 @@ const Advanced = () => { saveSettings({ gpusInUse: updatedGpusInUse }) } - const gpuSelectionPlaceHolder = - gpuList.length > 0 ? 'Select GPU' : "You don't have any compatible GPU" - /** * Handle click outside */ @@ -317,81 +314,86 @@ const Advanced = () => { />
-
- -
- - } - onClick={() => setOpen(!open)} - /> -
-
-

{vulkanEnabled ? 'Vulkan Supported GPUs' : 'Nvidia'}

-
-
- {gpuList - .filter((gpu) => - vulkanEnabled - ? gpu.name - : gpu.name?.toLowerCase().includes('nvidia') - ) - .map((gpu) => ( -
- handleGPUChange(gpu.id)} - label={ - - {gpu.name} - {!vulkanEnabled && ( - {gpu.vram}MB VRAM - )} - - } - /> -
- ))} -
- {gpuEnabled && gpusInUse.length > 1 && ( -
- -

- If multi-GPU is enabled with different GPU models or - without NVLink, it could impact token speed. -

+ + {gpuList.length > 0 && ( +
+ +
+ + } + onClick={() => setOpen(!open)} + /> +
+
+

+ {vulkanEnabled ? 'Vulkan Supported GPUs' : 'Nvidia'} +

+
+
+ {gpuList + .filter((gpu) => + vulkanEnabled + ? gpu.name + : gpu.name?.toLowerCase().includes('nvidia') + ) + .map((gpu) => ( +
+ handleGPUChange(gpu.id)} + label={ + + {gpu.name} + {!vulkanEnabled && ( + {gpu.vram}MB VRAM + )} + + } + /> +
+ ))}
- )} + {gpuEnabled && gpusInUse.length > 1 && ( +
+ +

+ If multi-GPU is enabled with different GPU models + or without NVLink, it could impact token speed. +

+
+ )} +
-
+ )}
)} diff --git a/web/screens/Thread/ThreadCenterPanel/ChatInput/RichTextEditor.tsx b/web/screens/Thread/ThreadCenterPanel/ChatInput/RichTextEditor.tsx new file mode 100644 index 0000000000..8f296368f4 --- /dev/null +++ b/web/screens/Thread/ThreadCenterPanel/ChatInput/RichTextEditor.tsx @@ -0,0 +1,408 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { useCallback, useEffect, useRef, useState } from 'react' + +import { MessageStatus } from '@janhq/core' +import hljs from 'highlight.js' + +import { useAtom, useAtomValue } from 'jotai' +import { BaseEditor, createEditor, Editor, Element, Transforms } from 'slate' +import { withHistory } from 'slate-history' // Import withHistory +import { + Editable, + ReactEditor, + Slate, + withReact, + RenderLeafProps, +} from 'slate-react' + +import { twMerge } from 'tailwind-merge' + +import { currentPromptAtom } from '@/containers/Providers/Jotai' + +import { useActiveModel } from '@/hooks/useActiveModel' +import useSendChatMessage from '@/hooks/useSendChatMessage' + +import { getCurrentChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom' + +import { + getActiveThreadIdAtom, + activeSettingInputBoxAtom, +} from '@/helpers/atoms/Thread.atom' + +type CustomElement = { + type: 'paragraph' | 'code' | null + children: CustomText[] + language?: string // Store the language for code blocks +} +type CustomText = { + text: string + code?: boolean + language?: string + className?: string + type?: 'paragraph' | 'code' // Add the type property + format?: 'bold' | 'italic' +} + +declare module 'slate' { + interface CustomTypes { + Editor: BaseEditor & ReactEditor + Element: CustomElement + Text: CustomText + } +} + +const initialValue: CustomElement[] = [ + { + type: 'paragraph', + children: [{ text: '' }], + }, +] + +type RichTextEditorProps = React.TextareaHTMLAttributes + +const RichTextEditor = ({ + className, + style, + disabled, + placeholder, + spellCheck, +}: RichTextEditorProps) => { + const [editor] = useState(() => withHistory(withReact(createEditor()))) + const currentLanguage = useRef('plaintext') + const [currentPrompt, setCurrentPrompt] = useAtom(currentPromptAtom) + const textareaRef = useRef(null) + const activeThreadId = useAtomValue(getActiveThreadIdAtom) + const activeSettingInputBox = useAtomValue(activeSettingInputBoxAtom) + const messages = useAtomValue(getCurrentChatMessagesAtom) + const { sendChatMessage } = useSendChatMessage() + const { stopInference } = useActiveModel() + + // The decorate function identifies code blocks and marks the ranges + const decorate = useCallback( + (entry: [any, any]) => { + const ranges: any[] = [] + const [node, path] = entry + + if (Editor.isBlock(editor, node) && node.type === 'paragraph') { + node.children.forEach((child: { text: any }, childIndex: number) => { + const text = child.text + + // Match bold text pattern *text* + const boldMatches = [...text.matchAll(/(\*.*?\*)/g)] // Find bold patterns + boldMatches.forEach((match) => { + const startOffset = match.index + 1 || 0 + const length = match[0].length - 2 + + ranges.push({ + anchor: { path: [...path, childIndex], offset: startOffset }, + focus: { + path: [...path, childIndex], + offset: startOffset + length, + }, + format: 'italic', + className: 'italic', + }) + }) + }) + } + + if (Editor.isBlock(editor, node) && node.type === 'paragraph') { + node.children.forEach((child: { text: any }, childIndex: number) => { + const text = child.text + + // Match bold text pattern **text** + const boldMatches = [...text.matchAll(/(\*\*.*?\*\*)/g)] // Find bold patterns + boldMatches.forEach((match) => { + const startOffset = match.index + 2 || 0 + const length = match[0].length - 4 + + ranges.push({ + anchor: { path: [...path, childIndex], offset: startOffset }, + focus: { + path: [...path, childIndex], + offset: startOffset + length, + }, + format: 'bold', + className: 'font-bold', + }) + }) + }) + } + + if (Editor.isBlock(editor, node) && node.type === 'code') { + node.children.forEach((child: { text: any }, childIndex: number) => { + const text = child.text + + // Match code block start and end + const startMatch = text.match(/^```(\w*)$/) + const endMatch = text.match(/^```$/) + const inlineMatch = text.match(/^`([^`]+)`$/) // Match inline code + + if (startMatch) { + // If it's the start of a code block, store the language + currentLanguage.current = startMatch[1] || 'plaintext' + } else if (endMatch) { + // Reset language when code block ends + currentLanguage.current = 'plaintext' + } else if (inlineMatch) { + // Apply syntax highlighting to inline code + const codeContent = inlineMatch[1] // Get the content within the backticks + try { + hljs.highlight(codeContent, { + language: + currentLanguage.current.length > 1 + ? currentLanguage.current + : 'plaintext', + }).value + } catch (err) { + hljs.highlight(codeContent, { + language: 'javascript', + }).value + } + + // Calculate the range for the inline code + const length = codeContent.length + ranges.push({ + anchor: { + path: [...path, childIndex], + offset: inlineMatch.index + 1, + }, + focus: { + path: [...path, childIndex], + offset: inlineMatch.index + 1 + length, + }, + type: 'code', + code: true, + language: currentLanguage.current, + className: '', // Specify class name if needed + }) + } else if (currentLanguage.current !== 'plaintext') { + // Highlight entire code line if in a code block + const leadingSpaces = text.match(/^\s*/)?.[0] ?? '' // Capture leading spaces + const codeContent = text.trimStart() // Remove leading spaces for highlighting + + let highlighted = '' + highlighted = hljs.highlightAuto(codeContent).value + try { + highlighted = hljs.highlight(codeContent, { + language: + currentLanguage.current.length > 1 + ? currentLanguage.current + : 'plaintext', + }).value + } catch (err) { + highlighted = hljs.highlight(codeContent, { + language: 'javascript', + }).value + } + + const parser = new DOMParser() + const doc = parser.parseFromString(highlighted, 'text/html') + + let slateTextIndex = 0 + + // Adjust to include leading spaces in the ranges and preserve formatting + ranges.push({ + anchor: { path: [...path, childIndex], offset: 0 }, + focus: { + path: [...path, childIndex], + offset: leadingSpaces.length, + }, + type: 'code', + code: true, + language: currentLanguage.current, + className: '', // No class for leading spaces + }) + + doc.body.childNodes.forEach((childNode) => { + const childText = childNode.textContent || '' + const length = childText.length + const className = + childNode.nodeType === Node.ELEMENT_NODE + ? (childNode as HTMLElement).className + : '' + + ranges.push({ + anchor: { + path: [...path, childIndex], + offset: slateTextIndex + leadingSpaces.length, + }, + focus: { + path: [...path, childIndex], + offset: slateTextIndex + leadingSpaces.length + length, + }, + type: 'code', + code: true, + language: currentLanguage.current, + className, + }) + + slateTextIndex += length + }) + } else { + ranges.push({ + anchor: { path: [...path, childIndex], offset: 0 }, + focus: { path: [...path, childIndex], offset: text.length }, + type: 'paragraph', // Treat as a paragraph + code: false, + }) + } + }) + } + + return ranges + }, + [editor] + ) + + // RenderLeaf applies the decoration styles + const renderLeaf = useCallback( + ({ attributes, children, leaf }: RenderLeafProps) => { + if (leaf.format === 'italic') { + return ( + + {children} + + ) + } + if (leaf.format === 'bold') { + return ( + + {children} + + ) + } + if (leaf.code) { + // Apply syntax highlighting to code blocks + return ( + + {children} + + ) + } + + return {children} + }, + [] + ) + + useEffect(() => { + if (textareaRef.current) { + textareaRef.current.focus() + } + }, [activeThreadId]) + + useEffect(() => { + if (textareaRef.current?.clientHeight) { + textareaRef.current.style.height = activeSettingInputBox + ? '100px' + : '40px' + textareaRef.current.style.height = textareaRef.current.scrollHeight + 'px' + textareaRef.current.style.overflow = + textareaRef.current.clientHeight >= 390 ? 'auto' : 'hidden' + } + }, [textareaRef.current?.clientHeight, currentPrompt, activeSettingInputBox]) + + const onStopInferenceClick = async () => { + stopInference() + } + + const resetEditor = useCallback(() => { + Transforms.delete(editor, { + at: { + anchor: Editor.start(editor, []), + focus: Editor.end(editor, []), + }, + }) + + // Adjust the height of the textarea to its initial state + if (textareaRef.current) { + textareaRef.current.style.height = '40px' // Reset to the initial height or your desired height + textareaRef.current.style.overflow = 'hidden' // Reset overflow style + } + + // Ensure the editor re-renders decorations + editor.onChange() + }, [editor]) + + const handleKeyDown = useCallback( + (event: React.KeyboardEvent) => { + if (event.key === 'Enter' && !event.shiftKey) { + event.preventDefault() + if (messages[messages.length - 1]?.status !== MessageStatus.Pending) { + sendChatMessage(currentPrompt) + resetEditor() + } else onStopInferenceClick() + } + + if (event.key === '`') { + // Determine whether any of the currently selected blocks are code blocks. + const [match] = Editor.nodes(editor, { + match: (n) => + Element.isElement(n) && (n as CustomElement).type === 'code', + }) + // Toggle the block type dependsing on whether there's already a match. + Transforms.setNodes( + editor, + { type: match ? 'paragraph' : 'code' }, + { match: (n) => Element.isElement(n) && Editor.isBlock(editor, n) } + ) + } + + if (event.key === 'Tab') { + const [match] = Editor.nodes(editor, { + match: (n) => { + return (n as CustomElement).type === 'code' + }, + mode: 'lowest', + }) + + if (match) { + event.preventDefault() + // Insert a tab character + Editor.insertText(editor, ' ') // Insert 2 spaces + } + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [currentPrompt, editor, messages] + ) + + return ( + { + const combinedText = value + .map((block) => { + if ('children' in block) { + return block.children.map((child) => child.text).join('') + } + return '' + }) + .join('\n') + + setCurrentPrompt(combinedText) + }} + > + + + ) +} + +export default RichTextEditor diff --git a/web/screens/Thread/ThreadCenterPanel/ChatInput/index.tsx b/web/screens/Thread/ThreadCenterPanel/ChatInput/index.tsx index a7c5ad1216..83a68fa8a9 100644 --- a/web/screens/Thread/ThreadCenterPanel/ChatInput/index.tsx +++ b/web/screens/Thread/ThreadCenterPanel/ChatInput/index.tsx @@ -29,6 +29,8 @@ import { isLocalEngine } from '@/utils/modelEngine' import FileUploadPreview from '../FileUploadPreview' import ImageUploadPreview from '../ImageUploadPreview' +import RichTextEditor from './RichTextEditor' + import { showRightPanelAtom } from '@/helpers/atoms/App.atom' import { experimentalFeatureEnabledAtom } from '@/helpers/atoms/AppConfig.atom' import { getCurrentChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom' @@ -40,7 +42,6 @@ import { getActiveThreadIdAtom, isGeneratingResponseAtom, threadStatesAtom, - waitingToSendMessage, } from '@/helpers/atoms/Thread.atom' import { activeTabThreadRightPanelAtom } from '@/helpers/atoms/ThreadRightPanel.atom' @@ -48,7 +49,6 @@ const ChatInput = () => { const activeThread = useAtomValue(activeThreadAtom) const { stateModel } = useActiveModel() const messages = useAtomValue(getCurrentChatMessagesAtom) - // const [activeSetting, setActiveSetting] = useState(false) const spellCheck = useAtomValue(spellCheckAtom) const [currentPrompt, setCurrentPrompt] = useAtom(currentPromptAtom) @@ -59,7 +59,6 @@ const ChatInput = () => { const selectedModel = useAtomValue(selectedModelAtom) const activeThreadId = useAtomValue(getActiveThreadIdAtom) - const [isWaitingToSend, setIsWaitingToSend] = useAtom(waitingToSendMessage) const [fileUpload, setFileUpload] = useAtom(fileUploadAtom) const [showAttacmentMenus, setShowAttacmentMenus] = useState(false) const textareaRef = useRef(null) @@ -78,52 +77,15 @@ const ChatInput = () => { (threadState) => threadState.waitingForResponse ) - const onPromptChange = (e: React.ChangeEvent) => { - setCurrentPrompt(e.target.value) - } - const refAttachmentMenus = useClickOutside(() => setShowAttacmentMenus(false)) const [showRightPanel, setShowRightPanel] = useAtom(showRightPanelAtom) - useEffect(() => { - if (isWaitingToSend && activeThreadId) { - setIsWaitingToSend(false) - sendChatMessage(currentPrompt) - } - }, [ - activeThreadId, - isWaitingToSend, - currentPrompt, - setIsWaitingToSend, - sendChatMessage, - ]) - useEffect(() => { if (textareaRef.current) { textareaRef.current.focus() } }, [activeThreadId]) - useEffect(() => { - if (textareaRef.current?.clientHeight) { - textareaRef.current.style.height = activeSettingInputBox - ? '100px' - : '40px' - textareaRef.current.style.height = textareaRef.current.scrollHeight + 'px' - textareaRef.current.style.overflow = - textareaRef.current.clientHeight >= 390 ? 'auto' : 'hidden' - } - }, [textareaRef.current?.clientHeight, currentPrompt, activeSettingInputBox]) - - const onKeyDown = async (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) { - e.preventDefault() - if (messages[messages.length - 1]?.status !== MessageStatus.Pending) - sendChatMessage(currentPrompt) - else onStopInferenceClick() - } - } - const onStopInferenceClick = async () => { stopInference() } @@ -163,22 +125,25 @@ const ChatInput = () => {
{renderPreview(fileUpload)} -