Merge branch 'dev' into feat/old-mac-support

This commit is contained in:
Sherzod Mutalov 2025-07-13 16:29:36 +05:00 committed by GitHub
commit 1ff62de3b6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
53 changed files with 2489 additions and 959 deletions

View File

@ -1,24 +0,0 @@
#!/usr/bin/env bash
make clean
# To reproduce https://github.com/menloresearch/jan/pull/5463
TAURI_TOOLKIT_PATH="${XDG_CACHE_HOME:-$HOME/.cache}/tauri"
mkdir -p "$TAURI_TOOLKIT_PATH"
wget https://github.com/linuxdeploy/linuxdeploy/releases/download/1-alpha-20250213-2/linuxdeploy-x86_64.AppImage -O "$TAURI_TOOLKIT_PATH/linuxdeploy-x86_64.AppImage"
chmod +x "$TAURI_TOOLKIT_PATH/linuxdeploy-x86_64.AppImage"
jq '.bundle.resources = ["resources/pre-install/**/*"] | .bundle.externalBin = ["binaries/cortex-server", "resources/bin/uv"]' ./src-tauri/tauri.conf.json > /tmp/tauri.conf.json
mv /tmp/tauri.conf.json ./src-tauri/tauri.conf.json
make build-tauri
cp ./src-tauri/resources/bin/bun ./src-tauri/target/release/bundle/appimage/Jan.AppDir/usr/bin/bun
mkdir -p ./src-tauri/target/release/bundle/appimage/Jan.AppDir/usr/lib/Jan/binaries/engines
cp -f ./src-tauri/binaries/deps/*.so* ./src-tauri/target/release/bundle/appimage/Jan.AppDir/usr/lib/Jan/binaries/
cp -f ./src-tauri/binaries/*.so* ./src-tauri/target/release/bundle/appimage/Jan.AppDir/usr/lib/Jan/binaries/
cp -rf ./src-tauri/binaries/engines ./src-tauri/target/release/bundle/appimage/Jan.AppDir/usr/lib/Jan/binaries/
APP_IMAGE=./src-tauri/target/release/bundle/appimage/$(ls ./src-tauri/target/release/bundle/appimage/ | grep AppImage | head -1)
echo $APP_IMAGE
rm -f $APP_IMAGE
/opt/bin/appimagetool ./src-tauri/target/release/bundle/appimage/Jan.AppDir $APP_IMAGE

View File

@ -14,7 +14,3 @@ sudo apt install -yqq libwebkit2gtk-4.1-dev \
librsvg2-dev \ librsvg2-dev \
xdg-utils \ xdg-utils \
libfuse2 libfuse2
sudo mkdir -p /opt/bin
sudo wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage -O /opt/bin/appimagetool
sudo chmod +x /opt/bin/appimagetool

View File

@ -1,28 +0,0 @@
import hashlib
import base64
import sys
def hash_file(file_path):
# Create a SHA-512 hash object
sha512 = hashlib.sha512()
# Read and update the hash object with the content of the file
with open(file_path, 'rb') as f:
while True:
data = f.read(1024 * 1024) # Read in 1 MB chunks
if not data:
break
sha512.update(data)
# Obtain the hash result and encode it in base64
hash_base64 = base64.b64encode(sha512.digest()).decode('utf-8')
return hash_base64
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python3 script.py <file_path>")
sys.exit(1)
file_path = sys.argv[1]
hash_base64_output = hash_file(file_path)
print(hash_base64_output)

View File

@ -38,6 +38,7 @@ on:
jobs: jobs:
base_branch_cov: base_branch_cov:
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest runs-on: ubuntu-latest
continue-on-error: true continue-on-error: true
steps: steps:
@ -49,20 +50,25 @@ jobs:
with: with:
node-version: 20 node-version: 20
- name: 'Cleanup cache'
continue-on-error: true
run: |
rm -rf ~/jan
make clean
- name: Install dependencies - name: Install dependencies
run: | run: |
make config-yarn make lint
yarn
yarn build:core
- name: Run test coverage - name: Run test coverage
run: yarn test:coverage run: |
yarn test:coverage
- name: Upload code coverage for ref branch - name: Upload code coverage for ref branch
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: ref-lcov.info name: ref-lcov.info
path: ./coverage/lcov.info path: coverage/merged/lcov.info
test-on-macos: test-on-macos:
runs-on: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) && 'macos-latest' || 'macos-selfhosted-12-arm64' }} runs-on: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) && 'macos-latest' || 'macos-selfhosted-12-arm64' }}
@ -78,10 +84,6 @@ jobs:
with: with:
node-version: 20 node-version: 20
- name: Set IS_TEST environment variable
if: github.event.pull_request.head.repo.full_name == github.repository
run: echo "IS_TEST=true" >> $GITHUB_ENV
- name: 'Cleanup cache' - name: 'Cleanup cache'
continue-on-error: true continue-on-error: true
run: | run: |
@ -223,50 +225,45 @@ jobs:
path: electron/playwright-report/ path: electron/playwright-report/
retention-days: 2 retention-days: 2
# coverage-check: coverage-check:
# runs-on: ubuntu-latest if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
# needs: base_branch_cov runs-on: ubuntu-latest
# continue-on-error: true 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' continue-on-error: true
# steps: steps:
# - name: Getting the repo - name: Getting the repo
# uses: actions/checkout@v3 uses: actions/checkout@v3
# with: with:
# fetch-depth: 0 fetch-depth: 0
# - name: Installing node - name: Installing node
# uses: actions/setup-node@v3 uses: actions/setup-node@v3
# with: with:
# node-version: 20 node-version: 20
- name: 'Cleanup cache'
continue-on-error: true
run: |
rm -rf ~/jan
make clean
# - name: Install yarn - name: Install dependencies
# run: npm install -g yarn run: |
make lint
# - name: 'Cleanup cache' - name: Run test coverage
# continue-on-error: true run: |
# run: | yarn test:coverage
# rm -rf ~/jan
# make clean
# - name: Download code coverage report from base branch - name: Download code coverage report from base branch
# uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
# with: with:
# name: ref-lcov.info name: ref-lcov.info
- name: Generate Code Coverage report
# - name: Linter and test coverage id: code-coverage
# run: | uses: barecheck/code-coverage-action@v1
# export DISPLAY=$(w -h | awk 'NR==1 {print $2}') with:
# echo -e "Display ID: $DISPLAY" github-token: ${{ secrets.GITHUB_TOKEN }}
# make lint lcov-file: './coverage/merged/lcov.info'
# yarn build:test base-lcov-file: './lcov.info'
# yarn test:coverage send-summary-comment: true
show-annotations: 'warning'
# - name: Generate Code Coverage report
# id: code-coverage
# uses: barecheck/code-coverage-action@v1
# with:
# github-token: ${{ secrets.GITHUB_TOKEN }}
# lcov-file: './coverage/lcov.info'
# base-lcov-file: './lcov.info'
# send-summary-comment: true
# show-annotations: 'warning'

View File

@ -39,8 +39,6 @@ on:
required: false required: false
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: TAURI_SIGNING_PRIVATE_KEY_PASSWORD:
required: false required: false
TAURI_SIGNING_PUBLIC_KEY:
required: false
outputs: outputs:
DEB_SIG: DEB_SIG:
value: ${{ jobs.build-linux-x64.outputs.DEB_SIG }} value: ${{ jobs.build-linux-x64.outputs.DEB_SIG }}
@ -104,20 +102,15 @@ jobs:
run: | run: |
echo "Version: ${{ inputs.new_version }}" echo "Version: ${{ inputs.new_version }}"
# Update tauri.conf.json # Update tauri.conf.json
jq --arg version "${{ inputs.new_version }}" '.version = $version | .bundle.createUpdaterArtifacts = true | .bundle.resources = ["resources/pre-install/**/*"] | .bundle.externalBin = ["binaries/cortex-server", "resources/bin/uv"]' ./src-tauri/tauri.conf.json > /tmp/tauri.conf.json jq --arg version "${{ inputs.new_version }}" '.version = $version | .bundle.createUpdaterArtifacts = true' ./src-tauri/tauri.conf.json > /tmp/tauri.conf.json
mv /tmp/tauri.conf.json ./src-tauri/tauri.conf.json mv /tmp/tauri.conf.json ./src-tauri/tauri.conf.json
if [ "${{ inputs.channel }}" != "stable" ]; then if [ "${{ inputs.channel }}" != "stable" ]; then
jq '.bundle.linux.deb.files = {"usr/bin/bun": "resources/bin/bun", jq '.bundle.linux.deb.files = {"usr/bin/bun": "resources/bin/bun",
"usr/lib/Jan-${{ inputs.channel }}/binaries": "binaries/deps", "usr/lib/Jan-${{ inputs.channel }}/binaries": "binaries/deps",
"usr/lib/Jan-${{ inputs.channel }}/binaries/engines": "binaries/engines", "usr/lib/Jan-${{ inputs.channel }}/binaries/engines": "binaries/engines",
"usr/lib/Jan-${{ inputs.channel }}/binaries/libvulkan.so": "binaries/libvulkan.so"}' ./src-tauri/tauri.conf.json > /tmp/tauri.conf.json "usr/lib/Jan-${{ inputs.channel }}/binaries/libvulkan.so": "binaries/libvulkan.so"}' ./src-tauri/tauri.linux.conf.json > /tmp/tauri.linux.conf.json
else mv /tmp/tauri.linux.conf.json ./src-tauri/tauri.linux.conf.json
jq '.bundle.linux.deb.files = {"usr/bin/bun": "resources/bin/bun",
"usr/lib/Jan/binaries": "binaries/deps",
"usr/lib/Jan/binaries/engines": "binaries/engines",
"usr/lib/Jan/binaries/libvulkan.so": "binaries/libvulkan.so"}' ./src-tauri/tauri.conf.json > /tmp/tauri.conf.json
fi fi
mv /tmp/tauri.conf.json ./src-tauri/tauri.conf.json
jq --arg version "${{ inputs.new_version }}" '.version = $version' web-app/package.json > /tmp/package.json jq --arg version "${{ inputs.new_version }}" '.version = $version' web-app/package.json > /tmp/package.json
mv /tmp/package.json web-app/package.json mv /tmp/package.json web-app/package.json
@ -150,55 +143,21 @@ jobs:
fi fi
- name: Build app - name: Build app
run: | run: |
# Pin linuxdeploy version to prevent @tauri-apps/cli-linux-x64-gnu from pulling in an outdated version
TAURI_TOOLKIT_PATH="${XDG_CACHE_HOME:-$HOME/.cache}/tauri"
mkdir -p "$TAURI_TOOLKIT_PATH"
wget https://github.com/linuxdeploy/linuxdeploy/releases/download/1-alpha-20250213-2/linuxdeploy-x86_64.AppImage -O "$TAURI_TOOLKIT_PATH/linuxdeploy-x86_64.AppImage"
chmod +x "$TAURI_TOOLKIT_PATH/linuxdeploy-x86_64.AppImage"
make build-tauri make build-tauri
# Copy engines and bun to appimage
wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage -O ./appimagetool
chmod +x ./appimagetool
if [ "${{ inputs.channel }}" != "stable" ]; then
ls ./src-tauri/target/release/bundle/appimage/
cp ./src-tauri/resources/bin/bun ./src-tauri/target/release/bundle/appimage/Jan-${{ inputs.channel }}.AppDir/usr/bin/bun
mkdir -p ./src-tauri/target/release/bundle/appimage/Jan-${{ inputs.channel }}.AppDir/usr/lib/Jan-${{ inputs.channel }}/binaries/engines
cp -f ./src-tauri/binaries/deps/*.so* ./src-tauri/target/release/bundle/appimage/Jan-${{ inputs.channel }}.AppDir/usr/lib/Jan-${{ inputs.channel }}/binaries/
cp -f ./src-tauri/binaries/*.so* ./src-tauri/target/release/bundle/appimage/Jan-${{ inputs.channel }}.AppDir/usr/lib/Jan-${{ inputs.channel }}/binaries/
cp -rf ./src-tauri/binaries/engines ./src-tauri/target/release/bundle/appimage/Jan-${{ inputs.channel }}.AppDir/usr/lib/Jan-${{ inputs.channel }}/binaries/
APP_IMAGE=./src-tauri/target/release/bundle/appimage/$(ls ./src-tauri/target/release/bundle/appimage/ | grep .AppImage | head -1)
echo $APP_IMAGE
rm -f $APP_IMAGE
./appimagetool ./src-tauri/target/release/bundle/appimage/Jan-${{ inputs.channel }}.AppDir $APP_IMAGE
yarn tauri signer sign \
--private-key "$TAURI_SIGNING_PRIVATE_KEY" \
--password "$TAURI_SIGNING_PRIVATE_KEY_PASSWORD" \
"$APP_IMAGE"
else
cp ./src-tauri/resources/bin/bun ./src-tauri/target/release/bundle/appimage/Jan.AppDir/usr/bin/bun
mkdir -p ./src-tauri/target/release/bundle/appimage/Jan.AppDir/usr/lib/Jan/binaries/engines
cp -f ./src-tauri/binaries/deps/*.so* ./src-tauri/target/release/bundle/appimage/Jan.AppDir/usr/lib/Jan/binaries/
cp -f ./src-tauri/binaries/*.so* ./src-tauri/target/release/bundle/appimage/Jan.AppDir/usr/lib/Jan/binaries/
cp -rf ./src-tauri/binaries/engines ./src-tauri/target/release/bundle/appimage/Jan.AppDir/usr/lib/Jan/binaries/
APP_IMAGE=./src-tauri/target/release/bundle/appimage/$(ls ./src-tauri/target/release/bundle/appimage/ | grep AppImage | head -1) APP_IMAGE=./src-tauri/target/release/bundle/appimage/$(ls ./src-tauri/target/release/bundle/appimage/ | grep AppImage | head -1)
echo $APP_IMAGE
rm -f $APP_IMAGE
./appimagetool ./src-tauri/target/release/bundle/appimage/Jan.AppDir $APP_IMAGE
yarn tauri signer sign \ yarn tauri signer sign \
--private-key "$TAURI_SIGNING_PRIVATE_KEY" \ --private-key "$TAURI_SIGNING_PRIVATE_KEY" \
--password "$TAURI_SIGNING_PRIVATE_KEY_PASSWORD" \ --password "$TAURI_SIGNING_PRIVATE_KEY_PASSWORD" \
"$APP_IMAGE" "$APP_IMAGE"
fi
env: env:
RELEASE_CHANNEL: '${{ inputs.channel }}'
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }} POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }} POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }}
# CORTEX_API_PORT: ${{ inputs.cortex_api_port }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
TAURI_SIGNING_PUBLIC_KEY: ${{ secrets.TAURI_SIGNING_PUBLIC_KEY }}
# Publish app # Publish app
@ -217,8 +176,8 @@ jobs:
name: jan-linux-amd64-${{ inputs.new_version }}-AppImage name: jan-linux-amd64-${{ inputs.new_version }}-AppImage
path: ./src-tauri/target/release/bundle/appimage/*.AppImage path: ./src-tauri/target/release/bundle/appimage/*.AppImage
## create zip file and latest-linux.yml for linux electron auto updater ## Set output filename for linux
- name: Create zip file and latest-linux.yml for linux electron auto updater - name: Set output filename for linux
id: packageinfo id: packageinfo
run: | run: |
cd ./src-tauri/target/release/bundle cd ./src-tauri/target/release/bundle
@ -235,35 +194,6 @@ jobs:
APPIMAGE_SIG=$(cat appimage/Jan_${{ inputs.new_version }}_amd64.AppImage.sig) APPIMAGE_SIG=$(cat appimage/Jan_${{ inputs.new_version }}_amd64.AppImage.sig)
fi fi
DEB_FILE_SIZE=$(stat -c%s deb/$DEB_FILE_NAME)
APPIMAGE_FILE_SIZE=$(stat -c%s appimage/$APPIMAGE_FILE_NAME)
echo "deb file size: $DEB_FILE_SIZE"
echo "appimage file size: $APPIMAGE_FILE_SIZE"
DEB_SH512_CHECKSUM=$(python3 ../../../../.github/scripts/electron-checksum.py deb/$DEB_FILE_NAME)
APPIMAGE_SH512_CHECKSUM=$(python3 ../../../../.github/scripts/electron-checksum.py appimage/$APPIMAGE_FILE_NAME)
echo "deb sh512 checksum: $DEB_SH512_CHECKSUM"
echo "appimage sh512 checksum: $APPIMAGE_SH512_CHECKSUM"
CURRENT_TIME=$(date -u +"%Y-%m-%dT%H:%M:%S.%3NZ")
echo "releaseDate: $CURRENT_TIME"
# Create latest-linux.yml file
echo "version: ${{ inputs.new_version }}" > latest-linux.yml
echo "files:" >> latest-linux.yml
echo " - url: $DEB_FILE_NAME" >> latest-linux.yml
echo " sha512: $DEB_SH512_CHECKSUM" >> latest-linux.yml
echo " size: $DEB_FILE_SIZE" >> latest-linux.yml
echo " - url: $APPIMAGE_FILE_NAME" >> latest-linux.yml
echo " sha512: $APPIMAGE_SH512_CHECKSUM" >> latest-linux.yml
echo " size: $APPIMAGE_FILE_SIZE" >> latest-linux.yml
echo "path: $APPIMAGE_FILE_NAME" >> latest-linux.yml
echo "sha512: $APPIMAGE_SH512_CHECKSUM" >> latest-linux.yml
echo "releaseDate: $CURRENT_TIME" >> latest-linux.yml
cat latest-linux.yml
cp latest-linux.yml beta-linux.yml
echo "DEB_SIG=$DEB_SIG" >> $GITHUB_OUTPUT echo "DEB_SIG=$DEB_SIG" >> $GITHUB_OUTPUT
echo "APPIMAGE_SIG=$APPIMAGE_SIG" >> $GITHUB_OUTPUT echo "APPIMAGE_SIG=$APPIMAGE_SIG" >> $GITHUB_OUTPUT
echo "DEB_FILE_NAME=$DEB_FILE_NAME" >> $GITHUB_OUTPUT echo "DEB_FILE_NAME=$DEB_FILE_NAME" >> $GITHUB_OUTPUT
@ -275,10 +205,6 @@ jobs:
run: | run: |
cd ./src-tauri/target/release/bundle cd ./src-tauri/target/release/bundle
# Upload for electron updater for nightly
aws s3 cp ./latest-linux.yml s3://${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}/temp-${{ inputs.channel }}/latest-linux.yml
aws s3 cp ./beta-linux.yml s3://${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}/temp-${{ inputs.channel }}/beta-linux.yml
# Upload for tauri updater # Upload for tauri updater
aws s3 cp ./appimage/Jan-${{ inputs.channel }}_${{ inputs.new_version }}_amd64.AppImage s3://${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}/temp-${{ inputs.channel }}/Jan-${{ inputs.channel }}_${{ inputs.new_version }}_amd64.AppImage aws s3 cp ./appimage/Jan-${{ inputs.channel }}_${{ inputs.new_version }}_amd64.AppImage s3://${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}/temp-${{ inputs.channel }}/Jan-${{ inputs.channel }}_${{ inputs.new_version }}_amd64.AppImage
aws s3 cp ./deb/Jan-${{ inputs.channel }}_${{ inputs.new_version }}_amd64.deb s3://${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}/temp-${{ inputs.channel }}/Jan-${{ inputs.channel }}_${{ inputs.new_version }}_amd64.deb aws s3 cp ./deb/Jan-${{ inputs.channel }}_${{ inputs.new_version }}_amd64.deb s3://${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}/temp-${{ inputs.channel }}/Jan-${{ inputs.channel }}_${{ inputs.new_version }}_amd64.deb
@ -290,28 +216,6 @@ jobs:
AWS_DEFAULT_REGION: ${{ secrets.DELTA_AWS_REGION }} AWS_DEFAULT_REGION: ${{ secrets.DELTA_AWS_REGION }}
AWS_EC2_METADATA_DISABLED: 'true' AWS_EC2_METADATA_DISABLED: 'true'
## Upload to github release for stable release
- name: Upload release assert if public provider is github
if: inputs.channel == 'stable'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: actions/upload-release-asset@v1.0.1
with:
upload_url: ${{ inputs.upload_url }}
asset_path: ./src-tauri/target/release/bundle/latest-linux.yml
asset_name: latest-linux.yml
asset_content_type: text/yaml
- name: Upload release assert if public provider is github
if: inputs.channel == 'beta'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: actions/upload-release-asset@v1.0.1
with:
upload_url: ${{ inputs.upload_url }}
asset_path: ./src-tauri/target/release/bundle/beta-linux.yml
asset_name: beta-linux.yml
asset_content_type: text/yaml
- name: Upload release assert if public provider is github - name: Upload release assert if public provider is github
if: inputs.public_provider == 'github' if: inputs.public_provider == 'github'
env: env:

View File

@ -49,11 +49,13 @@ on:
required: false required: false
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: TAURI_SIGNING_PRIVATE_KEY_PASSWORD:
required: false required: false
TAURI_SIGNING_PUBLIC_KEY:
required: false
outputs: outputs:
MAC_UNIVERSAL_SIG: MAC_UNIVERSAL_SIG:
value: ${{ jobs.build-macos.outputs.MAC_UNIVERSAL_SIG }} value: ${{ jobs.build-macos.outputs.MAC_UNIVERSAL_SIG }}
FILE_NAME:
value: ${{ jobs.build-macos.outputs.FILE_NAME }}
DMG_NAME:
value: ${{ jobs.build-macos.outputs.DMG_NAME }}
TAR_NAME: TAR_NAME:
value: ${{ jobs.build-macos.outputs.TAR_NAME }} value: ${{ jobs.build-macos.outputs.TAR_NAME }}
@ -62,6 +64,8 @@ jobs:
runs-on: macos-latest runs-on: macos-latest
outputs: outputs:
MAC_UNIVERSAL_SIG: ${{ steps.metadata.outputs.MAC_UNIVERSAL_SIG }} MAC_UNIVERSAL_SIG: ${{ steps.metadata.outputs.MAC_UNIVERSAL_SIG }}
FILE_NAME: ${{ steps.metadata.outputs.FILE_NAME }}
DMG_NAME: ${{ steps.metadata.outputs.DMG_NAME }}
TAR_NAME: ${{ steps.metadata.outputs.TAR_NAME }} TAR_NAME: ${{ steps.metadata.outputs.TAR_NAME }}
permissions: permissions:
contents: write contents: write
@ -170,7 +174,6 @@ jobs:
APP_PATH: '.' APP_PATH: '.'
POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }} POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }} POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }}
# CORTEX_API_PORT: ${{ inputs.cortex_api_port }}
APPLE_CERTIFICATE: ${{ secrets.CODE_SIGN_P12_BASE64 }} APPLE_CERTIFICATE: ${{ secrets.CODE_SIGN_P12_BASE64 }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.CODE_SIGN_P12_PASSWORD }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.CODE_SIGN_P12_PASSWORD }}
APPLE_API_ISSUER: ${{ secrets.NOTARY_ISSUER }} APPLE_API_ISSUER: ${{ secrets.NOTARY_ISSUER }}
@ -178,7 +181,6 @@ jobs:
APPLE_API_KEY_PATH: /tmp/notary-key.p8 APPLE_API_KEY_PATH: /tmp/notary-key.p8
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
TAURI_SIGNING_PUBLIC_KEY: ${{ secrets.TAURI_SIGNING_PUBLIC_KEY }}
# Publish app # Publish app
@ -191,8 +193,8 @@ jobs:
path: | path: |
./src-tauri/target/universal-apple-darwin/release/bundle/dmg/*.dmg ./src-tauri/target/universal-apple-darwin/release/bundle/dmg/*.dmg
## create zip file and latest-mac.yml for mac electron auto updater ## Set output filename for mac
- name: create zip file and latest-mac.yml for mac electron auto updater - name: Set output filename for mac
run: | run: |
cd ./src-tauri/target/universal-apple-darwin/release/bundle/macos cd ./src-tauri/target/universal-apple-darwin/release/bundle/macos
if [ "${{ inputs.channel }}" != "stable" ]; then if [ "${{ inputs.channel }}" != "stable" ]; then
@ -209,27 +211,6 @@ jobs:
TAR_NAME=Jan.app.tar.gz TAR_NAME=Jan.app.tar.gz
fi fi
FILE_SIZE=$(stat -f%z $FILE_NAME)
echo "size: $FILE_SIZE"
SH512_CHECKSUM=$(python3 ../../../../../../.github/scripts/electron-checksum.py $FILE_NAME)
echo "sha512: $SH512_CHECKSUM"
CURRENT_TIME=$(date -u +"%Y-%m-%dT%H:%M:%S.%3NZ")
echo "releaseDate: $CURRENT_TIME"
# Create latest-mac.yml file
echo "version: ${{ inputs.new_version }}" > latest-mac.yml
echo "files:" >> latest-mac.yml
echo " - url: $FILE_NAME" >> latest-mac.yml
echo " sha512: $SH512_CHECKSUM" >> latest-mac.yml
echo " size: $FILE_SIZE" >> latest-mac.yml
echo "path: $FILE_NAME" >> latest-mac.yml
echo "sha512: $SH512_CHECKSUM" >> latest-mac.yml
echo "releaseDate: $CURRENT_TIME" >> latest-mac.yml
cat latest-mac.yml
cp latest-mac.yml beta-mac.yml
echo "::set-output name=MAC_UNIVERSAL_SIG::$MAC_UNIVERSAL_SIG" echo "::set-output name=MAC_UNIVERSAL_SIG::$MAC_UNIVERSAL_SIG"
echo "::set-output name=FILE_NAME::$FILE_NAME" echo "::set-output name=FILE_NAME::$FILE_NAME"
echo "::set-output name=DMG_NAME::$DMG_NAME" echo "::set-output name=DMG_NAME::$DMG_NAME"
@ -242,12 +223,6 @@ jobs:
run: | run: |
cd ./src-tauri/target/universal-apple-darwin/release/bundle cd ./src-tauri/target/universal-apple-darwin/release/bundle
# Upload for electron updater for nightly
aws s3 cp ./macos/latest-mac.yml s3://${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}/temp-${{ inputs.channel }}/latest-mac.yml
aws s3 cp ./macos/beta-mac.yml s3://${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}/temp-${{ inputs.channel }}/beta-mac.yml
aws s3 cp ./macos/jan-${{ inputs.channel }}-mac-universal-${{ inputs.new_version }}.zip s3://${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}/temp-${{ inputs.channel }}/jan-${{ inputs.channel }}-mac-universal-${{ inputs.new_version }}.zip
# aws s3 cp ./macos/jan-${{ inputs.channel }}-mac-universal-${{ inputs.new_version }}.zip.sig s3://${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}/temp-${{ inputs.channel }}/jan-${{ inputs.channel }}-mac-universal-${{ inputs.new_version }}.zip.sig
# Upload for tauri updater # Upload for tauri updater
aws s3 cp ./dmg/Jan-${{ inputs.channel }}_${{ inputs.new_version }}_universal.dmg s3://${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}/temp-${{ inputs.channel }}/Jan-${{ inputs.channel }}_${{ inputs.new_version }}_universal.dmg aws s3 cp ./dmg/Jan-${{ inputs.channel }}_${{ inputs.new_version }}_universal.dmg s3://${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}/temp-${{ inputs.channel }}/Jan-${{ inputs.channel }}_${{ inputs.new_version }}_universal.dmg
aws s3 cp ./macos/Jan-${{ inputs.channel }}.app.tar.gz s3://${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}/temp-${{ inputs.channel }}/Jan-${{ inputs.channel }}_${{ inputs.new_version }}.app.tar.gz aws s3 cp ./macos/Jan-${{ inputs.channel }}.app.tar.gz s3://${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}/temp-${{ inputs.channel }}/Jan-${{ inputs.channel }}_${{ inputs.new_version }}.app.tar.gz
@ -258,29 +233,6 @@ jobs:
AWS_DEFAULT_REGION: ${{ secrets.DELTA_AWS_REGION }} AWS_DEFAULT_REGION: ${{ secrets.DELTA_AWS_REGION }}
AWS_EC2_METADATA_DISABLED: 'true' AWS_EC2_METADATA_DISABLED: 'true'
## Upload to github release for stable release
- name: Upload release assert if public provider is github
if: inputs.channel == 'stable'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: actions/upload-release-asset@v1.0.1
with:
upload_url: ${{ inputs.upload_url }}
asset_path: ./src-tauri/target/universal-apple-darwin/release/bundle/macos/latest-mac.yml
asset_name: latest-mac.yml
asset_content_type: text/yaml
- name: Upload release assert if public provider is github
if: inputs.channel == 'beta'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: actions/upload-release-asset@v1.0.1
with:
upload_url: ${{ inputs.upload_url }}
asset_path: ./src-tauri/target/universal-apple-darwin/release/bundle/macos/beta-mac.yml
asset_name: beta-mac.yml
asset_content_type: text/yaml
- name: Upload release assert if public provider is github - name: Upload release assert if public provider is github
if: inputs.public_provider == 'github' if: inputs.public_provider == 'github'
env: env:

View File

@ -49,8 +49,6 @@ on:
required: false required: false
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: TAURI_SIGNING_PRIVATE_KEY_PASSWORD:
required: false required: false
TAURI_SIGNING_PUBLIC_KEY:
required: false
outputs: outputs:
WIN_SIG: WIN_SIG:
value: ${{ jobs.build-windows-x64.outputs.WIN_SIG }} value: ${{ jobs.build-windows-x64.outputs.WIN_SIG }}
@ -95,8 +93,10 @@ jobs:
run: | run: |
echo "Version: ${{ inputs.new_version }}" echo "Version: ${{ inputs.new_version }}"
# Update tauri.conf.json # Update tauri.conf.json
jq --arg version "${{ inputs.new_version }}" '.version = $version | .bundle.createUpdaterArtifacts = true | .bundle.windows.nsis.template = "tauri.bundle.windows.nsis.template"' ./src-tauri/tauri.conf.json > /tmp/tauri.conf.json jq --arg version "${{ inputs.new_version }}" '.version = $version | .bundle.createUpdaterArtifacts = true' ./src-tauri/tauri.conf.json > /tmp/tauri.conf.json
mv /tmp/tauri.conf.json ./src-tauri/tauri.conf.json mv /tmp/tauri.conf.json ./src-tauri/tauri.conf.json
jq '.bundle.windows.nsis.template = "tauri.bundle.windows.nsis.template"' ./src-tauri/tauri.windows.conf.json > /tmp/tauri.windows.conf.json
mv /tmp/tauri.windows.conf.json ./src-tauri/tauri.windows.conf.json
jq --arg version "${{ inputs.new_version }}" '.version = $version' web-app/package.json > /tmp/package.json jq --arg version "${{ inputs.new_version }}" '.version = $version' web-app/package.json > /tmp/package.json
mv /tmp/package.json web-app/package.json mv /tmp/package.json web-app/package.json
@ -195,10 +195,8 @@ jobs:
AWS_MAX_ATTEMPTS: '5' AWS_MAX_ATTEMPTS: '5'
POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }} POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }} POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }}
# CORTEX_API_PORT: ${{ inputs.cortex_api_port }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
TAURI_SIGNING_PUBLIC_KEY: ${{ secrets.TAURI_SIGNING_PUBLIC_KEY }}
- name: Upload Artifact - name: Upload Artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
@ -207,8 +205,8 @@ jobs:
path: | path: |
./src-tauri/target/release/bundle/nsis/*.exe ./src-tauri/target/release/bundle/nsis/*.exe
## create zip file and latest.yml for windows electron auto updater ## Set output filename for windows
- name: create zip file and latest.yml for windows electron auto updater - name: Set output filename for windows
shell: bash shell: bash
run: | run: |
cd ./src-tauri/target/release/bundle/nsis cd ./src-tauri/target/release/bundle/nsis
@ -220,27 +218,6 @@ jobs:
WIN_SIG=$(cat Jan_${{ inputs.new_version }}_x64-setup.exe.sig) WIN_SIG=$(cat Jan_${{ inputs.new_version }}_x64-setup.exe.sig)
fi fi
FILE_SIZE=$(stat -c %s $FILE_NAME)
echo "size: $FILE_SIZE"
SH512_CHECKSUM=$(python3 ../../../../../.github/scripts/electron-checksum.py $FILE_NAME)
echo "sha512: $SH512_CHECKSUM"
CURRENT_TIME=$(date -u +"%Y-%m-%dT%H:%M:%S.%3NZ")
echo "releaseDate: $CURRENT_TIME"
# Create latest.yml file
echo "version: ${{ inputs.new_version }}" > latest.yml
echo "files:" >> latest.yml
echo " - url: $FILE_NAME" >> latest.yml
echo " sha512: $SH512_CHECKSUM" >> latest.yml
echo " size: $FILE_SIZE" >> latest.yml
echo "path: $FILE_NAME" >> latest.yml
echo "sha512: $SH512_CHECKSUM" >> latest.yml
echo "releaseDate: $CURRENT_TIME" >> latest.yml
cat latest.yml
cp latest.yml beta.yml
echo "::set-output name=WIN_SIG::$WIN_SIG" echo "::set-output name=WIN_SIG::$WIN_SIG"
echo "::set-output name=FILE_NAME::$FILE_NAME" echo "::set-output name=FILE_NAME::$FILE_NAME"
id: metadata id: metadata
@ -252,10 +229,6 @@ jobs:
run: | run: |
cd ./src-tauri/target/release/bundle/nsis cd ./src-tauri/target/release/bundle/nsis
# Upload for electron updater for nightly
aws s3 cp ./latest.yml s3://${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}/temp-${{ inputs.channel }}/latest.yml
aws s3 cp ./beta.yml s3://${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}/temp-${{ inputs.channel }}/beta.yml
# Upload for tauri updater # Upload for tauri updater
aws s3 cp ./${{ steps.metadata.outputs.FILE_NAME }} s3://${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}/temp-${{ inputs.channel }}/${{ steps.metadata.outputs.FILE_NAME }} aws s3 cp ./${{ steps.metadata.outputs.FILE_NAME }} s3://${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}/temp-${{ inputs.channel }}/${{ steps.metadata.outputs.FILE_NAME }}
aws s3 cp ./${{ steps.metadata.outputs.FILE_NAME }}.sig s3://${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}/temp-${{ inputs.channel }}/${{ steps.metadata.outputs.FILE_NAME }}.sig aws s3 cp ./${{ steps.metadata.outputs.FILE_NAME }}.sig s3://${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}/temp-${{ inputs.channel }}/${{ steps.metadata.outputs.FILE_NAME }}.sig
@ -265,29 +238,6 @@ jobs:
AWS_DEFAULT_REGION: ${{ secrets.DELTA_AWS_REGION }} AWS_DEFAULT_REGION: ${{ secrets.DELTA_AWS_REGION }}
AWS_EC2_METADATA_DISABLED: 'true' AWS_EC2_METADATA_DISABLED: 'true'
## Upload to github release for stable release
- name: Upload release assert if public provider is github
if: inputs.channel == 'stable'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: actions/upload-release-asset@v1.0.1
with:
upload_url: ${{ inputs.upload_url }}
asset_path: ./src-tauri/target/release/bundle/nsis/latest.yml
asset_name: latest.yml
asset_content_type: text/yaml
- name: Upload release assert if public provider is github
if: inputs.channel == 'beta'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: actions/upload-release-asset@v1.0.1
with:
upload_url: ${{ inputs.upload_url }}
asset_path: ./src-tauri/target/release/bundle/nsis/beta.yml
asset_name: beta.yml
asset_content_type: text/yaml
- name: Upload release assert if public provider is github - name: Upload release assert if public provider is github
if: inputs.public_provider == 'github' if: inputs.public_provider == 'github'
env: env:

1
.gitignore vendored
View File

@ -50,3 +50,4 @@ src-tauri/resources/bin
.opencode .opencode
OpenCode.md OpenCode.md
archive/ archive/
.cache/

View File

@ -22,6 +22,8 @@ config-yarn:
install-and-build: config-yarn install-and-build: config-yarn
ifeq ($(OS),Windows_NT) ifeq ($(OS),Windows_NT)
echo "skip" echo "skip"
else ifeq ($(shell uname -s),Linux)
chmod +x src-tauri/build-utils/*
endif endif
yarn install yarn install
yarn build:core yarn build:core
@ -82,6 +84,7 @@ else ifeq ($(shell uname -s),Linux)
rm -rf ./src-tauri/target rm -rf ./src-tauri/target
rm -rf "~/jan/extensions" rm -rf "~/jan/extensions"
rm -rf "~/.cache/jan*" rm -rf "~/.cache/jan*"
rm -rf "./.cache"
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 '{}' +

View File

@ -7,8 +7,8 @@ module.exports = {
}, },
runner: './testRunner.js', runner: './testRunner.js',
transform: { transform: {
"^.+\\.tsx?$": [ '^.+\\.tsx?$': [
"ts-jest", 'ts-jest',
{ {
diagnostics: false, diagnostics: false,
}, },

View File

@ -32,7 +32,7 @@
"eslint-plugin-jest": "^27.9.0", "eslint-plugin-jest": "^27.9.0",
"jest": "^30.0.3", "jest": "^30.0.3",
"jest-junit": "^16.0.0", "jest-junit": "^16.0.0",
"jest-runner": "^29.7.0", "jest-runner": "^30.0.3",
"pacote": "^21.0.0", "pacote": "^21.0.0",
"request": "^2.88.2", "request": "^2.88.2",
"request-progress": "^3.0.0", "request-progress": "^3.0.0",

View File

@ -43,7 +43,7 @@ describe('EngineManager', () => {
}) })
describe('cortex engine migration', () => { describe('cortex engine migration', () => {
test('should map nitro to cortex engine', () => { test.skip('should map nitro to cortex engine', () => {
const cortexEngine = new MockAIEngine(InferenceEngine.cortex) const cortexEngine = new MockAIEngine(InferenceEngine.cortex)
// @ts-ignore // @ts-ignore
engineManager.register(cortexEngine) engineManager.register(cortexEngine)
@ -53,7 +53,7 @@ describe('EngineManager', () => {
expect(retrievedEngine).toBe(cortexEngine) expect(retrievedEngine).toBe(cortexEngine)
}) })
test('should map cortex_llamacpp to cortex engine', () => { test.skip('should map cortex_llamacpp to cortex engine', () => {
const cortexEngine = new MockAIEngine(InferenceEngine.cortex) const cortexEngine = new MockAIEngine(InferenceEngine.cortex)
// @ts-ignore // @ts-ignore
engineManager.register(cortexEngine) engineManager.register(cortexEngine)
@ -63,7 +63,7 @@ describe('EngineManager', () => {
expect(retrievedEngine).toBe(cortexEngine) expect(retrievedEngine).toBe(cortexEngine)
}) })
test('should map cortex_onnx to cortex engine', () => { test.skip('should map cortex_onnx to cortex engine', () => {
const cortexEngine = new MockAIEngine(InferenceEngine.cortex) const cortexEngine = new MockAIEngine(InferenceEngine.cortex)
// @ts-ignore // @ts-ignore
engineManager.register(cortexEngine) engineManager.register(cortexEngine)
@ -73,7 +73,7 @@ describe('EngineManager', () => {
expect(retrievedEngine).toBe(cortexEngine) expect(retrievedEngine).toBe(cortexEngine)
}) })
test('should map cortex_tensorrtllm to cortex engine', () => { test.skip('should map cortex_tensorrtllm to cortex engine', () => {
const cortexEngine = new MockAIEngine(InferenceEngine.cortex) const cortexEngine = new MockAIEngine(InferenceEngine.cortex)
// @ts-ignore // @ts-ignore
engineManager.register(cortexEngine) engineManager.register(cortexEngine)

View File

@ -23,7 +23,7 @@ describe('fs module', () => {
it('should call writeFileSync with correct arguments', () => { it('should call writeFileSync with correct arguments', () => {
const args = ['path/to/file', 'data'] const args = ['path/to/file', 'data']
fs.writeFileSync(...args) fs.writeFileSync(...args)
expect(globalThis.core.api.writeFileSync).toHaveBeenCalledWith(...args) expect(globalThis.core.api.writeFileSync).toHaveBeenCalledWith({ args })
}) })
it('should call writeBlob with correct arguments', async () => { it('should call writeBlob with correct arguments', async () => {
@ -90,8 +90,7 @@ describe('fs module', () => {
it('should call fileStat with correct arguments', async () => { it('should call fileStat with correct arguments', async () => {
const path = 'path/to/file' const path = 'path/to/file'
const outsideJanDataFolder = true await fs.fileStat(path)
await fs.fileStat(path, outsideJanDataFolder) expect(globalThis.core.api.fileStat).toHaveBeenCalledWith({ args: path })
expect(globalThis.core.api.fileStat).toHaveBeenCalledWith(path, outsideJanDataFolder)
}) })
}) })

View File

@ -1,7 +1,7 @@
import { useExtensions } from './index' import { useExtensions } from './index'
test('testUseExtensionsMissingPath', () => { test('testUseExtensionsMissingPath', () => {
expect(() => useExtensions(undefined as any)).toThrowError('A path to the extensions folder is required to use extensions') expect(() => useExtensions(undefined as any)).toThrow(
'A path to the extensions folder is required to use extensions'
)
}) })

View File

@ -1,8 +1,7 @@
import { Model, ModelSettingParams, ModelRuntimeParams } from '../model'
import { InferenceEngine } from '../engine'
test.skip('testValidModelCreation', () => {
import { Model, ModelSettingParams, ModelRuntimeParams, InferenceEngine } from '../model'
test('testValidModelCreation', () => {
const model: Model = { const model: Model = {
object: 'model', object: 'model',
version: '1.0', version: '1.0',
@ -15,16 +14,16 @@
settings: { ctx_len: 100, ngl: 50, embedding: true }, settings: { ctx_len: 100, ngl: 50, embedding: true },
parameters: { temperature: 0.5, token_limit: 100, top_k: 10 }, parameters: { temperature: 0.5, token_limit: 100, top_k: 10 },
metadata: { author: 'Author', tags: ['tag1', 'tag2'], size: 100 }, metadata: { author: 'Author', tags: ['tag1', 'tag2'], size: 100 },
engine: InferenceEngine.anthropic engine: InferenceEngine.anthropic,
}; }
expect(model).toBeDefined(); expect(model).toBeDefined()
expect(model.object).toBe('model'); expect(model.object).toBe('model')
expect(model.version).toBe('1.0'); expect(model.version).toBe('1.0')
expect(model.sources).toHaveLength(1); expect(model.sources).toHaveLength(1)
expect(model.sources[0].filename).toBe('model.bin'); expect(model.sources[0].filename).toBe('model.bin')
expect(model.settings).toBeDefined(); expect(model.settings).toBeDefined()
expect(model.parameters).toBeDefined(); expect(model.parameters).toBeDefined()
expect(model.metadata).toBeDefined(); expect(model.metadata).toBeDefined()
expect(model.engine).toBe(InferenceEngine.anthropic); expect(model.engine).toBe(InferenceEngine.anthropic)
}); })

View File

@ -1,19 +1,9 @@
import * as SettingComponent from './settingComponent'
import { createSettingComponent } from './settingComponent'; it('should not throw any errors when importing settingComponent', () => {
expect(() => require('./settingComponent')).not.toThrow()
})
it('should throw an error when creating a setting component with invalid controller type', () => { it('should export SettingComponentProps type', () => {
const props: SettingComponentProps = { expect(SettingComponent).toBeDefined()
key: 'invalidControllerKey', })
title: 'Invalid Controller Title',
description: 'Invalid Controller Description',
controllerType: 'invalid' as any,
controllerProps: {
placeholder: 'Enter text',
value: 'Initial Value',
type: 'text',
textAlign: 'left',
inputActions: ['unobscure'],
},
};
expect(() => createSettingComponent(props)).toThrowError();
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@ -94,7 +94,7 @@ const Hero = () => {
</p> </p>
<div className="w-4/5 mx-auto mt-10 relative"> <div className="w-4/5 mx-auto mt-10 relative">
<ThemeImage <ThemeImage
className="absolute object-cover w-full object-center mx-auto h-full top-0 left-0 scale-150" className="absolute object-cover w-full object-center mx-auto h-full top-0 left-0 scale-125"
source={{ source={{
light: '/assets/images/homepage/glow.png', light: '/assets/images/homepage/glow.png',
dark: '/assets/images/homepage/glow.png', dark: '/assets/images/homepage/glow.png',

View File

@ -587,6 +587,22 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@isaacs/balanced-match@npm:^4.0.1":
version: 4.0.1
resolution: "@isaacs/balanced-match@npm:4.0.1"
checksum: 10c0/7da011805b259ec5c955f01cee903da72ad97c5e6f01ca96197267d3f33103d5b2f8a1af192140f3aa64526c593c8d098ae366c2b11f7f17645d12387c2fd420
languageName: node
linkType: hard
"@isaacs/brace-expansion@npm:^5.0.0":
version: 5.0.0
resolution: "@isaacs/brace-expansion@npm:5.0.0"
dependencies:
"@isaacs/balanced-match": "npm:^4.0.1"
checksum: 10c0/b4d4812f4be53afc2c5b6c545001ff7a4659af68d4484804e9d514e183d20269bb81def8682c01a22b17c4d6aed14292c8494f7d2ac664e547101c1a905aa977
languageName: node
linkType: hard
"@isaacs/cliui@npm:^8.0.2": "@isaacs/cliui@npm:^8.0.2":
version: 8.0.2 version: 8.0.2
resolution: "@isaacs/cliui@npm:8.0.2" resolution: "@isaacs/cliui@npm:8.0.2"
@ -636,7 +652,7 @@ __metadata:
dependencies: dependencies:
"@janhq/core": ../../core/package.tgz "@janhq/core": ../../core/package.tgz
cpx: "npm:^1.5.0" cpx: "npm:^1.5.0"
rimraf: "npm:^3.0.2" rimraf: "npm:^6.0.1"
rolldown: "npm:1.0.0-beta.1" rolldown: "npm:1.0.0-beta.1"
run-script-os: "npm:^1.1.6" run-script-os: "npm:^1.1.6"
ts-loader: "npm:^9.5.0" ts-loader: "npm:^9.5.0"
@ -650,7 +666,7 @@ __metadata:
dependencies: dependencies:
"@janhq/core": ../../core/package.tgz "@janhq/core": ../../core/package.tgz
cpx: "npm:^1.5.0" cpx: "npm:^1.5.0"
rimraf: "npm:^3.0.2" rimraf: "npm:^6.0.1"
rolldown: "npm:1.0.0-beta.1" rolldown: "npm:1.0.0-beta.1"
ts-loader: "npm:^9.5.0" ts-loader: "npm:^9.5.0"
typescript: "npm:^5.7.2" typescript: "npm:^5.7.2"
@ -659,64 +675,89 @@ __metadata:
"@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fassistant-extension%40workspace%3Aassistant-extension": "@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fassistant-extension%40workspace%3Aassistant-extension":
version: 0.1.10 version: 0.1.10
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=5531aa&locator=%40janhq%2Fassistant-extension%40workspace%3Aassistant-extension" resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=f2e0b5&locator=%40janhq%2Fassistant-extension%40workspace%3Aassistant-extension"
dependencies: dependencies:
rxjs: "npm:^7.8.1" rxjs: "npm:^7.8.1"
ulidx: "npm:^2.3.0" ulidx: "npm:^2.3.0"
checksum: 10c0/ee7fe21267cf795dba890781d1e7807a6cb3ecb915ce9ecbd3a8386a2ebc916a8b70a775ce5d9d9f74d2ec29e20b65cea4ef6cdd0ea250a8ff2d5e6bd2237b1e checksum: 10c0/6f085adb6211c2d0735c2ebccf38c26b90c78a786d6d327fab3e1a9e10380a522429e19f9b9752f7c5a42eb6d80f37c7022c5146b2e4ca1fada609dccbea4194
languageName: node languageName: node
linkType: hard linkType: hard
"@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fconversational-extension%40workspace%3Aconversational-extension": "@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fconversational-extension%40workspace%3Aconversational-extension":
version: 0.1.10 version: 0.1.10
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=5531aa&locator=%40janhq%2Fconversational-extension%40workspace%3Aconversational-extension" resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=f2e0b5&locator=%40janhq%2Fconversational-extension%40workspace%3Aconversational-extension"
dependencies: dependencies:
rxjs: "npm:^7.8.1" rxjs: "npm:^7.8.1"
ulidx: "npm:^2.3.0" ulidx: "npm:^2.3.0"
checksum: 10c0/ee7fe21267cf795dba890781d1e7807a6cb3ecb915ce9ecbd3a8386a2ebc916a8b70a775ce5d9d9f74d2ec29e20b65cea4ef6cdd0ea250a8ff2d5e6bd2237b1e checksum: 10c0/6f085adb6211c2d0735c2ebccf38c26b90c78a786d6d327fab3e1a9e10380a522429e19f9b9752f7c5a42eb6d80f37c7022c5146b2e4ca1fada609dccbea4194
languageName: node
linkType: hard
"@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fdownload-extension%40workspace%3Adownload-extension":
version: 0.1.10
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=f2e0b5&locator=%40janhq%2Fdownload-extension%40workspace%3Adownload-extension"
dependencies:
rxjs: "npm:^7.8.1"
ulidx: "npm:^2.3.0"
checksum: 10c0/6f085adb6211c2d0735c2ebccf38c26b90c78a786d6d327fab3e1a9e10380a522429e19f9b9752f7c5a42eb6d80f37c7022c5146b2e4ca1fada609dccbea4194
languageName: node languageName: node
linkType: hard linkType: hard
"@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fengine-management-extension%40workspace%3Aengine-management-extension": "@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fengine-management-extension%40workspace%3Aengine-management-extension":
version: 0.1.10 version: 0.1.10
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=5531aa&locator=%40janhq%2Fengine-management-extension%40workspace%3Aengine-management-extension" resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=f2e0b5&locator=%40janhq%2Fengine-management-extension%40workspace%3Aengine-management-extension"
dependencies: dependencies:
rxjs: "npm:^7.8.1" rxjs: "npm:^7.8.1"
ulidx: "npm:^2.3.0" ulidx: "npm:^2.3.0"
checksum: 10c0/ee7fe21267cf795dba890781d1e7807a6cb3ecb915ce9ecbd3a8386a2ebc916a8b70a775ce5d9d9f74d2ec29e20b65cea4ef6cdd0ea250a8ff2d5e6bd2237b1e checksum: 10c0/6f085adb6211c2d0735c2ebccf38c26b90c78a786d6d327fab3e1a9e10380a522429e19f9b9752f7c5a42eb6d80f37c7022c5146b2e4ca1fada609dccbea4194
languageName: node languageName: node
linkType: hard linkType: hard
"@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fhardware-management-extension%40workspace%3Ahardware-management-extension": "@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fhardware-management-extension%40workspace%3Ahardware-management-extension":
version: 0.1.10 version: 0.1.10
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=5531aa&locator=%40janhq%2Fhardware-management-extension%40workspace%3Ahardware-management-extension" resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=f2e0b5&locator=%40janhq%2Fhardware-management-extension%40workspace%3Ahardware-management-extension"
dependencies: dependencies:
rxjs: "npm:^7.8.1" rxjs: "npm:^7.8.1"
ulidx: "npm:^2.3.0" ulidx: "npm:^2.3.0"
checksum: 10c0/ee7fe21267cf795dba890781d1e7807a6cb3ecb915ce9ecbd3a8386a2ebc916a8b70a775ce5d9d9f74d2ec29e20b65cea4ef6cdd0ea250a8ff2d5e6bd2237b1e checksum: 10c0/6f085adb6211c2d0735c2ebccf38c26b90c78a786d6d327fab3e1a9e10380a522429e19f9b9752f7c5a42eb6d80f37c7022c5146b2e4ca1fada609dccbea4194
languageName: node languageName: node
linkType: hard linkType: hard
"@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Finference-cortex-extension%40workspace%3Ainference-cortex-extension": "@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Finference-cortex-extension%40workspace%3Ainference-cortex-extension":
version: 0.1.10 version: 0.1.10
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=5531aa&locator=%40janhq%2Finference-cortex-extension%40workspace%3Ainference-cortex-extension" resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=f2e0b5&locator=%40janhq%2Finference-cortex-extension%40workspace%3Ainference-cortex-extension"
dependencies: dependencies:
rxjs: "npm:^7.8.1" rxjs: "npm:^7.8.1"
ulidx: "npm:^2.3.0" ulidx: "npm:^2.3.0"
checksum: 10c0/ee7fe21267cf795dba890781d1e7807a6cb3ecb915ce9ecbd3a8386a2ebc916a8b70a775ce5d9d9f74d2ec29e20b65cea4ef6cdd0ea250a8ff2d5e6bd2237b1e checksum: 10c0/6f085adb6211c2d0735c2ebccf38c26b90c78a786d6d327fab3e1a9e10380a522429e19f9b9752f7c5a42eb6d80f37c7022c5146b2e4ca1fada609dccbea4194
languageName: node languageName: node
linkType: hard linkType: hard
"@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fmodel-extension%40workspace%3Amodel-extension": "@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fmodel-extension%40workspace%3Amodel-extension":
version: 0.1.10 version: 0.1.10
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=5531aa&locator=%40janhq%2Fmodel-extension%40workspace%3Amodel-extension" resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=f2e0b5&locator=%40janhq%2Fmodel-extension%40workspace%3Amodel-extension"
dependencies: dependencies:
rxjs: "npm:^7.8.1" rxjs: "npm:^7.8.1"
ulidx: "npm:^2.3.0" ulidx: "npm:^2.3.0"
checksum: 10c0/ee7fe21267cf795dba890781d1e7807a6cb3ecb915ce9ecbd3a8386a2ebc916a8b70a775ce5d9d9f74d2ec29e20b65cea4ef6cdd0ea250a8ff2d5e6bd2237b1e checksum: 10c0/6f085adb6211c2d0735c2ebccf38c26b90c78a786d6d327fab3e1a9e10380a522429e19f9b9752f7c5a42eb6d80f37c7022c5146b2e4ca1fada609dccbea4194
languageName: node languageName: node
linkType: hard linkType: hard
"@janhq/download-extension@workspace:download-extension":
version: 0.0.0-use.local
resolution: "@janhq/download-extension@workspace:download-extension"
dependencies:
"@janhq/core": ../../core/package.tgz
"@tauri-apps/api": "npm:^2.5.0"
cpx: "npm:^1.5.0"
rimraf: "npm:^6.0.1"
rolldown: "npm:1.0.0-beta.1"
run-script-os: "npm:^1.1.6"
typescript: "npm:5.8.3"
vitest: "npm:^3.0.6"
languageName: unknown
linkType: soft
"@janhq/engine-management-extension@workspace:engine-management-extension": "@janhq/engine-management-extension@workspace:engine-management-extension":
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "@janhq/engine-management-extension@workspace:engine-management-extension" resolution: "@janhq/engine-management-extension@workspace:engine-management-extension"
@ -1436,6 +1477,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@tauri-apps/api@npm:^2.5.0":
version: 2.6.0
resolution: "@tauri-apps/api@npm:2.6.0"
checksum: 10c0/211353d951c7e3e5298f074ec762b5853ff0cdee261478c27db1e450fcf3d6f2c03a616483abbf9dfc79f13c6dfcfa7db0b790c1384c113951c0d694809f05ef
languageName: node
linkType: hard
"@tybys/wasm-util@npm:^0.9.0": "@tybys/wasm-util@npm:^0.9.0":
version: 0.9.0 version: 0.9.0
resolution: "@tybys/wasm-util@npm:0.9.0" resolution: "@tybys/wasm-util@npm:0.9.0"
@ -2587,7 +2635,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.3": "cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.3, cross-spawn@npm:^7.0.6":
version: 7.0.6 version: 7.0.6
resolution: "cross-spawn@npm:7.0.6" resolution: "cross-spawn@npm:7.0.6"
dependencies: dependencies:
@ -3392,6 +3440,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"foreground-child@npm:^3.3.1":
version: 3.3.1
resolution: "foreground-child@npm:3.3.1"
dependencies:
cross-spawn: "npm:^7.0.6"
signal-exit: "npm:^4.0.1"
checksum: 10c0/8986e4af2430896e65bc2788d6679067294d6aee9545daefc84923a0a4b399ad9c7a3ea7bd8c0b2b80fdf4a92de4c69df3f628233ff3224260e9c1541a9e9ed3
languageName: node
linkType: hard
"fragment-cache@npm:^0.2.1": "fragment-cache@npm:^0.2.1":
version: 0.2.1 version: 0.2.1
resolution: "fragment-cache@npm:0.2.1" resolution: "fragment-cache@npm:0.2.1"
@ -3583,6 +3641,22 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"glob@npm:^11.0.0":
version: 11.0.3
resolution: "glob@npm:11.0.3"
dependencies:
foreground-child: "npm:^3.3.1"
jackspeak: "npm:^4.1.1"
minimatch: "npm:^10.0.3"
minipass: "npm:^7.1.2"
package-json-from-dist: "npm:^1.0.0"
path-scurry: "npm:^2.0.0"
bin:
glob: dist/esm/bin.mjs
checksum: 10c0/7d24457549ec2903920dfa3d8e76850e7c02aa709122f0164b240c712f5455c0b457e6f2a1eee39344c6148e39895be8094ae8cfef7ccc3296ed30bce250c661
languageName: node
linkType: hard
"glob@npm:^7.0.5, glob@npm:^7.1.3, glob@npm:^7.1.4": "glob@npm:^7.0.5, glob@npm:^7.1.3, glob@npm:^7.1.4":
version: 7.2.3 version: 7.2.3
resolution: "glob@npm:7.2.3" resolution: "glob@npm:7.2.3"
@ -4205,6 +4279,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"jackspeak@npm:^4.1.1":
version: 4.1.1
resolution: "jackspeak@npm:4.1.1"
dependencies:
"@isaacs/cliui": "npm:^8.0.2"
checksum: 10c0/84ec4f8e21d6514db24737d9caf65361511f75e5e424980eebca4199f400874f45e562ac20fa8aeb1dd20ca2f3f81f0788b6e9c3e64d216a5794fd6f30e0e042
languageName: node
linkType: hard
"jake@npm:^10.8.5": "jake@npm:^10.8.5":
version: 10.9.2 version: 10.9.2
resolution: "jake@npm:10.9.2" resolution: "jake@npm:10.9.2"
@ -4829,6 +4912,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"lru-cache@npm:^11.0.0":
version: 11.1.0
resolution: "lru-cache@npm:11.1.0"
checksum: 10c0/85c312f7113f65fae6a62de7985348649937eb34fb3d212811acbf6704dc322a421788aca253b62838f1f07049a84cc513d88f494e373d3756514ad263670a64
languageName: node
linkType: hard
"lru-cache@npm:^5.1.1": "lru-cache@npm:^5.1.1":
version: 5.1.1 version: 5.1.1
resolution: "lru-cache@npm:5.1.1" resolution: "lru-cache@npm:5.1.1"
@ -5028,6 +5118,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"minimatch@npm:^10.0.3":
version: 10.0.3
resolution: "minimatch@npm:10.0.3"
dependencies:
"@isaacs/brace-expansion": "npm:^5.0.0"
checksum: 10c0/e43e4a905c5d70ac4cec8530ceaeccb9c544b1ba8ac45238e2a78121a01c17ff0c373346472d221872563204eabe929ad02669bb575cb1f0cc30facab369f70f
languageName: node
linkType: hard
"minimatch@npm:^3.0.2, minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": "minimatch@npm:^3.0.2, minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2":
version: 3.1.2 version: 3.1.2
resolution: "minimatch@npm:3.1.2" resolution: "minimatch@npm:3.1.2"
@ -5574,6 +5673,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"path-scurry@npm:^2.0.0":
version: 2.0.0
resolution: "path-scurry@npm:2.0.0"
dependencies:
lru-cache: "npm:^11.0.0"
minipass: "npm:^7.1.2"
checksum: 10c0/3da4adedaa8e7ef8d6dc4f35a0ff8f05a9b4d8365f2b28047752b62d4c1ad73eec21e37b1579ef2d075920157856a3b52ae8309c480a6f1a8bbe06ff8e52b33c
languageName: node
linkType: hard
"path-type@npm:^1.0.0": "path-type@npm:^1.0.0":
version: 1.1.0 version: 1.1.0
resolution: "path-type@npm:1.1.0" resolution: "path-type@npm:1.1.0"
@ -5987,6 +6096,18 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"rimraf@npm:^6.0.1":
version: 6.0.1
resolution: "rimraf@npm:6.0.1"
dependencies:
glob: "npm:^11.0.0"
package-json-from-dist: "npm:^1.0.0"
bin:
rimraf: dist/esm/bin.mjs
checksum: 10c0/b30b6b072771f0d1e73b4ca5f37bb2944ee09375be9db5f558fcd3310000d29dfcfa93cf7734d75295ad5a7486dc8e40f63089ced1722a664539ffc0c3ece8c6
languageName: node
linkType: hard
"rolldown@npm:1.0.0-beta.1": "rolldown@npm:1.0.0-beta.1":
version: 1.0.0-beta.1 version: 1.0.0-beta.1
resolution: "rolldown@npm:1.0.0-beta.1" resolution: "rolldown@npm:1.0.0-beta.1"
@ -6979,6 +7100,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"typescript@npm:5.8.3":
version: 5.8.3
resolution: "typescript@npm:5.8.3"
bin:
tsc: bin/tsc
tsserver: bin/tsserver
checksum: 10c0/5f8bb01196e542e64d44db3d16ee0e4063ce4f3e3966df6005f2588e86d91c03e1fb131c2581baf0fb65ee79669eea6e161cd448178986587e9f6844446dbb48
languageName: node
linkType: hard
"typescript@npm:^5.3.3, typescript@npm:^5.7.2": "typescript@npm:^5.3.3, typescript@npm:^5.7.2":
version: 5.7.2 version: 5.7.2
resolution: "typescript@npm:5.7.2" resolution: "typescript@npm:5.7.2"
@ -6999,6 +7130,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"typescript@patch:typescript@npm%3A5.8.3#optional!builtin<compat/typescript>":
version: 5.8.3
resolution: "typescript@patch:typescript@npm%3A5.8.3#optional!builtin<compat/typescript>::version=5.8.3&hash=5786d5"
bin:
tsc: bin/tsc
tsserver: bin/tsserver
checksum: 10c0/39117e346ff8ebd87ae1510b3a77d5d92dae5a89bde588c747d25da5c146603a99c8ee588c7ef80faaf123d89ed46f6dbd918d534d641083177d5fac38b8a1cb
languageName: node
linkType: hard
"typescript@patch:typescript@npm%3A^5.3.3#optional!builtin<compat/typescript>, typescript@patch:typescript@npm%3A^5.7.2#optional!builtin<compat/typescript>": "typescript@patch:typescript@npm%3A^5.3.3#optional!builtin<compat/typescript>, typescript@patch:typescript@npm%3A^5.7.2#optional!builtin<compat/typescript>":
version: 5.7.2 version: 5.7.2
resolution: "typescript@patch:typescript@npm%3A5.7.2#optional!builtin<compat/typescript>::version=5.7.2&hash=5786d5" resolution: "typescript@patch:typescript@npm%3A5.7.2#optional!builtin<compat/typescript>::version=5.7.2&hash=5786d5"

View File

@ -1,3 +1,3 @@
module.exports = { module.exports = {
projects: ['<rootDir>/core', '<rootDir>/web', '<rootDir>/joi'], projects: ['<rootDir>/core'],
} }

View File

@ -164,6 +164,7 @@ elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
rm -rf ./src-tauri/target 2>/dev/null || true rm -rf ./src-tauri/target 2>/dev/null || true
rm -rf ~/jan/extensions 2>/dev/null || true rm -rf ~/jan/extensions 2>/dev/null || true
rm -rf "~/.cache/jan*" 2>/dev/null || true rm -rf "~/.cache/jan*" 2>/dev/null || true
rm -rf "./.cache" 2>/dev/null || true
else else
# macOS cleanup (matches Makefile) # macOS cleanup (matches Makefile)
find . -name "node_modules" -type d -prune -exec rm -rf '{}' + 2>/dev/null || true find . -name "node_modules" -type d -prune -exec rm -rf '{}' + 2>/dev/null || true

View File

@ -12,8 +12,11 @@
"lint": "yarn workspace @janhq/web-app lint", "lint": "yarn workspace @janhq/web-app lint",
"dev": "yarn dev:tauri", "dev": "yarn dev:tauri",
"build": "yarn build:web && yarn build:tauri", "build": "yarn build:web && yarn build:tauri",
"test": "yarn workspace @janhq/web-app test", "test": "jest && yarn workspace @janhq/web-app test",
"test:coverage": "yarn workspace @janhq/web-app test", "test:coverage": "yarn test:coverage:jest && yarn test:coverage:vitest && yarn merge:coverage",
"test:coverage:jest": "jest --coverage --coverageDirectory=coverage/jest",
"test:coverage:vitest": "yarn workspace @janhq/web-app test:coverage",
"merge:coverage": "node scripts/merge-coverage.js",
"test:prepare": "yarn build:icon && yarn copy:lib && yarn copy:assets:tauri && yarn build --no-bundle ", "test:prepare": "yarn build:icon && yarn copy:lib && yarn copy:assets:tauri && yarn build --no-bundle ",
"test:e2e:linux": "yarn test:prepare && xvfb-run yarn workspace tests-e2-js test", "test:e2e:linux": "yarn test:prepare && xvfb-run yarn workspace tests-e2-js test",
"test:e2e:win32": "yarn test:prepare && yarn workspace tests-e2-js test", "test:e2e:win32": "yarn test:prepare && yarn workspace tests-e2-js test",
@ -30,9 +33,10 @@
"copy:lib:win32": "cpx \"./lib/windows/*.dll\" \"./src-tauri/resources/lib/\"", "copy:lib:win32": "cpx \"./lib/windows/*.dll\" \"./src-tauri/resources/lib/\"",
"copy:lib:darwin": "mkdir -p \"./src-tauri/resources/lib/\"", "copy:lib:darwin": "mkdir -p \"./src-tauri/resources/lib/\"",
"download:bin": "node ./scripts/download-bin.mjs", "download:bin": "node ./scripts/download-bin.mjs",
"build:tauri:linux:win32": "yarn download:bin && yarn install:cortex && yarn build:icon && yarn copy:assets:tauri && yarn tauri build", "build:tauri:win32": "yarn download:bin && yarn tauri build",
"build:tauri:darwin": "yarn install:cortex && yarn build:icon && yarn copy:assets:tauri && yarn tauri build --target universal-apple-darwin", "build:tauri:linux": "yarn download:bin && ./src-tauri/build-utils/shim-linuxdeploy.sh yarn tauri build && ./src-tauri/build-utils/buildAppImage.sh",
"build:tauri": "run-script-os", "build:tauri:darwin": "yarn tauri build --target universal-apple-darwin",
"build:tauri": "yarn install:cortex && yarn build:icon && yarn copy:assets:tauri && run-script-os",
"build:icon": "tauri icon ./src-tauri/icons/icon.png", "build:icon": "tauri icon ./src-tauri/icons/icon.png",
"build:core": "cd core && yarn build && yarn pack", "build:core": "cd core && yarn build && yarn pack",
"build:web": "yarn workspace @janhq/web-app build", "build:web": "yarn workspace @janhq/web-app build",
@ -45,8 +49,13 @@
"cpx": "^1.5.0", "cpx": "^1.5.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"husky": "^9.1.5", "husky": "^9.1.5",
"istanbul-api": "^3.0.0",
"istanbul-lib-coverage": "^3.2.2",
"istanbul-lib-report": "^3.0.1",
"istanbul-reports": "^3.1.7",
"jest": "^30.0.3", "jest": "^30.0.3",
"jest-environment-jsdom": "^29.7.0", "jest-environment-jsdom": "^29.7.0",
"nyc": "^17.1.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"run-script-os": "^1.1.6", "run-script-os": "^1.1.6",
"tar": "^4.4.19", "tar": "^4.4.19",

145
scripts/merge-coverage.js Normal file
View File

@ -0,0 +1,145 @@
const { createCoverageMap } = require('istanbul-lib-coverage')
const { createReporter } = require('istanbul-api')
const fs = require('fs')
const path = require('path')
const coverageDir = path.join(__dirname, '../coverage')
const jestCoverage = path.join(coverageDir, 'jest/coverage-final.json')
const vitestCoverage = path.join(coverageDir, 'vitest/coverage-final.json')
const mergedDir = path.join(coverageDir, 'merged')
function normalizePath(filePath, workspace) {
if (workspace === 'jest') {
return `[CORE] ${filePath}`
} else if (workspace === 'vitest') {
return `[WEB-APP] ${filePath}`
}
return filePath
}
async function mergeCoverage() {
const map = createCoverageMap({})
console.log('🔍 Checking coverage files...')
console.log('Jest coverage path:', jestCoverage)
console.log('Vitest coverage path:', vitestCoverage)
console.log('Jest file exists:', fs.existsSync(jestCoverage))
console.log('Vitest file exists:', fs.existsSync(vitestCoverage))
// Load Jest coverage (core workspace)
if (fs.existsSync(jestCoverage)) {
const jestData = JSON.parse(fs.readFileSync(jestCoverage, 'utf8'))
console.log('Jest data keys:', Object.keys(jestData).length)
map.merge(jestData)
console.log('✓ Merged Jest coverage (core workspace)')
} else {
console.log('❌ Jest coverage file not found')
}
// Load Vitest coverage (web-app workspace)
if (fs.existsSync(vitestCoverage)) {
const vitestData = JSON.parse(fs.readFileSync(vitestCoverage, 'utf8'))
console.log('Vitest data keys:', Object.keys(vitestData).length)
map.merge(vitestData)
console.log('✓ Merged Vitest coverage (web-app workspace)')
} else {
console.log('❌ Vitest coverage file not found')
}
console.log('📊 Total files in coverage map:', map.files().length)
// Create merged directory
if (!fs.existsSync(mergedDir)) {
fs.mkdirSync(mergedDir, { recursive: true })
console.log('✓ Created merged directory')
}
try {
console.log('🔄 Generating reports...')
const context = require('istanbul-lib-report').createContext({
dir: mergedDir,
coverageMap: map,
})
const htmlReporter = require('istanbul-reports').create('html')
const lcovReporter = require('istanbul-reports').create('lcov')
const textReporter = require('istanbul-reports').create('text')
// Generate reports
htmlReporter.execute(context)
lcovReporter.execute(context)
textReporter.execute(context)
console.log('\n📊 Coverage reports merged successfully!')
console.log('📁 HTML report: coverage/merged/index.html')
console.log('📁 LCOV report: coverage/merged/lcov.info')
// Check if files were created
if (fs.existsSync(mergedDir)) {
const mergedFiles = fs.readdirSync(mergedDir)
console.log('📁 Files in merged directory:', mergedFiles)
}
} catch (error) {
console.error('❌ Error generating reports:', error.message)
console.error('Stack trace:', error.stack)
throw error
}
// Generate separate reports for each workspace
await generateWorkspaceReports()
}
async function generateWorkspaceReports() {
// Generate separate core report
if (fs.existsSync(jestCoverage)) {
const coreMap = createCoverageMap({})
const jestData = JSON.parse(fs.readFileSync(jestCoverage, 'utf8'))
coreMap.merge(jestData)
const coreDir = path.join(coverageDir, 'core-only')
if (!fs.existsSync(coreDir)) {
fs.mkdirSync(coreDir, { recursive: true })
}
const coreContext = require('istanbul-lib-report').createContext({
dir: coreDir,
coverageMap: coreMap,
})
const htmlReporter = require('istanbul-reports').create('html')
const textSummaryReporter =
require('istanbul-reports').create('text-summary')
htmlReporter.execute(coreContext)
textSummaryReporter.execute(coreContext)
console.log('📁 Core-only report: coverage/core-only/index.html')
}
// Generate separate web-app report
if (fs.existsSync(vitestCoverage)) {
const webAppMap = createCoverageMap({})
const vitestData = JSON.parse(fs.readFileSync(vitestCoverage, 'utf8'))
webAppMap.merge(vitestData)
const webAppDir = path.join(coverageDir, 'web-app-only')
if (!fs.existsSync(webAppDir)) {
fs.mkdirSync(webAppDir, { recursive: true })
}
const webAppContext = require('istanbul-lib-report').createContext({
dir: webAppDir,
coverageMap: webAppMap,
})
const htmlReporter = require('istanbul-reports').create('html')
const textSummaryReporter =
require('istanbul-reports').create('text-summary')
htmlReporter.execute(webAppContext)
textSummaryReporter.execute(webAppContext)
console.log('📁 Web-app-only report: coverage/web-app-only/index.html')
}
}
mergeCoverage().catch(console.error)

View File

@ -0,0 +1,33 @@
#!/bin/bash
APPIMAGETOOL="./.cache/build-tools/appimagetool"
RELEASE_CHANNEL=${RELEASE_CHANNEL:-"stable"}
# pull in AppImageTool if it's not pre cached
mkdir -p ./.cache/build-tools
if [ ! -f "${APPIMAGETOOL}" ]; then
wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage -O "${APPIMAGETOOL}"
chmod +x "${APPIMAGETOOL}"
fi
if [ "${RELEASE_CHANNEL}" != "stable" ]; then
APP_DIR=./src-tauri/target/release/bundle/appimage/Jan-${RELEASE_CHANNEL}.AppDir
LIB_DIR=$APP_DIR/usr/lib/Jan-${RELEASE_CHANNEL}/binaries
else
APP_DIR=./src-tauri/target/release/bundle/appimage/Jan.AppDir
LIB_DIR=$APP_DIR/usr/lib/Jan/binaries
fi
# bundle additional resources in the AppDir without pulling in their dependencies
cp ./src-tauri/resources/bin/bun $APP_DIR/usr/bin/bun
mkdir -p $LIB_DIR/engines
cp -f ./src-tauri/binaries/deps/*.so* $LIB_DIR/
cp -f ./src-tauri/binaries/*.so* $LIB_DIR/
cp -rf ./src-tauri/binaries/engines $LIB_DIR/
# remove appimage generated by tauri build
APP_IMAGE=./src-tauri/target/release/bundle/appimage/$(ls ./src-tauri/target/release/bundle/appimage/ | grep AppImage | head -1)
echo $APP_IMAGE
rm -f $APP_IMAGE
# repackage appimage with additional resources
"${APPIMAGETOOL}" $APP_DIR $APP_IMAGE

View File

@ -0,0 +1,30 @@
#!/usr/bin/env bash
set -euo pipefail
# wrapper script to pin linuxdeploy version and inject environment variables into the
# build process. While yarn supports injecting environment vairables via env files,
# this applies to all yarn scripts. Using a wrapper allows granular control over
# when environment variables are injected, and avoids tainting the system .cache
# avoid redownloading corepack if possible
export COREPACK_HOME=${COREPACK_HOME:-${XDG_CACHE_HOME:-$HOME/.cache}/node/corepack}
# move cache home to <project root>/.cache
export XDG_CACHE_HOME=${PWD}/.cache
LINUXDEPLOY_VER="1-alpha-20250213-2"
LINUXDEPLOY="$XDG_CACHE_HOME/tauri/linuxdeploy-$LINUXDEPLOY_VER-x86_64.AppImage"
SYMLINK="$XDG_CACHE_HOME/tauri/linuxdeploy-x86_64.AppImage"
mkdir -p "$XDG_CACHE_HOME/tauri"
if [ ! -f "$LINUXDEPLOY" ]; then
GLOB_PATTERN="$XDG_CACHE_HOME/tauri/linuxdeploy-*-x86_64.AppImage"
rm -f $GLOB_PATTERN
wget "https://github.com/linuxdeploy/linuxdeploy/releases/download/$LINUXDEPLOY_VER/linuxdeploy-x86_64.AppImage" -O "$LINUXDEPLOY"
chmod a+x "$LINUXDEPLOY"
fi
rm -f "$SYMLINK"
ln -s "$LINUXDEPLOY" "$SYMLINK"
"$@"

View File

@ -452,7 +452,7 @@ Function .onInit
StrCpy $PassiveMode 1 StrCpy $PassiveMode 1
${EndIf} ${EndIf}
; always run in passive mode ; always run in passive mode
StrCpy $PassiveMode 1 ; StrCpy $PassiveMode 1
${GetOptions} $CMDLINE "/NS" $NoShortcutMode ${GetOptions} $CMDLINE "/NS" $NoShortcutMode
${IfNot} ${Errors} ${IfNot} ${Errors}

View File

@ -76,7 +76,6 @@
}, },
"bundle": { "bundle": {
"active": true, "active": true,
"targets": ["nsis", "app", "dmg", "deb", "appimage"],
"createUpdaterArtifacts": false, "createUpdaterArtifacts": false,
"icon": [ "icon": [
"icons/32x32.png", "icons/32x32.png",
@ -84,32 +83,6 @@
"icons/128x128@2x.png", "icons/128x128@2x.png",
"icons/icon.icns", "icons/icon.icns",
"icons/icon.ico" "icons/icon.ico"
], ]
"resources": [
"resources/pre-install/**/*",
"resources/lib/",
"binaries/**/*"
],
"externalBin": [
"binaries/cortex-server",
"resources/bin/bun",
"resources/bin/uv"
],
"linux": {
"appimage": {
"bundleMediaFramework": false,
"files": {}
},
"deb": {
"files": {
"usr/bin/bun": "resources/bin/bun",
"usr/lib/Jan/binaries": "binaries/deps",
"usr/lib/Jan/binaries/engines": "binaries/engines"
}
}
},
"windows": {
"signCommand": "powershell -ExecutionPolicy Bypass -File ./sign.ps1 %1"
}
} }
} }

