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:
parent
1442479666
commit
5890ade451
67
Dockerfile
67
Dockerfile
@ -1,39 +1,58 @@
|
|||||||
FROM node:20-bullseye AS base
|
FROM node:20-bookworm AS base
|
||||||
|
|
||||||
# 1. Install dependencies only when needed
|
# 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
|
WORKDIR /app
|
||||||
|
|
||||||
# Install dependencies based on the preferred package manager
|
# Install dependencies based on the preferred package manager
|
||||||
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
|
COPY . ./
|
||||||
RUN yarn install
|
|
||||||
|
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
|
# # 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
|
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
|
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 the package.json, yarn.lock, and build output of server yarn space to leverage Docker cache
|
||||||
COPY --from=builder /app/server/build ./
|
COPY --from=builder /app/server ./server/
|
||||||
|
COPY --from=builder /app/docs/openapi ./docs/openapi/
|
||||||
|
|
||||||
# Automatically leverage output traces to reduce image size
|
# Copy pre-install dependencies
|
||||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
COPY --from=builder /app/pre-install ./pre-install/
|
||||||
COPY --from=builder /app/server/node_modules ./node_modules
|
|
||||||
COPY --from=builder /app/server/package.json ./package.json
|
|
||||||
|
|
||||||
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
|
RUN npm install -g serve@latest
|
||||||
ENV APPDATA /app/data
|
|
||||||
|
|
||||||
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
65
Dockerfile.gpu
Normal 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
|
||||||
4
Makefile
4
Makefile
@ -24,9 +24,9 @@ endif
|
|||||||
|
|
||||||
check-file-counts: install-and-build
|
check-file-counts: install-and-build
|
||||||
ifeq ($(OS),Windows_NT)
|
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
|
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
|
endif
|
||||||
|
|
||||||
dev: check-file-counts
|
dev: check-file-counts
|
||||||
|
|||||||
25
README.md
25
README.md
@ -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.
|
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
|
## Acknowledgements
|
||||||
|
|
||||||
Jan builds on top of other open-source projects:
|
Jan builds on top of other open-source projects:
|
||||||
|
|||||||
@ -57,6 +57,7 @@
|
|||||||
"rollup-plugin-typescript2": "^0.36.0",
|
"rollup-plugin-typescript2": "^0.36.0",
|
||||||
"ts-jest": "^26.1.1",
|
"ts-jest": "^26.1.1",
|
||||||
"tslib": "^2.6.2",
|
"tslib": "^2.6.2",
|
||||||
"typescript": "^5.2.2"
|
"typescript": "^5.2.2",
|
||||||
|
"rimraf": "^3.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,6 +30,7 @@ export enum DownloadRoute {
|
|||||||
downloadFile = 'downloadFile',
|
downloadFile = 'downloadFile',
|
||||||
pauseDownload = 'pauseDownload',
|
pauseDownload = 'pauseDownload',
|
||||||
resumeDownload = 'resumeDownload',
|
resumeDownload = 'resumeDownload',
|
||||||
|
getDownloadProgress = 'getDownloadProgress',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum DownloadEvent {
|
export enum DownloadEvent {
|
||||||
|
|||||||
@ -5,8 +5,27 @@ import { HttpServer } from '../HttpServer'
|
|||||||
import { createWriteStream } from 'fs'
|
import { createWriteStream } from 'fs'
|
||||||
import { getJanDataFolderPath } from '../../utils'
|
import { getJanDataFolderPath } from '../../utils'
|
||||||
import { normalizeFilePath } from '../../path'
|
import { normalizeFilePath } from '../../path'
|
||||||
|
import { DownloadState } from '../../../types'
|
||||||
|
|
||||||
export const downloadRouter = async (app: HttpServer) => {
|
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) => {
|
app.post(`/${DownloadRoute.downloadFile}`, async (req, res) => {
|
||||||
const strictSSL = !(req.query.ignoreSSL === 'true')
|
const strictSSL = !(req.query.ignoreSSL === 'true')
|
||||||
const proxy = req.query.proxy?.startsWith('http') ? req.query.proxy : undefined
|
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 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 request = require('request')
|
||||||
const progress = require('request-progress')
|
const progress = require('request-progress')
|
||||||
@ -27,17 +49,44 @@ export const downloadRouter = async (app: HttpServer) => {
|
|||||||
const rq = request({ url: normalizedArgs[0], strictSSL, proxy })
|
const rq = request({ url: normalizedArgs[0], strictSSL, proxy })
|
||||||
progress(rq, {})
|
progress(rq, {})
|
||||||
.on('progress', function (state: any) {
|
.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) {
|
.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 () {
|
.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]))
|
.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) => {
|
app.post(`/${DownloadRoute.abortDownload}`, async (req, res) => {
|
||||||
@ -54,5 +103,10 @@ export const downloadRouter = async (app: HttpServer) => {
|
|||||||
const rq = DownloadManager.instance.networkRequests[fileName]
|
const rq = DownloadManager.instance.networkRequests[fileName]
|
||||||
DownloadManager.instance.networkRequests[fileName] = undefined
|
DownloadManager.instance.networkRequests[fileName] = undefined
|
||||||
rq?.abort()
|
rq?.abort()
|
||||||
|
if (rq) {
|
||||||
|
res.status(200).send({ message: 'Download aborted' })
|
||||||
|
} else {
|
||||||
|
res.status(404).send({ message: 'Download not found' })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,29 @@
|
|||||||
import { FileManagerRoute } from '../../../api'
|
import { FileManagerRoute } from '../../../api'
|
||||||
import { HttpServer } from '../../index'
|
import { HttpServer } from '../../index'
|
||||||
|
import { join } from 'path'
|
||||||
|
|
||||||
export const fsRouter = async (app: HttpServer) => {
|
export const fileManagerRouter = async (app: HttpServer) => {
|
||||||
app.post(`/app/${FileManagerRoute.syncFile}`, async (request: any, reply: any) => {})
|
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.getUserHomePath}`, async (request: any, reply: any) => {})
|
||||||
|
app.post(`/fs/${FileManagerRoute.fileStat}`, async (request: any, reply: any) => {})
|
||||||
app.post(`/app/${FileManagerRoute.fileStat}`, async (request: any, reply: any) => {})
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import { FileSystemRoute } from '../../../api'
|
import { FileManagerRoute, FileSystemRoute } from '../../../api'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { HttpServer } from '../HttpServer'
|
import { HttpServer } from '../HttpServer'
|
||||||
import { getJanDataFolderPath } from '../../utils'
|
import { getJanDataFolderPath } from '../../utils'
|
||||||
import { normalizeFilePath } from '../../path'
|
import { normalizeFilePath } from '../../path'
|
||||||
|
import { writeFileSync } from 'fs'
|
||||||
|
|
||||||
export const fsRouter = async (app: HttpServer) => {
|
export const fsRouter = async (app: HttpServer) => {
|
||||||
const moduleName = 'fs'
|
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}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { threadRouter } from './thread'
|
|||||||
import { fsRouter } from './fs'
|
import { fsRouter } from './fs'
|
||||||
import { extensionRouter } from './extension'
|
import { extensionRouter } from './extension'
|
||||||
import { downloadRouter } from './download'
|
import { downloadRouter } from './download'
|
||||||
|
import { fileManagerRouter } from './fileManager'
|
||||||
|
|
||||||
export const v1Router = async (app: HttpServer) => {
|
export const v1Router = async (app: HttpServer) => {
|
||||||
// MARK: External Routes
|
// MARK: External Routes
|
||||||
@ -16,6 +17,8 @@ export const v1Router = async (app: HttpServer) => {
|
|||||||
app.register(fsRouter, {
|
app.register(fsRouter, {
|
||||||
prefix: '/fs',
|
prefix: '/fs',
|
||||||
})
|
})
|
||||||
|
app.register(fileManagerRouter)
|
||||||
|
|
||||||
app.register(extensionRouter, {
|
app.register(extensionRouter, {
|
||||||
prefix: '/extension',
|
prefix: '/extension',
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,15 +1,18 @@
|
|||||||
|
import { DownloadState } from '../types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages file downloads and network requests.
|
* Manages file downloads and network requests.
|
||||||
*/
|
*/
|
||||||
export class DownloadManager {
|
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() {
|
constructor() {
|
||||||
if (DownloadManager.instance) {
|
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.
|
* @param {Request | undefined} request - The network request to set, or undefined to clear the request.
|
||||||
*/
|
*/
|
||||||
setRequest(fileName: string, request: any | undefined) {
|
setRequest(fileName: string, request: any | undefined) {
|
||||||
this.networkRequests[fileName] = request;
|
this.networkRequests[fileName] = request
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,8 +41,8 @@ async function registerExtensionProtocol() {
|
|||||||
console.error('Electron is not available')
|
console.error('Electron is not available')
|
||||||
}
|
}
|
||||||
const extensionPath = ExtensionManager.instance.getExtensionsPath()
|
const extensionPath = ExtensionManager.instance.getExtensionsPath()
|
||||||
if (electron) {
|
if (electron && electron.protocol) {
|
||||||
return electron.protocol.registerFileProtocol('extension', (request: any, callback: any) => {
|
return electron.protocol?.registerFileProtocol('extension', (request: any, callback: any) => {
|
||||||
const entry = request.url.substr('extension://'.length - 1)
|
const entry = request.url.substr('extension://'.length - 1)
|
||||||
|
|
||||||
const url = normalize(extensionPath + entry)
|
const url = normalize(extensionPath + entry)
|
||||||
@ -69,7 +69,7 @@ export function useExtensions(extensionsPath: string) {
|
|||||||
|
|
||||||
// Read extension list from extensions folder
|
// Read extension list from extensions folder
|
||||||
const extensions = JSON.parse(
|
const extensions = JSON.parse(
|
||||||
readFileSync(ExtensionManager.instance.getExtensionsFile(), 'utf-8'),
|
readFileSync(ExtensionManager.instance.getExtensionsFile(), 'utf-8')
|
||||||
)
|
)
|
||||||
try {
|
try {
|
||||||
// Create and store a Extension instance for each extension in list
|
// Create and store a Extension instance for each extension in list
|
||||||
@ -82,7 +82,7 @@ export function useExtensions(extensionsPath: string) {
|
|||||||
throw new Error(
|
throw new Error(
|
||||||
'Could not successfully rebuild list of installed extensions.\n' +
|
'Could not successfully rebuild list of installed extensions.\n' +
|
||||||
error +
|
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() {
|
export function getStore() {
|
||||||
if (!ExtensionManager.instance.getExtensionsFile()) {
|
if (!ExtensionManager.instance.getExtensionsFile()) {
|
||||||
throw new Error(
|
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'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
8
core/src/types/assistant/assistantEvent.ts
Normal file
8
core/src/types/assistant/assistantEvent.ts
Normal 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',
|
||||||
|
}
|
||||||
|
|
||||||
@ -1,2 +1,3 @@
|
|||||||
export * from './assistantEntity'
|
export * from './assistantEntity'
|
||||||
|
export * from './assistantEvent'
|
||||||
export * from './assistantInterface'
|
export * from './assistantInterface'
|
||||||
|
|||||||
@ -2,3 +2,26 @@ export type FileStat = {
|
|||||||
isDirectory: boolean
|
isDirectory: boolean
|
||||||
size: number
|
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
|
||||||
|
}
|
||||||
|
|||||||
@ -12,4 +12,6 @@ export enum ModelEvent {
|
|||||||
OnModelStop = 'OnModelStop',
|
OnModelStop = 'OnModelStop',
|
||||||
/** The `OnModelStopped` event is emitted when a model stopped ok. */
|
/** The `OnModelStopped` event is emitted when a model stopped ok. */
|
||||||
OnModelStopped = 'OnModelStopped',
|
OnModelStopped = 'OnModelStopped',
|
||||||
|
/** The `OnModelUpdate` event is emitted when the model list is updated. */
|
||||||
|
OnModelsUpdate = 'OnModelsUpdate',
|
||||||
}
|
}
|
||||||
|
|||||||
110
docker-compose.yml
Normal file
110
docker-compose.yml
Normal 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
|
||||||
@ -5,7 +5,11 @@ import request from 'request'
|
|||||||
import { createWriteStream, renameSync } from 'fs'
|
import { createWriteStream, renameSync } from 'fs'
|
||||||
import { DownloadEvent, DownloadRoute } from '@janhq/core'
|
import { DownloadEvent, DownloadRoute } from '@janhq/core'
|
||||||
const progress = require('request-progress')
|
const progress = require('request-progress')
|
||||||
import { DownloadManager, getJanDataFolderPath, normalizeFilePath } from '@janhq/core/node'
|
import {
|
||||||
|
DownloadManager,
|
||||||
|
getJanDataFolderPath,
|
||||||
|
normalizeFilePath,
|
||||||
|
} from '@janhq/core/node'
|
||||||
|
|
||||||
export function handleDownloaderIPCs() {
|
export function handleDownloaderIPCs() {
|
||||||
/**
|
/**
|
||||||
@ -56,20 +60,23 @@ export function handleDownloaderIPCs() {
|
|||||||
*/
|
*/
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
DownloadRoute.downloadFile,
|
DownloadRoute.downloadFile,
|
||||||
async (_event, url, fileName, network) => {
|
async (_event, url, localPath, network) => {
|
||||||
const strictSSL = !network?.ignoreSSL
|
const strictSSL = !network?.ignoreSSL
|
||||||
const proxy = network?.proxy?.startsWith('http')
|
const proxy = network?.proxy?.startsWith('http')
|
||||||
? network.proxy
|
? network.proxy
|
||||||
: undefined
|
: undefined
|
||||||
|
if (typeof localPath === 'string') {
|
||||||
if (typeof fileName === 'string') {
|
localPath = normalizeFilePath(localPath)
|
||||||
fileName = normalizeFilePath(fileName)
|
|
||||||
}
|
}
|
||||||
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 })
|
const rq = request({ url, strictSSL, proxy })
|
||||||
|
|
||||||
// Put request to download manager instance
|
// Put request to download manager instance
|
||||||
DownloadManager.instance.setRequest(fileName, rq)
|
DownloadManager.instance.setRequest(localPath, rq)
|
||||||
|
|
||||||
// Downloading file to a temp file first
|
// Downloading file to a temp file first
|
||||||
const downloadingTempFile = `${destination}.download`
|
const downloadingTempFile = `${destination}.download`
|
||||||
@ -81,6 +88,7 @@ export function handleDownloaderIPCs() {
|
|||||||
{
|
{
|
||||||
...state,
|
...state,
|
||||||
fileName,
|
fileName,
|
||||||
|
modelId,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -90,11 +98,12 @@ export function handleDownloaderIPCs() {
|
|||||||
{
|
{
|
||||||
fileName,
|
fileName,
|
||||||
err,
|
err,
|
||||||
|
modelId,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.on('end', function () {
|
.on('end', function () {
|
||||||
if (DownloadManager.instance.networkRequests[fileName]) {
|
if (DownloadManager.instance.networkRequests[localPath]) {
|
||||||
// Finished downloading, rename temp file to actual file
|
// Finished downloading, rename temp file to actual file
|
||||||
renameSync(downloadingTempFile, destination)
|
renameSync(downloadingTempFile, destination)
|
||||||
|
|
||||||
@ -102,14 +111,16 @@ export function handleDownloaderIPCs() {
|
|||||||
DownloadEvent.onFileDownloadSuccess,
|
DownloadEvent.onFileDownloadSuccess,
|
||||||
{
|
{
|
||||||
fileName,
|
fileName,
|
||||||
|
modelId,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
DownloadManager.instance.setRequest(fileName, undefined)
|
DownloadManager.instance.setRequest(localPath, undefined)
|
||||||
} else {
|
} else {
|
||||||
WindowManager?.instance.currentWindow?.webContents.send(
|
WindowManager?.instance.currentWindow?.webContents.send(
|
||||||
DownloadEvent.onFileDownloadError,
|
DownloadEvent.onFileDownloadError,
|
||||||
{
|
{
|
||||||
fileName,
|
fileName,
|
||||||
|
modelId,
|
||||||
err: { message: 'aborted' },
|
err: { message: 'aborted' },
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@ -38,6 +38,7 @@ export function handleFileMangerIPCs() {
|
|||||||
getResourcePath()
|
getResourcePath()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Handles the 'getUserHomePath' IPC event. This event is triggered to get the user home path.
|
||||||
ipcMain.handle(FileManagerRoute.getUserHomePath, async (_event) =>
|
ipcMain.handle(FileManagerRoute.getUserHomePath, async (_event) =>
|
||||||
app.getPath('home')
|
app.getPath('home')
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
import { ipcMain } from 'electron'
|
import { ipcMain } from 'electron'
|
||||||
|
|
||||||
import { getJanDataFolderPath, normalizeFilePath } from '@janhq/core/node'
|
import { getJanDataFolderPath, normalizeFilePath } from '@janhq/core/node'
|
||||||
import fs from 'fs'
|
import { FileSystemRoute } from '@janhq/core'
|
||||||
import { FileManagerRoute, FileSystemRoute } from '@janhq/core'
|
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
/**
|
/**
|
||||||
* Handles file system operations.
|
* Handles file system operations.
|
||||||
|
|||||||
@ -57,17 +57,18 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint . --ext \".js,.jsx,.ts,.tsx\"",
|
"lint": "eslint . --ext \".js,.jsx,.ts,.tsx\"",
|
||||||
"test:e2e": "playwright test --workers=1",
|
"test:e2e": "playwright test --workers=1",
|
||||||
"dev": "tsc -p . && electron .",
|
"copy:assets": "rimraf --glob \"./pre-install/*.tgz\" && cpx \"../pre-install/*.tgz\" \"./pre-install\"",
|
||||||
"build": "run-script-os",
|
"dev": "yarn copy:assets && tsc -p . && electron .",
|
||||||
"build:test": "run-script-os",
|
"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:darwin": "tsc -p . && electron-builder -p never -m --dir",
|
||||||
"build:test:win32": "tsc -p . && electron-builder -p never -w --dir",
|
"build:test:win32": "tsc -p . && electron-builder -p never -w --dir",
|
||||||
"build:test:linux": "tsc -p . && electron-builder -p never -l --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:win32": "tsc -p . && electron-builder -p never -w",
|
||||||
"build:linux": "tsc -p . && electron-builder -p never -l deb -l AppImage",
|
"build:linux": "tsc -p . && electron-builder -p never -l deb -l AppImage",
|
||||||
"build:publish": "run-script-os",
|
"build:publish": "yarn copy:assets && run-script-os",
|
||||||
"build:publish:darwin": "tsc -p . && electron-builder -p always -m",
|
"build:publish:darwin": "tsc -p . && electron-builder -p always -m --x64 --arm64",
|
||||||
"build:publish:win32": "tsc -p . && electron-builder -p always -w",
|
"build:publish:win32": "tsc -p . && electron-builder -p always -w",
|
||||||
"build:publish:linux": "tsc -p . && electron-builder -p always -l deb -l AppImage"
|
"build:publish:linux": "tsc -p . && electron-builder -p always -l deb -l AppImage"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -8,9 +8,9 @@
|
|||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc --module commonjs && rollup -c rollup.config.ts",
|
"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: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 ../../electron/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 ../../electron/pre-install",
|
"build:publish:win32": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../pre-install",
|
||||||
"build:publish": "run-script-os"
|
"build:publish": "run-script-os"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import {
|
|||||||
joinPath,
|
joinPath,
|
||||||
executeOnMain,
|
executeOnMain,
|
||||||
AssistantExtension,
|
AssistantExtension,
|
||||||
|
AssistantEvent,
|
||||||
} from "@janhq/core";
|
} from "@janhq/core";
|
||||||
|
|
||||||
export default class JanAssistantExtension extends AssistantExtension {
|
export default class JanAssistantExtension extends AssistantExtension {
|
||||||
@ -21,7 +22,7 @@ export default class JanAssistantExtension extends AssistantExtension {
|
|||||||
async onLoad() {
|
async onLoad() {
|
||||||
// making the assistant directory
|
// making the assistant directory
|
||||||
const assistantDirExist = await fs.existsSync(
|
const assistantDirExist = await fs.existsSync(
|
||||||
JanAssistantExtension._homeDir,
|
JanAssistantExtension._homeDir
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
localStorage.getItem(`${EXTENSION_NAME}-version`) !== VERSION ||
|
localStorage.getItem(`${EXTENSION_NAME}-version`) !== VERSION ||
|
||||||
@ -31,14 +32,16 @@ export default class JanAssistantExtension extends AssistantExtension {
|
|||||||
await fs.mkdirSync(JanAssistantExtension._homeDir);
|
await fs.mkdirSync(JanAssistantExtension._homeDir);
|
||||||
|
|
||||||
// Write assistant metadata
|
// Write assistant metadata
|
||||||
this.createJanAssistant();
|
await this.createJanAssistant();
|
||||||
// Finished migration
|
// Finished migration
|
||||||
localStorage.setItem(`${EXTENSION_NAME}-version`, VERSION);
|
localStorage.setItem(`${EXTENSION_NAME}-version`, VERSION);
|
||||||
|
// Update the assistant list
|
||||||
|
events.emit(AssistantEvent.OnAssistantsUpdate, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Events subscription
|
// Events subscription
|
||||||
events.on(MessageEvent.OnMessageSent, (data: MessageRequest) =>
|
events.on(MessageEvent.OnMessageSent, (data: MessageRequest) =>
|
||||||
JanAssistantExtension.handleMessageRequest(data, this),
|
JanAssistantExtension.handleMessageRequest(data, this)
|
||||||
);
|
);
|
||||||
|
|
||||||
events.on(InferenceEvent.OnInferenceStopped, () => {
|
events.on(InferenceEvent.OnInferenceStopped, () => {
|
||||||
@ -53,7 +56,7 @@ export default class JanAssistantExtension extends AssistantExtension {
|
|||||||
|
|
||||||
private static async handleMessageRequest(
|
private static async handleMessageRequest(
|
||||||
data: MessageRequest,
|
data: MessageRequest,
|
||||||
instance: JanAssistantExtension,
|
instance: JanAssistantExtension
|
||||||
) {
|
) {
|
||||||
instance.isCancelled = false;
|
instance.isCancelled = false;
|
||||||
instance.controller = new AbortController();
|
instance.controller = new AbortController();
|
||||||
@ -80,7 +83,7 @@ export default class JanAssistantExtension extends AssistantExtension {
|
|||||||
NODE,
|
NODE,
|
||||||
"toolRetrievalIngestNewDocument",
|
"toolRetrievalIngestNewDocument",
|
||||||
docFile,
|
docFile,
|
||||||
data.model?.proxyEngine,
|
data.model?.proxyEngine
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -96,7 +99,7 @@ export default class JanAssistantExtension extends AssistantExtension {
|
|||||||
NODE,
|
NODE,
|
||||||
"toolRetrievalUpdateTextSplitter",
|
"toolRetrievalUpdateTextSplitter",
|
||||||
data.thread.assistants[0].tools[0]?.settings?.chunk_size ?? 4000,
|
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(
|
const retrievalResult = await executeOnMain(
|
||||||
NODE,
|
NODE,
|
||||||
"toolRetrievalQueryResult",
|
"toolRetrievalQueryResult",
|
||||||
prompt,
|
prompt
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update the message content
|
// Update the message content
|
||||||
@ -168,7 +171,7 @@ export default class JanAssistantExtension extends AssistantExtension {
|
|||||||
try {
|
try {
|
||||||
await fs.writeFileSync(
|
await fs.writeFileSync(
|
||||||
assistantMetadataPath,
|
assistantMetadataPath,
|
||||||
JSON.stringify(assistant, null, 2),
|
JSON.stringify(assistant, null, 2)
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@ -180,7 +183,7 @@ export default class JanAssistantExtension extends AssistantExtension {
|
|||||||
// get all the assistant metadata json
|
// get all the assistant metadata json
|
||||||
const results: Assistant[] = [];
|
const results: Assistant[] = [];
|
||||||
const allFileName: string[] = await fs.readdirSync(
|
const allFileName: string[] = await fs.readdirSync(
|
||||||
JanAssistantExtension._homeDir,
|
JanAssistantExtension._homeDir
|
||||||
);
|
);
|
||||||
for (const fileName of allFileName) {
|
for (const fileName of allFileName) {
|
||||||
const filePath = await joinPath([
|
const filePath = await joinPath([
|
||||||
@ -190,7 +193,7 @@ export default class JanAssistantExtension extends AssistantExtension {
|
|||||||
|
|
||||||
if (filePath.includes(".DS_Store")) continue;
|
if (filePath.includes(".DS_Store")) continue;
|
||||||
const jsonFiles: string[] = (await fs.readdirSync(filePath)).filter(
|
const jsonFiles: string[] = (await fs.readdirSync(filePath)).filter(
|
||||||
(file: string) => file === "assistant.json",
|
(file: string) => file === "assistant.json"
|
||||||
);
|
);
|
||||||
|
|
||||||
if (jsonFiles.length !== 1) {
|
if (jsonFiles.length !== 1) {
|
||||||
@ -200,7 +203,7 @@ export default class JanAssistantExtension extends AssistantExtension {
|
|||||||
|
|
||||||
const content = await fs.readFileSync(
|
const content = await fs.readFileSync(
|
||||||
await joinPath([filePath, jsonFiles[0]]),
|
await joinPath([filePath, jsonFiles[0]]),
|
||||||
"utf-8",
|
"utf-8"
|
||||||
);
|
);
|
||||||
const assistant: Assistant =
|
const assistant: Assistant =
|
||||||
typeof content === "object" ? content : JSON.parse(content);
|
typeof content === "object" ? content : JSON.parse(content);
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc -b . && webpack --config webpack.config.js",
|
"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": {
|
"exports": {
|
||||||
".": "./dist/index.js",
|
".": "./dist/index.js",
|
||||||
|
|||||||
@ -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: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:win32": "download.bat",
|
||||||
"downloadnitro": "run-script-os",
|
"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: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 ../../electron/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 ../../electron/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"
|
"build:publish": "run-script-os"
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc -b . && webpack --config webpack.config.js",
|
"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": {
|
"exports": {
|
||||||
".": "./dist/index.js",
|
".": "./dist/index.js",
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc -b . && webpack --config webpack.config.js",
|
"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": {
|
"exports": {
|
||||||
".": "./dist/index.js",
|
".": "./dist/index.js",
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc -b . && webpack --config webpack.config.js",
|
"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": {
|
"devDependencies": {
|
||||||
"cpx": "^1.5.0",
|
"cpx": "^1.5.0",
|
||||||
|
|||||||
@ -1,3 +1,15 @@
|
|||||||
declare const EXTENSION_NAME: string
|
export {}
|
||||||
declare const MODULE_PATH: string
|
declare global {
|
||||||
declare const VERSION: stringå
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
11
extensions/model-extension/src/helpers/path.ts
Normal file
11
extensions/model-extension/src/helpers/path.ts
Normal 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
|
||||||
|
}
|
||||||
@ -8,7 +8,13 @@ import {
|
|||||||
ModelExtension,
|
ModelExtension,
|
||||||
Model,
|
Model,
|
||||||
getJanDataFolderPath,
|
getJanDataFolderPath,
|
||||||
|
events,
|
||||||
|
DownloadEvent,
|
||||||
|
DownloadRoute,
|
||||||
|
ModelEvent,
|
||||||
} from '@janhq/core'
|
} from '@janhq/core'
|
||||||
|
import { DownloadState } from '@janhq/core/.'
|
||||||
|
import { extractFileName } from './helpers/path'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A extension for models
|
* A extension for models
|
||||||
@ -29,6 +35,8 @@ export default class JanModelExtension extends ModelExtension {
|
|||||||
*/
|
*/
|
||||||
async onLoad() {
|
async onLoad() {
|
||||||
this.copyModelsToHomeDir()
|
this.copyModelsToHomeDir()
|
||||||
|
// Handle Desktop Events
|
||||||
|
this.handleDesktopEvents()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -61,6 +69,8 @@ export default class JanModelExtension extends ModelExtension {
|
|||||||
|
|
||||||
// Finished migration
|
// Finished migration
|
||||||
localStorage.setItem(`${EXTENSION_NAME}-version`, VERSION)
|
localStorage.setItem(`${EXTENSION_NAME}-version`, VERSION)
|
||||||
|
|
||||||
|
events.emit(ModelEvent.OnModelsUpdate, {})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
}
|
}
|
||||||
@ -83,31 +93,66 @@ export default class JanModelExtension extends ModelExtension {
|
|||||||
if (model.sources.length > 1) {
|
if (model.sources.length > 1) {
|
||||||
// path to model binaries
|
// path to model binaries
|
||||||
for (const source of model.sources) {
|
for (const source of model.sources) {
|
||||||
let path = this.extractFileName(source.url)
|
let path = extractFileName(
|
||||||
|
source.url,
|
||||||
|
JanModelExtension._supportedModelFormat
|
||||||
|
)
|
||||||
if (source.filename) {
|
if (source.filename) {
|
||||||
path = await joinPath([modelDirPath, source.filename])
|
path = await joinPath([modelDirPath, source.filename])
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadFile(source.url, path, network)
|
downloadFile(source.url, path, network)
|
||||||
}
|
}
|
||||||
|
// TODO: handle multiple binaries for web later
|
||||||
} else {
|
} else {
|
||||||
const fileName = this.extractFileName(model.sources[0]?.url)
|
const fileName = extractFileName(
|
||||||
|
model.sources[0]?.url,
|
||||||
|
JanModelExtension._supportedModelFormat
|
||||||
|
)
|
||||||
const path = await joinPath([modelDirPath, fileName])
|
const path = await joinPath([modelDirPath, fileName])
|
||||||
downloadFile(model.sources[0]?.url, path, network)
|
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 {
|
private async startPollingDownloadProgress(modelId: string): Promise<void> {
|
||||||
const extractedFileName = url.split('/').pop()
|
// wait for some seconds before polling
|
||||||
const fileName = extractedFileName
|
await new Promise((resolve) => setTimeout(resolve, 3000))
|
||||||
.toLowerCase()
|
|
||||||
.endsWith(JanModelExtension._supportedModelFormat)
|
return new Promise((resolve) => {
|
||||||
? extractedFileName
|
const interval = setInterval(async () => {
|
||||||
: extractedFileName + JanModelExtension._supportedModelFormat
|
fetch(
|
||||||
return fileName
|
`${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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultModel = await this.getDefaultModel() as Model
|
const defaultModel = (await this.getDefaultModel()) as Model
|
||||||
if (!defaultModel) {
|
if (!defaultModel) {
|
||||||
console.error('Unable to find default model')
|
console.error('Unable to find default model')
|
||||||
return
|
return
|
||||||
@ -382,4 +427,28 @@ export default class JanModelExtension extends ModelExtension {
|
|||||||
async getConfiguredModels(): Promise<Model[]> {
|
async getConfiguredModels(): Promise<Model[]> {
|
||||||
return this.getModelsMetadata()
|
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)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"strict": false,
|
"strict": false,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"rootDir": "./src"
|
"rootDir": "./src",
|
||||||
},
|
},
|
||||||
"include": ["./src"]
|
"include": ["./src"],
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,7 +19,7 @@ module.exports = {
|
|||||||
new webpack.DefinePlugin({
|
new webpack.DefinePlugin({
|
||||||
EXTENSION_NAME: JSON.stringify(packageJson.name),
|
EXTENSION_NAME: JSON.stringify(packageJson.name),
|
||||||
MODULE_PATH: JSON.stringify(`${packageJson.name}/${packageJson.module}`),
|
MODULE_PATH: JSON.stringify(`${packageJson.name}/${packageJson.module}`),
|
||||||
VERSION: JSON.stringify(packageJson.version),
|
VERSION: JSON.stringify(packageJson.version)
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
output: {
|
output: {
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc -b . && webpack --config webpack.config.js",
|
"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": {
|
"devDependencies": {
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
|
|||||||
13
package.json
13
package.json
@ -21,22 +21,23 @@
|
|||||||
"lint": "yarn workspace jan lint && yarn workspace jan-web lint",
|
"lint": "yarn workspace jan lint && yarn workspace jan-web lint",
|
||||||
"test:unit": "yarn workspace @janhq/core test",
|
"test:unit": "yarn workspace @janhq/core test",
|
||||||
"test": "yarn workspace jan test:e2e",
|
"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:electron": "yarn copy:assets && yarn workspace jan dev",
|
||||||
"dev:web": "yarn workspace jan-web 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\"",
|
"dev": "concurrently --kill-others \"yarn dev:web\" \"wait-on http://localhost:3000 && yarn dev:electron\"",
|
||||||
"test-local": "yarn lint && yarn build:test && yarn test",
|
"test-local": "yarn lint && yarn build:test && yarn test",
|
||||||
"dev:uikit": "yarn workspace @janhq/uikit install && yarn workspace @janhq/uikit dev",
|
"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: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:core": "cd core && yarn install && yarn run build",
|
||||||
"build:web": "yarn workspace jan-web build && cpx \"web/out/**\" \"electron/renderer/\"",
|
"build:web": "yarn workspace jan-web build && cpx \"web/out/**\" \"electron/renderer/\"",
|
||||||
"build:electron": "yarn copy:assets && yarn workspace jan build",
|
"build:electron": "yarn copy:assets && yarn workspace jan build",
|
||||||
"build:electron:test": "yarn workspace jan build:test",
|
"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: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 ./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: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 ./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 ./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:extensions": "run-script-os",
|
||||||
"build:test": "yarn copy:assets && yarn build:web && yarn workspace jan build:test",
|
"build:test": "yarn copy:assets && yarn build:web && yarn workspace jan build:test",
|
||||||
"build": "yarn build:web && yarn build:electron",
|
"build": "yarn build:web && yarn build:electron",
|
||||||
|
|||||||
0
pre-install/.gitkeep
Normal file
0
pre-install/.gitkeep
Normal file
47
server/helpers/setup.ts
Normal file
47
server/helpers/setup.ts
Normal 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");
|
||||||
|
}
|
||||||
@ -38,6 +38,7 @@ export interface ServerConfig {
|
|||||||
isVerboseEnabled?: boolean;
|
isVerboseEnabled?: boolean;
|
||||||
schemaPath?: string;
|
schemaPath?: string;
|
||||||
baseDir?: string;
|
baseDir?: string;
|
||||||
|
storageAdataper?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -103,9 +104,12 @@ export const startServer = async (configs?: ServerConfig) => {
|
|||||||
{ prefix: "extensions" }
|
{ prefix: "extensions" }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Register proxy middleware
|
||||||
|
if (configs?.storageAdataper)
|
||||||
|
server.addHook("preHandler", configs.storageAdataper);
|
||||||
|
|
||||||
// Register API routes
|
// Register API routes
|
||||||
await server.register(v1Router, { prefix: "/v1" });
|
await server.register(v1Router, { prefix: "/v1" });
|
||||||
|
|
||||||
// Start listening for requests
|
// Start listening for requests
|
||||||
await server
|
await server
|
||||||
.listen({
|
.listen({
|
||||||
|
|||||||
@ -1,3 +1,7 @@
|
|||||||
import { startServer } from "./index";
|
import { s3 } from "./middleware/s3";
|
||||||
|
import { setup } from "./helpers/setup";
|
||||||
startServer();
|
import { startServer as start } from "./index";
|
||||||
|
/**
|
||||||
|
* Setup extensions and start the server
|
||||||
|
*/
|
||||||
|
setup().then(() => start({ storageAdataper: s3 }));
|
||||||
|
|||||||
70
server/middleware/s3.ts
Normal file
70
server/middleware/s3.ts
Normal 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]);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"watch": ["main.ts", "v1"],
|
|
||||||
"ext": "ts, json",
|
|
||||||
"exec": "tsc && node ./build/main.js"
|
|
||||||
}
|
|
||||||
@ -13,16 +13,18 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint . --ext \".js,.jsx,.ts,.tsx\"",
|
"lint": "eslint . --ext \".js,.jsx,.ts,.tsx\"",
|
||||||
"test:e2e": "playwright test --workers=1",
|
"test:e2e": "playwright test --workers=1",
|
||||||
"dev": "tsc --watch & node --watch build/main.js",
|
"build:core": "cd node_modules/@janhq/core && yarn install && yarn build",
|
||||||
"build": "tsc"
|
"dev": "yarn build:core && tsc --watch & node --watch build/main.js",
|
||||||
|
"build": "yarn build:core && tsc"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alumna/reflect": "^1.1.3",
|
"@alumna/reflect": "^1.1.3",
|
||||||
|
"@cyclic.sh/s3fs": "^1.2.9",
|
||||||
"@fastify/cors": "^8.4.2",
|
"@fastify/cors": "^8.4.2",
|
||||||
"@fastify/static": "^6.12.0",
|
"@fastify/static": "^6.12.0",
|
||||||
"@fastify/swagger": "^8.13.0",
|
"@fastify/swagger": "^8.13.0",
|
||||||
"@fastify/swagger-ui": "2.0.1",
|
"@fastify/swagger-ui": "2.0.1",
|
||||||
"@janhq/core": "link:./core",
|
"@janhq/core": "file:../core",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"fastify": "^4.24.3",
|
"fastify": "^4.24.3",
|
||||||
"request": "^2.88.2",
|
"request": "^2.88.2",
|
||||||
@ -39,5 +41,8 @@
|
|||||||
"run-script-os": "^1.1.6",
|
"run-script-os": "^1.1.6",
|
||||||
"@types/tcp-port-used": "^1.0.4",
|
"@types/tcp-port-used": "^1.0.4",
|
||||||
"typescript": "^5.2.2"
|
"typescript": "^5.2.2"
|
||||||
}
|
},
|
||||||
|
"bundleDependencies": [
|
||||||
|
"@janhq/core"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,5 +20,5 @@
|
|||||||
// "sourceMap": true,
|
// "sourceMap": true,
|
||||||
|
|
||||||
"include": ["./**/*.ts"],
|
"include": ["./**/*.ts"],
|
||||||
"exclude": ["core", "build", "dist", "tests", "node_modules"]
|
"exclude": ["core", "build", "dist", "tests", "node_modules", "extensions"]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,22 +13,22 @@ import {
|
|||||||
import { useAtomValue } from 'jotai'
|
import { useAtomValue } from 'jotai'
|
||||||
|
|
||||||
import useDownloadModel from '@/hooks/useDownloadModel'
|
import useDownloadModel from '@/hooks/useDownloadModel'
|
||||||
import { useDownloadState } from '@/hooks/useDownloadState'
|
import { modelDownloadStateAtom } from '@/hooks/useDownloadState'
|
||||||
|
|
||||||
import { formatDownloadPercentage } from '@/utils/converter'
|
import { formatDownloadPercentage } from '@/utils/converter'
|
||||||
|
|
||||||
import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
|
import { getDownloadingModelAtom } from '@/helpers/atoms/Model.atom'
|
||||||
|
|
||||||
export default function DownloadingState() {
|
export default function DownloadingState() {
|
||||||
const { downloadStates } = useDownloadState()
|
const downloadStates = useAtomValue(modelDownloadStateAtom)
|
||||||
const downloadingModels = useAtomValue(downloadingModelsAtom)
|
const downloadingModels = useAtomValue(getDownloadingModelAtom)
|
||||||
const { abortModelDownload } = useDownloadModel()
|
const { abortModelDownload } = useDownloadModel()
|
||||||
|
|
||||||
const totalCurrentProgress = downloadStates
|
const totalCurrentProgress = Object.values(downloadStates)
|
||||||
.map((a) => a.size.transferred + a.size.transferred)
|
.map((a) => a.size.transferred + a.size.transferred)
|
||||||
.reduce((partialSum, a) => partialSum + a, 0)
|
.reduce((partialSum, a) => partialSum + a, 0)
|
||||||
|
|
||||||
const totalSize = downloadStates
|
const totalSize = Object.values(downloadStates)
|
||||||
.map((a) => a.size.total + a.size.total)
|
.map((a) => a.size.total + a.size.total)
|
||||||
.reduce((partialSum, a) => partialSum + a, 0)
|
.reduce((partialSum, a) => partialSum + a, 0)
|
||||||
|
|
||||||
@ -36,12 +36,14 @@ export default function DownloadingState() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{downloadStates?.length > 0 && (
|
{Object.values(downloadStates)?.length > 0 && (
|
||||||
<Modal>
|
<Modal>
|
||||||
<ModalTrigger asChild>
|
<ModalTrigger asChild>
|
||||||
<div className="relative block">
|
<div className="relative block">
|
||||||
<Button size="sm" themes="outline">
|
<Button size="sm" themes="outline">
|
||||||
<span>{downloadStates.length} Downloading model</span>
|
<span>
|
||||||
|
{Object.values(downloadStates).length} Downloading model
|
||||||
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
<span
|
<span
|
||||||
className="absolute left-0 h-full rounded-md rounded-l-md bg-primary/20"
|
className="absolute left-0 h-full rounded-md rounded-l-md bg-primary/20"
|
||||||
@ -55,8 +57,7 @@ export default function DownloadingState() {
|
|||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
<ModalTitle>Downloading model</ModalTitle>
|
<ModalTitle>Downloading model</ModalTitle>
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
{downloadStates.map((item, i) => {
|
{Object.values(downloadStates).map((item, i) => (
|
||||||
return (
|
|
||||||
<div className="pt-2" key={i}>
|
<div className="pt-2" key={i}>
|
||||||
<Progress
|
<Progress
|
||||||
className="mb-2 h-2"
|
className="mb-2 h-2"
|
||||||
@ -87,8 +88,7 @@ export default function DownloadingState() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
))}
|
||||||
})}
|
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -25,8 +25,7 @@ import { MainViewState } from '@/constants/screens'
|
|||||||
|
|
||||||
import { useActiveModel } from '@/hooks/useActiveModel'
|
import { useActiveModel } from '@/hooks/useActiveModel'
|
||||||
|
|
||||||
import { useDownloadState } from '@/hooks/useDownloadState'
|
import { modelDownloadStateAtom } from '@/hooks/useDownloadState'
|
||||||
|
|
||||||
import useGetSystemResources from '@/hooks/useGetSystemResources'
|
import useGetSystemResources from '@/hooks/useGetSystemResources'
|
||||||
import { useMainViewState } from '@/hooks/useMainViewState'
|
import { useMainViewState } from '@/hooks/useMainViewState'
|
||||||
|
|
||||||
@ -53,7 +52,7 @@ const BottomBar = () => {
|
|||||||
const downloadedModels = useAtomValue(downloadedModelsAtom)
|
const downloadedModels = useAtomValue(downloadedModelsAtom)
|
||||||
|
|
||||||
const { setMainViewState } = useMainViewState()
|
const { setMainViewState } = useMainViewState()
|
||||||
const { downloadStates } = useDownloadState()
|
const downloadStates = useAtomValue(modelDownloadStateAtom)
|
||||||
const setShowSelectModelModal = useSetAtom(showSelectModelModalAtom)
|
const setShowSelectModelModal = useSetAtom(showSelectModelModalAtom)
|
||||||
const [serverEnabled] = useAtom(serverEnabledAtom)
|
const [serverEnabled] = useAtom(serverEnabledAtom)
|
||||||
|
|
||||||
@ -109,7 +108,7 @@ const BottomBar = () => {
|
|||||||
)}
|
)}
|
||||||
{downloadedModels.length === 0 &&
|
{downloadedModels.length === 0 &&
|
||||||
!stateModel.loading &&
|
!stateModel.loading &&
|
||||||
downloadStates.length === 0 && (
|
Object.values(downloadStates).length === 0 && (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
themes="outline"
|
themes="outline"
|
||||||
|
|||||||
@ -17,23 +17,22 @@ import {
|
|||||||
import { atom, useAtomValue } from 'jotai'
|
import { atom, useAtomValue } from 'jotai'
|
||||||
|
|
||||||
import useDownloadModel from '@/hooks/useDownloadModel'
|
import useDownloadModel from '@/hooks/useDownloadModel'
|
||||||
import { useDownloadState } from '@/hooks/useDownloadState'
|
|
||||||
|
import { modelDownloadStateAtom } from '@/hooks/useDownloadState'
|
||||||
|
|
||||||
import { formatDownloadPercentage } from '@/utils/converter'
|
import { formatDownloadPercentage } from '@/utils/converter'
|
||||||
|
|
||||||
import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
|
import { getDownloadingModelAtom } from '@/helpers/atoms/Model.atom'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
model: Model
|
model: Model
|
||||||
isFromList?: boolean
|
isFromList?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ModalCancelDownload({ model, isFromList }: Props) {
|
const ModalCancelDownload: React.FC<Props> = ({ model, isFromList }) => {
|
||||||
const { modelDownloadStateAtom } = useDownloadState()
|
const downloadingModels = useAtomValue(getDownloadingModelAtom)
|
||||||
const downloadingModels = useAtomValue(downloadingModelsAtom)
|
|
||||||
const downloadAtom = useMemo(
|
const downloadAtom = useMemo(
|
||||||
() => atom((get) => get(modelDownloadStateAtom)[model.id]),
|
() => atom((get) => get(modelDownloadStateAtom)[model.id]),
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
[model.id]
|
[model.id]
|
||||||
)
|
)
|
||||||
const downloadState = useAtomValue(downloadAtom)
|
const downloadState = useAtomValue(downloadAtom)
|
||||||
@ -98,3 +97,5 @@ export default function ModalCancelDownload({ model, isFromList }: Props) {
|
|||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default ModalCancelDownload
|
||||||
|
|||||||
@ -1,93 +1,62 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* 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 { DownloadEvent, events } from '@janhq/core'
|
||||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
import { useSetAtom } from 'jotai'
|
||||||
|
|
||||||
import { useDownloadState } from '@/hooks/useDownloadState'
|
import { setDownloadStateAtom } from '@/hooks/useDownloadState'
|
||||||
|
|
||||||
import { modelBinFileName } from '@/utils/model'
|
|
||||||
|
|
||||||
import EventHandler from './EventHandler'
|
import EventHandler from './EventHandler'
|
||||||
|
|
||||||
import { appDownloadProgress } from './Jotai'
|
import { appDownloadProgress } from './Jotai'
|
||||||
|
|
||||||
import {
|
const EventListenerWrapper = ({ children }: PropsWithChildren) => {
|
||||||
downloadedModelsAtom,
|
const setDownloadState = useSetAtom(setDownloadStateAtom)
|
||||||
downloadingModelsAtom,
|
|
||||||
} from '@/helpers/atoms/Model.atom'
|
|
||||||
|
|
||||||
export default function EventListenerWrapper({ children }: PropsWithChildren) {
|
|
||||||
const setProgress = useSetAtom(appDownloadProgress)
|
const setProgress = useSetAtom(appDownloadProgress)
|
||||||
const models = useAtomValue(downloadingModelsAtom)
|
|
||||||
const modelsRef = useRef(models)
|
|
||||||
|
|
||||||
const [downloadedModels, setDownloadedModels] = useAtom(downloadedModelsAtom)
|
const onFileDownloadUpdate = useCallback(
|
||||||
const {
|
async (state: DownloadState) => {
|
||||||
setDownloadState,
|
console.debug('onFileDownloadUpdate', state)
|
||||||
setDownloadStateSuccess,
|
setDownloadState(state)
|
||||||
setDownloadStateFailed,
|
},
|
||||||
setDownloadStateCancelled,
|
[setDownloadState]
|
||||||
} = useDownloadState()
|
)
|
||||||
const downloadedModelRef = useRef(downloadedModels)
|
|
||||||
|
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(() => {
|
useEffect(() => {
|
||||||
modelsRef.current = models
|
console.log('EventListenerWrapper: registering event listeners...')
|
||||||
}, [models])
|
|
||||||
useEffect(() => {
|
events.on(DownloadEvent.onFileDownloadUpdate, onFileDownloadUpdate)
|
||||||
downloadedModelRef.current = downloadedModels
|
events.on(DownloadEvent.onFileDownloadError, onFileDownloadError)
|
||||||
}, [downloadedModels])
|
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(() => {
|
useEffect(() => {
|
||||||
if (window && window.electronAPI) {
|
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(
|
window.electronAPI.onAppUpdateDownloadUpdate(
|
||||||
(_event: string, progress: any) => {
|
(_event: string, progress: any) => {
|
||||||
setProgress(progress.percent)
|
setProgress(progress.percent)
|
||||||
@ -107,14 +76,9 @@ export default function EventListenerWrapper({ children }: PropsWithChildren) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
return () => {}
|
return () => {}
|
||||||
}, [
|
}, [setDownloadState, setProgress])
|
||||||
setDownloadState,
|
|
||||||
setDownloadStateCancelled,
|
|
||||||
setDownloadStateFailed,
|
|
||||||
setDownloadStateSuccess,
|
|
||||||
setDownloadedModels,
|
|
||||||
setProgress,
|
|
||||||
])
|
|
||||||
|
|
||||||
return <EventHandler>{children}</EventHandler>
|
return <EventHandler>{children}</EventHandler>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default EventListenerWrapper
|
||||||
|
|||||||
@ -4,23 +4,28 @@ import { atom } from 'jotai'
|
|||||||
export const stateModel = atom({ state: 'start', loading: false, model: '' })
|
export const stateModel = atom({ state: 'start', loading: false, model: '' })
|
||||||
export const activeAssistantModelAtom = atom<Model | undefined>(undefined)
|
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(
|
export const getDownloadingModelAtom = atom((get) => get(downloadingModelsAtom))
|
||||||
null,
|
|
||||||
(get, set, model: Model) => {
|
export const addDownloadingModelAtom = atom(null, (get, set, model: Model) => {
|
||||||
const currentModels = get(downloadingModelsAtom)
|
const downloadingModels = get(downloadingModelsAtom)
|
||||||
set(downloadingModelsAtom, [...currentModels, model])
|
if (!downloadingModels.find((e) => e.id === model.id)) {
|
||||||
|
set(downloadingModelsAtom, [...downloadingModels, model])
|
||||||
}
|
}
|
||||||
)
|
})
|
||||||
|
|
||||||
export const removeDownloadingModelAtom = atom(
|
export const removeDownloadingModelAtom = atom(
|
||||||
null,
|
null,
|
||||||
(get, set, modelId: string) => {
|
(get, set, modelId: string) => {
|
||||||
const currentModels = get(downloadingModelsAtom)
|
const downloadingModels = get(downloadingModelsAtom)
|
||||||
|
|
||||||
set(
|
set(
|
||||||
downloadingModelsAtom,
|
downloadingModelsAtom,
|
||||||
currentModels.filter((e) => e.id !== modelId)
|
downloadingModels.filter((e) => e.id !== modelId)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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'
|
import { useSetAtom } from 'jotai'
|
||||||
|
|
||||||
@ -10,14 +16,19 @@ import { assistantsAtom } from '@/helpers/atoms/Assistant.atom'
|
|||||||
const useAssistants = () => {
|
const useAssistants = () => {
|
||||||
const setAssistants = useSetAtom(assistantsAtom)
|
const setAssistants = useSetAtom(assistantsAtom)
|
||||||
|
|
||||||
useEffect(() => {
|
const getData = useCallback(async () => {
|
||||||
const getAssistants = async () => {
|
|
||||||
const assistants = await getLocalAssistants()
|
const assistants = await getLocalAssistants()
|
||||||
setAssistants(assistants)
|
setAssistants(assistants)
|
||||||
}
|
|
||||||
|
|
||||||
getAssistants()
|
|
||||||
}, [setAssistants])
|
}, [setAssistants])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getData()
|
||||||
|
|
||||||
|
events.on(AssistantEvent.OnAssistantsUpdate, () => getData())
|
||||||
|
return () => {
|
||||||
|
events.off(AssistantEvent.OnAssistantsUpdate, () => getData())
|
||||||
|
}
|
||||||
|
}, [getData])
|
||||||
}
|
}
|
||||||
|
|
||||||
const getLocalAssistants = async (): Promise<Assistant[]> =>
|
const getLocalAssistants = async (): Promise<Assistant[]> =>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useContext } from 'react'
|
import { useCallback, useContext } from 'react'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Model,
|
Model,
|
||||||
@ -15,36 +15,21 @@ import { FeatureToggleContext } from '@/context/FeatureToggle'
|
|||||||
|
|
||||||
import { modelBinFileName } from '@/utils/model'
|
import { modelBinFileName } from '@/utils/model'
|
||||||
|
|
||||||
import { useDownloadState } from './useDownloadState'
|
import { setDownloadStateAtom } from './useDownloadState'
|
||||||
|
|
||||||
import { extensionManager } from '@/extension/ExtensionManager'
|
import { extensionManager } from '@/extension/ExtensionManager'
|
||||||
import { addNewDownloadingModelAtom } from '@/helpers/atoms/Model.atom'
|
import { addDownloadingModelAtom } from '@/helpers/atoms/Model.atom'
|
||||||
|
|
||||||
export default function useDownloadModel() {
|
export default function useDownloadModel() {
|
||||||
const { ignoreSSL, proxy } = useContext(FeatureToggleContext)
|
const { ignoreSSL, proxy } = useContext(FeatureToggleContext)
|
||||||
const { setDownloadState } = useDownloadState()
|
const setDownloadState = useSetAtom(setDownloadStateAtom)
|
||||||
const addNewDownloadingModel = useSetAtom(addNewDownloadingModelAtom)
|
const addDownloadingModel = useSetAtom(addDownloadingModelAtom)
|
||||||
|
|
||||||
const downloadModel = async (model: Model) => {
|
const downloadModel = useCallback(
|
||||||
const childrenDownloadProgress: DownloadState[] = []
|
async (model: Model) => {
|
||||||
model.sources.forEach((source: ModelArtifact) => {
|
const childProgresses: DownloadState[] = model.sources.map(
|
||||||
childrenDownloadProgress.push({
|
(source: ModelArtifact) => ({
|
||||||
modelId: source.filename,
|
filename: source.filename,
|
||||||
time: {
|
|
||||||
elapsed: 0,
|
|
||||||
remaining: 0,
|
|
||||||
},
|
|
||||||
speed: 0,
|
|
||||||
percent: 0,
|
|
||||||
size: {
|
|
||||||
total: 0,
|
|
||||||
transferred: 0,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// set an initial download state
|
|
||||||
setDownloadState({
|
|
||||||
modelId: model.id,
|
modelId: model.id,
|
||||||
time: {
|
time: {
|
||||||
elapsed: 0,
|
elapsed: 0,
|
||||||
@ -56,15 +41,34 @@ export default function useDownloadModel() {
|
|||||||
total: 0,
|
total: 0,
|
||||||
transferred: 0,
|
transferred: 0,
|
||||||
},
|
},
|
||||||
children: childrenDownloadProgress,
|
downloadState: 'downloading',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// set an initial download state
|
||||||
|
setDownloadState({
|
||||||
|
filename: '',
|
||||||
|
modelId: model.id,
|
||||||
|
time: {
|
||||||
|
elapsed: 0,
|
||||||
|
remaining: 0,
|
||||||
|
},
|
||||||
|
speed: 0,
|
||||||
|
percent: 0,
|
||||||
|
size: {
|
||||||
|
total: 0,
|
||||||
|
transferred: 0,
|
||||||
|
},
|
||||||
|
children: childProgresses,
|
||||||
|
downloadState: 'downloading',
|
||||||
})
|
})
|
||||||
|
|
||||||
addNewDownloadingModel(model)
|
addDownloadingModel(model)
|
||||||
|
|
||||||
await extensionManager
|
await localDownloadModel(model, ignoreSSL, proxy)
|
||||||
.get<ModelExtension>(ExtensionTypeEnum.Model)
|
},
|
||||||
?.downloadModel(model, { ignoreSSL, proxy })
|
[ignoreSSL, proxy, addDownloadingModel, setDownloadState]
|
||||||
}
|
)
|
||||||
|
|
||||||
const abortModelDownload = async (model: Model) => {
|
const abortModelDownload = async (model: Model) => {
|
||||||
await abortDownload(
|
await abortDownload(
|
||||||
@ -77,3 +81,12 @@ export default function useDownloadModel() {
|
|||||||
abortModelDownload,
|
abortModelDownload,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const localDownloadModel = async (
|
||||||
|
model: Model,
|
||||||
|
ignoreSSL: boolean,
|
||||||
|
proxy: string
|
||||||
|
) =>
|
||||||
|
extensionManager
|
||||||
|
.get<ModelExtension>(ExtensionTypeEnum.Model)
|
||||||
|
?.downloadModel(model, { ignoreSSL, proxy })
|
||||||
|
|||||||
@ -1,96 +1,64 @@
|
|||||||
import { atom, useSetAtom, useAtomValue } from 'jotai'
|
import { atom } from 'jotai'
|
||||||
|
|
||||||
import { toaster } from '@/containers/Toast'
|
import { toaster } from '@/containers/Toast'
|
||||||
|
|
||||||
|
import {
|
||||||
|
configuredModelsAtom,
|
||||||
|
downloadedModelsAtom,
|
||||||
|
removeDownloadingModelAtom,
|
||||||
|
} from '@/helpers/atoms/Model.atom'
|
||||||
|
|
||||||
// download states
|
// download states
|
||||||
const modelDownloadStateAtom = atom<Record<string, DownloadState>>({})
|
export const modelDownloadStateAtom = atom<Record<string, DownloadState>>({})
|
||||||
|
|
||||||
const setDownloadStateAtom = atom(null, (get, set, state: DownloadState) => {
|
/**
|
||||||
|
* Used to set the download state for a particular model.
|
||||||
|
*/
|
||||||
|
export const setDownloadStateAtom = atom(
|
||||||
|
null,
|
||||||
|
(get, set, state: DownloadState) => {
|
||||||
const currentState = { ...get(modelDownloadStateAtom) }
|
const currentState = { ...get(modelDownloadStateAtom) }
|
||||||
console.debug(
|
|
||||||
`current download state for ${state.modelId} is ${JSON.stringify(state)}`
|
if (state.downloadState === 'end') {
|
||||||
|
// download successfully
|
||||||
|
delete currentState[state.modelId]
|
||||||
|
set(removeDownloadingModelAtom, state.modelId)
|
||||||
|
const model = get(configuredModelsAtom).find(
|
||||||
|
(e) => e.id === state.modelId
|
||||||
)
|
)
|
||||||
currentState[state.modelId] = state
|
if (model) set(downloadedModelsAtom, (prev) => [...prev, model])
|
||||||
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({
|
toaster({
|
||||||
title: 'Download Completed',
|
title: 'Download Completed',
|
||||||
description: `Download ${modelId} completed`,
|
description: `Download ${state.modelId} completed`,
|
||||||
type: 'success',
|
type: 'success',
|
||||||
})
|
})
|
||||||
})
|
} else if (state.downloadState === 'error') {
|
||||||
|
// download error
|
||||||
const setDownloadStateFailedAtom = atom(
|
delete currentState[state.modelId]
|
||||||
null,
|
set(removeDownloadingModelAtom, state.modelId)
|
||||||
(get, set, modelId: string, error: string) => {
|
if (state.error === 'aborted') {
|
||||||
const currentState = { ...get(modelDownloadStateAtom) }
|
toaster({
|
||||||
const state = currentState[modelId]
|
title: 'Cancel Download',
|
||||||
if (!state) {
|
description: `Model ${state.modelId} download cancelled`,
|
||||||
console.debug(`Cannot find download state for ${modelId}`)
|
type: 'warning',
|
||||||
return
|
})
|
||||||
}
|
} else {
|
||||||
if (error.includes('certificate')) {
|
let error = state.error
|
||||||
error += '. To fix enable "Ignore SSL Certificates" in Advanced settings.'
|
if (state.error?.includes('certificate')) {
|
||||||
|
error +=
|
||||||
|
'. To fix enable "Ignore SSL Certificates" in Advanced settings.'
|
||||||
}
|
}
|
||||||
toaster({
|
toaster({
|
||||||
title: 'Download Failed',
|
title: 'Download Failed',
|
||||||
description: `Model ${modelId} download failed: ${error}`,
|
description: `Model ${state.modelId} download failed: ${error}`,
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// download in progress
|
||||||
|
currentState[state.modelId] = state
|
||||||
|
}
|
||||||
|
|
||||||
delete currentState[modelId]
|
|
||||||
set(modelDownloadStateAtom, currentState)
|
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}`)
|
|
||||||
toaster({
|
|
||||||
title: 'Cancel Download',
|
|
||||||
description: `Model ${modelId} cancel download`,
|
|
||||||
type: 'warning',
|
|
||||||
})
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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'
|
import { useSetAtom } from 'jotai'
|
||||||
|
|
||||||
@ -14,23 +20,30 @@ const useModels = () => {
|
|||||||
const setDownloadedModels = useSetAtom(downloadedModelsAtom)
|
const setDownloadedModels = useSetAtom(downloadedModelsAtom)
|
||||||
const setConfiguredModels = useSetAtom(configuredModelsAtom)
|
const setConfiguredModels = useSetAtom(configuredModelsAtom)
|
||||||
|
|
||||||
useEffect(() => {
|
const getData = useCallback(() => {
|
||||||
const getDownloadedModels = async () => {
|
const getDownloadedModels = async () => {
|
||||||
const models = await getLocalDownloadedModels()
|
const models = await getLocalDownloadedModels()
|
||||||
setDownloadedModels(models)
|
setDownloadedModels(models)
|
||||||
}
|
}
|
||||||
|
|
||||||
getDownloadedModels()
|
|
||||||
}, [setDownloadedModels])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const getConfiguredModels = async () => {
|
const getConfiguredModels = async () => {
|
||||||
const models = await getLocalConfiguredModels()
|
const models = await getLocalConfiguredModels()
|
||||||
setConfiguredModels(models)
|
setConfiguredModels(models)
|
||||||
}
|
}
|
||||||
|
getDownloadedModels()
|
||||||
getConfiguredModels()
|
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[]> =>
|
const getLocalConfiguredModels = async (): Promise<Model[]> =>
|
||||||
|
|||||||
@ -10,8 +10,6 @@ import {
|
|||||||
|
|
||||||
import { useSetAtom } from 'jotai'
|
import { useSetAtom } from 'jotai'
|
||||||
|
|
||||||
import { loadModelErrorAtom } from './useActiveModel'
|
|
||||||
|
|
||||||
import { extensionManager } from '@/extension'
|
import { extensionManager } from '@/extension'
|
||||||
import { setConvoMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
|
import { setConvoMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
|
||||||
import {
|
import {
|
||||||
@ -26,7 +24,6 @@ export default function useSetActiveThread() {
|
|||||||
const setThreadMessage = useSetAtom(setConvoMessagesAtom)
|
const setThreadMessage = useSetAtom(setConvoMessagesAtom)
|
||||||
const setThreadModelParams = useSetAtom(setThreadModelParamsAtom)
|
const setThreadModelParams = useSetAtom(setThreadModelParamsAtom)
|
||||||
const setIsGeneratingResponse = useSetAtom(isGeneratingResponseAtom)
|
const setIsGeneratingResponse = useSetAtom(isGeneratingResponseAtom)
|
||||||
const setLoadModelError = useSetAtom(loadModelErrorAtom)
|
|
||||||
|
|
||||||
const setActiveThread = useCallback(
|
const setActiveThread = useCallback(
|
||||||
async (thread: Thread) => {
|
async (thread: Thread) => {
|
||||||
|
|||||||
@ -27,7 +27,9 @@ const nextConfig = {
|
|||||||
VERSION: JSON.stringify(packageJson.version),
|
VERSION: JSON.stringify(packageJson.version),
|
||||||
ANALYTICS_ID: JSON.stringify(process.env.ANALYTICS_ID),
|
ANALYTICS_ID: JSON.stringify(process.env.ANALYTICS_ID),
|
||||||
ANALYTICS_HOST: JSON.stringify(process.env.ANALYTICS_HOST),
|
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',
|
isMac: process.platform === 'darwin',
|
||||||
isWindows: process.platform === 'win32',
|
isWindows: process.platform === 'win32',
|
||||||
isLinux: process.platform === 'linux',
|
isLinux: process.platform === 'linux',
|
||||||
|
|||||||
@ -10,8 +10,6 @@ import LogoMark from '@/containers/Brand/Logo/Mark'
|
|||||||
|
|
||||||
import { MainViewState } from '@/constants/screens'
|
import { MainViewState } from '@/constants/screens'
|
||||||
|
|
||||||
import { loadModelErrorAtom } from '@/hooks/useActiveModel'
|
|
||||||
|
|
||||||
import { useMainViewState } from '@/hooks/useMainViewState'
|
import { useMainViewState } from '@/hooks/useMainViewState'
|
||||||
|
|
||||||
import ChatItem from '../ChatItem'
|
import ChatItem from '../ChatItem'
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import { Button } from '@janhq/uikit'
|
|||||||
import { useAtomValue, useSetAtom } from 'jotai'
|
import { useAtomValue, useSetAtom } from 'jotai'
|
||||||
import { RefreshCcw } from 'lucide-react'
|
import { RefreshCcw } from 'lucide-react'
|
||||||
|
|
||||||
import { useActiveModel } from '@/hooks/useActiveModel'
|
|
||||||
import useSendChatMessage from '@/hooks/useSendChatMessage'
|
import useSendChatMessage from '@/hooks/useSendChatMessage'
|
||||||
|
|
||||||
import { extensionManager } from '@/extension'
|
import { extensionManager } from '@/extension'
|
||||||
|
|||||||
@ -25,7 +25,7 @@ import { MainViewState } from '@/constants/screens'
|
|||||||
import { useCreateNewThread } from '@/hooks/useCreateNewThread'
|
import { useCreateNewThread } from '@/hooks/useCreateNewThread'
|
||||||
import useDownloadModel from '@/hooks/useDownloadModel'
|
import useDownloadModel from '@/hooks/useDownloadModel'
|
||||||
|
|
||||||
import { useDownloadState } from '@/hooks/useDownloadState'
|
import { modelDownloadStateAtom } from '@/hooks/useDownloadState'
|
||||||
|
|
||||||
import { useMainViewState } from '@/hooks/useMainViewState'
|
import { useMainViewState } from '@/hooks/useMainViewState'
|
||||||
|
|
||||||
@ -49,7 +49,6 @@ type Props = {
|
|||||||
const ExploreModelItemHeader: React.FC<Props> = ({ model, onClick, open }) => {
|
const ExploreModelItemHeader: React.FC<Props> = ({ model, onClick, open }) => {
|
||||||
const { downloadModel } = useDownloadModel()
|
const { downloadModel } = useDownloadModel()
|
||||||
const downloadedModels = useAtomValue(downloadedModelsAtom)
|
const downloadedModels = useAtomValue(downloadedModelsAtom)
|
||||||
const { modelDownloadStateAtom } = useDownloadState()
|
|
||||||
const { requestCreateNewThread } = useCreateNewThread()
|
const { requestCreateNewThread } = useCreateNewThread()
|
||||||
const totalRam = useAtomValue(totalRamAtom)
|
const totalRam = useAtomValue(totalRamAtom)
|
||||||
const nvidiaTotalVram = useAtomValue(nvidiaTotalVramAtom)
|
const nvidiaTotalVram = useAtomValue(nvidiaTotalVramAtom)
|
||||||
|
|||||||
@ -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
|
|
||||||
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -3,6 +3,7 @@ import {
|
|||||||
AppRoute,
|
AppRoute,
|
||||||
DownloadRoute,
|
DownloadRoute,
|
||||||
ExtensionRoute,
|
ExtensionRoute,
|
||||||
|
FileManagerRoute,
|
||||||
FileSystemRoute,
|
FileSystemRoute,
|
||||||
} from '@janhq/core'
|
} from '@janhq/core'
|
||||||
|
|
||||||
@ -22,6 +23,7 @@ export const APIRoutes = [
|
|||||||
route: r,
|
route: r,
|
||||||
})),
|
})),
|
||||||
...Object.values(FileSystemRoute).map((r) => ({ path: `fs`, 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
|
// Define the restAPI object with methods for each API route
|
||||||
@ -50,4 +52,6 @@ export const restAPI = {
|
|||||||
}
|
}
|
||||||
}, {}),
|
}, {}),
|
||||||
openExternalUrl,
|
openExternalUrl,
|
||||||
|
// Jan Server URL
|
||||||
|
baseApiUrl: API_BASE_URL,
|
||||||
}
|
}
|
||||||
|
|||||||
3
web/types/downloadState.d.ts
vendored
3
web/types/downloadState.d.ts
vendored
@ -1,12 +1,13 @@
|
|||||||
type DownloadState = {
|
type DownloadState = {
|
||||||
modelId: string
|
modelId: string
|
||||||
|
filename: string
|
||||||
time: DownloadTime
|
time: DownloadTime
|
||||||
speed: number
|
speed: number
|
||||||
percent: number
|
percent: number
|
||||||
size: DownloadSize
|
size: DownloadSize
|
||||||
isFinished?: boolean
|
|
||||||
children?: DownloadState[]
|
children?: DownloadState[]
|
||||||
error?: string
|
error?: string
|
||||||
|
downloadState: 'downloading' | 'error' | 'end'
|
||||||
}
|
}
|
||||||
|
|
||||||
type DownloadTime = {
|
type DownloadTime = {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user