chore: server download progress + S3 (#1925)

* fix: reduce the number of api call

Signed-off-by: James <james@jan.ai>

* fix: download progress

Signed-off-by: James <james@jan.ai>

* chore: save blob

* fix: server boot up

* fix: download state not updating

Signed-off-by: James <james@jan.ai>

* fix: copy assets

* Add Dockerfile CPU for Jan Server and Jan Web

* Add Dockerfile GPU for Jan Server and Jan Web

* feat: S3 adapter

* Update check find count from ./pre-install and correct copy:asserts command

* server add bundleDependencies @janhq/core

* server add bundleDependencies @janhq/core

* fix: update success/failed download state (#1945)

* fix: update success/failed download state

Signed-off-by: James <james@jan.ai>

* fix: download model progress and state handling for both Desktop and Web

---------

Signed-off-by: James <james@jan.ai>
Co-authored-by: James <james@jan.ai>
Co-authored-by: Louis <louis@jan.ai>

* chore: refactor

* fix: load models empty first time open

* Add Docker compose

* fix: assistants onUpdate

---------

Signed-off-by: James <james@jan.ai>
Co-authored-by: James <james@jan.ai>
Co-authored-by: Hien To <tominhhien97@gmail.com>
Co-authored-by: NamH <NamNh0122@gmail.com>
This commit is contained in:
Louis 2024-02-07 17:54:35 +07:00 committed by GitHub
parent 1442479666
commit 5890ade451
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
61 changed files with 957 additions and 518 deletions

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

65
Dockerfile.gpu Normal file
View File

@ -0,0 +1,65 @@
FROM nvidia/cuda:12.0.0-devel-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 && curl -sL https://deb.nodesource.com/setup_20.x | bash - && apt install nodejs -y && rm -rf /var/lib/apt/lists/*
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 && curl -sL https://deb.nodesource.com/setup_20.x | bash - && apt-get install nodejs -y && rm -rf /var/lib/apt/lists/*
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-12.0/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

@ -218,6 +218,31 @@ 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 docker`, 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
```bash
# CPU mode
docker compose --profile cpu up
# GPU mode
docker compose --profile gpu up
```
This will start the web server and you can access Jan at `http://localhost:3000`.
## 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

@ -5,8 +5,27 @@ import { HttpServer } from '../HttpServer'
import { createWriteStream } from 'fs'
import { getJanDataFolderPath } from '../../utils'
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
@ -19,7 +38,10 @@ export const downloadRouter = async (app: HttpServer) => {
})
const localPath = normalizedArgs[1]
const fileName = localPath.split('/').pop() ?? ''
const array = localPath.split('/')
const fileName = array.pop() ?? ''
const modelId = array.pop() ?? ''
console.debug('downloadFile', normalizedArgs, fileName, modelId)
const request = require('request')
const progress = require('request-progress')
@ -27,17 +49,44 @@ export const downloadRouter = async (app: HttpServer) => {
const rq = request({ url: normalizedArgs[0], strictSSL, proxy })
progress(rq, {})
.on('progress', function (state: any) {
console.log('download onProgress', state)
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)
console.debug(`Download ${modelId} onError`, err.message)
const currentDownloadState = DownloadManager.instance.downloadProgressMap[modelId]
if (currentDownloadState) {
DownloadManager.instance.downloadProgressMap[modelId] = {
...currentDownloadState,
downloadState: 'error',
}
}
})
.on('end', function () {
console.log('download onEnd')
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(fileName, rq)
DownloadManager.instance.setRequest(localPath, rq)
res.status(200).send({ message: 'Download started' })
})
app.post(`/${DownloadRoute.abortDownload}`, async (req, res) => {
@ -54,5 +103,10 @@ export const downloadRouter = async (app: HttpServer) => {
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',
}

110
docker-compose.yml Normal file
View File

@ -0,0 +1,110 @@
version: '3.7'
services:
minio:
image: minio/minio
volumes:
- minio_data:/data
ports:
- "9000:9000"
- "9001:9001"
environment:
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:
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:
image: jan:latest
volumes:
- app_data:/app/server/build/jan
build:
context: .
dockerfile: Dockerfile
environment:
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:
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:
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
# docker compose --profile cpu up
# docker compose --profile gpu up

View File

@ -5,7 +5,11 @@ import request from 'request'
import { createWriteStream, renameSync } from 'fs'
import { DownloadEvent, DownloadRoute } from '@janhq/core'
const progress = require('request-progress')
import { DownloadManager, getJanDataFolderPath, normalizeFilePath } from '@janhq/core/node'
import {
DownloadManager,
getJanDataFolderPath,
normalizeFilePath,
} from '@janhq/core/node'
export function handleDownloaderIPCs() {
/**
@ -56,20 +60,23 @@ export function handleDownloaderIPCs() {
*/
ipcMain.handle(
DownloadRoute.downloadFile,
async (_event, url, fileName, network) => {
async (_event, url, localPath, network) => {
const strictSSL = !network?.ignoreSSL
const proxy = network?.proxy?.startsWith('http')
? network.proxy
: undefined
if (typeof fileName === 'string') {
fileName = normalizeFilePath(fileName)
if (typeof localPath === 'string') {
localPath = normalizeFilePath(localPath)
}
const destination = resolve(getJanDataFolderPath(), fileName)
const array = localPath.split('/')
const fileName = array.pop() ?? ''
const modelId = array.pop() ?? ''
const destination = resolve(getJanDataFolderPath(), localPath)
const rq = request({ url, strictSSL, proxy })
// Put request to download manager instance
DownloadManager.instance.setRequest(fileName, rq)
DownloadManager.instance.setRequest(localPath, rq)
// Downloading file to a temp file first
const downloadingTempFile = `${destination}.download`
@ -81,6 +88,7 @@ export function handleDownloaderIPCs() {
{
...state,
fileName,
modelId,
}
)
})
@ -90,11 +98,12 @@ export function handleDownloaderIPCs() {
{
fileName,
err,
modelId,
}
)
})
.on('end', function () {
if (DownloadManager.instance.networkRequests[fileName]) {
if (DownloadManager.instance.networkRequests[localPath]) {
// Finished downloading, rename temp file to actual file
renameSync(downloadingTempFile, destination)
@ -102,14 +111,16 @@ export function handleDownloaderIPCs() {
DownloadEvent.onFileDownloadSuccess,
{
fileName,
modelId,
}
)
DownloadManager.instance.setRequest(fileName, undefined)
DownloadManager.instance.setRequest(localPath, undefined)
} else {
WindowManager?.instance.currentWindow?.webContents.send(
DownloadEvent.onFileDownloadError,
{
fileName,
modelId,
err: { message: 'aborted' },
}
)

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

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

View File

@ -8,9 +8,9 @@
"license": "AGPL-3.0",
"scripts": {
"build": "tsc --module commonjs && rollup -c rollup.config.ts",
"build:publish:linux": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/pre-install",
"build:publish:darwin": "rimraf *.tgz --glob && npm run build && ../../.github/scripts/auto-sign.sh && npm pack && cpx *.tgz ../../electron/pre-install",
"build:publish:win32": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/pre-install",
"build:publish:linux": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../pre-install",
"build:publish:darwin": "rimraf *.tgz --glob && npm run build && ../../.github/scripts/auto-sign.sh && npm pack && cpx *.tgz ../../pre-install",
"build:publish:win32": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../pre-install",
"build:publish": "run-script-os"
},
"devDependencies": {

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

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

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

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

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"
},
"devDependencies": {
"cpx": "^1.5.0",

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

@ -8,7 +8,7 @@
"license": "AGPL-3.0",
"scripts": {
"build": "tsc -b . && webpack --config webpack.config.js",
"build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/pre-install"
"build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../pre-install"
},
"devDependencies": {
"rimraf": "^3.0.2",

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

@ -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,8 +25,7 @@ import { MainViewState } from '@/constants/screens'
import { useActiveModel } from '@/hooks/useActiveModel'
import { useDownloadState } from '@/hooks/useDownloadState'
import { modelDownloadStateAtom } from '@/hooks/useDownloadState'
import useGetSystemResources from '@/hooks/useGetSystemResources'
import { useMainViewState } from '@/hooks/useMainViewState'
@ -53,7 +52,7 @@ const BottomBar = () => {
const downloadedModels = useAtomValue(downloadedModelsAtom)
const { setMainViewState } = useMainViewState()
const { downloadStates } = useDownloadState()
const downloadStates = useAtomValue(modelDownloadStateAtom)
const setShowSelectModelModal = useSetAtom(showSelectModelModalAtom)
const [serverEnabled] = useAtom(serverEnabledAtom)
@ -109,7 +108,7 @@ const BottomBar = () => {
)}
{downloadedModels.length === 0 &&
!stateModel.loading &&
downloadStates.length === 0 && (
Object.values(downloadStates).length === 0 && (
<Button
size="sm"
themes="outline"

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

@ -1,93 +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 { useAtom, useAtomValue, useSetAtom } from 'jotai'
import { DownloadEvent, events } from '@janhq/core'
import { useSetAtom } from 'jotai'
import { useDownloadState } from '@/hooks/useDownloadState'
import { modelBinFileName } from '@/utils/model'
import { setDownloadStateAtom } from '@/hooks/useDownloadState'
import EventHandler from './EventHandler'
import { appDownloadProgress } from './Jotai'
import {
downloadedModelsAtom,
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 [downloadedModels, setDownloadedModels] = useAtom(downloadedModelsAtom)
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)
@ -107,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

@ -4,23 +4,28 @@ import { atom } from 'jotai'
export const stateModel = atom({ state: 'start', loading: false, model: '' })
export const activeAssistantModelAtom = atom<Model | undefined>(undefined)
export const downloadingModelsAtom = atom<Model[]>([])
/**
* Stores the list of models which are being downloaded.
*/
const downloadingModelsAtom = atom<Model[]>([])
export const addNewDownloadingModelAtom = atom(
null,
(get, set, model: Model) => {
const currentModels = get(downloadingModelsAtom)
set(downloadingModelsAtom, [...currentModels, model])
export const getDownloadingModelAtom = atom((get) => get(downloadingModelsAtom))
export const addDownloadingModelAtom = atom(null, (get, set, model: Model) => {
const downloadingModels = get(downloadingModelsAtom)
if (!downloadingModels.find((e) => e.id === model.id)) {
set(downloadingModelsAtom, [...downloadingModels, model])
}
)
})
export const removeDownloadingModelAtom = atom(
null,
(get, set, modelId: string) => {
const currentModels = get(downloadingModelsAtom)
const downloadingModels = get(downloadingModelsAtom)
set(
downloadingModelsAtom,
currentModels.filter((e) => e.id !== modelId)
downloadingModels.filter((e) => e.id !== modelId)
)
}
)

View File

@ -1,6 +1,12 @@
import { useEffect } from 'react'
import { useCallback, useEffect } from 'react'
import { Assistant, AssistantExtension, ExtensionTypeEnum } from '@janhq/core'
import {
Assistant,
AssistantEvent,
AssistantExtension,
ExtensionTypeEnum,
events,
} from '@janhq/core'
import { useSetAtom } from 'jotai'
@ -10,14 +16,19 @@ import { assistantsAtom } from '@/helpers/atoms/Assistant.atom'
const useAssistants = () => {
const setAssistants = useSetAtom(assistantsAtom)
useEffect(() => {
const getAssistants = async () => {
const assistants = await getLocalAssistants()
setAssistants(assistants)
}
getAssistants()
const getData = useCallback(async () => {
const assistants = await getLocalAssistants()
setAssistants(assistants)
}, [setAssistants])
useEffect(() => {
getData()
events.on(AssistantEvent.OnAssistantsUpdate, () => getData())
return () => {
events.off(AssistantEvent.OnAssistantsUpdate, () => getData())
}
}, [getData])
}
const getLocalAssistants = async (): Promise<Assistant[]> =>

View File

@ -1,4 +1,4 @@
import { useContext } from 'react'
import { useCallback, useContext } from 'react'
import {
Model,
@ -15,21 +15,40 @@ import { FeatureToggleContext } from '@/context/FeatureToggle'
import { modelBinFileName } from '@/utils/model'
import { useDownloadState } from './useDownloadState'
import { setDownloadStateAtom } from './useDownloadState'
import { extensionManager } from '@/extension/ExtensionManager'
import { addNewDownloadingModelAtom } from '@/helpers/atoms/Model.atom'
import { addDownloadingModelAtom } from '@/helpers/atoms/Model.atom'
export default function useDownloadModel() {
const { ignoreSSL, proxy } = useContext(FeatureToggleContext)
const { setDownloadState } = useDownloadState()
const addNewDownloadingModel = useSetAtom(addNewDownloadingModelAtom)
const setDownloadState = useSetAtom(setDownloadStateAtom)
const addDownloadingModel = useSetAtom(addDownloadingModelAtom)
const downloadModel = async (model: Model) => {
const childrenDownloadProgress: DownloadState[] = []
model.sources.forEach((source: ModelArtifact) => {
childrenDownloadProgress.push({
modelId: source.filename,
const downloadModel = useCallback(
async (model: Model) => {
const childProgresses: DownloadState[] = model.sources.map(
(source: ModelArtifact) => ({
filename: source.filename,
modelId: model.id,
time: {
elapsed: 0,
remaining: 0,
},
speed: 0,
percent: 0,
size: {
total: 0,
transferred: 0,
},
downloadState: 'downloading',
})
)
// set an initial download state
setDownloadState({
filename: '',
modelId: model.id,
time: {
elapsed: 0,
remaining: 0,
@ -40,31 +59,16 @@ export default function useDownloadModel() {
total: 0,
transferred: 0,
},
children: childProgresses,
downloadState: 'downloading',
})
})
// set an initial download state
setDownloadState({
modelId: model.id,
time: {
elapsed: 0,
remaining: 0,
},
speed: 0,
percent: 0,
size: {
total: 0,
transferred: 0,
},
children: childrenDownloadProgress,
})
addDownloadingModel(model)
addNewDownloadingModel(model)
await extensionManager
.get<ModelExtension>(ExtensionTypeEnum.Model)
?.downloadModel(model, { ignoreSSL, proxy })
}
await localDownloadModel(model, ignoreSSL, proxy)
},
[ignoreSSL, proxy, addDownloadingModel, setDownloadState]
)
const abortModelDownload = async (model: Model) => {
await abortDownload(
@ -77,3 +81,12 @@ export default function useDownloadModel() {
abortModelDownload,
}
}
const localDownloadModel = async (
model: Model,
ignoreSSL: boolean,
proxy: string
) =>
extensionManager
.get<ModelExtension>(ExtensionTypeEnum.Model)
?.downloadModel(model, { ignoreSSL, proxy })

View File

@ -1,96 +1,64 @@
import { atom, useSetAtom, useAtomValue } from 'jotai'
import { atom } from 'jotai'
import { toaster } from '@/containers/Toast'
import {
configuredModelsAtom,
downloadedModelsAtom,
removeDownloadingModelAtom,
} from '@/helpers/atoms/Model.atom'
// download states
const modelDownloadStateAtom = atom<Record<string, DownloadState>>({})
export const modelDownloadStateAtom = atom<Record<string, DownloadState>>({})
const setDownloadStateAtom = atom(null, (get, set, state: DownloadState) => {
const currentState = { ...get(modelDownloadStateAtom) }
console.debug(
`current download state for ${state.modelId} is ${JSON.stringify(state)}`
)
currentState[state.modelId] = state
set(modelDownloadStateAtom, currentState)
})
const setDownloadStateSuccessAtom = atom(null, (get, set, modelId: string) => {
const currentState = { ...get(modelDownloadStateAtom) }
const state = currentState[modelId]
if (!state) {
console.debug(`Cannot find download state for ${modelId}`)
return
}
delete currentState[modelId]
set(modelDownloadStateAtom, currentState)
toaster({
title: 'Download Completed',
description: `Download ${modelId} completed`,
type: 'success',
})
})
const setDownloadStateFailedAtom = atom(
/**
* Used to set the download state for a particular model.
*/
export const setDownloadStateAtom = atom(
null,
(get, set, modelId: string, error: string) => {
(get, set, state: DownloadState) => {
const currentState = { ...get(modelDownloadStateAtom) }
const state = currentState[modelId]
if (!state) {
console.debug(`Cannot find download state for ${modelId}`)
return
}
if (error.includes('certificate')) {
error += '. To fix enable "Ignore SSL Certificates" in Advanced settings.'
}
toaster({
title: 'Download Failed',
description: `Model ${modelId} download failed: ${error}`,
type: 'error',
})
delete currentState[modelId]
set(modelDownloadStateAtom, currentState)
}
)
const setDownloadStateCancelledAtom = atom(
null,
(get, set, modelId: string) => {
const currentState = { ...get(modelDownloadStateAtom) }
const state = currentState[modelId]
if (!state) {
console.debug(`Cannot find download state for ${modelId}`)
if (state.downloadState === 'end') {
// download successfully
delete currentState[state.modelId]
set(removeDownloadingModelAtom, state.modelId)
const model = get(configuredModelsAtom).find(
(e) => e.id === state.modelId
)
if (model) set(downloadedModelsAtom, (prev) => [...prev, model])
toaster({
title: 'Cancel Download',
description: `Model ${modelId} cancel download`,
type: 'warning',
title: 'Download Completed',
description: `Download ${state.modelId} completed`,
type: 'success',
})
return
} else if (state.downloadState === 'error') {
// download error
delete currentState[state.modelId]
set(removeDownloadingModelAtom, state.modelId)
if (state.error === 'aborted') {
toaster({
title: 'Cancel Download',
description: `Model ${state.modelId} download cancelled`,
type: 'warning',
})
} else {
let error = state.error
if (state.error?.includes('certificate')) {
error +=
'. To fix enable "Ignore SSL Certificates" in Advanced settings.'
}
toaster({
title: 'Download Failed',
description: `Model ${state.modelId} download failed: ${error}`,
type: 'error',
})
}
} else {
// download in progress
currentState[state.modelId] = state
}
delete currentState[modelId]
set(modelDownloadStateAtom, currentState)
}
)
export function useDownloadState() {
const modelDownloadState = useAtomValue(modelDownloadStateAtom)
const setDownloadState = useSetAtom(setDownloadStateAtom)
const setDownloadStateSuccess = useSetAtom(setDownloadStateSuccessAtom)
const setDownloadStateFailed = useSetAtom(setDownloadStateFailedAtom)
const setDownloadStateCancelled = useSetAtom(setDownloadStateCancelledAtom)
const downloadStates: DownloadState[] = []
for (const [, value] of Object.entries(modelDownloadState)) {
downloadStates.push(value)
}
return {
modelDownloadStateAtom,
modelDownloadState,
setDownloadState,
setDownloadStateSuccess,
setDownloadStateFailed,
setDownloadStateCancelled,
downloadStates,
}
}

View File

@ -1,6 +1,12 @@
import { useEffect } from 'react'
import { useCallback, useEffect } from 'react'
import { ExtensionTypeEnum, Model, ModelExtension } from '@janhq/core'
import {
ExtensionTypeEnum,
Model,
ModelEvent,
ModelExtension,
events,
} from '@janhq/core'
import { useSetAtom } from 'jotai'
@ -14,23 +20,30 @@ const useModels = () => {
const setDownloadedModels = useSetAtom(downloadedModelsAtom)
const setConfiguredModels = useSetAtom(configuredModelsAtom)
useEffect(() => {
const getData = useCallback(() => {
const getDownloadedModels = async () => {
const models = await getLocalDownloadedModels()
setDownloadedModels(models)
}
getDownloadedModels()
}, [setDownloadedModels])
useEffect(() => {
const getConfiguredModels = async () => {
const models = await getLocalConfiguredModels()
setConfiguredModels(models)
}
getDownloadedModels()
getConfiguredModels()
}, [setConfiguredModels])
}, [setDownloadedModels, setConfiguredModels])
useEffect(() => {
// Try get data on mount
getData()
// Listen for model updates
events.on(ModelEvent.OnModelsUpdate, async () => getData())
return () => {
// Remove listener on unmount
events.off(ModelEvent.OnModelsUpdate, async () => {})
}
}, [getData])
}
const getLocalConfiguredModels = async (): Promise<Model[]> =>

View File

@ -10,8 +10,6 @@ import {
import { useSetAtom } from 'jotai'
import { loadModelErrorAtom } from './useActiveModel'
import { extensionManager } from '@/extension'
import { setConvoMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
import {
@ -26,7 +24,6 @@ export default function useSetActiveThread() {
const setThreadMessage = useSetAtom(setConvoMessagesAtom)
const setThreadModelParams = useSetAtom(setThreadModelParamsAtom)
const setIsGeneratingResponse = useSetAtom(isGeneratingResponseAtom)
const setLoadModelError = useSetAtom(loadModelErrorAtom)
const setActiveThread = useCallback(
async (thread: Thread) => {

View File

@ -27,7 +27,9 @@ const nextConfig = {
VERSION: JSON.stringify(packageJson.version),
ANALYTICS_ID: JSON.stringify(process.env.ANALYTICS_ID),
ANALYTICS_HOST: JSON.stringify(process.env.ANALYTICS_HOST),
API_BASE_URL: JSON.stringify('http://localhost:1337'),
API_BASE_URL: JSON.stringify(
process.env.API_BASE_URL ?? 'http://localhost:1337'
),
isMac: process.platform === 'darwin',
isWindows: process.platform === 'win32',
isLinux: process.platform === 'linux',

View File

@ -10,8 +10,6 @@ import LogoMark from '@/containers/Brand/Logo/Mark'
import { MainViewState } from '@/constants/screens'
import { loadModelErrorAtom } from '@/hooks/useActiveModel'
import { useMainViewState } from '@/hooks/useMainViewState'
import ChatItem from '../ChatItem'

View File

@ -9,7 +9,6 @@ import { Button } from '@janhq/uikit'
import { useAtomValue, useSetAtom } from 'jotai'
import { RefreshCcw } from 'lucide-react'
import { useActiveModel } from '@/hooks/useActiveModel'
import useSendChatMessage from '@/hooks/useSendChatMessage'
import { extensionManager } from '@/extension'

View File

@ -25,7 +25,7 @@ import { MainViewState } from '@/constants/screens'
import { useCreateNewThread } from '@/hooks/useCreateNewThread'
import useDownloadModel from '@/hooks/useDownloadModel'
import { useDownloadState } from '@/hooks/useDownloadState'
import { modelDownloadStateAtom } from '@/hooks/useDownloadState'
import { useMainViewState } from '@/hooks/useMainViewState'
@ -49,7 +49,6 @@ type Props = {
const ExploreModelItemHeader: React.FC<Props> = ({ model, onClick, open }) => {
const { downloadModel } = useDownloadModel()
const downloadedModels = useAtomValue(downloadedModelsAtom)
const { modelDownloadStateAtom } = useDownloadState()
const { requestCreateNewThread } = useCreateNewThread()
const totalRam = useAtomValue(totalRamAtom)
const nvidiaTotalVram = useAtomValue(nvidiaTotalVramAtom)

View File

@ -1,84 +0,0 @@
import React, { useMemo } from 'react'
import { Model } from '@janhq/core'
import { Button } from '@janhq/uikit'
import { atom, useAtomValue } from 'jotai'
import ModalCancelDownload from '@/containers/ModalCancelDownload'
import { MainViewState } from '@/constants/screens'
import useDownloadModel from '@/hooks/useDownloadModel'
import { useDownloadState } from '@/hooks/useDownloadState'
import { useMainViewState } from '@/hooks/useMainViewState'
import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
type Props = {
model: Model
isRecommended: boolean
}
const ModelVersionItem: React.FC<Props> = ({ model }) => {
const { downloadModel } = useDownloadModel()
const downloadedModels = useAtomValue(downloadedModelsAtom)
const { setMainViewState } = useMainViewState()
const isDownloaded =
downloadedModels.find(
(downloadedModel) => downloadedModel.id === model.id
) != null
const { modelDownloadStateAtom, downloadStates } = useDownloadState()
const downloadAtom = useMemo(
() => atom((get) => get(modelDownloadStateAtom)[model.id ?? '']),
/* eslint-disable react-hooks/exhaustive-deps */
[model.id]
)
const downloadState = useAtomValue(downloadAtom)
const onDownloadClick = () => {
downloadModel(model)
}
let downloadButton = (
<Button themes="outline" size="sm" onClick={() => onDownloadClick()}>
Download
</Button>
)
if (isDownloaded) {
downloadButton = (
<Button
themes="outline"
size="sm"
onClick={() => {
setMainViewState(MainViewState.MyModels)
}}
>
Use
</Button>
)
}
if (downloadState != null && downloadStates.length > 0) {
downloadButton = <ModalCancelDownload model={model} isFromList />
}
return (
<div className="flex items-center justify-between gap-4 border-t border-border pb-3 pl-3 pr-4 pt-3 first:border-t-0">
<div className="flex items-center gap-2">
<span className="line-clamp-1 flex-1" title={model.name}>
{model.name}
</span>
</div>
<div className="flex items-center gap-4">
<div className="flex justify-end gap-2"></div>
{downloadButton}
</div>
</div>
)
}
export default ModelVersionItem

View File

@ -1,25 +0,0 @@
import { Model } from '@janhq/core'
import ModelVersionItem from '../ModelVersionItem'
type Props = {
models: Model[]
recommendedVersion: string
}
export default function ModelVersionList({
models,
recommendedVersion,
}: Props) {
return (
<div className="pt-4">
{models.map((model) => (
<ModelVersionItem
key={model.name}
model={model}
isRecommended={model.name === recommendedVersion}
/>
))}
</div>
)
}

View File

@ -3,6 +3,7 @@ import {
AppRoute,
DownloadRoute,
ExtensionRoute,
FileManagerRoute,
FileSystemRoute,
} from '@janhq/core'
@ -22,6 +23,7 @@ export const APIRoutes = [
route: r,
})),
...Object.values(FileSystemRoute).map((r) => ({ path: `fs`, route: r })),
...Object.values(FileManagerRoute).map((r) => ({ path: `fs`, route: r })),
]
// Define the restAPI object with methods for each API route
@ -50,4 +52,6 @@ export const restAPI = {
}
}, {}),
openExternalUrl,
// Jan Server URL
baseApiUrl: API_BASE_URL,
}

View File

@ -1,12 +1,13 @@
type DownloadState = {
modelId: string
filename: string
time: DownloadTime
speed: number
percent: number
size: DownloadSize
isFinished?: boolean
children?: DownloadState[]
error?: string
downloadState: 'downloading' | 'error' | 'end'
}
type DownloadTime = {