View File

@ -0,0 +1,21 @@
{
"bundle": {
"targets": ["deb", "appimage"],
"resources": ["resources/pre-install/**/*"],
"externalBin": ["binaries/cortex-server", "resources/bin/uv"],
"linux": {
"appimage": {
"bundleMediaFramework": false,
"files": {}
},
"deb": {
"files": {
"usr/bin/bun": "resources/bin/bun",
"usr/lib/Jan/binaries": "binaries/deps",
"usr/lib/Jan/binaries/engines": "binaries/engines",
"usr/lib/Jan/binaries/libvulkan.so": "binaries/libvulkan.so"
}
}
}
}
}

View File

@ -0,0 +1,15 @@
{
"bundle": {
"targets": ["app", "dmg"],
"resources": [
"resources/pre-install/**/*",
"resources/lib/",
"binaries/**/*"
],
"externalBin": [
"binaries/cortex-server",
"resources/bin/bun",
"resources/bin/uv"
]
}
}

View File

@ -0,0 +1,18 @@
{
"bundle": {
"targets": ["nsis"],
"resources": [
"resources/pre-install/**/*",
"resources/lib/",
"binaries/**/*"
],
"externalBin": [
"binaries/cortex-server",
"resources/bin/bun",
"resources/bin/uv"
],
"windows": {
"signCommand": "powershell -ExecutionPolicy Bypass -File ./sign.ps1 %1"
}
}
}

