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
|
||||
FROM base AS deps
|
||||
FROM base AS builder
|
||||
|
||||
# Install g++ 11
|
||||
RUN apt update && apt install -y gcc-11 g++-11 cpp-11 jq xsel && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies based on the preferred package manager
|
||||
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
|
||||
RUN yarn install
|
||||
COPY . ./
|
||||
|
||||
RUN export NITRO_VERSION=$(cat extensions/inference-nitro-extension/bin/version.txt) && \
|
||||
jq --arg nitroVersion $NITRO_VERSION '(.scripts."downloadnitro:linux" | gsub("\\${NITRO_VERSION}"; $nitroVersion)) | gsub("\r"; "")' extensions/inference-nitro-extension/package.json > /tmp/newcommand.txt && export NEW_COMMAND=$(sed 's/^"//;s/"$//' /tmp/newcommand.txt) && jq --arg newCommand "$NEW_COMMAND" '.scripts."downloadnitro:linux" = $newCommand' extensions/inference-nitro-extension/package.json > /tmp/package.json && mv /tmp/package.json extensions/inference-nitro-extension/package.json
|
||||
RUN make install-and-build
|
||||
RUN yarn workspace jan-web install
|
||||
|
||||
RUN export NODE_ENV=production && yarn workspace jan-web build
|
||||
|
||||
# # 2. Rebuild the source code only when needed
|
||||
FROM base AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
# This will do the trick, use the corresponding env file for each environment.
|
||||
RUN yarn workspace server install
|
||||
RUN yarn server:prod
|
||||
|
||||
# 3. Production image, copy all the files and run next
|
||||
FROM base AS runner
|
||||
|
||||
# Install g++ 11
|
||||
RUN apt update && apt install -y gcc-11 g++-11 cpp-11 jq xsel && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
# Copy the package.json and yarn.lock of root yarn space to leverage Docker cache
|
||||
COPY --from=builder /app/package.json ./package.json
|
||||
COPY --from=builder /app/node_modules ./node_modules/
|
||||
COPY --from=builder /app/yarn.lock ./yarn.lock
|
||||
|
||||
# RUN addgroup -g 1001 -S nodejs;
|
||||
COPY --from=builder /app/server/build ./
|
||||
# Copy the package.json, yarn.lock, and build output of server yarn space to leverage Docker cache
|
||||
COPY --from=builder /app/server ./server/
|
||||
COPY --from=builder /app/docs/openapi ./docs/openapi/
|
||||
|
||||
# Automatically leverage output traces to reduce image size
|
||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||
COPY --from=builder /app/server/node_modules ./node_modules
|
||||
COPY --from=builder /app/server/package.json ./package.json
|
||||
# Copy pre-install dependencies
|
||||
COPY --from=builder /app/pre-install ./pre-install/
|
||||
|
||||
EXPOSE 4000 3928
|
||||
# Copy the package.json, yarn.lock, and output of web yarn space to leverage Docker cache
|
||||
COPY --from=builder /app/web/out ./web/out/
|
||||
COPY --from=builder /app/web/.next ./web/.next/
|
||||
COPY --from=builder /app/web/package.json ./web/package.json
|
||||
COPY --from=builder /app/web/yarn.lock ./web/yarn.lock
|
||||
COPY --from=builder /app/models ./models/
|
||||
|
||||
ENV PORT 4000
|
||||
ENV APPDATA /app/data
|
||||
RUN npm install -g serve@latest
|
||||
|
||||
CMD ["node", "main.js"]
|
||||
EXPOSE 1337 3000 3928
|
||||
|
||||
ENV JAN_API_HOST 0.0.0.0
|
||||
ENV JAN_API_PORT 1337
|
||||
|
||||
CMD ["sh", "-c", "cd server && node build/main.js & cd web && npx serve out"]
|
||||
|
||||
# docker build -t jan .
|
||||
# docker run -p 1337:1337 -p 3000:3000 -p 3928:3928 jan
|
||||
|
||||
65
Dockerfile.gpu
Normal file
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
|
||||
ifeq ($(OS),Windows_NT)
|
||||
powershell -Command "if ((Get-ChildItem -Path electron/pre-install -Filter *.tgz | Measure-Object | Select-Object -ExpandProperty Count) -ne (Get-ChildItem -Path extensions -Directory | Measure-Object | Select-Object -ExpandProperty Count)) { Write-Host 'Number of .tgz files in electron/pre-install does not match the number of subdirectories in extension'; exit 1 } else { Write-Host 'Extension build successful' }"
|
||||
powershell -Command "if ((Get-ChildItem -Path pre-install -Filter *.tgz | Measure-Object | Select-Object -ExpandProperty Count) -ne (Get-ChildItem -Path extensions -Directory | Measure-Object | Select-Object -ExpandProperty Count)) { Write-Host 'Number of .tgz files in pre-install does not match the number of subdirectories in extension'; exit 1 } else { Write-Host 'Extension build successful' }"
|
||||
else
|
||||
@tgz_count=$$(find electron/pre-install -type f -name "*.tgz" | wc -l); dir_count=$$(find extensions -mindepth 1 -maxdepth 1 -type d | wc -l); if [ $$tgz_count -ne $$dir_count ]; then echo "Number of .tgz files in electron/pre-install ($$tgz_count) does not match the number of subdirectories in extension ($$dir_count)"; exit 1; else echo "Extension build successful"; fi
|
||||
@tgz_count=$$(find pre-install -type f -name "*.tgz" | wc -l); dir_count=$$(find extensions -mindepth 1 -maxdepth 1 -type d | wc -l); if [ $$tgz_count -ne $$dir_count ]; then echo "Number of .tgz files in pre-install ($$tgz_count) does not match the number of subdirectories in extension ($$dir_count)"; exit 1; else echo "Extension build successful"; fi
|
||||
endif
|
||||
|
||||
dev: check-file-counts
|
||||
|
||||
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.
|
||||
|
||||
### Docker mode
|
||||
|
||||
- Supported OS: Linux, WSL2 Docker
|
||||
- Pre-requisites:
|
||||
- `docker` and `docker compose`, follow instruction [here](https://docs.docker.com/engine/install/ubuntu/)
|
||||
|
||||
```bash
|
||||
curl -fsSL https://get.docker.com -o get-docker.sh
|
||||
sudo sh ./get-docker.sh --dry-run
|
||||
```
|
||||
|
||||
- `nvidia docker`, follow instruction [here](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) (If you want to run with GPU mode)
|
||||
|
||||
- Run Jan in Docker mode
|
||||
|
||||
```bash
|
||||
# CPU mode
|
||||
docker compose --profile cpu up
|
||||
|
||||
# GPU mode
|
||||
docker compose --profile gpu up
|
||||
```
|
||||
|
||||
This will start the web server and you can access Jan at `http://localhost:3000`.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
Jan builds on top of other open-source projects:
|
||||
|
||||
@ -57,6 +57,7 @@
|
||||
"rollup-plugin-typescript2": "^0.36.0",
|
||||
"ts-jest": "^26.1.1",
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.2.2"
|
||||
"typescript": "^5.2.2",
|
||||
"rimraf": "^3.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,6 +30,7 @@ export enum DownloadRoute {
|
||||
downloadFile = 'downloadFile',
|
||||
pauseDownload = 'pauseDownload',
|
||||
resumeDownload = 'resumeDownload',
|
||||
getDownloadProgress = 'getDownloadProgress',
|
||||
}
|
||||
|
||||
export enum DownloadEvent {
|
||||
|
||||
@ -5,8 +5,27 @@ import { HttpServer } from '../HttpServer'
|
||||
import { createWriteStream } from 'fs'
|
||||
import { getJanDataFolderPath } from '../../utils'
|
||||
import { normalizeFilePath } from '../../path'
|
||||
import { DownloadState } from '../../../types'
|
||||
|
||||
export const downloadRouter = async (app: HttpServer) => {
|
||||
app.get(`/${DownloadRoute.getDownloadProgress}/:modelId`, async (req, res) => {
|
||||
const modelId = req.params.modelId
|
||||
|
||||
console.debug(`Getting download progress for model ${modelId}`)
|
||||
console.debug(
|
||||
`All Download progress: ${JSON.stringify(DownloadManager.instance.downloadProgressMap)}`
|
||||
)
|
||||
|
||||
// check if null DownloadManager.instance.downloadProgressMap
|
||||
if (!DownloadManager.instance.downloadProgressMap[modelId]) {
|
||||
return res.status(404).send({
|
||||
message: 'Download progress not found',
|
||||
})
|
||||
} else {
|
||||
return res.status(200).send(DownloadManager.instance.downloadProgressMap[modelId])
|
||||
}
|
||||
})
|
||||
|
||||
app.post(`/${DownloadRoute.downloadFile}`, async (req, res) => {
|
||||
const strictSSL = !(req.query.ignoreSSL === 'true')
|
||||
const proxy = req.query.proxy?.startsWith('http') ? req.query.proxy : undefined
|
||||
@ -19,7 +38,10 @@ export const downloadRouter = async (app: HttpServer) => {
|
||||
})
|
||||
|
||||
const localPath = normalizedArgs[1]
|
||||
const fileName = localPath.split('/').pop() ?? ''
|
||||
const array = localPath.split('/')
|
||||
const fileName = array.pop() ?? ''
|
||||
const modelId = array.pop() ?? ''
|
||||
console.debug('downloadFile', normalizedArgs, fileName, modelId)
|
||||
|
||||
const request = require('request')
|
||||
const progress = require('request-progress')
|
||||
@ -27,17 +49,44 @@ export const downloadRouter = async (app: HttpServer) => {
|
||||
const rq = request({ url: normalizedArgs[0], strictSSL, proxy })
|
||||
progress(rq, {})
|
||||
.on('progress', function (state: any) {
|
||||
console.log('download onProgress', state)
|
||||
const downloadProps: DownloadState = {
|
||||
...state,
|
||||
modelId,
|
||||
fileName,
|
||||
downloadState: 'downloading',
|
||||
}
|
||||
console.debug(`Download ${modelId} onProgress`, downloadProps)
|
||||
DownloadManager.instance.downloadProgressMap[modelId] = downloadProps
|
||||
})
|
||||
.on('error', function (err: Error) {
|
||||
console.log('download onError', err)
|
||||
console.debug(`Download ${modelId} onError`, err.message)
|
||||
|
||||
const currentDownloadState = DownloadManager.instance.downloadProgressMap[modelId]
|
||||
if (currentDownloadState) {
|
||||
DownloadManager.instance.downloadProgressMap[modelId] = {
|
||||
...currentDownloadState,
|
||||
downloadState: 'error',
|
||||
}
|
||||
}
|
||||
})
|
||||
.on('end', function () {
|
||||
console.log('download onEnd')
|
||||
console.debug(`Download ${modelId} onEnd`)
|
||||
|
||||
const currentDownloadState = DownloadManager.instance.downloadProgressMap[modelId]
|
||||
if (currentDownloadState) {
|
||||
if (currentDownloadState.downloadState === 'downloading') {
|
||||
// if the previous state is downloading, then set the state to end (success)
|
||||
DownloadManager.instance.downloadProgressMap[modelId] = {
|
||||
...currentDownloadState,
|
||||
downloadState: 'end',
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.pipe(createWriteStream(normalizedArgs[1]))
|
||||
|
||||
DownloadManager.instance.setRequest(fileName, rq)
|
||||
DownloadManager.instance.setRequest(localPath, rq)
|
||||
res.status(200).send({ message: 'Download started' })
|
||||
})
|
||||
|
||||
app.post(`/${DownloadRoute.abortDownload}`, async (req, res) => {
|
||||
@ -54,5 +103,10 @@ export const downloadRouter = async (app: HttpServer) => {
|
||||
const rq = DownloadManager.instance.networkRequests[fileName]
|
||||
DownloadManager.instance.networkRequests[fileName] = undefined
|
||||
rq?.abort()
|
||||
if (rq) {
|
||||
res.status(200).send({ message: 'Download aborted' })
|
||||
} else {
|
||||
res.status(404).send({ message: 'Download not found' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -1,14 +1,29 @@
|
||||
import { FileManagerRoute } from '../../../api'
|
||||
import { HttpServer } from '../../index'
|
||||
import { join } from 'path'
|
||||
|
||||
export const fsRouter = async (app: HttpServer) => {
|
||||
app.post(`/app/${FileManagerRoute.syncFile}`, async (request: any, reply: any) => {})
|
||||
export const fileManagerRouter = async (app: HttpServer) => {
|
||||
app.post(`/fs/${FileManagerRoute.syncFile}`, async (request: any, reply: any) => {
|
||||
const reflect = require('@alumna/reflect')
|
||||
const args = JSON.parse(request.body)
|
||||
return reflect({
|
||||
src: args[0],
|
||||
dest: args[1],
|
||||
recursive: true,
|
||||
delete: false,
|
||||
overwrite: true,
|
||||
errorOnExist: false,
|
||||
})
|
||||
})
|
||||
|
||||
app.post(`/app/${FileManagerRoute.getJanDataFolderPath}`, async (request: any, reply: any) => {})
|
||||
app.post(`/fs/${FileManagerRoute.getJanDataFolderPath}`, async (request: any, reply: any) =>
|
||||
global.core.appPath()
|
||||
)
|
||||
|
||||
app.post(`/app/${FileManagerRoute.getResourcePath}`, async (request: any, reply: any) => {})
|
||||
app.post(`/fs/${FileManagerRoute.getResourcePath}`, async (request: any, reply: any) =>
|
||||
join(global.core.appPath(), '../../..')
|
||||
)
|
||||
|
||||
app.post(`/app/${FileManagerRoute.getUserHomePath}`, async (request: any, reply: any) => {})
|
||||
|
||||
app.post(`/app/${FileManagerRoute.fileStat}`, async (request: any, reply: any) => {})
|
||||
app.post(`/fs/${FileManagerRoute.fileStat}`, async (request: any, reply: any) => {})
|
||||
}
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { FileSystemRoute } from '../../../api'
|
||||
import { FileManagerRoute, FileSystemRoute } from '../../../api'
|
||||
import { join } from 'path'
|
||||
import { HttpServer } from '../HttpServer'
|
||||
import { getJanDataFolderPath } from '../../utils'
|
||||
import { normalizeFilePath } from '../../path'
|
||||
import { writeFileSync } from 'fs'
|
||||
|
||||
export const fsRouter = async (app: HttpServer) => {
|
||||
const moduleName = 'fs'
|
||||
@ -26,4 +27,14 @@ export const fsRouter = async (app: HttpServer) => {
|
||||
}
|
||||
})
|
||||
})
|
||||
app.post(`/${FileManagerRoute.writeBlob}`, async (request: any, reply: any) => {
|
||||
try {
|
||||
const args = JSON.parse(request.body) as any[]
|
||||
console.log('writeBlob:', args[0])
|
||||
const dataBuffer = Buffer.from(args[1], 'base64')
|
||||
writeFileSync(args[0], dataBuffer)
|
||||
} catch (err) {
|
||||
console.error(`writeFile ${request.body} result: ${err}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import { threadRouter } from './thread'
|
||||
import { fsRouter } from './fs'
|
||||
import { extensionRouter } from './extension'
|
||||
import { downloadRouter } from './download'
|
||||
import { fileManagerRouter } from './fileManager'
|
||||
|
||||
export const v1Router = async (app: HttpServer) => {
|
||||
// MARK: External Routes
|
||||
@ -16,6 +17,8 @@ export const v1Router = async (app: HttpServer) => {
|
||||
app.register(fsRouter, {
|
||||
prefix: '/fs',
|
||||
})
|
||||
app.register(fileManagerRouter)
|
||||
|
||||
app.register(extensionRouter, {
|
||||
prefix: '/extension',
|
||||
})
|
||||
|
||||
@ -1,15 +1,18 @@
|
||||
import { DownloadState } from '../types'
|
||||
|
||||
/**
|
||||
* Manages file downloads and network requests.
|
||||
*/
|
||||
export class DownloadManager {
|
||||
public networkRequests: Record<string, any> = {};
|
||||
public networkRequests: Record<string, any> = {}
|
||||
|
||||
public static instance: DownloadManager = new DownloadManager();
|
||||
public static instance: DownloadManager = new DownloadManager()
|
||||
|
||||
public downloadProgressMap: Record<string, DownloadState> = {}
|
||||
|
||||
constructor() {
|
||||
if (DownloadManager.instance) {
|
||||
return DownloadManager.instance;
|
||||
return DownloadManager.instance
|
||||
}
|
||||
}
|
||||
/**
|
||||
@ -18,6 +21,6 @@ export class DownloadManager {
|
||||
* @param {Request | undefined} request - The network request to set, or undefined to clear the request.
|
||||
*/
|
||||
setRequest(fileName: string, request: any | undefined) {
|
||||
this.networkRequests[fileName] = request;
|
||||
this.networkRequests[fileName] = request
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,8 +41,8 @@ async function registerExtensionProtocol() {
|
||||
console.error('Electron is not available')
|
||||
}
|
||||
const extensionPath = ExtensionManager.instance.getExtensionsPath()
|
||||
if (electron) {
|
||||
return electron.protocol.registerFileProtocol('extension', (request: any, callback: any) => {
|
||||
if (electron && electron.protocol) {
|
||||
return electron.protocol?.registerFileProtocol('extension', (request: any, callback: any) => {
|
||||
const entry = request.url.substr('extension://'.length - 1)
|
||||
|
||||
const url = normalize(extensionPath + entry)
|
||||
@ -69,7 +69,7 @@ export function useExtensions(extensionsPath: string) {
|
||||
|
||||
// Read extension list from extensions folder
|
||||
const extensions = JSON.parse(
|
||||
readFileSync(ExtensionManager.instance.getExtensionsFile(), 'utf-8'),
|
||||
readFileSync(ExtensionManager.instance.getExtensionsFile(), 'utf-8')
|
||||
)
|
||||
try {
|
||||
// Create and store a Extension instance for each extension in list
|
||||
@ -82,7 +82,7 @@ export function useExtensions(extensionsPath: string) {
|
||||
throw new Error(
|
||||
'Could not successfully rebuild list of installed extensions.\n' +
|
||||
error +
|
||||
'\nPlease check the extensions.json file in the extensions folder.',
|
||||
'\nPlease check the extensions.json file in the extensions folder.'
|
||||
)
|
||||
}
|
||||
|
||||
@ -122,7 +122,7 @@ function loadExtension(ext: any) {
|
||||
export function getStore() {
|
||||
if (!ExtensionManager.instance.getExtensionsFile()) {
|
||||
throw new Error(
|
||||
'The extension path has not yet been set up. Please run useExtensions before accessing the store',
|
||||
'The extension path has not yet been set up. Please run useExtensions before accessing the store'
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
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 './assistantEvent'
|
||||
export * from './assistantInterface'
|
||||
|
||||
@ -2,3 +2,26 @@ export type FileStat = {
|
||||
isDirectory: boolean
|
||||
size: number
|
||||
}
|
||||
|
||||
export type DownloadState = {
|
||||
modelId: string
|
||||
filename: string
|
||||
time: DownloadTime
|
||||
speed: number
|
||||
percent: number
|
||||
|
||||
size: DownloadSize
|
||||
children?: DownloadState[]
|
||||
error?: string
|
||||
downloadState: 'downloading' | 'error' | 'end'
|
||||
}
|
||||
|
||||
type DownloadTime = {
|
||||
elapsed: number
|
||||
remaining: number
|
||||
}
|
||||
|
||||
type DownloadSize = {
|
||||
total: number
|
||||
transferred: number
|
||||
}
|
||||
|
||||
@ -12,4 +12,6 @@ export enum ModelEvent {
|
||||
OnModelStop = 'OnModelStop',
|
||||
/** The `OnModelStopped` event is emitted when a model stopped ok. */
|
||||
OnModelStopped = 'OnModelStopped',
|
||||
/** The `OnModelUpdate` event is emitted when the model list is updated. */
|
||||
OnModelsUpdate = 'OnModelsUpdate',
|
||||
}
|
||||
|
||||
110
docker-compose.yml
Normal file
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 { DownloadEvent, DownloadRoute } from '@janhq/core'
|
||||
const progress = require('request-progress')
|
||||
import { DownloadManager, getJanDataFolderPath, normalizeFilePath } from '@janhq/core/node'
|
||||
import {
|
||||
DownloadManager,
|
||||
getJanDataFolderPath,
|
||||
normalizeFilePath,
|
||||
} from '@janhq/core/node'
|
||||
|
||||
export function handleDownloaderIPCs() {
|
||||
/**
|
||||
@ -56,20 +60,23 @@ export function handleDownloaderIPCs() {
|
||||
*/
|
||||
ipcMain.handle(
|
||||
DownloadRoute.downloadFile,
|
||||
async (_event, url, fileName, network) => {
|
||||
async (_event, url, localPath, network) => {
|
||||
const strictSSL = !network?.ignoreSSL
|
||||
const proxy = network?.proxy?.startsWith('http')
|
||||
? network.proxy
|
||||
: undefined
|
||||
|
||||
if (typeof fileName === 'string') {
|
||||
fileName = normalizeFilePath(fileName)
|
||||
if (typeof localPath === 'string') {
|
||||
localPath = normalizeFilePath(localPath)
|
||||
}
|
||||
const destination = resolve(getJanDataFolderPath(), fileName)
|
||||
const array = localPath.split('/')
|
||||
const fileName = array.pop() ?? ''
|
||||
const modelId = array.pop() ?? ''
|
||||
|
||||
const destination = resolve(getJanDataFolderPath(), localPath)
|
||||
const rq = request({ url, strictSSL, proxy })
|
||||
|
||||
// Put request to download manager instance
|
||||
DownloadManager.instance.setRequest(fileName, rq)
|
||||
DownloadManager.instance.setRequest(localPath, rq)
|
||||
|
||||
// Downloading file to a temp file first
|
||||
const downloadingTempFile = `${destination}.download`
|
||||
@ -81,6 +88,7 @@ export function handleDownloaderIPCs() {
|
||||
{
|
||||
...state,
|
||||
fileName,
|
||||
modelId,
|
||||
}
|
||||
)
|
||||
})
|
||||
@ -90,11 +98,12 @@ export function handleDownloaderIPCs() {
|
||||
{
|
||||
fileName,
|
||||
err,
|
||||
modelId,
|
||||
}
|
||||
)
|
||||
})
|
||||
.on('end', function () {
|
||||
if (DownloadManager.instance.networkRequests[fileName]) {
|
||||
if (DownloadManager.instance.networkRequests[localPath]) {
|
||||
// Finished downloading, rename temp file to actual file
|
||||
renameSync(downloadingTempFile, destination)
|
||||
|
||||
@ -102,14 +111,16 @@ export function handleDownloaderIPCs() {
|
||||
DownloadEvent.onFileDownloadSuccess,
|
||||
{
|
||||
fileName,
|
||||
modelId,
|
||||
}
|
||||
)
|
||||
DownloadManager.instance.setRequest(fileName, undefined)
|
||||
DownloadManager.instance.setRequest(localPath, undefined)
|
||||
} else {
|
||||
WindowManager?.instance.currentWindow?.webContents.send(
|
||||
DownloadEvent.onFileDownloadError,
|
||||
{
|
||||
fileName,
|
||||
modelId,
|
||||
err: { message: 'aborted' },
|
||||
}
|
||||
)
|
||||
|
||||
@ -38,6 +38,7 @@ export function handleFileMangerIPCs() {
|
||||
getResourcePath()
|
||||
)
|
||||
|
||||
// Handles the 'getUserHomePath' IPC event. This event is triggered to get the user home path.
|
||||
ipcMain.handle(FileManagerRoute.getUserHomePath, async (_event) =>
|
||||
app.getPath('home')
|
||||
)
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import { ipcMain } from 'electron'
|
||||
|
||||
import { getJanDataFolderPath, normalizeFilePath } from '@janhq/core/node'
|
||||
import fs from 'fs'
|
||||
import { FileManagerRoute, FileSystemRoute } from '@janhq/core'
|
||||
import { FileSystemRoute } from '@janhq/core'
|
||||
import { join } from 'path'
|
||||
/**
|
||||
* Handles file system operations.
|
||||
|
||||
@ -57,17 +57,18 @@
|
||||
"scripts": {
|
||||
"lint": "eslint . --ext \".js,.jsx,.ts,.tsx\"",
|
||||
"test:e2e": "playwright test --workers=1",
|
||||
"dev": "tsc -p . && electron .",
|
||||
"build": "run-script-os",
|
||||
"build:test": "run-script-os",
|
||||
"copy:assets": "rimraf --glob \"./pre-install/*.tgz\" && cpx \"../pre-install/*.tgz\" \"./pre-install\"",
|
||||
"dev": "yarn copy:assets && tsc -p . && electron .",
|
||||
"build": "yarn copy:assets && run-script-os",
|
||||
"build:test": "yarn copy:assets && run-script-os",
|
||||
"build:test:darwin": "tsc -p . && electron-builder -p never -m --dir",
|
||||
"build:test:win32": "tsc -p . && electron-builder -p never -w --dir",
|
||||
"build:test:linux": "tsc -p . && electron-builder -p never -l --dir",
|
||||
"build:darwin": "tsc -p . && electron-builder -p never -m",
|
||||
"build:darwin": "tsc -p . && electron-builder -p never -m --x64 --arm64",
|
||||
"build:win32": "tsc -p . && electron-builder -p never -w",
|
||||
"build:linux": "tsc -p . && electron-builder -p never -l deb -l AppImage",
|
||||
"build:publish": "run-script-os",
|
||||
"build:publish:darwin": "tsc -p . && electron-builder -p always -m",
|
||||
"build:publish": "yarn copy:assets && run-script-os",
|
||||
"build:publish:darwin": "tsc -p . && electron-builder -p always -m --x64 --arm64",
|
||||
"build:publish:win32": "tsc -p . && electron-builder -p always -w",
|
||||
"build:publish:linux": "tsc -p . && electron-builder -p always -l deb -l AppImage"
|
||||
},
|
||||
|
||||
@ -8,9 +8,9 @@
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
"build": "tsc --module commonjs && rollup -c rollup.config.ts",
|
||||
"build:publish:linux": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/pre-install",
|
||||
"build:publish:darwin": "rimraf *.tgz --glob && npm run build && ../../.github/scripts/auto-sign.sh && npm pack && cpx *.tgz ../../electron/pre-install",
|
||||
"build:publish:win32": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/pre-install",
|
||||
"build:publish:linux": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../pre-install",
|
||||
"build:publish:darwin": "rimraf *.tgz --glob && npm run build && ../../.github/scripts/auto-sign.sh && npm pack && cpx *.tgz ../../pre-install",
|
||||
"build:publish:win32": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../pre-install",
|
||||
"build:publish": "run-script-os"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
joinPath,
|
||||
executeOnMain,
|
||||
AssistantExtension,
|
||||
AssistantEvent,
|
||||
} from "@janhq/core";
|
||||
|
||||
export default class JanAssistantExtension extends AssistantExtension {
|
||||
@ -21,7 +22,7 @@ export default class JanAssistantExtension extends AssistantExtension {
|
||||
async onLoad() {
|
||||
// making the assistant directory
|
||||
const assistantDirExist = await fs.existsSync(
|
||||
JanAssistantExtension._homeDir,
|
||||
JanAssistantExtension._homeDir
|
||||
);
|
||||
if (
|
||||
localStorage.getItem(`${EXTENSION_NAME}-version`) !== VERSION ||
|
||||
@ -31,14 +32,16 @@ export default class JanAssistantExtension extends AssistantExtension {
|
||||
await fs.mkdirSync(JanAssistantExtension._homeDir);
|
||||
|
||||
// Write assistant metadata
|
||||
this.createJanAssistant();
|
||||
await this.createJanAssistant();
|
||||
// Finished migration
|
||||
localStorage.setItem(`${EXTENSION_NAME}-version`, VERSION);
|
||||
// Update the assistant list
|
||||
events.emit(AssistantEvent.OnAssistantsUpdate, {});
|
||||
}
|
||||
|
||||
// Events subscription
|
||||
events.on(MessageEvent.OnMessageSent, (data: MessageRequest) =>
|
||||
JanAssistantExtension.handleMessageRequest(data, this),
|
||||
JanAssistantExtension.handleMessageRequest(data, this)
|
||||
);
|
||||
|
||||
events.on(InferenceEvent.OnInferenceStopped, () => {
|
||||
@ -53,7 +56,7 @@ export default class JanAssistantExtension extends AssistantExtension {
|
||||
|
||||
private static async handleMessageRequest(
|
||||
data: MessageRequest,
|
||||
instance: JanAssistantExtension,
|
||||
instance: JanAssistantExtension
|
||||
) {
|
||||
instance.isCancelled = false;
|
||||
instance.controller = new AbortController();
|
||||
@ -80,7 +83,7 @@ export default class JanAssistantExtension extends AssistantExtension {
|
||||
NODE,
|
||||
"toolRetrievalIngestNewDocument",
|
||||
docFile,
|
||||
data.model?.proxyEngine,
|
||||
data.model?.proxyEngine
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -96,7 +99,7 @@ export default class JanAssistantExtension extends AssistantExtension {
|
||||
NODE,
|
||||
"toolRetrievalUpdateTextSplitter",
|
||||
data.thread.assistants[0].tools[0]?.settings?.chunk_size ?? 4000,
|
||||
data.thread.assistants[0].tools[0]?.settings?.chunk_overlap ?? 200,
|
||||
data.thread.assistants[0].tools[0]?.settings?.chunk_overlap ?? 200
|
||||
);
|
||||
}
|
||||
|
||||
@ -110,7 +113,7 @@ export default class JanAssistantExtension extends AssistantExtension {
|
||||
const retrievalResult = await executeOnMain(
|
||||
NODE,
|
||||
"toolRetrievalQueryResult",
|
||||
prompt,
|
||||
prompt
|
||||
);
|
||||
|
||||
// Update the message content
|
||||
@ -168,7 +171,7 @@ export default class JanAssistantExtension extends AssistantExtension {
|
||||
try {
|
||||
await fs.writeFileSync(
|
||||
assistantMetadataPath,
|
||||
JSON.stringify(assistant, null, 2),
|
||||
JSON.stringify(assistant, null, 2)
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@ -180,7 +183,7 @@ export default class JanAssistantExtension extends AssistantExtension {
|
||||
// get all the assistant metadata json
|
||||
const results: Assistant[] = [];
|
||||
const allFileName: string[] = await fs.readdirSync(
|
||||
JanAssistantExtension._homeDir,
|
||||
JanAssistantExtension._homeDir
|
||||
);
|
||||
for (const fileName of allFileName) {
|
||||
const filePath = await joinPath([
|
||||
@ -190,7 +193,7 @@ export default class JanAssistantExtension extends AssistantExtension {
|
||||
|
||||
if (filePath.includes(".DS_Store")) continue;
|
||||
const jsonFiles: string[] = (await fs.readdirSync(filePath)).filter(
|
||||
(file: string) => file === "assistant.json",
|
||||
(file: string) => file === "assistant.json"
|
||||
);
|
||||
|
||||
if (jsonFiles.length !== 1) {
|
||||
@ -200,7 +203,7 @@ export default class JanAssistantExtension extends AssistantExtension {
|
||||
|
||||
const content = await fs.readFileSync(
|
||||
await joinPath([filePath, jsonFiles[0]]),
|
||||
"utf-8",
|
||||
"utf-8"
|
||||
);
|
||||
const assistant: Assistant =
|
||||
typeof content === "object" ? content : JSON.parse(content);
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "tsc -b . && webpack --config webpack.config.js",
|
||||
"build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/pre-install"
|
||||
"build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../pre-install"
|
||||
},
|
||||
"exports": {
|
||||
".": "./dist/index.js",
|
||||
|
||||
@ -12,9 +12,9 @@
|
||||
"downloadnitro:darwin": "NITRO_VERSION=$(cat ./bin/version.txt) && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-mac-arm64.tar.gz -e --strip 1 -o ./bin/mac-arm64 && chmod +x ./bin/mac-arm64/nitro && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-mac-amd64.tar.gz -e --strip 1 -o ./bin/mac-x64 && chmod +x ./bin/mac-x64/nitro",
|
||||
"downloadnitro:win32": "download.bat",
|
||||
"downloadnitro": "run-script-os",
|
||||
"build:publish:darwin": "rimraf *.tgz --glob && npm run build && npm run downloadnitro && ../../.github/scripts/auto-sign.sh && cpx \"bin/**\" \"dist/bin\" && npm pack && cpx *.tgz ../../electron/pre-install",
|
||||
"build:publish:win32": "rimraf *.tgz --glob && npm run build && npm run downloadnitro && cpx \"bin/**\" \"dist/bin\" && npm pack && cpx *.tgz ../../electron/pre-install",
|
||||
"build:publish:linux": "rimraf *.tgz --glob && npm run build && npm run downloadnitro && cpx \"bin/**\" \"dist/bin\" && npm pack && cpx *.tgz ../../electron/pre-install",
|
||||
"build:publish:darwin": "rimraf *.tgz --glob && npm run build && npm run downloadnitro && ../../.github/scripts/auto-sign.sh && cpx \"bin/**\" \"dist/bin\" && npm pack && cpx *.tgz ../../pre-install",
|
||||
"build:publish:win32": "rimraf *.tgz --glob && npm run build && npm run downloadnitro && cpx \"bin/**\" \"dist/bin\" && npm pack && cpx *.tgz ../../pre-install",
|
||||
"build:publish:linux": "rimraf *.tgz --glob && npm run build && npm run downloadnitro && cpx \"bin/**\" \"dist/bin\" && npm pack && cpx *.tgz ../../pre-install",
|
||||
"build:publish": "run-script-os"
|
||||
},
|
||||
"exports": {
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
"build": "tsc -b . && webpack --config webpack.config.js",
|
||||
"build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/pre-install"
|
||||
"build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../pre-install"
|
||||
},
|
||||
"exports": {
|
||||
".": "./dist/index.js",
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
"build": "tsc -b . && webpack --config webpack.config.js",
|
||||
"build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/pre-install"
|
||||
"build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../pre-install"
|
||||
},
|
||||
"exports": {
|
||||
".": "./dist/index.js",
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
"build": "tsc -b . && webpack --config webpack.config.js",
|
||||
"build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/pre-install"
|
||||
"build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../pre-install"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cpx": "^1.5.0",
|
||||
|
||||
@ -1,3 +1,15 @@
|
||||
declare const EXTENSION_NAME: string
|
||||
declare const MODULE_PATH: string
|
||||
declare const VERSION: stringå
|
||||
export {}
|
||||
declare global {
|
||||
declare const EXTENSION_NAME: string
|
||||
declare const MODULE_PATH: string
|
||||
declare const VERSION: string
|
||||
|
||||
interface Core {
|
||||
api: APIFunctions
|
||||
events: EventEmitter
|
||||
}
|
||||
interface Window {
|
||||
core?: Core | undefined
|
||||
electronAPI?: any | undefined
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
Model,
|
||||
getJanDataFolderPath,
|
||||
events,
|
||||
DownloadEvent,
|
||||
DownloadRoute,
|
||||
ModelEvent,
|
||||
} from '@janhq/core'
|
||||
import { DownloadState } from '@janhq/core/.'
|
||||
import { extractFileName } from './helpers/path'
|
||||
|
||||
/**
|
||||
* A extension for models
|
||||
@ -29,6 +35,8 @@ export default class JanModelExtension extends ModelExtension {
|
||||
*/
|
||||
async onLoad() {
|
||||
this.copyModelsToHomeDir()
|
||||
// Handle Desktop Events
|
||||
this.handleDesktopEvents()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -61,6 +69,8 @@ export default class JanModelExtension extends ModelExtension {
|
||||
|
||||
// Finished migration
|
||||
localStorage.setItem(`${EXTENSION_NAME}-version`, VERSION)
|
||||
|
||||
events.emit(ModelEvent.OnModelsUpdate, {})
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
@ -83,31 +93,66 @@ export default class JanModelExtension extends ModelExtension {
|
||||
if (model.sources.length > 1) {
|
||||
// path to model binaries
|
||||
for (const source of model.sources) {
|
||||
let path = this.extractFileName(source.url)
|
||||
let path = extractFileName(
|
||||
source.url,
|
||||
JanModelExtension._supportedModelFormat
|
||||
)
|
||||
if (source.filename) {
|
||||
path = await joinPath([modelDirPath, source.filename])
|
||||
}
|
||||
|
||||
downloadFile(source.url, path, network)
|
||||
}
|
||||
// TODO: handle multiple binaries for web later
|
||||
} else {
|
||||
const fileName = this.extractFileName(model.sources[0]?.url)
|
||||
const fileName = extractFileName(
|
||||
model.sources[0]?.url,
|
||||
JanModelExtension._supportedModelFormat
|
||||
)
|
||||
const path = await joinPath([modelDirPath, fileName])
|
||||
downloadFile(model.sources[0]?.url, path, network)
|
||||
|
||||
if (window && window.core?.api && window.core.api.baseApiUrl) {
|
||||
this.startPollingDownloadProgress(model.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* try to retrieve the download file name from the source url
|
||||
* Specifically for Jan server.
|
||||
*/
|
||||
private extractFileName(url: string): string {
|
||||
const extractedFileName = url.split('/').pop()
|
||||
const fileName = extractedFileName
|
||||
.toLowerCase()
|
||||
.endsWith(JanModelExtension._supportedModelFormat)
|
||||
? extractedFileName
|
||||
: extractedFileName + JanModelExtension._supportedModelFormat
|
||||
return fileName
|
||||
private async startPollingDownloadProgress(modelId: string): Promise<void> {
|
||||
// wait for some seconds before polling
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000))
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const interval = setInterval(async () => {
|
||||
fetch(
|
||||
`${window.core.api.baseApiUrl}/v1/download/${DownloadRoute.getDownloadProgress}/${modelId}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: { contentType: 'application/json' },
|
||||
}
|
||||
).then(async (res) => {
|
||||
const state: DownloadState = await res.json()
|
||||
if (state.downloadState === 'end') {
|
||||
events.emit(DownloadEvent.onFileDownloadSuccess, state)
|
||||
clearInterval(interval)
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
if (state.downloadState === 'error') {
|
||||
events.emit(DownloadEvent.onFileDownloadError, state)
|
||||
clearInterval(interval)
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
events.emit(DownloadEvent.onFileDownloadUpdate, state)
|
||||
})
|
||||
}, 1000)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@ -318,7 +363,7 @@ export default class JanModelExtension extends ModelExtension {
|
||||
return
|
||||
}
|
||||
|
||||
const defaultModel = await this.getDefaultModel() as Model
|
||||
const defaultModel = (await this.getDefaultModel()) as Model
|
||||
if (!defaultModel) {
|
||||
console.error('Unable to find default model')
|
||||
return
|
||||
@ -382,4 +427,28 @@ export default class JanModelExtension extends ModelExtension {
|
||||
async getConfiguredModels(): Promise<Model[]> {
|
||||
return this.getModelsMetadata()
|
||||
}
|
||||
|
||||
handleDesktopEvents() {
|
||||
if (window && window.electronAPI) {
|
||||
window.electronAPI.onFileDownloadUpdate(
|
||||
async (_event: string, state: any | undefined) => {
|
||||
if (!state) return
|
||||
state.downloadState = 'update'
|
||||
events.emit(DownloadEvent.onFileDownloadUpdate, state)
|
||||
}
|
||||
)
|
||||
window.electronAPI.onFileDownloadError(
|
||||
async (_event: string, state: any) => {
|
||||
state.downloadState = 'error'
|
||||
events.emit(DownloadEvent.onFileDownloadError, state)
|
||||
}
|
||||
)
|
||||
window.electronAPI.onFileDownloadSuccess(
|
||||
async (_event: string, state: any) => {
|
||||
state.downloadState = 'end'
|
||||
events.emit(DownloadEvent.onFileDownloadSuccess, state)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": false,
|
||||
"skipLibCheck": true,
|
||||
"rootDir": "./src"
|
||||
"rootDir": "./src",
|
||||
},
|
||||
"include": ["./src"]
|
||||
"include": ["./src"],
|
||||
}
|
||||
|
||||
@ -19,7 +19,7 @@ module.exports = {
|
||||
new webpack.DefinePlugin({
|
||||
EXTENSION_NAME: JSON.stringify(packageJson.name),
|
||||
MODULE_PATH: JSON.stringify(`${packageJson.name}/${packageJson.module}`),
|
||||
VERSION: JSON.stringify(packageJson.version),
|
||||
VERSION: JSON.stringify(packageJson.version)
|
||||
}),
|
||||
],
|
||||
output: {
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
"build": "tsc -b . && webpack --config webpack.config.js",
|
||||
"build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/pre-install"
|
||||
"build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../pre-install"
|
||||
},
|
||||
"devDependencies": {
|
||||
"rimraf": "^3.0.2",
|
||||
|
||||
13
package.json
13
package.json
@ -21,22 +21,23 @@
|
||||
"lint": "yarn workspace jan lint && yarn workspace jan-web lint",
|
||||
"test:unit": "yarn workspace @janhq/core test",
|
||||
"test": "yarn workspace jan test:e2e",
|
||||
"copy:assets": "cpx \"models/**\" \"electron/models/\" && cpx \"docs/openapi/**\" \"electron/docs/openapi\"",
|
||||
"copy:assets": "cpx \"models/**\" \"electron/models/\" && cpx \"pre-install/*.tgz\" \"electron/pre-install/\" && cpx \"docs/openapi/**\" \"electron/docs/openapi\"",
|
||||
"dev:electron": "yarn copy:assets && yarn workspace jan dev",
|
||||
"dev:web": "yarn workspace jan-web dev",
|
||||
"dev:server": "yarn workspace @janhq/server dev",
|
||||
"dev:server": "yarn copy:assets && yarn workspace @janhq/server dev",
|
||||
"dev": "concurrently --kill-others \"yarn dev:web\" \"wait-on http://localhost:3000 && yarn dev:electron\"",
|
||||
"test-local": "yarn lint && yarn build:test && yarn test",
|
||||
"dev:uikit": "yarn workspace @janhq/uikit install && yarn workspace @janhq/uikit dev",
|
||||
"build:uikit": "yarn workspace @janhq/uikit install && yarn workspace @janhq/uikit build",
|
||||
"build:server": "cd server && yarn install && yarn run build",
|
||||
"build:server": "yarn copy:assets && cd server && yarn install && yarn run build",
|
||||
"build:core": "cd core && yarn install && yarn run build",
|
||||
"build:web": "yarn workspace jan-web build && cpx \"web/out/**\" \"electron/renderer/\"",
|
||||
"build:electron": "yarn copy:assets && yarn workspace jan build",
|
||||
"build:electron:test": "yarn workspace jan build:test",
|
||||
"build:extensions:windows": "rimraf ./electron/pre-install/*.tgz && powershell -command \"$jobs = Get-ChildItem -Path './extensions' -Directory | ForEach-Object { Start-Job -Name ($_.Name) -ScriptBlock { param($_dir); try { Set-Location $_dir; npm install; npm run build:publish; Write-Output 'Build successful in ' + $_dir } catch { Write-Error 'Error in ' + $_dir; throw } } -ArgumentList $_.FullName }; $jobs | Wait-Job; $jobs | ForEach-Object { Receive-Job -Job $_ -Keep } | ForEach-Object { Write-Host $_ }; $failed = $jobs | Where-Object { $_.State -ne 'Completed' -or $_.ChildJobs[0].JobStateInfo.State -ne 'Completed' }; if ($failed) { Exit 1 }\"",
|
||||
"build:extensions:linux": "rimraf ./electron/pre-install/*.tgz && find ./extensions -mindepth 1 -maxdepth 1 -type d -print0 | xargs -0 -n 1 -P 4 -I {} sh -c 'cd {} && npm install && npm run build:publish'",
|
||||
"build:extensions:darwin": "rimraf ./electron/pre-install/*.tgz && find ./extensions -mindepth 1 -maxdepth 1 -type d -print0 | xargs -0 -n 1 -P 4 -I {} sh -c 'cd {} && npm install && npm run build:publish'",
|
||||
"build:extensions:windows": "rimraf ./pre-install/*.tgz && powershell -command \"$jobs = Get-ChildItem -Path './extensions' -Directory | ForEach-Object { Start-Job -Name ($_.Name) -ScriptBlock { param($_dir); try { Set-Location $_dir; npm install; npm run build:publish; Write-Output 'Build successful in ' + $_dir } catch { Write-Error 'Error in ' + $_dir; throw } } -ArgumentList $_.FullName }; $jobs | Wait-Job; $jobs | ForEach-Object { Receive-Job -Job $_ -Keep } | ForEach-Object { Write-Host $_ }; $failed = $jobs | Where-Object { $_.State -ne 'Completed' -or $_.ChildJobs[0].JobStateInfo.State -ne 'Completed' }; if ($failed) { Exit 1 }\"",
|
||||
"build:extensions:linux": "rimraf ./pre-install/*.tgz && find ./extensions -mindepth 1 -maxdepth 1 -type d -print0 | xargs -0 -n 1 -P 4 -I {} sh -c 'cd {} && npm install && npm run build:publish'",
|
||||
"build:extensions:darwin": "rimraf ./pre-install/*.tgz && find ./extensions -mindepth 1 -maxdepth 1 -type d -print0 | xargs -0 -n 1 -P 4 -I {} sh -c 'cd {} && npm install && npm run build:publish'",
|
||||
"build:extensions:server": "yarn workspace build:extensions ",
|
||||
"build:extensions": "run-script-os",
|
||||
"build:test": "yarn copy:assets && yarn build:web && yarn workspace jan build:test",
|
||||
"build": "yarn build:web && yarn build:electron",
|
||||
|
||||
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;
|
||||
schemaPath?: string;
|
||||
baseDir?: string;
|
||||
storageAdataper?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -103,9 +104,12 @@ export const startServer = async (configs?: ServerConfig) => {
|
||||
{ prefix: "extensions" }
|
||||
);
|
||||
|
||||
// Register proxy middleware
|
||||
if (configs?.storageAdataper)
|
||||
server.addHook("preHandler", configs.storageAdataper);
|
||||
|
||||
// Register API routes
|
||||
await server.register(v1Router, { prefix: "/v1" });
|
||||
|
||||
// Start listening for requests
|
||||
await server
|
||||
.listen({
|
||||
|
||||
@ -1,3 +1,7 @@
|
||||
import { startServer } from "./index";
|
||||
|
||||
startServer();
|
||||
import { s3 } from "./middleware/s3";
|
||||
import { setup } from "./helpers/setup";
|
||||
import { startServer as start } from "./index";
|
||||
/**
|
||||
* Setup extensions and start the server
|
||||
*/
|
||||
setup().then(() => start({ storageAdataper: s3 }));
|
||||
|
||||
70
server/middleware/s3.ts
Normal file
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": {
|
||||
"lint": "eslint . --ext \".js,.jsx,.ts,.tsx\"",
|
||||
"test:e2e": "playwright test --workers=1",
|
||||
"dev": "tsc --watch & node --watch build/main.js",
|
||||
"build": "tsc"
|
||||
"build:core": "cd node_modules/@janhq/core && yarn install && yarn build",
|
||||
"dev": "yarn build:core && tsc --watch & node --watch build/main.js",
|
||||
"build": "yarn build:core && tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@alumna/reflect": "^1.1.3",
|
||||
"@cyclic.sh/s3fs": "^1.2.9",
|
||||
"@fastify/cors": "^8.4.2",
|
||||
"@fastify/static": "^6.12.0",
|
||||
"@fastify/swagger": "^8.13.0",
|
||||
"@fastify/swagger-ui": "2.0.1",
|
||||
"@janhq/core": "link:./core",
|
||||
"@janhq/core": "file:../core",
|
||||
"dotenv": "^16.3.1",
|
||||
"fastify": "^4.24.3",
|
||||
"request": "^2.88.2",
|
||||
@ -39,5 +41,8 @@
|
||||
"run-script-os": "^1.1.6",
|
||||
"@types/tcp-port-used": "^1.0.4",
|
||||
"typescript": "^5.2.2"
|
||||
}
|
||||
},
|
||||
"bundleDependencies": [
|
||||
"@janhq/core"
|
||||
]
|
||||
}
|
||||
|
||||
@ -20,5 +20,5 @@
|
||||
// "sourceMap": true,
|
||||
|
||||
"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 useDownloadModel from '@/hooks/useDownloadModel'
|
||||
import { useDownloadState } from '@/hooks/useDownloadState'
|
||||
import { modelDownloadStateAtom } from '@/hooks/useDownloadState'
|
||||
|
||||
import { formatDownloadPercentage } from '@/utils/converter'
|
||||
|
||||
import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
|
||||
import { getDownloadingModelAtom } from '@/helpers/atoms/Model.atom'
|
||||
|
||||
export default function DownloadingState() {
|
||||
const { downloadStates } = useDownloadState()
|
||||
const downloadingModels = useAtomValue(downloadingModelsAtom)
|
||||
const downloadStates = useAtomValue(modelDownloadStateAtom)
|
||||
const downloadingModels = useAtomValue(getDownloadingModelAtom)
|
||||
const { abortModelDownload } = useDownloadModel()
|
||||
|
||||
const totalCurrentProgress = downloadStates
|
||||
const totalCurrentProgress = Object.values(downloadStates)
|
||||
.map((a) => a.size.transferred + a.size.transferred)
|
||||
.reduce((partialSum, a) => partialSum + a, 0)
|
||||
|
||||
const totalSize = downloadStates
|
||||
const totalSize = Object.values(downloadStates)
|
||||
.map((a) => a.size.total + a.size.total)
|
||||
.reduce((partialSum, a) => partialSum + a, 0)
|
||||
|
||||
@ -36,12 +36,14 @@ export default function DownloadingState() {
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{downloadStates?.length > 0 && (
|
||||
{Object.values(downloadStates)?.length > 0 && (
|
||||
<Modal>
|
||||
<ModalTrigger asChild>
|
||||
<div className="relative block">
|
||||
<Button size="sm" themes="outline">
|
||||
<span>{downloadStates.length} Downloading model</span>
|
||||
<span>
|
||||
{Object.values(downloadStates).length} Downloading model
|
||||
</span>
|
||||
</Button>
|
||||
<span
|
||||
className="absolute left-0 h-full rounded-md rounded-l-md bg-primary/20"
|
||||
@ -55,40 +57,38 @@ export default function DownloadingState() {
|
||||
<ModalHeader>
|
||||
<ModalTitle>Downloading model</ModalTitle>
|
||||
</ModalHeader>
|
||||
{downloadStates.map((item, i) => {
|
||||
return (
|
||||
<div className="pt-2" key={i}>
|
||||
<Progress
|
||||
className="mb-2 h-2"
|
||||
value={
|
||||
formatDownloadPercentage(item?.percent, {
|
||||
hidePercentage: true,
|
||||
}) as number
|
||||
}
|
||||
/>
|
||||
<div className="flex items-center justify-between gap-x-2">
|
||||
<div className="flex gap-x-2">
|
||||
<p className="line-clamp-1">{item?.modelId}</p>
|
||||
<span>{formatDownloadPercentage(item?.percent)}</span>
|
||||
</div>
|
||||
<Button
|
||||
themes="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (item?.modelId) {
|
||||
const model = downloadingModels.find(
|
||||
(model) => model.id === item.modelId
|
||||
)
|
||||
if (model) abortModelDownload(model)
|
||||
}
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
{Object.values(downloadStates).map((item, i) => (
|
||||
<div className="pt-2" key={i}>
|
||||
<Progress
|
||||
className="mb-2 h-2"
|
||||
value={
|
||||
formatDownloadPercentage(item?.percent, {
|
||||
hidePercentage: true,
|
||||
}) as number
|
||||
}
|
||||
/>
|
||||
<div className="flex items-center justify-between gap-x-2">
|
||||
<div className="flex gap-x-2">
|
||||
<p className="line-clamp-1">{item?.modelId}</p>
|
||||
<span>{formatDownloadPercentage(item?.percent)}</span>
|
||||
</div>
|
||||
<Button
|
||||
themes="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (item?.modelId) {
|
||||
const model = downloadingModels.find(
|
||||
(model) => model.id === item.modelId
|
||||
)
|
||||
if (model) abortModelDownload(model)
|
||||
}
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
@ -25,8 +25,7 @@ import { MainViewState } from '@/constants/screens'
|
||||
|
||||
import { useActiveModel } from '@/hooks/useActiveModel'
|
||||
|
||||
import { useDownloadState } from '@/hooks/useDownloadState'
|
||||
|
||||
import { modelDownloadStateAtom } from '@/hooks/useDownloadState'
|
||||
import useGetSystemResources from '@/hooks/useGetSystemResources'
|
||||
import { useMainViewState } from '@/hooks/useMainViewState'
|
||||
|
||||
@ -53,7 +52,7 @@ const BottomBar = () => {
|
||||
const downloadedModels = useAtomValue(downloadedModelsAtom)
|
||||
|
||||
const { setMainViewState } = useMainViewState()
|
||||
const { downloadStates } = useDownloadState()
|
||||
const downloadStates = useAtomValue(modelDownloadStateAtom)
|
||||
const setShowSelectModelModal = useSetAtom(showSelectModelModalAtom)
|
||||
const [serverEnabled] = useAtom(serverEnabledAtom)
|
||||
|
||||
@ -109,7 +108,7 @@ const BottomBar = () => {
|
||||
)}
|
||||
{downloadedModels.length === 0 &&
|
||||
!stateModel.loading &&
|
||||
downloadStates.length === 0 && (
|
||||
Object.values(downloadStates).length === 0 && (
|
||||
<Button
|
||||
size="sm"
|
||||
themes="outline"
|
||||
|
||||
@ -17,23 +17,22 @@ import {
|
||||
import { atom, useAtomValue } from 'jotai'
|
||||
|
||||
import useDownloadModel from '@/hooks/useDownloadModel'
|
||||
import { useDownloadState } from '@/hooks/useDownloadState'
|
||||
|
||||
import { modelDownloadStateAtom } from '@/hooks/useDownloadState'
|
||||
|
||||
import { formatDownloadPercentage } from '@/utils/converter'
|
||||
|
||||
import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
|
||||
import { getDownloadingModelAtom } from '@/helpers/atoms/Model.atom'
|
||||
|
||||
type Props = {
|
||||
model: Model
|
||||
isFromList?: boolean
|
||||
}
|
||||
|
||||
export default function ModalCancelDownload({ model, isFromList }: Props) {
|
||||
const { modelDownloadStateAtom } = useDownloadState()
|
||||
const downloadingModels = useAtomValue(downloadingModelsAtom)
|
||||
const ModalCancelDownload: React.FC<Props> = ({ model, isFromList }) => {
|
||||
const downloadingModels = useAtomValue(getDownloadingModelAtom)
|
||||
const downloadAtom = useMemo(
|
||||
() => atom((get) => get(modelDownloadStateAtom)[model.id]),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[model.id]
|
||||
)
|
||||
const downloadState = useAtomValue(downloadAtom)
|
||||
@ -98,3 +97,5 @@ export default function ModalCancelDownload({ model, isFromList }: Props) {
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModalCancelDownload
|
||||
|
||||
@ -1,93 +1,62 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { PropsWithChildren, useCallback, useEffect } from 'react'
|
||||
|
||||
import { PropsWithChildren, useEffect, useRef } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
import { baseName } from '@janhq/core'
|
||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||
import { DownloadEvent, events } from '@janhq/core'
|
||||
import { useSetAtom } from 'jotai'
|
||||
|
||||
import { useDownloadState } from '@/hooks/useDownloadState'
|
||||
|
||||
import { modelBinFileName } from '@/utils/model'
|
||||
import { setDownloadStateAtom } from '@/hooks/useDownloadState'
|
||||
|
||||
import EventHandler from './EventHandler'
|
||||
|
||||
import { appDownloadProgress } from './Jotai'
|
||||
|
||||
import {
|
||||
downloadedModelsAtom,
|
||||
downloadingModelsAtom,
|
||||
} from '@/helpers/atoms/Model.atom'
|
||||
|
||||
export default function EventListenerWrapper({ children }: PropsWithChildren) {
|
||||
const EventListenerWrapper = ({ children }: PropsWithChildren) => {
|
||||
const setDownloadState = useSetAtom(setDownloadStateAtom)
|
||||
const setProgress = useSetAtom(appDownloadProgress)
|
||||
const models = useAtomValue(downloadingModelsAtom)
|
||||
const modelsRef = useRef(models)
|
||||
|
||||
const [downloadedModels, setDownloadedModels] = useAtom(downloadedModelsAtom)
|
||||
const {
|
||||
setDownloadState,
|
||||
setDownloadStateSuccess,
|
||||
setDownloadStateFailed,
|
||||
setDownloadStateCancelled,
|
||||
} = useDownloadState()
|
||||
const downloadedModelRef = useRef(downloadedModels)
|
||||
const onFileDownloadUpdate = useCallback(
|
||||
async (state: DownloadState) => {
|
||||
console.debug('onFileDownloadUpdate', state)
|
||||
setDownloadState(state)
|
||||
},
|
||||
[setDownloadState]
|
||||
)
|
||||
|
||||
const onFileDownloadError = useCallback(
|
||||
(state: DownloadState) => {
|
||||
console.debug('onFileDownloadError', state)
|
||||
setDownloadState(state)
|
||||
},
|
||||
[setDownloadState]
|
||||
)
|
||||
|
||||
const onFileDownloadSuccess = useCallback(
|
||||
(state: DownloadState) => {
|
||||
console.debug('onFileDownloadSuccess', state)
|
||||
setDownloadState(state)
|
||||
},
|
||||
[setDownloadState]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
modelsRef.current = models
|
||||
}, [models])
|
||||
useEffect(() => {
|
||||
downloadedModelRef.current = downloadedModels
|
||||
}, [downloadedModels])
|
||||
console.log('EventListenerWrapper: registering event listeners...')
|
||||
|
||||
events.on(DownloadEvent.onFileDownloadUpdate, onFileDownloadUpdate)
|
||||
events.on(DownloadEvent.onFileDownloadError, onFileDownloadError)
|
||||
events.on(DownloadEvent.onFileDownloadSuccess, onFileDownloadSuccess)
|
||||
|
||||
return () => {
|
||||
console.log('EventListenerWrapper: unregistering event listeners...')
|
||||
events.off(DownloadEvent.onFileDownloadUpdate, onFileDownloadUpdate)
|
||||
events.off(DownloadEvent.onFileDownloadError, onFileDownloadError)
|
||||
events.off(DownloadEvent.onFileDownloadSuccess, onFileDownloadSuccess)
|
||||
}
|
||||
}, [onFileDownloadUpdate, onFileDownloadError, onFileDownloadSuccess])
|
||||
|
||||
useEffect(() => {
|
||||
if (window && window.electronAPI) {
|
||||
window.electronAPI.onFileDownloadUpdate(
|
||||
async (_event: string, state: any | undefined) => {
|
||||
if (!state) return
|
||||
const modelName = await baseName(state.fileName)
|
||||
const model = modelsRef.current.find(
|
||||
(model) => modelBinFileName(model) === modelName
|
||||
)
|
||||
if (model)
|
||||
setDownloadState({
|
||||
...state,
|
||||
modelId: model.id,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
window.electronAPI.onFileDownloadError(
|
||||
async (_event: string, state: any) => {
|
||||
const modelName = await baseName(state.fileName)
|
||||
const model = modelsRef.current.find(
|
||||
(model) => modelBinFileName(model) === modelName
|
||||
)
|
||||
if (model) {
|
||||
if (state.err?.message !== 'aborted') {
|
||||
console.error('Download error', state)
|
||||
setDownloadStateFailed(model.id, state.err.message)
|
||||
} else {
|
||||
setDownloadStateCancelled(model.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
window.electronAPI.onFileDownloadSuccess(
|
||||
async (_event: string, state: any) => {
|
||||
if (state && state.fileName) {
|
||||
const modelName = await baseName(state.fileName)
|
||||
const model = modelsRef.current.find(
|
||||
(model) => modelBinFileName(model) === modelName
|
||||
)
|
||||
if (model) {
|
||||
setDownloadStateSuccess(model.id)
|
||||
setDownloadedModels([...downloadedModelRef.current, model])
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
window.electronAPI.onAppUpdateDownloadUpdate(
|
||||
(_event: string, progress: any) => {
|
||||
setProgress(progress.percent)
|
||||
@ -107,14 +76,9 @@ export default function EventListenerWrapper({ children }: PropsWithChildren) {
|
||||
})
|
||||
}
|
||||
return () => {}
|
||||
}, [
|
||||
setDownloadState,
|
||||
setDownloadStateCancelled,
|
||||
setDownloadStateFailed,
|
||||
setDownloadStateSuccess,
|
||||
setDownloadedModels,
|
||||
setProgress,
|
||||
])
|
||||
}, [setDownloadState, setProgress])
|
||||
|
||||
return <EventHandler>{children}</EventHandler>
|
||||
}
|
||||
|
||||
export default EventListenerWrapper
|
||||
|
||||
@ -4,23 +4,28 @@ import { atom } from 'jotai'
|
||||
export const stateModel = atom({ state: 'start', loading: false, model: '' })
|
||||
export const activeAssistantModelAtom = atom<Model | undefined>(undefined)
|
||||
|
||||
export const downloadingModelsAtom = atom<Model[]>([])
|
||||
/**
|
||||
* Stores the list of models which are being downloaded.
|
||||
*/
|
||||
const downloadingModelsAtom = atom<Model[]>([])
|
||||
|
||||
export const addNewDownloadingModelAtom = atom(
|
||||
null,
|
||||
(get, set, model: Model) => {
|
||||
const currentModels = get(downloadingModelsAtom)
|
||||
set(downloadingModelsAtom, [...currentModels, model])
|
||||
export const getDownloadingModelAtom = atom((get) => get(downloadingModelsAtom))
|
||||
|
||||
export const addDownloadingModelAtom = atom(null, (get, set, model: Model) => {
|
||||
const downloadingModels = get(downloadingModelsAtom)
|
||||
if (!downloadingModels.find((e) => e.id === model.id)) {
|
||||
set(downloadingModelsAtom, [...downloadingModels, model])
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
export const removeDownloadingModelAtom = atom(
|
||||
null,
|
||||
(get, set, modelId: string) => {
|
||||
const currentModels = get(downloadingModelsAtom)
|
||||
const downloadingModels = get(downloadingModelsAtom)
|
||||
|
||||
set(
|
||||
downloadingModelsAtom,
|
||||
currentModels.filter((e) => e.id !== modelId)
|
||||
downloadingModels.filter((e) => e.id !== modelId)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@ -1,6 +1,12 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
|
||||
import { Assistant, AssistantExtension, ExtensionTypeEnum } from '@janhq/core'
|
||||
import {
|
||||
Assistant,
|
||||
AssistantEvent,
|
||||
AssistantExtension,
|
||||
ExtensionTypeEnum,
|
||||
events,
|
||||
} from '@janhq/core'
|
||||
|
||||
import { useSetAtom } from 'jotai'
|
||||
|
||||
@ -10,14 +16,19 @@ import { assistantsAtom } from '@/helpers/atoms/Assistant.atom'
|
||||
const useAssistants = () => {
|
||||
const setAssistants = useSetAtom(assistantsAtom)
|
||||
|
||||
useEffect(() => {
|
||||
const getAssistants = async () => {
|
||||
const assistants = await getLocalAssistants()
|
||||
setAssistants(assistants)
|
||||
}
|
||||
|
||||
getAssistants()
|
||||
const getData = useCallback(async () => {
|
||||
const assistants = await getLocalAssistants()
|
||||
setAssistants(assistants)
|
||||
}, [setAssistants])
|
||||
|
||||
useEffect(() => {
|
||||
getData()
|
||||
|
||||
events.on(AssistantEvent.OnAssistantsUpdate, () => getData())
|
||||
return () => {
|
||||
events.off(AssistantEvent.OnAssistantsUpdate, () => getData())
|
||||
}
|
||||
}, [getData])
|
||||
}
|
||||
|
||||
const getLocalAssistants = async (): Promise<Assistant[]> =>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useContext } from 'react'
|
||||
import { useCallback, useContext } from 'react'
|
||||
|
||||
import {
|
||||
Model,
|
||||
@ -15,21 +15,40 @@ import { FeatureToggleContext } from '@/context/FeatureToggle'
|
||||
|
||||
import { modelBinFileName } from '@/utils/model'
|
||||
|
||||
import { useDownloadState } from './useDownloadState'
|
||||
import { setDownloadStateAtom } from './useDownloadState'
|
||||
|
||||
import { extensionManager } from '@/extension/ExtensionManager'
|
||||
import { addNewDownloadingModelAtom } from '@/helpers/atoms/Model.atom'
|
||||
import { addDownloadingModelAtom } from '@/helpers/atoms/Model.atom'
|
||||
|
||||
export default function useDownloadModel() {
|
||||
const { ignoreSSL, proxy } = useContext(FeatureToggleContext)
|
||||
const { setDownloadState } = useDownloadState()
|
||||
const addNewDownloadingModel = useSetAtom(addNewDownloadingModelAtom)
|
||||
const setDownloadState = useSetAtom(setDownloadStateAtom)
|
||||
const addDownloadingModel = useSetAtom(addDownloadingModelAtom)
|
||||
|
||||
const downloadModel = async (model: Model) => {
|
||||
const childrenDownloadProgress: DownloadState[] = []
|
||||
model.sources.forEach((source: ModelArtifact) => {
|
||||
childrenDownloadProgress.push({
|
||||
modelId: source.filename,
|
||||
const downloadModel = useCallback(
|
||||
async (model: Model) => {
|
||||
const childProgresses: DownloadState[] = model.sources.map(
|
||||
(source: ModelArtifact) => ({
|
||||
filename: source.filename,
|
||||
modelId: model.id,
|
||||
time: {
|
||||
elapsed: 0,
|
||||
remaining: 0,
|
||||
},
|
||||
speed: 0,
|
||||
percent: 0,
|
||||
size: {
|
||||
total: 0,
|
||||
transferred: 0,
|
||||
},
|
||||
downloadState: 'downloading',
|
||||
})
|
||||
)
|
||||
|
||||
// set an initial download state
|
||||
setDownloadState({
|
||||
filename: '',
|
||||
modelId: model.id,
|
||||
time: {
|
||||
elapsed: 0,
|
||||
remaining: 0,
|
||||
@ -40,31 +59,16 @@ export default function useDownloadModel() {
|
||||
total: 0,
|
||||
transferred: 0,
|
||||
},
|
||||
children: childProgresses,
|
||||
downloadState: 'downloading',
|
||||
})
|
||||
})
|
||||
|
||||
// set an initial download state
|
||||
setDownloadState({
|
||||
modelId: model.id,
|
||||
time: {
|
||||
elapsed: 0,
|
||||
remaining: 0,
|
||||
},
|
||||
speed: 0,
|
||||
percent: 0,
|
||||
size: {
|
||||
total: 0,
|
||||
transferred: 0,
|
||||
},
|
||||
children: childrenDownloadProgress,
|
||||
})
|
||||
addDownloadingModel(model)
|
||||
|
||||
addNewDownloadingModel(model)
|
||||
|
||||
await extensionManager
|
||||
.get<ModelExtension>(ExtensionTypeEnum.Model)
|
||||
?.downloadModel(model, { ignoreSSL, proxy })
|
||||
}
|
||||
await localDownloadModel(model, ignoreSSL, proxy)
|
||||
},
|
||||
[ignoreSSL, proxy, addDownloadingModel, setDownloadState]
|
||||
)
|
||||
|
||||
const abortModelDownload = async (model: Model) => {
|
||||
await abortDownload(
|
||||
@ -77,3 +81,12 @@ export default function useDownloadModel() {
|
||||
abortModelDownload,
|
||||
}
|
||||
}
|
||||
|
||||
const localDownloadModel = async (
|
||||
model: Model,
|
||||
ignoreSSL: boolean,
|
||||
proxy: string
|
||||
) =>
|
||||
extensionManager
|
||||
.get<ModelExtension>(ExtensionTypeEnum.Model)
|
||||
?.downloadModel(model, { ignoreSSL, proxy })
|
||||
|
||||
@ -1,96 +1,64 @@
|
||||
import { atom, useSetAtom, useAtomValue } from 'jotai'
|
||||
import { atom } from 'jotai'
|
||||
|
||||
import { toaster } from '@/containers/Toast'
|
||||
|
||||
import {
|
||||
configuredModelsAtom,
|
||||
downloadedModelsAtom,
|
||||
removeDownloadingModelAtom,
|
||||
} from '@/helpers/atoms/Model.atom'
|
||||
|
||||
// download states
|
||||
const modelDownloadStateAtom = atom<Record<string, DownloadState>>({})
|
||||
export const modelDownloadStateAtom = atom<Record<string, DownloadState>>({})
|
||||
|
||||
const setDownloadStateAtom = atom(null, (get, set, state: DownloadState) => {
|
||||
const currentState = { ...get(modelDownloadStateAtom) }
|
||||
console.debug(
|
||||
`current download state for ${state.modelId} is ${JSON.stringify(state)}`
|
||||
)
|
||||
currentState[state.modelId] = state
|
||||
set(modelDownloadStateAtom, currentState)
|
||||
})
|
||||
|
||||
const setDownloadStateSuccessAtom = atom(null, (get, set, modelId: string) => {
|
||||
const currentState = { ...get(modelDownloadStateAtom) }
|
||||
const state = currentState[modelId]
|
||||
if (!state) {
|
||||
console.debug(`Cannot find download state for ${modelId}`)
|
||||
return
|
||||
}
|
||||
delete currentState[modelId]
|
||||
set(modelDownloadStateAtom, currentState)
|
||||
toaster({
|
||||
title: 'Download Completed',
|
||||
description: `Download ${modelId} completed`,
|
||||
type: 'success',
|
||||
})
|
||||
})
|
||||
|
||||
const setDownloadStateFailedAtom = atom(
|
||||
/**
|
||||
* Used to set the download state for a particular model.
|
||||
*/
|
||||
export const setDownloadStateAtom = atom(
|
||||
null,
|
||||
(get, set, modelId: string, error: string) => {
|
||||
(get, set, state: DownloadState) => {
|
||||
const currentState = { ...get(modelDownloadStateAtom) }
|
||||
const state = currentState[modelId]
|
||||
if (!state) {
|
||||
console.debug(`Cannot find download state for ${modelId}`)
|
||||
return
|
||||
}
|
||||
if (error.includes('certificate')) {
|
||||
error += '. To fix enable "Ignore SSL Certificates" in Advanced settings.'
|
||||
}
|
||||
toaster({
|
||||
title: 'Download Failed',
|
||||
description: `Model ${modelId} download failed: ${error}`,
|
||||
type: 'error',
|
||||
})
|
||||
|
||||
delete currentState[modelId]
|
||||
set(modelDownloadStateAtom, currentState)
|
||||
}
|
||||
)
|
||||
const setDownloadStateCancelledAtom = atom(
|
||||
null,
|
||||
(get, set, modelId: string) => {
|
||||
const currentState = { ...get(modelDownloadStateAtom) }
|
||||
const state = currentState[modelId]
|
||||
if (!state) {
|
||||
console.debug(`Cannot find download state for ${modelId}`)
|
||||
if (state.downloadState === 'end') {
|
||||
// download successfully
|
||||
delete currentState[state.modelId]
|
||||
set(removeDownloadingModelAtom, state.modelId)
|
||||
const model = get(configuredModelsAtom).find(
|
||||
(e) => e.id === state.modelId
|
||||
)
|
||||
if (model) set(downloadedModelsAtom, (prev) => [...prev, model])
|
||||
toaster({
|
||||
title: 'Cancel Download',
|
||||
description: `Model ${modelId} cancel download`,
|
||||
type: 'warning',
|
||||
title: 'Download Completed',
|
||||
description: `Download ${state.modelId} completed`,
|
||||
type: 'success',
|
||||
})
|
||||
|
||||
return
|
||||
} else if (state.downloadState === 'error') {
|
||||
// download error
|
||||
delete currentState[state.modelId]
|
||||
set(removeDownloadingModelAtom, state.modelId)
|
||||
if (state.error === 'aborted') {
|
||||
toaster({
|
||||
title: 'Cancel Download',
|
||||
description: `Model ${state.modelId} download cancelled`,
|
||||
type: 'warning',
|
||||
})
|
||||
} else {
|
||||
let error = state.error
|
||||
if (state.error?.includes('certificate')) {
|
||||
error +=
|
||||
'. To fix enable "Ignore SSL Certificates" in Advanced settings.'
|
||||
}
|
||||
toaster({
|
||||
title: 'Download Failed',
|
||||
description: `Model ${state.modelId} download failed: ${error}`,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// download in progress
|
||||
currentState[state.modelId] = state
|
||||
}
|
||||
delete currentState[modelId]
|
||||
|
||||
set(modelDownloadStateAtom, currentState)
|
||||
}
|
||||
)
|
||||
|
||||
export function useDownloadState() {
|
||||
const modelDownloadState = useAtomValue(modelDownloadStateAtom)
|
||||
const setDownloadState = useSetAtom(setDownloadStateAtom)
|
||||
const setDownloadStateSuccess = useSetAtom(setDownloadStateSuccessAtom)
|
||||
const setDownloadStateFailed = useSetAtom(setDownloadStateFailedAtom)
|
||||
const setDownloadStateCancelled = useSetAtom(setDownloadStateCancelledAtom)
|
||||
|
||||
const downloadStates: DownloadState[] = []
|
||||
for (const [, value] of Object.entries(modelDownloadState)) {
|
||||
downloadStates.push(value)
|
||||
}
|
||||
|
||||
return {
|
||||
modelDownloadStateAtom,
|
||||
modelDownloadState,
|
||||
setDownloadState,
|
||||
setDownloadStateSuccess,
|
||||
setDownloadStateFailed,
|
||||
setDownloadStateCancelled,
|
||||
downloadStates,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,12 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
|
||||
import { ExtensionTypeEnum, Model, ModelExtension } from '@janhq/core'
|
||||
import {
|
||||
ExtensionTypeEnum,
|
||||
Model,
|
||||
ModelEvent,
|
||||
ModelExtension,
|
||||
events,
|
||||
} from '@janhq/core'
|
||||
|
||||
import { useSetAtom } from 'jotai'
|
||||
|
||||
@ -14,23 +20,30 @@ const useModels = () => {
|
||||
const setDownloadedModels = useSetAtom(downloadedModelsAtom)
|
||||
const setConfiguredModels = useSetAtom(configuredModelsAtom)
|
||||
|
||||
useEffect(() => {
|
||||
const getData = useCallback(() => {
|
||||
const getDownloadedModels = async () => {
|
||||
const models = await getLocalDownloadedModels()
|
||||
setDownloadedModels(models)
|
||||
}
|
||||
|
||||
getDownloadedModels()
|
||||
}, [setDownloadedModels])
|
||||
|
||||
useEffect(() => {
|
||||
const getConfiguredModels = async () => {
|
||||
const models = await getLocalConfiguredModels()
|
||||
setConfiguredModels(models)
|
||||
}
|
||||
|
||||
getDownloadedModels()
|
||||
getConfiguredModels()
|
||||
}, [setConfiguredModels])
|
||||
}, [setDownloadedModels, setConfiguredModels])
|
||||
|
||||
useEffect(() => {
|
||||
// Try get data on mount
|
||||
getData()
|
||||
|
||||
// Listen for model updates
|
||||
events.on(ModelEvent.OnModelsUpdate, async () => getData())
|
||||
return () => {
|
||||
// Remove listener on unmount
|
||||
events.off(ModelEvent.OnModelsUpdate, async () => {})
|
||||
}
|
||||
}, [getData])
|
||||
}
|
||||
|
||||
const getLocalConfiguredModels = async (): Promise<Model[]> =>
|
||||
|
||||
@ -10,8 +10,6 @@ import {
|
||||
|
||||
import { useSetAtom } from 'jotai'
|
||||
|
||||
import { loadModelErrorAtom } from './useActiveModel'
|
||||
|
||||
import { extensionManager } from '@/extension'
|
||||
import { setConvoMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
|
||||
import {
|
||||
@ -26,7 +24,6 @@ export default function useSetActiveThread() {
|
||||
const setThreadMessage = useSetAtom(setConvoMessagesAtom)
|
||||
const setThreadModelParams = useSetAtom(setThreadModelParamsAtom)
|
||||
const setIsGeneratingResponse = useSetAtom(isGeneratingResponseAtom)
|
||||
const setLoadModelError = useSetAtom(loadModelErrorAtom)
|
||||
|
||||
const setActiveThread = useCallback(
|
||||
async (thread: Thread) => {
|
||||
|
||||
@ -27,7 +27,9 @@ const nextConfig = {
|
||||
VERSION: JSON.stringify(packageJson.version),
|
||||
ANALYTICS_ID: JSON.stringify(process.env.ANALYTICS_ID),
|
||||
ANALYTICS_HOST: JSON.stringify(process.env.ANALYTICS_HOST),
|
||||
API_BASE_URL: JSON.stringify('http://localhost:1337'),
|
||||
API_BASE_URL: JSON.stringify(
|
||||
process.env.API_BASE_URL ?? 'http://localhost:1337'
|
||||
),
|
||||
isMac: process.platform === 'darwin',
|
||||
isWindows: process.platform === 'win32',
|
||||
isLinux: process.platform === 'linux',
|
||||
|
||||
@ -10,8 +10,6 @@ import LogoMark from '@/containers/Brand/Logo/Mark'
|
||||
|
||||
import { MainViewState } from '@/constants/screens'
|
||||
|
||||
import { loadModelErrorAtom } from '@/hooks/useActiveModel'
|
||||
|
||||
import { useMainViewState } from '@/hooks/useMainViewState'
|
||||
|
||||
import ChatItem from '../ChatItem'
|
||||
|
||||
@ -9,7 +9,6 @@ import { Button } from '@janhq/uikit'
|
||||
import { useAtomValue, useSetAtom } from 'jotai'
|
||||
import { RefreshCcw } from 'lucide-react'
|
||||
|
||||
import { useActiveModel } from '@/hooks/useActiveModel'
|
||||
import useSendChatMessage from '@/hooks/useSendChatMessage'
|
||||
|
||||
import { extensionManager } from '@/extension'
|
||||
|
||||
@ -25,7 +25,7 @@ import { MainViewState } from '@/constants/screens'
|
||||
import { useCreateNewThread } from '@/hooks/useCreateNewThread'
|
||||
import useDownloadModel from '@/hooks/useDownloadModel'
|
||||
|
||||
import { useDownloadState } from '@/hooks/useDownloadState'
|
||||
import { modelDownloadStateAtom } from '@/hooks/useDownloadState'
|
||||
|
||||
import { useMainViewState } from '@/hooks/useMainViewState'
|
||||
|
||||
@ -49,7 +49,6 @@ type Props = {
|
||||
const ExploreModelItemHeader: React.FC<Props> = ({ model, onClick, open }) => {
|
||||
const { downloadModel } = useDownloadModel()
|
||||
const downloadedModels = useAtomValue(downloadedModelsAtom)
|
||||
const { modelDownloadStateAtom } = useDownloadState()
|
||||
const { requestCreateNewThread } = useCreateNewThread()
|
||||
const totalRam = useAtomValue(totalRamAtom)
|
||||
const nvidiaTotalVram = useAtomValue(nvidiaTotalVramAtom)
|
||||
|
||||
@ -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,
|
||||
DownloadRoute,
|
||||
ExtensionRoute,
|
||||
FileManagerRoute,
|
||||
FileSystemRoute,
|
||||
} from '@janhq/core'
|
||||
|
||||
@ -22,6 +23,7 @@ export const APIRoutes = [
|
||||
route: r,
|
||||
})),
|
||||
...Object.values(FileSystemRoute).map((r) => ({ path: `fs`, route: r })),
|
||||
...Object.values(FileManagerRoute).map((r) => ({ path: `fs`, route: r })),
|
||||
]
|
||||
|
||||
// Define the restAPI object with methods for each API route
|
||||
@ -50,4 +52,6 @@ export const restAPI = {
|
||||
}
|
||||
}, {}),
|
||||
openExternalUrl,
|
||||
// Jan Server URL
|
||||
baseApiUrl: API_BASE_URL,
|
||||
}
|
||||
|
||||
3
web/types/downloadState.d.ts
vendored
3
web/types/downloadState.d.ts
vendored
@ -1,12 +1,13 @@
|
||||
type DownloadState = {
|
||||
modelId: string
|
||||
filename: string
|
||||
time: DownloadTime
|
||||
speed: number
|
||||
percent: number
|
||||
size: DownloadSize
|
||||
isFinished?: boolean
|
||||
children?: DownloadState[]
|
||||
error?: string
|
||||
downloadState: 'downloading' | 'error' | 'end'
|
||||
}
|
||||
|
||||
type DownloadTime = {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user