Merge dev branch

This commit is contained in:
Daniel 2024-02-10 14:04:10 +08:00
commit 63a101618e
145 changed files with 3721 additions and 1889 deletions

View File

@ -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

View File

@ -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"]
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

85
Dockerfile.gpu Normal file
View File

@ -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

View File

@ -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

View File

@ -43,31 +43,31 @@ Jan is an open-source ChatGPT alternative that runs 100% offline on your compute
<tr style="text-align:center">
<td style="text-align:center"><b>Stable (Recommended)</b></td>
<td style="text-align:center">
<a href='https://github.com/janhq/jan/releases/download/v0.4.5/jan-win-x64-0.4.5.exe'>
<a href='https://github.com/janhq/jan/releases/download/v0.4.6/jan-win-x64-0.4.6.exe'>
<img src='./docs/static/img/windows.png' style="height:14px; width: 14px" />
<b>jan.exe</b>
</a>
</td>
<td style="text-align:center">
<a href='https://github.com/janhq/jan/releases/download/v0.4.5/jan-mac-x64-0.4.5.dmg'>
<a href='https://github.com/janhq/jan/releases/download/v0.4.6/jan-mac-x64-0.4.6.dmg'>
<img src='./docs/static/img/mac.png' style="height:15px; width: 15px" />
<b>Intel</b>
</a>
</td>
<td style="text-align:center">
<a href='https://github.com/janhq/jan/releases/download/v0.4.5/jan-mac-arm64-0.4.5.dmg'>
<a href='https://github.com/janhq/jan/releases/download/v0.4.6/jan-mac-arm64-0.4.6.dmg'>
<img src='./docs/static/img/mac.png' style="height:15px; width: 15px" />
<b>M1/M2</b>
</a>
</td>
<td style="text-align:center">
<a href='https://github.com/janhq/jan/releases/download/v0.4.5/jan-linux-amd64-0.4.5.deb'>
<a href='https://github.com/janhq/jan/releases/download/v0.4.6/jan-linux-amd64-0.4.6.deb'>
<img src='./docs/static/img/linux.png' style="height:14px; width: 14px" />
<b>jan.deb</b>
</a>
</td>
<td style="text-align:center">
<a href='https://github.com/janhq/jan/releases/download/v0.4.5/jan-linux-x86_64-0.4.5.AppImage'>
<a href='https://github.com/janhq/jan/releases/download/v0.4.6/jan-linux-x86_64-0.4.6.AppImage'>
<img src='./docs/static/img/linux.png' style="height:14px; width: 14px" />
<b>jan.AppImage</b>
</a>
@ -76,31 +76,31 @@ Jan is an open-source ChatGPT alternative that runs 100% offline on your compute
<tr style="text-align:center">
<td style="text-align:center"><b>Experimental (Nightly Build)</b></td>
<td style="text-align:center">
<a href='https://delta.jan.ai/latest/jan-win-x64-0.4.5-216.exe'>
<a href='https://delta.jan.ai/latest/jan-win-x64-0.4.6-264.exe'>
<img src='./docs/static/img/windows.png' style="height:14px; width: 14px" />
<b>jan.exe</b>
</a>
</td>
<td style="text-align:center">
<a href='https://delta.jan.ai/latest/jan-mac-x64-0.4.5-216.dmg'>
<a href='https://delta.jan.ai/latest/jan-mac-x64-0.4.6-264.dmg'>
<img src='./docs/static/img/mac.png' style="height:15px; width: 15px" />
<b>Intel</b>
</a>
</td>
<td style="text-align:center">
<a href='https://delta.jan.ai/latest/jan-mac-arm64-0.4.5-216.dmg'>
<a href='https://delta.jan.ai/latest/jan-mac-arm64-0.4.6-264.dmg'>
<img src='./docs/static/img/mac.png' style="height:15px; width: 15px" />
<b>M1/M2</b>
</a>
</td>
<td style="text-align:center">
<a href='https://delta.jan.ai/latest/jan-linux-amd64-0.4.5-216.deb'>
<a href='https://delta.jan.ai/latest/jan-linux-amd64-0.4.6-264.deb'>
<img src='./docs/static/img/linux.png' style="height:14px; width: 14px" />
<b>jan.deb</b>
</a>
</td>
<td style="text-align:center">
<a href='https://delta.jan.ai/latest/jan-linux-x86_64-0.4.5-216.AppImage'>
<a href='https://delta.jan.ai/latest/jan-linux-x86_64-0.4.6-264.AppImage'>
<img src='./docs/static/img/linux.png' style="height:14px; width: 14px" />
<b>jan.AppImage</b>
</a>
@ -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:

View File

@ -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"
}
}

View File

@ -30,6 +30,7 @@ export enum DownloadRoute {
downloadFile = 'downloadFile',
pauseDownload = 'pauseDownload',
resumeDownload = 'resumeDownload',
getDownloadProgress = 'getDownloadProgress',
}
export enum DownloadEvent {

View File

@ -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) => {

View File

@ -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' })
}
})
}

View File

@ -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) => {})
}

View File

@ -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}`)
}
})
}

View File

@ -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',
})

View File

@ -1,15 +1,18 @@
import { DownloadState } from '../types'
/**
* Manages file downloads and network requests.
*/
export class DownloadManager {
public networkRequests: Record<string, any> = {};
public networkRequests: Record<string, any> = {}
public static instance: DownloadManager = new DownloadManager();
public static instance: DownloadManager = new DownloadManager()
public downloadProgressMap: Record<string, DownloadState> = {}
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
}
}

View File

@ -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'
)
}

View File

@ -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',
}

View File

@ -1,2 +1,3 @@
export * from './assistantEntity'
export * from './assistantEvent'
export * from './assistantInterface'

View File

@ -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
}

View File

@ -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',
}

117
docker-compose.yml Normal file
View File

@ -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

View File

@ -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
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

View File

@ -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.

View File

@ -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/).

View File

@ -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.

View File

@ -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

View File

@ -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.

View File

@ -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:
<Tabs groupId="operating-systems">
<TabItem value="mac" label="macOS">

View File

@ -17,4 +17,8 @@ keywords:
]
---
1. You may receive an error response `Error occurred: Unexpected token '<', "<!DOCTYPE"...is not valid JSON`, when you start a chat with OpenAI models. Using a VPN may help fix the issue.
You may receive an error response `Error occurred: Unexpected token '<', "<!DOCTYPE"...is not valid JSON`, when you start a chat with OpenAI models.
1. Check that you added an OpenAI API key. You can get an API key from OpenAI's [developer platform](https://platform.openai.com/). Alternatively, we recommend you download a local model from Jan Hub, which remains free to use and runs on your own computer!
2. Using a VPN may help fix the issue.

View File

@ -0,0 +1,26 @@
---
title: Undefined Issue
slug: /troubleshooting/undefined-issue
description: Undefined issue troubleshooting guide.
keywords:
[
Jan AI,
Jan,
ChatGPT alternative,
local AI,
private AI,
conversational AI,
no-subscription fee,
large language model,
troubleshooting,
undefined issue,
]
---
You may encounter an "undefined" issue when using Jan. Here are some troubleshooting steps to help you resolve the issue.
1. Try wiping the Jan folder and reopening the Jan app and see if the issue persists.
2. If the issue persists, try to go `~/jan/extensions/@janhq/inference-nitro-extensions/dist/bin/<your-os>/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).

View File

@ -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

View File

@ -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

View File

@ -316,4 +316,4 @@ components:
deleted:
type: boolean
description: Indicates whether the assistant was successfully deleted.
example: true
example: true

View File

@ -188,4 +188,4 @@ components:
total_tokens:
type: integer
example: 533
description: Total number of tokens used
description: Total number of tokens used

View File

@ -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"
$ref: "#/components/schemas/MessageFileObject"

View File

@ -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.

View File

@ -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
example: true

View File

@ -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',
}
)
}

View File

@ -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')
)

View File

@ -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.

View File

@ -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"

View File

@ -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

View File

@ -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,
})
})

View File

@ -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,
})
})

View File

@ -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 })
})

View File

@ -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,
})
})

View File

@ -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,
})
})

View File

@ -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)

View File

@ -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 })
})

View File

@ -7,10 +7,11 @@
"author": "Jan <service@jan.ai>",
"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"
]
}

View File

@ -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",
],

View File

@ -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);

View File

@ -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"

View File

@ -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<void> {
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<void> {
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<void> {
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<void> {
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<any> {
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<string[]> {
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<ThreadMessage[]> {
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) {

View File

@ -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",

View File

@ -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(),

View File

@ -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.

View File

@ -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",

View File

@ -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,

View File

@ -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 {

View File

@ -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<void> {
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<string, any> = DEFALT_SETTINGS): Record<string, any> {
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<void> {
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<void> {
.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();
}

View File

@ -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": {

View File

@ -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"
},

View File

@ -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"
}
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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<void> {
// 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<Model[]> {
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)
}
)
}
}
}

View File

@ -8,7 +8,7 @@
"forceConsistentCasingInFileNames": true,
"strict": false,
"skipLibCheck": true,
"rootDir": "./src"
"rootDir": "./src",
},
"include": ["./src"]
"include": ["./src"],
}

View File

@ -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: {

View File

@ -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"
]
}

View File

@ -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,
};
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 MiB

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 MiB