View File

@ -8,7 +8,8 @@
"build": "tsc -b && vite build", "build": "tsc -b && vite build",
"lint": "eslint .", "lint": "eslint .",
"preview": "vite preview", "preview": "vite preview",
"test": "vitest" "test": "vitest --run",
"test:coverage": "vitest --coverage --run"
}, },
"dependencies": { "dependencies": {
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",
@ -45,6 +46,7 @@
"fzf": "^0.5.2", "fzf": "^0.5.2",
"i18next": "^25.0.1", "i18next": "^25.0.1",
"katex": "^0.16.22", "katex": "^0.16.22",
"lodash.clonedeep": "^4.5.0",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"lucide-react": "^0.522.0", "lucide-react": "^0.522.0",
"motion": "^12.10.5", "motion": "^12.10.5",
@ -77,16 +79,24 @@
"@eslint/js": "^9.22.0", "@eslint/js": "^9.22.0",
"@tanstack/router-plugin": "^1.116.1", "@tanstack/router-plugin": "^1.116.1",
"@types/culori": "^2.1.1", "@types/culori": "^2.1.1",
"@types/istanbul-lib-report": "^3",
"@types/istanbul-reports": "^3",
"@types/lodash.clonedeep": "^4",
"@types/lodash.debounce": "^4", "@types/lodash.debounce": "^4",
"@types/node": "^22.14.1", "@types/node": "^22.14.1",
"@types/react": "^19.0.10", "@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4", "@types/react-dom": "^19.0.4",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
"@vitest/coverage-v8": "3.2.4",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"eslint": "^9.22.0", "eslint": "^9.22.0",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19", "eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.0.0", "globals": "^16.0.0",
"istanbul-api": "^3.0.0",
"istanbul-lib-coverage": "^3.2.2",
"istanbul-lib-report": "^3.0.1",
"istanbul-reports": "^3.1.7",
"tailwind-merge": "^3.2.0", "tailwind-merge": "^3.2.0",
"typescript": "~5.8.3", "typescript": "~5.8.3",
"typescript-eslint": "^8.26.1", "typescript-eslint": "^8.26.1",

View File

@ -14,6 +14,7 @@ const LANGUAGES = [
{ value: 'vn', label: 'Tiếng Việt' }, { value: 'vn', label: 'Tiếng Việt' },
{ value: 'zh-CN', label: '简体中文' }, { value: 'zh-CN', label: '简体中文' },
{ value: 'zh-TW', label: '繁體中文' }, { value: 'zh-TW', label: '繁體中文' },
{ value: 'de-DE', label: 'Deutsch' },
] ]
export default function LanguageSwitcher() { export default function LanguageSwitcher() {

View File

@ -12,27 +12,30 @@ interface Props {
// Zustand store for thinking block state // Zustand store for thinking block state
type ThinkingBlockState = { type ThinkingBlockState = {
thinkingState: { [id: string]: boolean } thinkingState: { [id: string]: boolean }
toggleState: (id: string) => void setThinkingState: (id: string, expanded: boolean) => void
} }
const useThinkingStore = create<ThinkingBlockState>((set) => ({ const useThinkingStore = create<ThinkingBlockState>((set) => ({
thinkingState: {}, thinkingState: {},
toggleState: (id) => setThinkingState: (id, expanded) =>
set((state) => ({ set((state) => ({
thinkingState: { thinkingState: {
...state.thinkingState, ...state.thinkingState,
[id]: !state.thinkingState[id], [id]: expanded,
}, },
})), })),
})) }))
const ThinkingBlock = ({ id, text }: Props) => { const ThinkingBlock = ({ id, text }: Props) => {
const { thinkingState, toggleState } = useThinkingStore() const { thinkingState, setThinkingState } = useThinkingStore()
const { streamingContent } = useAppState() const { streamingContent } = useAppState()
const { t } = useTranslation() const { t } = useTranslation()
const loading = !text.includes('</think>') && streamingContent const loading = !text.includes('</think>') && streamingContent
const isExpanded = thinkingState[id] ?? false const isExpanded = thinkingState[id] ?? (loading ? true : false)
const handleClick = () => toggleState(id) const handleClick = () => {
const newExpandedState = !isExpanded
setThinkingState(id, newExpandedState)
}
if (!text.replace(/<\/?think>/g, '').trim()) return null if (!text.replace(/<\/?think>/g, '').trim()) return null

View File

@ -26,7 +26,6 @@ import {
} from '@/components/ui/dialog' } from '@/components/ui/dialog'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { toast } from 'sonner'
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@ -75,6 +74,69 @@ const CopyButton = ({ text }: { text: string }) => {
) )
} }
const EditDialog = ({
message,
setMessage,
}: {
message: string
setMessage: (message: string) => void
}) => {
const { t } = useTranslation()
const [draft, setDraft] = useState(message)
const handleSave = () => {
if (draft !== message) {
setMessage(draft)
}
}
return (
<Dialog>
<DialogTrigger>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex outline-0 items-center gap-1 hover:text-accent transition-colors cursor-pointer group relative">
<IconPencil size={16} />
</div>
</TooltipTrigger>
<TooltipContent>
<p>{t('edit')}</p>
</TooltipContent>
</Tooltip>
</DialogTrigger>
<DialogContent className="w-3/4 h-3/4">
<DialogHeader>
<DialogTitle>{t('common:dialogs.editMessage.title')}</DialogTitle>
<Textarea
value={draft}
onChange={(e) => setDraft(e.target.value)}
className="mt-2 resize-none h-full w-full"
onKeyDown={(e) => {
// Prevent key from being captured by parent components
e.stopPropagation()
}}
/>
<DialogFooter className="mt-2 flex items-center">
<DialogClose asChild>
<Button variant="link" size="sm" className="hover:no-underline">
Cancel
</Button>
</DialogClose>
<DialogClose asChild>
<Button
disabled={draft === message || !draft}
onClick={handleSave}
>
Save
</Button>
</DialogClose>
</DialogFooter>
</DialogHeader>
</DialogContent>
</Dialog>
)
}
// Use memo to prevent unnecessary re-renders, but allow re-renders when props change // Use memo to prevent unnecessary re-renders, but allow re-renders when props change
export const ThreadContent = memo( export const ThreadContent = memo(
( (
@ -85,9 +147,9 @@ export const ThreadContent = memo(
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
streamTools?: any streamTools?: any
contextOverflowModal?: React.ReactNode | null contextOverflowModal?: React.ReactNode | null
updateMessage?: (item: ThreadMessage, message: string) => void
} }
) => { ) => {
const [message, setMessage] = useState(item.content?.[0]?.text?.value || '')
const { t } = useTranslation() const { t } = useTranslation()
// Use useMemo to stabilize the components prop // Use useMemo to stabilize the components prop
@ -166,23 +228,6 @@ export const ThreadContent = memo(
} }
}, [deleteMessage, getMessages, item]) }, [deleteMessage, getMessages, item])
const editMessage = useCallback(
(messageId: string) => {
const threadMessages = getMessages(item.thread_id)
const index = threadMessages.findIndex((msg) => msg.id === messageId)
if (index === -1) return
// Delete all messages after the edited message
for (let i = threadMessages.length - 1; i >= index; i--) {
deleteMessage(threadMessages[i].thread_id, threadMessages[i].id)
}
sendMessage(message)
},
[deleteMessage, getMessages, item.thread_id, message, sendMessage]
)
const isToolCalls = const isToolCalls =
item.metadata && item.metadata &&
'tool_calls' in item.metadata && 'tool_calls' in item.metadata &&
@ -209,61 +254,14 @@ export const ThreadContent = memo(
</div> </div>
</div> </div>
<div className="flex items-center justify-end gap-2 text-main-view-fg/60 text-xs mt-2"> <div className="flex items-center justify-end gap-2 text-main-view-fg/60 text-xs mt-2">
<Dialog> <EditDialog
<DialogTrigger> message={item.content?.[0]?.text.value}
<Tooltip> setMessage={(message) => {
<TooltipTrigger asChild> if (item.updateMessage) {
<div className="flex outline-0 items-center gap-1 hover:text-accent transition-colors cursor-pointer group relative"> item.updateMessage(item, message)
<IconPencil size={16} /> }
</div>
</TooltipTrigger>
<TooltipContent>
<p>{t('edit')}</p>
</TooltipContent>
</Tooltip>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('common:dialogs.editMessage.title')}</DialogTitle>
<Textarea
value={message}
onChange={(e) => {
setMessage(e.target.value)
}}
className="mt-2 resize-none"
onKeyDown={(e) => {
// Prevent key from being captured by parent components
e.stopPropagation()
}} }}
/> />
<DialogFooter className="mt-2 flex items-center">
<DialogClose asChild>
<Button
variant="link"
size="sm"
className="hover:no-underline"
>
Cancel
</Button>
</DialogClose>
<DialogClose asChild>
<Button
disabled={!message}
onClick={() => {
editMessage(item.id)
toast.success(t('common:toast.editMessage.title'), {
id: 'edit-message',
description: t('common:toast.editMessage.description'),
})
}}
>
Save
</Button>
</DialogClose>
</DialogFooter>
</DialogHeader>
</DialogContent>
</Dialog>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<button <button
@ -360,6 +358,12 @@ export const ThreadContent = memo(
'hidden' 'hidden'
)} )}
> >
<EditDialog
message={item.content?.[0]?.text.value}
setMessage={(message) =>
item.updateMessage && item.updateMessage(item, message)
}
/>
<CopyButton text={item.content?.[0]?.text.value || ''} /> <CopyButton text={item.content?.[0]?.text.value || ''} />
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
@ -391,7 +395,9 @@ export const ThreadContent = memo(
</DialogTrigger> </DialogTrigger>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>{t('common:dialogs.messageMetadata.title')}</DialogTitle> <DialogTitle>
{t('common:dialogs.messageMetadata.title')}
</DialogTitle>
<div className="space-y-2"> <div className="space-y-2">
<div className="border border-main-view-fg/10 rounded-md overflow-hidden"> <div className="border border-main-view-fg/10 rounded-md overflow-hidden">
<CodeEditor <CodeEditor

View File

@ -138,6 +138,14 @@ export const sendCompletion = async (
baseURL: provider.base_url, baseURL: provider.base_url,
// Use Tauri's fetch to avoid CORS issues only for openai-compatible provider // Use Tauri's fetch to avoid CORS issues only for openai-compatible provider
...(providerName === 'openai-compatible' && { fetch: fetchTauri }), ...(providerName === 'openai-compatible' && { fetch: fetchTauri }),
// OpenRouter identification headers for Jan
// ref: https://openrouter.ai/docs/api-reference/overview#headers
...(provider.provider === 'openrouter' && {
defaultHeaders: {
'HTTP-Referer': 'https://jan.ai',
'X-Title': 'Jan',
},
}),
} as ExtendedConfigOptions) } as ExtendedConfigOptions)
if ( if (
thread.model.id && thread.model.id &&
@ -286,10 +294,10 @@ export const extractToolCall = (
* @param calls * @param calls
* @param builder * @param builder
* @param message * @param message
* @param content * @param abortController
* @param approvedTools - Record of approved tools per thread * @param approvedTools
* @param showModal - Function to show approval modal, returns true if approved * @param showModal
* @param allowAllMCPPermissions - Global setting to allow all MCP permissions without modal * @param allowAllMCPPermissions
*/ */
export const postMessageProcessing = async ( export const postMessageProcessing = async (
calls: ChatCompletionMessageToolCall[], calls: ChatCompletionMessageToolCall[],

View File

@ -0,0 +1,32 @@
{
"title": "Assistenten",
"editAssistant": "Assistent bearbeiten",
"deleteAssistant": "Assistenten löschen",
"deleteConfirmation": "Assistenten löschen",
"deleteConfirmationDesc": "Bist Du sicher, daß Du diesen Assistenten löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.",
"cancel": "Abbrechen",
"delete": "Löschen",
"addAssistant": "Assistenten hinzufügen",
"emoji": "Emoji",
"name": "Name",
"enterName": "Namen eingeben",
"description": "Beschreibung (optional)",
"enterDescription": "Beschreibung eingeben",
"instructions": "Anweisungen",
"enterInstructions": "Anweisungen eingeben",
"predefinedParameters": "Vordefinierte Parameter",
"parameters": "Parameter",
"key": "Schlüssel",
"value": "Wert",
"stringValue": "String",
"numberValue": "Number",
"booleanValue": "Boolean",
"jsonValue": "JSON",
"trueValue": "True",
"falseValue": "False",
"jsonValuePlaceholder": "JSON Value",
"save": "Speichern",
"createNew": "Neuen Assistenten anlegen",
"personality": "Persönlichkeit",
"capabilities": "Fähigkeiten"
}

View File

@ -0,0 +1,10 @@
{
"welcome": "Hi, wie geht es Dir?",
"description": "Wie kann ich Dir heute helfen?",
"status": {
"empty": "Keine Chats gefunden"
},
"sendMessage": "Nachricht senden",
"newConversation": "Neue Konversation",
"clearHistory": "Verlauf löschen"
}

View File

@ -0,0 +1,267 @@
{
"assistants": "Assistenten",
"hardware": "Hardware",
"mcp-servers": "Mcp Server",
"local_api_server": "Lokaler API Server",
"https_proxy": "HTTPS Proxy",
"extensions": "Erweiterungen",
"general": "Allgemein",
"settings": "Einstellungen",
"modelProviders": "Modell Anbieter",
"appearance": "Erscheinung",
"privacy": "Privatsphäre",
"keyboardShortcuts": "Shortcuts",
"newChat": "Neuer Chat",
"favorites": "Favoriten",
"recents": "Kürzlich",
"hub": "Hub",
"helpSupport": "Hilfe & Support",
"helpUsImproveJan": "Hilf uns Jan zu verbessern",
"unstarAll": "Alle De-Favorisieren",
"unstar": "De-Favorisieren",
"deleteAll": "Alles löschen",
"star": "Favorisieren",
"rename": "Umbenennen",
"delete": "Löschen",
"copied": "Kopiert!",
"dataFolder": "Daten Ordner",
"others": "Andere",
"language": "Sprache",
"reset": "Zurücksetzen",
"search": "Suchen",
"name": "Name",
"cancel": "Abbrechen",
"create": "Anlegen",
"save": "Speichern",
"edit": "Editieren",
"copy": "Kopieren",
"back": "Zurück",
"close": "Schließen",
"next": "Nächster",
"finish": "Abschließen",
"skip": "Überspringen",
"allow": "Erlauben",
"deny": "Verbieten",
"start": "Start",
"stop": "Stop",
"preview": "Vorschau",
"compactWidth": "Kompakte Breite",
"fullWidth": "Volle Breite",
"dark": "Dunkel",
"light": "Hell",
"system": "System",
"auto": "Automatisch",
"english": "Englisch",
"medium": "Medium",
"newThread": "Neuer Thread",
"noResultsFound": "Keine Ergebnisse gefunden",
"noThreadsYet": "Keine Threads bisher",
"noThreadsYetDesc": "Starte eine neue Unterhaltung, um deinen Threadverlauf hier anzuzeigen.",
"downloads": "Downloads",
"downloading": "Downloading",
"cancelDownload": "Download abbrechen",
"downloadCancelled": "Download wurde abgebrochen",
"downloadComplete": "Download abgeschlossen",
"thinking": "Denke nach...",
"thought": "Gedanke",
"callingTool": "Rufe Werkzeug auf",
"completed": "Abgeschlossen",
"image": "Bild",
"vision": "Vision",
"embeddings": "Einbettungen",
"tools": "Werkzeuge",
"webSearch": "Web Suche",
"reasoning": "Argumentation",
"selectAModel": "Wähle ein Modell",
"noToolsAvailable": "Keine Werkzeuge verfügbar",
"noModelsFoundFor": "Keine Modelle gefunden zu \"{{searchValue}}\"",
"customAvatar": "Benutzerdefinierter Avatar",
"editAssistant": "Assistenten bearbeiten",
"jan": "Jan",
"metadata": "Metadaten",
"regenerate": "Neu generieren",
"threadImage": "Thread Bild",
"editMessage": "Nachricht bearbeiten",
"deleteMessage": "Nachricht löschen",
"deleteThread": "Thread löschen",
"renameThread": "Thread umbenennen",
"threadTitle": "Thread Titel",
"deleteAllThreads": "Alle Threads löschen",
"allThreadsUnfavorited": "Alle Threads defavorisieren",
"deleteAllThreadsConfirm": "Bist Du sicher, daß Du alle Threads löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.",
"addProvider": "Anbieter hinzufügen",
"addOpenAIProvider": "OpenAI Anbieter hinzufügen",
"enterNameForProvider": "Gib einen Namen ein für den Anbieter",
"providerAlreadyExists": "Ein Anbieter mit dem Namen \"{{name}}\" existiert bereits. Bitte wähle einen anderen Namen.",
"adjustFontSize": "Schriftgröße einstellen",
"changeLanguage": "Sprache wechseln",
"editTheme": " Vorlage bearbeiten",
"editCodeBlockStyle": "Code Block Stil bearbeiten",
"editServerHost": "Server Host bearbeiten",
"pickColorWindowBackground": "Fensterhintergrundfarbe wählen",
"pickColorAppMainView": "Farben wählen für das Hauptfenster",
"pickColorAppPrimary": "Wähle primäre App-Farbe",
"pickColorAppAccent": "Wähle hervorgehobene App-Farbe",
"pickColorAppDestructive": "Wähle destruktive App-Farbe",
"apiKeyRequired": "API Key ist erforderlich",
"enterTrustedHosts": "Vertraute Hosts eingeben",
"placeholder": {
"chatInput": "Frage mich etwas..."
},
"confirm": "Bestätige",
"loading": "Lade...",
"error": "Fehler",
"success": "Erfolg",
"warning": "Warnung",
"noResultsFoundDesc": "Wir konnten keinen Chat finden, welcher mit deiner Suche übereinstimmt. Versuche andere Schlüsselworte.",
"searchModels": "Suche Modelle...",
"searchStyles": "Suche Styles...",
"createAssistant": "Assistenten anlegen",
"enterApiKey": "API Key eingeben",
"scrollToBottom": "Zum Ende scrollen",
"addModel": {
"title": "Modell hinzufügen",
"modelId": "Modell ID",
"enterModelId": "Modell ID eingeben",
"addModel": "Modell hinzufügen",
"description": "Neues Modell zum Anbieter hinzufügen",
"exploreModels": "Modelle des Anbieters ansehen"
},
"mcpServers": {
"editServer": "Server bearbeiten",
"addServer": "Server hinzufügen",
"serverName": "Server Name",
"enterServerName": "Server Namen eingeben",
"command": "Kommando",
"enterCommand": "Kommando eingeben",
"arguments": "Argumente",
"argument": "Argument {{index}}",
"envVars": "Umgebungs Variable",
"key": "Schlüssel",
"value": "Wert",
"save": "Speichern"
},
"deleteServer": {
"title": "Server löschen",
"delete": "Löschen"
},
"editJson": {
"errorParse": "Failed to parse JSON",
"errorPaste": "Failed to paste JSON",
"errorFormat": "Invalid JSON format",
"titleAll": "Edit All Servers Configuration",
"placeholder": "Enter JSON configuration...",
"save": "Save"
},
"editModel": {
"title": "Modell bearbeiten: {{modelId}}",
"description": "Konfiguriere die Modelfähigkeiten durch Umschalten der untenstehenden Optionen.",
"capabilities": "Modelfähigkeiten",
"tools": "Werkzeuge",
"vision": "Vision",
"embeddings": "Einbettungen",
"notAvailable": "Nicht verfügbar bisher"
},
"outOfContextError": {
"truncateInput": "Input verkleinern",
"title": "Out of context error",
"description": "Dieser Chat erreicht das KI Speicher Limit. Wir können das Speicherfenster vergrößern (auch Kontextgröße genannt), so daß sich die KI an mehr erinnern kann, aber dies erfordert es mehr Speicher zu verwenden. Um Platz zu schaffen können wir auch den Input verkleinern, was bedeutet, daß die KI einen Teil seiner Chat-Historie vergisst.",
"increaseContextSizeDescription": "Möchtest Du die Kontextgröße erhöhen?",
"increaseContextSize": "Kontextgröße erhöhen"
},
"toolApproval": {
"title": "Anfrage für Werkzeugnutzung",
"description": "Der Assistant möchte <strong>{{toolName}}</strong> verwenden",
"securityNotice": "Erlaube nur Werkzeuge zu nutzen denen Du vertraust. Werkzeuge können auf deine Daten oder System zugreifen.",
"deny": "Ablehnen",
"allowOnce": "Einmal erlauben",
"alwaysAllow": "Immer erlauben"
},
"deleteModel": {
"title": "Modell löschen: {{modelId}}",
"description": "Bist Du sicher, daß Du dieses Modell löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.",
"success": "Modell {{modelId}} wurde permanent gelöscht.",
"cancel": "Abbrechen",
"delete": "Löschen"
},
"deleteProvider": {
"title": "Anbieter löschen",
"description": "Lösche diesen Anbieter und alle seine Modelle. Diese Aktion kann nicht rückgängig gemacht werden.",
"success": "Anbieter {{provider}} wurde permanent gelöscht.",
"confirmTitle": "Anbieter löschen: {{provider}}",
"confirmDescription": "Bist Du sicher, daß Du diesen Anbieter löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.",
"cancel": "Abbrechen",
"delete": "Löschen"
},
"modelSettings": {
"title": "Modell Einstellungen - {{modelId}}",
"description": "Modeleinstellungen konfigurieren, um die Leistung und das Verhalten zu optimieren."
},
"dialogs": {
"changeDataFolder": {
"title": "Speicherort des Datenordners ändern",
"description": "Bist Du sicher den Speicherort des Datenordners zu ändern? Dies wird alle Daten zum neuen Speicherort verschieben und anschließend die Anwendung neu starten.",
"currentLocation": "Aktueller Speicherort:",
"newLocation": "Neuer Speicherort:",
"cancel": "Abbrechen",
"changeLocation": "Speicherort ändern"
},
"deleteAllThreads": {
"title": "Alle Threads löschen",
"description": "Alle Threads werden gelöscht. Diese Aktion kann nicht rückgängig gemacht werden."
},
"deleteThread": {
"description": "Bist Du sicher, daß Du diesen Thread löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden."
},
"editMessage": {
"title": "Nachricht bearbeiten"
},
"messageMetadata": {
"title": "Nachricht Metadaten"
}
},
"toast": {
"allThreadsUnfavorited": {
"title": "Alle Threads De-Favorisieren ",
"description": "Alle deine Threads wurden defavorisiert."
},
"deleteAllThreads": {
"title": "Alle Threads löschen",
"description": "Alle deine Threads wurden permanent gelöscht."
},
"renameThread": {
"title": "Thread umbenennen",
"description": "Thread Titel wurde umbenannt zu '{{title}}'"
},
"deleteThread": {
"title": "Thread löschen",
"description": "This thread has been permanently deleted."
},
"editMessage": {
"title": "Nachricht bearbeiten",
"description": "Die Nachricht wurde erfolgreich bearbeitet. Bitte warte auf die Antwort des Models."
},
"appUpdateDownloaded": {
"title": "App Update heruntergeladen",
"description": "Das App-Update wurde erfolgreich heruntergeladen."
},
"appUpdateDownloadFailed": {
"title": "App Update Download fehlgeschlagen",
"description": "Das App-Update konnte nicht heruntergeladen werden. Bitte versuche es noch einmal."
},
"downloadComplete": {
"title": "Download abgeschlossen",
"description": "Das Modell {{modelId}} wurde heruntergeladen"
},
"downloadCancelled": {
"title": "Download abgebrochen",
"description": "Der Download-Prozess wurde abgebrochen"
}
},
"cortexFailureDialog": {
"title": "Problem mit der lokalen KI-Engine",
"description": "Die Lokale KI-Engine (Cortex) konnte nach mehreren Versuchen nicht gestartet werden. Dies verhindert möglicherweise die korrekte Ausführung diverser Funktionalitäten.",
"contactSupport": "Support kontaktieren",
"restartJan": "Jan neu starten"
}
}

View File

@ -0,0 +1,31 @@
{
"sortNewest": "Neueste",
"sortMostDownloaded": "Meist heruntergeladen",
"use": "Nutzen",
"download": "Herunterladen",
"downloaded": "Heruntergeladen",
"loadingModels": "Lade Modelle...",
"noModels": "Keine Modelle gefunden",
"by": "Von",
"downloads": "Downloads",
"variants": "Varianten",
"showVariants": "Zeige Varianten",
"useModel": "Nutze dieses Modell",
"downloadModel": "Modell herunterladen",
"searchPlaceholder": "Suche nach Modellen auf Hugging Face...",
"editTheme": "Bearbeite Erscheinungsbild",
"joyride": {
"recommendedModelTitle": "Empfohlenes Modell",
"recommendedModelContent": "Durchsuche und lade leistungsstarke KI-Modelle verschiedener Anbieter an einem Ort herunter. Wir empfehlen mit Jan-Nano zu beginnen, einem Modell, das für Funktionsaufrufe, Werkzeug-Integration und Forschungsfunktionen optimiert ist. Es eignet sich ideal für die Entwicklung interaktiver KI-Agenten.",
"downloadInProgressTitle": "Download läuft",
"downloadInProgressContent": "Dein Modell wird jetzt heruntergeladen. Verfolge hier den Fortschritt. Sobald der Download abgeschlossen ist, ist es einsatzbereit.",
"downloadModelTitle": "Modell herunterladen",
"downloadModelContent": "Klicke auf den Download Button um das Herunterladen zu beginnen.",
"back": "Zurück",
"close": "Schließen",
"lastWithDownload": "Download",
"last": "Fertig",
"next": "Nächstes",
"skip": "Überspringen"
}
}

View File

@ -0,0 +1,3 @@
{
"noLogs": "Keine Logs verfügbar"
}

View File

@ -0,0 +1,43 @@
{
"editServer": "MCP Server bearbeiten",
"addServer": "MCP Server hinzufügen",
"serverName": "Server Name",
"enterServerName": "Server Namen eingeben",
"command": "Kommando",
"enterCommand": "Kommando eingeben (uvx oder npx)",
"arguments": "Argumente",
"argument": "Argument {{index}}",
"envVars": "Umgebungs Variablen",
"key": "Schlüssel",
"value": "Wert",
"save": "Speichern",
"status": "Status",
"connected": "Verbunden",
"disconnected": "Nicht verbunden",
"deleteServer": {
"title": "MCP Server löschen",
"description": "Bist Du sicher, dass Du den MCP Server {{serverName}} löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.",
"delete": ""
},
"editJson": {
"title": "JSON für den MCP Server bearbeiten: {{serverName}}",
"titleAll": "JSON für alle MCP Server bearbeiten",
"placeholder": "JSON Konfiguration eingeben",
"errorParse": "Fehler beim Parsen der initialen Daten",
"errorPaste": "Ungültiges JSON Format in dem eingefügten Inhalt",
"errorFormat": "Ungültiges JSON Format",
"save": "Speichern"
},
"checkParams": "Bitte überprüfe die Parameter gemäß dem Tutorial.",
"title": "MCP Server",
"experimental": "Experimentell",
"editAllJson": "JSON aller Server bearbeiten",
"findMore": "Finde mehr MCP Server bei",
"allowPermissions": "Erlaube allen MCP Werkzeugen den Zugriff",
"allowPermissionsDesc": "Wenn aktiviert, werden alle MCP-Werkzeug-Aufrufe automatisch genehmigt, ohne dass Berechtigungsdialoge angezeigt werden.",
"noServers": "Keine MCP Server gefunden",
"args": "Argumente",
"env": "Umgebung",
"serverStatusActive": "Server {{serverKey}} erfolgreich aktiviert",
"serverStatusInactive": "Server {{serverKey}} erfolgreich deaktiviert"
}

View File

@ -0,0 +1,7 @@
{
"title": "Out of context error",
"description": "Dieser Chat erreicht das KI Speicher Limit. Wir können das Speicherfenster vergrößern (auch Kontextgröße genannt), so dass sich die KI an mehr erinnern kann, aber dies erfordert es mehr Speicher zu verwenden. Um Platz zu schaffen können wir auch den Input verkleinern, was bedeutet, dass die KI einen Teil seiner Chat-Historie vergisst.",
"increaseContextSizeDescription": "Möchtest Du die Kontextgröße erhöhen?",
"truncateInput": "Input verkleinern",
"increaseContextSize": "Kontextgröße erhöhen"
}

View File

@ -0,0 +1,5 @@
{
"addProvider": "Anbieter hinzufügen",
"addOpenAIProvider": "OpenAI Anbieter hinzufügen",
"enterNameForProvider": "Namen für Anbieter eingeben"
}

View File

@ -0,0 +1,68 @@
{
"joyride": {
"chooseProviderTitle": "Wähle einen Anbieter",
"chooseProviderContent": "Wähle den Anbieter aus, den Du verwenden möchtest, und stelle sicher, daß Du Zugriff auf einen API-Schlüssel dafür hast.",
"getApiKeyTitle": "Hole Dir Deinen API-Schlüssel",
"getApiKeyContent": "Melde Dich bei dem Anbieter an, um Deinen API-Schlüssel zu finden oder zu generieren.",
"insertApiKeyTitle": "Gebe Deinen API-Schlüssel ein",
"insertApiKeyContent": "Füge hier Deinen API-Schlüssel ein, um eine Verbindung zum Anbieter herzustellen und ihn zu aktivieren.",
"back": "Zurück",
"close": "Schließen",
"last": "Abschließen",
"next": "Nächster",
"skip": "Überspringen"
},
"refreshModelsError": "Der Anbieter muss über eine Basis-URL und einen API-Schlüssel verfügen, um Modelle abzurufen.",
"refreshModelsSuccess": "{{count}} neue(s) Modell(e) hinzugefügt von {{provider}}.",
"noNewModels": "Keine neuen Modelle gefunden. Alle verfügbaren Modelle sind bereits hinzugefügt.",
"refreshModelsFailed": "Das Abrufen der Modelle von {{provider}} ist fehlgeschlagen. Bitte überprüfe Deinen API-Schlüssel und die Basis-URL.",
"models": "Modelle",
"refreshing": "Aktualisiere...",
"refresh": "Aktualisieren",
"import": "Importieren",
"importModelSuccess": "Modell von {{provider}} wurde erfolgreich importiert.",
"importModelError": "Modellimport ist fehlgeschlagen:",
"stop": "Stop",
"start": "Start",
"noModelFound": "Kein Modell gefunden",
"noModelFoundDesc": "Verfügbare Modelle werden hier aufgelistet. Wenn Du noch keine Modelle hast, können diese im Hub heruntergeladen werden.",
"configuration": "Konfiguration",
"apiEndpoint": "API Endpunkt",
"testConnection": "Teste Verbindung",
"addModel": {
"title": "Neues Modell hinzufügen",
"description": "Neues Modell zu Anbieter {{provider}} hinzufügen.",
"modelId": "Modell ID",
"enterModelId": "Modell ID eingeben",
"exploreModels": "Sehe Modellliste von {{provider}}",
"addModel": "Modell hinzufügen"
},
"deleteModel": {
"title": "Lösche Modell: {{modelId}}",
"description": "Möchtest Du dieses Modell wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.",
"success": "Modell {{modelId}} wurde permanent gelöscht.",
"cancel": "Abbrechen",
"delete": "Löschen"
},
"deleteProvider": {
"title": "Anbieter löschen",
"description": "Lösche diesen Anbieter und alle seine Modelle. Diese Aktion kann nicht rückgängig gemacht werden.",
"success": "Anbieter {{provider}} wurde permanent gelöscht.",
"confirmTitle": "Lösche Anbieter: {{provider}}",
"confirmDescription": "Möchtest Du diesen Anbieter wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.",
"cancel": "Abbrechen",
"delete": "Löschen"
},
"editModel": {
"title": "Modell bearbeiten: {{modelId}}",
"description": "Konfiguriere die Modellfunktionen, indem Du die folgenden Optionen einstellst.",
"capabilities": "Fähigkeiten",
"tools": "Werkzeuge",
"vision": "Vision",
"embeddings": "Einbettungen",
"notAvailable": "Noch nicht verfügbar"
},
"addProvider": "Anbieter hinzufügen",
"addOpenAIProvider": "OpenAI Anbieter hinzufügen",
"enterNameForProvider": "Namen für den Anbieter eingeben"
}

View File

@ -0,0 +1,248 @@
{
"autoDownload": "Automatisch neue Updates herunterladen",
"checkForUpdates": "Auf Updates prüfen",
"checkForUpdatesDesc": "Prüfe, ob eine neuere Version von Jan verfügbar ist.",
"checkingForUpdates": "Suche nach Updates...",
"noUpdateAvailable": "Du verwendest die neueste Version",
"devVersion": "Entwicklungsversion erkannt",
"updateError": "Fehler beim Suchen nach Updates",
"changeLocation": "Ort ändern",
"copied": "Kopiert",
"copyPath": "Pfad kopieren",
"openLogs": "Logs öffnen",
"revealLogs": "Logs aufzeigen",
"showInFinder": "Zeige im Finder",
"showInFileExplorer": "Zeige in Datei Explorer",
"openContainingFolder": "Enthaltenen Ordner öffnen",
"failedToRelocateDataFolder": "Datenordner konnte nicht verschoben werden",
"failedToRelocateDataFolderDesc": "Der Datenordner konnte nicht verschoben werden. Bitte versuche es erneut.",
"factoryResetTitle": "Auf Werkseinstellungen zurücksetzen",
"factoryResetDesc": "Dadurch werden alle App-Einstellungen auf die Standardeinstellungen zurückgesetzt. Dieser Vorgang kann nicht rückgängig gemacht werden. Wir empfehlen dies nur, wenn die App beschädigt ist.",
"cancel": "Abbrechen",
"reset": "Zurücksetzen",
"resources": "Ressourcen",
"documentation": "Dokumentation",
"documentationDesc": "Erfahre, wie Du Jan verwenden und seine Funktionen erkunden kannst.",
"viewDocs": "Dokumentation ansehen",
"releaseNotes": "Release Notes",
"releaseNotesDesc": "Siehe, was es Neues in der neuesten Version von Jan gibt.",
"viewReleases": "Releases ansehen",
"community": "Community",
"github": "GitHub",
"githubDesc": "Beitragen zu Jan's Entwicklung.",
"discord": "Discord",
"discordDesc": "Trete unserer Community für Unterstützung und Diskussionen bei.",
"support": "Support",
"reportAnIssue": "Melde ein Problem",
"reportAnIssueDesc": "Hast Du einen Bug gefunden? Hilf uns, indem Du ein Problem auf GitHub meldest.",
"reportIssue": "Problem melden",
"credits": "Credits",
"creditsDesc1": "Jan wurde mit ❤️ gebaut vom Menlo Team.",
"creditsDesc2": "Besonderer Dank gilt unseren Open-Source-Abhängigkeiten - insbesondere llama.cpp und Tauri - und unserer großartigen KI-Community.",
"appVersion": "App Version",
"dataFolder": {
"appData": "App Daten",
"appDataDesc": "Standardspeicherort für Nachrichten und andere Benutzerdaten.",
"appLogs": "App Logs",
"appLogsDesc": "Zeige detaillierte Logs der App an."
},
"others": {
"spellCheck": "Rechtschreibprüfung",
"spellCheckDesc": "Aktiviere die Rechtschreibprüfung für Deine Threads.",
"resetFactory": "Auf Werkseinstellungen zurücksetzen",
"resetFactoryDesc": "Setzt die Anwendung in den ursprünglichen Zustand zurück und löscht dabei alle Modelle und den Chatverlauf. Diese Aktion ist irreversibel und wird nur empfohlen, wenn die Anwendung beschädigt ist."
},
"shortcuts": {
"application": "Anwendung",
"newChat": "Neuer Chat",
"newChatDesc": "Neuen Chat anlegen.",
"toggleSidebar": "Seitenleiste umschalten",
"toggleSidebarDesc": "Seitenleiste ein- oder ausblenden.",
"zoomIn": "Vergrößern",
"zoomInDesc": "Erhöhe die Zoomstufe.",
"zoomOut": "Verkleinern",
"zoomOutDesc": "Verringere die Zoomstufe.",
"chat": "Chat",
"sendMessage": "Nachricht senden",
"sendMessageDesc": "Aktuelle Nachricht senden",
"enter": "Eingabe",
"newLine": "Neue Zeile",
"newLineDesc": "Neue Zeile einfügen.",
"shiftEnter": "Shift + Enter",
"navigation": "Navigation",
"goToSettings": "Gehe zu den Einstellungen",
"goToSettingsDesc": "Einstellungen öffnen."
},
"appearance": {
"title": "Erscheinungsbild",
"theme": "Theme",
"themeDesc": "Dem Betriebssystem anpassen.",
"fontSize": "Schriftgröße",
"fontSizeDesc": "Einstellen der App Schriftgröße",
"windowBackground": "Fenster Hintergrund",
"windowBackgroundDesc": "Lege die Hintergrundfarbe des App-Fensters fest.",
"appMainView": "App-Hauptansicht",
"appMainViewDesc": "Lege die Hintergrundfarbe des Hauptinhaltsbereichs fest.",
"primary": "Primär",
"primaryDesc": "Lege die Primärfarbe für UI-Komponenten fest.",
"accent": "Akzent",
"accentDesc": "Lege die Akzentfarbe für UI-Hervorhebungen fest.",
"destructive": "Destruktiv",
"destructiveDesc": "Lege die Farbe für destruktive Aktionen fest.",
"resetToDefault": "Auf Werkseinstellungen zurücksetzen",
"resetToDefaultDesc": "Setzt alle Darstellungseinstellungen auf die Standardeinstellungen zurück.",
"resetAppearanceSuccess": "Erscheinungsbild erfolgreich zurückgesetzt",
"resetAppearanceSuccessDesc": "Alle Darstellungseinstellungen wurden auf die Standardeinstellungen zurückgesetzt.",
"chatWidth": "Chat Breite",
"chatWidthDesc": "Passe die Breite der Chatansicht an.",
"codeBlockTitle": "Code Block",
"codeBlockDesc": "Wähle einen Stil zur Syntaxhervorhebung.",
"showLineNumbers": "Zeilennummern anzeigen",
"showLineNumbersDesc": "Zeilennummern in Codeblöcken anzeigen.",
"resetCodeBlockStyle": "Codeblockstil zurücksetzen",
"resetCodeBlockStyleDesc": "Codeblockstil auf Standard zurücksetzen.",
"resetCodeBlockSuccess": "Codeblockstil erfolgreich zurückgesetzt",
"resetCodeBlockSuccessDesc": "Der Codeblockstil wurde auf die Standardeinstellung zurückgesetzt."
},
"hardware": {
"os": "Betriebssystem",
"name": "Name",
"version": "Version",
"cpu": "CPU",
"model": "Modell",
"architecture": "Architektur",
"cores": "Kerne",
"instructions": "Instruktionen",
"usage": "Nutzung",
"memory": "Speicher",
"totalRam": "RAM Total",
"availableRam": "Verfügbarer RAM",
"vulkan": "Vulkan",
"enableVulkan": "Vulkan aktivieren",
"enableVulkanDesc": "Verwende die Vulkan-API zur GPU-Beschleunigung. Aktiviere Vulkan nicht, wenn Du eine NVIDIA-GPU verwendest, da dies zu Kompatibilitätsproblemen führen kann.",
"gpus": "GPUs",
"noGpus": "Keine GPUs erkannt",
"vram": "VRAM",
"freeOf": "frei von",
"driverVersion": "Treiber Version",
"computeCapability": "Rechenfähigkeit",
"systemMonitor": "System Monitor"
},
"httpsProxy": {
"proxy": "Proxy",
"proxyUrl": "Proxy URL",
"proxyUrlDesc": "Die URL und der Port deines Proxyservers.",
"proxyUrlPlaceholder": "http://proxy.example.com:8080",
"authentication": "Authentifizierung",
"authenticationDesc": "Anmeldeinformationen für den Proxyserver, falls erforderlich.",
"username": "Username",
"password": "Passwort",
"noProxy": "Kein Proxy",
"noProxyDesc": "Eine durch Kommas getrennte Liste von Hosts, um den Proxy zu umgehen.",
"noProxyPlaceholder": "localhost,127.0.0.1,.local",
"sslVerification": "SSL Verifikation",
"ignoreSsl": "SSL Certificates ignorieren",
"ignoreSslDesc": "Erlaube selbstsignierte oder nicht verifizierte Zertifikate. Dies kann für einige Proxys erforderlich sein, verringert aber die Sicherheit. Aktiviere diese Option nur, wenn Du Deinem Proxy vertraust.",
"proxySsl": "Proxy SSL",
"proxySslDesc": "Validieren des SSL-Zertifikats, wenn eine Verbindung mit dem Proxy hergestellt wird.",
"proxyHostSsl": "Proxy Host SSL",
"proxyHostSslDesc": "Validieren des SSL-Zertifikats des Proxy-Hosts.",
"peerSsl": "Peer SSL",
"peerSslDesc": "Validieren des SSL-Zertifikats von Peer-Verbindungen.",
"hostSsl": "Host SSL",
"hostSslDesc": "Validieren der SSL-Zertifikate der Zielhosts."
},
"localApiServer": {
"title": "Lokaler API Server",
"description": "Führe lokal einen OpenAI-kompatiblen Server aus.",
"startServer": "Start Server",
"stopServer": "Stop Server",
"serverLogs": "Server Logs",
"serverLogsDesc": "Zeige detaillierte Logs des lokalen API-Servers an.",
"openLogs": "Logs öffnen",
"serverConfiguration": "Server Konfiguration",
"serverHost": "Server Host",
"serverHostDesc": "Netzwerkadresse für den Server.",
"serverPort": "Server Port",
"serverPortDesc": "Portnummer für den API Server.",
"apiPrefix": "API Prefix",
"apiPrefixDesc": "Pfadprefix für den API Endpunkt.",
"apiKey": "API Schlüssel",
"apiKeyDesc": "Authentifiziere Anfragen mit einem API-Schlüssel.",
"trustedHosts": "Vertrauenswürdige Hosts",
"trustedHostsDesc": "Hosts, die auf den Server zugreifen dürfen, durch Kommas getrennt.",
"advancedSettings": "Erweiterte Einstellungen",
"cors": "Cross-Origin Resource Sharing (CORS)",
"corsDesc": "Erlaube Cross-Origin-Anfragen an den API-Server.",
"verboseLogs": "Ausführliche Server Logs",
"verboseLogsDesc": "Aktiviere detaillierte Server Logs zum Debuggen"
},
"privacy": {
"analytics": "Analytik",
"helpUsImprove": "Hilf uns, uns zu verbessern",
"helpUsImproveDesc": "Um uns bei der Verbesserung von Jan zu unterstützen, kannst Du uns anonyme Daten wie Funktionsnutzung und Benutzerzahlen mitteilen. Wir erfassen niemals Deine Chats oder persönlichen Daten.",
"privacyPolicy": "Du hast die volle Kontrolle über Deine Daten. Erfahre mehr in unserer Datenschutzerklärung.",
"analyticsDesc": "Um Jan zu verbessern, müssen wir verstehen, wie es genutzt wird - aber nur mit deiner Hilfe. Du kannst diese Einstellung jederzeit ändern.",
"privacyPromises": "Deine Auswahl hier ändert nichts an unseren grundlegenden Datenschutzversprechen:",
"promise1": "Deine Gespräche bleiben privat und auf deinem Gerät",
"promise2": "Wir erfassen niemals Deine persönlichen Daten oder Chat-Inhalte",
"promise3": "Der gesamte Datenaustausch erfolgt anonym und aggregiert",
"promise4": "Du kannst dich jederzeit abmelden, ohne die Funktionalität zu verlieren",
"promise5": "Wir sind transparent darüber, was wir sammeln und warum"
},
"general": {
"showInFinder": "Im Finder zeigen",
"showInFileExplorer": "Zeige im Datei Explorer",
"openContainingFolder": "Enthaltenen Ordner öffnen",
"failedToRelocateDataFolder": "Datenordner konnte nicht verschoben werden",
"failedToRelocateDataFolderDesc": "Der Datenordner konnte nicht verschoben werden. Bitte versuche es erneut.",
"devVersion": "Entwicklungsversion erkannt",
"noUpdateAvailable": "Du verwendest die neueste Version",
"updateError": "Fehler beim Suchen nach Updates",
"appVersion": "App Version",
"checkForUpdates": "Auf Updates prüfen",
"checkForUpdatesDesc": "Prüfe, ob eine neuere Version von Jan verfügbar ist.",
"checkingForUpdates": "Suche nach Updates...",
"copied": "Kopiert",
"copyPath": "Pfad kopieren",
"changeLocation": "Ort ändern",
"openLogs": "Logs öffnen",
"revealLogs": "Logs anzeigen",
"factoryResetTitle": "Auf Werkseinstellungen zurücksetzen",
"factoryResetDesc": "Dadurch werden alle App-Einstellungen auf die Standardeinstellungen zurückgesetzt. Dieser Vorgang kann nicht rückgängig gemacht werden. Wir empfehlen dies nur, wenn die App beschädigt ist.",
"cancel": "Abbrechen",
"reset": "Zurücksetzen",
"resources": "Ressourcen",
"documentation": "Dokumentation",
"documentationDesc": "Erfahre, wie Du Jan verwenden und seine Funktionen erkunden kannst.",
"viewDocs": "Dokumentation ansehen",
"releaseNotes": "Release Notes",
"releaseNotesDesc": "Sehe, was es Neues in der neuesten Version von Jan gibt.",
"viewReleases": "Releases anzeigen",
"community": "Community",
"github": "GitHub",
"githubDesc": "Trage zu Jan's Entwicklung bei.",
"discord": "Discord",
"discordDesc": "Trete unserer Community für Unterstützung und Diskussionen bei.",
"support": "Support",
"reportAnIssue": "Melde ein Problem",
"reportAnIssueDesc": "Hast Du einen Bug gefunden? Hilf uns, indem Du ein Problem auf GitHub meldest.",
"reportIssue": "Problem melden",
"credits": "Credits",
"creditsDesc1": "Jan wurde mit ❤️ gebaut vom Menlo Team.",
"creditsDesc2": "Besonderer Dank gilt unseren Open-Source-Abhängigkeiten - insbesondere llama.cpp und Tauri - und unserer großartigen KI-Community."
},
"extensions": {
"title": "Erweiterungen"
},
"dialogs": {
"changeDataFolder": {
"title": "Speicherort des Datenordners ändern",
"description": "Möchtest Du den Speicherort des Datenordners wirklich ändern? Dadurch werden alle Deine Daten an den neuen Speicherort verschoben und die Anwendung neu gestartet.",
"currentLocation": "Aktueller Ort:",
"newLocation": "Neuer Ort:",
"cancel": "Abbrechen",
"changeLocation": "Ort ändern"
}
}
}

View File

@ -0,0 +1,6 @@
{
"welcome": "Willkommen bei Jan",
"description": "Um zu beginnen, musst Du entweder ein lokales KI-Modell herunterladen oder über einen API-Schlüssel eine Verbindung zu einem Cloud-Modell herstellen.",
"localModel": "Lokales Modell einrichten",
"remoteProvider": "Fernen Anbieter einrichten"
}

View File

@ -0,0 +1,28 @@
{
"title": "System Monitor",
"cpuUsage": "CPU Nutzung",
"model": "Modell",
"cores": "Kerne",
"architecture": "Architektur",
"currentUsage": "Aktuelle Nutzung",
"memoryUsage": "Speicher Nutzung",
"totalRam": "RAM Total",
"availableRam": "Verfügbarer RAM",
"usedRam": "Genutzter RAM",
"runningModels": "Laufende Modelle",
"noRunningModels": "Momentan laufen keine Modelle",
"provider": "Anbieter",
"uptime": "Betriebszeit",
"actions": "Aktionen",
"stop": "Stop",
"activeGpus": "Aktive GPUs",
"noGpus": "Keine GPUs detektiert",
"noActiveGpus": "Keine aktiven GPUs. Alle GPUs sind momentan deaktiviert.",
"vramUsage": "VRAM Nutzung",
"driverVersion": "Treiber Version:",
"computeCapability": "Rechenfähigkeit:",
"active": "Aktiv",
"performance": "Leistung",
"resources": "Ressourcen",
"refresh": "Aktualisieren"
}

View File

@ -0,0 +1,11 @@
{
"title": "Werkzeugaufruf angefordert",
"description": "Der Assistent möchte folgendes Werkzeug verwenden: <strong>{{toolName}}</strong>",
"securityNotice": "<strong>Sicherheitshinweis:</strong> Schädliche Werkzeuge oder Konversationsinhalte könnten den Assistenten möglicherweise zu schädlichen Aktionen verleiten. Überprüfe jeden Werkzeug-Aufruf sorgfältig, bevor Du ihn genehmigst.",
"deny": "Verweigern",
"allowOnce": "Einmal erlauben",
"alwaysAllow": "Immer erlauben",
"permissions": "Berechtigungen",
"approve": "Genehmigen",
"reject": "Ablehnen"
}

View File

@ -0,0 +1,10 @@
{
"toolApproval": {
"title": "Werkzeuggenehmigung erforderlich",
"description": "Der Assistent möchte das Werkzeug <strong>{{toolName}}</strong> verwenden.",
"securityNotice": "Dieses Werkzeug möchte eine Aktion ausführen. Bitte überprüfen und genehmigen oder Ablehnen.",
"deny": "Ablehnen",
"allowOnce": "Einmal erlauben",
"alwaysAllow": "Immer erlauben"
}
}

View File

@ -0,0 +1,10 @@
{
"newVersion": "Neue Version {{version}}",
"updateAvailable": "Update Verfügbar",
"nightlyBuild": "Nächtlicher Build",
"showReleaseNotes": "Zeige Release Notes",
"hideReleaseNotes": "Verstecke Release Notes",
"remindMeLater": "Erinnere Mich Später",
"downloading": "Lade herunter...",
"updateNow": "Jetzt Aktualisieren"
}

View File

@ -2,8 +2,10 @@ import { useEffect, useMemo, useRef, useState } from 'react'
import { createFileRoute, useParams } from '@tanstack/react-router' import { createFileRoute, useParams } from '@tanstack/react-router'
import { UIEventHandler } from 'react' import { UIEventHandler } from 'react'
import debounce from 'lodash.debounce' import debounce from 'lodash.debounce'
import cloneDeep from 'lodash.clonedeep'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { ArrowDown } from 'lucide-react' import { ArrowDown } from 'lucide-react'
import { Play } from 'lucide-react'
import HeaderPage from '@/containers/HeaderPage' import HeaderPage from '@/containers/HeaderPage'
import { useThreads } from '@/hooks/useThreads' import { useThreads } from '@/hooks/useThreads'
@ -18,7 +20,9 @@ import { useAppState } from '@/hooks/useAppState'
import DropdownAssistant from '@/containers/DropdownAssistant' import DropdownAssistant from '@/containers/DropdownAssistant'
import { useAssistant } from '@/hooks/useAssistant' import { useAssistant } from '@/hooks/useAssistant'
import { useAppearance } from '@/hooks/useAppearance' import { useAppearance } from '@/hooks/useAppearance'
import { ContentType, ThreadMessage } from '@janhq/core'
import { useTranslation } from '@/i18n/react-i18next-compat' import { useTranslation } from '@/i18n/react-i18next-compat'
import { useChat } from '@/hooks/useChat'
import { useSmallScreen } from '@/hooks/useMediaQuery' import { useSmallScreen } from '@/hooks/useMediaQuery'
// as route.threadsDetail // as route.threadsDetail
@ -38,6 +42,7 @@ function ThreadDetail() {
const { setMessages } = useMessages() const { setMessages } = useMessages()
const { streamingContent } = useAppState() const { streamingContent } = useAppState()
const { appMainViewBgColor, chatWidth } = useAppearance() const { appMainViewBgColor, chatWidth } = useAppearance()
const { sendMessage } = useChat()
const isSmallScreen = useSmallScreen() const isSmallScreen = useSmallScreen()
const { messages } = useMessages( const { messages } = useMessages(
@ -180,6 +185,26 @@ function ThreadDetail() {
lastScrollTopRef.current = scrollTop lastScrollTopRef.current = scrollTop
} }
const updateMessage = (item: ThreadMessage, message: string) => {
const newMessages: ThreadMessage[] = messages.map((m) => {
if (m.id === item.id) {
const msg: ThreadMessage = cloneDeep(m)
msg.content = [
{
type: ContentType.Text,
text: {
value: message,
annotations: m.content[0].text?.annotations ?? [],
},
},
]
return msg
}
return m
})
setMessages(threadId, newMessages)
}
// Use a shorter debounce time for more responsive scrolling // Use a shorter debounce time for more responsive scrolling
const debouncedScroll = debounce(handleDOMScroll) const debouncedScroll = debounce(handleDOMScroll)
@ -193,10 +218,22 @@ function ThreadDetail() {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])
// used when there is a sent/added user message and no assistant message (error or manual deletion)
const generateAIResponse = () => {
const latestUserMessage = messages[messages.length - 1]
if (latestUserMessage?.content?.[0]?.text?.value) {
sendMessage(latestUserMessage.content[0].text.value, false)
}
}
const threadModel = useMemo(() => thread?.model, [thread]) const threadModel = useMemo(() => thread?.model, [thread])
if (!messages || !threadModel) return null if (!messages || !threadModel) return null
const showScrollToBottomBtn = !isAtBottom && hasScrollbar
const showGenerateAIResponseBtn =
messages[messages.length - 1]?.role === 'user' && !streamingContent
return ( return (
<div className="flex flex-col h-full"> <div className="flex flex-col h-full">
<HeaderPage> <HeaderPage>
@ -243,6 +280,7 @@ function ThreadDetail() {
)) ))
} }
index={index} index={index}
updateMessage={updateMessage}
/> />
</div> </div>
) )
@ -266,9 +304,11 @@ function ThreadDetail() {
appMainViewBgColor.a === 1 appMainViewBgColor.a === 1
? 'from-main-view/20 bg-gradient-to-b to-main-view backdrop-blur' ? 'from-main-view/20 bg-gradient-to-b to-main-view backdrop-blur'
: 'bg-transparent', : 'bg-transparent',
!isAtBottom && hasScrollbar && 'visibility-visible opacity-100' (showScrollToBottomBtn || showGenerateAIResponseBtn) &&
'visibility-visible opacity-100'
)} )}
> >
{showScrollToBottomBtn && (
<div <div
className="bg-main-view-fg/10 px-4 border border-main-view-fg/5 flex items-center justify-center rounded-xl gap-x-2 cursor-pointer pointer-events-auto" className="bg-main-view-fg/10 px-4 border border-main-view-fg/5 flex items-center justify-center rounded-xl gap-x-2 cursor-pointer pointer-events-auto"
onClick={() => { onClick={() => {
@ -279,6 +319,16 @@ function ThreadDetail() {
<p className="text-xs">{t('scrollToBottom')}</p> <p className="text-xs">{t('scrollToBottom')}</p>
<ArrowDown size={12} /> <ArrowDown size={12} />
</div> </div>
)}
{showGenerateAIResponseBtn && (
<div
className="bg-main-view-fg/10 px-4 border border-main-view-fg/5 flex items-center justify-center rounded-xl gap-x-2 cursor-pointer pointer-events-auto"
onClick={generateAIResponse}
>
<p className="text-xs">{t('Generate AI Response')}</p>
<Play size={12} />
</div>
)}
</div> </div>
<ChatInput model={threadModel} /> <ChatInput model={threadModel} />
</div> </div>

View File

@ -72,5 +72,13 @@ export default defineConfig(({ mode }) => {
ignored: ['**/src-tauri/**'], ignored: ['**/src-tauri/**'],
}, },
}, },
test: {
environment: 'jsdom',
coverage: {
provider: 'v8',
reporter: ['json', 'lcov'],
reportsDirectory: '../coverage/vitest',
},
},
} }
}) })

1366
yarn.lock

File diff suppressed because it is too large Load Diff