diff --git a/.github/workflows/clean-cloudflare-page-preview-url-and-r2.yml b/.github/workflows/clean-cloudflare-page-preview-url-and-r2.yml
index 620f74714..de761ca69 100644
--- a/.github/workflows/clean-cloudflare-page-preview-url-and-r2.yml
+++ b/.github/workflows/clean-cloudflare-page-preview-url-and-r2.yml
@@ -55,10 +55,10 @@ jobs:
steps:
- name: install-aws-cli-action
uses: unfor19/install-aws-cli-action@v1
- - name: Delete object older than 7 days
+ - name: Delete object older than 10 days
run: |
# Get the list of objects in the 'latest' folder
- OBJECTS=$(aws s3api list-objects --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --query 'Contents[?LastModified<`'$(date -d "$current_date -30 days" -u +"%Y-%m-%dT%H:%M:%SZ")'`].{Key: Key}' --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com | jq -c .)
+ OBJECTS=$(aws s3api list-objects --bucket ${{ secrets.CLOUDFLARE_R2_BUCKET_NAME }} --query 'Contents[?LastModified<`'$(date -d "$current_date -10 days" -u +"%Y-%m-%dT%H:%M:%SZ")'`].{Key: Key}' --endpoint-url https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com | jq -c .)
# Create a JSON file for the delete operation
echo "{\"Objects\": $OBJECTS, \"Quiet\": false}" > delete.json
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 34eecc9f3..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..cc95fe1d4 100644
--- a/core/src/node/api/routes/download.ts
+++ b/core/src/node/api/routes/download.ts
@@ -1,58 +1,112 @@
import { DownloadRoute } from '../../../api'
-import { join } from 'path'
+import { join, sep } from 'path'
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(sep)
+ 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(sep).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..ec58002e4 100644
--- a/docs/blog/authors.yml
+++ b/docs/blog/authors.yml
@@ -1,6 +1,76 @@
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
+
+hieu-jan:
+ name: Henry Ho
+ title: Software Engineer
+ url: https://github.com/hieu-jan
+ image_url: https://avatars.githubusercontent.com/u/150573299?v=4
+ email: hieu@jan.ai
+
+0xsage:
+ name: Nicole Zhu
+ title: Co-Founder
+ url: https://github.com/0xsage
+ image_url: https://avatars.githubusercontent.com/u/69952136?v=4
+ email: nicole@jan.ai
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..85261847b 100644
--- a/electron/handlers/download.ts
+++ b/electron/handlers/download.ts
@@ -1,11 +1,15 @@
import { ipcMain } from 'electron'
-import { resolve } from 'path'
+import { resolve, sep } from 'path'
import { WindowManager } from './../managers/window'
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() {
/**
@@ -42,7 +46,7 @@ export function handleDownloaderIPCs() {
DownloadEvent.onFileDownloadError,
{
fileName,
- err: { message: 'aborted' },
+ error: 'aborted',
}
)
}
@@ -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(sep)
+ 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,20 +88,22 @@ export function handleDownloaderIPCs() {
{
...state,
fileName,
+ modelId,
}
)
})
- .on('error', function (err: Error) {
+ .on('error', function (error: Error) {
WindowManager?.instance.currentWindow?.webContents.send(
DownloadEvent.onFileDownloadError,
{
fileName,
- err,
+ modelId,
+ error,
}
)
})
.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,15 +111,17 @@ 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,
- err: { message: 'aborted' },
+ modelId,
+ error: '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/package.json b/electron/package.json
index 08f15b262..48f3a0811 100644
--- a/electron/package.json
+++ b/electron/package.json
@@ -11,7 +11,6 @@
"productName": "Jan",
"files": [
"renderer/**/*",
- "build/*.{js,map}",
"build/**/*.{js,map}",
"pre-install",
"models/**/*",
@@ -57,17 +56,18 @@
"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",
+ "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:darwin": "tsc -p . && electron-builder -p always -m",
+ "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"
},
@@ -76,7 +76,6 @@
"@janhq/core": "link:./core",
"@janhq/server": "link:./server",
"@npmcli/arborist": "^7.1.0",
- "@types/request": "^2.48.12",
"@uiball/loaders": "^1.3.0",
"electron-store": "^8.1.0",
"electron-updater": "^6.1.7",
@@ -85,8 +84,6 @@
"pacote": "^17.0.4",
"request": "^2.88.2",
"request-progress": "^3.0.0",
- "rimraf": "^5.0.5",
- "typescript": "^5.2.2",
"ulid": "^2.3.0",
"use-debounce": "^9.0.4"
},
@@ -95,6 +92,7 @@
"@playwright/test": "^1.38.1",
"@types/npmcli__arborist": "^5.6.4",
"@types/pacote": "^11.1.7",
+ "@types/request": "^2.48.12",
"@typescript-eslint/eslint-plugin": "^6.7.3",
"@typescript-eslint/parser": "^6.7.3",
"electron": "28.0.0",
@@ -102,7 +100,9 @@
"electron-devtools-installer": "^3.2.0",
"electron-playwright-helpers": "^1.6.0",
"eslint-plugin-react": "^7.33.2",
- "run-script-os": "^1.1.6"
+ "rimraf": "^5.0.5",
+ "run-script-os": "^1.1.6",
+ "typescript": "^5.2.2"
},
"installConfig": {
"hoistingLimits": "workspaces"
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..2d0d8f5c7 100644
--- a/extensions/assistant-extension/package.json
+++ b/extensions/assistant-extension/package.json
@@ -7,10 +7,11 @@
"author": "Jan ",
"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",
+ "clean:modules": "rimraf node_modules/pdf-parse/test && cd node_modules/pdf-parse/lib/pdf.js && rimraf v1.9.426 v1.10.88 v2.0.550",
+ "build": "yarn clean:modules && tsc --module commonjs && rollup -c rollup.config.ts",
+ "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": {
@@ -25,7 +26,7 @@
"rollup-plugin-define": "^1.0.1",
"rollup-plugin-sourcemaps": "^0.6.3",
"rollup-plugin-typescript2": "^0.36.0",
- "typescript": "^5.3.3",
+ "typescript": "^5.2.2",
"run-script-os": "^1.1.6"
},
"dependencies": {
@@ -44,9 +45,6 @@
],
"bundleDependencies": [
"@janhq/core",
- "@langchain/community",
- "hnswlib-node",
- "langchain",
- "pdf-parse"
+ "hnswlib-node"
]
}
diff --git a/extensions/assistant-extension/rollup.config.ts b/extensions/assistant-extension/rollup.config.ts
index 7916ef9c8..1e3c38fab 100644
--- a/extensions/assistant-extension/rollup.config.ts
+++ b/extensions/assistant-extension/rollup.config.ts
@@ -48,9 +48,6 @@ export default [
// Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash')
external: [
"@janhq/core/node",
- "@langchain/community",
- "langchain",
- "langsmith",
"path",
"hnswlib-node",
],
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/conversational-extension/package.json b/extensions/conversational-extension/package.json
index a60c12339..8a6da14e5 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",
@@ -17,12 +17,12 @@
"cpx": "^1.5.0",
"rimraf": "^3.0.2",
"webpack": "^5.88.2",
- "webpack-cli": "^5.1.4"
+ "webpack-cli": "^5.1.4",
+ "ts-loader": "^9.5.0"
},
"dependencies": {
"@janhq/core": "file:../../core",
- "path-browserify": "^1.0.1",
- "ts-loader": "^9.5.0"
+ "path-browserify": "^1.0.1"
},
"engines": {
"node": ">=18.0.0"
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..b65cf445f 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": {
@@ -35,12 +35,12 @@
"rollup-plugin-sourcemaps": "^0.6.3",
"rollup-plugin-typescript2": "^0.36.0",
"run-script-os": "^1.1.6",
- "typescript": "^5.2.2"
+ "typescript": "^5.2.2",
+ "@types/os-utils": "^0.0.4",
+ "@rollup/plugin-replace": "^5.0.5"
},
"dependencies": {
"@janhq/core": "file:../../core",
- "@rollup/plugin-replace": "^5.0.5",
- "@types/os-utils": "^0.0.4",
"fetch-retry": "^5.0.6",
"path-browserify": "^1.0.1",
"rxjs": "^7.8.1",
diff --git a/extensions/inference-nitro-extension/rollup.config.ts b/extensions/inference-nitro-extension/rollup.config.ts
index 374a054cd..77a9fb208 100644
--- a/extensions/inference-nitro-extension/rollup.config.ts
+++ b/extensions/inference-nitro-extension/rollup.config.ts
@@ -27,6 +27,9 @@ export default [
TROUBLESHOOTING_URL: JSON.stringify(
"https://jan.ai/guides/troubleshooting"
),
+ JAN_SERVER_INFERENCE_URL: JSON.stringify(
+ "http://localhost:1337/v1/chat/completions"
+ ),
}),
// Allow json resolution
json(),
diff --git a/extensions/inference-nitro-extension/src/@types/global.d.ts b/extensions/inference-nitro-extension/src/@types/global.d.ts
index bc126337f..7a4fb4805 100644
--- a/extensions/inference-nitro-extension/src/@types/global.d.ts
+++ b/extensions/inference-nitro-extension/src/@types/global.d.ts
@@ -1,6 +1,7 @@
declare const NODE: string;
declare const INFERENCE_URL: string;
declare const TROUBLESHOOTING_URL: string;
+declare const JAN_SERVER_INFERENCE_URL: string;
/**
* The response from the initModel function.
diff --git a/extensions/inference-nitro-extension/src/helpers/sse.ts b/extensions/inference-nitro-extension/src/helpers/sse.ts
index c6352383d..aab260828 100644
--- a/extensions/inference-nitro-extension/src/helpers/sse.ts
+++ b/extensions/inference-nitro-extension/src/helpers/sse.ts
@@ -6,6 +6,7 @@ import { Observable } from "rxjs";
* @returns An Observable that emits the generated response as a string.
*/
export function requestInference(
+ inferenceUrl: string,
recentMessages: any[],
model: Model,
controller?: AbortController
@@ -17,7 +18,7 @@ export function requestInference(
stream: true,
...model.parameters,
});
- fetch(INFERENCE_URL, {
+ fetch(inferenceUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
diff --git a/extensions/inference-nitro-extension/src/index.ts b/extensions/inference-nitro-extension/src/index.ts
index 2b0021ba0..9e96ad93f 100644
--- a/extensions/inference-nitro-extension/src/index.ts
+++ b/extensions/inference-nitro-extension/src/index.ts
@@ -68,35 +68,48 @@ export default class JanInferenceNitroExtension extends InferenceExtension {
*/
private nitroProcessInfo: any = undefined;
+ private inferenceUrl = "";
+
/**
* Subscribes to events emitted by the @janhq/core package.
*/
async onLoad() {
if (!(await fs.existsSync(JanInferenceNitroExtension._homeDir))) {
- await fs
- .mkdirSync(JanInferenceNitroExtension._homeDir)
- .catch((err: Error) => console.debug(err));
+ try {
+ await fs.mkdirSync(JanInferenceNitroExtension._homeDir);
+ } catch (e) {
+ console.debug(e);
+ }
}
+ // init inference url
+ // @ts-ignore
+ const electronApi = window?.electronAPI;
+ this.inferenceUrl = INFERENCE_URL;
+ if (!electronApi) {
+ this.inferenceUrl = JAN_SERVER_INFERENCE_URL;
+ }
+ console.debug("Inference url: ", this.inferenceUrl);
+
if (!(await fs.existsSync(JanInferenceNitroExtension._settingsDir)))
await fs.mkdirSync(JanInferenceNitroExtension._settingsDir);
this.writeDefaultEngineSettings();
// Events subscription
events.on(MessageEvent.OnMessageSent, (data: MessageRequest) =>
- this.onMessageRequest(data),
+ this.onMessageRequest(data)
);
events.on(ModelEvent.OnModelInit, (model: Model) =>
- this.onModelInit(model),
+ this.onModelInit(model)
);
events.on(ModelEvent.OnModelStop, (model: Model) =>
- this.onModelStop(model),
+ this.onModelStop(model)
);
events.on(InferenceEvent.OnInferenceStopped, () =>
- this.onInferenceStopped(),
+ this.onInferenceStopped()
);
// Attempt to fetch nvidia info
@@ -121,7 +134,7 @@ export default class JanInferenceNitroExtension extends InferenceExtension {
} else {
await fs.writeFileSync(
engineFile,
- JSON.stringify(this._engineSettings, null, 2),
+ JSON.stringify(this._engineSettings, null, 2)
);
}
} catch (err) {
@@ -149,7 +162,7 @@ export default class JanInferenceNitroExtension extends InferenceExtension {
this.getNitroProcesHealthIntervalId = setInterval(
() => this.periodicallyGetNitroHealth(),
- JanInferenceNitroExtension._intervalHealthCheck,
+ JanInferenceNitroExtension._intervalHealthCheck
);
}
@@ -206,7 +219,11 @@ export default class JanInferenceNitroExtension extends InferenceExtension {
return new Promise(async (resolve, reject) => {
if (!this._currentModel) return Promise.reject("No model loaded");
- requestInference(data.messages ?? [], this._currentModel).subscribe({
+ requestInference(
+ this.inferenceUrl,
+ data.messages ?? [],
+ this._currentModel
+ ).subscribe({
next: (_content: any) => {},
complete: async () => {
resolve(message);
@@ -251,7 +268,12 @@ export default class JanInferenceNitroExtension extends InferenceExtension {
...(this._currentModel || {}),
...(data.model || {}),
};
- requestInference(data.messages ?? [], model, this.controller).subscribe({
+ requestInference(
+ this.inferenceUrl,
+ data.messages ?? [],
+ model,
+ this.controller
+ ).subscribe({
next: (content: any) => {
const messageContent: ThreadContent = {
type: ContentType.Text,
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..5efdbf874 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",
@@ -18,13 +18,13 @@
"cpx": "^1.5.0",
"rimraf": "^3.0.2",
"webpack": "^5.88.2",
- "webpack-cli": "^5.1.4"
+ "webpack-cli": "^5.1.4",
+ "ts-loader": "^9.5.0"
},
"dependencies": {
"@janhq/core": "file:../../core",
"fetch-retry": "^5.0.6",
"path-browserify": "^1.0.1",
- "ts-loader": "^9.5.0",
"ulid": "^2.3.0"
},
"engines": {
diff --git a/extensions/inference-triton-trtllm-extension/package.json b/extensions/inference-triton-trtllm-extension/package.json
index 1d27f9f18..455f8030e 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",
@@ -18,13 +18,13 @@
"cpx": "^1.5.0",
"rimraf": "^3.0.2",
"webpack": "^5.88.2",
- "webpack-cli": "^5.1.4"
+ "webpack-cli": "^5.1.4",
+ "ts-loader": "^9.5.0"
},
"dependencies": {
"@janhq/core": "file:../../core",
"fetch-retry": "^5.0.6",
"path-browserify": "^1.0.1",
- "ts-loader": "^9.5.0",
"ulid": "^2.3.0",
"rxjs": "^7.8.1"
},
diff --git a/extensions/model-extension/package.json b/extensions/model-extension/package.json
index 86f177d14..5d1674007 100644
--- a/extensions/model-extension/package.json
+++ b/extensions/model-extension/package.json
@@ -8,13 +8,14 @@
"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",
"rimraf": "^3.0.2",
"webpack": "^5.88.2",
- "webpack-cli": "^5.1.4"
+ "webpack-cli": "^5.1.4",
+ "ts-loader": "^9.5.0"
},
"files": [
"dist/*",
@@ -23,7 +24,6 @@
],
"dependencies": {
"@janhq/core": "file:../../core",
- "path-browserify": "^1.0.1",
- "ts-loader": "^9.5.0"
+ "path-browserify": "^1.0.1"
}
}
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 b9fa7731e..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)
+ })
}
/**
@@ -318,7 +363,7 @@ export default class JanModelExtension extends ModelExtension {
return
}
- const defaultModel = await this.getDefaultModel() as Model
+ const defaultModel = (await this.getDefaultModel()) as Model
if (!defaultModel) {
console.error('Unable to find default model')
return
@@ -382,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..582f7cd7b 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,17 +8,17 @@
"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",
"webpack": "^5.88.2",
- "webpack-cli": "^5.1.4"
+ "webpack-cli": "^5.1.4",
+ "ts-loader": "^9.5.0"
},
"dependencies": {
"@janhq/core": "file:../../core",
- "node-os-utils": "^1.3.7",
- "ts-loader": "^9.5.0"
+ "node-os-utils": "^1.3.7"
},
"files": [
"dist/*",
@@ -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/models/mistral-ins-7b-q4/cover.png b/models/mistral-ins-7b-q4/cover.png
index 000445ecb..73b82e599 100644
Binary files a/models/mistral-ins-7b-q4/cover.png and b/models/mistral-ins-7b-q4/cover.png differ
diff --git a/models/openhermes-neural-7b/cover.png b/models/openhermes-neural-7b/cover.png
index 5b9da0aef..8976d8449 100644
Binary files a/models/openhermes-neural-7b/cover.png and b/models/openhermes-neural-7b/cover.png differ
diff --git a/models/trinity-v1.2-7b/cover.png b/models/trinity-v1.2-7b/cover.png
index a548e3c17..fbef0bb56 100644
Binary files a/models/trinity-v1.2-7b/cover.png and b/models/trinity-v1.2-7b/cover.png differ
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/uikit/src/input/styles.scss b/uikit/src/input/styles.scss
index 9990da8b4..e649f494d 100644
--- a/uikit/src/input/styles.scss
+++ b/uikit/src/input/styles.scss
@@ -1,6 +1,6 @@
.input {
@apply border-border placeholder:text-muted-foreground flex h-9 w-full rounded-lg border bg-transparent px-3 py-1 transition-colors;
- @apply disabled:cursor-not-allowed disabled:bg-zinc-100 disabled:dark:bg-zinc-800 disabled:dark:text-zinc-600;
+ @apply disabled:text-muted-foreground disabled:cursor-not-allowed disabled:bg-zinc-100 disabled:dark:bg-zinc-800 disabled:dark:text-zinc-600;
@apply focus-within:outline-none focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1;
@apply file:border-0 file:bg-transparent file:font-medium;
}
diff --git a/uikit/src/select/styles.scss b/uikit/src/select/styles.scss
index bc5b6c0cc..90485723a 100644
--- a/uikit/src/select/styles.scss
+++ b/uikit/src/select/styles.scss
@@ -1,6 +1,6 @@
.select {
@apply placeholder:text-muted-foreground border-border flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border bg-transparent px-3 py-2 text-sm shadow-sm disabled:cursor-not-allowed [&>span]:line-clamp-1;
- @apply disabled:cursor-not-allowed disabled:bg-zinc-100 disabled:dark:bg-zinc-800 disabled:dark:text-zinc-600;
+ @apply disabled:text-muted-foreground disabled:cursor-not-allowed disabled:bg-zinc-100 disabled:dark:bg-zinc-800 disabled:dark:text-zinc-600;
@apply focus-within:outline-none focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1;
&-caret {
@@ -21,6 +21,7 @@
&-item {
@apply hover:bg-secondary relative my-1 block w-full cursor-pointer select-none items-center rounded-sm px-4 py-2 text-sm data-[disabled]:pointer-events-none data-[disabled]:opacity-50;
+ @apply focus:outline-none focus-visible:outline-0;
}
&-trigger-viewport {
diff --git a/web/containers/CardSidebar/index.tsx b/web/containers/CardSidebar/index.tsx
index 38a8678d9..cb9942087 100644
--- a/web/containers/CardSidebar/index.tsx
+++ b/web/containers/CardSidebar/index.tsx
@@ -156,7 +156,10 @@ export default function CardSidebar({
>
) : (
<>
- Opens {title}.json.
+ Opens{' '}
+
+ {title === 'Tools' ? 'assistant' : title}.json.
+
Changes affect all new threads.
>
)}
diff --git a/web/containers/DropdownListSidebar/index.tsx b/web/containers/DropdownListSidebar/index.tsx
index 140a1aba1..2679d6869 100644
--- a/web/containers/DropdownListSidebar/index.tsx
+++ b/web/containers/DropdownListSidebar/index.tsx
@@ -14,7 +14,14 @@ import {
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'
-import { MonitorIcon } from 'lucide-react'
+import {
+ MonitorIcon,
+ LayoutGridIcon,
+ FoldersIcon,
+ GlobeIcon,
+ CheckIcon,
+ CopyIcon,
+} from 'lucide-react'
import { twMerge } from 'tailwind-merge'
@@ -22,6 +29,7 @@ import { MainViewState } from '@/constants/screens'
import { useActiveModel } from '@/hooks/useActiveModel'
+import { useClipboard } from '@/hooks/useClipboard'
import { useMainViewState } from '@/hooks/useMainViewState'
import useRecommendedModel from '@/hooks/useRecommendedModel'
@@ -42,6 +50,8 @@ import {
export const selectedModelAtom = atom(undefined)
+const engineOptions = ['Local', 'Remote']
+
// TODO: Move all of the unscoped logics outside of the component
const DropdownListSidebar = ({
strictedThread = true,
@@ -51,13 +61,24 @@ const DropdownListSidebar = ({
const activeThread = useAtomValue(activeThreadAtom)
const [selectedModel, setSelectedModel] = useAtom(selectedModelAtom)
const setThreadModelParams = useSetAtom(setThreadModelParamsAtom)
-
+ const [isTabActive, setIsTabActive] = useState(0)
const { stateModel } = useActiveModel()
const [serverEnabled, setServerEnabled] = useAtom(serverEnabledAtom)
const { setMainViewState } = useMainViewState()
const [loader, setLoader] = useState(0)
const { recommendedModel, downloadedModels } = useRecommendedModel()
const { updateModelParameter } = useUpdateModelParameters()
+ const clipboard = useClipboard({ timeout: 1000 })
+ const [copyId, setCopyId] = useState('')
+
+ const localModel = downloadedModels.filter(
+ (model) => model.engine === InferenceEngine.nitro
+ )
+ const remoteModel = downloadedModels.filter(
+ (model) => model.engine === InferenceEngine.openai
+ )
+
+ const modelOptions = isTabActive === 0 ? localModel : remoteModel
useEffect(() => {
if (!activeThread) return
@@ -171,48 +192,145 @@ const DropdownListSidebar = ({
-
-
- Local
+
+
+ {engineOptions.map((name, i) => {
+ return (
+ - setIsTabActive(i)}
+ >
+ {i === 0 ? (
+
+ ) : (
+
+ )}
+
+ {name}
+
+
+ )
+ })}
+
+
{downloadedModels.length === 0 ? (
{`Oops, you don't have a model yet.`}
) : (
-
- {downloadedModels.map((x, i) => (
-
-
- {x.name}
-
-
- {toGibibytes(x.metadata.size)}
-
- {x.engine == InferenceEngine.nitro && (
-
+
+ <>
+ {modelOptions.map((x, i) => (
+
+
+
+ {x.engine === InferenceEngine.openai && (
+
+ )}
+
+
+ {x.name}
+
+
+
+ {toGibibytes(x.metadata.size)}
+
+ {x.engine == InferenceEngine.nitro && (
+
+ )}
+
+
+
+
+
+ {x.id}
+ {clipboard.copied && copyId === x.id ? (
+
+ ) : (
+ {
+ clipboard.copy(x.id)
+ setCopyId(x.id)
+ }}
+ />
)}
-
- ))}
+ ))}
+ >
)}
-
+
+
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/ModalTroubleShoot/AppLogs.tsx b/web/containers/ModalTroubleShoot/AppLogs.tsx
new file mode 100644
index 000000000..d4f6bddb8
--- /dev/null
+++ b/web/containers/ModalTroubleShoot/AppLogs.tsx
@@ -0,0 +1,203 @@
+import React, { useEffect, useState } from 'react'
+
+import { Button } from '@janhq/uikit'
+
+import { CopyIcon, CheckIcon } from 'lucide-react'
+
+import { useClipboard } from '@/hooks/useClipboard'
+import { useLogs } from '@/hooks/useLogs'
+
+const AppLogs = () => {
+ const { getLogs } = useLogs()
+ const [logs, setLogs] = useState([])
+
+ useEffect(() => {
+ getLogs('app').then((log) => {
+ if (typeof log?.split === 'function') {
+ setLogs(log.split(/\r?\n|\r|\n/g))
+ }
+ })
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
+
+ const clipboard = useClipboard({ timeout: 1000 })
+
+ return (
+ <>
+
+
+
+
+ {logs.length > 1 ? (
+
+
+ {logs.slice(-100).map((log, i) => {
+ return (
+
+ {log}
+
+ )
+ })}
+
+
+ ) : (
+
+
+ Empty logs
+
+ )}
+
+ >
+ )
+}
+
+export default AppLogs
diff --git a/web/containers/ModalTroubleShoot/DeviceSpecs.tsx b/web/containers/ModalTroubleShoot/DeviceSpecs.tsx
new file mode 100644
index 000000000..5ebb610d1
--- /dev/null
+++ b/web/containers/ModalTroubleShoot/DeviceSpecs.tsx
@@ -0,0 +1,46 @@
+import React from 'react'
+
+import { Button } from '@janhq/uikit'
+
+import { CopyIcon, CheckIcon } from 'lucide-react'
+
+import { useClipboard } from '@/hooks/useClipboard'
+
+// TODO @Louis help add missing information device specs
+const DeviceSpecs = () => {
+ const userAgent = window.navigator.userAgent
+ const clipboard = useClipboard({ timeout: 1000 })
+
+ return (
+ <>
+
+
+
+
+ >
+ )
+}
+
+export default DeviceSpecs
diff --git a/web/containers/ModalTroubleShoot/index.tsx b/web/containers/ModalTroubleShoot/index.tsx
new file mode 100644
index 000000000..547398c4f
--- /dev/null
+++ b/web/containers/ModalTroubleShoot/index.tsx
@@ -0,0 +1,121 @@
+import { useState } from 'react'
+import ScrollToBottom from 'react-scroll-to-bottom'
+
+import { Modal, ModalContent, ModalHeader, ModalTitle } from '@janhq/uikit'
+import { motion as m } from 'framer-motion'
+import { atom, useAtom } from 'jotai'
+import { twMerge } from 'tailwind-merge'
+
+import ServerLogs from '../ServerLogs'
+
+import AppLogs from './AppLogs'
+import DeviceSpecs from './DeviceSpecs'
+
+export const modalTroubleShootingAtom = atom(false)
+const logOption = ['App Logs', 'Server Logs', 'Device Specs']
+
+const ModalTroubleShooting: React.FC = () => {
+ const [modalTroubleShooting, setModalTroubleShooting] = useAtom(
+ modalTroubleShootingAtom
+ )
+ const [isTabActive, setIsTabActivbe] = useState(0)
+
+ return (
+
+
+
+ Troubleshooting Assistance
+
+
+ {`We're here to help! Your report is crucial for debugging and shaping
+ the next version. Here’s how you can report & get further support:`}
+
+
+
+
+
+
+ Step 2
+
+ {`If you can't find what you need in our troubleshooting guide, feel
+ free reach out to us for extra help:`}
+
+
+
+
+
+ {/* TODO @faisal replace this once we have better tabs component UI */}
+
+
+ {logOption.map((name, i) => {
+ return (
+ - setIsTabActivbe(i)}
+ >
+
+ {name}
+
+ {isTabActive === i && (
+
+ )}
+
+ )
+ })}
+
+
+
+ {isTabActive === 0 && }
+ {isTabActive === 1 && }
+ {isTabActive === 2 && }
+
+
+
+
+
+ )
+}
+
+export default ModalTroubleShooting
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..170ec5e64 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
@@ -93,12 +95,12 @@ export default function EventHandler({ children }: { children: ReactNode }) {
(res: any) => {
const errorMessage = `${res.error}`
console.error('Failed to load model: ' + errorMessage)
- setLoadModelError(errorMessage)
setStateModel(() => ({
state: 'start',
loading: false,
model: res.modelId,
}))
+ setLoadModelError(errorMessage)
setQueuedMessage(false)
},
[setStateModel, setQueuedMessage, setLoadModelError]
@@ -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 c8a20bca7..e7a179ec4 100644
--- a/web/containers/Providers/index.tsx
+++ b/web/containers/Providers/index.tsx
@@ -23,6 +23,8 @@ import Umami from '@/utils/umami'
import Loader from '../Loader'
+import DataLoader from './DataLoader'
+
import KeyListener from './KeyListener'
import { extensionManager } from '@/extension'
@@ -81,7 +83,9 @@ const Providers = (props: PropsWithChildren) => {
- {children}
+
+ {children}
+
{!isMac && }
diff --git a/web/containers/ServerLogs/index.tsx b/web/containers/ServerLogs/index.tsx
new file mode 100644
index 000000000..c97643769
--- /dev/null
+++ b/web/containers/ServerLogs/index.tsx
@@ -0,0 +1,213 @@
+/* eslint-disable @typescript-eslint/naming-convention */
+import { useEffect, useState } from 'react'
+
+import React from 'react'
+
+import { Button } from '@janhq/uikit'
+import { useAtomValue } from 'jotai'
+
+import { CopyIcon, CheckIcon } from 'lucide-react'
+
+import { useClipboard } from '@/hooks/useClipboard'
+import { useLogs } from '@/hooks/useLogs'
+
+import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom'
+
+type ServerLogsProps = { limit?: number; withCopy?: boolean }
+
+const ServerLogs = (props: ServerLogsProps) => {
+ const { limit = 0 } = props
+ const { getLogs } = useLogs()
+ const serverEnabled = useAtomValue(serverEnabledAtom)
+ const [logs, setLogs] = useState([])
+
+ const clipboard = useClipboard({ timeout: 1000 })
+
+ useEffect(() => {
+ getLogs('server').then((log) => {
+ if (typeof log?.split === 'function') {
+ setLogs(log.split(/\r?\n|\r|\n/g))
+ }
+ })
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [logs, serverEnabled])
+
+ return (
+ <>
+
+
+
+
+ {logs.length > 1 ? (
+
+
+ {logs.slice(-limit).map((log, i) => {
+ return (
+
+ {log}
+
+ )
+ })}
+
+
+ ) : (
+
+
+ Empty logs
+
+ )}
+
+ >
+ )
+}
+
+export default ServerLogs
diff --git a/web/containers/Toast/index.tsx b/web/containers/Toast/index.tsx
index 7cffa89b9..eae340fee 100644
--- a/web/containers/Toast/index.tsx
+++ b/web/containers/Toast/index.tsx
@@ -19,8 +19,8 @@ const ErrorIcon = () => {
xmlns="http://www.w3.org/2000/svg"
>
@@ -38,8 +38,8 @@ const WarningIcon = () => {
xmlns="http://www.w3.org/2000/svg"
>
@@ -57,8 +57,8 @@ const SuccessIcon = () => {
xmlns="http://www.w3.org/2000/svg"
>
@@ -76,8 +76,8 @@ const DefaultIcon = () => {
xmlns="http://www.w3.org/2000/svg"
>
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..6f64c4070 100644
--- a/web/hooks/useCreateNewThread.ts
+++ b/web/hooks/useCreateNewThread.ts
@@ -1,3 +1,5 @@
+import { useContext } from 'react'
+
import {
Assistant,
ConversationalExtension,
@@ -6,12 +8,15 @@ import {
ThreadAssistantInfo,
ThreadState,
Model,
+ AssistantTool,
} 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'
+import { FeatureToggleContext } from '@/context/FeatureToggle'
+
import { generateThreadId } from '@/utils/thread'
import useRecommendedModel from './useRecommendedModel'
@@ -19,6 +24,8 @@ import useRecommendedModel from './useRecommendedModel'
import useSetActiveThread from './useSetActiveThread'
import { extensionManager } from '@/extension'
+
+import { getCurrentChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
import {
threadsAtom,
threadStatesAtom,
@@ -50,20 +57,38 @@ export const useCreateNewThread = () => {
const setFileUpload = useSetAtom(fileUploadAtom)
const setSelectedModel = useSetAtom(selectedModelAtom)
const setThreadModelParams = useSetAtom(setThreadModelParamsAtom)
+ const messages = useAtomValue(getCurrentChatMessagesAtom)
+ const { experimentalFeature } = useContext(FeatureToggleContext)
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
+ }
+
+ // modify assistant tools when experimental on, retieval toggle enabled in default
+ const assistantTools: AssistantTool = {
+ type: 'retrieval',
+ enabled: true,
+ settings: assistant.tools && assistant.tools[0].settings,
+ }
+
const createdAt = Date.now()
const assistantInfo: ThreadAssistantInfo = {
assistant_id: assistant.id,
assistant_name: assistant.name,
- tools: assistant.tools,
+ tools: experimentalFeature ? [assistantTools] : assistant.tools,
model: {
id: defaultModel?.id ?? '*',
settings: defaultModel?.settings ?? {},
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..863c612ed 100644
--- a/web/hooks/useDownloadState.ts
+++ b/web/hooks/useDownloadState.ts
@@ -1,96 +1,67 @@
-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 (
+ typeof error?.includes === 'function' &&
+ 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/useServerLog.ts b/web/hooks/useLogs.tsx
similarity index 67%
rename from web/hooks/useServerLog.ts
rename to web/hooks/useLogs.tsx
index b263534b6..7c504428f 100644
--- a/web/hooks/useServerLog.ts
+++ b/web/hooks/useLogs.tsx
@@ -5,12 +5,12 @@ import {
getJanDataFolderPath,
} from '@janhq/core'
-export const useServerLog = () => {
- const getServerLog = async () => {
- if (!(await fs.existsSync(await joinPath(['file://logs', 'server.log']))))
+export const useLogs = () => {
+ const getLogs = async (file: string) => {
+ if (!(await fs.existsSync(await joinPath(['file://logs', `${file}.log`]))))
return {}
const logs = await fs.readFileSync(
- await joinPath(['file://logs', 'server.log']),
+ await joinPath(['file://logs', `${file}.log`]),
'utf-8'
)
@@ -25,5 +25,5 @@ export const useServerLog = () => {
const clearServerLog = async () => {
await fs.writeFileSync(await joinPath(['file://logs', 'server.log']), '')
}
- return { getServerLog, openServerLog, clearServerLog }
+ return { getLogs, openServerLog, clearServerLog }
}
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/usePath.ts b/web/hooks/usePath.ts
index aea25bef1..35fb853b4 100644
--- a/web/hooks/usePath.ts
+++ b/web/hooks/usePath.ts
@@ -25,6 +25,7 @@ export const usePath = () => {
if (!selectedModel) return
filePath = await joinPath(['models', selectedModel.id])
break
+ case 'Tools':
case 'Assistant':
if (!assistantId) return
filePath = await joinPath(['assistants', assistantId])
@@ -59,6 +60,7 @@ export const usePath = () => {
filePath = await joinPath(['models', selectedModel.id, 'model.json'])
break
case 'Assistant':
+ case 'Tools':
if (!assistantId) return
filePath = await joinPath(['assistants', assistantId, 'assistant.json'])
break
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 f5649ccaf..4bcd223eb 100644
--- a/web/hooks/useSetActiveThread.ts
+++ b/web/hooks/useSetActiveThread.ts
@@ -1,3 +1,5 @@
+import { useCallback } from 'react'
+
import {
InferenceEvent,
ExtensionTypeEnum,
@@ -6,51 +8,51 @@ import {
ConversationalExtension,
} from '@janhq/core'
-import { useAtomValue, useSetAtom } from 'jotai'
-
-import { loadModelErrorAtom } from './useActiveModel'
+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 setLoadModelError = useSetAtom(loadModelErrorAtom)
- 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)
- setLoadModelError(undefined)
- 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 66f14d076..ee0b4592d 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,10 +17,13 @@ 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()
if (downloadedModels.length === 0)
@@ -81,7 +81,8 @@ const ChatBody: React.FC = () => {
{messages.map((message, index) => (
- {(message.status !== MessageStatus.Pending ||
+ {((message.status !== MessageStatus.Error &&
+ message.status !== MessageStatus.Pending) ||
message.content.length > 0) && (
)}
diff --git a/web/screens/Chat/ChatInput/index.tsx b/web/screens/Chat/ChatInput/index.tsx
index ee1ac9a41..b53c4d651 100644
--- a/web/screens/Chat/ChatInput/index.tsx
+++ b/web/screens/Chat/ChatInput/index.tsx
@@ -112,14 +112,12 @@ const ChatInput: React.FC = () => {
const file = event.target.files?.[0]
if (!file) return
setFileUpload([{ file: file, type: 'pdf' }])
- setCurrentPrompt('Summarize this for me')
}
const handleImageChange = (event: React.ChangeEvent ) => {
const file = event.target.files?.[0]
if (!file) return
setFileUpload([{ file: file, type: 'image' }])
- setCurrentPrompt('What do you see in this image?')
}
const renderPreview = (fileUpload: any) => {
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..ea9906335 100644
--- a/web/screens/Chat/ErrorMessage/index.tsx
+++ b/web/screens/Chat/ErrorMessage/index.tsx
@@ -9,7 +9,10 @@ import { Button } from '@janhq/uikit'
import { useAtomValue, useSetAtom } from 'jotai'
import { RefreshCcw } from 'lucide-react'
-import { useActiveModel } from '@/hooks/useActiveModel'
+import ModalTroubleShooting, {
+ modalTroubleShootingAtom,
+} from '@/containers/ModalTroubleShoot'
+
import useSendChatMessage from '@/hooks/useSendChatMessage'
import { extensionManager } from '@/extension'
@@ -24,6 +27,7 @@ const ErrorMessage = ({ message }: { message: ThreadMessage }) => {
const thread = useAtomValue(activeThreadAtom)
const deleteMessage = useSetAtom(deleteMessageAtom)
const { resendChatMessage } = useSendChatMessage()
+ const setModalTroubleShooting = useSetAtom(modalTroubleShootingAtom)
const regenerateMessage = async () => {
const lastMessageIndex = messages.length - 1
@@ -65,29 +69,22 @@ const ErrorMessage = ({ message }: { message: ThreadMessage }) => {
)}
{message.status === MessageStatus.Error && (
-
-
- <>
- Apologies, something's amiss!
- Jan's in beta. Find troubleshooting guides{' '}
-
- here
- {' '}
- or reach out to us on{' '}
-
- Discord
- {' '}
- for assistance.
- >
-
+
+ {`Apologies, something’s amiss!`}
+
+ Jan’s in beta. Access
+ setModalTroubleShooting(true)}
+ >
+ troubleshooting assistance
+
+ now.
+
+
)}
>
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/Sidebar/index.tsx b/web/screens/Chat/Sidebar/index.tsx
index 8088501b9..b2ab6dab5 100644
--- a/web/screens/Chat/Sidebar/index.tsx
+++ b/web/screens/Chat/Sidebar/index.tsx
@@ -1,11 +1,20 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import React, { useContext } from 'react'
-import { InferenceEngine } from '@janhq/core'
-import { Input, Textarea, Switch } from '@janhq/uikit'
+import {
+ Input,
+ Textarea,
+ Switch,
+ Tooltip,
+ TooltipArrow,
+ TooltipContent,
+ TooltipPortal,
+ TooltipTrigger,
+} from '@janhq/uikit'
import { atom, useAtomValue } from 'jotai'
+import { InfoIcon } from 'lucide-react'
import { twMerge } from 'tailwind-merge'
import LogoMark from '@/containers/Brand/Logo/Mark'
@@ -134,76 +143,174 @@ const Sidebar: React.FC = () => {
}}
/>
- {experimentalFeature && (
-
- {activeThread?.assistants[0]?.tools &&
- componentDataAssistantSetting.length > 0 && (
-
- {
- if (activeThread)
- updateThreadMetadata({
- ...activeThread,
- assistants: [
- {
- ...activeThread.assistants[0],
- tools: [
- {
- type: 'retrieval',
- enabled: e,
- settings:
- (activeThread.assistants[0].tools &&
- activeThread.assistants[0]
- .tools[0]?.settings) ??
- {},
- },
- ],
- },
- ],
- })
- }}
- />
- }
- >
- {activeThread?.assistants[0]?.tools[0].enabled && (
-
-
-
-
-
-
-
-
-
- )}
-
-
- )}
-
- )}
+
+ {experimentalFeature && (
+
+ {activeThread?.assistants[0]?.tools &&
+ componentDataAssistantSetting.length > 0 && (
+
+
+
+
+
+
+
+
+ {
+ if (activeThread)
+ updateThreadMetadata({
+ ...activeThread,
+ assistants: [
+ {
+ ...activeThread.assistants[0],
+ tools: [
+ {
+ type: 'retrieval',
+ enabled: e,
+ settings:
+ (activeThread.assistants[0]
+ .tools &&
+ activeThread.assistants[0]
+ .tools[0]?.settings) ??
+ {},
+ },
+ ],
+ },
+ ],
+ })
+ }}
+ />
+
+
+
+ {activeThread?.assistants[0]?.tools[0].enabled && (
+
+
+
+
+
+
+
+
+
+
+
+ Embedding model is crucial for
+ understanding and processing the input
+ text effectively by converting text to
+ numerical representations. Align the model
+ choice with your task, evaluate its
+ performance, and consider factors like
+ resource availability. Experiment to find
+ the best fit for your specific use case.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Vector Database is crucial for efficient
+ storage and retrieval of embeddings.
+ Consider your specific task, available
+ resources, and language requirements.
+ Experiment to find the best fit for your
+ specific use case.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+
+
+ )}
+
+ )}
+
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 e3eedb6c1..29f440cb6 100644
--- a/web/screens/Chat/index.tsx
+++ b/web/screens/Chat/index.tsx
@@ -110,11 +110,6 @@ const ChatScreen: React.FC = () => {
const imageType = files[0]?.type.includes('image')
setFileUpload([{ file: files[0], type: imageType ? 'image' : 'pdf' }])
setDragOver(false)
- if (imageType) {
- setCurrentPrompt('What do you see in this image?')
- } else {
- setCurrentPrompt('Summarize this for me')
- }
},
onDropRejected: (e) => {
if (
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/ExploreModelList/index.tsx b/web/screens/ExploreModels/ExploreModelList/index.tsx
index 54c8120dd..ddd474b53 100644
--- a/web/screens/ExploreModels/ExploreModelList/index.tsx
+++ b/web/screens/ExploreModels/ExploreModelList/index.tsx
@@ -10,7 +10,7 @@ const ExploreModelList: React.FC = ({ models }) => {
const takenModelIds: string[] = []
const featuredModels = models
.filter((m) => {
- if (m.metadata.tags.includes('Featured')) {
+ if (m.metadata?.tags?.includes('Featured')) {
takenModelIds.push(m.id)
return m
}
@@ -19,7 +19,7 @@ const ExploreModelList: React.FC = ({ models }) => {
const recommendedModels = models
.filter((m) => {
- if (m.metadata.tags.includes('Recommended')) {
+ if (m.metadata?.tags?.includes('Recommended')) {
takenModelIds.push(m.id)
return m
}
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 (
{
- const { getServerLog } = useServerLog()
- const serverEnabled = useAtomValue(serverEnabledAtom)
- const [logs, setLogs] = useState([])
-
- useEffect(() => {
- getServerLog().then((log) => {
- if (typeof log?.split === 'function') {
- setLogs(log.split(/\r?\n|\r|\n/g))
- }
- })
-
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [logs, serverEnabled])
-
- return (
-
- {logs.length > 1 ? (
-
-
- {logs.map((log, i) => {
- return (
-
- {log}
-
- )
- })}
-
-
- ) : (
-
-
- Empty logs
-
- )}
-
- )
-}
-
-export default Logs
diff --git a/web/screens/LocalServer/index.tsx b/web/screens/LocalServer/index.tsx
index b96f4c228..88a9d86ba 100644
--- a/web/screens/LocalServer/index.tsx
+++ b/web/screens/LocalServer/index.tsx
@@ -20,11 +20,12 @@ import {
SelectValue,
} from '@janhq/uikit'
-import { atom, useAtom, useAtomValue } from 'jotai'
+import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'
import { Paintbrush, CodeIcon } from 'lucide-react'
import { ExternalLinkIcon, InfoIcon } from 'lucide-react'
+import { AlertTriangleIcon } from 'lucide-react'
import { twMerge } from 'tailwind-merge'
import CardSidebar from '@/containers/CardSidebar'
@@ -33,8 +34,13 @@ import DropdownListSidebar, {
selectedModelAtom,
} from '@/containers/DropdownListSidebar'
-import { useActiveModel } from '@/hooks/useActiveModel'
-import { useServerLog } from '@/hooks/useServerLog'
+import ModalTroubleShooting, {
+ modalTroubleShootingAtom,
+} from '@/containers/ModalTroubleShoot'
+import ServerLogs from '@/containers/ServerLogs'
+
+import { loadModelErrorAtom, useActiveModel } from '@/hooks/useActiveModel'
+import { useLogs } from '@/hooks/useLogs'
import { getConfigurationsData } from '@/utils/componentSettings'
import { toSettingParams } from '@/utils/modelParam'
@@ -45,8 +51,6 @@ import SettingComponentBuilder from '../Chat/ModelSetting/SettingComponent'
import { showRightSideBarAtom } from '../Chat/Sidebar'
-import Logs from './Logs'
-
import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom'
import { getActiveThreadModelParamsAtom } from '@/helpers/atoms/Thread.atom'
@@ -60,11 +64,12 @@ const LocalServerScreen = () => {
const [serverEnabled, setServerEnabled] = useAtom(serverEnabledAtom)
const showRightSideBar = useAtomValue(showRightSideBarAtom)
const activeModelParams = useAtomValue(getActiveThreadModelParamsAtom)
+ const setModalTroubleShooting = useSetAtom(modalTroubleShootingAtom)
const modelEngineParams = toSettingParams(activeModelParams)
const componentDataEngineSetting = getConfigurationsData(modelEngineParams)
- const { openServerLog, clearServerLog } = useServerLog()
+ const { openServerLog, clearServerLog } = useLogs()
const { startModel, stateModel } = useActiveModel()
const selectedModel = useAtomValue(selectedModelAtom)
@@ -72,6 +77,7 @@ const LocalServerScreen = () => {
const [isVerboseEnabled, setIsVerboseEnabled] = useAtom(verboseEnabledAtom)
const [host, setHost] = useAtom(hostAtom)
const [port, setPort] = useAtom(portAtom)
+ const [loadModelError, setLoadModelError] = useAtom(loadModelErrorAtom)
const hostOptions = ['127.0.0.1', '0.0.0.0']
@@ -122,6 +128,7 @@ const LocalServerScreen = () => {
if (serverEnabled) {
window.core?.api?.stopServer()
setServerEnabled(false)
+ setLoadModelError(undefined)
} else {
startModel(String(selectedModel?.id))
window.core?.api?.startServer({
@@ -350,7 +357,9 @@ const LocalServerScreen = () => {
) : (
-
+
+
+
)}
@@ -364,7 +373,43 @@ const LocalServerScreen = () => {
)}
>
+
+
+
+
+ You can concurrently send requests to one active local model and
+ multiple remote models.
+
+
+ {loadModelError && (
+
+
+
+ Model failed to start. Access{' '}
+ setModalTroubleShooting(true)}
+ >
+ troubleshooting assistance
+
+
+
+ )}
{componentDataEngineSetting.filter(
(x) => x.name === 'prompt_template'
@@ -393,6 +438,7 @@ const LocalServerScreen = () => {
)}
+
)
}
diff --git a/web/screens/Settings/Advanced/index.tsx b/web/screens/Settings/Advanced/index.tsx
index d2f7d81ee..f6c8fb4d8 100644
--- a/web/screens/Settings/Advanced/index.tsx
+++ b/web/screens/Settings/Advanced/index.tsx
@@ -33,7 +33,10 @@ const Advanced = () => {
} = 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 */}
@@ -133,10 +154,40 @@ const Advanced = () => {
/>
)}
-
{/* 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/tsconfig.json b/web/tsconfig.json
index 26f0e8ef3..1729c971f 100644
--- a/web/tsconfig.json
+++ b/web/tsconfig.json
@@ -17,13 +17,13 @@
"incremental": true,
"plugins": [
{
- "name": "next",
- },
+ "name": "next"
+ }
],
"paths": {
- "@/*": ["./*"],
- },
+ "@/*": ["./*"]
+ }
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
- "exclude": ["node_modules"],
+ "exclude": ["node_modules"]
}
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 277ae1223..dc406a7d2 100644
--- a/web/utils/umami.tsx
+++ b/web/utils/umami.tsx
@@ -1,31 +1,67 @@
import { useEffect } from 'react'
-const Umami = () => {
- useEffect(() => {
- if (!VERSION || !ANALYTICS_HOST || !ANALYTICS_ID) return
- fetch(ANALYTICS_HOST, {
- method: 'POST',
- // eslint-disable-next-line @typescript-eslint/naming-convention
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- payload: {
- website: ANALYTICS_ID,
- hostname: 'jan.ai',
- screen: `${screen.width}x${screen.height}`,
- language: navigator.language,
- referrer: 'index.html',
- data: { version: VERSION },
- type: 'event',
- title: document.title,
- url: 'index.html',
- name: VERSION,
- },
- type: 'event',
- }),
- })
- }, [])
+import Script from 'next/script'
- return <>>
+// Define the type for the umami data object
+interface UmamiData {
+ version: string
+}
+
+declare global {
+ interface Window {
+ umami:
+ | {
+ track: (event: string, data?: UmamiData) => void
+ }
+ | undefined
+ }
+}
+
+const Umami = () => {
+ const appVersion = VERSION
+ const analyticsScriptPath = './umami_script.js'
+ const analyticsId = ANALYTICS_ID
+
+ useEffect(() => {
+ if (!appVersion || !analyticsScriptPath || !analyticsId) return
+
+ const ping = () => {
+ // Check if umami is defined before ping
+ if (window.umami !== null && typeof window.umami !== 'undefined') {
+ window.umami.track(appVersion, {
+ version: appVersion,
+ })
+ }
+ }
+
+ // Wait for umami to be defined before ping
+ if (window.umami !== null && typeof window.umami !== 'undefined') {
+ ping()
+ } else {
+ // Listen for umami script load event
+ document.addEventListener('umami:loaded', ping)
+ }
+
+ // Cleanup function to remove event listener if the component unmounts
+ return () => {
+ document.removeEventListener('umami:loaded', ping)
+ }
+ }, [appVersion, analyticsScriptPath, analyticsId])
+
+ return (
+ <>
+ {appVersion && analyticsScriptPath && analyticsId && (
+ |