diff --git a/.github/scripts/auto-sign.sh b/.github/scripts/auto-sign.sh
index 5e6ef9750..a2130e791 100755
--- a/.github/scripts/auto-sign.sh
+++ b/.github/scripts/auto-sign.sh
@@ -8,3 +8,5 @@ fi
# If both variables are set, execute the following commands
find "$APP_PATH" \( -type f -perm +111 -o -name "*.node" \) -exec codesign -s "$DEVELOPER_ID" --options=runtime {} \;
+
+find "$APP_PATH" -type f -name "*.o" -exec codesign -s "$DEVELOPER_ID" --options=runtime {} \;
diff --git a/.github/workflows/jan-electron-build-nightly.yml b/.github/workflows/jan-electron-build-nightly.yml
index cad2ac227..bc32f9ccc 100644
--- a/.github/workflows/jan-electron-build-nightly.yml
+++ b/.github/workflows/jan-electron-build-nightly.yml
@@ -48,8 +48,17 @@ jobs:
get-update-version:
uses: ./.github/workflows/template-get-update-version.yml
- build-macos:
- uses: ./.github/workflows/template-build-macos.yml
+ build-macos-x64:
+ uses: ./.github/workflows/template-build-macos-x64.yml
+ needs: [get-update-version, set-public-provider]
+ secrets: inherit
+ with:
+ ref: ${{ needs.set-public-provider.outputs.ref }}
+ public_provider: ${{ needs.set-public-provider.outputs.public_provider }}
+ new_version: ${{ needs.get-update-version.outputs.new_version }}
+
+ build-macos-arm64:
+ uses: ./.github/workflows/template-build-macos-arm64.yml
needs: [get-update-version, set-public-provider]
secrets: inherit
with:
@@ -76,8 +85,51 @@ jobs:
public_provider: ${{ needs.set-public-provider.outputs.public_provider }}
new_version: ${{ needs.get-update-version.outputs.new_version }}
+ combine-latest-mac-yml:
+ needs: [set-public-provider, build-macos-x64, build-macos-arm64]
+ runs-on: ubuntu-latest
+ steps:
+ - name: Getting the repo
+ uses: actions/checkout@v3
+ with:
+ ref: ${{ needs.set-public-provider.outputs.ref }}
+ - name: Download mac-x64 artifacts
+ uses: actions/download-artifact@v3
+ with:
+ name: latest-mac-x64
+ path: ./latest-mac-x64
+ - name: Download mac-arm artifacts
+ uses: actions/download-artifact@v3
+ with:
+ name: latest-mac-arm64
+ path: ./latest-mac-arm64
+
+ - name: 'Merge latest-mac.yml'
+ # unfortunately electron-builder doesn't understand that we have two different releases for mac-x64 and mac-arm, so we need to manually merge the latest files
+ # see https://github.com/electron-userland/electron-builder/issues/5592
+ run: |
+ ls -la .
+ ls -la ./latest-mac-x64
+ ls -la ./latest-mac-arm64
+ ls -la ./electron
+ cp ./electron/merge-latest-ymls.js /tmp/merge-latest-ymls.js
+ npm install js-yaml --prefix /tmp
+ node /tmp/merge-latest-ymls.js ./latest-mac-x64/latest-mac.yml ./latest-mac-arm64/latest-mac.yml ./latest-mac.yml
+ cat ./latest-mac.yml
+
+ - name: Upload latest-mac.yml
+ if: ${{ needs.set-public-provider.outputs.public_provider == 'cloudflare-r2' }}
+ run: |
+ aws s3api put-object --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --key "latest/latest-mac.yml" --body "./latest-mac.yml"
+ env:
+ AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }}
+ AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }}
+ AWS_DEFAULT_REGION: auto
+ AWS_EC2_METADATA_DISABLED: "true"
+
+
noti-discord-nightly-and-update-url-readme:
- needs: [build-macos, build-windows-x64, build-linux-x64, get-update-version, set-public-provider]
+ needs: [build-macos-x64, build-macos-arm64, build-windows-x64, build-linux-x64, get-update-version, set-public-provider, combine-latest-mac-yml]
secrets: inherit
if: github.event_name == 'schedule'
uses: ./.github/workflows/template-noti-discord-and-update-url-readme.yml
@@ -88,7 +140,7 @@ jobs:
new_version: ${{ needs.get-update-version.outputs.new_version }}
noti-discord-pre-release-and-update-url-readme:
- needs: [build-macos, build-windows-x64, build-linux-x64, get-update-version, set-public-provider]
+ needs: [build-macos-x64, build-macos-arm64, build-windows-x64, build-linux-x64, get-update-version, set-public-provider, combine-latest-mac-yml]
secrets: inherit
if: github.event_name == 'push'
uses: ./.github/workflows/template-noti-discord-and-update-url-readme.yml
@@ -99,7 +151,7 @@ jobs:
new_version: ${{ needs.get-update-version.outputs.new_version }}
noti-discord-manual-and-update-url-readme:
- needs: [build-macos, build-windows-x64, build-linux-x64, get-update-version, set-public-provider]
+ needs: [build-macos-x64, build-macos-arm64, build-windows-x64, build-linux-x64, get-update-version, set-public-provider, combine-latest-mac-yml]
secrets: inherit
if: github.event_name == 'workflow_dispatch' && github.event.inputs.public_provider == 'cloudflare-r2'
uses: ./.github/workflows/template-noti-discord-and-update-url-readme.yml
diff --git a/.github/workflows/jan-electron-build.yml b/.github/workflows/jan-electron-build.yml
index 20102447b..89e130bbd 100644
--- a/.github/workflows/jan-electron-build.yml
+++ b/.github/workflows/jan-electron-build.yml
@@ -9,8 +9,42 @@ jobs:
get-update-version:
uses: ./.github/workflows/template-get-update-version.yml
- build-macos:
- uses: ./.github/workflows/template-build-macos.yml
+ create-draft-release:
+ runs-on: ubuntu-latest
+ if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
+ outputs:
+ upload_url: ${{ steps.create_release.outputs.upload_url }}
+ version: ${{ steps.get_version.outputs.version }}
+ permissions:
+ contents: write
+ steps:
+ - name: Extract tag name without v prefix
+ id: get_version
+ run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV && echo "::set-output name=version::${GITHUB_REF#refs/tags/v}"
+ env:
+ GITHUB_REF: ${{ github.ref }}
+ - name: Create Draft Release
+ id: create_release
+ uses: actions/create-release@v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ tag_name: ${{ github.ref_name }}
+ release_name: "${{ env.VERSION }}"
+ draft: true
+ prerelease: false
+
+ build-macos-x64:
+ uses: ./.github/workflows/template-build-macos-x64.yml
+ secrets: inherit
+ needs: [get-update-version]
+ with:
+ ref: ${{ github.ref }}
+ public_provider: github
+ new_version: ${{ needs.get-update-version.outputs.new_version }}
+
+ build-macos-arm64:
+ uses: ./.github/workflows/template-build-macos-arm64.yml
secrets: inherit
needs: [get-update-version]
with:
@@ -36,8 +70,52 @@ jobs:
public_provider: github
new_version: ${{ needs.get-update-version.outputs.new_version }}
+ combine-latest-mac-yml:
+ needs: [build-macos-x64, build-macos-arm64, create-draft-release]
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ steps:
+ - name: Getting the repo
+ uses: actions/checkout@v3
+
+ - name: Download mac-x64 artifacts
+ uses: actions/download-artifact@v3
+ with:
+ name: latest-mac-x64
+ path: ./latest-mac-x64
+ - name: Download mac-arm artifacts
+ uses: actions/download-artifact@v3
+ with:
+ name: latest-mac-arm64
+ path: ./latest-mac-arm64
+
+ - name: 'Merge latest-mac.yml'
+ # unfortunately electron-builder doesn't understand that we have two different releases for mac-x64 and mac-arm, so we need to manually merge the latest files
+ # see https://github.com/electron-userland/electron-builder/issues/5592
+ run: |
+ ls -la .
+ ls -la ./latest-mac-x64
+ ls -la ./latest-mac-arm64
+ ls -la ./electron
+ cp ./electron/merge-latest-ymls.js /tmp/merge-latest-ymls.js
+ npm install js-yaml --prefix /tmp
+ node /tmp/merge-latest-ymls.js ./latest-mac-x64/latest-mac.yml ./latest-mac-arm64/latest-mac.yml ./latest-mac.yml
+ cat ./latest-mac.yml
+
+ - name: Yet Another Upload Release Asset Action
+ uses: shogo82148/actions-upload-release-asset@v1.7.2
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ upload_url: ${{ needs.create-draft-release.outputs.upload_url }}
+ asset_path: ./latest-mac.yml
+ asset_name: latest-mac.yml
+ asset_content_type: text/yaml
+ overwrite: true
+
update_release_draft:
- needs: [build-macos, build-windows-x64, build-linux-x64]
+ needs: [build-macos-x64, build-macos-arm64, build-windows-x64, build-linux-x64, combine-latest-mac-yml]
permissions:
# write permission is required to create a github release
contents: write
diff --git a/.github/workflows/template-build-macos-arm64.yml b/.github/workflows/template-build-macos-arm64.yml
new file mode 100644
index 000000000..54355d55c
--- /dev/null
+++ b/.github/workflows/template-build-macos-arm64.yml
@@ -0,0 +1,160 @@
+name: build-macos
+on:
+ workflow_call:
+ inputs:
+ ref:
+ required: true
+ type: string
+ default: 'refs/heads/main'
+ public_provider:
+ required: true
+ type: string
+ default: none
+ description: 'none: build only, github: build and publish to github, cloudflare: build and publish to cloudflare'
+ new_version:
+ required: true
+ type: string
+ default: ''
+ cloudflare_r2_path:
+ required: false
+ type: string
+ default: '/latest/'
+ secrets:
+ CLOUDFLARE_R2_BUCKET_NAME:
+ required: false
+ CLOUDFLARE_R2_ACCESS_KEY_ID:
+ required: false
+ CLOUDFLARE_R2_SECRET_ACCESS_KEY:
+ required: false
+ CLOUDFLARE_ACCOUNT_ID:
+ required: false
+ CODE_SIGN_P12_BASE64:
+ required: false
+ CODE_SIGN_P12_PASSWORD:
+ required: false
+ APPLE_ID:
+ required: false
+ APPLE_APP_SPECIFIC_PASSWORD:
+ required: false
+ DEVELOPER_ID:
+ required: false
+
+jobs:
+ build-macos:
+ runs-on: macos-silicon
+ environment: production
+ permissions:
+ contents: write
+ steps:
+ - name: Getting the repo
+ uses: actions/checkout@v3
+ with:
+ ref: ${{ inputs.ref }}
+
+ - name: Installing node
+ uses: actions/setup-node@v1
+ with:
+ node-version: 20
+ - name: Unblock keychain
+ run: |
+ security unlock-keychain -p ${{ secrets.KEYCHAIN_PASSWORD }} ~/Library/Keychains/login.keychain-db
+ # - uses: actions/setup-python@v5
+ # with:
+ # python-version: '3.11'
+
+ # - name: Install jq
+ # uses: dcarbone/install-jq-action@v2.0.1
+
+ - name: Update app version based on latest release tag with build number
+ if: inputs.public_provider != 'github'
+ run: |
+ echo "Version: ${{ inputs.new_version }}"
+ # Update the version in electron/package.json
+ jq --arg version "${{ inputs.new_version }}" '.version = $version' electron/package.json > /tmp/package.json
+ mv /tmp/package.json electron/package.json
+
+ jq --arg version "${{ inputs.new_version }}" '.version = $version' web/package.json > /tmp/package.json
+ mv /tmp/package.json web/package.json
+
+ jq '.build.publish = [{"provider": "generic", "url": "${{ secrets.CLOUDFLARE_R2_PUBLIC_URL }}", "channel": "latest"}, {"provider": "s3", "bucket": "${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }}", "region": "auto", "endpoint": "https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com", "path": "${{ inputs.cloudflare_r2_path }}", "channel": "latest"}]' electron/package.json > /tmp/package.json
+ mv /tmp/package.json electron/package.json
+ cat electron/package.json
+
+ - name: Update app version base on tag
+ if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github'
+ run: |
+ if [[ ! "${VERSION_TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
+ echo "Error: Tag is not valid!"
+ exit 1
+ fi
+ jq --arg version "${VERSION_TAG#v}" '.version = $version' electron/package.json > /tmp/package.json
+ mv /tmp/package.json electron/package.json
+ jq --arg version "${VERSION_TAG#v}" '.version = $version' web/package.json > /tmp/package.json
+ mv /tmp/package.json web/package.json
+ env:
+ VERSION_TAG: ${{ inputs.new_version }}
+
+ # - name: Get Cer for code signing
+ # run: base64 -d <<< "$CODE_SIGN_P12_BASE64" > /tmp/codesign.p12
+ # shell: bash
+ # env:
+ # CODE_SIGN_P12_BASE64: ${{ secrets.CODE_SIGN_P12_BASE64 }}
+
+ # - uses: apple-actions/import-codesign-certs@v2
+ # continue-on-error: true
+ # with:
+ # p12-file-base64: ${{ secrets.CODE_SIGN_P12_BASE64 }}
+ # p12-password: ${{ secrets.CODE_SIGN_P12_PASSWORD }}
+
+ - name: Build and publish app to cloudflare r2 or github artifactory
+ if: inputs.public_provider != 'github'
+ run: |
+ # check public_provider is true or not
+ echo "public_provider is ${{ inputs.public_provider }}"
+ if [ "${{ inputs.public_provider }}" == "none" ]; then
+ make build
+ else
+ make build-and-publish
+ fi
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ # CSC_LINK: "/tmp/codesign.p12"
+ # CSC_KEY_PASSWORD: ${{ secrets.CODE_SIGN_P12_PASSWORD }}
+ # CSC_IDENTITY_AUTO_DISCOVERY: "true"
+ APPLE_ID: ${{ secrets.APPLE_ID }}
+ APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
+ APP_PATH: "."
+ DEVELOPER_ID: ${{ secrets.DEVELOPER_ID }}
+ AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }}
+ AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }}
+ AWS_DEFAULT_REGION: auto
+ AWS_EC2_METADATA_DISABLED: "true"
+
+ - name: Build and publish app to github
+ if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github'
+ run: |
+ make build-and-publish
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ # CSC_LINK: "/tmp/codesign.p12"
+ # CSC_KEY_PASSWORD: ${{ secrets.CODE_SIGN_P12_PASSWORD }}
+ # CSC_IDENTITY_AUTO_DISCOVERY: "true"
+ APPLE_ID: ${{ secrets.APPLE_ID }}
+ APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
+ APP_PATH: "."
+ DEVELOPER_ID: ${{ secrets.DEVELOPER_ID }}
+ ANALYTICS_ID: ${{ secrets.JAN_APP_UMAMI_PROJECT_API_KEY }}
+ ANALYTICS_HOST: ${{ secrets.JAN_APP_UMAMI_URL }}
+
+ - name: Upload Artifact
+ if: inputs.public_provider != 'github'
+ uses: actions/upload-artifact@v2
+ with:
+ name: jan-mac-arm64-${{ inputs.new_version }}
+ path: ./electron/dist/jan-mac-arm64-${{ inputs.new_version }}.dmg
+
+ - name: Upload Artifact
+ uses: actions/upload-artifact@v2
+ with:
+ name: latest-mac-arm64
+ path: ./electron/dist/latest-mac.yml
\ No newline at end of file
diff --git a/.github/workflows/template-build-macos.yml b/.github/workflows/template-build-macos-x64.yml
similarity index 97%
rename from .github/workflows/template-build-macos.yml
rename to .github/workflows/template-build-macos-x64.yml
index 0ad1d3a6a..e313c2947 100644
--- a/.github/workflows/template-build-macos.yml
+++ b/.github/workflows/template-build-macos-x64.yml
@@ -148,9 +148,8 @@ jobs:
path: ./electron/dist/jan-mac-x64-${{ inputs.new_version }}.dmg
- name: Upload Artifact
- if: inputs.public_provider != 'github'
uses: actions/upload-artifact@v2
with:
- name: jan-mac-arm64-${{ inputs.new_version }}
- path: ./electron/dist/jan-mac-arm64-${{ inputs.new_version }}.dmg
+ name: latest-mac-x64
+ path: ./electron/dist/latest-mac.yml
diff --git a/Dockerfile b/Dockerfile
index 949a92673..82c657604 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,39 +1,58 @@
-FROM node:20-bullseye AS base
+FROM node:20-bookworm AS base
# 1. Install dependencies only when needed
-FROM base AS deps
+FROM base AS builder
+
+# Install g++ 11
+RUN apt update && apt install -y gcc-11 g++-11 cpp-11 jq xsel && rm -rf /var/lib/apt/lists/*
+
WORKDIR /app
# Install dependencies based on the preferred package manager
-COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
-RUN yarn install
+COPY . ./
+
+RUN export NITRO_VERSION=$(cat extensions/inference-nitro-extension/bin/version.txt) && \
+ jq --arg nitroVersion $NITRO_VERSION '(.scripts."downloadnitro:linux" | gsub("\\${NITRO_VERSION}"; $nitroVersion)) | gsub("\r"; "")' extensions/inference-nitro-extension/package.json > /tmp/newcommand.txt && export NEW_COMMAND=$(sed 's/^"//;s/"$//' /tmp/newcommand.txt) && jq --arg newCommand "$NEW_COMMAND" '.scripts."downloadnitro:linux" = $newCommand' extensions/inference-nitro-extension/package.json > /tmp/package.json && mv /tmp/package.json extensions/inference-nitro-extension/package.json
+RUN make install-and-build
+RUN yarn workspace jan-web install
+
+RUN export NODE_ENV=production && yarn workspace jan-web build
# # 2. Rebuild the source code only when needed
-FROM base AS builder
-WORKDIR /app
-COPY --from=deps /app/node_modules ./node_modules
-COPY . .
-# This will do the trick, use the corresponding env file for each environment.
-RUN yarn workspace server install
-RUN yarn server:prod
-
-# 3. Production image, copy all the files and run next
FROM base AS runner
+
+# Install g++ 11
+RUN apt update && apt install -y gcc-11 g++-11 cpp-11 jq xsel && rm -rf /var/lib/apt/lists/*
+
WORKDIR /app
-ENV NODE_ENV=production
+# Copy the package.json and yarn.lock of root yarn space to leverage Docker cache
+COPY --from=builder /app/package.json ./package.json
+COPY --from=builder /app/node_modules ./node_modules/
+COPY --from=builder /app/yarn.lock ./yarn.lock
-# RUN addgroup -g 1001 -S nodejs;
-COPY --from=builder /app/server/build ./
+# Copy the package.json, yarn.lock, and build output of server yarn space to leverage Docker cache
+COPY --from=builder /app/server ./server/
+COPY --from=builder /app/docs/openapi ./docs/openapi/
-# Automatically leverage output traces to reduce image size
-# https://nextjs.org/docs/advanced-features/output-file-tracing
-COPY --from=builder /app/server/node_modules ./node_modules
-COPY --from=builder /app/server/package.json ./package.json
+# Copy pre-install dependencies
+COPY --from=builder /app/pre-install ./pre-install/
-EXPOSE 4000 3928
+# Copy the package.json, yarn.lock, and output of web yarn space to leverage Docker cache
+COPY --from=builder /app/web/out ./web/out/
+COPY --from=builder /app/web/.next ./web/.next/
+COPY --from=builder /app/web/package.json ./web/package.json
+COPY --from=builder /app/web/yarn.lock ./web/yarn.lock
+COPY --from=builder /app/models ./models/
-ENV PORT 4000
-ENV APPDATA /app/data
+RUN npm install -g serve@latest
-CMD ["node", "main.js"]
\ No newline at end of file
+EXPOSE 1337 3000 3928
+
+ENV JAN_API_HOST 0.0.0.0
+ENV JAN_API_PORT 1337
+
+CMD ["sh", "-c", "cd server && node build/main.js & cd web && npx serve out"]
+
+# docker build -t jan .
+# docker run -p 1337:1337 -p 3000:3000 -p 3928:3928 jan
diff --git a/Dockerfile.gpu b/Dockerfile.gpu
new file mode 100644
index 000000000..f67990afd
--- /dev/null
+++ b/Dockerfile.gpu
@@ -0,0 +1,85 @@
+# Please change the base image to the appropriate CUDA version base on NVIDIA Driver Compatibility
+# Run nvidia-smi to check the CUDA version and the corresponding driver version
+# Then update the base image to the appropriate CUDA version refer https://catalog.ngc.nvidia.com/orgs/nvidia/containers/cuda/tags
+
+FROM nvidia/cuda:12.2.0-runtime-ubuntu22.04 AS base
+
+# 1. Install dependencies only when needed
+FROM base AS builder
+
+# Install g++ 11
+RUN apt update && apt install -y gcc-11 g++-11 cpp-11 jq xsel curl gnupg make python3-dev && curl -sL https://deb.nodesource.com/setup_20.x | bash - && apt install nodejs -y && rm -rf /var/lib/apt/lists/*
+
+# Update alternatives for GCC and related tools
+RUN update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-11 110 \
+ --slave /usr/bin/g++ g++ /usr/bin/g++-11 \
+ --slave /usr/bin/gcov gcov /usr/bin/gcov-11 \
+ --slave /usr/bin/gcc-ar gcc-ar /usr/bin/gcc-ar-11 \
+ --slave /usr/bin/gcc-ranlib gcc-ranlib /usr/bin/gcc-ranlib-11 && \
+ update-alternatives --install /usr/bin/cpp cpp /usr/bin/cpp-11 110
+
+RUN npm install -g yarn
+
+WORKDIR /app
+
+# Install dependencies based on the preferred package manager
+COPY . ./
+
+RUN export NITRO_VERSION=$(cat extensions/inference-nitro-extension/bin/version.txt) && \
+ jq --arg nitroVersion $NITRO_VERSION '(.scripts."downloadnitro:linux" | gsub("\\${NITRO_VERSION}"; $nitroVersion)) | gsub("\r"; "")' extensions/inference-nitro-extension/package.json > /tmp/newcommand.txt && export NEW_COMMAND=$(sed 's/^"//;s/"$//' /tmp/newcommand.txt) && jq --arg newCommand "$NEW_COMMAND" '.scripts."downloadnitro:linux" = $newCommand' extensions/inference-nitro-extension/package.json > /tmp/package.json && mv /tmp/package.json extensions/inference-nitro-extension/package.json
+RUN make install-and-build
+RUN yarn workspace jan-web install
+
+RUN export NODE_ENV=production && yarn workspace jan-web build
+
+# # 2. Rebuild the source code only when needed
+FROM base AS runner
+
+# Install g++ 11
+RUN apt update && apt install -y gcc-11 g++-11 cpp-11 jq xsel curl gnupg make python3-dev && curl -sL https://deb.nodesource.com/setup_20.x | bash - && apt-get install nodejs -y && rm -rf /var/lib/apt/lists/*
+
+# Update alternatives for GCC and related tools
+RUN update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-11 110 \
+ --slave /usr/bin/g++ g++ /usr/bin/g++-11 \
+ --slave /usr/bin/gcov gcov /usr/bin/gcov-11 \
+ --slave /usr/bin/gcc-ar gcc-ar /usr/bin/gcc-ar-11 \
+ --slave /usr/bin/gcc-ranlib gcc-ranlib /usr/bin/gcc-ranlib-11 && \
+ update-alternatives --install /usr/bin/cpp cpp /usr/bin/cpp-11 110
+
+RUN npm install -g yarn
+
+WORKDIR /app
+
+# Copy the package.json and yarn.lock of root yarn space to leverage Docker cache
+COPY --from=builder /app/package.json ./package.json
+COPY --from=builder /app/node_modules ./node_modules/
+COPY --from=builder /app/yarn.lock ./yarn.lock
+
+# Copy the package.json, yarn.lock, and build output of server yarn space to leverage Docker cache
+COPY --from=builder /app/server ./server/
+COPY --from=builder /app/docs/openapi ./docs/openapi/
+
+# Copy pre-install dependencies
+COPY --from=builder /app/pre-install ./pre-install/
+
+# Copy the package.json, yarn.lock, and output of web yarn space to leverage Docker cache
+COPY --from=builder /app/web/out ./web/out/
+COPY --from=builder /app/web/.next ./web/.next/
+COPY --from=builder /app/web/package.json ./web/package.json
+COPY --from=builder /app/web/yarn.lock ./web/yarn.lock
+COPY --from=builder /app/models ./models/
+
+RUN npm install -g serve@latest
+
+EXPOSE 1337 3000 3928
+
+ENV LD_LIBRARY_PATH=/usr/local/cuda/targets/x86_64-linux/lib:/usr/local/cuda-12.0/compat${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}
+
+ENV JAN_API_HOST 0.0.0.0
+ENV JAN_API_PORT 1337
+
+CMD ["sh", "-c", "cd server && node build/main.js & cd web && npx serve out"]
+
+# pre-requisites: nvidia-docker
+# docker build -t jan-gpu . -f Dockerfile.gpu
+# docker run -p 1337:1337 -p 3000:3000 -p 3928:3928 --gpus all jan-gpu
diff --git a/Makefile b/Makefile
index 905a68321..ffb1abee2 100644
--- a/Makefile
+++ b/Makefile
@@ -24,9 +24,9 @@ endif
check-file-counts: install-and-build
ifeq ($(OS),Windows_NT)
- powershell -Command "if ((Get-ChildItem -Path electron/pre-install -Filter *.tgz | Measure-Object | Select-Object -ExpandProperty Count) -ne (Get-ChildItem -Path extensions -Directory | Measure-Object | Select-Object -ExpandProperty Count)) { Write-Host 'Number of .tgz files in electron/pre-install does not match the number of subdirectories in extension'; exit 1 } else { Write-Host 'Extension build successful' }"
+ powershell -Command "if ((Get-ChildItem -Path pre-install -Filter *.tgz | Measure-Object | Select-Object -ExpandProperty Count) -ne (Get-ChildItem -Path extensions -Directory | Measure-Object | Select-Object -ExpandProperty Count)) { Write-Host 'Number of .tgz files in pre-install does not match the number of subdirectories in extension'; exit 1 } else { Write-Host 'Extension build successful' }"
else
- @tgz_count=$$(find electron/pre-install -type f -name "*.tgz" | wc -l); dir_count=$$(find extensions -mindepth 1 -maxdepth 1 -type d | wc -l); if [ $$tgz_count -ne $$dir_count ]; then echo "Number of .tgz files in electron/pre-install ($$tgz_count) does not match the number of subdirectories in extension ($$dir_count)"; exit 1; else echo "Extension build successful"; fi
+ @tgz_count=$$(find pre-install -type f -name "*.tgz" | wc -l); dir_count=$$(find extensions -mindepth 1 -maxdepth 1 -type d | wc -l); if [ $$tgz_count -ne $$dir_count ]; then echo "Number of .tgz files in pre-install ($$tgz_count) does not match the number of subdirectories in extension ($$dir_count)"; exit 1; else echo "Extension build successful"; fi
endif
dev: check-file-counts
diff --git a/README.md b/README.md
index 02a5aa6e9..5b5263ed1 100644
--- a/README.md
+++ b/README.md
@@ -43,31 +43,31 @@ Jan is an open-source ChatGPT alternative that runs 100% offline on your compute
| Experimental (Nightly Build) |
-
+
jan.exe
|
-
+
Intel
|
-
+
M1/M2
|
-
+
jan.deb
|
-
+
jan.AppImage
@@ -218,6 +218,76 @@ make build
This will build the app MacOS m1/m2 for production (with code signing already done) and put the result in `dist` folder.
+### Docker mode
+
+- Supported OS: Linux, WSL2 Docker
+- Pre-requisites:
+ - `docker` and `docker compose`, follow instruction [here](https://docs.docker.com/engine/install/ubuntu/)
+
+ ```bash
+ curl -fsSL https://get.docker.com -o get-docker.sh
+ sudo sh ./get-docker.sh --dry-run
+ ```
+
+ - `nvidia-driver` and `nvidia-docker2`, follow instruction [here](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) (If you want to run with GPU mode)
+
+- Run Jan in Docker mode
+
+ - **Option 1**: Run Jan in CPU mode
+
+ ```bash
+ docker compose --profile cpu up -d
+ ```
+
+ - **Option 2**: Run Jan in GPU mode
+
+ - **Step 1**: Check cuda compatibility with your nvidia driver by running `nvidia-smi` and check the cuda version in the output
+
+ ```bash
+ nvidia-smi
+
+ # Output
+ +---------------------------------------------------------------------------------------+
+ | NVIDIA-SMI 531.18 Driver Version: 531.18 CUDA Version: 12.1 |
+ |-----------------------------------------+----------------------+----------------------+
+ | GPU Name TCC/WDDM | Bus-Id Disp.A | Volatile Uncorr. ECC |
+ | Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
+ | | | MIG M. |
+ |=========================================+======================+======================|
+ | 0 NVIDIA GeForce RTX 4070 Ti WDDM | 00000000:01:00.0 On | N/A |
+ | 0% 44C P8 16W / 285W| 1481MiB / 12282MiB | 2% Default |
+ | | | N/A |
+ +-----------------------------------------+----------------------+----------------------+
+ | 1 NVIDIA GeForce GTX 1660 Ti WDDM | 00000000:02:00.0 Off | N/A |
+ | 0% 49C P8 14W / 120W| 0MiB / 6144MiB | 0% Default |
+ | | | N/A |
+ +-----------------------------------------+----------------------+----------------------+
+ | 2 NVIDIA GeForce GTX 1660 Ti WDDM | 00000000:05:00.0 Off | N/A |
+ | 29% 38C P8 11W / 120W| 0MiB / 6144MiB | 0% Default |
+ | | | N/A |
+ +-----------------------------------------+----------------------+----------------------+
+
+ +---------------------------------------------------------------------------------------+
+ | Processes: |
+ | GPU GI CI PID Type Process name GPU Memory |
+ | ID ID Usage |
+ |=======================================================================================|
+ ```
+
+ - **Step 2**: Go to https://catalog.ngc.nvidia.com/orgs/nvidia/containers/cuda/tags and find the smallest minor version of image tag that matches the cuda version from the output of `nvidia-smi` (e.g. 12.1 -> 12.1.0)
+
+ - **Step 3**: Update the `Dockerfile.gpu` line number 5 with the latest minor version of the image tag from step 2 (e.g. change `FROM nvidia/cuda:12.2.0-runtime-ubuntu22.04 AS base` to `FROM nvidia/cuda:12.1.0-runtime-ubuntu22.04 AS base`)
+
+ - **Step 4**: Run command to start Jan in GPU mode
+
+ ```bash
+ # GPU mode
+ docker compose --profile gpu up -d
+ ```
+
+ This will start the web server and you can access Jan at `http://localhost:3000`.
+ > Note: Currently, Docker mode is only work for development and localhost, production is not supported yet. RAG feature is not supported in Docker mode yet.
+
## Acknowledgements
Jan builds on top of other open-source projects:
diff --git a/core/package.json b/core/package.json
index 437e6d0a6..c3abe2d56 100644
--- a/core/package.json
+++ b/core/package.json
@@ -57,6 +57,7 @@
"rollup-plugin-typescript2": "^0.36.0",
"ts-jest": "^26.1.1",
"tslib": "^2.6.2",
- "typescript": "^5.2.2"
+ "typescript": "^5.2.2",
+ "rimraf": "^3.0.2"
}
}
diff --git a/core/src/api/index.ts b/core/src/api/index.ts
index 0d7cc51f7..f4ec3cd7e 100644
--- a/core/src/api/index.ts
+++ b/core/src/api/index.ts
@@ -30,6 +30,7 @@ export enum DownloadRoute {
downloadFile = 'downloadFile',
pauseDownload = 'pauseDownload',
resumeDownload = 'resumeDownload',
+ getDownloadProgress = 'getDownloadProgress',
}
export enum DownloadEvent {
diff --git a/core/src/node/api/routes/common.ts b/core/src/node/api/routes/common.ts
index 27385e561..8887755fe 100644
--- a/core/src/node/api/routes/common.ts
+++ b/core/src/node/api/routes/common.ts
@@ -12,6 +12,8 @@ import {
import { JanApiRouteConfiguration } from '../common/configuration'
import { startModel, stopModel } from '../common/startStopModel'
import { ModelSettingParams } from '../../../types'
+import { getJanDataFolderPath } from '../../utils'
+import { normalizeFilePath } from '../../path'
export const commonRouter = async (app: HttpServer) => {
// Common Routes
@@ -52,7 +54,14 @@ export const commonRouter = async (app: HttpServer) => {
// App Routes
app.post(`/app/${AppRoute.joinPath}`, async (request: any, reply: any) => {
const args = JSON.parse(request.body) as any[]
- reply.send(JSON.stringify(join(...args[0])))
+
+ const paths = args[0].map((arg: string) =>
+ typeof arg === 'string' && (arg.startsWith(`file:/`) || arg.startsWith(`file:\\`))
+ ? join(getJanDataFolderPath(), normalizeFilePath(arg))
+ : arg
+ )
+
+ reply.send(JSON.stringify(join(...paths)))
})
app.post(`/app/${AppRoute.baseName}`, async (request: any, reply: any) => {
diff --git a/core/src/node/api/routes/download.ts b/core/src/node/api/routes/download.ts
index b4e11f957..7fb05daee 100644
--- a/core/src/node/api/routes/download.ts
+++ b/core/src/node/api/routes/download.ts
@@ -4,55 +4,109 @@ import { DownloadManager } from '../../download'
import { HttpServer } from '../HttpServer'
import { createWriteStream } from 'fs'
import { getJanDataFolderPath } from '../../utils'
-import { normalizeFilePath } from "../../path";
+import { normalizeFilePath } from '../../path'
+import { DownloadState } from '../../../types'
export const downloadRouter = async (app: HttpServer) => {
+ app.get(`/${DownloadRoute.getDownloadProgress}/:modelId`, async (req, res) => {
+ const modelId = req.params.modelId
+
+ console.debug(`Getting download progress for model ${modelId}`)
+ console.debug(
+ `All Download progress: ${JSON.stringify(DownloadManager.instance.downloadProgressMap)}`
+ )
+
+ // check if null DownloadManager.instance.downloadProgressMap
+ if (!DownloadManager.instance.downloadProgressMap[modelId]) {
+ return res.status(404).send({
+ message: 'Download progress not found',
+ })
+ } else {
+ return res.status(200).send(DownloadManager.instance.downloadProgressMap[modelId])
+ }
+ })
+
app.post(`/${DownloadRoute.downloadFile}`, async (req, res) => {
- const strictSSL = !(req.query.ignoreSSL === "true");
- const proxy = req.query.proxy?.startsWith("http") ? req.query.proxy : undefined;
- const body = JSON.parse(req.body as any);
+ const strictSSL = !(req.query.ignoreSSL === 'true')
+ const proxy = req.query.proxy?.startsWith('http') ? req.query.proxy : undefined
+ const body = JSON.parse(req.body as any)
const normalizedArgs = body.map((arg: any) => {
- if (typeof arg === "string") {
- return join(getJanDataFolderPath(), normalizeFilePath(arg));
+ if (typeof arg === 'string' && arg.startsWith('file:')) {
+ return join(getJanDataFolderPath(), normalizeFilePath(arg))
}
- return arg;
- });
+ return arg
+ })
- const localPath = normalizedArgs[1];
- const fileName = localPath.split("/").pop() ?? "";
+ const localPath = normalizedArgs[1]
+ const array = localPath.split('/')
+ const fileName = array.pop() ?? ''
+ const modelId = array.pop() ?? ''
+ console.debug('downloadFile', normalizedArgs, fileName, modelId)
- const request = require("request");
- const progress = require("request-progress");
+ const request = require('request')
+ const progress = require('request-progress')
- const rq = request({ url: normalizedArgs[0], strictSSL, proxy });
+ const rq = request({ url: normalizedArgs[0], strictSSL, proxy })
progress(rq, {})
- .on("progress", function (state: any) {
- console.log("download onProgress", state);
+ .on('progress', function (state: any) {
+ const downloadProps: DownloadState = {
+ ...state,
+ modelId,
+ fileName,
+ downloadState: 'downloading',
+ }
+ console.debug(`Download ${modelId} onProgress`, downloadProps)
+ DownloadManager.instance.downloadProgressMap[modelId] = downloadProps
})
- .on("error", function (err: Error) {
- console.log("download onError", err);
- })
- .on("end", function () {
- console.log("download onEnd");
- })
- .pipe(createWriteStream(normalizedArgs[1]));
+ .on('error', function (err: Error) {
+ console.debug(`Download ${modelId} onError`, err.message)
- DownloadManager.instance.setRequest(fileName, rq);
- });
+ const currentDownloadState = DownloadManager.instance.downloadProgressMap[modelId]
+ if (currentDownloadState) {
+ DownloadManager.instance.downloadProgressMap[modelId] = {
+ ...currentDownloadState,
+ downloadState: 'error',
+ }
+ }
+ })
+ .on('end', function () {
+ console.debug(`Download ${modelId} onEnd`)
+
+ const currentDownloadState = DownloadManager.instance.downloadProgressMap[modelId]
+ if (currentDownloadState) {
+ if (currentDownloadState.downloadState === 'downloading') {
+ // if the previous state is downloading, then set the state to end (success)
+ DownloadManager.instance.downloadProgressMap[modelId] = {
+ ...currentDownloadState,
+ downloadState: 'end',
+ }
+ }
+ }
+ })
+ .pipe(createWriteStream(normalizedArgs[1]))
+
+ DownloadManager.instance.setRequest(localPath, rq)
+ res.status(200).send({ message: 'Download started' })
+ })
app.post(`/${DownloadRoute.abortDownload}`, async (req, res) => {
- const body = JSON.parse(req.body as any);
+ const body = JSON.parse(req.body as any)
const normalizedArgs = body.map((arg: any) => {
- if (typeof arg === "string") {
- return join(getJanDataFolderPath(), normalizeFilePath(arg));
+ if (typeof arg === 'string' && arg.startsWith('file:')) {
+ return join(getJanDataFolderPath(), normalizeFilePath(arg))
}
- return arg;
- });
+ return arg
+ })
- const localPath = normalizedArgs[0];
- const fileName = localPath.split("/").pop() ?? "";
- const rq = DownloadManager.instance.networkRequests[fileName];
- DownloadManager.instance.networkRequests[fileName] = undefined;
- rq?.abort();
- });
-};
+ const localPath = normalizedArgs[0]
+ const fileName = localPath.split('/').pop() ?? ''
+ const rq = DownloadManager.instance.networkRequests[fileName]
+ DownloadManager.instance.networkRequests[fileName] = undefined
+ rq?.abort()
+ if (rq) {
+ res.status(200).send({ message: 'Download aborted' })
+ } else {
+ res.status(404).send({ message: 'Download not found' })
+ }
+ })
+}
diff --git a/core/src/node/api/routes/fileManager.ts b/core/src/node/api/routes/fileManager.ts
index 66056444e..b4c73dda1 100644
--- a/core/src/node/api/routes/fileManager.ts
+++ b/core/src/node/api/routes/fileManager.ts
@@ -1,14 +1,29 @@
import { FileManagerRoute } from '../../../api'
import { HttpServer } from '../../index'
+import { join } from 'path'
-export const fsRouter = async (app: HttpServer) => {
- app.post(`/app/${FileManagerRoute.syncFile}`, async (request: any, reply: any) => {})
+export const fileManagerRouter = async (app: HttpServer) => {
+ app.post(`/fs/${FileManagerRoute.syncFile}`, async (request: any, reply: any) => {
+ const reflect = require('@alumna/reflect')
+ const args = JSON.parse(request.body)
+ return reflect({
+ src: args[0],
+ dest: args[1],
+ recursive: true,
+ delete: false,
+ overwrite: true,
+ errorOnExist: false,
+ })
+ })
- app.post(`/app/${FileManagerRoute.getJanDataFolderPath}`, async (request: any, reply: any) => {})
+ app.post(`/fs/${FileManagerRoute.getJanDataFolderPath}`, async (request: any, reply: any) =>
+ global.core.appPath()
+ )
- app.post(`/app/${FileManagerRoute.getResourcePath}`, async (request: any, reply: any) => {})
+ app.post(`/fs/${FileManagerRoute.getResourcePath}`, async (request: any, reply: any) =>
+ join(global.core.appPath(), '../../..')
+ )
app.post(`/app/${FileManagerRoute.getUserHomePath}`, async (request: any, reply: any) => {})
-
- app.post(`/app/${FileManagerRoute.fileStat}`, async (request: any, reply: any) => {})
+ app.post(`/fs/${FileManagerRoute.fileStat}`, async (request: any, reply: any) => {})
}
diff --git a/core/src/node/api/routes/fs.ts b/core/src/node/api/routes/fs.ts
index c5404ccce..9535418a0 100644
--- a/core/src/node/api/routes/fs.ts
+++ b/core/src/node/api/routes/fs.ts
@@ -1,8 +1,9 @@
-import { FileSystemRoute } from '../../../api'
+import { FileManagerRoute, FileSystemRoute } from '../../../api'
import { join } from 'path'
import { HttpServer } from '../HttpServer'
import { getJanDataFolderPath } from '../../utils'
import { normalizeFilePath } from '../../path'
+import { writeFileSync } from 'fs'
export const fsRouter = async (app: HttpServer) => {
const moduleName = 'fs'
@@ -26,4 +27,14 @@ export const fsRouter = async (app: HttpServer) => {
}
})
})
+ app.post(`/${FileManagerRoute.writeBlob}`, async (request: any, reply: any) => {
+ try {
+ const args = JSON.parse(request.body) as any[]
+ console.log('writeBlob:', args[0])
+ const dataBuffer = Buffer.from(args[1], 'base64')
+ writeFileSync(args[0], dataBuffer)
+ } catch (err) {
+ console.error(`writeFile ${request.body} result: ${err}`)
+ }
+ })
}
diff --git a/core/src/node/api/routes/v1.ts b/core/src/node/api/routes/v1.ts
index a2a48cd8b..301c41ac0 100644
--- a/core/src/node/api/routes/v1.ts
+++ b/core/src/node/api/routes/v1.ts
@@ -4,6 +4,7 @@ import { threadRouter } from './thread'
import { fsRouter } from './fs'
import { extensionRouter } from './extension'
import { downloadRouter } from './download'
+import { fileManagerRouter } from './fileManager'
export const v1Router = async (app: HttpServer) => {
// MARK: External Routes
@@ -16,6 +17,8 @@ export const v1Router = async (app: HttpServer) => {
app.register(fsRouter, {
prefix: '/fs',
})
+ app.register(fileManagerRouter)
+
app.register(extensionRouter, {
prefix: '/extension',
})
diff --git a/core/src/node/download.ts b/core/src/node/download.ts
index 6d15fc344..b3f284440 100644
--- a/core/src/node/download.ts
+++ b/core/src/node/download.ts
@@ -1,15 +1,18 @@
+import { DownloadState } from '../types'
/**
* Manages file downloads and network requests.
*/
export class DownloadManager {
- public networkRequests: Record = {};
+ public networkRequests: Record = {}
- public static instance: DownloadManager = new DownloadManager();
+ public static instance: DownloadManager = new DownloadManager()
+
+ public downloadProgressMap: Record = {}
constructor() {
if (DownloadManager.instance) {
- return DownloadManager.instance;
+ return DownloadManager.instance
}
}
/**
@@ -18,6 +21,6 @@ export class DownloadManager {
* @param {Request | undefined} request - The network request to set, or undefined to clear the request.
*/
setRequest(fileName: string, request: any | undefined) {
- this.networkRequests[fileName] = request;
+ this.networkRequests[fileName] = request
}
}
diff --git a/core/src/node/extension/index.ts b/core/src/node/extension/index.ts
index ed8544773..994fc97f2 100644
--- a/core/src/node/extension/index.ts
+++ b/core/src/node/extension/index.ts
@@ -41,8 +41,8 @@ async function registerExtensionProtocol() {
console.error('Electron is not available')
}
const extensionPath = ExtensionManager.instance.getExtensionsPath()
- if (electron) {
- return electron.protocol.registerFileProtocol('extension', (request: any, callback: any) => {
+ if (electron && electron.protocol) {
+ return electron.protocol?.registerFileProtocol('extension', (request: any, callback: any) => {
const entry = request.url.substr('extension://'.length - 1)
const url = normalize(extensionPath + entry)
@@ -69,7 +69,7 @@ export function useExtensions(extensionsPath: string) {
// Read extension list from extensions folder
const extensions = JSON.parse(
- readFileSync(ExtensionManager.instance.getExtensionsFile(), 'utf-8'),
+ readFileSync(ExtensionManager.instance.getExtensionsFile(), 'utf-8')
)
try {
// Create and store a Extension instance for each extension in list
@@ -82,7 +82,7 @@ export function useExtensions(extensionsPath: string) {
throw new Error(
'Could not successfully rebuild list of installed extensions.\n' +
error +
- '\nPlease check the extensions.json file in the extensions folder.',
+ '\nPlease check the extensions.json file in the extensions folder.'
)
}
@@ -122,7 +122,7 @@ function loadExtension(ext: any) {
export function getStore() {
if (!ExtensionManager.instance.getExtensionsFile()) {
throw new Error(
- 'The extension path has not yet been set up. Please run useExtensions before accessing the store',
+ 'The extension path has not yet been set up. Please run useExtensions before accessing the store'
)
}
diff --git a/core/src/types/assistant/assistantEvent.ts b/core/src/types/assistant/assistantEvent.ts
new file mode 100644
index 000000000..f8f3e6ad0
--- /dev/null
+++ b/core/src/types/assistant/assistantEvent.ts
@@ -0,0 +1,8 @@
+/**
+ * The `EventName` enumeration contains the names of all the available events in the Jan platform.
+ */
+export enum AssistantEvent {
+ /** The `OnAssistantsUpdate` event is emitted when the assistant list is updated. */
+ OnAssistantsUpdate = 'OnAssistantsUpdate',
+ }
+
\ No newline at end of file
diff --git a/core/src/types/assistant/index.ts b/core/src/types/assistant/index.ts
index 83ea73f85..e18589551 100644
--- a/core/src/types/assistant/index.ts
+++ b/core/src/types/assistant/index.ts
@@ -1,2 +1,3 @@
export * from './assistantEntity'
+export * from './assistantEvent'
export * from './assistantInterface'
diff --git a/core/src/types/file/index.ts b/core/src/types/file/index.ts
index 6526cfc6d..57d687d2f 100644
--- a/core/src/types/file/index.ts
+++ b/core/src/types/file/index.ts
@@ -2,3 +2,26 @@ export type FileStat = {
isDirectory: boolean
size: number
}
+
+export type DownloadState = {
+ modelId: string
+ filename: string
+ time: DownloadTime
+ speed: number
+ percent: number
+
+ size: DownloadSize
+ children?: DownloadState[]
+ error?: string
+ downloadState: 'downloading' | 'error' | 'end'
+}
+
+type DownloadTime = {
+ elapsed: number
+ remaining: number
+}
+
+type DownloadSize = {
+ total: number
+ transferred: number
+}
diff --git a/core/src/types/model/modelEvent.ts b/core/src/types/model/modelEvent.ts
index 978a48724..443f3a34f 100644
--- a/core/src/types/model/modelEvent.ts
+++ b/core/src/types/model/modelEvent.ts
@@ -12,4 +12,6 @@ export enum ModelEvent {
OnModelStop = 'OnModelStop',
/** The `OnModelStopped` event is emitted when a model stopped ok. */
OnModelStopped = 'OnModelStopped',
+ /** The `OnModelUpdate` event is emitted when the model list is updated. */
+ OnModelsUpdate = 'OnModelsUpdate',
}
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 000000000..4195a3294
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,117 @@
+# Docker Compose file for setting up Minio, createbuckets, app_cpu, and app_gpu services
+
+version: '3.7'
+
+services:
+ # Minio service for object storage
+ minio:
+ image: minio/minio
+ volumes:
+ - minio_data:/data
+ ports:
+ - "9000:9000"
+ - "9001:9001"
+ environment:
+ # Set the root user and password for Minio
+ MINIO_ROOT_USER: minioadmin # This acts as AWS_ACCESS_KEY
+ MINIO_ROOT_PASSWORD: minioadmin # This acts as AWS_SECRET_ACCESS_KEY
+ command: server --console-address ":9001" /data
+ restart: always
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
+ interval: 30s
+ timeout: 20s
+ retries: 3
+ networks:
+ vpcbr:
+ ipv4_address: 10.5.0.2
+
+ # createbuckets service to create a bucket and set its policy
+ createbuckets:
+ image: minio/mc
+ depends_on:
+ - minio
+ entrypoint: >
+ /bin/sh -c "
+ /usr/bin/mc alias set myminio http://minio:9000 minioadmin minioadmin;
+ /usr/bin/mc mb myminio/mybucket;
+ /usr/bin/mc policy set public myminio/mybucket;
+ exit 0;
+ "
+ networks:
+ vpcbr:
+
+ # app_cpu service for running the CPU version of the application
+ app_cpu:
+ image: jan:latest
+ volumes:
+ - app_data:/app/server/build/jan
+ build:
+ context: .
+ dockerfile: Dockerfile
+ environment:
+ # Set the AWS access key, secret access key, bucket name, endpoint, and region for app_cpu
+ AWS_ACCESS_KEY_ID: minioadmin
+ AWS_SECRET_ACCESS_KEY: minioadmin
+ S3_BUCKET_NAME: mybucket
+ AWS_ENDPOINT: http://10.5.0.2:9000
+ AWS_REGION: us-east-1
+ restart: always
+ profiles:
+ - cpu
+ ports:
+ - "3000:3000"
+ - "1337:1337"
+ - "3928:3928"
+ networks:
+ vpcbr:
+ ipv4_address: 10.5.0.3
+
+ # app_gpu service for running the GPU version of the application
+ app_gpu:
+ deploy:
+ resources:
+ reservations:
+ devices:
+ - driver: nvidia
+ count: all
+ capabilities: [gpu]
+ image: jan-gpu:latest
+ volumes:
+ - app_data:/app/server/build/jan
+ build:
+ context: .
+ dockerfile: Dockerfile.gpu
+ restart: always
+ environment:
+ # Set the AWS access key, secret access key, bucket name, endpoint, and region for app_gpu
+ AWS_ACCESS_KEY_ID: minioadmin
+ AWS_SECRET_ACCESS_KEY: minioadmin
+ S3_BUCKET_NAME: mybucket
+ AWS_ENDPOINT: http://10.5.0.2:9000
+ AWS_REGION: us-east-1
+ profiles:
+ - gpu
+ ports:
+ - "3000:3000"
+ - "1337:1337"
+ - "3928:3928"
+ networks:
+ vpcbr:
+ ipv4_address: 10.5.0.4
+
+volumes:
+ minio_data:
+ app_data:
+
+networks:
+ vpcbr:
+ driver: bridge
+ ipam:
+ config:
+ - subnet: 10.5.0.0/16
+ gateway: 10.5.0.1
+
+# Usage:
+# - Run 'docker-compose --profile cpu up -d' to start the app_cpu service
+# - Run 'docker-compose --profile gpu up -d' to start the app_gpu service
diff --git a/docs/blog/authors.yml b/docs/blog/authors.yml
index f30d4610d..4c1dc521e 100644
--- a/docs/blog/authors.yml
+++ b/docs/blog/authors.yml
@@ -1,6 +1,63 @@
dan-jan:
name: Daniel Onggunhao
title: Co-Founder
- url: https://github.com/dan-jan
+ url: https://github.com/dan-jan
image_url: https://avatars.githubusercontent.com/u/101145494?v=4
- email: daniel@jan.ai
\ No newline at end of file
+ email: daniel@jan.ai
+
+namchuai:
+ name: Nam Nguyen
+ title: Developer
+ url: https://github.com/namchuai
+ image_url: https://avatars.githubusercontent.com/u/10397206?v=4
+ email: james@jan.ai
+
+hiro-v:
+ name: Hiro Vuong
+ title: MLE
+ url: https://github.com/hiro-v
+ image_url: https://avatars.githubusercontent.com/u/22463238?v=4
+ email: hiro@jan.ai
+
+ashley-jan:
+ name: Ashley Tran
+ title: Product Designer
+ url: https://github.com/imtuyethan
+ image_url: https://avatars.githubusercontent.com/u/89722390?v=4
+ email: ashley@jan.ai
+
+hientominh:
+ name: Hien To
+ title: DevOps Engineer
+ url: https://github.com/hientominh
+ image_url: https://avatars.githubusercontent.com/u/37921427?v=4
+ email: hien@jan.ai
+
+Van-QA:
+ name: Van Pham
+ title: QA & Release Manager
+ url: https://github.com/Van-QA
+ image_url: https://avatars.githubusercontent.com/u/64197333?v=4
+ email: van@jan.ai
+
+louis-jan:
+ name: Louis Le
+ title: Software Engineer
+ url: https://github.com/louis-jan
+ image_url: https://avatars.githubusercontent.com/u/133622055?v=4
+ email: louis@jan.ai
+
+hahuyhoang411:
+ name: Rex Ha
+ title: LLM Researcher & Content Writer
+ url: https://github.com/hahuyhoang411
+ image_url: https://avatars.githubusercontent.com/u/64120343?v=4
+ email: rex@jan.ai
+
+automaticcat:
+ name: Alan Dao
+ title: AI Engineer
+ url: https://github.com/tikikun
+ image_url: https://avatars.githubusercontent.com/u/22268502?v=4
+ email: alan@jan.ai
+
diff --git a/docs/docs/about/01-README.md b/docs/docs/about/01-README.md
index 3b2759513..d5d3b8dc2 100644
--- a/docs/docs/about/01-README.md
+++ b/docs/docs/about/01-README.md
@@ -110,9 +110,10 @@ Adhering to Jan's privacy preserving philosophy, our analytics philosophy is to
#### What is tracked
-1. By default, Github tracks downloads and device metadata for all public Github repos. This helps us troubleshoot & ensure cross platform support.
-1. We use Posthog to track a single `app.opened` event without additional user metadata, in order to understand retention.
-1. Additionally, we plan to enable a `Settings` feature for users to turn off all tracking.
+1. By default, Github tracks downloads and device metadata for all public GitHub repositories. This helps us troubleshoot & ensure cross-platform support.
+2. We use [Umami](https://umami.is/) to collect, analyze, and understand application data while maintaining visitor privacy and data ownership. We are using the Umami Cloud in Europe to ensure GDPR compliance. Please see [Umami Privacy Policy](https://umami.is/privacy) for more details.
+3. We use Umami to track a single `app.opened` event without additional user metadata, in order to understand retention. In addition, we track `app.event` to understand app version usage.
+4. Additionally, we plan to enable a `Settings` feature for users to turn off all tracking.
#### Request for help
diff --git a/docs/docs/developer/01-overview/04-install-and-prerequisites.md b/docs/docs/developer/01-overview/04-install-and-prerequisites.md
new file mode 100644
index 000000000..110f62e36
--- /dev/null
+++ b/docs/docs/developer/01-overview/04-install-and-prerequisites.md
@@ -0,0 +1,79 @@
+---
+title: Installation and Prerequisites
+slug: /developer/prereq
+description: Guide to install and setup Jan for development.
+keywords:
+ [
+ Jan AI,
+ Jan,
+ ChatGPT alternative,
+ local AI,
+ private AI,
+ conversational AI,
+ no-subscription fee,
+ large language model,
+ installation,
+ prerequisites,
+ developer setup,
+ ]
+---
+
+## Requirements
+
+### Hardware Requirements
+
+Ensure your system meets the following specifications to guarantee a smooth development experience:
+
+- [Hardware Requirements](../../guides/02-installation/06-hardware.md)
+
+### System Requirements
+
+Make sure your operating system meets the specific requirements for Jan development:
+
+- [Windows](../../install/windows/#system-requirements)
+- [MacOS](../../install/mac/#system-requirements)
+- [Linux](../../install/linux/#system-requirements)
+
+## Prerequisites
+
+- [Node.js](https://nodejs.org/en/) (version 20.0.0 or higher)
+- [yarn](https://yarnpkg.com/) (version 1.22.0 or higher)
+- [make](https://www.gnu.org/software/make/) (version 3.81 or higher)
+
+## Instructions
+
+1. **Clone the Repository:**
+
+```bash
+git clone https://github.com/janhq/jan
+cd jan
+git checkout -b DESIRED_BRANCH
+```
+
+2. **Install Dependencies**
+
+```bash
+yarn install
+```
+
+3. **Run Development and Use Jan Desktop**
+
+```bash
+make dev
+```
+
+This command starts the development server and opens the Jan Desktop app.
+
+## For Production Build
+
+```bash
+# Do steps 1 and 2 in the previous section
+# Build the app
+make build
+```
+
+This will build the app MacOS (M1/M2/M3) for production (with code signing already done) and place the result in `/electron/dist` folder.
+
+## Troubleshooting
+
+If you run into any issues due to a broken build, please check the [Stuck on a Broken Build](../../troubleshooting/stuck-on-broken-build) guide.
diff --git a/docs/docs/guides/02-installation/01-mac.md b/docs/docs/guides/02-installation/01-mac.md
index 8e67b5bed..7a3961384 100644
--- a/docs/docs/guides/02-installation/01-mac.md
+++ b/docs/docs/guides/02-installation/01-mac.md
@@ -12,11 +12,16 @@ keywords:
conversational AI,
no-subscription fee,
large language model,
+ installation guide,
]
---
# Installing Jan on MacOS
+## System Requirements
+
+Ensure that your MacOS version is 13 or higher to run Jan.
+
## Installation
Jan is available for download via our homepage, [https://jan.ai/](https://jan.ai/).
diff --git a/docs/docs/guides/02-installation/02-windows.md b/docs/docs/guides/02-installation/02-windows.md
index b200554d2..d60ab86f7 100644
--- a/docs/docs/guides/02-installation/02-windows.md
+++ b/docs/docs/guides/02-installation/02-windows.md
@@ -12,11 +12,23 @@ keywords:
conversational AI,
no-subscription fee,
large language model,
+ installation guide,
]
---
# Installing Jan on Windows
+## System Requirements
+
+Ensure that your system meets the following requirements:
+
+- Windows 10 or higher is required to run Jan.
+
+To enable GPU support, you will need:
+
+- NVIDIA GPU with CUDA Toolkit 11.7 or higher
+- NVIDIA driver 470.63.01 or higher
+
## Installation
Jan is available for download via our homepage, [https://jan.ai](https://jan.ai/).
@@ -59,13 +71,3 @@ To remove all user data associated with Jan, you can delete the `/jan` directory
cd C:\Users\%USERNAME%\AppData\Roaming
rmdir /S jan
```
-
-## Troubleshooting
-
-### Microsoft Defender
-
-**Error: "Microsoft Defender SmartScreen prevented an unrecognized app from starting"**
-
-Windows Defender may display the above warning when running the Jan Installer, as a standard security measure.
-
-To proceed, select the "More info" option and select the "Run Anyway" option to continue with the installation.
diff --git a/docs/docs/guides/02-installation/03-linux.md b/docs/docs/guides/02-installation/03-linux.md
index 21dfac1a9..0ec7fea60 100644
--- a/docs/docs/guides/02-installation/03-linux.md
+++ b/docs/docs/guides/02-installation/03-linux.md
@@ -12,11 +12,24 @@ keywords:
conversational AI,
no-subscription fee,
large language model,
+ installation guide,
]
---
# Installing Jan on Linux
+## System Requirements
+
+Ensure that your system meets the following requirements:
+
+- glibc 2.27 or higher (check with `ldd --version`)
+- gcc 11, g++ 11, cpp 11, or higher, refer to this [link](https://jan.ai/guides/troubleshooting/gpu-not-used/#specific-requirements-for-linux) for more information.
+
+To enable GPU support, you will need:
+
+- NVIDIA GPU with CUDA Toolkit 11.7 or higher
+- NVIDIA driver 470.63.01 or higher
+
## Installation
Jan is available for download via our homepage, [https://jan.ai](https://jan.ai/).
@@ -66,7 +79,6 @@ jan-linux-amd64-{version}.deb
# AppImage
jan-linux-x86_64-{version}.AppImage
```
-```
## Uninstall Jan
diff --git a/docs/docs/guides/04-using-models/03-integrate-with-remote-server.mdx b/docs/docs/guides/04-using-models/03-integrate-with-remote-server.mdx
index 533797fca..f0db1bd55 100644
--- a/docs/docs/guides/04-using-models/03-integrate-with-remote-server.mdx
+++ b/docs/docs/guides/04-using-models/03-integrate-with-remote-server.mdx
@@ -65,6 +65,13 @@ Navigate to the `~/jan/models` folder. Create a folder named `gpt-3.5-turbo-16k`
}
```
+:::tip
+
+- You can find the list of available models in the [OpenAI Platform](https://platform.openai.com/docs/models/overview).
+- Please note that the `id` property need to match the model name in the list. For example, if you want to use the [GPT-4 Turbo](https://platform.openai.com/docs/models/gpt-4-and-gpt-4-turbo), you need to set the `id` property as `gpt-4-1106-preview`.
+
+:::
+
### 2. Configure OpenAI API Keys
You can find your API keys in the [OpenAI Platform](https://platform.openai.com/api-keys) and set the OpenAI API keys in `~/jan/engines/openai.json` file.
diff --git a/docs/docs/guides/08-troubleshooting/02-somethings-amiss.mdx b/docs/docs/guides/08-troubleshooting/02-somethings-amiss.mdx
index a5669e36d..4e16e362a 100644
--- a/docs/docs/guides/08-troubleshooting/02-somethings-amiss.mdx
+++ b/docs/docs/guides/08-troubleshooting/02-somethings-amiss.mdx
@@ -45,7 +45,9 @@ This may occur due to several reasons. Please follow these steps to resolve it:
5. If you are on Nvidia GPUs, please download [Cuda](https://developer.nvidia.com/cuda-downloads).
-6. When [checking app logs](https://jan.ai/troubleshooting/how-to-get-error-logs/), if you encounter the error log `Bind address failed at 127.0.0.1:3928`, it indicates that the port used by Nitro might already be in use. Use the following commands to check the port status:
+6. If you're using Linux, please ensure that your system meets the following requirements gcc 11, g++ 11, cpp 11, or higher, refer to this [link](https://jan.ai/guides/troubleshooting/gpu-not-used/#specific-requirements-for-linux) for more information.
+
+7. When [checking app logs](https://jan.ai/troubleshooting/how-to-get-error-logs/), if you encounter the error log `Bind address failed at 127.0.0.1:3928`, it indicates that the port used by Nitro might already be in use. Use the following commands to check the port status:
diff --git a/docs/docs/guides/08-troubleshooting/06-unexpected-token.mdx b/docs/docs/guides/08-troubleshooting/06-unexpected-token.mdx
index 973001f1b..1de609ffa 100644
--- a/docs/docs/guides/08-troubleshooting/06-unexpected-token.mdx
+++ b/docs/docs/guides/08-troubleshooting/06-unexpected-token.mdx
@@ -17,4 +17,8 @@ keywords:
]
---
-1. You may receive an error response `Error occurred: Unexpected token '<', "/nitro` and run the nitro manually and see if you get any error messages.
+3. Resolve the error messages you get from the nitro and see if the issue persists.
+4. Reopen the Jan app and see if the issue is resolved.
+5. If the issue persists, please share with us the [app logs](https://jan.ai/troubleshooting/how-to-get-error-logs/) via [Jan Discord](https://discord.gg/mY69SZaMaC).
diff --git a/docs/docs/template/QA_script.md b/docs/docs/template/QA_script.md
index 05dbed2b4..bba667bcd 100644
--- a/docs/docs/template/QA_script.md
+++ b/docs/docs/template/QA_script.md
@@ -1,6 +1,6 @@
# [Release Version] QA Script
-**Release Version:**
+**Release Version:** v0.4.6
**Operating System:**
@@ -25,10 +25,10 @@
### 3. Users uninstall app
-- [ ] :key: Check that the uninstallation process removes all components of the app from the system.
+- [ ] :key::warning: Check that the uninstallation process removes the app successfully from the system.
- [ ] Clean the Jan root directory and open the app to check if it creates all the necessary folders, especially models and extensions.
- [ ] When updating the app, check if the `/models` directory has any JSON files that change according to the update.
-- [ ] Verify if updating the app also updates extensions correctly (test functionality changes; support notifications for necessary tests with each version related to extensions update).
+- [ ] Verify if updating the app also updates extensions correctly (test functionality changes, support notifications for necessary tests with each version related to extensions update).
### 4. Users close app
@@ -60,49 +60,45 @@
- [ ] :key: Ensure that the conversation thread is maintained without any loss of data upon sending multiple messages.
- [ ] Test for the ability to send different types of messages (e.g., text, emojis, code blocks).
- [ ] :key: Validate the scroll functionality in the chat window for lengthy conversations.
-- [ ] Check if the user can renew responses multiple times.
- [ ] Check if the user can copy the response.
- [ ] Check if the user can delete responses.
-- [ ] :warning: Test if the user deletes the message midway, then the assistant stops that response.
- [ ] :key: Check the `clear message` button works.
- [ ] :key: Check the `delete entire chat` works.
-- [ ] :warning: Check if deleting all the chat retains the system prompt.
+- [ ] Check if deleting all the chat retains the system prompt.
- [ ] Check the output format of the AI (code blocks, JSON, markdown, ...).
- [ ] :key: Validate that there is appropriate error handling and messaging if the assistant fails to respond.
- [ ] Test assistant's ability to maintain context over multiple exchanges.
- [ ] :key: Check the `create new chat` button works correctly
- [ ] Confirm that by changing `models` mid-thread the app can still handle it.
-- [ ] Check that by changing `instructions` mid-thread the app can still handle it.
-- [ ] Check the `regenerate` button renews the response.
-- [ ] Check the `Instructions` update correctly after the user updates it midway.
+- [ ] Check the `regenerate` button renews the response (single / multiple times).
+- [ ] Check the `Instructions` update correctly after the user updates it midway (mid-thread).
### 2. Users can customize chat settings like model parameters via both the GUI & thread.json
-- [ ] :key: Confirm that the chat settings options are accessible via the GUI.
+- [ ] :key: Confirm that the Threads settings options are accessible.
- [ ] Test the functionality to adjust model parameters (e.g., Temperature, Top K, Top P) from the GUI and verify they are reflected in the chat behavior.
- [ ] :key: Ensure that changes can be saved and persisted between sessions.
- [ ] Validate that users can access and modify the thread.json file.
- [ ] :key: Check that changes made in thread.json are correctly applied to the chat session upon reload or restart.
-- [ ] Verify if there is a revert option to go back to previous settings after changes are made.
-- [ ] Test for user feedback or confirmation after saving changes to settings.
- [ ] Check the maximum and minimum limits of the adjustable parameters and how they affect the assistant's responses.
- [ ] :key: Validate user permissions for those who can change settings and persist them.
- [ ] :key: Ensure that users switch between threads with different models, the app can handle it.
-### 3. Users can click on a history thread
+### 3. Model dropdown
+- [ ] :key: Model list should highlight recommended based on user RAM
+- [ ] Model size should display (for both installed and imported models)
+### 4. Users can click on a history thread
- [ ] Test the ability to click on any thread in the history panel.
- [ ] :key: Verify that clicking a thread brings up the past conversation in the main chat window.
- [ ] :key: Ensure that the selected thread is highlighted or otherwise indicated in the history panel.
- [ ] Confirm that the chat window displays the entire conversation from the selected history thread without any missing messages.
- [ ] :key: Check the performance and accuracy of the history feature when dealing with a large number of threads.
- [ ] Validate that historical threads reflect the exact state of the chat at that time, including settings.
-- [ ] :key: :warning: Test the search functionality within the history panel for quick navigation.
- [ ] :key: Verify the ability to delete or clean old threads.
- [ ] :key: Confirm that changing the title of the thread updates correctly.
-### 4. Users can config instructions for the assistant.
-
+### 5. Users can config instructions for the assistant.
- [ ] Ensure there is a clear interface to input or change instructions for the assistant.
- [ ] Test if the instructions set by the user are being followed by the assistant in subsequent conversations.
- [ ] :key: Validate that changes to instructions are updated in real time and do not require a restart of the application or session.
@@ -112,6 +108,8 @@
- [ ] Validate that instructions can be saved with descriptive names for easy retrieval.
- [ ] :key: Check if the assistant can handle conflicting instructions and how it resolves them.
- [ ] Ensure that instruction configurations are documented for user reference.
+- [ ] :key: RAG - Users can import documents and the system should process queries about the uploaded file, providing accurate and appropriate responses in the conversation thread.
+
## D. Hub
@@ -125,8 +123,7 @@
- [ ] Display the best model for their RAM at the top.
- [ ] :key: Ensure that models are labeled with RAM requirements and compatibility.
-- [ ] :key: Validate that the download function is disabled for models that exceed the user's system capabilities.
-- [ ] Test that the platform provides alternative recommendations for models not suitable due to RAM limitations.
+- [ ] :warning: Test that the platform provides alternative recommendations for models not suitable due to RAM limitations.
- [ ] :key: Check the download model functionality and validate if the cancel download feature works correctly.
### 3. Users can download models via a HuggingFace URL (coming soon)
@@ -139,7 +136,7 @@
- [ ] :key: Have clear instructions so users can do their own.
- [ ] :key: Ensure the new model updates after restarting the app.
-- [ ] Ensure it raises clear errors for users to fix the problem while adding a new model.
+- [ ] :warning:Ensure it raises clear errors for users to fix the problem while adding a new model.
### 5. Users can use the model as they want
@@ -149,9 +146,13 @@
- [ ] Check if starting another model stops the other model entirely.
- [ ] Check the `Explore models` navigate correctly to the model panel.
- [ ] :key: Check when deleting a model it will delete all the files on the user's computer.
-- [ ] The recommended tags should present right for the user's hardware.
+- [ ] :warning:The recommended tags should present right for the user's hardware.
- [ ] Assess that the descriptions of models are accurate and informative.
+### 6. Users can Integrate With a Remote Server
+- [ ] :key: Import openAI GPT model https://jan.ai/guides/using-models/integrate-with-remote-server/ and the model displayed in Hub / Thread dropdown
+- [ ] Users can use the remote model properly
+
## E. System Monitor
### 1. Users can see disk and RAM utilization
@@ -181,7 +182,7 @@
- [ ] Confirm that the application saves the theme preference and persists it across sessions.
- [ ] Validate that all elements of the UI are compatible with the theme changes and maintain legibility and contrast.
-### 2. Users change the extensions
+### 2. Users change the extensions [TBU]
- [ ] Confirm that the `Extensions` tab lists all available plugins.
- [ ] :key: Test the toggle switch for each plugin to ensure it enables or disables the plugin correctly.
@@ -208,3 +209,19 @@
- [ ] :key: Test that the application prevents the installation of incompatible or corrupt plugin files.
- [ ] :key: Check that the user can uninstall or disable custom plugins as easily as pre-installed ones.
- [ ] Verify that the application's performance remains stable after the installation of custom plugins.
+
+### 5. Advanced Settings
+- [ ] Attemp to test downloading model from hub using **HTTP Proxy** [guideline](https://github.com/janhq/jan/pull/1562)
+- [ ] Users can move **Jan data folder**
+- [ ] Users can click on Reset button to **factory reset** app settings to its original state & delete all usage data.
+
+## G. Local API server
+
+### 1. Local Server Usage with Server Options
+- [ ] :key: Explore API Reference: Swagger API for sending/receiving requests
+ - [ ] Use default server option
+ - [ ] Configure and use custom server options
+- [ ] Test starting/stopping the local API server with different Model/Model settings
+- [ ] Server logs captured with correct Server Options provided
+- [ ] Verify functionality of Open logs/Clear feature
+- [ ] Ensure that threads and other functions impacting the model are disabled while the local server is running
diff --git a/docs/openapi/jan.yaml b/docs/openapi/jan.yaml
index bfff0ad73..864c80fdf 100644
--- a/docs/openapi/jan.yaml
+++ b/docs/openapi/jan.yaml
@@ -67,20 +67,31 @@ paths:
x-codeSamples:
- lang: cURL
source: |
- curl http://localhost:1337/v1/chat/completions \
- -H "Content-Type: application/json" \
+ curl -X 'POST' \
+ 'http://localhost:1337/v1/chat/completions' \
+ -H 'accept: application/json' \
+ -H 'Content-Type: application/json' \
-d '{
- "model": "tinyllama-1.1b",
"messages": [
{
- "role": "system",
- "content": "You are a helpful assistant."
+ "content": "You are a helpful assistant.",
+ "role": "system"
},
{
- "role": "user",
- "content": "Hello!"
+ "content": "Hello!",
+ "role": "user"
}
- ]
+ ],
+ "model": "tinyllama-1.1b",
+ "stream": true,
+ "max_tokens": 2048,
+ "stop": [
+ "hello"
+ ],
+ "frequency_penalty": 0,
+ "presence_penalty": 0,
+ "temperature": 0.7,
+ "top_p": 0.95
}'
/models:
get:
@@ -103,7 +114,9 @@ paths:
x-codeSamples:
- lang: cURL
source: |
- curl http://localhost:1337/v1/models
+ curl -X 'GET' \
+ 'http://localhost:1337/v1/models' \
+ -H 'accept: application/json'
"/models/download/{model_id}":
get:
operationId: downloadModel
@@ -131,7 +144,9 @@ paths:
x-codeSamples:
- lang: cURL
source: |
- curl -X POST http://localhost:1337/v1/models/download/{model_id}
+ curl -X 'GET' \
+ 'http://localhost:1337/v1/models/download/{model_id}' \
+ -H 'accept: application/json'
"/models/{model_id}":
get:
operationId: retrieveModel
@@ -162,7 +177,9 @@ paths:
x-codeSamples:
- lang: cURL
source: |
- curl http://localhost:1337/v1/models/{model_id}
+ curl -X 'GET' \
+ 'http://localhost:1337/v1/models/{model_id}' \
+ -H 'accept: application/json'
delete:
operationId: deleteModel
tags:
@@ -191,7 +208,9 @@ paths:
x-codeSamples:
- lang: cURL
source: |
- curl -X DELETE http://localhost:1337/v1/models/{model_id}
+ curl -X 'DELETE' \
+ 'http://localhost:1337/v1/models/{model_id}' \
+ -H 'accept: application/json'
/threads:
post:
operationId: createThread
diff --git a/docs/openapi/specs/assistants.yaml b/docs/openapi/specs/assistants.yaml
index d784c315a..5db1f6a97 100644
--- a/docs/openapi/specs/assistants.yaml
+++ b/docs/openapi/specs/assistants.yaml
@@ -316,4 +316,4 @@ components:
deleted:
type: boolean
description: Indicates whether the assistant was successfully deleted.
- example: true
\ No newline at end of file
+ example: true
diff --git a/docs/openapi/specs/chat.yaml b/docs/openapi/specs/chat.yaml
index b324501a8..cfa391598 100644
--- a/docs/openapi/specs/chat.yaml
+++ b/docs/openapi/specs/chat.yaml
@@ -188,4 +188,4 @@ components:
total_tokens:
type: integer
example: 533
- description: Total number of tokens used
\ No newline at end of file
+ description: Total number of tokens used
diff --git a/docs/openapi/specs/messages.yaml b/docs/openapi/specs/messages.yaml
index d9d7d87a4..6f5fe1a58 100644
--- a/docs/openapi/specs/messages.yaml
+++ b/docs/openapi/specs/messages.yaml
@@ -1,3 +1,4 @@
+---
components:
schemas:
MessageObject:
@@ -75,7 +76,7 @@ components:
example: msg_abc123
object:
type: string
- description: "Type of the object, indicating it's a thread message."
+ description: Type of the object, indicating it's a thread message.
default: thread.message
created_at:
type: integer
@@ -88,7 +89,7 @@ components:
example: thread_abc123
role:
type: string
- description: "Role of the sender, either 'user' or 'assistant'."
+ description: Role of the sender, either 'user' or 'assistant'.
example: user
content:
type: array
@@ -97,7 +98,7 @@ components:
properties:
type:
type: string
- description: "Type of content, e.g., 'text'."
+ description: Type of content, e.g., 'text'.
example: text
text:
type: object
@@ -110,21 +111,21 @@ components:
type: array
items:
type: string
- description: "Annotations for the text content, if any."
+ description: Annotations for the text content, if any.
example: []
file_ids:
type: array
items:
type: string
- description: "Array of file IDs associated with the message, if any."
+ description: Array of file IDs associated with the message, if any.
example: []
assistant_id:
type: string
- description: "Identifier of the assistant involved in the message, if applicable."
+ description: Identifier of the assistant involved in the message, if applicable.
example: null
run_id:
type: string
- description: "Run ID associated with the message, if applicable."
+ description: Run ID associated with the message, if applicable.
example: null
metadata:
type: object
@@ -139,7 +140,7 @@ components:
example: msg_abc123
object:
type: string
- description: "Type of the object, indicating it's a thread message."
+ description: Type of the object, indicating it's a thread message.
example: thread.message
created_at:
type: integer
@@ -152,7 +153,7 @@ components:
example: thread_abc123
role:
type: string
- description: "Role of the sender, either 'user' or 'assistant'."
+ description: Role of the sender, either 'user' or 'assistant'.
example: user
content:
type: array
@@ -161,7 +162,7 @@ components:
properties:
type:
type: string
- description: "Type of content, e.g., 'text'."
+ description: Type of content, e.g., 'text'.
example: text
text:
type: object
@@ -174,21 +175,21 @@ components:
type: array
items:
type: string
- description: "Annotations for the text content, if any."
+ description: Annotations for the text content, if any.
example: []
file_ids:
type: array
items:
type: string
- description: "Array of file IDs associated with the message, if any."
+ description: Array of file IDs associated with the message, if any.
example: []
assistant_id:
type: string
- description: "Identifier of the assistant involved in the message, if applicable."
+ description: Identifier of the assistant involved in the message, if applicable.
example: null
run_id:
type: string
- description: "Run ID associated with the message, if applicable."
+ description: Run ID associated with the message, if applicable.
example: null
metadata:
type: object
@@ -199,7 +200,7 @@ components:
properties:
object:
type: string
- description: "Type of the object, indicating it's a list."
+ description: Type of the object, indicating it's a list.
default: list
data:
type: array
@@ -226,7 +227,7 @@ components:
example: msg_abc123
object:
type: string
- description: "Type of the object, indicating it's a thread message."
+ description: Type of the object, indicating it's a thread message.
example: thread.message
created_at:
type: integer
@@ -239,7 +240,7 @@ components:
example: thread_abc123
role:
type: string
- description: "Role of the sender, either 'user' or 'assistant'."
+ description: Role of the sender, either 'user' or 'assistant'.
example: user
content:
type: array
@@ -248,7 +249,7 @@ components:
properties:
type:
type: string
- description: "Type of content, e.g., 'text'."
+ description: Type of content, e.g., 'text'.
text:
type: object
properties:
@@ -260,20 +261,20 @@ components:
type: array
items:
type: string
- description: "Annotations for the text content, if any."
+ description: Annotations for the text content, if any.
file_ids:
type: array
items:
type: string
- description: "Array of file IDs associated with the message, if any."
+ description: Array of file IDs associated with the message, if any.
example: []
assistant_id:
type: string
- description: "Identifier of the assistant involved in the message, if applicable."
+ description: Identifier of the assistant involved in the message, if applicable.
example: null
run_id:
type: string
- description: "Run ID associated with the message, if applicable."
+ description: Run ID associated with the message, if applicable.
example: null
metadata:
type: object
@@ -309,4 +310,4 @@ components:
data:
type: array
items:
- $ref: "#/components/schemas/MessageFileObject"
\ No newline at end of file
+ $ref: "#/components/schemas/MessageFileObject"
diff --git a/docs/openapi/specs/models.yaml b/docs/openapi/specs/models.yaml
index 8113f3ab8..40e6abaaf 100644
--- a/docs/openapi/specs/models.yaml
+++ b/docs/openapi/specs/models.yaml
@@ -18,114 +18,82 @@ components:
Model:
type: object
properties:
- type:
+ source_url:
type: string
- default: model
- description: The type of the object.
- version:
- type: string
- default: "1"
- description: The version number of the model.
+ format: uri
+ description: URL to the source of the model.
+ example: https://huggingface.co/janhq/trinity-v1.2-GGUF/resolve/main/trinity-v1.2.Q4_K_M.gguf
id:
type: string
- description: Unique identifier used in chat-completions model_name, matches
+ description:
+ Unique identifier used in chat-completions model_name, matches
folder name.
- example: zephyr-7b
+ example: trinity-v1.2-7b
+ object:
+ type: string
+ example: model
name:
type: string
description: Name of the model.
- example: Zephyr 7B
- owned_by:
+ example: Trinity-v1.2 7B Q4
+ version:
type: string
- description: Compatibility field for OpenAI.
- default: ""
- created:
- type: integer
- format: int64
- description: Unix timestamp representing the creation time.
+ default: "1.0"
+ description: The version number of the model.
description:
type: string
description: Description of the model.
- state:
- type: string
- enum:
- - null
- - downloading
- - ready
- - starting
- - stopping
- description: Current state of the model.
+ example:
+ Trinity is an experimental model merge using the Slerp method.
+ Recommended for daily assistance purposes.
format:
type: string
description: State format of the model, distinct from the engine.
- example: ggufv3
- source:
- type: array
- items:
- type: object
- properties:
- url:
- format: uri
- description: URL to the source of the model.
- example: https://huggingface.co/TheBloke/zephyr-7B-beta-GGUF/blob/main/zephyr-7b-beta.Q4_K_M.gguf
- filename:
- type: string
- description: Filename of the model.
- example: zephyr-7b-beta.Q4_K_M.gguf
+ example: gguf
settings:
type: object
properties:
ctx_len:
- type: string
+ type: integer
description: Context length.
- example: "4096"
- ngl:
+ example: 4096
+ prompt_template:
type: string
- description: Number of layers.
- example: "100"
- embedding:
- type: string
- description: Indicates if embedding is enabled.
- example: "true"
- n_parallel:
- type: string
- description: Number of parallel processes.
- example: "4"
+ example: "<|im_start|>system\n{system_message}<|im_end|>\n<|im_start|>user\n{prompt}<|im_end|>\n<|im_start|>assistant"
additionalProperties: false
parameters:
type: object
properties:
temperature:
- type: string
- description: Temperature setting for the model.
- example: "0.7"
- token_limit:
- type: string
- description: Token limit for the model.
- example: "4096"
- top_k:
- type: string
- description: Top-k setting for the model.
- example: "0"
+ example: 0.7
top_p:
- type: string
- description: Top-p setting for the model.
- example: "1"
+ example: 0.95
stream:
- type: string
- description: Indicates if streaming is enabled.
- example: "true"
+ example: true
+ max_tokens:
+ example: 4096
+ stop:
+ example: []
+ frequency_penalty:
+ example: 0
+ presence_penalty:
+ example: 0
additionalProperties: false
metadata:
- type: object
- description: Additional metadata.
- assets:
- type: array
- items:
+ author:
type: string
- description: List of assets related to the model.
- required:
- - source
+ example: Jan
+ tags:
+ example:
+ - 7B
+ - Merged
+ - Featured
+ size:
+ example: 4370000000,
+ cover:
+ example: https://raw.githubusercontent.com/janhq/jan/main/models/trinity-v1.2-7b/cover.png
+ engine:
+ example: nitro
ModelObject:
type: object
properties:
@@ -133,7 +101,7 @@ components:
type: string
description: |
The identifier of the model.
- example: zephyr-7b
+ example: trinity-v1.2-7b
object:
type: string
description: |
@@ -153,197 +121,89 @@ components:
GetModelResponse:
type: object
properties:
+ source_url:
+ type: string
+ format: uri
+ description: URL to the source of the model.
+ example: https://huggingface.co/TheBloke/Mistral-7B-Instruct-v0.2-GGUF/resolve/main/mistral-7b-instruct-v0.2.Q4_K_M.gguf
id:
type: string
- description: The identifier of the model.
- example: zephyr-7b
+ description:
+ Unique identifier used in chat-completions model_name, matches
+ folder name.
+ example: mistral-ins-7b-q4
object:
type: string
- description: Type of the object, indicating it's a model.
- default: model
- created:
- type: integer
- format: int64
- description: Unix timestamp representing the creation time of the model.
- owned_by:
+ example: model
+ name:
type: string
- description: The entity that owns the model.
- example: _
- state:
+ description: Name of the model.
+ example: Mistral Instruct 7B Q4
+ version:
type: string
- enum:
- - not_downloaded
- - downloaded
- - running
- - stopped
- description: The current state of the model.
- source:
- type: array
- items:
- type: object
- properties:
- url:
- format: uri
- description: URL to the source of the model.
- example: https://huggingface.co/TheBloke/zephyr-7B-beta-GGUF/blob/main/zephyr-7b-beta.Q4_K_M.gguf
- filename:
- type: string
- description: Filename of the model.
- example: zephyr-7b-beta.Q4_K_M.gguf
- engine_parameters:
- type: object
- properties:
- pre_prompt:
- type: string
- description: Predefined prompt used for setting up internal configurations.
- default: ""
- example: Initial setup complete.
- system_prompt:
- type: string
- description: Prefix used for system-level prompts.
- default: "SYSTEM: "
- user_prompt:
- type: string
- description: Prefix used for user prompts.
- default: "USER: "
- ai_prompt:
- type: string
- description: Prefix used for assistant prompts.
- default: "ASSISTANT: "
- ngl:
- type: integer
- description: Number of neural network layers loaded onto the GPU for
- acceleration.
- minimum: 0
- maximum: 100
- default: 100
- example: 100
- ctx_len:
- type: integer
- description: Context length for model operations, varies based on the specific
- model.
- minimum: 128
- maximum: 4096
- default: 4096
- example: 4096
- n_parallel:
- type: integer
- description: Number of parallel operations, relevant when continuous batching is
- enabled.
- minimum: 1
- maximum: 10
- default: 1
- example: 4
- cont_batching:
- type: boolean
- description: Indicates if continuous batching is used for processing.
- default: false
- example: false
- cpu_threads:
- type: integer
- description: Number of threads allocated for CPU-based inference.
- minimum: 1
- example: 8
- embedding:
- type: boolean
- description: Indicates if embedding layers are enabled in the model.
- default: true
- example: true
- model_parameters:
+ default: "1.0"
+ description: The version number of the model.
+ description:
+ type: string
+ description: Description of the model.
+ example:
+ Trinity is an experimental model merge using the Slerp method.
+ Recommended for daily assistance purposes.
+ format:
+ type: string
+ description: State format of the model, distinct from the engine.
+ example: gguf
+ settings:
type: object
properties:
ctx_len:
type: integer
- description: Maximum context length the model can handle.
- minimum: 0
- maximum: 4096
- default: 4096
+ description: Context length.
example: 4096
- ngl:
- type: integer
- description: Number of layers in the neural network.
- minimum: 1
- maximum: 100
- default: 100
- example: 100
- embedding:
- type: boolean
- description: Indicates if embedding layers are used.
- default: true
- example: true
- n_parallel:
- type: integer
- description: Number of parallel processes the model can run.
- minimum: 1
- maximum: 10
- default: 1
- example: 4
+ prompt_template:
+ type: string
+ example: "[INST] {prompt} [/INST]"
+ additionalProperties: false
+ parameters:
+ type: object
+ properties:
temperature:
- type: number
- description: Controls randomness in model's responses. Higher values lead to
- more random responses.
- minimum: 0
- maximum: 2
- default: 0.7
example: 0.7
- token_limit:
- type: integer
- description: Maximum number of tokens the model can generate in a single
- response.
- minimum: 1
- maximum: 4096
- default: 4096
- example: 4096
- top_k:
- type: integer
- description: Limits the model to consider only the top k most likely next tokens
- at each step.
- minimum: 0
- maximum: 100
- default: 0
- example: 0
top_p:
- type: number
- description: Nucleus sampling parameter. The model considers the smallest set of
- tokens whose cumulative probability exceeds the top_p value.
- minimum: 0
- maximum: 1
- default: 1
- example: 1
+ example: 0.95
+ stream:
+ example: true
+ max_tokens:
+ example: 4096
+ stop:
+ example: []
+ frequency_penalty:
+ example: 0
+ presence_penalty:
+ example: 0
+ additionalProperties: false
metadata:
- type: object
- properties:
- engine:
- type: string
- description: The engine used by the model.
- enum:
- - nitro
- - openai
- - hf_inference
- quantization:
- type: string
- description: Quantization parameter of the model.
- example: Q3_K_L
- size:
- type: string
- description: Size of the model.
- example: 7B
- required:
- - id
- - object
- - created
- - owned_by
- - state
- - source
- - parameters
- - metadata
+ author:
+ type: string
+ example: MistralAI
+ tags:
+ example:
+ - 7B
+ - Featured
+ - Foundation Model
+ size:
+ example: 4370000000,
+ cover:
+ example: https://raw.githubusercontent.com/janhq/jan/main/models/mistral-ins-7b-q4/cover.png
+ engine:
+ example: nitro
DeleteModelResponse:
type: object
properties:
id:
type: string
description: The identifier of the model that was deleted.
- example: model-zephyr-7B
+ example: mistral-ins-7b-q4
object:
type: string
description: Type of the object, indicating it's a model.
diff --git a/docs/openapi/specs/threads.yaml b/docs/openapi/specs/threads.yaml
index fe00f7588..40b2463fa 100644
--- a/docs/openapi/specs/threads.yaml
+++ b/docs/openapi/specs/threads.yaml
@@ -142,7 +142,7 @@ components:
example: Jan
instructions:
type: string
- description: |
+ description: >
The instruction of assistant, defaults to "Be my grammar corrector"
model:
type: object
@@ -224,4 +224,4 @@ components:
deleted:
type: boolean
description: Indicates whether the thread was successfully deleted.
- example: true
\ No newline at end of file
+ example: true
diff --git a/electron/handlers/download.ts b/electron/handlers/download.ts
index f63e56f6b..5f1d8371e 100644
--- a/electron/handlers/download.ts
+++ b/electron/handlers/download.ts
@@ -5,7 +5,11 @@ import request from 'request'
import { createWriteStream, renameSync } from 'fs'
import { DownloadEvent, DownloadRoute } from '@janhq/core'
const progress = require('request-progress')
-import { DownloadManager, getJanDataFolderPath, normalizeFilePath } from '@janhq/core/node'
+import {
+ DownloadManager,
+ getJanDataFolderPath,
+ normalizeFilePath,
+} from '@janhq/core/node'
export function handleDownloaderIPCs() {
/**
@@ -56,20 +60,23 @@ export function handleDownloaderIPCs() {
*/
ipcMain.handle(
DownloadRoute.downloadFile,
- async (_event, url, fileName, network) => {
+ async (_event, url, localPath, network) => {
const strictSSL = !network?.ignoreSSL
const proxy = network?.proxy?.startsWith('http')
? network.proxy
: undefined
-
- if (typeof fileName === 'string') {
- fileName = normalizeFilePath(fileName)
+ if (typeof localPath === 'string') {
+ localPath = normalizeFilePath(localPath)
}
- const destination = resolve(getJanDataFolderPath(), fileName)
+ const array = localPath.split('/')
+ const fileName = array.pop() ?? ''
+ const modelId = array.pop() ?? ''
+
+ const destination = resolve(getJanDataFolderPath(), localPath)
const rq = request({ url, strictSSL, proxy })
// Put request to download manager instance
- DownloadManager.instance.setRequest(fileName, rq)
+ DownloadManager.instance.setRequest(localPath, rq)
// Downloading file to a temp file first
const downloadingTempFile = `${destination}.download`
@@ -81,6 +88,7 @@ export function handleDownloaderIPCs() {
{
...state,
fileName,
+ modelId,
}
)
})
@@ -90,11 +98,12 @@ export function handleDownloaderIPCs() {
{
fileName,
err,
+ modelId,
}
)
})
.on('end', function () {
- if (DownloadManager.instance.networkRequests[fileName]) {
+ if (DownloadManager.instance.networkRequests[localPath]) {
// Finished downloading, rename temp file to actual file
renameSync(downloadingTempFile, destination)
@@ -102,14 +111,16 @@ export function handleDownloaderIPCs() {
DownloadEvent.onFileDownloadSuccess,
{
fileName,
+ modelId,
}
)
- DownloadManager.instance.setRequest(fileName, undefined)
+ DownloadManager.instance.setRequest(localPath, undefined)
} else {
WindowManager?.instance.currentWindow?.webContents.send(
DownloadEvent.onFileDownloadError,
{
fileName,
+ modelId,
err: { message: 'aborted' },
}
)
diff --git a/electron/handlers/fileManager.ts b/electron/handlers/fileManager.ts
index e328cb53b..15c371d34 100644
--- a/electron/handlers/fileManager.ts
+++ b/electron/handlers/fileManager.ts
@@ -38,6 +38,7 @@ export function handleFileMangerIPCs() {
getResourcePath()
)
+ // Handles the 'getUserHomePath' IPC event. This event is triggered to get the user home path.
ipcMain.handle(FileManagerRoute.getUserHomePath, async (_event) =>
app.getPath('home')
)
diff --git a/electron/handlers/fs.ts b/electron/handlers/fs.ts
index 34026b940..8ac575cb2 100644
--- a/electron/handlers/fs.ts
+++ b/electron/handlers/fs.ts
@@ -1,8 +1,7 @@
import { ipcMain } from 'electron'
import { getJanDataFolderPath, normalizeFilePath } from '@janhq/core/node'
-import fs from 'fs'
-import { FileManagerRoute, FileSystemRoute } from '@janhq/core'
+import { FileSystemRoute } from '@janhq/core'
import { join } from 'path'
/**
* Handles file system operations.
diff --git a/electron/merge-latest-ymls.js b/electron/merge-latest-ymls.js
new file mode 100644
index 000000000..8172a3176
--- /dev/null
+++ b/electron/merge-latest-ymls.js
@@ -0,0 +1,27 @@
+const yaml = require('js-yaml')
+const fs = require('fs')
+
+// get two file paths from arguments:
+const [, , ...args] = process.argv
+const file1 = args[0]
+const file2 = args[1]
+const file3 = args[2]
+
+// check that all arguments are present and throw error instead
+if (!file1 || !file2 || !file3) {
+ throw new Error('Please provide 3 file paths as arguments: path to file1, to file2 and destination path')
+}
+
+const doc1 = yaml.load(fs.readFileSync(file1, 'utf8'))
+console.log('doc1: ', doc1)
+
+const doc2 = yaml.load(fs.readFileSync(file2, 'utf8'))
+console.log('doc2: ', doc2)
+
+const merged = { ...doc1, ...doc2 }
+merged.files.push(...doc1.files)
+
+console.log('merged', merged)
+
+const mergedYml = yaml.dump(merged)
+fs.writeFileSync(file3, mergedYml, 'utf8')
diff --git a/electron/package.json b/electron/package.json
index 2892fedc6..229979b41 100644
--- a/electron/package.json
+++ b/electron/package.json
@@ -57,16 +57,17 @@
"scripts": {
"lint": "eslint . --ext \".js,.jsx,.ts,.tsx\"",
"test:e2e": "playwright test --workers=1",
- "dev": "tsc -p . && electron .",
- "build": "run-script-os",
- "build:test": "run-script-os",
+ "copy:assets": "rimraf --glob \"./pre-install/*.tgz\" && cpx \"../pre-install/*.tgz\" \"./pre-install\"",
+ "dev": "yarn copy:assets && tsc -p . && electron .",
+ "build": "yarn copy:assets && run-script-os",
+ "build:test": "yarn copy:assets && run-script-os",
"build:test:darwin": "tsc -p . && electron-builder -p never -m --dir",
"build:test:win32": "tsc -p . && electron-builder -p never -w --dir",
"build:test:linux": "tsc -p . && electron-builder -p never -l --dir",
"build:darwin": "tsc -p . && electron-builder -p never -m --x64 --arm64",
"build:win32": "tsc -p . && electron-builder -p never -w",
"build:linux": "tsc -p . && electron-builder -p never -l deb -l AppImage",
- "build:publish": "run-script-os",
+ "build:publish": "yarn copy:assets && run-script-os",
"build:publish:darwin": "tsc -p . && electron-builder -p always -m --x64 --arm64",
"build:publish:win32": "tsc -p . && electron-builder -p always -w",
"build:publish:linux": "tsc -p . && electron-builder -p always -l deb -l AppImage"
diff --git a/electron/playwright.config.ts b/electron/playwright.config.ts
index 1fa3313f2..8047b7513 100644
--- a/electron/playwright.config.ts
+++ b/electron/playwright.config.ts
@@ -1,9 +1,16 @@
import { PlaywrightTestConfig } from '@playwright/test'
const config: PlaywrightTestConfig = {
- testDir: './tests',
+ testDir: './tests/e2e',
retries: 0,
globalTimeout: 300000,
+ use: {
+ screenshot: 'only-on-failure',
+ video: 'retain-on-failure',
+ trace: 'retain-on-failure',
+ },
+
+ reporter: [['html', { outputFolder: './playwright-report' }]],
}
export default config
diff --git a/electron/tests/e2e/hub.e2e.spec.ts b/electron/tests/e2e/hub.e2e.spec.ts
new file mode 100644
index 000000000..68632058e
--- /dev/null
+++ b/electron/tests/e2e/hub.e2e.spec.ts
@@ -0,0 +1,34 @@
+import {
+ page,
+ test,
+ setupElectron,
+ teardownElectron,
+ TIMEOUT,
+} from '../pages/basePage'
+import { expect } from '@playwright/test'
+
+test.beforeAll(async () => {
+ const appInfo = await setupElectron()
+ expect(appInfo.asar).toBe(true)
+ expect(appInfo.executable).toBeTruthy()
+ expect(appInfo.main).toBeTruthy()
+ expect(appInfo.name).toBe('jan')
+ expect(appInfo.packageJson).toBeTruthy()
+ expect(appInfo.packageJson.name).toBe('jan')
+ expect(appInfo.platform).toBeTruthy()
+ expect(appInfo.platform).toBe(process.platform)
+ expect(appInfo.resourcesDir).toBeTruthy()
+})
+
+test.afterAll(async () => {
+ await teardownElectron()
+})
+
+test('explores hub', async () => {
+ await page.getByTestId('Hub').first().click({
+ timeout: TIMEOUT,
+ })
+ await page.getByTestId('hub-container-test-id').isVisible({
+ timeout: TIMEOUT,
+ })
+})
diff --git a/electron/tests/e2e/navigation.e2e.spec.ts b/electron/tests/e2e/navigation.e2e.spec.ts
new file mode 100644
index 000000000..2da59953c
--- /dev/null
+++ b/electron/tests/e2e/navigation.e2e.spec.ts
@@ -0,0 +1,38 @@
+import { expect } from '@playwright/test'
+import {
+ page,
+ setupElectron,
+ TIMEOUT,
+ test,
+ teardownElectron,
+} from '../pages/basePage'
+
+test.beforeAll(async () => {
+ await setupElectron()
+})
+
+test.afterAll(async () => {
+ await teardownElectron()
+})
+
+test('renders left navigation panel', async () => {
+ const systemMonitorBtn = await page
+ .getByTestId('System Monitor')
+ .first()
+ .isEnabled({
+ timeout: TIMEOUT,
+ })
+ const settingsBtn = await page
+ .getByTestId('Thread')
+ .first()
+ .isEnabled({ timeout: TIMEOUT })
+ expect([systemMonitorBtn, settingsBtn].filter((e) => !e).length).toBe(0)
+ // Chat section should be there
+ await page.getByTestId('Local API Server').first().click({
+ timeout: TIMEOUT,
+ })
+ const localServer = page.getByTestId('local-server-testid').first()
+ await expect(localServer).toBeVisible({
+ timeout: TIMEOUT,
+ })
+})
diff --git a/electron/tests/e2e/settings.e2e.spec.ts b/electron/tests/e2e/settings.e2e.spec.ts
new file mode 100644
index 000000000..54215d9b1
--- /dev/null
+++ b/electron/tests/e2e/settings.e2e.spec.ts
@@ -0,0 +1,23 @@
+import { expect } from '@playwright/test'
+
+import {
+ setupElectron,
+ teardownElectron,
+ test,
+ page,
+ TIMEOUT,
+} from '../pages/basePage'
+
+test.beforeAll(async () => {
+ await setupElectron()
+})
+
+test.afterAll(async () => {
+ await teardownElectron()
+})
+
+test('shows settings', async () => {
+ await page.getByTestId('Settings').first().click({ timeout: TIMEOUT })
+ const settingDescription = page.getByTestId('testid-setting-description')
+ await expect(settingDescription).toBeVisible({ timeout: TIMEOUT })
+})
diff --git a/electron/tests/hub.e2e.spec.ts b/electron/tests/hub.e2e.spec.ts
deleted file mode 100644
index cc72e037e..000000000
--- a/electron/tests/hub.e2e.spec.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import { _electron as electron } from 'playwright'
-import { ElectronApplication, Page, expect, test } from '@playwright/test'
-
-import {
- findLatestBuild,
- parseElectronApp,
- stubDialog,
-} from 'electron-playwright-helpers'
-
-let electronApp: ElectronApplication
-let page: Page
-const TIMEOUT: number = parseInt(process.env.TEST_TIMEOUT || '300000')
-
-test.beforeAll(async () => {
- process.env.CI = 'e2e'
-
- const latestBuild = findLatestBuild('dist')
- expect(latestBuild).toBeTruthy()
-
- // parse the packaged Electron app and find paths and other info
- const appInfo = parseElectronApp(latestBuild)
- expect(appInfo).toBeTruthy()
-
- electronApp = await electron.launch({
- args: [appInfo.main], // main file from package.json
- executablePath: appInfo.executable, // path to the Electron executable
- })
- await stubDialog(electronApp, 'showMessageBox', { response: 1 })
-
- page = await electronApp.firstWindow({
- timeout: TIMEOUT,
- })
-})
-
-test.afterAll(async () => {
- await electronApp.close()
- await page.close()
-})
-
-test('explores hub', async () => {
- test.setTimeout(TIMEOUT)
- await page.getByTestId('Hub').first().click({
- timeout: TIMEOUT,
- })
- await page.getByTestId('hub-container-test-id').isVisible({
- timeout: TIMEOUT,
- })
-})
diff --git a/electron/tests/navigation.e2e.spec.ts b/electron/tests/navigation.e2e.spec.ts
deleted file mode 100644
index 5c8721c2f..000000000
--- a/electron/tests/navigation.e2e.spec.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import { _electron as electron } from 'playwright'
-import { ElectronApplication, Page, expect, test } from '@playwright/test'
-
-import {
- findLatestBuild,
- parseElectronApp,
- stubDialog,
-} from 'electron-playwright-helpers'
-
-let electronApp: ElectronApplication
-let page: Page
-const TIMEOUT: number = parseInt(process.env.TEST_TIMEOUT || '300000')
-
-test.beforeAll(async () => {
- process.env.CI = 'e2e'
-
- const latestBuild = findLatestBuild('dist')
- expect(latestBuild).toBeTruthy()
-
- // parse the packaged Electron app and find paths and other info
- const appInfo = parseElectronApp(latestBuild)
- expect(appInfo).toBeTruthy()
-
- electronApp = await electron.launch({
- args: [appInfo.main], // main file from package.json
- executablePath: appInfo.executable, // path to the Electron executable
- })
- await stubDialog(electronApp, 'showMessageBox', { response: 1 })
-
- page = await electronApp.firstWindow({
- timeout: TIMEOUT,
- })
-})
-
-test.afterAll(async () => {
- await electronApp.close()
- await page.close()
-})
-
-test('renders left navigation panel', async () => {
- test.setTimeout(TIMEOUT)
- const systemMonitorBtn = await page
- .getByTestId('System Monitor')
- .first()
- .isEnabled({
- timeout: TIMEOUT,
- })
- const settingsBtn = await page
- .getByTestId('Thread')
- .first()
- .isEnabled({ timeout: TIMEOUT })
- expect([systemMonitorBtn, settingsBtn].filter((e) => !e).length).toBe(0)
- // Chat section should be there
- await page.getByTestId('Local API Server').first().click({
- timeout: TIMEOUT,
- })
- const localServer = await page.getByTestId('local-server-testid').first()
- await expect(localServer).toBeVisible({
- timeout: TIMEOUT,
- })
-})
diff --git a/electron/tests/pages/basePage.ts b/electron/tests/pages/basePage.ts
new file mode 100644
index 000000000..5f1a6fca1
--- /dev/null
+++ b/electron/tests/pages/basePage.ts
@@ -0,0 +1,67 @@
+import {
+ expect,
+ test as base,
+ _electron as electron,
+ ElectronApplication,
+ Page,
+} from '@playwright/test'
+import {
+ findLatestBuild,
+ parseElectronApp,
+ stubDialog,
+} from 'electron-playwright-helpers'
+
+export const TIMEOUT: number = parseInt(process.env.TEST_TIMEOUT || '300000')
+
+export let electronApp: ElectronApplication
+export let page: Page
+
+export async function setupElectron() {
+ process.env.CI = 'e2e'
+
+ const latestBuild = findLatestBuild('dist')
+ expect(latestBuild).toBeTruthy()
+
+ // parse the packaged Electron app and find paths and other info
+ const appInfo = parseElectronApp(latestBuild)
+ expect(appInfo).toBeTruthy()
+
+ electronApp = await electron.launch({
+ args: [appInfo.main], // main file from package.json
+ executablePath: appInfo.executable, // path to the Electron executable
+ })
+ await stubDialog(electronApp, 'showMessageBox', { response: 1 })
+
+ page = await electronApp.firstWindow({
+ timeout: TIMEOUT,
+ })
+ // Return appInfo for future use
+ return appInfo
+}
+
+export async function teardownElectron() {
+ await page.close()
+ await electronApp.close()
+}
+
+export const test = base.extend<{
+ attachScreenshotsToReport: void
+}>({
+ attachScreenshotsToReport: [
+ async ({ request }, use, testInfo) => {
+ await use()
+
+ // After the test, we can check whether the test passed or failed.
+ if (testInfo.status !== testInfo.expectedStatus) {
+ const screenshot = await page.screenshot()
+ await testInfo.attach('screenshot', {
+ body: screenshot,
+ contentType: 'image/png',
+ })
+ }
+ },
+ { auto: true },
+ ],
+})
+
+test.setTimeout(TIMEOUT)
diff --git a/electron/tests/settings.e2e.spec.ts b/electron/tests/settings.e2e.spec.ts
deleted file mode 100644
index ad2d7b4a4..000000000
--- a/electron/tests/settings.e2e.spec.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import { _electron as electron } from 'playwright'
-import { ElectronApplication, Page, expect, test } from '@playwright/test'
-
-import {
- findLatestBuild,
- parseElectronApp,
- stubDialog,
-} from 'electron-playwright-helpers'
-
-let electronApp: ElectronApplication
-let page: Page
-const TIMEOUT: number = parseInt(process.env.TEST_TIMEOUT || '300000')
-
-test.beforeAll(async () => {
- process.env.CI = 'e2e'
-
- const latestBuild = findLatestBuild('dist')
- expect(latestBuild).toBeTruthy()
-
- // parse the packaged Electron app and find paths and other info
- const appInfo = parseElectronApp(latestBuild)
- expect(appInfo).toBeTruthy()
-
- electronApp = await electron.launch({
- args: [appInfo.main], // main file from package.json
- executablePath: appInfo.executable, // path to the Electron executable
- })
- await stubDialog(electronApp, 'showMessageBox', { response: 1 })
-
- page = await electronApp.firstWindow({
- timeout: TIMEOUT,
- })
-})
-
-test.afterAll(async () => {
- await electronApp.close()
- await page.close()
-})
-
-test('shows settings', async () => {
- test.setTimeout(TIMEOUT)
- await page.getByTestId('Settings').first().click({ timeout: TIMEOUT })
- const settingDescription = page.getByTestId('testid-setting-description')
- await expect(settingDescription).toBeVisible({ timeout: TIMEOUT })
-})
diff --git a/extensions/assistant-extension/package.json b/extensions/assistant-extension/package.json
index 84bcdf47e..5f45ecabe 100644
--- a/extensions/assistant-extension/package.json
+++ b/extensions/assistant-extension/package.json
@@ -8,9 +8,9 @@
"license": "AGPL-3.0",
"scripts": {
"build": "tsc --module commonjs && rollup -c rollup.config.ts",
- "build:publish:linux": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/pre-install",
- "build:publish:darwin": "rimraf *.tgz --glob && npm run build && ../../.github/scripts/auto-sign.sh && npm pack && cpx *.tgz ../../electron/pre-install",
- "build:publish:win32": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/pre-install",
+ "build:publish:linux": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../pre-install",
+ "build:publish:darwin": "rimraf *.tgz --glob && npm run build && ../../.github/scripts/auto-sign.sh && npm pack && cpx *.tgz ../../pre-install",
+ "build:publish:win32": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../pre-install",
"build:publish": "run-script-os"
},
"devDependencies": {
diff --git a/extensions/assistant-extension/src/index.ts b/extensions/assistant-extension/src/index.ts
index 6495ea786..8bc8cafdc 100644
--- a/extensions/assistant-extension/src/index.ts
+++ b/extensions/assistant-extension/src/index.ts
@@ -9,6 +9,7 @@ import {
joinPath,
executeOnMain,
AssistantExtension,
+ AssistantEvent,
} from "@janhq/core";
export default class JanAssistantExtension extends AssistantExtension {
@@ -21,7 +22,7 @@ export default class JanAssistantExtension extends AssistantExtension {
async onLoad() {
// making the assistant directory
const assistantDirExist = await fs.existsSync(
- JanAssistantExtension._homeDir,
+ JanAssistantExtension._homeDir
);
if (
localStorage.getItem(`${EXTENSION_NAME}-version`) !== VERSION ||
@@ -31,14 +32,16 @@ export default class JanAssistantExtension extends AssistantExtension {
await fs.mkdirSync(JanAssistantExtension._homeDir);
// Write assistant metadata
- this.createJanAssistant();
+ await this.createJanAssistant();
// Finished migration
localStorage.setItem(`${EXTENSION_NAME}-version`, VERSION);
+ // Update the assistant list
+ events.emit(AssistantEvent.OnAssistantsUpdate, {});
}
// Events subscription
events.on(MessageEvent.OnMessageSent, (data: MessageRequest) =>
- JanAssistantExtension.handleMessageRequest(data, this),
+ JanAssistantExtension.handleMessageRequest(data, this)
);
events.on(InferenceEvent.OnInferenceStopped, () => {
@@ -53,7 +56,7 @@ export default class JanAssistantExtension extends AssistantExtension {
private static async handleMessageRequest(
data: MessageRequest,
- instance: JanAssistantExtension,
+ instance: JanAssistantExtension
) {
instance.isCancelled = false;
instance.controller = new AbortController();
@@ -80,7 +83,7 @@ export default class JanAssistantExtension extends AssistantExtension {
NODE,
"toolRetrievalIngestNewDocument",
docFile,
- data.model?.proxyEngine,
+ data.model?.proxyEngine
);
}
}
@@ -96,7 +99,7 @@ export default class JanAssistantExtension extends AssistantExtension {
NODE,
"toolRetrievalUpdateTextSplitter",
data.thread.assistants[0].tools[0]?.settings?.chunk_size ?? 4000,
- data.thread.assistants[0].tools[0]?.settings?.chunk_overlap ?? 200,
+ data.thread.assistants[0].tools[0]?.settings?.chunk_overlap ?? 200
);
}
@@ -110,7 +113,7 @@ export default class JanAssistantExtension extends AssistantExtension {
const retrievalResult = await executeOnMain(
NODE,
"toolRetrievalQueryResult",
- prompt,
+ prompt
);
// Update the message content
@@ -168,7 +171,7 @@ export default class JanAssistantExtension extends AssistantExtension {
try {
await fs.writeFileSync(
assistantMetadataPath,
- JSON.stringify(assistant, null, 2),
+ JSON.stringify(assistant, null, 2)
);
} catch (err) {
console.error(err);
@@ -180,7 +183,7 @@ export default class JanAssistantExtension extends AssistantExtension {
// get all the assistant metadata json
const results: Assistant[] = [];
const allFileName: string[] = await fs.readdirSync(
- JanAssistantExtension._homeDir,
+ JanAssistantExtension._homeDir
);
for (const fileName of allFileName) {
const filePath = await joinPath([
@@ -190,7 +193,7 @@ export default class JanAssistantExtension extends AssistantExtension {
if (filePath.includes(".DS_Store")) continue;
const jsonFiles: string[] = (await fs.readdirSync(filePath)).filter(
- (file: string) => file === "assistant.json",
+ (file: string) => file === "assistant.json"
);
if (jsonFiles.length !== 1) {
@@ -200,7 +203,7 @@ export default class JanAssistantExtension extends AssistantExtension {
const content = await fs.readFileSync(
await joinPath([filePath, jsonFiles[0]]),
- "utf-8",
+ "utf-8"
);
const assistant: Assistant =
typeof content === "object" ? content : JSON.parse(content);
diff --git a/extensions/assistant-extension/src/node/tools/retrieval/index.ts b/extensions/assistant-extension/src/node/tools/retrieval/index.ts
index cd7e9abb1..8c7a6aa2b 100644
--- a/extensions/assistant-extension/src/node/tools/retrieval/index.ts
+++ b/extensions/assistant-extension/src/node/tools/retrieval/index.ts
@@ -35,21 +35,19 @@ export class Retrieval {
if (engine === "nitro") {
this.embeddingModel = new OpenAIEmbeddings(
{ openAIApiKey: "nitro-embedding" },
- { basePath: "http://127.0.0.1:3928/v1" }
+ { basePath: "http://127.0.0.1:3928/v1" },
);
} else {
// Fallback to OpenAI Settings
this.embeddingModel = new OpenAIEmbeddings({
- configuration: {
- apiKey: settings.api_key,
- },
+ openAIApiKey: settings.api_key,
});
}
}
public ingestAgentKnowledge = async (
filePath: string,
- memoryPath: string
+ memoryPath: string,
): Promise => {
const loader = new PDFLoader(filePath, {
splitPages: true,
diff --git a/extensions/conversational-extension/package.json b/extensions/conversational-extension/package.json
index a60c12339..b84c75d3d 100644
--- a/extensions/conversational-extension/package.json
+++ b/extensions/conversational-extension/package.json
@@ -7,7 +7,7 @@
"license": "MIT",
"scripts": {
"build": "tsc -b . && webpack --config webpack.config.js",
- "build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/pre-install"
+ "build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../pre-install"
},
"exports": {
".": "./dist/index.js",
diff --git a/extensions/conversational-extension/src/index.ts b/extensions/conversational-extension/src/index.ts
index 3d28a9c1d..bf8c213ad 100644
--- a/extensions/conversational-extension/src/index.ts
+++ b/extensions/conversational-extension/src/index.ts
@@ -12,7 +12,7 @@ import {
* functionality for managing threads.
*/
export default class JSONConversationalExtension extends ConversationalExtension {
- private static readonly _homeDir = 'file://threads'
+ private static readonly _threadFolder = 'file://threads'
private static readonly _threadInfoFileName = 'thread.json'
private static readonly _threadMessagesFileName = 'messages.jsonl'
@@ -20,8 +20,8 @@ export default class JSONConversationalExtension extends ConversationalExtension
* Called when the extension is loaded.
*/
async onLoad() {
- if (!(await fs.existsSync(JSONConversationalExtension._homeDir)))
- await fs.mkdirSync(JSONConversationalExtension._homeDir)
+ if (!(await fs.existsSync(JSONConversationalExtension._threadFolder)))
+ await fs.mkdirSync(JSONConversationalExtension._threadFolder)
console.debug('JSONConversationalExtension loaded')
}
@@ -68,7 +68,7 @@ export default class JSONConversationalExtension extends ConversationalExtension
async saveThread(thread: Thread): Promise {
try {
const threadDirPath = await joinPath([
- JSONConversationalExtension._homeDir,
+ JSONConversationalExtension._threadFolder,
thread.id,
])
const threadJsonPath = await joinPath([
@@ -92,7 +92,7 @@ export default class JSONConversationalExtension extends ConversationalExtension
*/
async deleteThread(threadId: string): Promise {
const path = await joinPath([
- JSONConversationalExtension._homeDir,
+ JSONConversationalExtension._threadFolder,
`${threadId}`,
])
try {
@@ -109,7 +109,7 @@ export default class JSONConversationalExtension extends ConversationalExtension
async addNewMessage(message: ThreadMessage): Promise {
try {
const threadDirPath = await joinPath([
- JSONConversationalExtension._homeDir,
+ JSONConversationalExtension._threadFolder,
message.thread_id,
])
const threadMessagePath = await joinPath([
@@ -177,7 +177,7 @@ export default class JSONConversationalExtension extends ConversationalExtension
): Promise {
try {
const threadDirPath = await joinPath([
- JSONConversationalExtension._homeDir,
+ JSONConversationalExtension._threadFolder,
threadId,
])
const threadMessagePath = await joinPath([
@@ -205,7 +205,7 @@ export default class JSONConversationalExtension extends ConversationalExtension
private async readThread(threadDirName: string): Promise {
return fs.readFileSync(
await joinPath([
- JSONConversationalExtension._homeDir,
+ JSONConversationalExtension._threadFolder,
threadDirName,
JSONConversationalExtension._threadInfoFileName,
]),
@@ -219,14 +219,14 @@ export default class JSONConversationalExtension extends ConversationalExtension
*/
private async getValidThreadDirs(): Promise {
const fileInsideThread: string[] = await fs.readdirSync(
- JSONConversationalExtension._homeDir
+ JSONConversationalExtension._threadFolder
)
const threadDirs: string[] = []
for (let i = 0; i < fileInsideThread.length; i++) {
if (fileInsideThread[i].includes('.DS_Store')) continue
const path = await joinPath([
- JSONConversationalExtension._homeDir,
+ JSONConversationalExtension._threadFolder,
fileInsideThread[i],
])
@@ -246,7 +246,7 @@ export default class JSONConversationalExtension extends ConversationalExtension
async getAllMessages(threadId: string): Promise {
try {
const threadDirPath = await joinPath([
- JSONConversationalExtension._homeDir,
+ JSONConversationalExtension._threadFolder,
threadId,
])
@@ -263,22 +263,17 @@ export default class JSONConversationalExtension extends ConversationalExtension
JSONConversationalExtension._threadMessagesFileName,
])
- const result = await fs
- .readFileSync(messageFilePath, 'utf-8')
- .then((content) =>
- content
- .toString()
- .split('\n')
- .filter((line) => line !== '')
- )
+ let readResult = await fs.readFileSync(messageFilePath, 'utf-8')
+
+ if (typeof readResult === 'object') {
+ readResult = JSON.stringify(readResult)
+ }
+
+ const result = readResult.split('\n').filter((line) => line !== '')
const messages: ThreadMessage[] = []
result.forEach((line: string) => {
- try {
- messages.push(JSON.parse(line) as ThreadMessage)
- } catch (err) {
- console.error(err)
- }
+ messages.push(JSON.parse(line))
})
return messages
} catch (err) {
diff --git a/extensions/inference-nitro-extension/package.json b/extensions/inference-nitro-extension/package.json
index 8ad516ad9..cccfbefd0 100644
--- a/extensions/inference-nitro-extension/package.json
+++ b/extensions/inference-nitro-extension/package.json
@@ -12,9 +12,9 @@
"downloadnitro:darwin": "NITRO_VERSION=$(cat ./bin/version.txt) && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-mac-arm64.tar.gz -e --strip 1 -o ./bin/mac-arm64 && chmod +x ./bin/mac-arm64/nitro && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-mac-amd64.tar.gz -e --strip 1 -o ./bin/mac-x64 && chmod +x ./bin/mac-x64/nitro",
"downloadnitro:win32": "download.bat",
"downloadnitro": "run-script-os",
- "build:publish:darwin": "rimraf *.tgz --glob && npm run build && npm run downloadnitro && ../../.github/scripts/auto-sign.sh && cpx \"bin/**\" \"dist/bin\" && npm pack && cpx *.tgz ../../electron/pre-install",
- "build:publish:win32": "rimraf *.tgz --glob && npm run build && npm run downloadnitro && cpx \"bin/**\" \"dist/bin\" && npm pack && cpx *.tgz ../../electron/pre-install",
- "build:publish:linux": "rimraf *.tgz --glob && npm run build && npm run downloadnitro && cpx \"bin/**\" \"dist/bin\" && npm pack && cpx *.tgz ../../electron/pre-install",
+ "build:publish:darwin": "rimraf *.tgz --glob && npm run build && npm run downloadnitro && ../../.github/scripts/auto-sign.sh && cpx \"bin/**\" \"dist/bin\" && npm pack && cpx *.tgz ../../pre-install",
+ "build:publish:win32": "rimraf *.tgz --glob && npm run build && npm run downloadnitro && cpx \"bin/**\" \"dist/bin\" && npm pack && cpx *.tgz ../../pre-install",
+ "build:publish:linux": "rimraf *.tgz --glob && npm run build && npm run downloadnitro && cpx \"bin/**\" \"dist/bin\" && npm pack && cpx *.tgz ../../pre-install",
"build:publish": "run-script-os"
},
"exports": {
diff --git a/extensions/inference-nitro-extension/src/index.ts b/extensions/inference-nitro-extension/src/index.ts
index 81a0031ac..9e96ad93f 100644
--- a/extensions/inference-nitro-extension/src/index.ts
+++ b/extensions/inference-nitro-extension/src/index.ts
@@ -243,9 +243,6 @@ export default class JanInferenceNitroExtension extends InferenceExtension {
*/
private async onMessageRequest(data: MessageRequest) {
if (data.model?.engine !== InferenceEngine.nitro || !this._currentModel) {
- console.log(
- `Model is not nitro or no model loaded ${data.model?.engine} ${this._currentModel}`
- );
return;
}
diff --git a/extensions/inference-nitro-extension/src/node/execute.ts b/extensions/inference-nitro-extension/src/node/execute.ts
index ca266639c..83b5226d4 100644
--- a/extensions/inference-nitro-extension/src/node/execute.ts
+++ b/extensions/inference-nitro-extension/src/node/execute.ts
@@ -25,12 +25,12 @@ export const executableNitroFile = (): NitroExecutableOptions => {
if (nvidiaInfo["run_mode"] === "cpu") {
binaryFolder = path.join(binaryFolder, "win-cpu");
} else {
- if (nvidiaInfo["cuda"].version === "12") {
- binaryFolder = path.join(binaryFolder, "win-cuda-12-0");
- } else {
+ if (nvidiaInfo["cuda"].version === "11") {
binaryFolder = path.join(binaryFolder, "win-cuda-11-7");
+ } else {
+ binaryFolder = path.join(binaryFolder, "win-cuda-12-0");
}
- cudaVisibleDevices = nvidiaInfo["gpu_highest_vram"];
+ cudaVisibleDevices = nvidiaInfo["gpus_in_use"].join(",");
}
binaryName = "nitro.exe";
} else if (process.platform === "darwin") {
@@ -50,12 +50,12 @@ export const executableNitroFile = (): NitroExecutableOptions => {
if (nvidiaInfo["run_mode"] === "cpu") {
binaryFolder = path.join(binaryFolder, "linux-cpu");
} else {
- if (nvidiaInfo["cuda"].version === "12") {
- binaryFolder = path.join(binaryFolder, "linux-cuda-12-0");
- } else {
+ if (nvidiaInfo["cuda"].version === "11") {
binaryFolder = path.join(binaryFolder, "linux-cuda-11-7");
+ } else {
+ binaryFolder = path.join(binaryFolder, "linux-cuda-12-0");
}
- cudaVisibleDevices = nvidiaInfo["gpu_highest_vram"];
+ cudaVisibleDevices = nvidiaInfo["gpus_in_use"].join(",");
}
}
return {
diff --git a/extensions/inference-nitro-extension/src/node/nvidia.ts b/extensions/inference-nitro-extension/src/node/nvidia.ts
index 13e43290b..bed2856a1 100644
--- a/extensions/inference-nitro-extension/src/node/nvidia.ts
+++ b/extensions/inference-nitro-extension/src/node/nvidia.ts
@@ -19,6 +19,8 @@ const DEFALT_SETTINGS = {
},
gpus: [],
gpu_highest_vram: "",
+ gpus_in_use: [],
+ is_initial: true,
};
/**
@@ -48,11 +50,15 @@ export interface NitroProcessInfo {
*/
export async function updateNvidiaInfo() {
if (process.platform !== "darwin") {
- await Promise.all([
- updateNvidiaDriverInfo(),
- updateCudaExistence(),
- updateGpuInfo(),
- ]);
+ let data;
+ try {
+ data = JSON.parse(readFileSync(NVIDIA_INFO_FILE, "utf-8"));
+ } catch (error) {
+ data = DEFALT_SETTINGS;
+ writeFileSync(NVIDIA_INFO_FILE, JSON.stringify(data, null, 2));
+ }
+ updateNvidiaDriverInfo();
+ updateGpuInfo();
}
}
@@ -73,12 +79,7 @@ export async function updateNvidiaDriverInfo(): Promise {
exec(
"nvidia-smi --query-gpu=driver_version --format=csv,noheader",
(error, stdout) => {
- let data;
- try {
- data = JSON.parse(readFileSync(NVIDIA_INFO_FILE, "utf-8"));
- } catch (error) {
- data = DEFALT_SETTINGS;
- }
+ let data = JSON.parse(readFileSync(NVIDIA_INFO_FILE, "utf-8"));
if (!error) {
const firstLine = stdout.split("\n")[0].trim();
@@ -107,7 +108,7 @@ export function checkFileExistenceInPaths(
/**
* Validate cuda for linux and windows
*/
-export function updateCudaExistence() {
+export function updateCudaExistence(data: Record = DEFALT_SETTINGS): Record {
let filesCuda12: string[];
let filesCuda11: string[];
let paths: string[];
@@ -141,19 +142,14 @@ export function updateCudaExistence() {
cudaVersion = "12";
}
- let data;
- try {
- data = JSON.parse(readFileSync(NVIDIA_INFO_FILE, "utf-8"));
- } catch (error) {
- data = DEFALT_SETTINGS;
- }
-
data["cuda"].exist = cudaExists;
data["cuda"].version = cudaVersion;
- if (cudaExists) {
+ console.log(data["is_initial"], data["gpus_in_use"]);
+ if (cudaExists && data["is_initial"] && data["gpus_in_use"].length > 0) {
data.run_mode = "gpu";
}
- writeFileSync(NVIDIA_INFO_FILE, JSON.stringify(data, null, 2));
+ data.is_initial = false;
+ return data;
}
/**
@@ -161,14 +157,9 @@ export function updateCudaExistence() {
*/
export async function updateGpuInfo(): Promise {
exec(
- "nvidia-smi --query-gpu=index,memory.total --format=csv,noheader,nounits",
+ "nvidia-smi --query-gpu=index,memory.total,name --format=csv,noheader,nounits",
(error, stdout) => {
- let data;
- try {
- data = JSON.parse(readFileSync(NVIDIA_INFO_FILE, "utf-8"));
- } catch (error) {
- data = DEFALT_SETTINGS;
- }
+ let data = JSON.parse(readFileSync(NVIDIA_INFO_FILE, "utf-8"));
if (!error) {
// Get GPU info and gpu has higher memory first
@@ -178,21 +169,27 @@ export async function updateGpuInfo(): Promise {
.trim()
.split("\n")
.map((line) => {
- let [id, vram] = line.split(", ");
+ let [id, vram, name] = line.split(", ");
vram = vram.replace(/\r/g, "");
if (parseFloat(vram) > highestVram) {
highestVram = parseFloat(vram);
highestVramId = id;
}
- return { id, vram };
+ return { id, vram, name };
});
- data["gpus"] = gpus;
- data["gpu_highest_vram"] = highestVramId;
+ data.gpus = gpus;
+ data.gpu_highest_vram = highestVramId;
} else {
- data["gpus"] = [];
+ data.gpus = [];
+ data.gpu_highest_vram = "";
}
+ if (!data["gpus_in_use"] || data["gpus_in_use"].length === 0) {
+ data.gpus_in_use = [data["gpu_highest_vram"]];
+ }
+
+ data = updateCudaExistence(data);
writeFileSync(NVIDIA_INFO_FILE, JSON.stringify(data, null, 2));
Promise.resolve();
}
diff --git a/extensions/inference-openai-extension/package.json b/extensions/inference-openai-extension/package.json
index 5fa0ce974..0ba6f18db 100644
--- a/extensions/inference-openai-extension/package.json
+++ b/extensions/inference-openai-extension/package.json
@@ -8,7 +8,7 @@
"license": "AGPL-3.0",
"scripts": {
"build": "tsc -b . && webpack --config webpack.config.js",
- "build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/pre-install"
+ "build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../pre-install"
},
"exports": {
".": "./dist/index.js",
diff --git a/extensions/inference-triton-trtllm-extension/package.json b/extensions/inference-triton-trtllm-extension/package.json
index 1d27f9f18..0f4c2de23 100644
--- a/extensions/inference-triton-trtllm-extension/package.json
+++ b/extensions/inference-triton-trtllm-extension/package.json
@@ -8,7 +8,7 @@
"license": "AGPL-3.0",
"scripts": {
"build": "tsc -b . && webpack --config webpack.config.js",
- "build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/pre-install"
+ "build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../pre-install"
},
"exports": {
".": "./dist/index.js",
diff --git a/extensions/model-extension/package.json b/extensions/model-extension/package.json
index 86f177d14..1af5d38cb 100644
--- a/extensions/model-extension/package.json
+++ b/extensions/model-extension/package.json
@@ -8,7 +8,7 @@
"license": "AGPL-3.0",
"scripts": {
"build": "tsc -b . && webpack --config webpack.config.js",
- "build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/pre-install"
+ "build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../pre-install"
},
"devDependencies": {
"cpx": "^1.5.0",
diff --git a/extensions/model-extension/src/@types/global.d.ts b/extensions/model-extension/src/@types/global.d.ts
index e998455f2..7a9202a62 100644
--- a/extensions/model-extension/src/@types/global.d.ts
+++ b/extensions/model-extension/src/@types/global.d.ts
@@ -1,3 +1,15 @@
-declare const EXTENSION_NAME: string
-declare const MODULE_PATH: string
-declare const VERSION: stringå
+export {}
+declare global {
+ declare const EXTENSION_NAME: string
+ declare const MODULE_PATH: string
+ declare const VERSION: string
+
+ interface Core {
+ api: APIFunctions
+ events: EventEmitter
+ }
+ interface Window {
+ core?: Core | undefined
+ electronAPI?: any | undefined
+ }
+}
diff --git a/extensions/model-extension/src/helpers/path.ts b/extensions/model-extension/src/helpers/path.ts
new file mode 100644
index 000000000..cbb151aa6
--- /dev/null
+++ b/extensions/model-extension/src/helpers/path.ts
@@ -0,0 +1,11 @@
+/**
+ * try to retrieve the download file name from the source url
+ */
+
+export function extractFileName(url: string, fileExtension: string): string {
+ const extractedFileName = url.split('/').pop()
+ const fileName = extractedFileName.toLowerCase().endsWith(fileExtension)
+ ? extractedFileName
+ : extractedFileName + fileExtension
+ return fileName
+}
diff --git a/extensions/model-extension/src/index.ts b/extensions/model-extension/src/index.ts
index 5640177a0..e26fd4929 100644
--- a/extensions/model-extension/src/index.ts
+++ b/extensions/model-extension/src/index.ts
@@ -8,7 +8,13 @@ import {
ModelExtension,
Model,
getJanDataFolderPath,
+ events,
+ DownloadEvent,
+ DownloadRoute,
+ ModelEvent,
} from '@janhq/core'
+import { DownloadState } from '@janhq/core/.'
+import { extractFileName } from './helpers/path'
/**
* A extension for models
@@ -29,6 +35,8 @@ export default class JanModelExtension extends ModelExtension {
*/
async onLoad() {
this.copyModelsToHomeDir()
+ // Handle Desktop Events
+ this.handleDesktopEvents()
}
/**
@@ -61,6 +69,8 @@ export default class JanModelExtension extends ModelExtension {
// Finished migration
localStorage.setItem(`${EXTENSION_NAME}-version`, VERSION)
+
+ events.emit(ModelEvent.OnModelsUpdate, {})
} catch (err) {
console.error(err)
}
@@ -83,31 +93,66 @@ export default class JanModelExtension extends ModelExtension {
if (model.sources.length > 1) {
// path to model binaries
for (const source of model.sources) {
- let path = this.extractFileName(source.url)
+ let path = extractFileName(
+ source.url,
+ JanModelExtension._supportedModelFormat
+ )
if (source.filename) {
path = await joinPath([modelDirPath, source.filename])
}
downloadFile(source.url, path, network)
}
+ // TODO: handle multiple binaries for web later
} else {
- const fileName = this.extractFileName(model.sources[0]?.url)
+ const fileName = extractFileName(
+ model.sources[0]?.url,
+ JanModelExtension._supportedModelFormat
+ )
const path = await joinPath([modelDirPath, fileName])
downloadFile(model.sources[0]?.url, path, network)
+
+ if (window && window.core?.api && window.core.api.baseApiUrl) {
+ this.startPollingDownloadProgress(model.id)
+ }
}
}
/**
- * try to retrieve the download file name from the source url
+ * Specifically for Jan server.
*/
- private extractFileName(url: string): string {
- const extractedFileName = url.split('/').pop()
- const fileName = extractedFileName
- .toLowerCase()
- .endsWith(JanModelExtension._supportedModelFormat)
- ? extractedFileName
- : extractedFileName + JanModelExtension._supportedModelFormat
- return fileName
+ private async startPollingDownloadProgress(modelId: string): Promise {
+ // wait for some seconds before polling
+ await new Promise((resolve) => setTimeout(resolve, 3000))
+
+ return new Promise((resolve) => {
+ const interval = setInterval(async () => {
+ fetch(
+ `${window.core.api.baseApiUrl}/v1/download/${DownloadRoute.getDownloadProgress}/${modelId}`,
+ {
+ method: 'GET',
+ headers: { contentType: 'application/json' },
+ }
+ ).then(async (res) => {
+ const state: DownloadState = await res.json()
+ if (state.downloadState === 'end') {
+ events.emit(DownloadEvent.onFileDownloadSuccess, state)
+ clearInterval(interval)
+ resolve()
+ return
+ }
+
+ if (state.downloadState === 'error') {
+ events.emit(DownloadEvent.onFileDownloadError, state)
+ clearInterval(interval)
+ resolve()
+ return
+ }
+
+ events.emit(DownloadEvent.onFileDownloadUpdate, state)
+ })
+ }, 1000)
+ })
}
/**
@@ -286,6 +331,7 @@ export default class JanModelExtension extends ModelExtension {
* model.json file associated with it.
*
* This function will create a model.json file for the model.
+ * It works only with single binary file model.
*
* @param dirName the director which reside in ~/jan/models but does not have model.json file.
*/
@@ -302,15 +348,14 @@ export default class JanModelExtension extends ModelExtension {
let binaryFileSize: number | undefined = undefined
for (const file of files) {
- if (file.endsWith(JanModelExtension._incompletedModelFileName)) continue
- if (file.endsWith('.json')) continue
-
- const path = await joinPath([JanModelExtension._homeDir, dirName, file])
- const fileStats = await fs.fileStat(path)
- if (fileStats.isDirectory) continue
- binaryFileSize = fileStats.size
- binaryFileName = file
- break
+ if (file.endsWith(JanModelExtension._supportedModelFormat)) {
+ const path = await joinPath([JanModelExtension._homeDir, dirName, file])
+ const fileStats = await fs.fileStat(path)
+ if (fileStats.isDirectory) continue
+ binaryFileSize = fileStats.size
+ binaryFileName = file
+ break
+ }
}
if (!binaryFileName) {
@@ -318,7 +363,7 @@ export default class JanModelExtension extends ModelExtension {
return
}
- const defaultModel = await this.getDefaultModel()
+ const defaultModel = (await this.getDefaultModel()) as Model
if (!defaultModel) {
console.error('Unable to find default model')
return
@@ -326,8 +371,19 @@ export default class JanModelExtension extends ModelExtension {
const model: Model = {
...defaultModel,
+ // Overwrite default N/A fields
id: dirName,
name: dirName,
+ sources: [
+ {
+ url: binaryFileName,
+ filename: binaryFileName,
+ },
+ ],
+ settings: {
+ ...defaultModel.settings,
+ llama_model_path: binaryFileName,
+ },
created: Date.now(),
description: `${dirName} - user self import model`,
metadata: {
@@ -371,4 +427,28 @@ export default class JanModelExtension extends ModelExtension {
async getConfiguredModels(): Promise {
return this.getModelsMetadata()
}
+
+ handleDesktopEvents() {
+ if (window && window.electronAPI) {
+ window.electronAPI.onFileDownloadUpdate(
+ async (_event: string, state: any | undefined) => {
+ if (!state) return
+ state.downloadState = 'update'
+ events.emit(DownloadEvent.onFileDownloadUpdate, state)
+ }
+ )
+ window.electronAPI.onFileDownloadError(
+ async (_event: string, state: any) => {
+ state.downloadState = 'error'
+ events.emit(DownloadEvent.onFileDownloadError, state)
+ }
+ )
+ window.electronAPI.onFileDownloadSuccess(
+ async (_event: string, state: any) => {
+ state.downloadState = 'end'
+ events.emit(DownloadEvent.onFileDownloadSuccess, state)
+ }
+ )
+ }
+ }
}
diff --git a/extensions/model-extension/tsconfig.json b/extensions/model-extension/tsconfig.json
index addd8e127..c175d9437 100644
--- a/extensions/model-extension/tsconfig.json
+++ b/extensions/model-extension/tsconfig.json
@@ -8,7 +8,7 @@
"forceConsistentCasingInFileNames": true,
"strict": false,
"skipLibCheck": true,
- "rootDir": "./src"
+ "rootDir": "./src",
},
- "include": ["./src"]
+ "include": ["./src"],
}
diff --git a/extensions/model-extension/webpack.config.js b/extensions/model-extension/webpack.config.js
index 347719f91..c67bf8dc0 100644
--- a/extensions/model-extension/webpack.config.js
+++ b/extensions/model-extension/webpack.config.js
@@ -19,7 +19,7 @@ module.exports = {
new webpack.DefinePlugin({
EXTENSION_NAME: JSON.stringify(packageJson.name),
MODULE_PATH: JSON.stringify(`${packageJson.name}/${packageJson.module}`),
- VERSION: JSON.stringify(packageJson.version),
+ VERSION: JSON.stringify(packageJson.version)
}),
],
output: {
diff --git a/extensions/monitoring-extension/package.json b/extensions/monitoring-extension/package.json
index 9935e536e..538f6bdee 100644
--- a/extensions/monitoring-extension/package.json
+++ b/extensions/monitoring-extension/package.json
@@ -1,6 +1,6 @@
{
"name": "@janhq/monitoring-extension",
- "version": "1.0.9",
+ "version": "1.0.10",
"description": "This extension provides system health and OS level data",
"main": "dist/index.js",
"module": "dist/module.js",
@@ -8,7 +8,7 @@
"license": "AGPL-3.0",
"scripts": {
"build": "tsc -b . && webpack --config webpack.config.js",
- "build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/pre-install"
+ "build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../pre-install"
},
"devDependencies": {
"rimraf": "^3.0.2",
@@ -26,6 +26,7 @@
"README.md"
],
"bundleDependencies": [
- "node-os-utils"
+ "node-os-utils",
+ "@janhq/core"
]
}
diff --git a/extensions/monitoring-extension/src/module.ts b/extensions/monitoring-extension/src/module.ts
index 86b553d52..2c1b14343 100644
--- a/extensions/monitoring-extension/src/module.ts
+++ b/extensions/monitoring-extension/src/module.ts
@@ -1,4 +1,14 @@
const nodeOsUtils = require("node-os-utils");
+const getJanDataFolderPath = require("@janhq/core/node").getJanDataFolderPath;
+const path = require("path");
+const { readFileSync } = require("fs");
+const exec = require("child_process").exec;
+
+const NVIDIA_INFO_FILE = path.join(
+ getJanDataFolderPath(),
+ "settings",
+ "settings.json"
+);
const getResourcesInfo = () =>
new Promise((resolve) => {
@@ -16,18 +26,48 @@ const getResourcesInfo = () =>
});
const getCurrentLoad = () =>
- new Promise((resolve) => {
+ new Promise((resolve, reject) => {
nodeOsUtils.cpu.usage().then((cpuPercentage) => {
- const response = {
- cpu: {
- usage: cpuPercentage,
- },
+ let data = {
+ run_mode: "cpu",
+ gpus_in_use: [],
};
- resolve(response);
+ if (process.platform !== "darwin") {
+ data = JSON.parse(readFileSync(NVIDIA_INFO_FILE, "utf-8"));
+ }
+ if (data.run_mode === "gpu" && data.gpus_in_use.length > 0) {
+ const gpuIds = data["gpus_in_use"].join(",");
+ if (gpuIds !== "") {
+ exec(
+ `nvidia-smi --query-gpu=index,name,temperature.gpu,utilization.gpu,memory.total,memory.free,utilization.memory --format=csv,noheader,nounits --id=${gpuIds}`,
+ (error, stdout, stderr) => {
+ if (error) {
+ console.error(`exec error: ${error}`);
+ reject(error);
+ return;
+ }
+ const gpuInfo = stdout.trim().split("\n").map((line) => {
+ const [id, name, temperature, utilization, memoryTotal, memoryFree, memoryUtilization] = line.split(", ").map(item => item.replace(/\r/g, ""));
+ return { id, name, temperature, utilization, memoryTotal, memoryFree, memoryUtilization };
+ });
+ resolve({
+ cpu: { usage: cpuPercentage },
+ gpu: gpuInfo
+ });
+ }
+ );
+ } else {
+ // Handle the case where gpuIds is empty
+ resolve({ cpu: { usage: cpuPercentage }, gpu: [] });
+ }
+ } else {
+ // Handle the case where run_mode is not 'gpu' or no GPUs are in use
+ resolve({ cpu: { usage: cpuPercentage }, gpu: [] });
+ }
});
});
module.exports = {
getResourcesInfo,
getCurrentLoad,
-};
+};
\ No newline at end of file
diff --git a/package.json b/package.json
index 4b8bc4af0..957934fda 100644
--- a/package.json
+++ b/package.json
@@ -21,22 +21,23 @@
"lint": "yarn workspace jan lint && yarn workspace jan-web lint",
"test:unit": "yarn workspace @janhq/core test",
"test": "yarn workspace jan test:e2e",
- "copy:assets": "cpx \"models/**\" \"electron/models/\" && cpx \"docs/openapi/**\" \"electron/docs/openapi\"",
+ "copy:assets": "cpx \"models/**\" \"electron/models/\" && cpx \"pre-install/*.tgz\" \"electron/pre-install/\" && cpx \"docs/openapi/**\" \"electron/docs/openapi\"",
"dev:electron": "yarn copy:assets && yarn workspace jan dev",
"dev:web": "yarn workspace jan-web dev",
- "dev:server": "yarn workspace @janhq/server dev",
+ "dev:server": "yarn copy:assets && yarn workspace @janhq/server dev",
"dev": "concurrently --kill-others \"yarn dev:web\" \"wait-on http://localhost:3000 && yarn dev:electron\"",
"test-local": "yarn lint && yarn build:test && yarn test",
"dev:uikit": "yarn workspace @janhq/uikit install && yarn workspace @janhq/uikit dev",
"build:uikit": "yarn workspace @janhq/uikit install && yarn workspace @janhq/uikit build",
- "build:server": "cd server && yarn install && yarn run build",
+ "build:server": "yarn copy:assets && cd server && yarn install && yarn run build",
"build:core": "cd core && yarn install && yarn run build",
"build:web": "yarn workspace jan-web build && cpx \"web/out/**\" \"electron/renderer/\"",
"build:electron": "yarn copy:assets && yarn workspace jan build",
"build:electron:test": "yarn workspace jan build:test",
- "build:extensions:windows": "rimraf ./electron/pre-install/*.tgz && powershell -command \"$jobs = Get-ChildItem -Path './extensions' -Directory | ForEach-Object { Start-Job -Name ($_.Name) -ScriptBlock { param($_dir); try { Set-Location $_dir; npm install; npm run build:publish; Write-Output 'Build successful in ' + $_dir } catch { Write-Error 'Error in ' + $_dir; throw } } -ArgumentList $_.FullName }; $jobs | Wait-Job; $jobs | ForEach-Object { Receive-Job -Job $_ -Keep } | ForEach-Object { Write-Host $_ }; $failed = $jobs | Where-Object { $_.State -ne 'Completed' -or $_.ChildJobs[0].JobStateInfo.State -ne 'Completed' }; if ($failed) { Exit 1 }\"",
- "build:extensions:linux": "rimraf ./electron/pre-install/*.tgz && find ./extensions -mindepth 1 -maxdepth 1 -type d -print0 | xargs -0 -n 1 -P 4 -I {} sh -c 'cd {} && npm install && npm run build:publish'",
- "build:extensions:darwin": "rimraf ./electron/pre-install/*.tgz && find ./extensions -mindepth 1 -maxdepth 1 -type d -print0 | xargs -0 -n 1 -P 4 -I {} sh -c 'cd {} && npm install && npm run build:publish'",
+ "build:extensions:windows": "rimraf ./pre-install/*.tgz && powershell -command \"$jobs = Get-ChildItem -Path './extensions' -Directory | ForEach-Object { Start-Job -Name ($_.Name) -ScriptBlock { param($_dir); try { Set-Location $_dir; npm install; npm run build:publish; Write-Output 'Build successful in ' + $_dir } catch { Write-Error 'Error in ' + $_dir; throw } } -ArgumentList $_.FullName }; $jobs | Wait-Job; $jobs | ForEach-Object { Receive-Job -Job $_ -Keep } | ForEach-Object { Write-Host $_ }; $failed = $jobs | Where-Object { $_.State -ne 'Completed' -or $_.ChildJobs[0].JobStateInfo.State -ne 'Completed' }; if ($failed) { Exit 1 }\"",
+ "build:extensions:linux": "rimraf ./pre-install/*.tgz && find ./extensions -mindepth 1 -maxdepth 1 -type d -print0 | xargs -0 -n 1 -P 4 -I {} sh -c 'cd {} && npm install && npm run build:publish'",
+ "build:extensions:darwin": "rimraf ./pre-install/*.tgz && find ./extensions -mindepth 1 -maxdepth 1 -type d -print0 | xargs -0 -n 1 -P 4 -I {} sh -c 'cd {} && npm install && npm run build:publish'",
+ "build:extensions:server": "yarn workspace build:extensions ",
"build:extensions": "run-script-os",
"build:test": "yarn copy:assets && yarn build:web && yarn workspace jan build:test",
"build": "yarn build:web && yarn build:electron",
diff --git a/pre-install/.gitkeep b/pre-install/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/server/helpers/setup.ts b/server/helpers/setup.ts
new file mode 100644
index 000000000..51d8eebe5
--- /dev/null
+++ b/server/helpers/setup.ts
@@ -0,0 +1,47 @@
+import { join, extname } from "path";
+import { existsSync, readdirSync, writeFileSync, mkdirSync } from "fs";
+import { init, installExtensions } from "@janhq/core/node";
+
+export async function setup() {
+ /**
+ * Setup Jan Data Directory
+ */
+ const appDir = process.env.JAN_DATA_DIRECTORY ?? join(__dirname, "..", "jan");
+
+ console.debug(`Create app data directory at ${appDir}...`);
+ if (!existsSync(appDir)) mkdirSync(appDir);
+ //@ts-ignore
+ global.core = {
+ // Define appPath function for app to retrieve app path globaly
+ appPath: () => appDir,
+ };
+ init({
+ extensionsPath: join(appDir, "extensions"),
+ });
+
+ /**
+ * Write app configurations. See #1619
+ */
+ console.debug("Writing config file...");
+ writeFileSync(
+ join(appDir, "settings.json"),
+ JSON.stringify({
+ data_folder: appDir,
+ }),
+ "utf-8"
+ );
+
+ /**
+ * Install extensions
+ */
+
+ console.debug("Installing extensions...");
+
+ const baseExtensionPath = join(__dirname, "../../..", "pre-install");
+ const extensions = readdirSync(baseExtensionPath)
+ .filter((file) => extname(file) === ".tgz")
+ .map((file) => join(baseExtensionPath, file));
+
+ await installExtensions(extensions);
+ console.debug("Extensions installed");
+}
diff --git a/server/index.ts b/server/index.ts
index 05bfdca96..91349a81f 100644
--- a/server/index.ts
+++ b/server/index.ts
@@ -38,6 +38,7 @@ export interface ServerConfig {
isVerboseEnabled?: boolean;
schemaPath?: string;
baseDir?: string;
+ storageAdataper?: any;
}
/**
@@ -103,9 +104,12 @@ export const startServer = async (configs?: ServerConfig) => {
{ prefix: "extensions" }
);
+ // Register proxy middleware
+ if (configs?.storageAdataper)
+ server.addHook("preHandler", configs.storageAdataper);
+
// Register API routes
await server.register(v1Router, { prefix: "/v1" });
-
// Start listening for requests
await server
.listen({
diff --git a/server/main.ts b/server/main.ts
index c3eb69135..3be397e6f 100644
--- a/server/main.ts
+++ b/server/main.ts
@@ -1,3 +1,7 @@
-import { startServer } from "./index";
-
-startServer();
+import { s3 } from "./middleware/s3";
+import { setup } from "./helpers/setup";
+import { startServer as start } from "./index";
+/**
+ * Setup extensions and start the server
+ */
+setup().then(() => start({ storageAdataper: s3 }));
diff --git a/server/middleware/s3.ts b/server/middleware/s3.ts
new file mode 100644
index 000000000..624865222
--- /dev/null
+++ b/server/middleware/s3.ts
@@ -0,0 +1,70 @@
+import { join } from "path";
+
+// Middleware to intercept requests and proxy if certain conditions are met
+const config = {
+ endpoint: process.env.AWS_ENDPOINT,
+ region: process.env.AWS_REGION,
+ credentials: {
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID,
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
+ },
+};
+
+const S3_BUCKET_NAME = process.env.S3_BUCKET_NAME;
+
+const fs = require("@cyclic.sh/s3fs")(S3_BUCKET_NAME, config);
+const PROXY_PREFIX = "/v1/fs";
+const PROXY_ROUTES = ["/threads", "/messages"];
+
+export const s3 = (req: any, reply: any, done: any) => {
+ // Proxy FS requests to S3 using S3FS
+ if (req.url.startsWith(PROXY_PREFIX)) {
+ const route = req.url.split("/").pop();
+ const args = parseRequestArgs(req);
+
+ // Proxy matched requests to the s3fs module
+ if (args.length && PROXY_ROUTES.some((route) => args[0].includes(route))) {
+ try {
+ // Handle customized route
+ // S3FS does not handle appendFileSync
+ if (route === "appendFileSync") {
+ let result = handAppendFileSync(args);
+
+ reply.status(200).send(result);
+ return;
+ }
+ // Reroute the other requests to the s3fs module
+ const result = fs[route](...args);
+ reply.status(200).send(result);
+ return;
+ } catch (ex) {
+ console.log(ex);
+ }
+ }
+ }
+ // Let other requests go through
+ done();
+};
+
+const parseRequestArgs = (req: Request) => {
+ const {
+ getJanDataFolderPath,
+ normalizeFilePath,
+ } = require("@janhq/core/node");
+
+ return JSON.parse(req.body as any).map((arg: any) =>
+ typeof arg === "string" &&
+ (arg.startsWith(`file:/`) || arg.startsWith(`file:\\`))
+ ? join(getJanDataFolderPath(), normalizeFilePath(arg))
+ : arg
+ );
+};
+
+const handAppendFileSync = (args: any[]) => {
+ if (fs.existsSync(args[0])) {
+ const data = fs.readFileSync(args[0], "utf-8");
+ return fs.writeFileSync(args[0], data + args[1]);
+ } else {
+ return fs.writeFileSync(args[0], args[1]);
+ }
+};
diff --git a/server/nodemon.json b/server/nodemon.json
deleted file mode 100644
index 0ea41ca96..000000000
--- a/server/nodemon.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "watch": ["main.ts", "v1"],
- "ext": "ts, json",
- "exec": "tsc && node ./build/main.js"
-}
\ No newline at end of file
diff --git a/server/package.json b/server/package.json
index f61730da4..c1a104506 100644
--- a/server/package.json
+++ b/server/package.json
@@ -13,16 +13,18 @@
"scripts": {
"lint": "eslint . --ext \".js,.jsx,.ts,.tsx\"",
"test:e2e": "playwright test --workers=1",
- "dev": "tsc --watch & node --watch build/main.js",
- "build": "tsc"
+ "build:core": "cd node_modules/@janhq/core && yarn install && yarn build",
+ "dev": "yarn build:core && tsc --watch & node --watch build/main.js",
+ "build": "yarn build:core && tsc"
},
"dependencies": {
"@alumna/reflect": "^1.1.3",
+ "@cyclic.sh/s3fs": "^1.2.9",
"@fastify/cors": "^8.4.2",
"@fastify/static": "^6.12.0",
"@fastify/swagger": "^8.13.0",
"@fastify/swagger-ui": "2.0.1",
- "@janhq/core": "link:./core",
+ "@janhq/core": "file:../core",
"dotenv": "^16.3.1",
"fastify": "^4.24.3",
"request": "^2.88.2",
@@ -39,5 +41,8 @@
"run-script-os": "^1.1.6",
"@types/tcp-port-used": "^1.0.4",
"typescript": "^5.2.2"
- }
+ },
+ "bundleDependencies": [
+ "@janhq/core"
+ ]
}
diff --git a/server/tsconfig.json b/server/tsconfig.json
index 2c4fc4a64..dd27b8932 100644
--- a/server/tsconfig.json
+++ b/server/tsconfig.json
@@ -20,5 +20,5 @@
// "sourceMap": true,
"include": ["./**/*.ts"],
- "exclude": ["core", "build", "dist", "tests", "node_modules"]
+ "exclude": ["core", "build", "dist", "tests", "node_modules", "extensions"]
}
diff --git a/web/containers/Layout/BottomBar/DownloadingState/index.tsx b/web/containers/Layout/BottomBar/DownloadingState/index.tsx
index 7aef36caf..c7191d0b9 100644
--- a/web/containers/Layout/BottomBar/DownloadingState/index.tsx
+++ b/web/containers/Layout/BottomBar/DownloadingState/index.tsx
@@ -13,22 +13,22 @@ import {
import { useAtomValue } from 'jotai'
import useDownloadModel from '@/hooks/useDownloadModel'
-import { useDownloadState } from '@/hooks/useDownloadState'
+import { modelDownloadStateAtom } from '@/hooks/useDownloadState'
import { formatDownloadPercentage } from '@/utils/converter'
-import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
+import { getDownloadingModelAtom } from '@/helpers/atoms/Model.atom'
export default function DownloadingState() {
- const { downloadStates } = useDownloadState()
- const downloadingModels = useAtomValue(downloadingModelsAtom)
+ const downloadStates = useAtomValue(modelDownloadStateAtom)
+ const downloadingModels = useAtomValue(getDownloadingModelAtom)
const { abortModelDownload } = useDownloadModel()
- const totalCurrentProgress = downloadStates
+ const totalCurrentProgress = Object.values(downloadStates)
.map((a) => a.size.transferred + a.size.transferred)
.reduce((partialSum, a) => partialSum + a, 0)
- const totalSize = downloadStates
+ const totalSize = Object.values(downloadStates)
.map((a) => a.size.total + a.size.total)
.reduce((partialSum, a) => partialSum + a, 0)
@@ -36,12 +36,14 @@ export default function DownloadingState() {
return (
- {downloadStates?.length > 0 && (
+ {Object.values(downloadStates)?.length > 0 && (
Downloading model
- {downloadStates.map((item, i) => {
- return (
-
-
-
-
- {item?.modelId}
- {formatDownloadPercentage(item?.percent)}
-
-
+ {Object.values(downloadStates).map((item, i) => (
+
+
+
+
+ {item?.modelId}
+ {formatDownloadPercentage(item?.percent)}
+
- )
- })}
+
+ ))}
)}
diff --git a/web/containers/Layout/BottomBar/index.tsx b/web/containers/Layout/BottomBar/index.tsx
index 6e334b9ef..5021b821c 100644
--- a/web/containers/Layout/BottomBar/index.tsx
+++ b/web/containers/Layout/BottomBar/index.tsx
@@ -25,12 +25,12 @@ import { MainViewState } from '@/constants/screens'
import { useActiveModel } from '@/hooks/useActiveModel'
-import { useDownloadState } from '@/hooks/useDownloadState'
-import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
+import { modelDownloadStateAtom } from '@/hooks/useDownloadState'
import useGetSystemResources from '@/hooks/useGetSystemResources'
import { useMainViewState } from '@/hooks/useMainViewState'
import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom'
+import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
const menuLinks = [
{
@@ -47,14 +47,22 @@ const menuLinks = [
const BottomBar = () => {
const { activeModel, stateModel } = useActiveModel()
- const { ram, cpu } = useGetSystemResources()
+ const { ram, cpu, gpus } = useGetSystemResources()
const progress = useAtomValue(appDownloadProgress)
- const { downloadedModels } = useGetDownloadedModels()
+ const downloadedModels = useAtomValue(downloadedModelsAtom)
+
const { setMainViewState } = useMainViewState()
- const { downloadStates } = useDownloadState()
+ const downloadStates = useAtomValue(modelDownloadStateAtom)
const setShowSelectModelModal = useSetAtom(showSelectModelModalAtom)
const [serverEnabled] = useAtom(serverEnabledAtom)
+ const calculateGpuMemoryUsage = (gpu: Record ) => {
+ const total = parseInt(gpu.memoryTotal)
+ const free = parseInt(gpu.memoryFree)
+ if (!total || !free) return 0
+ return Math.round(((total - free) / total) * 100)
+ }
+
return (
@@ -100,7 +108,7 @@ const BottomBar = () => {
)}
{downloadedModels.length === 0 &&
!stateModel.loading &&
- downloadStates.length === 0 && (
+ Object.values(downloadStates).length === 0 && (
+ {gpus.length > 0 && (
+
+ {gpus.map((gpu, index) => (
+
+ ))}
+
+ )}
{/* VERSION is defined by webpack, please see next.config.js */}
Jan v{VERSION ?? ''}
diff --git a/web/containers/Layout/TopBar/CommandListDownloadedModel/index.tsx b/web/containers/Layout/TopBar/CommandListDownloadedModel/index.tsx
index 3edce06eb..ac5756e9f 100644
--- a/web/containers/Layout/TopBar/CommandListDownloadedModel/index.tsx
+++ b/web/containers/Layout/TopBar/CommandListDownloadedModel/index.tsx
@@ -11,7 +11,7 @@ import {
Badge,
} from '@janhq/uikit'
-import { useAtom } from 'jotai'
+import { useAtom, useAtomValue } from 'jotai'
import { DatabaseIcon, CpuIcon } from 'lucide-react'
import { showSelectModelModalAtom } from '@/containers/Providers/KeyListener'
@@ -19,14 +19,14 @@ import { showSelectModelModalAtom } from '@/containers/Providers/KeyListener'
import { MainViewState } from '@/constants/screens'
import { useActiveModel } from '@/hooks/useActiveModel'
-import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
import { useMainViewState } from '@/hooks/useMainViewState'
import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom'
+import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
export default function CommandListDownloadedModel() {
const { setMainViewState } = useMainViewState()
- const { downloadedModels } = useGetDownloadedModels()
+ const downloadedModels = useAtomValue(downloadedModelsAtom)
const { activeModel, startModel, stopModel } = useActiveModel()
const [serverEnabled] = useAtom(serverEnabledAtom)
const [showSelectModelModal, setShowSelectModelModal] = useAtom(
diff --git a/web/containers/Layout/TopBar/index.tsx b/web/containers/Layout/TopBar/index.tsx
index f72f5f066..206a9013d 100644
--- a/web/containers/Layout/TopBar/index.tsx
+++ b/web/containers/Layout/TopBar/index.tsx
@@ -20,7 +20,6 @@ import { MainViewState } from '@/constants/screens'
import { useClickOutside } from '@/hooks/useClickOutside'
import { useCreateNewThread } from '@/hooks/useCreateNewThread'
-import useGetAssistants, { getAssistants } from '@/hooks/useGetAssistants'
import { useMainViewState } from '@/hooks/useMainViewState'
import { usePath } from '@/hooks/usePath'
@@ -29,13 +28,14 @@ import { showRightSideBarAtom } from '@/screens/Chat/Sidebar'
import { openFileTitle } from '@/utils/titleUtils'
+import { assistantsAtom } from '@/helpers/atoms/Assistant.atom'
import { activeThreadAtom } from '@/helpers/atoms/Thread.atom'
const TopBar = () => {
const activeThread = useAtomValue(activeThreadAtom)
const { mainViewState } = useMainViewState()
const { requestCreateNewThread } = useCreateNewThread()
- const { assistants } = useGetAssistants()
+ const assistants = useAtomValue(assistantsAtom)
const [showRightSideBar, setShowRightSideBar] = useAtom(showRightSideBarAtom)
const [showLeftSideBar, setShowLeftSideBar] = useAtom(showLeftSideBarAtom)
const showing = useAtomValue(showRightSideBarAtom)
@@ -61,12 +61,7 @@ const TopBar = () => {
const onCreateConversationClick = async () => {
if (assistants.length === 0) {
- const res = await getAssistants()
- if (res.length === 0) {
- alert('No assistant available')
- return
- }
- requestCreateNewThread(res[0])
+ alert('No assistant available')
} else {
requestCreateNewThread(assistants[0])
}
diff --git a/web/containers/ModalCancelDownload/index.tsx b/web/containers/ModalCancelDownload/index.tsx
index 2a5626183..8d08665f4 100644
--- a/web/containers/ModalCancelDownload/index.tsx
+++ b/web/containers/ModalCancelDownload/index.tsx
@@ -17,23 +17,22 @@ import {
import { atom, useAtomValue } from 'jotai'
import useDownloadModel from '@/hooks/useDownloadModel'
-import { useDownloadState } from '@/hooks/useDownloadState'
+
+import { modelDownloadStateAtom } from '@/hooks/useDownloadState'
import { formatDownloadPercentage } from '@/utils/converter'
-import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
+import { getDownloadingModelAtom } from '@/helpers/atoms/Model.atom'
type Props = {
model: Model
isFromList?: boolean
}
-export default function ModalCancelDownload({ model, isFromList }: Props) {
- const { modelDownloadStateAtom } = useDownloadState()
- const downloadingModels = useAtomValue(downloadingModelsAtom)
+const ModalCancelDownload: React.FC = ({ model, isFromList }) => {
+ const downloadingModels = useAtomValue(getDownloadingModelAtom)
const downloadAtom = useMemo(
() => atom((get) => get(modelDownloadStateAtom)[model.id]),
- // eslint-disable-next-line react-hooks/exhaustive-deps
[model.id]
)
const downloadState = useAtomValue(downloadAtom)
@@ -98,3 +97,5 @@ export default function ModalCancelDownload({ model, isFromList }: Props) {
)
}
+
+export default ModalCancelDownload
diff --git a/web/containers/Providers/DataLoader.tsx b/web/containers/Providers/DataLoader.tsx
new file mode 100644
index 000000000..2b6675d98
--- /dev/null
+++ b/web/containers/Providers/DataLoader.tsx
@@ -0,0 +1,21 @@
+'use client'
+
+import { Fragment, ReactNode } from 'react'
+
+import useAssistants from '@/hooks/useAssistants'
+import useModels from '@/hooks/useModels'
+import useThreads from '@/hooks/useThreads'
+
+type Props = {
+ children: ReactNode
+}
+
+const DataLoader: React.FC = ({ children }) => {
+ useModels()
+ useThreads()
+ useAssistants()
+
+ return {children}
+}
+
+export default DataLoader
diff --git a/web/containers/Providers/EventHandler.tsx b/web/containers/Providers/EventHandler.tsx
index ec0fbfc90..cfd2c5629 100644
--- a/web/containers/Providers/EventHandler.tsx
+++ b/web/containers/Providers/EventHandler.tsx
@@ -18,7 +18,6 @@ import {
loadModelErrorAtom,
stateModelAtom,
} from '@/hooks/useActiveModel'
-import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
import { queuedMessageAtom } from '@/hooks/useSendChatMessage'
@@ -29,16 +28,18 @@ import {
addNewMessageAtom,
updateMessageAtom,
} from '@/helpers/atoms/ChatMessage.atom'
+import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
import {
updateThreadWaitingForResponseAtom,
threadsAtom,
isGeneratingResponseAtom,
+ updateThreadAtom,
} from '@/helpers/atoms/Thread.atom'
export default function EventHandler({ children }: { children: ReactNode }) {
const addNewMessage = useSetAtom(addNewMessageAtom)
const updateMessage = useSetAtom(updateMessageAtom)
- const { downloadedModels } = useGetDownloadedModels()
+ const downloadedModels = useAtomValue(downloadedModelsAtom)
const setActiveModel = useSetAtom(activeModelAtom)
const setStateModel = useSetAtom(stateModelAtom)
const setQueuedMessage = useSetAtom(queuedMessageAtom)
@@ -49,6 +50,7 @@ export default function EventHandler({ children }: { children: ReactNode }) {
const modelsRef = useRef(downloadedModels)
const threadsRef = useRef(threads)
const setIsGeneratingResponse = useSetAtom(isGeneratingResponseAtom)
+ const updateThread = useSetAtom(updateThreadAtom)
useEffect(() => {
threadsRef.current = threads
@@ -126,11 +128,17 @@ export default function EventHandler({ children }: { children: ReactNode }) {
const thread = threadsRef.current?.find((e) => e.id == message.thread_id)
if (thread) {
- const messageContent = message.content[0]?.text.value ?? ''
+ const messageContent = message.content[0]?.text?.value
const metadata = {
...thread.metadata,
- lastMessage: messageContent,
+ ...(messageContent && { lastMessage: messageContent }),
}
+
+ updateThread({
+ ...thread,
+ metadata,
+ })
+
extensionManager
.get(ExtensionTypeEnum.Conversational)
?.saveThread({
@@ -143,7 +151,7 @@ export default function EventHandler({ children }: { children: ReactNode }) {
?.addNewMessage(message)
}
},
- [updateMessage, updateThreadWaiting]
+ [updateMessage, updateThreadWaiting, setIsGeneratingResponse, updateThread]
)
useEffect(() => {
diff --git a/web/containers/Providers/EventListener.tsx b/web/containers/Providers/EventListener.tsx
index 62d4cacb6..a72faf924 100644
--- a/web/containers/Providers/EventListener.tsx
+++ b/web/containers/Providers/EventListener.tsx
@@ -1,91 +1,62 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
+import { PropsWithChildren, useCallback, useEffect } from 'react'
-import { PropsWithChildren, useEffect, useRef } from 'react'
+import React from 'react'
-import { baseName } from '@janhq/core'
-import { useAtomValue, useSetAtom } from 'jotai'
+import { DownloadEvent, events } from '@janhq/core'
+import { useSetAtom } from 'jotai'
-import { useDownloadState } from '@/hooks/useDownloadState'
-import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
-
-import { modelBinFileName } from '@/utils/model'
+import { setDownloadStateAtom } from '@/hooks/useDownloadState'
import EventHandler from './EventHandler'
import { appDownloadProgress } from './Jotai'
-import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
-
-export default function EventListenerWrapper({ children }: PropsWithChildren) {
+const EventListenerWrapper = ({ children }: PropsWithChildren) => {
+ const setDownloadState = useSetAtom(setDownloadStateAtom)
const setProgress = useSetAtom(appDownloadProgress)
- const models = useAtomValue(downloadingModelsAtom)
- const modelsRef = useRef(models)
- const { setDownloadedModels, downloadedModels } = useGetDownloadedModels()
- const {
- setDownloadState,
- setDownloadStateSuccess,
- setDownloadStateFailed,
- setDownloadStateCancelled,
- } = useDownloadState()
- const downloadedModelRef = useRef(downloadedModels)
+ const onFileDownloadUpdate = useCallback(
+ async (state: DownloadState) => {
+ console.debug('onFileDownloadUpdate', state)
+ setDownloadState(state)
+ },
+ [setDownloadState]
+ )
+
+ const onFileDownloadError = useCallback(
+ (state: DownloadState) => {
+ console.debug('onFileDownloadError', state)
+ setDownloadState(state)
+ },
+ [setDownloadState]
+ )
+
+ const onFileDownloadSuccess = useCallback(
+ (state: DownloadState) => {
+ console.debug('onFileDownloadSuccess', state)
+ setDownloadState(state)
+ },
+ [setDownloadState]
+ )
useEffect(() => {
- modelsRef.current = models
- }, [models])
- useEffect(() => {
- downloadedModelRef.current = downloadedModels
- }, [downloadedModels])
+ console.log('EventListenerWrapper: registering event listeners...')
+
+ events.on(DownloadEvent.onFileDownloadUpdate, onFileDownloadUpdate)
+ events.on(DownloadEvent.onFileDownloadError, onFileDownloadError)
+ events.on(DownloadEvent.onFileDownloadSuccess, onFileDownloadSuccess)
+
+ return () => {
+ console.log('EventListenerWrapper: unregistering event listeners...')
+ events.off(DownloadEvent.onFileDownloadUpdate, onFileDownloadUpdate)
+ events.off(DownloadEvent.onFileDownloadError, onFileDownloadError)
+ events.off(DownloadEvent.onFileDownloadSuccess, onFileDownloadSuccess)
+ }
+ }, [onFileDownloadUpdate, onFileDownloadError, onFileDownloadSuccess])
useEffect(() => {
if (window && window.electronAPI) {
- window.electronAPI.onFileDownloadUpdate(
- async (_event: string, state: any | undefined) => {
- if (!state) return
- const modelName = await baseName(state.fileName)
- const model = modelsRef.current.find(
- (model) => modelBinFileName(model) === modelName
- )
- if (model)
- setDownloadState({
- ...state,
- modelId: model.id,
- })
- }
- )
-
- window.electronAPI.onFileDownloadError(
- async (_event: string, state: any) => {
- const modelName = await baseName(state.fileName)
- const model = modelsRef.current.find(
- (model) => modelBinFileName(model) === modelName
- )
- if (model) {
- if (state.err?.message !== 'aborted') {
- console.error('Download error', state)
- setDownloadStateFailed(model.id, state.err.message)
- } else {
- setDownloadStateCancelled(model.id)
- }
- }
- }
- )
-
- window.electronAPI.onFileDownloadSuccess(
- async (_event: string, state: any) => {
- if (state && state.fileName) {
- const modelName = await baseName(state.fileName)
- const model = modelsRef.current.find(
- (model) => modelBinFileName(model) === modelName
- )
- if (model) {
- setDownloadStateSuccess(model.id)
- setDownloadedModels([...downloadedModelRef.current, model])
- }
- }
- }
- )
-
window.electronAPI.onAppUpdateDownloadUpdate(
(_event: string, progress: any) => {
setProgress(progress.percent)
@@ -105,14 +76,9 @@ export default function EventListenerWrapper({ children }: PropsWithChildren) {
})
}
return () => {}
- }, [
- setDownloadState,
- setDownloadStateCancelled,
- setDownloadStateFailed,
- setDownloadStateSuccess,
- setDownloadedModels,
- setProgress,
- ])
+ }, [setDownloadState, setProgress])
return {children}
}
+
+export default EventListenerWrapper
diff --git a/web/containers/Providers/index.tsx b/web/containers/Providers/index.tsx
index 895c22177..e7a179ec4 100644
--- a/web/containers/Providers/index.tsx
+++ b/web/containers/Providers/index.tsx
@@ -21,6 +21,10 @@ import {
import Umami from '@/utils/umami'
+import Loader from '../Loader'
+
+import DataLoader from './DataLoader'
+
import KeyListener from './KeyListener'
import { extensionManager } from '@/extension'
@@ -30,6 +34,7 @@ const Providers = (props: PropsWithChildren) => {
const [setupCore, setSetupCore] = useState(false)
const [activated, setActivated] = useState(false)
+ const [settingUp, setSettingUp] = useState(false)
async function setupExtensions() {
// Register all active extensions
@@ -37,11 +42,13 @@ const Providers = (props: PropsWithChildren) => {
setTimeout(async () => {
if (!isCoreExtensionInstalled()) {
- setupBaseExtensions()
+ setSettingUp(true)
+ await setupBaseExtensions()
return
}
extensionManager.load()
+ setSettingUp(false)
setActivated(true)
}, 500)
}
@@ -71,11 +78,14 @@ const Providers = (props: PropsWithChildren) => {
+ {settingUp && }
{setupCore && activated && (
- {children}
+
+ {children}
+
{!isMac && }
diff --git a/web/helpers/atoms/Assistant.atom.ts b/web/helpers/atoms/Assistant.atom.ts
new file mode 100644
index 000000000..e90923d3d
--- /dev/null
+++ b/web/helpers/atoms/Assistant.atom.ts
@@ -0,0 +1,4 @@
+import { Assistant } from '@janhq/core/.'
+import { atom } from 'jotai'
+
+export const assistantsAtom = atom([])
diff --git a/web/helpers/atoms/ChatMessage.atom.ts b/web/helpers/atoms/ChatMessage.atom.ts
index b11e8f3be..45cc773e6 100644
--- a/web/helpers/atoms/ChatMessage.atom.ts
+++ b/web/helpers/atoms/ChatMessage.atom.ts
@@ -70,11 +70,12 @@ export const addNewMessageAtom = atom(
set(chatMessages, newData)
// Update thread last message
- set(
- updateThreadStateLastMessageAtom,
- newMessage.thread_id,
- newMessage.content
- )
+ if (newMessage.content.length)
+ set(
+ updateThreadStateLastMessageAtom,
+ newMessage.thread_id,
+ newMessage.content
+ )
}
)
@@ -131,7 +132,8 @@ export const updateMessageAtom = atom(
newData[conversationId] = updatedMessages
set(chatMessages, newData)
// Update thread last message
- set(updateThreadStateLastMessageAtom, conversationId, text)
+ if (text.length)
+ set(updateThreadStateLastMessageAtom, conversationId, text)
}
}
)
diff --git a/web/helpers/atoms/Model.atom.ts b/web/helpers/atoms/Model.atom.ts
index 6eb7f2ad6..512518df1 100644
--- a/web/helpers/atoms/Model.atom.ts
+++ b/web/helpers/atoms/Model.atom.ts
@@ -4,23 +4,32 @@ import { atom } from 'jotai'
export const stateModel = atom({ state: 'start', loading: false, model: '' })
export const activeAssistantModelAtom = atom(undefined)
-export const downloadingModelsAtom = atom([])
+/**
+ * Stores the list of models which are being downloaded.
+ */
+const downloadingModelsAtom = atom([])
-export const addNewDownloadingModelAtom = atom(
- null,
- (get, set, model: Model) => {
- const currentModels = get(downloadingModelsAtom)
- set(downloadingModelsAtom, [...currentModels, model])
+export const getDownloadingModelAtom = atom((get) => get(downloadingModelsAtom))
+
+export const addDownloadingModelAtom = atom(null, (get, set, model: Model) => {
+ const downloadingModels = get(downloadingModelsAtom)
+ if (!downloadingModels.find((e) => e.id === model.id)) {
+ set(downloadingModelsAtom, [...downloadingModels, model])
}
-)
+})
export const removeDownloadingModelAtom = atom(
null,
(get, set, modelId: string) => {
- const currentModels = get(downloadingModelsAtom)
+ const downloadingModels = get(downloadingModelsAtom)
+
set(
downloadingModelsAtom,
- currentModels.filter((e) => e.id !== modelId)
+ downloadingModels.filter((e) => e.id !== modelId)
)
}
)
+
+export const downloadedModelsAtom = atom([])
+
+export const configuredModelsAtom = atom([])
diff --git a/web/helpers/atoms/SystemBar.atom.ts b/web/helpers/atoms/SystemBar.atom.ts
index 42ef7b29f..22a7573ec 100644
--- a/web/helpers/atoms/SystemBar.atom.ts
+++ b/web/helpers/atoms/SystemBar.atom.ts
@@ -5,3 +5,5 @@ export const usedRamAtom = atom(0)
export const availableRamAtom = atom(0)
export const cpuUsageAtom = atom(0)
+
+export const nvidiaTotalVramAtom = atom(0)
diff --git a/web/hooks/useActiveModel.ts b/web/hooks/useActiveModel.ts
index 54a1fdbe0..1b61a0dd1 100644
--- a/web/hooks/useActiveModel.ts
+++ b/web/hooks/useActiveModel.ts
@@ -3,9 +3,9 @@ import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'
import { toaster } from '@/containers/Toast'
-import { useGetDownloadedModels } from './useGetDownloadedModels'
import { LAST_USED_MODEL_ID } from './useRecommendedModel'
+import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
import { activeThreadAtom } from '@/helpers/atoms/Thread.atom'
export const activeModelAtom = atom(undefined)
@@ -21,7 +21,7 @@ export function useActiveModel() {
const [activeModel, setActiveModel] = useAtom(activeModelAtom)
const activeThread = useAtomValue(activeThreadAtom)
const [stateModel, setStateModel] = useAtom(stateModelAtom)
- const { downloadedModels } = useGetDownloadedModels()
+ const downloadedModels = useAtomValue(downloadedModelsAtom)
const setLoadModelError = useSetAtom(loadModelErrorAtom)
const startModel = async (modelId: string) => {
diff --git a/web/hooks/useAssistants.ts b/web/hooks/useAssistants.ts
new file mode 100644
index 000000000..61679bce5
--- /dev/null
+++ b/web/hooks/useAssistants.ts
@@ -0,0 +1,39 @@
+import { useCallback, useEffect } from 'react'
+
+import {
+ Assistant,
+ AssistantEvent,
+ AssistantExtension,
+ ExtensionTypeEnum,
+ events,
+} from '@janhq/core'
+
+import { useSetAtom } from 'jotai'
+
+import { extensionManager } from '@/extension'
+import { assistantsAtom } from '@/helpers/atoms/Assistant.atom'
+
+const useAssistants = () => {
+ const setAssistants = useSetAtom(assistantsAtom)
+
+ const getData = useCallback(async () => {
+ const assistants = await getLocalAssistants()
+ setAssistants(assistants)
+ }, [setAssistants])
+
+ useEffect(() => {
+ getData()
+
+ events.on(AssistantEvent.OnAssistantsUpdate, () => getData())
+ return () => {
+ events.off(AssistantEvent.OnAssistantsUpdate, () => getData())
+ }
+ }, [getData])
+}
+
+const getLocalAssistants = async (): Promise =>
+ extensionManager
+ .get(ExtensionTypeEnum.Assistant)
+ ?.getAssistants() ?? []
+
+export default useAssistants
diff --git a/web/hooks/useCreateNewThread.ts b/web/hooks/useCreateNewThread.ts
index ee8df22df..406bf8f74 100644
--- a/web/hooks/useCreateNewThread.ts
+++ b/web/hooks/useCreateNewThread.ts
@@ -6,8 +6,9 @@ import {
ThreadAssistantInfo,
ThreadState,
Model,
+ MessageStatus,
} from '@janhq/core'
-import { atom, useSetAtom } from 'jotai'
+import { atom, useAtomValue, useSetAtom } from 'jotai'
import { selectedModelAtom } from '@/containers/DropdownListSidebar'
import { fileUploadAtom } from '@/containers/Providers/Jotai'
@@ -19,6 +20,8 @@ import useRecommendedModel from './useRecommendedModel'
import useSetActiveThread from './useSetActiveThread'
import { extensionManager } from '@/extension'
+
+import { getCurrentChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
import {
threadsAtom,
threadStatesAtom,
@@ -50,15 +53,25 @@ export const useCreateNewThread = () => {
const setFileUpload = useSetAtom(fileUploadAtom)
const setSelectedModel = useSetAtom(selectedModelAtom)
const setThreadModelParams = useSetAtom(setThreadModelParamsAtom)
+ const messages = useAtomValue(getCurrentChatMessagesAtom)
const { recommendedModel, downloadedModels } = useRecommendedModel()
+ const threads = useAtomValue(threadsAtom)
+
const requestCreateNewThread = async (
assistant: Assistant,
model?: Model | undefined
) => {
const defaultModel = model ?? recommendedModel ?? downloadedModels[0]
+ // check last thread message, if there empty last message use can not create thread
+ const lastMessage = threads[threads.length - 1]?.metadata?.lastMessage
+
+ if (!lastMessage && threads.length && !messages.length) {
+ return null
+ }
+
const createdAt = Date.now()
const assistantInfo: ThreadAssistantInfo = {
assistant_id: assistant.id,
diff --git a/web/hooks/useDeleteModel.ts b/web/hooks/useDeleteModel.ts
index fa0cfb45e..d9f2b94be 100644
--- a/web/hooks/useDeleteModel.ts
+++ b/web/hooks/useDeleteModel.ts
@@ -1,13 +1,14 @@
import { ExtensionTypeEnum, ModelExtension, Model } from '@janhq/core'
+import { useAtom } from 'jotai'
+
import { toaster } from '@/containers/Toast'
-import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
-
import { extensionManager } from '@/extension/ExtensionManager'
+import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
export default function useDeleteModel() {
- const { setDownloadedModels, downloadedModels } = useGetDownloadedModels()
+ const [downloadedModels, setDownloadedModels] = useAtom(downloadedModelsAtom)
const deleteModel = async (model: Model) => {
await extensionManager
diff --git a/web/hooks/useDownloadModel.ts b/web/hooks/useDownloadModel.ts
index 528108d18..3c544a24c 100644
--- a/web/hooks/useDownloadModel.ts
+++ b/web/hooks/useDownloadModel.ts
@@ -1,4 +1,4 @@
-import { useContext } from 'react'
+import { useCallback, useContext } from 'react'
import {
Model,
@@ -15,21 +15,40 @@ import { FeatureToggleContext } from '@/context/FeatureToggle'
import { modelBinFileName } from '@/utils/model'
-import { useDownloadState } from './useDownloadState'
+import { setDownloadStateAtom } from './useDownloadState'
import { extensionManager } from '@/extension/ExtensionManager'
-import { addNewDownloadingModelAtom } from '@/helpers/atoms/Model.atom'
+import { addDownloadingModelAtom } from '@/helpers/atoms/Model.atom'
export default function useDownloadModel() {
const { ignoreSSL, proxy } = useContext(FeatureToggleContext)
- const { setDownloadState } = useDownloadState()
- const addNewDownloadingModel = useSetAtom(addNewDownloadingModelAtom)
+ const setDownloadState = useSetAtom(setDownloadStateAtom)
+ const addDownloadingModel = useSetAtom(addDownloadingModelAtom)
- const downloadModel = async (model: Model) => {
- const childrenDownloadProgress: DownloadState[] = []
- model.sources.forEach((source: ModelArtifact) => {
- childrenDownloadProgress.push({
- modelId: source.filename,
+ const downloadModel = useCallback(
+ async (model: Model) => {
+ const childProgresses: DownloadState[] = model.sources.map(
+ (source: ModelArtifact) => ({
+ filename: source.filename,
+ modelId: model.id,
+ time: {
+ elapsed: 0,
+ remaining: 0,
+ },
+ speed: 0,
+ percent: 0,
+ size: {
+ total: 0,
+ transferred: 0,
+ },
+ downloadState: 'downloading',
+ })
+ )
+
+ // set an initial download state
+ setDownloadState({
+ filename: '',
+ modelId: model.id,
time: {
elapsed: 0,
remaining: 0,
@@ -40,31 +59,16 @@ export default function useDownloadModel() {
total: 0,
transferred: 0,
},
+ children: childProgresses,
+ downloadState: 'downloading',
})
- })
- // set an initial download state
- setDownloadState({
- modelId: model.id,
- time: {
- elapsed: 0,
- remaining: 0,
- },
- speed: 0,
- percent: 0,
- size: {
- total: 0,
- transferred: 0,
- },
- children: childrenDownloadProgress,
- })
+ addDownloadingModel(model)
- addNewDownloadingModel(model)
-
- await extensionManager
- .get(ExtensionTypeEnum.Model)
- ?.downloadModel(model, { ignoreSSL, proxy })
- }
+ await localDownloadModel(model, ignoreSSL, proxy)
+ },
+ [ignoreSSL, proxy, addDownloadingModel, setDownloadState]
+ )
const abortModelDownload = async (model: Model) => {
await abortDownload(
@@ -77,3 +81,12 @@ export default function useDownloadModel() {
abortModelDownload,
}
}
+
+const localDownloadModel = async (
+ model: Model,
+ ignoreSSL: boolean,
+ proxy: string
+) =>
+ extensionManager
+ .get(ExtensionTypeEnum.Model)
+ ?.downloadModel(model, { ignoreSSL, proxy })
diff --git a/web/hooks/useDownloadState.ts b/web/hooks/useDownloadState.ts
index 37f41d2a1..207cca69f 100644
--- a/web/hooks/useDownloadState.ts
+++ b/web/hooks/useDownloadState.ts
@@ -1,96 +1,64 @@
-import { atom, useSetAtom, useAtomValue } from 'jotai'
+import { atom } from 'jotai'
import { toaster } from '@/containers/Toast'
+import {
+ configuredModelsAtom,
+ downloadedModelsAtom,
+ removeDownloadingModelAtom,
+} from '@/helpers/atoms/Model.atom'
+
// download states
-const modelDownloadStateAtom = atom>({})
+export const modelDownloadStateAtom = atom>({})
-const setDownloadStateAtom = atom(null, (get, set, state: DownloadState) => {
- const currentState = { ...get(modelDownloadStateAtom) }
- console.debug(
- `current download state for ${state.modelId} is ${JSON.stringify(state)}`
- )
- currentState[state.modelId] = state
- set(modelDownloadStateAtom, currentState)
-})
-
-const setDownloadStateSuccessAtom = atom(null, (get, set, modelId: string) => {
- const currentState = { ...get(modelDownloadStateAtom) }
- const state = currentState[modelId]
- if (!state) {
- console.debug(`Cannot find download state for ${modelId}`)
- return
- }
- delete currentState[modelId]
- set(modelDownloadStateAtom, currentState)
- toaster({
- title: 'Download Completed',
- description: `Download ${modelId} completed`,
- type: 'success',
- })
-})
-
-const setDownloadStateFailedAtom = atom(
+/**
+ * Used to set the download state for a particular model.
+ */
+export const setDownloadStateAtom = atom(
null,
- (get, set, modelId: string, error: string) => {
+ (get, set, state: DownloadState) => {
const currentState = { ...get(modelDownloadStateAtom) }
- const state = currentState[modelId]
- if (!state) {
- console.debug(`Cannot find download state for ${modelId}`)
- return
- }
- if (error.includes('certificate')) {
- error += '. To fix enable "Ignore SSL Certificates" in Advanced settings.'
- }
- toaster({
- title: 'Download Failed',
- description: `Model ${modelId} download failed: ${error}`,
- type: 'error',
- })
- delete currentState[modelId]
- set(modelDownloadStateAtom, currentState)
- }
-)
-const setDownloadStateCancelledAtom = atom(
- null,
- (get, set, modelId: string) => {
- const currentState = { ...get(modelDownloadStateAtom) }
- const state = currentState[modelId]
- if (!state) {
- console.debug(`Cannot find download state for ${modelId}`)
+ if (state.downloadState === 'end') {
+ // download successfully
+ delete currentState[state.modelId]
+ set(removeDownloadingModelAtom, state.modelId)
+ const model = get(configuredModelsAtom).find(
+ (e) => e.id === state.modelId
+ )
+ if (model) set(downloadedModelsAtom, (prev) => [...prev, model])
toaster({
- title: 'Cancel Download',
- description: `Model ${modelId} cancel download`,
- type: 'warning',
+ title: 'Download Completed',
+ description: `Download ${state.modelId} completed`,
+ type: 'success',
})
-
- return
+ } else if (state.downloadState === 'error') {
+ // download error
+ delete currentState[state.modelId]
+ set(removeDownloadingModelAtom, state.modelId)
+ if (state.error === 'aborted') {
+ toaster({
+ title: 'Cancel Download',
+ description: `Model ${state.modelId} download cancelled`,
+ type: 'warning',
+ })
+ } else {
+ let error = state.error
+ if (state.error?.includes('certificate')) {
+ error +=
+ '. To fix enable "Ignore SSL Certificates" in Advanced settings.'
+ }
+ toaster({
+ title: 'Download Failed',
+ description: `Model ${state.modelId} download failed: ${error}`,
+ type: 'error',
+ })
+ }
+ } else {
+ // download in progress
+ currentState[state.modelId] = state
}
- delete currentState[modelId]
+
set(modelDownloadStateAtom, currentState)
}
)
-
-export function useDownloadState() {
- const modelDownloadState = useAtomValue(modelDownloadStateAtom)
- const setDownloadState = useSetAtom(setDownloadStateAtom)
- const setDownloadStateSuccess = useSetAtom(setDownloadStateSuccessAtom)
- const setDownloadStateFailed = useSetAtom(setDownloadStateFailedAtom)
- const setDownloadStateCancelled = useSetAtom(setDownloadStateCancelledAtom)
-
- const downloadStates: DownloadState[] = []
- for (const [, value] of Object.entries(modelDownloadState)) {
- downloadStates.push(value)
- }
-
- return {
- modelDownloadStateAtom,
- modelDownloadState,
- setDownloadState,
- setDownloadStateSuccess,
- setDownloadStateFailed,
- setDownloadStateCancelled,
- downloadStates,
- }
-}
diff --git a/web/hooks/useGetAssistants.ts b/web/hooks/useGetAssistants.ts
deleted file mode 100644
index 2b34bfbd1..000000000
--- a/web/hooks/useGetAssistants.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { useEffect, useState } from 'react'
-
-import { Assistant, ExtensionTypeEnum, AssistantExtension } from '@janhq/core'
-
-import { extensionManager } from '@/extension/ExtensionManager'
-
-export const getAssistants = async (): Promise =>
- extensionManager
- .get(ExtensionTypeEnum.Assistant)
- ?.getAssistants() ?? []
-
-/**
- * Hooks for get assistants
- *
- * @returns assistants
- */
-export default function useGetAssistants() {
- const [assistants, setAssistants] = useState([])
-
- useEffect(() => {
- getAssistants()
- .then((data) => setAssistants(data))
- .catch((err) => console.error(err))
- }, [])
-
- return { assistants }
-}
diff --git a/web/hooks/useGetConfiguredModels.ts b/web/hooks/useGetConfiguredModels.ts
deleted file mode 100644
index 8be052ae2..000000000
--- a/web/hooks/useGetConfiguredModels.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { useCallback, useEffect, useState } from 'react'
-
-import { ExtensionTypeEnum, ModelExtension, Model } from '@janhq/core'
-
-import { extensionManager } from '@/extension/ExtensionManager'
-
-export function useGetConfiguredModels() {
- const [loading, setLoading] = useState(false)
- const [models, setModels] = useState([])
-
- const fetchModels = useCallback(async () => {
- setLoading(true)
- const models = await getConfiguredModels()
- setLoading(false)
- setModels(models)
- }, [])
-
- useEffect(() => {
- fetchModels()
- }, [fetchModels])
-
- return { loading, models }
-}
-
-const getConfiguredModels = async (): Promise => {
- const models = await extensionManager
- .get(ExtensionTypeEnum.Model)
- ?.getConfiguredModels()
- return models ?? []
-}
diff --git a/web/hooks/useGetDownloadedModels.ts b/web/hooks/useGetDownloadedModels.ts
deleted file mode 100644
index bba420858..000000000
--- a/web/hooks/useGetDownloadedModels.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { useEffect } from 'react'
-
-import { ExtensionTypeEnum, ModelExtension, Model } from '@janhq/core'
-
-import { atom, useAtom } from 'jotai'
-
-import { extensionManager } from '@/extension/ExtensionManager'
-
-export const downloadedModelsAtom = atom([])
-
-export function useGetDownloadedModels() {
- const [downloadedModels, setDownloadedModels] = useAtom(downloadedModelsAtom)
-
- useEffect(() => {
- getDownloadedModels().then((downloadedModels) => {
- setDownloadedModels(downloadedModels)
- })
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [])
-
- return { downloadedModels, setDownloadedModels }
-}
-
-export const getDownloadedModels = async (): Promise =>
- extensionManager
- .get(ExtensionTypeEnum.Model)
- ?.getDownloadedModels() ?? []
diff --git a/web/hooks/useGetSystemResources.ts b/web/hooks/useGetSystemResources.ts
index de595ad7b..3f71040d7 100644
--- a/web/hooks/useGetSystemResources.ts
+++ b/web/hooks/useGetSystemResources.ts
@@ -10,15 +10,19 @@ import {
cpuUsageAtom,
totalRamAtom,
usedRamAtom,
+ nvidiaTotalVramAtom,
} from '@/helpers/atoms/SystemBar.atom'
export default function useGetSystemResources() {
const [ram, setRam] = useState(0)
const [cpu, setCPU] = useState(0)
+
+ const [gpus, setGPUs] = useState[]>([])
const setTotalRam = useSetAtom(totalRamAtom)
const setUsedRam = useSetAtom(usedRamAtom)
const setAvailableRam = useSetAtom(availableRamAtom)
const setCpuUsage = useSetAtom(cpuUsageAtom)
+ const setTotalNvidiaVram = useSetAtom(nvidiaTotalVramAtom)
const getSystemResources = async () => {
if (
@@ -48,17 +52,30 @@ export default function useGetSystemResources() {
)
setCPU(Math.round(currentLoadInfor?.cpu?.usage ?? 0))
setCpuUsage(Math.round(currentLoadInfor?.cpu?.usage ?? 0))
+
+ const gpus = currentLoadInfor?.gpu ?? []
+ setGPUs(gpus)
+
+ let totalNvidiaVram = 0
+ if (gpus.length > 0) {
+ totalNvidiaVram = gpus.reduce(
+ (total: number, gpu: { memoryTotal: string }) =>
+ total + Number(gpu.memoryTotal),
+ 0
+ )
+ }
+ setTotalNvidiaVram(totalNvidiaVram)
}
useEffect(() => {
getSystemResources()
- // Fetch interval - every 0.5s
+ // Fetch interval - every 2s
// TODO: Will we really need this?
// There is a possibility that this will be removed and replaced by the process event hook?
const intervalId = setInterval(() => {
getSystemResources()
- }, 500)
+ }, 5000)
// clean up interval
return () => clearInterval(intervalId)
@@ -69,5 +86,6 @@ export default function useGetSystemResources() {
totalRamAtom,
ram,
cpu,
+ gpus,
}
}
diff --git a/web/hooks/useModels.ts b/web/hooks/useModels.ts
new file mode 100644
index 000000000..b2aa0b518
--- /dev/null
+++ b/web/hooks/useModels.ts
@@ -0,0 +1,59 @@
+import { useCallback, useEffect } from 'react'
+
+import {
+ ExtensionTypeEnum,
+ Model,
+ ModelEvent,
+ ModelExtension,
+ events,
+} from '@janhq/core'
+
+import { useSetAtom } from 'jotai'
+
+import { extensionManager } from '@/extension'
+import {
+ configuredModelsAtom,
+ downloadedModelsAtom,
+} from '@/helpers/atoms/Model.atom'
+
+const useModels = () => {
+ const setDownloadedModels = useSetAtom(downloadedModelsAtom)
+ const setConfiguredModels = useSetAtom(configuredModelsAtom)
+
+ const getData = useCallback(() => {
+ const getDownloadedModels = async () => {
+ const models = await getLocalDownloadedModels()
+ setDownloadedModels(models)
+ }
+ const getConfiguredModels = async () => {
+ const models = await getLocalConfiguredModels()
+ setConfiguredModels(models)
+ }
+ getDownloadedModels()
+ getConfiguredModels()
+ }, [setDownloadedModels, setConfiguredModels])
+
+ useEffect(() => {
+ // Try get data on mount
+ getData()
+
+ // Listen for model updates
+ events.on(ModelEvent.OnModelsUpdate, async () => getData())
+ return () => {
+ // Remove listener on unmount
+ events.off(ModelEvent.OnModelsUpdate, async () => {})
+ }
+ }, [getData])
+}
+
+const getLocalConfiguredModels = async (): Promise =>
+ extensionManager
+ .get(ExtensionTypeEnum.Model)
+ ?.getConfiguredModels() ?? []
+
+const getLocalDownloadedModels = async (): Promise =>
+ extensionManager
+ .get(ExtensionTypeEnum.Model)
+ ?.getDownloadedModels() ?? []
+
+export default useModels
diff --git a/web/hooks/useRecommendedModel.ts b/web/hooks/useRecommendedModel.ts
index 427d2bf73..8122e2b77 100644
--- a/web/hooks/useRecommendedModel.ts
+++ b/web/hooks/useRecommendedModel.ts
@@ -5,9 +5,9 @@ import { Model, InferenceEngine } from '@janhq/core'
import { atom, useAtomValue } from 'jotai'
import { activeModelAtom } from './useActiveModel'
-import { getDownloadedModels } from './useGetDownloadedModels'
-import { activeThreadAtom, threadStatesAtom } from '@/helpers/atoms/Thread.atom'
+import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
+import { activeThreadAtom } from '@/helpers/atoms/Thread.atom'
export const lastUsedModel = atom(undefined)
@@ -24,19 +24,20 @@ export const LAST_USED_MODEL_ID = 'last-used-model-id'
*/
export default function useRecommendedModel() {
const activeModel = useAtomValue(activeModelAtom)
- const [downloadedModels, setDownloadedModels] = useState([])
+ const [sortedModels, setSortedModels] = useState([])
const [recommendedModel, setRecommendedModel] = useState()
const activeThread = useAtomValue(activeThreadAtom)
+ const downloadedModels = useAtomValue(downloadedModelsAtom)
const getAndSortDownloadedModels = useCallback(async (): Promise => {
- const models = (await getDownloadedModels()).sort((a, b) =>
+ const models = downloadedModels.sort((a, b) =>
a.engine !== InferenceEngine.nitro && b.engine === InferenceEngine.nitro
? 1
: -1
)
- setDownloadedModels(models)
+ setSortedModels(models)
return models
- }, [])
+ }, [downloadedModels])
const getRecommendedModel = useCallback(async (): Promise<
Model | undefined
@@ -98,5 +99,5 @@ export default function useRecommendedModel() {
getRecommendedModel()
}, [getRecommendedModel])
- return { recommendedModel, downloadedModels }
+ return { recommendedModel, downloadedModels: sortedModels }
}
diff --git a/web/hooks/useSetActiveThread.ts b/web/hooks/useSetActiveThread.ts
index 3545d0d23..4bcd223eb 100644
--- a/web/hooks/useSetActiveThread.ts
+++ b/web/hooks/useSetActiveThread.ts
@@ -1,3 +1,5 @@
+import { useCallback } from 'react'
+
import {
InferenceEvent,
ExtensionTypeEnum,
@@ -6,47 +8,51 @@ import {
ConversationalExtension,
} from '@janhq/core'
-import { useAtomValue, useSetAtom } from 'jotai'
+import { useSetAtom } from 'jotai'
import { extensionManager } from '@/extension'
import { setConvoMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
import {
ModelParams,
- getActiveThreadIdAtom,
isGeneratingResponseAtom,
setActiveThreadIdAtom,
setThreadModelParamsAtom,
} from '@/helpers/atoms/Thread.atom'
export default function useSetActiveThread() {
- const activeThreadId = useAtomValue(getActiveThreadIdAtom)
const setActiveThreadId = useSetAtom(setActiveThreadIdAtom)
const setThreadMessage = useSetAtom(setConvoMessagesAtom)
const setThreadModelParams = useSetAtom(setThreadModelParamsAtom)
const setIsGeneratingResponse = useSetAtom(isGeneratingResponseAtom)
- const setActiveThread = async (thread: Thread) => {
- if (activeThreadId === thread.id) {
- console.debug('Thread already active')
- return
- }
+ const setActiveThread = useCallback(
+ async (thread: Thread) => {
+ setIsGeneratingResponse(false)
+ events.emit(InferenceEvent.OnInferenceStopped, thread.id)
- setIsGeneratingResponse(false)
- events.emit(InferenceEvent.OnInferenceStopped, thread.id)
+ // load the corresponding messages
+ const messages = await getLocalThreadMessage(thread.id)
+ setThreadMessage(thread.id, messages)
- // load the corresponding messages
- const messages = await extensionManager
- .get(ExtensionTypeEnum.Conversational)
- ?.getAllMessages(thread.id)
- setThreadMessage(thread.id, messages ?? [])
+ setActiveThreadId(thread.id)
+ const modelParams: ModelParams = {
+ ...thread.assistants[0]?.model?.parameters,
+ ...thread.assistants[0]?.model?.settings,
+ }
+ setThreadModelParams(thread.id, modelParams)
+ },
+ [
+ setActiveThreadId,
+ setThreadMessage,
+ setThreadModelParams,
+ setIsGeneratingResponse,
+ ]
+ )
- setActiveThreadId(thread.id)
- const modelParams: ModelParams = {
- ...thread.assistants[0]?.model?.parameters,
- ...thread.assistants[0]?.model?.settings,
- }
- setThreadModelParams(thread.id, modelParams)
- }
-
- return { activeThreadId, setActiveThread }
+ return { setActiveThread }
}
+
+const getLocalThreadMessage = async (threadId: string) =>
+ extensionManager
+ .get(ExtensionTypeEnum.Conversational)
+ ?.getAllMessages(threadId) ?? []
diff --git a/web/hooks/useSettings.ts b/web/hooks/useSettings.ts
index 168e72489..289355b36 100644
--- a/web/hooks/useSettings.ts
+++ b/web/hooks/useSettings.ts
@@ -47,14 +47,17 @@ export const useSettings = () => {
const saveSettings = async ({
runMode,
notify,
+ gpusInUse,
}: {
runMode?: string | undefined
notify?: boolean | undefined
+ gpusInUse?: string[] | undefined
}) => {
const settingsFile = await joinPath(['file://settings', 'settings.json'])
const settings = await readSettings()
if (runMode != null) settings.run_mode = runMode
if (notify != null) settings.notify = notify
+ if (gpusInUse != null) settings.gpus_in_use = gpusInUse
await fs.writeFileSync(settingsFile, JSON.stringify(settings))
}
diff --git a/web/hooks/useThreads.ts b/web/hooks/useThreads.ts
index b7de014cc..1ac038b26 100644
--- a/web/hooks/useThreads.ts
+++ b/web/hooks/useThreads.ts
@@ -1,3 +1,5 @@
+import { useEffect } from 'react'
+
import {
ExtensionTypeEnum,
Thread,
@@ -5,14 +7,13 @@ import {
ConversationalExtension,
} from '@janhq/core'
-import { useAtomValue, useSetAtom } from 'jotai'
+import { useSetAtom } from 'jotai'
import useSetActiveThread from './useSetActiveThread'
import { extensionManager } from '@/extension/ExtensionManager'
import {
ModelParams,
- activeThreadAtom,
threadModelParamsAtom,
threadStatesAtom,
threadsAtom,
@@ -22,11 +23,10 @@ const useThreads = () => {
const setThreadStates = useSetAtom(threadStatesAtom)
const setThreads = useSetAtom(threadsAtom)
const setThreadModelRuntimeParams = useSetAtom(threadModelParamsAtom)
- const activeThread = useAtomValue(activeThreadAtom)
const { setActiveThread } = useSetActiveThread()
- const getThreads = async () => {
- try {
+ useEffect(() => {
+ const getThreads = async () => {
const localThreads = await getLocalThreads()
const localThreadStates: Record = {}
const threadModelParams: Record = {}
@@ -54,17 +54,19 @@ const useThreads = () => {
setThreadStates(localThreadStates)
setThreads(localThreads)
setThreadModelRuntimeParams(threadModelParams)
- if (localThreads.length && !activeThread) {
+
+ if (localThreads.length > 0) {
setActiveThread(localThreads[0])
}
- } catch (error) {
- console.error(error)
}
- }
- return {
- getThreads,
- }
+ getThreads()
+ }, [
+ setActiveThread,
+ setThreadModelRuntimeParams,
+ setThreadStates,
+ setThreads,
+ ])
}
const getLocalThreads = async (): Promise =>
diff --git a/web/next.config.js b/web/next.config.js
index a2e202c51..217f69698 100644
--- a/web/next.config.js
+++ b/web/next.config.js
@@ -27,7 +27,9 @@ const nextConfig = {
VERSION: JSON.stringify(packageJson.version),
ANALYTICS_ID: JSON.stringify(process.env.ANALYTICS_ID),
ANALYTICS_HOST: JSON.stringify(process.env.ANALYTICS_HOST),
- API_BASE_URL: JSON.stringify('http://localhost:1337'),
+ API_BASE_URL: JSON.stringify(
+ process.env.API_BASE_URL ?? 'http://localhost:1337'
+ ),
isMac: process.platform === 'darwin',
isWindows: process.platform === 'win32',
isLinux: process.platform === 'linux',
diff --git a/web/public/umami_script.js b/web/public/umami_script.js
new file mode 100644
index 000000000..b9db0b024
--- /dev/null
+++ b/web/public/umami_script.js
@@ -0,0 +1,210 @@
+!(function () {
+ 'use strict'
+ !(function (t) {
+ var e = t.screen,
+ n = e.width,
+ r = e.height,
+ a = t.navigator.language,
+ i = t.location,
+ o = t.localStorage,
+ u = t.document,
+ c = t.history,
+ f = 'jan.ai',
+ s = 'mainpage',
+ l = i.search,
+ d = u.currentScript
+ if (d) {
+ var m = 'data-',
+ h = d.getAttribute.bind(d),
+ v = h(m + 'website-id'),
+ p = h(m + 'host-url'),
+ g = 'false' !== h(m + 'auto-track'),
+ y = h(m + 'do-not-track'),
+ b = h(m + 'domains') || '',
+ S = b.split(',').map(function (t) {
+ return t.trim()
+ }),
+ k =
+ (p ? p.replace(/\/$/, '') : d.src.split('/').slice(0, -1).join('/')) +
+ '/api/send',
+ w = n + 'x' + r,
+ N = /data-umami-event-([\w-_]+)/,
+ T = m + 'umami-event',
+ j = 300,
+ A = function (t, e, n) {
+ var r = t[e]
+ return function () {
+ for (var e = [], a = arguments.length; a--; ) e[a] = arguments[a]
+ return n.apply(null, e), r.apply(t, e)
+ }
+ },
+ x = function () {
+ return {
+ website: v,
+ hostname: f,
+ screen: w,
+ language: a,
+ title: M,
+ url: I,
+ referrer: J,
+ }
+ },
+ E = function () {
+ return (
+ (o && o.getItem('umami.disabled')) ||
+ (y &&
+ (function () {
+ var e = t.doNotTrack,
+ n = t.navigator,
+ r = t.external,
+ a = 'msTrackingProtectionEnabled',
+ i =
+ e ||
+ n.doNotTrack ||
+ n.msDoNotTrack ||
+ (r && a in r && r[a]())
+ return '1' == i || 'yes' === i
+ })()) ||
+ (b && !S.includes(f))
+ )
+ },
+ O = function (t, e, n) {
+ n &&
+ ((J = I),
+ (I = (function (t) {
+ try {
+ return new URL(t).pathname
+ } catch (e) {
+ return t
+ }
+ })(n.toString())) !== J && setTimeout(D, j))
+ },
+ L = function (t, e) {
+ if ((void 0 === e && (e = 'event'), !E())) {
+ var n = {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ 'Content-Type': 'application/json',
+ }
+ return (
+ void 0 !== K && (n['x-umami-cache'] = K),
+ fetch(k, {
+ method: 'POST',
+ body: JSON.stringify({
+ type: e,
+ payload: t,
+ }),
+ headers: n,
+ })
+ .then(function (t) {
+ return t.text()
+ })
+ .then(function (t) {
+ return (K = t)
+ })
+ .catch(function () {})
+ )
+ }
+ },
+ D = function (t, e) {
+ return L(
+ 'string' == typeof t
+ ? Object.assign({}, x(), {
+ name: t,
+ data: 'object' == typeof e ? e : void 0,
+ })
+ : 'object' == typeof t
+ ? t
+ : 'function' == typeof t
+ ? t(x())
+ : x()
+ )
+ }
+ t.umami ||
+ (t.umami = {
+ track: D,
+ identify: function (t) {
+ return L(
+ Object.assign({}, x(), {
+ data: t,
+ }),
+ 'identify'
+ )
+ },
+ })
+ var K,
+ P,
+ _,
+ q,
+ C,
+ I = '' + s + l,
+ J = u.referrer,
+ M = u.title
+ if (g && !E()) {
+ ;(c.pushState = A(c, 'pushState', O)),
+ (c.replaceState = A(c, 'replaceState', O)),
+ (C = function (t) {
+ var e = t.getAttribute.bind(t),
+ n = e(T)
+ if (n) {
+ var r = {}
+ return (
+ t.getAttributeNames().forEach(function (t) {
+ var n = t.match(N)
+ n && (r[n[1]] = e(t))
+ }),
+ D(n, r)
+ )
+ }
+ return Promise.resolve()
+ }),
+ u.addEventListener(
+ 'click',
+ function (t) {
+ var e = t.target,
+ n =
+ 'A' === e.tagName
+ ? e
+ : (function (t, e) {
+ for (var n = t, r = 0; r < e; r++) {
+ if ('A' === n.tagName) return n
+ if (!(n = n.parentElement)) return null
+ }
+ return null
+ })(e, 10)
+ if (n) {
+ var r = n.href,
+ a =
+ '_blank' === n.target ||
+ t.ctrlKey ||
+ t.shiftKey ||
+ t.metaKey ||
+ (t.button && 1 === t.button)
+ if (n.getAttribute(T) && r)
+ return (
+ a || t.preventDefault(),
+ C(n).then(function () {
+ a || (i.href = r)
+ })
+ )
+ } else C(e)
+ },
+ !0
+ ),
+ (_ = new MutationObserver(function (t) {
+ var e = t[0]
+ M = e && e.target ? e.target.text : void 0
+ })),
+ (q = u.querySelector('head > title')) &&
+ _.observe(q, {
+ subtree: !0,
+ characterData: !0,
+ childList: !0,
+ })
+ var R = function () {
+ 'complete' !== u.readyState || P || (D(), (P = !0))
+ }
+ u.addEventListener('readystatechange', R, !0), R()
+ }
+ }
+ })(window)
+})()
diff --git a/web/screens/Chat/ChatBody/index.tsx b/web/screens/Chat/ChatBody/index.tsx
index 1ce6b591f..0e8d55c0b 100644
--- a/web/screens/Chat/ChatBody/index.tsx
+++ b/web/screens/Chat/ChatBody/index.tsx
@@ -10,9 +10,6 @@ import LogoMark from '@/containers/Brand/Logo/Mark'
import { MainViewState } from '@/constants/screens'
-import { loadModelErrorAtom } from '@/hooks/useActiveModel'
-import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
-
import { useMainViewState } from '@/hooks/useMainViewState'
import ChatItem from '../ChatItem'
@@ -20,12 +17,14 @@ import ChatItem from '../ChatItem'
import ErrorMessage from '../ErrorMessage'
import { getCurrentChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
+import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
const ChatBody: React.FC = () => {
const messages = useAtomValue(getCurrentChatMessagesAtom)
- const { downloadedModels } = useGetDownloadedModels()
+
+ const downloadedModels = useAtomValue(downloadedModelsAtom)
+
const { setMainViewState } = useMainViewState()
- const loadModelError = useAtomValue(loadModelErrorAtom)
if (downloadedModels.length === 0)
return (
@@ -86,9 +85,8 @@ const ChatBody: React.FC = () => {
message.content.length > 0) && (
)}
- {!loadModelError &&
- (message.status === MessageStatus.Error ||
- message.status === MessageStatus.Stopped) &&
+ {(message.status === MessageStatus.Error ||
+ message.status === MessageStatus.Stopped) &&
index === messages.length - 1 && (
)}
diff --git a/web/screens/Chat/CleanThreadModal/index.tsx b/web/screens/Chat/CleanThreadModal/index.tsx
new file mode 100644
index 000000000..6ef505e6f
--- /dev/null
+++ b/web/screens/Chat/CleanThreadModal/index.tsx
@@ -0,0 +1,65 @@
+import React, { useCallback } from 'react'
+
+import {
+ Button,
+ Modal,
+ ModalClose,
+ ModalContent,
+ ModalFooter,
+ ModalHeader,
+ ModalPortal,
+ ModalTitle,
+ ModalTrigger,
+} from '@janhq/uikit'
+import { Paintbrush } from 'lucide-react'
+
+import useDeleteThread from '@/hooks/useDeleteThread'
+
+type Props = {
+ threadId: string
+}
+
+const CleanThreadModal: React.FC = ({ threadId }) => {
+ const { cleanThread } = useDeleteThread()
+ const onCleanThreadClick = useCallback(
+ (e: React.MouseEvent) => {
+ e.stopPropagation()
+ cleanThread(threadId)
+ },
+ [cleanThread, threadId]
+ )
+
+ return (
+
+ e.stopPropagation()}>
+
+
+
+
+
+ Clean Thread
+
+ Are you sure you want to clean this thread?
+
+
+ e.stopPropagation()}>
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default React.memo(CleanThreadModal)
diff --git a/web/screens/Chat/DeleteThreadModal/index.tsx b/web/screens/Chat/DeleteThreadModal/index.tsx
new file mode 100644
index 000000000..edbdb09b4
--- /dev/null
+++ b/web/screens/Chat/DeleteThreadModal/index.tsx
@@ -0,0 +1,68 @@
+import React, { useCallback } from 'react'
+
+import {
+ Modal,
+ ModalTrigger,
+ ModalPortal,
+ ModalContent,
+ ModalHeader,
+ ModalTitle,
+ ModalFooter,
+ ModalClose,
+ Button,
+} from '@janhq/uikit'
+import { Trash2Icon } from 'lucide-react'
+
+import useDeleteThread from '@/hooks/useDeleteThread'
+
+type Props = {
+ threadId: string
+}
+
+const DeleteThreadModal: React.FC = ({ threadId }) => {
+ const { deleteThread } = useDeleteThread()
+ const onDeleteThreadClick = useCallback(
+ (e: React.MouseEvent) => {
+ e.stopPropagation()
+ deleteThread(threadId)
+ },
+ [deleteThread, threadId]
+ )
+
+ return (
+
+ e.stopPropagation()}>
+
+
+
+ Delete thread
+
+
+
+
+
+
+ Delete Thread
+
+
+ Are you sure you want to delete this thread? This action cannot be
+ undone.
+
+
+
+ e.stopPropagation()}>
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default React.memo(DeleteThreadModal)
diff --git a/web/screens/Chat/ErrorMessage/index.tsx b/web/screens/Chat/ErrorMessage/index.tsx
index 84a89cee8..b73884659 100644
--- a/web/screens/Chat/ErrorMessage/index.tsx
+++ b/web/screens/Chat/ErrorMessage/index.tsx
@@ -9,7 +9,6 @@ import { Button } from '@janhq/uikit'
import { useAtomValue, useSetAtom } from 'jotai'
import { RefreshCcw } from 'lucide-react'
-import { useActiveModel } from '@/hooks/useActiveModel'
import useSendChatMessage from '@/hooks/useSendChatMessage'
import { extensionManager } from '@/extension'
diff --git a/web/screens/Chat/LoadModelErrorMessage/index.tsx b/web/screens/Chat/LoadModelErrorMessage/index.tsx
deleted file mode 100644
index d3c4a704d..000000000
--- a/web/screens/Chat/LoadModelErrorMessage/index.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-import { MessageStatus, ThreadMessage } from '@janhq/core'
-import { useAtomValue } from 'jotai'
-
-import { useActiveModel } from '@/hooks/useActiveModel'
-
-import { totalRamAtom } from '@/helpers/atoms/SystemBar.atom'
-
-const LoadModelErrorMessage = () => {
- const { activeModel } = useActiveModel()
- const availableRam = useAtomValue(totalRamAtom)
-
- return (
- <>
-
-
- {Number(activeModel?.metadata.size) > availableRam ? (
- <>
- Oops! Model size exceeds available RAM. Consider selecting a
- smaller model or upgrading your RAM for smoother performance.
- >
- ) : (
- <>
- Apologies, something's amiss!
- Jan's in beta. Find troubleshooting guides{' '}
-
- here
- {' '}
- or reach out to us on{' '}
-
- Discord
- {' '}
- for assistance.
- >
- )}
-
-
- >
- )
-}
-export default LoadModelErrorMessage
diff --git a/web/screens/Chat/RequestDownloadModel/index.tsx b/web/screens/Chat/RequestDownloadModel/index.tsx
index e62dc562d..88fdadd57 100644
--- a/web/screens/Chat/RequestDownloadModel/index.tsx
+++ b/web/screens/Chat/RequestDownloadModel/index.tsx
@@ -2,15 +2,18 @@ import React, { Fragment, useCallback } from 'react'
import { Button } from '@janhq/uikit'
+import { useAtomValue } from 'jotai'
+
import LogoMark from '@/containers/Brand/Logo/Mark'
import { MainViewState } from '@/constants/screens'
-import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
import { useMainViewState } from '@/hooks/useMainViewState'
+import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
+
const RequestDownloadModel: React.FC = () => {
- const { downloadedModels } = useGetDownloadedModels()
+ const downloadedModels = useAtomValue(downloadedModelsAtom)
const { setMainViewState } = useMainViewState()
const onClick = useCallback(() => {
diff --git a/web/screens/Chat/SimpleTextMessage/index.tsx b/web/screens/Chat/SimpleTextMessage/index.tsx
index 261bb3497..9be45e7e6 100644
--- a/web/screens/Chat/SimpleTextMessage/index.tsx
+++ b/web/screens/Chat/SimpleTextMessage/index.tsx
@@ -18,7 +18,7 @@ import hljs from 'highlight.js'
import { useAtomValue } from 'jotai'
import { FolderOpenIcon } from 'lucide-react'
-import { Marked, Renderer } from 'marked'
+import { Marked, Renderer, marked as markedDefault } from 'marked'
import { markedHighlight } from 'marked-highlight'
@@ -37,13 +37,29 @@ import MessageToolbar from '../MessageToolbar'
import { getCurrentChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
+function isMarkdownValue(value: string): boolean {
+ const tokenTypes: string[] = []
+ markedDefault(value, {
+ walkTokens: (token) => {
+ tokenTypes.push(token.type)
+ },
+ })
+ const isMarkdown = ['code', 'codespan'].some((tokenType) => {
+ return tokenTypes.includes(tokenType)
+ })
+ return isMarkdown
+}
+
const SimpleTextMessage: React.FC = (props) => {
let text = ''
+ const isUser = props.role === ChatCompletionRole.User
+ const isSystem = props.role === ChatCompletionRole.System
+
if (props.content && props.content.length > 0) {
text = props.content[0]?.text?.value ?? ''
}
+
const clipboard = useClipboard({ timeout: 1000 })
- const { onViewFile, onViewFileContainer } = usePath()
const marked: Marked = new Marked(
markedHighlight({
@@ -88,9 +104,8 @@ const SimpleTextMessage: React.FC = (props) => {
}
)
+ const { onViewFile, onViewFileContainer } = usePath()
const parsedText = marked.parse(text)
- const isUser = props.role === ChatCompletionRole.User
- const isSystem = props.role === ChatCompletionRole.System
const [tokenCount, setTokenCount] = useState(0)
const [lastTimestamp, setLastTimestamp] = useState()
const [tokenSpeed, setTokenSpeed] = useState(0)
@@ -260,16 +275,29 @@ const SimpleTextMessage: React.FC = (props) => {
)}
-
+ {isUser && !isMarkdownValue(text) ? (
+
+ {text}
+
+ ) : (
+
+ )}
>
diff --git a/web/screens/Chat/ThreadList/index.tsx b/web/screens/Chat/ThreadList/index.tsx
index b4a045b1d..2ad9a28c4 100644
--- a/web/screens/Chat/ThreadList/index.tsx
+++ b/web/screens/Chat/ThreadList/index.tsx
@@ -1,76 +1,39 @@
-import { useEffect, useState } from 'react'
+import { useCallback } from 'react'
-import {
- Modal,
- ModalTrigger,
- ModalClose,
- ModalFooter,
- ModalPortal,
- ModalContent,
- ModalHeader,
- ModalTitle,
- Button,
-} from '@janhq/uikit'
+import { Thread } from '@janhq/core/'
import { motion as m } from 'framer-motion'
import { useAtomValue } from 'jotai'
-import {
- GalleryHorizontalEndIcon,
- MoreVerticalIcon,
- Trash2Icon,
- Paintbrush,
-} from 'lucide-react'
+import { GalleryHorizontalEndIcon, MoreVerticalIcon } from 'lucide-react'
import { twMerge } from 'tailwind-merge'
-import { useCreateNewThread } from '@/hooks/useCreateNewThread'
-import useDeleteThread from '@/hooks/useDeleteThread'
-
-import useGetAssistants from '@/hooks/useGetAssistants'
-import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
import useSetActiveThread from '@/hooks/useSetActiveThread'
-import useThreads from '@/hooks/useThreads'
-
import { displayDate } from '@/utils/datetime'
+import CleanThreadModal from '../CleanThreadModal'
+
+import DeleteThreadModal from '../DeleteThreadModal'
+
import {
- activeThreadAtom,
+ getActiveThreadIdAtom,
threadStatesAtom,
threadsAtom,
} from '@/helpers/atoms/Thread.atom'
export default function ThreadList() {
- const threads = useAtomValue(threadsAtom)
const threadStates = useAtomValue(threadStatesAtom)
- const { getThreads } = useThreads()
- const { assistants } = useGetAssistants()
- const { requestCreateNewThread } = useCreateNewThread()
- const activeThread = useAtomValue(activeThreadAtom)
- const { deleteThread, cleanThread } = useDeleteThread()
- const { downloadedModels } = useGetDownloadedModels()
- const [isThreadsReady, setIsThreadsReady] = useState(false)
+ const threads = useAtomValue(threadsAtom)
+ const activeThreadId = useAtomValue(getActiveThreadIdAtom)
+ const { setActiveThread } = useSetActiveThread()
- const { activeThreadId, setActiveThread: onThreadClick } =
- useSetActiveThread()
-
- useEffect(() => {
- getThreads().then(() => setIsThreadsReady(true))
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [])
-
- useEffect(() => {
- if (
- isThreadsReady &&
- downloadedModels.length !== 0 &&
- threads.length === 0 &&
- assistants.length !== 0 &&
- !activeThread
- ) {
- requestCreateNewThread(assistants[0])
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [assistants, threads, downloadedModels, activeThread, isThreadsReady])
+ const onThreadClick = useCallback(
+ (thread: Thread) => {
+ setActiveThread(thread)
+ },
+ [setActiveThread]
+ )
return (
@@ -83,133 +46,46 @@ export default function ThreadList() {
No Thread History
) : (
- threads.map((thread, i) => {
- const lastMessage =
- threadStates[thread.id]?.lastMessage ?? 'No new message'
- return (
- {
- onThreadClick(thread)
- }}
- >
-
-
- {thread.updated && displayDate(thread.updated)}
-
- {thread.title}
-
- {lastMessage || 'No new message'}
-
-
-
-
-
-
- e.stopPropagation()}>
-
-
-
-
-
- Clean Thread
-
- Are you sure you want to clean this thread?
-
-
- e.stopPropagation()}
- >
-
-
-
-
-
-
-
-
-
-
- e.stopPropagation()}>
-
-
-
- Delete thread
-
-
-
-
-
-
- Delete Thread
-
-
- Are you sure you want to delete this thread? This action
- cannot be undone.
-
-
-
- e.stopPropagation()}
- >
-
-
-
-
-
-
-
-
-
-
-
- {activeThreadId === thread.id && (
-
- )}
+ threads.map((thread) => (
+ {
+ onThreadClick(thread)
+ }}
+ >
+
+
+ {thread.updated && displayDate(thread.updated)}
+
+ {thread.title}
+
+ {threadStates[thread.id]?.lastMessage
+ ? threadStates[thread.id]?.lastMessage
+ : 'No new message'}
+
- )
- })
+
+ {activeThreadId === thread.id && (
+
+ )}
+
+ ))
)}
)
diff --git a/web/screens/Chat/index.tsx b/web/screens/Chat/index.tsx
index 1f7896604..e3eedb6c1 100644
--- a/web/screens/Chat/index.tsx
+++ b/web/screens/Chat/index.tsx
@@ -20,7 +20,7 @@ import { snackbar } from '@/containers/Toast'
import { FeatureToggleContext } from '@/context/FeatureToggle'
-import { activeModelAtom, loadModelErrorAtom } from '@/hooks/useActiveModel'
+import { activeModelAtom } from '@/hooks/useActiveModel'
import { queuedMessageAtom, reloadModelAtom } from '@/hooks/useSendChatMessage'
import ChatBody from '@/screens/Chat/ChatBody'
@@ -28,7 +28,6 @@ import ChatBody from '@/screens/Chat/ChatBody'
import ThreadList from '@/screens/Chat/ThreadList'
import ChatInput from './ChatInput'
-import LoadModelErrorMessage from './LoadModelErrorMessage'
import RequestDownloadModel from './RequestDownloadModel'
import Sidebar from './Sidebar'
@@ -70,7 +69,6 @@ const ChatScreen: React.FC = () => {
const activeModel = useAtomValue(activeModelAtom)
const isGeneratingResponse = useAtomValue(isGeneratingResponseAtom)
- const loadModelError = useAtomValue(loadModelErrorAtom)
const { getRootProps, isDragReject } = useDropzone({
noClick: true,
@@ -213,7 +211,6 @@ const ChatScreen: React.FC = () => {
)}
{activeModel && isGeneratingResponse && }
- {loadModelError && }
diff --git a/web/screens/ExploreModels/ExploreModelItemHeader/index.tsx b/web/screens/ExploreModels/ExploreModelItemHeader/index.tsx
index 3ffe2cbac..cf8c68821 100644
--- a/web/screens/ExploreModels/ExploreModelItemHeader/index.tsx
+++ b/web/screens/ExploreModels/ExploreModelItemHeader/index.tsx
@@ -25,17 +25,20 @@ import { MainViewState } from '@/constants/screens'
import { useCreateNewThread } from '@/hooks/useCreateNewThread'
import useDownloadModel from '@/hooks/useDownloadModel'
-import { useDownloadState } from '@/hooks/useDownloadState'
+import { modelDownloadStateAtom } from '@/hooks/useDownloadState'
-import { getAssistants } from '@/hooks/useGetAssistants'
-import { downloadedModelsAtom } from '@/hooks/useGetDownloadedModels'
import { useMainViewState } from '@/hooks/useMainViewState'
import { toGibibytes } from '@/utils/converter'
+import { assistantsAtom } from '@/helpers/atoms/Assistant.atom'
import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom'
-import { totalRamAtom } from '@/helpers/atoms/SystemBar.atom'
+import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
+import {
+ nvidiaTotalVramAtom,
+ totalRamAtom,
+} from '@/helpers/atoms/SystemBar.atom'
type Props = {
model: Model
@@ -46,10 +49,16 @@ type Props = {
const ExploreModelItemHeader: React.FC = ({ model, onClick, open }) => {
const { downloadModel } = useDownloadModel()
const downloadedModels = useAtomValue(downloadedModelsAtom)
- const { modelDownloadStateAtom } = useDownloadState()
const { requestCreateNewThread } = useCreateNewThread()
const totalRam = useAtomValue(totalRamAtom)
+ const nvidiaTotalVram = useAtomValue(nvidiaTotalVramAtom)
+ // Default nvidia returns vram in MB, need to convert to bytes to match the unit of totalRamW
+ let ram = nvidiaTotalVram * 1024 * 1024
+ if (ram === 0) {
+ ram = totalRam
+ }
const serverEnabled = useAtomValue(serverEnabledAtom)
+ const assistants = useAtomValue(assistantsAtom)
const downloadAtom = useMemo(
() => atom((get) => get(modelDownloadStateAtom)[model.id]),
@@ -60,17 +69,23 @@ const ExploreModelItemHeader: React.FC = ({ model, onClick, open }) => {
const onDownloadClick = useCallback(() => {
downloadModel(model)
- // eslint-disable-next-line react-hooks/exhaustive-deps
}, [model])
const isDownloaded = downloadedModels.find((md) => md.id === model.id) != null
let downloadButton = (
-
+
)
const onUseModelClick = useCallback(async () => {
- const assistants = await getAssistants()
if (assistants.length === 0) {
alert('No assistant available')
return
@@ -107,7 +122,7 @@ const ExploreModelItemHeader: React.FC = ({ model, onClick, open }) => {
}
const getLabel = (size: number) => {
- if (size * 1.25 >= totalRam) {
+ if (size * 1.25 >= ram) {
return (
Not enough RAM
diff --git a/web/screens/ExploreModels/ModelVersionItem/index.tsx b/web/screens/ExploreModels/ModelVersionItem/index.tsx
deleted file mode 100644
index 50d71b161..000000000
--- a/web/screens/ExploreModels/ModelVersionItem/index.tsx
+++ /dev/null
@@ -1,82 +0,0 @@
-import React, { useMemo } from 'react'
-
-import { Model } from '@janhq/core'
-import { Button } from '@janhq/uikit'
-import { atom, useAtomValue } from 'jotai'
-
-import ModalCancelDownload from '@/containers/ModalCancelDownload'
-
-import { MainViewState } from '@/constants/screens'
-
-import useDownloadModel from '@/hooks/useDownloadModel'
-import { useDownloadState } from '@/hooks/useDownloadState'
-import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
-import { useMainViewState } from '@/hooks/useMainViewState'
-
-type Props = {
- model: Model
- isRecommended: boolean
-}
-
-const ModelVersionItem: React.FC = ({ model }) => {
- const { downloadModel } = useDownloadModel()
- const { downloadedModels } = useGetDownloadedModels()
- const { setMainViewState } = useMainViewState()
- const isDownloaded =
- downloadedModels.find(
- (downloadedModel) => downloadedModel.id === model.id
- ) != null
-
- const { modelDownloadStateAtom, downloadStates } = useDownloadState()
-
- const downloadAtom = useMemo(
- () => atom((get) => get(modelDownloadStateAtom)[model.id ?? '']),
- /* eslint-disable react-hooks/exhaustive-deps */
- [model.id]
- )
- const downloadState = useAtomValue(downloadAtom)
-
- const onDownloadClick = () => {
- downloadModel(model)
- }
-
- let downloadButton = (
-
- )
-
- if (isDownloaded) {
- downloadButton = (
-
- )
- }
-
- if (downloadState != null && downloadStates.length > 0) {
- downloadButton =
- }
-
- return (
-
-
-
- {model.name}
-
-
-
-
- )
-}
-
-export default ModelVersionItem
diff --git a/web/screens/ExploreModels/ModelVersionList/index.tsx b/web/screens/ExploreModels/ModelVersionList/index.tsx
deleted file mode 100644
index 7992b7a51..000000000
--- a/web/screens/ExploreModels/ModelVersionList/index.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { Model } from '@janhq/core'
-
-import ModelVersionItem from '../ModelVersionItem'
-
-type Props = {
- models: Model[]
- recommendedVersion: string
-}
-
-export default function ModelVersionList({
- models,
- recommendedVersion,
-}: Props) {
- return (
-
- {models.map((model) => (
-
- ))}
-
- )
-}
diff --git a/web/screens/ExploreModels/index.tsx b/web/screens/ExploreModels/index.tsx
index 398b2db08..7002c60b7 100644
--- a/web/screens/ExploreModels/index.tsx
+++ b/web/screens/ExploreModels/index.tsx
@@ -1,4 +1,4 @@
-import { useState } from 'react'
+import { useCallback, useState } from 'react'
import { openExternalUrl } from '@janhq/core'
import {
@@ -12,24 +12,24 @@ import {
SelectItem,
} from '@janhq/uikit'
+import { useAtomValue } from 'jotai'
import { SearchIcon } from 'lucide-react'
-import Loader from '@/containers/Loader'
-
-import { useGetConfiguredModels } from '@/hooks/useGetConfiguredModels'
-
-import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
-
import ExploreModelList from './ExploreModelList'
+import {
+ configuredModelsAtom,
+ downloadedModelsAtom,
+} from '@/helpers/atoms/Model.atom'
+
const ExploreModelsScreen = () => {
- const { loading, models } = useGetConfiguredModels()
+ const configuredModels = useAtomValue(configuredModelsAtom)
+ const downloadedModels = useAtomValue(downloadedModelsAtom)
const [searchValue, setsearchValue] = useState('')
- const { downloadedModels } = useGetDownloadedModels()
const [sortSelected, setSortSelected] = useState('All Models')
const sortMenu = ['All Models', 'Recommended', 'Downloaded']
- const filteredModels = models.filter((x) => {
+ const filteredModels = configuredModels.filter((x) => {
if (sortSelected === 'Downloaded') {
return (
x.name.toLowerCase().includes(searchValue.toLowerCase()) &&
@@ -45,11 +45,9 @@ const ExploreModelsScreen = () => {
}
})
- const onHowToImportModelClick = () => {
+ const onHowToImportModelClick = useCallback(() => {
openExternalUrl('https://jan.ai/guides/using-models/import-manually/')
- }
-
- if (loading) return
+ }, [])
return (
{
} = useContext(FeatureToggleContext)
const [partialProxy, setPartialProxy] = useState (proxy)
const [gpuEnabled, setGpuEnabled] = useState(false)
-
+ const [gpuList, setGpuList] = useState([
+ { id: 'none', vram: null, name: 'none' },
+ ])
+ const [gpusInUse, setGpusInUse] = useState([])
const { readSettings, saveSettings, validateSettings, setShowNotification } =
useSettings()
@@ -54,6 +57,10 @@ const Advanced = () => {
const setUseGpuIfPossible = async () => {
const settings = await readSettings()
setGpuEnabled(settings.run_mode === 'gpu')
+ setGpusInUse(settings.gpus_in_use || [])
+ if (settings.gpus) {
+ setGpuList(settings.gpus)
+ }
}
setUseGpuIfPossible()
}, [readSettings])
@@ -69,6 +76,20 @@ const Advanced = () => {
})
}
+ const handleGPUChange = (gpuId: string) => {
+ let updatedGpusInUse = [...gpusInUse]
+ if (updatedGpusInUse.includes(gpuId)) {
+ updatedGpusInUse = updatedGpusInUse.filter((id) => id !== gpuId)
+ if (gpuEnabled && updatedGpusInUse.length === 0) {
+ updatedGpusInUse.push(gpuId)
+ }
+ } else {
+ updatedGpusInUse.push(gpuId)
+ }
+ setGpusInUse(updatedGpusInUse)
+ saveSettings({ gpusInUse: updatedGpusInUse })
+ }
+
return (
{/* Keyboard shortcut */}
@@ -109,10 +130,10 @@ const Advanced = () => {
- NVidia GPU
+ Nvidia GPU
- Enable GPU acceleration for NVidia GPUs.
+ Enable GPU acceleration for Nvidia GPUs.
{
/>
)}
-
{/* Directory */}
+ {gpuEnabled && (
+
+
+
+ {gpuList.map((gpu) => (
+
+ handleGPUChange(gpu.id)}
+ />
+
+
+ ))}
+
+
+ )}
+ {/* Warning message */}
+ {gpuEnabled && gpusInUse.length > 1 && (
+
+ If enabling multi-GPU without the same GPU model or without NVLink, it
+ may affect token speed.
+
+ )}
-
{/* Proxy */}
diff --git a/web/screens/Settings/Models/index.tsx b/web/screens/Settings/Models/index.tsx
index 3c5a0c6e3..f8997e751 100644
--- a/web/screens/Settings/Models/index.tsx
+++ b/web/screens/Settings/Models/index.tsx
@@ -2,16 +2,17 @@ import { useState } from 'react'
import { Input } from '@janhq/uikit'
+import { useAtomValue } from 'jotai'
import { SearchIcon } from 'lucide-react'
-import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
-
import RowModel from './Row'
+import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
+
const Column = ['Name', 'Model ID', 'Size', 'Version', 'Status', '']
export default function Models() {
- const { downloadedModels } = useGetDownloadedModels()
+ const downloadedModels = useAtomValue(downloadedModelsAtom)
const [searchValue, setsearchValue] = useState('')
const filteredDownloadedModels = downloadedModels.filter((x) => {
diff --git a/web/services/restService.ts b/web/services/restService.ts
index 25488ae15..6b749fd71 100644
--- a/web/services/restService.ts
+++ b/web/services/restService.ts
@@ -3,6 +3,7 @@ import {
AppRoute,
DownloadRoute,
ExtensionRoute,
+ FileManagerRoute,
FileSystemRoute,
} from '@janhq/core'
@@ -22,6 +23,7 @@ export const APIRoutes = [
route: r,
})),
...Object.values(FileSystemRoute).map((r) => ({ path: `fs`, route: r })),
+ ...Object.values(FileManagerRoute).map((r) => ({ path: `fs`, route: r })),
]
// Define the restAPI object with methods for each API route
@@ -50,4 +52,6 @@ export const restAPI = {
}
}, {}),
openExternalUrl,
+ // Jan Server URL
+ baseApiUrl: API_BASE_URL,
}
diff --git a/web/types/downloadState.d.ts b/web/types/downloadState.d.ts
index cca526bf1..766a0bde5 100644
--- a/web/types/downloadState.d.ts
+++ b/web/types/downloadState.d.ts
@@ -1,12 +1,13 @@
type DownloadState = {
modelId: string
+ filename: string
time: DownloadTime
speed: number
percent: number
size: DownloadSize
- isFinished?: boolean
children?: DownloadState[]
error?: string
+ downloadState: 'downloading' | 'error' | 'end'
}
type DownloadTime = {
diff --git a/web/utils/umami.tsx b/web/utils/umami.tsx
index ac9e70304..dc406a7d2 100644
--- a/web/utils/umami.tsx
+++ b/web/utils/umami.tsx
@@ -19,11 +19,12 @@ declare global {
const Umami = () => {
const appVersion = VERSION
- const analyticsHost = ANALYTICS_HOST
+ const analyticsScriptPath = './umami_script.js'
const analyticsId = ANALYTICS_ID
useEffect(() => {
- if (!appVersion || !analyticsHost || !analyticsId) return
+ if (!appVersion || !analyticsScriptPath || !analyticsId) return
+
const ping = () => {
// Check if umami is defined before ping
if (window.umami !== null && typeof window.umami !== 'undefined') {
@@ -45,15 +46,16 @@ const Umami = () => {
return () => {
document.removeEventListener('umami:loaded', ping)
}
- }, [appVersion, analyticsHost, analyticsId])
+ }, [appVersion, analyticsScriptPath, analyticsId])
return (
<>
- {appVersion && analyticsHost && analyticsId && (
+ {appVersion && analyticsScriptPath && analyticsId && (
|