After

Width:  |  Height:  |  Size: 356 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 352 KiB

View File

@ -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",

0
pre-install/.gitkeep Normal file
View File

47
server/helpers/setup.ts Normal file
View File

@ -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");
}

View File

@ -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({

View File

@ -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 }));

70
server/middleware/s3.ts Normal file
View File

@ -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]);
}
};

View File

@ -1,5 +0,0 @@
{
"watch": ["main.ts", "v1"],
"ext": "ts, json",
"exec": "tsc && node ./build/main.js"
}

View File

@ -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"
]
}

View File

@ -20,5 +20,5 @@
// "sourceMap": true,
"include": ["./**/*.ts"],
"exclude": ["core", "build", "dist", "tests", "node_modules"]
"exclude": ["core", "build", "dist", "tests", "node_modules", "extensions"]
}

View File

@ -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;
}

View File

@ -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 {

View File

@ -156,7 +156,10 @@ export default function CardSidebar({
</>
) : (
<>
Opens <span className="lowercase">{title}.json.</span>
Opens{' '}
<span className="lowercase">
{title === 'Tools' ? 'assistant' : title}.json.
</span>
&nbsp;Changes affect all new threads.
</>
)}

View File

@ -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<Model | undefined>(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 = ({
</SelectTrigger>
<SelectPortal>
<SelectContent className="right-2 block w-full min-w-[450px] pr-0">
<div className="flex w-full items-center space-x-2 px-4 py-2">
<MonitorIcon size={20} className="text-muted-foreground" />
<span>Local</span>
<div className="relative px-2 py-2 dark:bg-secondary/50">
<ul className="inline-flex w-full space-x-2 rounded-lg bg-zinc-100 px-1 dark:bg-secondary">
{engineOptions.map((name, i) => {
return (
<li
className={twMerge(
'relative my-1 flex w-full cursor-pointer items-center justify-center space-x-2 px-2 py-2',
isTabActive === i &&
'rounded-md bg-background dark:bg-white'
)}
key={i}
onClick={() => setIsTabActive(i)}
>
{i === 0 ? (
<MonitorIcon
size={20}
className="z-50 text-muted-foreground"
/>
) : (
<GlobeIcon
size={20}
className="z-50 text-muted-foreground"
/>
)}
<span
className={twMerge(
'relative z-50 font-medium text-muted-foreground',
isTabActive === i &&
'font-bold text-foreground dark:text-black'
)}
>
{name}
</span>
</li>
)
})}
</ul>
</div>
<div className="border-b border-border" />
{downloadedModels.length === 0 ? (
<div className="px-4 py-2">
<p>{`Oops, you don't have a model yet.`}</p>
</div>
) : (
<SelectGroup>
{downloadedModels.map((x, i) => (
<SelectItem
key={i}
value={x.id}
className={twMerge(
x.id === selectedModel?.id && 'bg-secondary'
)}
>
<div className="flex w-full justify-between">
<span className="line-clamp-1 block">{x.name}</span>
<div className="space-x-2">
<span className="font-bold text-muted-foreground">
{toGibibytes(x.metadata.size)}
</span>
{x.engine == InferenceEngine.nitro && (
<ModelLabel size={x.metadata.size} />
<SelectGroup className="py-2">
<>
{modelOptions.map((x, i) => (
<div
key={i}
className={twMerge(
x.id === selectedModel?.id && 'bg-secondary',
'hover:bg-secondary'
)}
>
<SelectItem
value={x.id}
className={twMerge(
x.id === selectedModel?.id && 'bg-secondary',
'my-0 pb-8 pt-4'
)}
>
<div className="relative flex w-full justify-between">
{x.engine === InferenceEngine.openai && (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="absolute top-1"
>
<path
d="M18.5681 8.18423C18.7917 7.51079 18.8691 6.79739 18.795 6.09168C18.7209 5.38596 18.497 4.70419 18.1384 4.0919C17.6067 3.16642 16.7948 2.43369 15.8199 1.99936C14.8449 1.56503 13.7572 1.45153 12.7135 1.67523C12.1206 1.0157 11.3646 0.523789 10.5214 0.248906C9.67823 -0.0259764 8.77756 -0.0741542 7.90986 0.109212C7.04216 0.292577 6.23798 0.701031 5.57809 1.29355C4.91821 1.88607 4.42584 2.64179 4.15046 3.48481C3.45518 3.62739 2.79834 3.91672 2.22384 4.33347C1.64933 4.75023 1.1704 5.28481 0.81904 5.90148C0.281569 6.82542 0.0518576 7.89634 0.163116 8.95943C0.274374 10.0225 0.720837 11.0227 1.43796 11.8153C1.21351 12.4884 1.13539 13.2017 1.20883 13.9074C1.28227 14.6132 1.50557 15.2951 1.86379 15.9076C2.39616 16.8334 3.20872 17.5663 4.18438 18.0006C5.16004 18.4349 6.24841 18.5483 7.29262 18.3243C7.76367 18.8548 8.34248 19.2786 8.99038 19.5676C9.63828 19.8566 10.3404 20.004 11.0498 20C12.1195 20.001 13.1618 19.662 14.0263 19.032C14.8909 18.4021 15.5329 17.5137 15.8596 16.4951C16.5548 16.3523 17.2116 16.0629 17.786 15.6461C18.3605 15.2294 18.8395 14.6949 19.191 14.0784C19.7222 13.1558 19.9479 12.0889 19.836 11.0303C19.7242 9.97163 19.2804 8.9754 18.5681 8.18423ZM11.0498 18.691C10.1737 18.6924 9.32512 18.3853 8.65279 17.8236L8.77104 17.7566L12.753 15.4581C12.8521 15.4 12.9343 15.3171 12.9917 15.2176C13.0491 15.118 13.0796 15.0053 13.0802 14.8904V9.27631L14.7635 10.2501C14.7719 10.2544 14.7791 10.2605 14.7846 10.268C14.7901 10.2755 14.7937 10.2843 14.7952 10.2935V14.9456C14.7931 15.9383 14.3978 16.8898 13.6959 17.5917C12.9939 18.2936 12.0425 18.6889 11.0498 18.691ZM2.99921 15.2531C2.55985 14.4945 2.4021 13.6052 2.55371 12.7417L2.67204 12.8127L6.65787 15.1112C6.7565 15.1691 6.86877 15.1996 6.98312 15.1996C7.09747 15.1996 7.20975 15.1691 7.30837 15.1112L12.1774 12.3041V14.2478C12.1769 14.2579 12.1742 14.2677 12.1694 14.2766C12.1646 14.2855 12.1579 14.2932 12.1497 14.2991L8.11654 16.6251C7.25581 17.121 6.2335 17.255 5.27405 16.9978C4.3146 16.7405 3.49644 16.1131 2.99921 15.2531ZM1.95054 6.57965C2.39294 5.81612 3.09123 5.23375 3.92179 4.93565V9.66665C3.92029 9.78094 3.94949 9.89355 4.00635 9.99271C4.06321 10.0919 4.14564 10.174 4.24504 10.2304L9.09037 13.0256L7.40696 13.9994C7.39785 14.0042 7.38769 14.0068 7.37737 14.0068C7.36706 14.0068 7.3569 14.0042 7.34779 13.9994L3.32254 11.6773C2.46343 11.1793 1.83666 10.3612 1.57951 9.40204C1.32236 8.44291 1.45577 7.42095 1.95054 6.55998V6.57965ZM15.7808 9.79281L10.9197 6.96998L12.5992 5.99998C12.6083 5.99514 12.6185 5.99261 12.6288 5.99261C12.6391 5.99261 12.6493 5.99514 12.6584 5.99998L16.6836 8.32606C17.2991 8.68119 17.8008 9.20407 18.1303 9.83365C18.4597 10.4632 18.6032 11.1735 18.5441 11.8816C18.485 12.5898 18.2257 13.2664 17.7964 13.8327C17.3672 14.3989 16.7857 14.8314 16.1199 15.0796V10.3486C16.1164 10.2345 16.0833 10.1232 16.0238 10.0258C15.9644 9.92833 15.8807 9.8481 15.7808 9.79281ZM17.4564 7.27356L17.338 7.20256L13.3601 4.8844C13.2609 4.82617 13.1479 4.79547 13.0329 4.79547C12.9178 4.79547 12.8049 4.82617 12.7056 4.8844L7.84071 7.6914V5.74781C7.83967 5.73793 7.84132 5.72795 7.84549 5.71893C7.84965 5.70991 7.85618 5.70218 7.86437 5.69656L11.8896 3.3744C12.5066 3.01899 13.2119 2.84659 13.9232 2.87736C14.6345 2.90813 15.3224 3.14079 15.9063 3.54813C16.4903 3.95548 16.9461 4.52066 17.2206 5.17759C17.4952 5.83452 17.577 6.55602 17.4565 7.25773L17.4564 7.27356ZM6.92196 10.7191L5.23862 9.74931C5.2302 9.74424 5.223 9.73738 5.21753 9.72921C5.21205 9.72105 5.20845 9.71178 5.20696 9.70206V5.06181C5.20788 4.34996 5.41144 3.65307 5.79383 3.05265C6.17622 2.45222 6.72164 1.97305 7.36632 1.67118C8.011 1.3693 8.7283 1.2572 9.43434 1.34796C10.1404 1.43873 10.806 1.72861 11.3534 2.18373L11.235 2.25081L7.25321 4.54915C7.1541 4.60727 7.07182 4.69017 7.01445 4.78971C6.95707 4.88925 6.92658 5.00201 6.92596 5.1169L6.92196 10.7191ZM7.83662 8.74798L10.005 7.49815L12.1774 8.74798V11.2475L10.0129 12.4972L7.84062 11.2475L7.83662 8.74798Z"
fill="#18181B"
/>
</svg>
)}
<div
className={twMerge(
x.engine === InferenceEngine.openai && 'pl-8'
)}
>
<span className="line-clamp-1 block">
{x.name}
</span>
<div className="absolute right-0 top-2 space-x-2">
<span className="font-bold text-muted-foreground">
{toGibibytes(x.metadata.size)}
</span>
{x.engine == InferenceEngine.nitro && (
<ModelLabel size={x.metadata.size} />
)}
</div>
</div>
</div>
</SelectItem>
<div
className={twMerge(
'absolute -mt-6 inline-flex items-center space-x-2 px-4 pb-2 text-muted-foreground',
x.engine === InferenceEngine.openai && 'left-8'
)}
>
<span className="text-xs">{x.id}</span>
{clipboard.copied && copyId === x.id ? (
<CheckIcon size={16} className="text-green-600" />
) : (
<CopyIcon
size={16}
className="z-20 cursor-pointer"
onClick={() => {
clipboard.copy(x.id)
setCopyId(x.id)
}}
/>
)}
</div>
</div>
</SelectItem>
))}
))}
</>
</SelectGroup>
)}
<div className="border-b border-border" />
<div className="w-full px-4 py-2">
<div className="flex w-full space-x-2 px-4 py-2">
<Button
block
themes="secondary"
onClick={() => setMainViewState(MainViewState.Settings)}
>
<FoldersIcon size={20} className="mr-2" />
<span>My Models</span>
</Button>
<Button
block
className="bg-blue-100 font-bold text-blue-600 hover:bg-blue-100 hover:text-blue-600"
onClick={() => setMainViewState(MainViewState.Hub)}
>
Explore The Hub
<LayoutGridIcon size={20} className="mr-2" />
<span>Explore The Hub</span>
</Button>
</div>
</SelectContent>

View File

@ -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 (
<Fragment>
{downloadStates?.length > 0 && (
{Object.values(downloadStates)?.length > 0 && (
<Modal>
<ModalTrigger asChild>
<div className="relative block">
<Button size="sm" themes="outline">
<span>{downloadStates.length} Downloading model</span>
<span>
{Object.values(downloadStates).length} Downloading model
</span>
</Button>
<span
className="absolute left-0 h-full rounded-md rounded-l-md bg-primary/20"
@ -55,40 +57,38 @@ export default function DownloadingState() {
<ModalHeader>
<ModalTitle>Downloading model</ModalTitle>
</ModalHeader>
{downloadStates.map((item, i) => {
return (
<div className="pt-2" key={i}>
<Progress
className="mb-2 h-2"
value={
formatDownloadPercentage(item?.percent, {
hidePercentage: true,
}) as number
}
/>
<div className="flex items-center justify-between gap-x-2">
<div className="flex gap-x-2">
<p className="line-clamp-1">{item?.modelId}</p>
<span>{formatDownloadPercentage(item?.percent)}</span>
</div>
<Button
themes="outline"
size="sm"
onClick={() => {
if (item?.modelId) {
const model = downloadingModels.find(
(model) => model.id === item.modelId
)
if (model) abortModelDownload(model)
}
}}
>
Cancel
</Button>
{Object.values(downloadStates).map((item, i) => (
<div className="pt-2" key={i}>
<Progress
className="mb-2 h-2"
value={
formatDownloadPercentage(item?.percent, {
hidePercentage: true,
}) as number
}
/>
<div className="flex items-center justify-between gap-x-2">
<div className="flex gap-x-2">
<p className="line-clamp-1">{item?.modelId}</p>
<span>{formatDownloadPercentage(item?.percent)}</span>
</div>
<Button
themes="outline"
size="sm"
onClick={() => {
if (item?.modelId) {
const model = downloadingModels.find(
(model) => model.id === item.modelId
)
if (model) abortModelDownload(model)
}
}}
>
Cancel
</Button>
</div>
)
})}
</div>
))}
</ModalContent>
</Modal>
)}

View File

@ -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<string, never>) => {
const total = parseInt(gpu.memoryTotal)
const free = parseInt(gpu.memoryFree)
if (!total || !free) return 0
return Math.round(((total - free) / total) * 100)
}
return (
<div className="fixed bottom-0 left-16 z-20 flex h-12 w-[calc(100%-64px)] items-center justify-between border-t border-border bg-background/80 px-3">
<div className="flex flex-shrink-0 items-center gap-x-2">
@ -100,7 +108,7 @@ const BottomBar = () => {
)}
{downloadedModels.length === 0 &&
!stateModel.loading &&
downloadStates.length === 0 && (
Object.values(downloadStates).length === 0 && (
<Button
size="sm"
themes="outline"
@ -117,6 +125,17 @@ const BottomBar = () => {
<SystemItem name="CPU:" value={`${cpu}%`} />
<SystemItem name="Mem:" value={`${ram}%`} />
</div>
{gpus.length > 0 && (
<div className="flex items-center gap-x-2">
{gpus.map((gpu, index) => (
<SystemItem
key={index}
name={`GPU ${gpu.id}:`}
value={`${gpu.utilization}% Util, ${calculateGpuMemoryUsage(gpu)}% Mem`}
/>
))}
</div>
)}
{/* VERSION is defined by webpack, please see next.config.js */}
<span className="text-xs text-muted-foreground">
Jan v{VERSION ?? ''}

View File

@ -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(

View File

@ -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])
}

View File

@ -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<Props> = ({ 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) {
</Modal>
)
}
export default ModalCancelDownload

View File

@ -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 (
<>
<div className="absolute -top-11 right-2">
<Button
themes="outline"
className="bg-white dark:bg-secondary/50"
onClick={() => {
clipboard.copy(logs.slice(-50) ?? '')
}}
>
<div className="flex items-center space-x-2">
{clipboard.copied ? (
<>
<CheckIcon size={14} className="text-green-600" />
<span>Copying...</span>
</>
) : (
<>
<CopyIcon size={14} />
<span>Copy All</span>
</>
)}
</div>
</Button>
</div>
<div className="overflow-hidden">
{logs.length > 1 ? (
<div className="h-full overflow-auto">
<code className="inline-block whitespace-pre-line text-xs">
{logs.slice(-100).map((log, i) => {
return (
<p key={i} className="my-2 leading-relaxed">
{log}
</p>
)
})}
</code>
</div>
) : (
<div className="mt-24 flex flex-col items-center justify-center">
<svg
width="115"
height="115"
viewBox="0 0 115 115"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="57.4999" cy="57.5009" r="50.2314" fill="#DADADA" />
<circle
cx="57.5"
cy="57.5"
r="55.9425"
fill="#E7E7E7"
stroke="white"
strokeWidth="3.1151"
/>
<mask
id="mask0_1206_120508"
maskUnits="userSpaceOnUse"
x="3"
y="3"
width="109"
height="109"
>
<circle cx="57.4993" cy="57.5003" r="54.1253" fill="white" />
</mask>
<g mask="url(#mask0_1206_120508)">
<path
d="M47.5039 116.445H58.5351L74.3593 39.8282L63.7828 37.6406L47.5039 116.445Z"
fill="#8D8D8D"
/>
<path
d="M72.165 39.4563L74.3716 39.8457L72.4246 38.418L72.165 39.4563Z"
fill="#8D8D8D"
/>
<path
d="M45.6797 114.947H56.7108L72.4257 38.4193L61.9585 36.1426L45.6797 114.947Z"
fill="url(#paint0_linear_1206_120508)"
/>
<path
d="M93.1887 90.6726L26.5474 76.906L24.6602 75.2136L31.7058 51.9418L34.7984 52.1448L30.0296 49.6041L32.757 36.0039L99.3983 49.7705L101.29 51.467L98.5257 64.844L93.2456 64.9414L96.1515 65.4974L98.0387 67.1898L93.1887 90.6726Z"
fill="#8D8D8D"
/>
<path
d="M91.3015 88.9801L24.6602 75.2136L29.8186 50.2454L32.9112 50.4483L30.3299 47.7656L32.757 36.0039L99.3983 49.7705L96.6345 63.1475L91.3583 63.2449L96.1515 65.4974L91.3015 88.9801Z"
fill="url(#paint1_linear_1206_120508)"
/>
<path
d="M92.7826 63.1065C92.7826 63.1065 92.7298 63.1065 92.6243 63.1065L92.1576 63.0741L90.3637 62.9279L89.069 62.8143L87.5308 62.6317C86.431 62.4937 85.1688 62.3638 83.7929 62.1365L81.6216 61.8078C80.8667 61.6901 80.0875 61.5359 79.2798 61.4019C77.6564 61.126 75.9396 60.7647 74.1295 60.3995C70.5133 59.6324 66.5563 58.703 62.4247 57.6518C54.1655 55.5252 46.7221 53.4797 41.2918 52.1525C39.9403 51.8075 38.7065 51.5275 37.6391 51.2677C36.5717 51.008 35.6098 50.797 34.8631 50.6306L33.1098 50.2247L32.6552 50.1151C32.6025 50.1035 32.5508 50.0872 32.501 50.0664C32.5545 50.0686 32.6076 50.0768 32.6593 50.0908L33.1219 50.176L34.8834 50.5291C35.6504 50.6833 36.5879 50.8822 37.6675 51.1297C38.7471 51.3773 39.9849 51.6452 41.3446 51.978C46.783 53.2605 54.2386 55.2816 62.4937 57.4043C66.6253 58.4554 70.5742 59.3929 74.1823 60.1722C75.9924 60.5415 77.7051 60.9109 79.3245 61.195C80.1362 61.3411 80.9114 61.4953 81.6622 61.6008L83.8254 61.9458C85.2012 62.1852 86.4553 62.3273 87.5552 62.4775L89.0893 62.6804L90.3799 62.8143L92.1698 63.0091L92.6324 63.0659L92.7826 63.1065Z"
fill="#A9A9A9"
/>
<path
d="M96.131 60.9773C96.0789 60.948 96.0288 60.9155 95.9808 60.8799L95.5749 60.5755C95.2056 60.3117 94.678 59.8937 93.9515 59.3985C91.9511 57.9951 89.8665 56.7156 87.7095 55.5673C84.5067 53.8752 81.1551 52.4813 77.697 51.4032C73.4578 50.101 69.0819 49.2947 64.6569 49.0005C59.9449 48.6555 55.4481 49.1142 51.353 49.2075C49.411 49.2762 47.4666 49.2369 45.529 49.0898C41.3921 48.7494 37.3342 47.762 33.5035 46.1636C32.6918 45.8267 32.0952 45.5426 31.6853 45.3519L31.2226 45.1165L31.0684 45.0312C31.1262 45.0462 31.1821 45.068 31.2348 45.0962L31.7096 45.3072C32.1155 45.4939 32.7364 45.7658 33.54 46.0865C35.8079 46.9951 38.1522 47.7 40.5451 48.1929C42.1954 48.5323 43.8654 48.7681 45.5452 48.899C47.4754 49.0336 49.4114 49.0647 51.3449 48.9924C55.4278 48.8869 59.9368 48.4201 64.6731 48.7651C69.1177 49.0615 73.5121 49.8788 77.766 51.2002C81.2331 52.295 84.5906 53.7108 87.7947 55.4293C89.949 56.5876 92.0247 57.8864 94.0083 59.3173C94.7105 59.8206 95.2259 60.2549 95.5912 60.5349L95.997 60.8596C96.0446 60.8953 96.0894 60.9347 96.131 60.9773Z"
fill="#A9A9A9"
/>
<path
d="M63.9192 43.0816C63.8188 43.1282 63.7141 43.1649 63.6067 43.1912L62.6935 43.4631C62.2876 43.5849 61.8128 43.7188 61.2405 43.8487C60.6683 43.9786 60.023 44.1572 59.2924 44.287C58.5619 44.4169 57.7745 44.5914 56.91 44.6929C56.0456 44.7943 55.1283 44.9364 54.1583 45.046C52.0463 45.2502 49.9242 45.3328 47.8027 45.2936C45.6814 45.2371 43.565 45.0623 41.4632 44.77C40.4973 44.6158 39.58 44.4818 38.7278 44.2951C37.8755 44.1084 37.1043 43.9461 36.3697 43.7675C35.6351 43.589 35.0101 43.4063 34.446 43.244C33.8818 43.0816 33.3989 42.9315 33.0092 42.7975L32.1082 42.485C32.0022 42.4531 31.8991 42.4123 31.7998 42.3633C31.9103 42.3761 32.0191 42.4006 32.1245 42.4363L33.0377 42.7042C33.4435 42.826 33.9143 42.968 34.4825 43.1101C35.0507 43.2521 35.7001 43.4469 36.4103 43.5971C37.1206 43.7472 37.916 43.942 38.7683 44.0922C39.6206 44.2424 40.5338 44.3966 41.4957 44.5427C43.5877 44.8202 45.693 44.9868 47.8027 45.0419C49.9143 45.0792 52.0264 45.0034 54.1299 44.8146C55.0959 44.7091 56.0172 44.6239 56.8735 44.4859C57.7299 44.3479 58.5253 44.2302 59.2518 44.08C59.9783 43.9299 60.6277 43.8 61.1999 43.6742C61.7722 43.5484 62.2633 43.4347 62.661 43.3292L63.5823 43.106C63.6933 43.0854 63.8063 43.0772 63.9192 43.0816Z"
fill="#A9A9A9"
/>
<path
d="M46.1782 66.8891C46.1782 66.8891 46.2837 66.9459 46.4786 67.0677L47.3552 67.6075C48.1263 68.0742 49.2546 68.7885 50.7644 69.5962C52.5839 70.6072 54.5341 71.3624 56.56 71.8405C57.1712 71.9765 57.7909 72.0714 58.4148 72.1246C59.0677 72.2062 59.729 72.063 60.2898 71.7188C60.5621 71.5185 60.7593 71.2327 60.8499 70.9071C60.9379 70.5705 60.9379 70.2169 60.8499 69.8803C60.6372 69.1763 60.1849 68.5689 59.5714 68.1635C54.4901 64.7949 47.3349 62.3395 39.2381 62.1122C38.2275 62.0797 37.2088 62.0797 36.1698 62.1122C35.1309 62.1447 34.0756 62.3882 33.3816 63.1593C33.2064 63.3413 33.0692 63.5562 32.9779 63.7918C32.8866 64.0273 32.8431 64.2786 32.85 64.5311C32.8877 65.0471 33.0864 65.5383 33.4181 65.9354C34.0716 66.7714 35.0132 67.3518 35.9182 67.9646C39.5709 70.4403 43.4387 72.8998 47.7895 74.6044C52.1402 76.309 56.7629 77.0761 61.1177 76.4308C62.1922 76.2718 63.2512 76.0219 64.2834 75.684C65.3 75.3967 66.2119 74.8217 66.9093 74.0281C67.5327 73.2028 67.8626 72.1929 67.8468 71.1587C67.8497 70.1451 67.625 69.1437 67.1893 68.2284C66.3039 66.4723 64.9462 64.9979 63.2688 63.971C61.6898 62.9951 59.9765 62.2554 58.1834 61.7753C56.4585 61.2761 54.7499 60.919 53.1183 60.5578L48.4226 59.4944L34.2542 56.276L30.4067 55.3872L29.4083 55.1518C29.181 55.099 29.0674 55.0625 29.0674 55.0625L29.4124 55.1274L30.4189 55.3385L34.2786 56.1827L48.4632 59.324L53.1589 60.3711C54.7824 60.7404 56.5032 61.0895 58.2402 61.5887C60.0548 62.0698 61.7887 62.8151 63.3865 63.8006C65.1016 64.8462 66.4904 66.3503 67.3963 68.1432C67.8467 69.0889 68.0797 70.1234 68.0782 71.1709C68.0964 72.2575 67.7507 73.319 67.096 74.1864C66.3707 75.017 65.4208 75.6203 64.3605 75.9235C63.3146 76.2697 62.2404 76.5237 61.1502 76.6824C56.7426 77.3399 52.0631 76.5566 47.7002 74.8479C43.3372 73.1393 39.441 70.6798 35.7965 68.1919C34.8955 67.5669 33.9376 66.9743 33.2477 66.0936C32.8831 65.658 32.6657 65.1181 32.6267 64.5514C32.6196 64.2683 32.6687 63.9866 32.7711 63.7226C32.8735 63.4586 33.0272 63.2174 33.2233 63.0132C33.599 62.6113 34.0734 62.3147 34.5992 62.1528C35.1094 61.9991 35.6373 61.9118 36.1698 61.893C37.2169 61.8525 38.2438 61.8565 39.2584 61.893C47.4039 62.1406 54.5794 64.6163 59.6932 68.0336C60.3373 68.4648 60.8099 69.1082 61.0285 69.8519C61.1239 70.2164 61.1239 70.5994 61.0285 70.9639C60.9286 71.3211 60.7095 71.6333 60.4075 71.8487C59.8169 72.2131 59.12 72.3662 58.431 72.2829C57.7989 72.2243 57.1713 72.1253 56.5519 71.9867C54.5186 71.4945 52.5639 70.7213 50.7441 69.6895C49.2343 68.8778 48.1142 68.1391 47.3512 67.6602C46.9778 67.4208 46.6896 67.2544 46.4907 67.1002C46.2919 66.9459 46.1782 66.8891 46.1782 66.8891Z"
fill="#A9A9A9"
/>
<path
d="M94.8364 71.2204C94.6993 71.2055 94.5635 71.1797 94.4305 71.1433C94.1789 71.0743 93.8014 71.0012 93.3185 70.916C91.9393 70.7187 90.5384 70.7297 89.1625 70.9484C87.1263 71.2911 85.1507 71.9282 83.2979 72.8397C81.0433 73.9901 78.866 75.2861 76.7799 76.7197C74.6823 78.1612 72.4837 79.4497 70.201 80.5753C68.3181 81.4721 66.3087 82.0743 64.243 82.3611C62.8484 82.5487 61.4325 82.5089 60.0505 82.2434C59.6768 82.1692 59.3081 82.0716 58.9466 81.9512C58.8182 81.9168 58.6932 81.8706 58.5732 81.8132C58.7037 81.8336 58.8326 81.8634 58.9588 81.9025C59.2104 81.9755 59.5838 82.0567 60.0668 82.15C61.4407 82.3746 62.841 82.3869 64.2187 82.1866C66.2604 81.8789 68.2442 81.266 70.1036 80.3683C72.3696 79.236 74.5543 77.9477 76.6419 76.5127C78.7383 75.0733 80.9295 73.777 83.2005 72.6327C85.0755 71.7192 87.077 71.0926 89.1382 70.7739C90.5308 70.568 91.9473 70.5845 93.3347 70.8226C93.72 70.8867 94.1009 70.9748 94.4752 71.0864C94.5995 71.1198 94.7204 71.1646 94.8364 71.2204Z"
fill="#A9A9A9"
/>
<path
d="M93.6026 77.826C93.6026 77.8504 93.286 77.7205 92.7016 77.5906C91.8761 77.4114 91.0248 77.3839 90.1894 77.5095C88.9516 77.719 87.7468 78.0901 86.6057 78.6134C85.195 79.2299 83.8293 79.9446 82.5187 80.7523C81.1063 81.5883 79.7589 82.4041 78.4602 83.025C77.321 83.5882 76.1214 84.0199 74.8846 84.3116C74.0488 84.5016 73.1926 84.5861 72.3358 84.5632C72.1034 84.5575 71.8716 84.5372 71.6418 84.5024C71.5603 84.4985 71.4797 84.4835 71.4023 84.4577C71.4023 84.4293 71.7392 84.4577 72.3358 84.4577C73.1828 84.4453 74.0257 84.3364 74.8481 84.133C76.0637 83.8193 77.242 83.3757 78.3628 82.8099C79.6371 82.1849 80.9724 81.3692 82.3888 80.529C83.7103 79.7132 85.0914 78.9983 86.5204 78.3902C87.683 77.8677 88.9122 77.5085 90.1731 77.3228C91.0279 77.2097 91.8965 77.2648 92.73 77.4851C92.9548 77.5461 93.1757 77.6207 93.3916 77.7083C93.4671 77.7375 93.5381 77.7771 93.6026 77.826Z"
fill="#A9A9A9"
/>
<path
d="M72.1531 44.1988C72.1531 44.2678 69.584 43.7645 66.4468 43.0746C63.3095 42.3846 60.7648 41.7718 60.7932 41.7069C60.8216 41.6419 63.3623 42.1411 66.4995 42.8311C69.6368 43.521 72.1531 44.1339 72.1531 44.1988Z"
fill="#A9A9A9"
/>
<path
d="M87.7278 22.8493C87.9286 21.4011 85.8726 20.21 84.3238 20.0848C83.5886 20.0273 82.8227 20.139 82.1249 19.8987C80.6135 19.3743 80.011 17.432 78.5371 16.8128C77.4342 16.3526 76.1544 16.762 75.0957 17.2967C74.0371 17.8313 72.9717 18.5046 71.7769 18.5825C70.7557 18.6468 69.6086 18.2644 68.7133 18.7686C68.0326 19.1442 67.6922 19.9224 67.059 20.3792C66.4259 20.836 65.6498 20.9206 64.8941 20.9612C64.1384 21.0018 63.3521 21.012 62.6611 21.3233C61.9701 21.6346 61.4017 22.3655 61.5446 23.1031L87.7278 22.8493Z"
fill="#ABABAB"
/>
<path
d="M39.1881 32.5312C39.3293 31.4869 37.8655 30.6287 36.7662 30.5385C36.2413 30.4963 35.6955 30.5769 35.1993 30.4022C34.121 30.0182 33.6916 28.6264 32.64 28.1791C31.8556 27.847 30.951 28.1426 30.1895 28.5285C29.428 28.9144 28.6741 29.4001 27.8229 29.4538C27.0824 29.5018 26.2789 29.2254 25.632 29.5901C25.1491 29.8608 24.8972 30.4195 24.4525 30.742C24.0078 31.0645 23.4486 31.126 22.9085 31.1624C22.3684 31.1989 21.8092 31.1989 21.3187 31.4254C20.8282 31.652 20.4198 32.1741 20.5229 32.7078L39.1881 32.5312Z"
fill="#ABABAB"
/>
<path
d="M76.46 61.6777L78.8178 62.1824L80.1702 66.9562L80.2674 66.977L83.4556 63.1752L85.8134 63.6799L80.8041 69.3391L80.0506 72.8588L77.9602 72.4114L78.7137 68.8917L76.46 61.6777Z"
fill="white"
/>
<path
d="M67.148 61.4992L67.5195 59.7637L75.6965 61.514L75.325 63.2496L72.2769 62.5971L70.5171 70.8178L68.4364 70.3724L70.1962 62.1517L67.148 61.4992Z"
fill="white"
/>
<path
d="M56.9049 67.9016L59.0361 57.9453L62.9642 58.7862C63.7193 58.9478 64.3318 59.2297 64.8016 59.632C65.272 60.0309 65.5922 60.5147 65.762 61.0832C65.9357 61.6491 65.9518 62.2627 65.8103 62.9238C65.6688 63.585 65.4013 64.1379 65.0078 64.5824C64.6144 65.0269 64.1169 65.3323 63.5153 65.4984C62.9169 65.6652 62.2353 65.6667 61.4705 65.503L58.9668 64.967L59.3279 63.2801L61.4913 63.7432C61.8964 63.8299 62.2451 63.8317 62.5375 63.7485C62.8338 63.6628 63.0734 63.5091 63.2565 63.2872C63.4435 63.0629 63.572 62.7871 63.6421 62.4597C63.7128 62.1291 63.7082 61.8265 63.628 61.5517C63.5518 61.2744 63.3954 61.0392 63.1587 60.8462C62.9228 60.65 62.6007 60.5082 62.1923 60.4207L60.7728 60.1169L59.0099 68.3522L56.9049 67.9016Z"
fill="white"
/>
<path
d="M46.5049 55.2637L49.1009 55.8194L50.4108 63.0957L50.5275 63.1206L54.7013 57.0182L57.2973 57.5739L55.1661 67.5302L53.1243 67.0931L54.5114 60.6128L54.4288 60.5951L50.4754 66.4753L49.0851 66.1776L47.8905 59.1701L47.8078 59.1524L46.4154 65.657L44.3736 65.2199L46.5049 55.2637Z"
fill="white"
/>
<path
d="M35.9977 63.425L38.1289 53.4688L44.8377 54.9048L44.4662 56.6404L39.8624 55.6549L39.3546 58.0273L43.6132 58.9389L43.2417 60.6744L38.9831 59.7628L38.4742 62.1401L43.0974 63.1297L42.7259 64.8653L35.9977 63.425Z"
fill="white"
/>
</g>
<defs>
<linearGradient
id="paint0_linear_1206_120508"
x1="59.1074"
y1="36.1426"
x2="59.1074"
y2="114.947"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#CFCFCF" />
<stop offset="1" stopColor="#C6C6C6" />
</linearGradient>
<linearGradient
id="paint1_linear_1206_120508"
x1="62.0292"
y1="36.0039"
x2="62.0292"
y2="88.9801"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#DDDDDD" />
<stop offset="1" stopColor="#B6B6B6" />
</linearGradient>
</defs>
</svg>
<p className="mt-4 text-muted-foreground">Empty logs</p>
</div>
)}
</div>
</>
)
}
export default AppLogs

View File

@ -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 (
<>
<div className="absolute -top-11 right-2">
<Button
themes="outline"
className="bg-white dark:bg-secondary/50"
onClick={() => {
clipboard.copy(userAgent ?? '')
}}
>
<div className="flex items-center space-x-2">
{clipboard.copied ? (
<>
<CheckIcon size={14} className="text-green-600" />
<span>Copying...</span>
</>
) : (
<>
<CopyIcon size={14} />
<span>Copy All</span>
</>
)}
</div>
</Button>
</div>
<div>
<p className="leading-relaxed">{userAgent}</p>
</div>
</>
)
}
export default DeviceSpecs

View File

@ -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 (
<Modal open={modalTroubleShooting} onOpenChange={setModalTroubleShooting}>
<ModalContent className="max-w-[60%] pb-4 pt-8">
<ModalHeader>
<ModalTitle>Troubleshooting Assistance</ModalTitle>
</ModalHeader>
<p className="-mt-2 pr-3 leading-relaxed text-muted-foreground">
{`We're here to help! Your report is crucial for debugging and shaping
the next version. Heres how you can report & get further support:`}
</p>
<div className="rounded-lg border border-border p-4 shadow">
<h2 className="font-semibold">Step 1</h2>
<p className="mt-1 text-muted-foreground">
Follow our&nbsp;
<a
href="https://jan.ai/guides/troubleshooting"
target="_blank"
className="text-blue-600 hover:underline dark:text-blue-300"
>
troubleshooting guide
</a>
&nbsp;for step-by-step solutions.
</p>
</div>
<div className="block overflow-hidden rounded-lg border border-border pb-2 pt-4 shadow">
<div className="px-4">
<h2 className="font-semibold">Step 2</h2>
<p className="mt-1 text-muted-foreground">
{`If you can't find what you need in our troubleshooting guide, feel
free reach out to us for extra help:`}
</p>
<ul className="mt-2 list-disc space-y-2 pl-6">
<li>
<p className="font-medium">
Copy your 2-hour logs & device specifications provided below.{' '}
</p>
</li>
<li>
<p className="font-medium">
Go to our&nbsp;
<a
href="https://discord.gg/AsJ8krTT3N"
target="_blank"
className="text-blue-600 hover:underline dark:text-blue-300"
>
Discord
</a>
&nbsp; & send it to #🆘|get-help channel for further support.
</p>
</li>
</ul>
</div>
<div className="flex flex-col pt-4">
{/* TODO @faisal replace this once we have better tabs component UI */}
<div className="relative bg-zinc-100 px-4 py-2 dark:bg-secondary/50">
<ul className="inline-flex space-x-2 rounded-lg bg-zinc-200 px-1 dark:bg-secondary">
{logOption.map((name, i) => {
return (
<li
className="relative cursor-pointer px-4 py-2"
key={i}
onClick={() => setIsTabActivbe(i)}
>
<span
className={twMerge(
'relative z-50 font-medium text-muted-foreground',
isTabActive === i &&
'font-bold text-foreground dark:text-black'
)}
>
{name}
</span>
{isTabActive === i && (
<m.div
className="absolute left-0 top-1 h-[calc(100%-8px)] w-full rounded-md bg-background dark:bg-white"
layoutId="log-state-active"
/>
)}
</li>
)
})}
</ul>
</div>
<ScrollToBottom className={twMerge('relative h-[140px] px-4 py-2')}>
{isTabActive === 0 && <AppLogs />}
{isTabActive === 1 && <ServerLogs limit={50} withCopy />}
{isTabActive === 2 && <DeviceSpecs />}
</ScrollToBottom>
</div>
</div>
</ModalContent>
</Modal>
)
}
export default ModalTroubleShooting

View File

@ -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<Props> = ({ children }) => {
useModels()
useThreads()
useAssistants()
return <Fragment>{children}</Fragment>
}
export default DataLoader

View File

@ -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<ConversationalExtension>(ExtensionTypeEnum.Conversational)
?.saveThread({
@ -143,7 +151,7 @@ export default function EventHandler({ children }: { children: ReactNode }) {
?.addNewMessage(message)
}
},
[updateMessage, updateThreadWaiting]
[updateMessage, updateThreadWaiting, setIsGeneratingResponse, updateThread]
)
useEffect(() => {

View File

@ -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 <EventHandler>{children}</EventHandler>
}
export default EventListenerWrapper

View File

@ -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) => {
<KeyListener>
<FeatureToggleWrapper>
<EventListenerWrapper>
<TooltipProvider delayDuration={0}>{children}</TooltipProvider>
<TooltipProvider delayDuration={0}>
<DataLoader>{children}</DataLoader>
</TooltipProvider>
{!isMac && <GPUDriverPrompt />}
</EventListenerWrapper>
<Toaster />

View File

@ -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 (
<>
<div className="absolute -top-11 right-2">
<Button
themes="outline"
className="bg-white dark:bg-secondary/50"
onClick={() => {
clipboard.copy(logs.slice(-100) ?? '')
}}
>
<div className="flex items-center space-x-2">
{clipboard.copied ? (
<>
<CheckIcon size={14} className="text-green-600" />
<span>Copying...</span>
</>
) : (
<>
<CopyIcon size={14} />
<span>Copy All</span>
</>
)}
</div>
</Button>
</div>
<div className="overflow-hidden">
{logs.length > 1 ? (
<div className="h-full overflow-auto">
<code className="inline-block whitespace-pre-line text-xs">
{logs.slice(-limit).map((log, i) => {
return (
<p key={i} className="my-2 leading-relaxed">
{log}
</p>
)
})}
</code>
</div>
) : (
<div className="mt-24 flex flex-col items-center justify-center">
<svg
width="115"
height="115"
viewBox="0 0 115 115"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="57.4999" cy="57.5009" r="50.2314" fill="#DADADA" />
<circle
cx="57.5"
cy="57.5"
r="55.9425"
fill="#E7E7E7"
stroke="white"
strokeWidth="3.1151"
/>
<mask
id="mask0_1206_120508"
maskUnits="userSpaceOnUse"
x="3"
y="3"
width="109"
height="109"
>
<circle cx="57.4993" cy="57.5003" r="54.1253" fill="white" />
</mask>
<g mask="url(#mask0_1206_120508)">
<path
d="M47.5039 116.445H58.5351L74.3593 39.8282L63.7828 37.6406L47.5039 116.445Z"
fill="#8D8D8D"
/>
<path
d="M72.165 39.4563L74.3716 39.8457L72.4246 38.418L72.165 39.4563Z"
fill="#8D8D8D"
/>
<path
d="M45.6797 114.947H56.7108L72.4257 38.4193L61.9585 36.1426L45.6797 114.947Z"
fill="url(#paint0_linear_1206_120508)"
/>
<path
d="M93.1887 90.6726L26.5474 76.906L24.6602 75.2136L31.7058 51.9418L34.7984 52.1448L30.0296 49.6041L32.757 36.0039L99.3983 49.7705L101.29 51.467L98.5257 64.844L93.2456 64.9414L96.1515 65.4974L98.0387 67.1898L93.1887 90.6726Z"
fill="#8D8D8D"
/>
<path
d="M91.3015 88.9801L24.6602 75.2136L29.8186 50.2454L32.9112 50.4483L30.3299 47.7656L32.757 36.0039L99.3983 49.7705L96.6345 63.1475L91.3583 63.2449L96.1515 65.4974L91.3015 88.9801Z"
fill="url(#paint1_linear_1206_120508)"
/>
<path
d="M92.7826 63.1065C92.7826 63.1065 92.7298 63.1065 92.6243 63.1065L92.1576 63.0741L90.3637 62.9279L89.069 62.8143L87.5308 62.6317C86.431 62.4937 85.1688 62.3638 83.7929 62.1365L81.6216 61.8078C80.8667 61.6901 80.0875 61.5359 79.2798 61.4019C77.6564 61.126 75.9396 60.7647 74.1295 60.3995C70.5133 59.6324 66.5563 58.703 62.4247 57.6518C54.1655 55.5252 46.7221 53.4797 41.2918 52.1525C39.9403 51.8075 38.7065 51.5275 37.6391 51.2677C36.5717 51.008 35.6098 50.797 34.8631 50.6306L33.1098 50.2247L32.6552 50.1151C32.6025 50.1035 32.5508 50.0872 32.501 50.0664C32.5545 50.0686 32.6076 50.0768 32.6593 50.0908L33.1219 50.176L34.8834 50.5291C35.6504 50.6833 36.5879 50.8822 37.6675 51.1297C38.7471 51.3773 39.9849 51.6452 41.3446 51.978C46.783 53.2605 54.2386 55.2816 62.4937 57.4043C66.6253 58.4554 70.5742 59.3929 74.1823 60.1722C75.9924 60.5415 77.7051 60.9109 79.3245 61.195C80.1362 61.3411 80.9114 61.4953 81.6622 61.6008L83.8254 61.9458C85.2012 62.1852 86.4553 62.3273 87.5552 62.4775L89.0893 62.6804L90.3799 62.8143L92.1698 63.0091L92.6324 63.0659L92.7826 63.1065Z"
fill="#A9A9A9"
/>
<path
d="M96.131 60.9773C96.0789 60.948 96.0288 60.9155 95.9808 60.8799L95.5749 60.5755C95.2056 60.3117 94.678 59.8937 93.9515 59.3985C91.9511 57.9951 89.8665 56.7156 87.7095 55.5673C84.5067 53.8752 81.1551 52.4813 77.697 51.4032C73.4578 50.101 69.0819 49.2947 64.6569 49.0005C59.9449 48.6555 55.4481 49.1142 51.353 49.2075C49.411 49.2762 47.4666 49.2369 45.529 49.0898C41.3921 48.7494 37.3342 47.762 33.5035 46.1636C32.6918 45.8267 32.0952 45.5426 31.6853 45.3519L31.2226 45.1165L31.0684 45.0312C31.1262 45.0462 31.1821 45.068 31.2348 45.0962L31.7096 45.3072C32.1155 45.4939 32.7364 45.7658 33.54 46.0865C35.8079 46.9951 38.1522 47.7 40.5451 48.1929C42.1954 48.5323 43.8654 48.7681 45.5452 48.899C47.4754 49.0336 49.4114 49.0647 51.3449 48.9924C55.4278 48.8869 59.9368 48.4201 64.6731 48.7651C69.1177 49.0615 73.5121 49.8788 77.766 51.2002C81.2331 52.295 84.5906 53.7108 87.7947 55.4293C89.949 56.5876 92.0247 57.8864 94.0083 59.3173C94.7105 59.8206 95.2259 60.2549 95.5912 60.5349L95.997 60.8596C96.0446 60.8953 96.0894 60.9347 96.131 60.9773Z"
fill="#A9A9A9"
/>
<path
d="M63.9192 43.0816C63.8188 43.1282 63.7141 43.1649 63.6067 43.1912L62.6935 43.4631C62.2876 43.5849 61.8128 43.7188 61.2405 43.8487C60.6683 43.9786 60.023 44.1572 59.2924 44.287C58.5619 44.4169 57.7745 44.5914 56.91 44.6929C56.0456 44.7943 55.1283 44.9364 54.1583 45.046C52.0463 45.2502 49.9242 45.3328 47.8027 45.2936C45.6814 45.2371 43.565 45.0623 41.4632 44.77C40.4973 44.6158 39.58 44.4818 38.7278 44.2951C37.8755 44.1084 37.1043 43.9461 36.3697 43.7675C35.6351 43.589 35.0101 43.4063 34.446 43.244C33.8818 43.0816 33.3989 42.9315 33.0092 42.7975L32.1082 42.485C32.0022 42.4531 31.8991 42.4123 31.7998 42.3633C31.9103 42.3761 32.0191 42.4006 32.1245 42.4363L33.0377 42.7042C33.4435 42.826 33.9143 42.968 34.4825 43.1101C35.0507 43.2521 35.7001 43.4469 36.4103 43.5971C37.1206 43.7472 37.916 43.942 38.7683 44.0922C39.6206 44.2424 40.5338 44.3966 41.4957 44.5427C43.5877 44.8202 45.693 44.9868 47.8027 45.0419C49.9143 45.0792 52.0264 45.0034 54.1299 44.8146C55.0959 44.7091 56.0172 44.6239 56.8735 44.4859C57.7299 44.3479 58.5253 44.2302 59.2518 44.08C59.9783 43.9299 60.6277 43.8 61.1999 43.6742C61.7722 43.5484 62.2633 43.4347 62.661 43.3292L63.5823 43.106C63.6933 43.0854 63.8063 43.0772 63.9192 43.0816Z"
fill="#A9A9A9"
/>
<path
d="M46.1782 66.8891C46.1782 66.8891 46.2837 66.9459 46.4786 67.0677L47.3552 67.6075C48.1263 68.0742 49.2546 68.7885 50.7644 69.5962C52.5839 70.6072 54.5341 71.3624 56.56 71.8405C57.1712 71.9765 57.7909 72.0714 58.4148 72.1246C59.0677 72.2062 59.729 72.063 60.2898 71.7188C60.5621 71.5185 60.7593 71.2327 60.8499 70.9071C60.9379 70.5705 60.9379 70.2169 60.8499 69.8803C60.6372 69.1763 60.1849 68.5689 59.5714 68.1635C54.4901 64.7949 47.3349 62.3395 39.2381 62.1122C38.2275 62.0797 37.2088 62.0797 36.1698 62.1122C35.1309 62.1447 34.0756 62.3882 33.3816 63.1593C33.2064 63.3413 33.0692 63.5562 32.9779 63.7918C32.8866 64.0273 32.8431 64.2786 32.85 64.5311C32.8877 65.0471 33.0864 65.5383 33.4181 65.9354C34.0716 66.7714 35.0132 67.3518 35.9182 67.9646C39.5709 70.4403 43.4387 72.8998 47.7895 74.6044C52.1402 76.309 56.7629 77.0761 61.1177 76.4308C62.1922 76.2718 63.2512 76.0219 64.2834 75.684C65.3 75.3967 66.2119 74.8217 66.9093 74.0281C67.5327 73.2028 67.8626 72.1929 67.8468 71.1587C67.8497 70.1451 67.625 69.1437 67.1893 68.2284C66.3039 66.4723 64.9462 64.9979 63.2688 63.971C61.6898 62.9951 59.9765 62.2554 58.1834 61.7753C56.4585 61.2761 54.7499 60.919 53.1183 60.5578L48.4226 59.4944L34.2542 56.276L30.4067 55.3872L29.4083 55.1518C29.181 55.099 29.0674 55.0625 29.0674 55.0625L29.4124 55.1274L30.4189 55.3385L34.2786 56.1827L48.4632 59.324L53.1589 60.3711C54.7824 60.7404 56.5032 61.0895 58.2402 61.5887C60.0548 62.0698 61.7887 62.8151 63.3865 63.8006C65.1016 64.8462 66.4904 66.3503 67.3963 68.1432C67.8467 69.0889 68.0797 70.1234 68.0782 71.1709C68.0964 72.2575 67.7507 73.319 67.096 74.1864C66.3707 75.017 65.4208 75.6203 64.3605 75.9235C63.3146 76.2697 62.2404 76.5237 61.1502 76.6824C56.7426 77.3399 52.0631 76.5566 47.7002 74.8479C43.3372 73.1393 39.441 70.6798 35.7965 68.1919C34.8955 67.5669 33.9376 66.9743 33.2477 66.0936C32.8831 65.658 32.6657 65.1181 32.6267 64.5514C32.6196 64.2683 32.6687 63.9866 32.7711 63.7226C32.8735 63.4586 33.0272 63.2174 33.2233 63.0132C33.599 62.6113 34.0734 62.3147 34.5992 62.1528C35.1094 61.9991 35.6373 61.9118 36.1698 61.893C37.2169 61.8525 38.2438 61.8565 39.2584 61.893C47.4039 62.1406 54.5794 64.6163 59.6932 68.0336C60.3373 68.4648 60.8099 69.1082 61.0285 69.8519C61.1239 70.2164 61.1239 70.5994 61.0285 70.9639C60.9286 71.3211 60.7095 71.6333 60.4075 71.8487C59.8169 72.2131 59.12 72.3662 58.431 72.2829C57.7989 72.2243 57.1713 72.1253 56.5519 71.9867C54.5186 71.4945 52.5639 70.7213 50.7441 69.6895C49.2343 68.8778 48.1142 68.1391 47.3512 67.6602C46.9778 67.4208 46.6896 67.2544 46.4907 67.1002C46.2919 66.9459 46.1782 66.8891 46.1782 66.8891Z"
fill="#A9A9A9"
/>
<path
d="M94.8364 71.2204C94.6993 71.2055 94.5635 71.1797 94.4305 71.1433C94.1789 71.0743 93.8014 71.0012 93.3185 70.916C91.9393 70.7187 90.5384 70.7297 89.1625 70.9484C87.1263 71.2911 85.1507 71.9282 83.2979 72.8397C81.0433 73.9901 78.866 75.2861 76.7799 76.7197C74.6823 78.1612 72.4837 79.4497 70.201 80.5753C68.3181 81.4721 66.3087 82.0743 64.243 82.3611C62.8484 82.5487 61.4325 82.5089 60.0505 82.2434C59.6768 82.1692 59.3081 82.0716 58.9466 81.9512C58.8182 81.9168 58.6932 81.8706 58.5732 81.8132C58.7037 81.8336 58.8326 81.8634 58.9588 81.9025C59.2104 81.9755 59.5838 82.0567 60.0668 82.15C61.4407 82.3746 62.841 82.3869 64.2187 82.1866C66.2604 81.8789 68.2442 81.266 70.1036 80.3683C72.3696 79.236 74.5543 77.9477 76.6419 76.5127C78.7383 75.0733 80.9295 73.777 83.2005 72.6327C85.0755 71.7192 87.077 71.0926 89.1382 70.7739C90.5308 70.568 91.9473 70.5845 93.3347 70.8226C93.72 70.8867 94.1009 70.9748 94.4752 71.0864C94.5995 71.1198 94.7204 71.1646 94.8364 71.2204Z"
fill="#A9A9A9"
/>
<path
d="M93.6026 77.826C93.6026 77.8504 93.286 77.7205 92.7016 77.5906C91.8761 77.4114 91.0248 77.3839 90.1894 77.5095C88.9516 77.719 87.7468 78.0901 86.6057 78.6134C85.195 79.2299 83.8293 79.9446 82.5187 80.7523C81.1063 81.5883 79.7589 82.4041 78.4602 83.025C77.321 83.5882 76.1214 84.0199 74.8846 84.3116C74.0488 84.5016 73.1926 84.5861 72.3358 84.5632C72.1034 84.5575 71.8716 84.5372 71.6418 84.5024C71.5603 84.4985 71.4797 84.4835 71.4023 84.4577C71.4023 84.4293 71.7392 84.4577 72.3358 84.4577C73.1828 84.4453 74.0257 84.3364 74.8481 84.133C76.0637 83.8193 77.242 83.3757 78.3628 82.8099C79.6371 82.1849 80.9724 81.3692 82.3888 80.529C83.7103 79.7132 85.0914 78.9983 86.5204 78.3902C87.683 77.8677 88.9122 77.5085 90.1731 77.3228C91.0279 77.2097 91.8965 77.2648 92.73 77.4851C92.9548 77.5461 93.1757 77.6207 93.3916 77.7083C93.4671 77.7375 93.5381 77.7771 93.6026 77.826Z"
fill="#A9A9A9"
/>
<path
d="M72.1531 44.1988C72.1531 44.2678 69.584 43.7645 66.4468 43.0746C63.3095 42.3846 60.7648 41.7718 60.7932 41.7069C60.8216 41.6419 63.3623 42.1411 66.4995 42.8311C69.6368 43.521 72.1531 44.1339 72.1531 44.1988Z"
fill="#A9A9A9"
/>
<path
d="M87.7278 22.8493C87.9286 21.4011 85.8726 20.21 84.3238 20.0848C83.5886 20.0273 82.8227 20.139 82.1249 19.8987C80.6135 19.3743 80.011 17.432 78.5371 16.8128C77.4342 16.3526 76.1544 16.762 75.0957 17.2967C74.0371 17.8313 72.9717 18.5046 71.7769 18.5825C70.7557 18.6468 69.6086 18.2644 68.7133 18.7686C68.0326 19.1442 67.6922 19.9224 67.059 20.3792C66.4259 20.836 65.6498 20.9206 64.8941 20.9612C64.1384 21.0018 63.3521 21.012 62.6611 21.3233C61.9701 21.6346 61.4017 22.3655 61.5446 23.1031L87.7278 22.8493Z"
fill="#ABABAB"
/>
<path
d="M39.1881 32.5312C39.3293 31.4869 37.8655 30.6287 36.7662 30.5385C36.2413 30.4963 35.6955 30.5769 35.1993 30.4022C34.121 30.0182 33.6916 28.6264 32.64 28.1791C31.8556 27.847 30.951 28.1426 30.1895 28.5285C29.428 28.9144 28.6741 29.4001 27.8229 29.4538C27.0824 29.5018 26.2789 29.2254 25.632 29.5901C25.1491 29.8608 24.8972 30.4195 24.4525 30.742C24.0078 31.0645 23.4486 31.126 22.9085 31.1624C22.3684 31.1989 21.8092 31.1989 21.3187 31.4254C20.8282 31.652 20.4198 32.1741 20.5229 32.7078L39.1881 32.5312Z"
fill="#ABABAB"
/>
<path
d="M76.46 61.6777L78.8178 62.1824L80.1702 66.9562L80.2674 66.977L83.4556 63.1752L85.8134 63.6799L80.8041 69.3391L80.0506 72.8588L77.9602 72.4114L78.7137 68.8917L76.46 61.6777Z"
fill="white"
/>
<path
d="M67.148 61.4992L67.5195 59.7637L75.6965 61.514L75.325 63.2496L72.2769 62.5971L70.5171 70.8178L68.4364 70.3724L70.1962 62.1517L67.148 61.4992Z"
fill="white"
/>
<path
d="M56.9049 67.9016L59.0361 57.9453L62.9642 58.7862C63.7193 58.9478 64.3318 59.2297 64.8016 59.632C65.272 60.0309 65.5922 60.5147 65.762 61.0832C65.9357 61.6491 65.9518 62.2627 65.8103 62.9238C65.6688 63.585 65.4013 64.1379 65.0078 64.5824C64.6144 65.0269 64.1169 65.3323 63.5153 65.4984C62.9169 65.6652 62.2353 65.6667 61.4705 65.503L58.9668 64.967L59.3279 63.2801L61.4913 63.7432C61.8964 63.8299 62.2451 63.8317 62.5375 63.7485C62.8338 63.6628 63.0734 63.5091 63.2565 63.2872C63.4435 63.0629 63.572 62.7871 63.6421 62.4597C63.7128 62.1291 63.7082 61.8265 63.628 61.5517C63.5518 61.2744 63.3954 61.0392 63.1587 60.8462C62.9228 60.65 62.6007 60.5082 62.1923 60.4207L60.7728 60.1169L59.0099 68.3522L56.9049 67.9016Z"
fill="white"
/>
<path
d="M46.5049 55.2637L49.1009 55.8194L50.4108 63.0957L50.5275 63.1206L54.7013 57.0182L57.2973 57.5739L55.1661 67.5302L53.1243 67.0931L54.5114 60.6128L54.4288 60.5951L50.4754 66.4753L49.0851 66.1776L47.8905 59.1701L47.8078 59.1524L46.4154 65.657L44.3736 65.2199L46.5049 55.2637Z"
fill="white"
/>
<path
d="M35.9977 63.425L38.1289 53.4688L44.8377 54.9048L44.4662 56.6404L39.8624 55.6549L39.3546 58.0273L43.6132 58.9389L43.2417 60.6744L38.9831 59.7628L38.4742 62.1401L43.0974 63.1297L42.7259 64.8653L35.9977 63.425Z"
fill="white"
/>
</g>
<defs>
<linearGradient
id="paint0_linear_1206_120508"
x1="59.1074"
y1="36.1426"
x2="59.1074"
y2="114.947"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#CFCFCF" />
<stop offset="1" stopColor="#C6C6C6" />
</linearGradient>
<linearGradient
id="paint1_linear_1206_120508"
x1="62.0292"
y1="36.0039"
x2="62.0292"
y2="88.9801"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#DDDDDD" />
<stop offset="1" stopColor="#B6B6B6" />
</linearGradient>
</defs>
</svg>
<p className="mt-4 text-muted-foreground">Empty logs</p>
</div>
)}
</div>
</>
)
}
export default ServerLogs

View File

@ -19,8 +19,8 @@ const ErrorIcon = () => {
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
fillRule="evenodd"
clipRule="evenodd"
d="M20 10C20 15.5228 15.5228 20 10 20H0.993697C0.110179 20 -0.332289 18.9229 0.292453 18.2929L2.2495 16.3195C0.843343 14.597 1.21409e-08 12.397 1.21409e-08 10C1.21409e-08 4.47715 4.47715 0 10 0C15.5228 0 20 4.47715 20 10ZM13.2071 6.79289C13.5976 7.18342 13.5976 7.81658 13.2071 8.20711L11.4142 10L13.2071 11.7929C13.5976 12.1834 13.5976 12.8166 13.2071 13.2071C12.8166 13.5976 12.1834 13.5976 11.7929 13.2071L10 11.4142L8.20711 13.2071C7.81658 13.5976 7.18342 13.5976 6.79289 13.2071C6.40237 12.8166 6.40237 12.1834 6.79289 11.7929L8.58579 10L6.79289 8.20711C6.40237 7.81658 6.40237 7.18342 6.79289 6.79289C7.18342 6.40237 7.81658 6.40237 8.20711 6.79289L10 8.58579L11.7929 6.79289C12.1834 6.40237 12.8166 6.40237 13.2071 6.79289Z"
fill="#EA2E4E"
/>
@ -38,8 +38,8 @@ const WarningIcon = () => {
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
fillRule="evenodd"
clipRule="evenodd"
d="M20 10C20 15.5228 15.5228 20 10 20H0.993697C0.110179 20 -0.332289 18.9229 0.292453 18.2929L2.2495 16.3195C0.843343 14.597 1.21409e-08 12.397 1.21409e-08 10C1.21409e-08 4.47715 4.47715 0 10 0C15.5228 0 20 4.47715 20 10ZM10.99 6C10.99 5.44772 10.5446 5 9.99502 5C9.44549 5 9 5.44772 9 6V10C9 10.5523 9.44549 11 9.99502 11C10.5446 11 10.99 10.5523 10.99 10V6ZM9.99502 13C9.44549 13 9 13.4477 9 14C9 14.5523 9.44549 15 9.99502 15H10.005C10.5545 15 11 14.5523 11 14C11 13.4477 10.5545 13 10.005 13H9.99502Z"
fill="#FACC15"
/>
@ -57,8 +57,8 @@ const SuccessIcon = () => {
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
fillRule="evenodd"
clipRule="evenodd"
d="M20 10C20 15.5228 15.5228 20 10 20H0.993697C0.110179 20 -0.332289 18.9229 0.292453 18.2929L2.2495 16.3195C0.843343 14.597 1.21409e-08 12.397 1.21409e-08 10C1.21409e-08 4.47715 4.47715 0 10 0C15.5228 0 20 4.47715 20 10ZM13.7071 8.70711C14.0976 8.31658 14.0976 7.68342 13.7071 7.29289C13.3166 6.90237 12.6834 6.90237 12.2929 7.29289L9 10.5858L7.70711 9.2929C7.31658 8.90237 6.68342 8.90237 6.29289 9.2929C5.90237 9.68342 5.90237 10.3166 6.29289 10.7071L8.29289 12.7071C8.48043 12.8946 8.73478 13 9 13C9.26522 13 9.51957 12.8946 9.70711 12.7071L13.7071 8.70711Z"
fill="#34D399"
/>
@ -76,8 +76,8 @@ const DefaultIcon = () => {
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
fillRule="evenodd"
clipRule="evenodd"
d="M10 20C15.5228 20 20 15.5228 20 10C20 4.47715 15.5228 0 10 0C4.47715 0 2.11188e-08 4.47715 2.11188e-08 10C2.11188e-08 12.397 0.843343 14.597 2.2495 16.3195L0.292453 18.2929C-0.332289 18.9229 0.110179 20 0.993697 20H10ZM5.5 8C5.5 7.44772 5.94772 7 6.5 7H13.5C14.0523 7 14.5 7.44772 14.5 8C14.5 8.55229 14.0523 9 13.5 9H6.5C5.94772 9 5.5 8.55229 5.5 8ZM6.5 11C5.94772 11 5.5 11.4477 5.5 12C5.5 12.5523 5.94772 13 6.5 13H9.5C10.0523 13 10.5 12.5523 10.5 12C10.5 11.4477 10.0523 11 9.5 11H6.5Z"
fill="#60A5FA"
/>

View File

@ -0,0 +1,4 @@
import { Assistant } from '@janhq/core/.'
import { atom } from 'jotai'
export const assistantsAtom = atom<Assistant[]>([])

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