refactor: clean up deprecated components and events (#4769)
This commit is contained in:
parent
c79c10c96b
commit
a8aa938f42
@ -25,7 +25,6 @@ export default defineConfig([
|
||||
'@types/pacote',
|
||||
'@npmcli/arborist',
|
||||
'ulidx',
|
||||
'node-fetch',
|
||||
'fs',
|
||||
'request',
|
||||
'crypto',
|
||||
|
||||
@ -2,7 +2,6 @@ import { openExternalUrl } from './core'
|
||||
import { joinPath } from './core'
|
||||
import { openFileExplorer } from './core'
|
||||
import { getJanDataFolderPath } from './core'
|
||||
import { abortDownload } from './core'
|
||||
import { executeOnMain } from './core'
|
||||
|
||||
describe('test core apis', () => {
|
||||
@ -53,18 +52,6 @@ describe('test core apis', () => {
|
||||
expect(result).toBe('/path/to/jan/data')
|
||||
})
|
||||
|
||||
it('should abort download', async () => {
|
||||
const fileName = 'testFile'
|
||||
globalThis.core = {
|
||||
api: {
|
||||
abortDownload: jest.fn().mockResolvedValue('aborted'),
|
||||
},
|
||||
}
|
||||
const result = await abortDownload(fileName)
|
||||
expect(globalThis.core.api.abortDownload).toHaveBeenCalledWith(fileName)
|
||||
expect(result).toBe('aborted')
|
||||
})
|
||||
|
||||
it('should execute function on main process', async () => {
|
||||
const extension = 'testExtension'
|
||||
const method = 'testMethod'
|
||||
|
||||
@ -1,9 +1,4 @@
|
||||
import {
|
||||
DownloadRequest,
|
||||
FileStat,
|
||||
NetworkConfig,
|
||||
SystemInformation,
|
||||
} from '../types'
|
||||
import { SystemInformation } from '../types'
|
||||
|
||||
/**
|
||||
* Execute a extension module function in main process
|
||||
@ -14,42 +9,19 @@ import {
|
||||
* @returns Promise<any>
|
||||
*
|
||||
*/
|
||||
const executeOnMain: (
|
||||
extension: string,
|
||||
method: string,
|
||||
...args: any[]
|
||||
) => Promise<any> = (extension, method, ...args) =>
|
||||
globalThis.core?.api?.invokeExtensionFunc(extension, method, ...args)
|
||||
const executeOnMain: (extension: string, method: string, ...args: any[]) => Promise<any> = (
|
||||
extension,
|
||||
method,
|
||||
...args
|
||||
) => globalThis.core?.api?.invokeExtensionFunc(extension, method, ...args)
|
||||
|
||||
/**
|
||||
* Downloads a file from a URL and saves it to the local file system.
|
||||
*
|
||||
* @param {DownloadRequest} downloadRequest - The request to download the file.
|
||||
* @param {NetworkConfig} network - Optional object to specify proxy/whether to ignore SSL certificates.
|
||||
*
|
||||
* @returns {Promise<any>} A promise that resolves when the file is downloaded.
|
||||
*/
|
||||
const downloadFile: (
|
||||
downloadRequest: DownloadRequest,
|
||||
network?: NetworkConfig
|
||||
) => Promise<any> = (downloadRequest, network) =>
|
||||
globalThis.core?.api?.downloadFile(downloadRequest, network)
|
||||
|
||||
/**
|
||||
* Aborts the download of a specific file.
|
||||
* @param {string} fileName - The name of the file whose download is to be aborted.
|
||||
* @returns {Promise<any>} A promise that resolves when the download has been aborted.
|
||||
*/
|
||||
const abortDownload: (fileName: string) => Promise<any> = (fileName) =>
|
||||
globalThis.core.api?.abortDownload(fileName)
|
||||
|
||||
/**
|
||||
* Gets Jan's data folder path.
|
||||
*
|
||||
* @returns {Promise<string>} A Promise that resolves with Jan's data folder path.
|
||||
*/
|
||||
const getJanDataFolderPath = (): Promise<string> =>
|
||||
globalThis.core.api?.getJanDataFolderPath()
|
||||
const getJanDataFolderPath = (): Promise<string> => globalThis.core.api?.getJanDataFolderPath()
|
||||
|
||||
/**
|
||||
* Opens the file explorer at a specific path.
|
||||
@ -72,16 +44,14 @@ const joinPath: (paths: string[]) => Promise<string> = (paths) =>
|
||||
* @param path - The file path to retrieve dirname.
|
||||
* @returns {Promise<string>} A promise that resolves the dirname.
|
||||
*/
|
||||
const dirName: (path: string) => Promise<string> = (path) =>
|
||||
globalThis.core.api?.dirName(path)
|
||||
const dirName: (path: string) => Promise<string> = (path) => globalThis.core.api?.dirName(path)
|
||||
|
||||
/**
|
||||
* Retrieve the basename from an url.
|
||||
* @param path - The path to retrieve.
|
||||
* @returns {Promise<string>} A promise that resolves with the basename.
|
||||
*/
|
||||
const baseName: (paths: string) => Promise<string> = (path) =>
|
||||
globalThis.core.api?.baseName(path)
|
||||
const baseName: (paths: string) => Promise<string> = (path) => globalThis.core.api?.baseName(path)
|
||||
|
||||
/**
|
||||
* Opens an external URL in the default web browser.
|
||||
@ -97,15 +67,13 @@ const openExternalUrl: (url: string) => Promise<any> = (url) =>
|
||||
*
|
||||
* @returns {Promise<string>} - A promise that resolves with the resource path.
|
||||
*/
|
||||
const getResourcePath: () => Promise<string> = () =>
|
||||
globalThis.core.api?.getResourcePath()
|
||||
const getResourcePath: () => Promise<string> = () => globalThis.core.api?.getResourcePath()
|
||||
|
||||
/**
|
||||
* Gets the user's home path.
|
||||
* @returns return user's home path
|
||||
*/
|
||||
const getUserHomePath = (): Promise<string> =>
|
||||
globalThis.core.api?.getUserHomePath()
|
||||
const getUserHomePath = (): Promise<string> => globalThis.core.api?.getUserHomePath()
|
||||
|
||||
/**
|
||||
* Log to file from browser processes.
|
||||
@ -123,10 +91,8 @@ const log: (message: string, fileName?: string) => void = (message, fileName) =>
|
||||
*
|
||||
* @returns {Promise<boolean>} - A promise that resolves with a boolean indicating whether the path is a subdirectory.
|
||||
*/
|
||||
const isSubdirectory: (from: string, to: string) => Promise<boolean> = (
|
||||
from: string,
|
||||
to: string
|
||||
) => globalThis.core.api?.isSubdirectory(from, to)
|
||||
const isSubdirectory: (from: string, to: string) => Promise<boolean> = (from: string, to: string) =>
|
||||
globalThis.core.api?.isSubdirectory(from, to)
|
||||
|
||||
/**
|
||||
* Get system information
|
||||
@ -159,8 +125,6 @@ export type RegisterExtensionPoint = (
|
||||
*/
|
||||
export {
|
||||
executeOnMain,
|
||||
downloadFile,
|
||||
abortDownload,
|
||||
getJanDataFolderPath,
|
||||
openFileExplorer,
|
||||
getResourcePath,
|
||||
|
||||
@ -39,11 +39,6 @@ describe('BaseExtension', () => {
|
||||
expect(baseExtension.onUnload).toBeDefined()
|
||||
})
|
||||
|
||||
it('should have installationState() return "NotRequired"', async () => {
|
||||
const installationState = await baseExtension.installationState()
|
||||
expect(installationState).toBe('NotRequired')
|
||||
})
|
||||
|
||||
it('should install the extension', async () => {
|
||||
await baseExtension.install()
|
||||
// Add your assertions here
|
||||
@ -84,11 +79,6 @@ describe('BaseExtension', () => {
|
||||
expect(baseExtension.onUnload).toBeDefined()
|
||||
})
|
||||
|
||||
it('should have installationState() return "NotRequired"', async () => {
|
||||
const installationState = await baseExtension.installationState()
|
||||
expect(installationState).toBe('NotRequired')
|
||||
})
|
||||
|
||||
it('should install the extension', async () => {
|
||||
await baseExtension.install()
|
||||
// Add your assertions here
|
||||
|
||||
@ -24,17 +24,6 @@ export interface Compatibility {
|
||||
version: string
|
||||
}
|
||||
|
||||
const ALL_INSTALLATION_STATE = [
|
||||
'NotRequired', // not required.
|
||||
'Installed', // require and installed. Good to go.
|
||||
'NotInstalled', // require to be installed.
|
||||
'Corrupted', // require but corrupted. Need to redownload.
|
||||
'NotCompatible', // require but not compatible.
|
||||
] as const
|
||||
|
||||
export type InstallationStateTuple = typeof ALL_INSTALLATION_STATE
|
||||
export type InstallationState = InstallationStateTuple[number]
|
||||
|
||||
/**
|
||||
* Represents a base extension.
|
||||
* This class should be extended by any class that represents an extension.
|
||||
@ -175,15 +164,6 @@ export abstract class BaseExtension implements ExtensionType {
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the prerequisites for the extension are installed.
|
||||
*
|
||||
* @returns {boolean} true if the prerequisites are installed, false otherwise.
|
||||
*/
|
||||
async installationState(): Promise<InstallationState> {
|
||||
return 'NotRequired'
|
||||
}
|
||||
|
||||
/**
|
||||
* Install the prerequisites for the extension.
|
||||
*
|
||||
|
||||
@ -55,17 +55,23 @@ const unlinkSync = (...args: any[]) => globalThis.core.api?.unlinkSync(...args)
|
||||
*/
|
||||
const appendFileSync = (...args: any[]) => globalThis.core.api?.appendFileSync(...args)
|
||||
|
||||
/**
|
||||
* Copies a file from the source path to the destination path.
|
||||
* @param src
|
||||
* @param dest
|
||||
* @returns
|
||||
*/
|
||||
const copyFile: (src: string, dest: string) => Promise<void> = (src, dest) =>
|
||||
globalThis.core.api?.copyFile(src, dest)
|
||||
|
||||
/**
|
||||
* Gets the list of gguf files in a directory
|
||||
*
|
||||
*
|
||||
* @param path - The paths to the file.
|
||||
* @returns {Promise<{any}>} - A promise that resolves with the list of gguf and non-gguf files
|
||||
*/
|
||||
const getGgufFiles: (paths: string[]) => Promise<any> = (
|
||||
paths) => globalThis.core.api?.getGgufFiles(paths)
|
||||
const getGgufFiles: (paths: string[]) => Promise<any> = (paths) =>
|
||||
globalThis.core.api?.getGgufFiles(paths)
|
||||
|
||||
/**
|
||||
* Gets the file's stats.
|
||||
|
||||
@ -1,25 +1,21 @@
|
||||
import {
|
||||
AppRoute,
|
||||
DownloadRoute,
|
||||
ExtensionRoute,
|
||||
FileManagerRoute,
|
||||
FileSystemRoute,
|
||||
} from '../../../types/api'
|
||||
import { Downloader } from '../processors/download'
|
||||
import { FileSystem } from '../processors/fs'
|
||||
import { Extension } from '../processors/extension'
|
||||
import { FSExt } from '../processors/fsExt'
|
||||
import { App } from '../processors/app'
|
||||
|
||||
export class RequestAdapter {
|
||||
downloader: Downloader
|
||||
fileSystem: FileSystem
|
||||
extension: Extension
|
||||
fsExt: FSExt
|
||||
app: App
|
||||
|
||||
constructor(observer?: Function) {
|
||||
this.downloader = new Downloader(observer)
|
||||
this.fileSystem = new FileSystem()
|
||||
this.extension = new Extension()
|
||||
this.fsExt = new FSExt()
|
||||
@ -28,9 +24,7 @@ export class RequestAdapter {
|
||||
|
||||
// TODO: Clearer Factory pattern here
|
||||
process(route: string, ...args: any) {
|
||||
if (route in DownloadRoute) {
|
||||
return this.downloader.process(route, ...args)
|
||||
} else if (route in FileSystemRoute) {
|
||||
if (route in FileSystemRoute) {
|
||||
return this.fileSystem.process(route, ...args)
|
||||
} else if (route in ExtensionRoute) {
|
||||
return this.extension.process(route, ...args)
|
||||
|
||||
@ -1,125 +0,0 @@
|
||||
import { Downloader } from './download'
|
||||
import { DownloadEvent } from '../../../types/api'
|
||||
import { DownloadManager } from '../../helper/download'
|
||||
|
||||
jest.mock('../../helper', () => ({
|
||||
getJanDataFolderPath: jest.fn().mockReturnValue('path/to/folder'),
|
||||
}))
|
||||
|
||||
jest.mock('../../helper/path', () => ({
|
||||
validatePath: jest.fn().mockReturnValue('path/to/folder'),
|
||||
normalizeFilePath: () =>
|
||||
process.platform === 'win32' ? 'C:\\Users\\path\\to\\file.gguf' : '/Users/path/to/file.gguf',
|
||||
}))
|
||||
|
||||
jest.mock(
|
||||
'request',
|
||||
jest.fn().mockReturnValue(() => ({
|
||||
on: jest.fn(),
|
||||
}))
|
||||
)
|
||||
|
||||
jest.mock('fs', () => ({
|
||||
createWriteStream: jest.fn(),
|
||||
}))
|
||||
|
||||
const requestMock = jest.fn((options, callback) => {
|
||||
callback(new Error('Test error'), null)
|
||||
})
|
||||
jest.mock('request', () => requestMock)
|
||||
|
||||
jest.mock('request-progress', () => {
|
||||
return jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
on: jest.fn().mockImplementation((event, callback) => {
|
||||
if (event === 'error') {
|
||||
callback(new Error('Download failed'))
|
||||
}
|
||||
return {
|
||||
on: jest.fn().mockImplementation((event, callback) => {
|
||||
if (event === 'error') {
|
||||
callback(new Error('Download failed'))
|
||||
}
|
||||
return {
|
||||
on: jest.fn().mockImplementation((event, callback) => {
|
||||
if (event === 'error') {
|
||||
callback(new Error('Download failed'))
|
||||
}
|
||||
return { pipe: jest.fn() }
|
||||
}),
|
||||
}
|
||||
}),
|
||||
}
|
||||
}),
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('Downloader', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks()
|
||||
})
|
||||
|
||||
it('should pause download correctly', () => {
|
||||
const observer = jest.fn()
|
||||
const fileName = process.platform === 'win32' ? 'C:\\path\\to\\file' : 'path/to/file'
|
||||
|
||||
const downloader = new Downloader(observer)
|
||||
const pauseMock = jest.fn()
|
||||
DownloadManager.instance.networkRequests[fileName] = { pause: pauseMock }
|
||||
|
||||
downloader.pauseDownload(observer, fileName)
|
||||
|
||||
expect(pauseMock).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should resume download correctly', () => {
|
||||
const observer = jest.fn()
|
||||
const fileName = process.platform === 'win32' ? 'C:\\path\\to\\file' : 'path/to/file'
|
||||
|
||||
const downloader = new Downloader(observer)
|
||||
const resumeMock = jest.fn()
|
||||
DownloadManager.instance.networkRequests[fileName] = { resume: resumeMock }
|
||||
|
||||
downloader.resumeDownload(observer, fileName)
|
||||
|
||||
expect(resumeMock).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle aborting a download correctly', () => {
|
||||
const observer = jest.fn()
|
||||
const fileName = process.platform === 'win32' ? 'C:\\path\\to\\file' : 'path/to/file'
|
||||
|
||||
const downloader = new Downloader(observer)
|
||||
const abortMock = jest.fn()
|
||||
DownloadManager.instance.networkRequests[fileName] = { abort: abortMock }
|
||||
|
||||
downloader.abortDownload(observer, fileName)
|
||||
|
||||
expect(abortMock).toHaveBeenCalled()
|
||||
expect(observer).toHaveBeenCalledWith(
|
||||
DownloadEvent.onFileDownloadError,
|
||||
expect.objectContaining({
|
||||
error: 'aborted',
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle download fail correctly', () => {
|
||||
const observer = jest.fn()
|
||||
const fileName = process.platform === 'win32' ? 'C:\\path\\to\\file' : 'path/to/file.gguf'
|
||||
|
||||
const downloader = new Downloader(observer)
|
||||
|
||||
downloader.downloadFile(observer, {
|
||||
localPath: fileName,
|
||||
url: 'http://127.0.0.1',
|
||||
})
|
||||
expect(observer).toHaveBeenCalledWith(
|
||||
DownloadEvent.onFileDownloadError,
|
||||
expect.objectContaining({
|
||||
error: expect.anything(),
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
@ -1,138 +0,0 @@
|
||||
import { resolve, sep } from 'path'
|
||||
import { DownloadEvent } from '../../../types/api'
|
||||
import { normalizeFilePath } from '../../helper/path'
|
||||
import { getJanDataFolderPath } from '../../helper'
|
||||
import { DownloadManager } from '../../helper/download'
|
||||
import { createWriteStream, renameSync } from 'fs'
|
||||
import { Processor } from './Processor'
|
||||
import { DownloadRequest, DownloadState, NetworkConfig } from '../../../types'
|
||||
|
||||
export class Downloader implements Processor {
|
||||
observer?: Function
|
||||
|
||||
constructor(observer?: Function) {
|
||||
this.observer = observer
|
||||
}
|
||||
|
||||
process(key: string, ...args: any[]): any {
|
||||
const instance = this as any
|
||||
const func = instance[key]
|
||||
return func(this.observer, ...args)
|
||||
}
|
||||
|
||||
downloadFile(observer: any, downloadRequest: DownloadRequest, network?: NetworkConfig) {
|
||||
const request = require('request')
|
||||
const progress = require('request-progress')
|
||||
|
||||
const strictSSL = !network?.ignoreSSL
|
||||
const proxy = network?.proxy?.startsWith('http') ? network.proxy : undefined
|
||||
|
||||
const { localPath, url } = downloadRequest
|
||||
let normalizedPath = localPath
|
||||
if (typeof localPath === 'string') {
|
||||
normalizedPath = normalizeFilePath(localPath)
|
||||
}
|
||||
const array = normalizedPath.split(sep)
|
||||
const fileName = array.pop() ?? ''
|
||||
const modelId = downloadRequest.modelId ?? array.pop() ?? ''
|
||||
|
||||
const destination = resolve(getJanDataFolderPath(), normalizedPath)
|
||||
const rq = request({ url, strictSSL, proxy })
|
||||
|
||||
// Put request to download manager instance
|
||||
DownloadManager.instance.setRequest(normalizedPath, rq)
|
||||
|
||||
// Downloading file to a temp file first
|
||||
const downloadingTempFile = `${destination}.download`
|
||||
|
||||
// adding initial download state
|
||||
const initialDownloadState: DownloadState = {
|
||||
modelId,
|
||||
fileName,
|
||||
percent: 0,
|
||||
size: {
|
||||
total: 0,
|
||||
transferred: 0,
|
||||
},
|
||||
children: [],
|
||||
downloadState: 'downloading',
|
||||
extensionId: downloadRequest.extensionId,
|
||||
downloadType: downloadRequest.downloadType,
|
||||
localPath: normalizedPath,
|
||||
}
|
||||
DownloadManager.instance.downloadProgressMap[modelId] = initialDownloadState
|
||||
DownloadManager.instance.downloadInfo[normalizedPath] = initialDownloadState
|
||||
|
||||
if (downloadRequest.downloadType === 'extension') {
|
||||
observer?.(DownloadEvent.onFileDownloadUpdate, initialDownloadState)
|
||||
}
|
||||
|
||||
progress(rq, {})
|
||||
.on('progress', (state: any) => {
|
||||
const currentDownloadState = DownloadManager.instance.downloadProgressMap[modelId]
|
||||
const downloadState: DownloadState = {
|
||||
...currentDownloadState,
|
||||
...state,
|
||||
fileName: fileName,
|
||||
downloadState: 'downloading',
|
||||
}
|
||||
console.debug('progress: ', downloadState)
|
||||
observer?.(DownloadEvent.onFileDownloadUpdate, downloadState)
|
||||
DownloadManager.instance.downloadProgressMap[modelId] = downloadState
|
||||
})
|
||||
.on('error', (error: Error) => {
|
||||
const currentDownloadState = DownloadManager.instance.downloadProgressMap[modelId]
|
||||
const downloadState: DownloadState = {
|
||||
...currentDownloadState,
|
||||
fileName: fileName,
|
||||
error: error.message,
|
||||
downloadState: 'error',
|
||||
}
|
||||
|
||||
observer?.(DownloadEvent.onFileDownloadError, downloadState)
|
||||
DownloadManager.instance.downloadProgressMap[modelId] = downloadState
|
||||
})
|
||||
.on('end', () => {
|
||||
const currentDownloadState = DownloadManager.instance.downloadProgressMap[modelId]
|
||||
if (
|
||||
currentDownloadState &&
|
||||
DownloadManager.instance.networkRequests[normalizedPath] &&
|
||||
DownloadManager.instance.downloadProgressMap[modelId]?.downloadState !== 'error'
|
||||
) {
|
||||
// Finished downloading, rename temp file to actual file
|
||||
renameSync(downloadingTempFile, destination)
|
||||
const downloadState: DownloadState = {
|
||||
...currentDownloadState,
|
||||
fileName: fileName,
|
||||
downloadState: 'end',
|
||||
}
|
||||
observer?.(DownloadEvent.onFileDownloadSuccess, downloadState)
|
||||
DownloadManager.instance.downloadProgressMap[modelId] = downloadState
|
||||
}
|
||||
})
|
||||
.pipe(createWriteStream(downloadingTempFile))
|
||||
}
|
||||
|
||||
abortDownload(observer: any, fileName: string) {
|
||||
const rq = DownloadManager.instance.networkRequests[fileName]
|
||||
if (rq) {
|
||||
DownloadManager.instance.networkRequests[fileName] = undefined
|
||||
rq?.abort()
|
||||
}
|
||||
|
||||
const downloadInfo = DownloadManager.instance.downloadInfo[fileName]
|
||||
observer?.(DownloadEvent.onFileDownloadError, {
|
||||
...downloadInfo,
|
||||
fileName,
|
||||
error: 'aborted',
|
||||
})
|
||||
}
|
||||
|
||||
resumeDownload(_observer: any, fileName: any) {
|
||||
DownloadManager.instance.networkRequests[fileName]?.resume()
|
||||
}
|
||||
|
||||
pauseDownload(_observer: any, fileName: any) {
|
||||
DownloadManager.instance.networkRequests[fileName]?.pause()
|
||||
}
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
import { DownloadManager } from './download';
|
||||
|
||||
it('should set a network request for a specific file', () => {
|
||||
const downloadManager = new DownloadManager();
|
||||
const fileName = 'testFile';
|
||||
const request = { url: 'http://example.com' };
|
||||
|
||||
downloadManager.setRequest(fileName, request);
|
||||
|
||||
expect(downloadManager.networkRequests[fileName]).toEqual(request);
|
||||
});
|
||||
@ -1,30 +0,0 @@
|
||||
import { DownloadState } from '../../types'
|
||||
|
||||
/**
|
||||
* Manages file downloads and network requests.
|
||||
*/
|
||||
export class DownloadManager {
|
||||
public networkRequests: Record<string, any> = {}
|
||||
|
||||
public static instance: DownloadManager = new DownloadManager()
|
||||
|
||||
// store the download information with key is model id
|
||||
public downloadProgressMap: Record<string, DownloadState> = {}
|
||||
|
||||
// store the download information with key is normalized file path
|
||||
public downloadInfo: Record<string, DownloadState> = {}
|
||||
|
||||
constructor() {
|
||||
if (DownloadManager.instance) {
|
||||
return DownloadManager.instance
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Sets a network request for a specific file.
|
||||
* @param {string} fileName - The name of the file.
|
||||
* @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
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,4 @@
|
||||
export * from './config'
|
||||
export * from './download'
|
||||
export * from './logger'
|
||||
export * from './module'
|
||||
export * from './path'
|
||||
|
||||
@ -65,30 +65,13 @@ export enum AppEvent {
|
||||
onMainViewStateChange = 'onMainViewStateChange',
|
||||
}
|
||||
|
||||
export enum DownloadRoute {
|
||||
abortDownload = 'abortDownload',
|
||||
downloadFile = 'downloadFile',
|
||||
pauseDownload = 'pauseDownload',
|
||||
resumeDownload = 'resumeDownload',
|
||||
getDownloadProgress = 'getDownloadProgress',
|
||||
}
|
||||
|
||||
export enum DownloadEvent {
|
||||
onFileDownloadUpdate = 'onFileDownloadUpdate',
|
||||
onFileDownloadError = 'onFileDownloadError',
|
||||
onFileDownloadSuccess = 'onFileDownloadSuccess',
|
||||
onFileDownloadStopped = 'onFileDownloadStopped',
|
||||
onFileDownloadStarted = 'onFileDownloadStarted',
|
||||
onFileUnzipSuccess = 'onFileUnzipSuccess',
|
||||
}
|
||||
|
||||
export enum LocalImportModelEvent {
|
||||
onLocalImportModelUpdate = 'onLocalImportModelUpdate',
|
||||
onLocalImportModelFailed = 'onLocalImportModelFailed',
|
||||
onLocalImportModelSuccess = 'onLocalImportModelSuccess',
|
||||
onLocalImportModelFinished = 'onLocalImportModelFinished',
|
||||
}
|
||||
|
||||
export enum ExtensionRoute {
|
||||
baseExtensions = 'baseExtensions',
|
||||
getActiveExtensions = 'getActiveExtensions',
|
||||
@ -131,10 +114,6 @@ export type AppEventFunctions = {
|
||||
[K in AppEvent]: ApiFunction
|
||||
}
|
||||
|
||||
export type DownloadRouteFunctions = {
|
||||
[K in DownloadRoute]: ApiFunction
|
||||
}
|
||||
|
||||
export type DownloadEventFunctions = {
|
||||
[K in DownloadEvent]: ApiFunction
|
||||
}
|
||||
@ -154,7 +133,6 @@ export type FileManagerRouteFunctions = {
|
||||
export type APIFunctions = NativeRouteFunctions &
|
||||
AppRouteFunctions &
|
||||
AppEventFunctions &
|
||||
DownloadRouteFunctions &
|
||||
DownloadEventFunctions &
|
||||
ExtensionRouteFunctions &
|
||||
FileSystemRouteFunctions &
|
||||
@ -162,7 +140,6 @@ export type APIFunctions = NativeRouteFunctions &
|
||||
|
||||
export const CoreRoutes = [
|
||||
...Object.values(AppRoute),
|
||||
...Object.values(DownloadRoute),
|
||||
...Object.values(ExtensionRoute),
|
||||
...Object.values(FileSystemRoute),
|
||||
...Object.values(FileManagerRoute),
|
||||
@ -172,7 +149,6 @@ export const APIRoutes = [...CoreRoutes, ...Object.values(NativeRoute)]
|
||||
export const APIEvents = [
|
||||
...Object.values(AppEvent),
|
||||
...Object.values(DownloadEvent),
|
||||
...Object.values(LocalImportModelEvent),
|
||||
]
|
||||
export type PayloadType = {
|
||||
messages: ChatCompletionMessage[]
|
||||
|
||||
@ -16,41 +16,9 @@ export type DownloadState = {
|
||||
|
||||
error?: string
|
||||
extensionId?: string
|
||||
downloadType?: DownloadType | string
|
||||
localPath?: string
|
||||
}
|
||||
|
||||
export type DownloadType = 'model' | 'extension'
|
||||
|
||||
export type DownloadRequest = {
|
||||
/**
|
||||
* The URL to download the file from.
|
||||
*/
|
||||
url: string
|
||||
|
||||
/**
|
||||
* The local path to save the file to.
|
||||
*/
|
||||
localPath: string
|
||||
|
||||
/**
|
||||
* The extension ID of the extension that initiated the download.
|
||||
*
|
||||
* Can be extension name.
|
||||
*/
|
||||
extensionId?: string
|
||||
|
||||
/**
|
||||
* The model ID of the model that initiated the download.
|
||||
*/
|
||||
modelId?: string
|
||||
|
||||
/**
|
||||
* The download type.
|
||||
*/
|
||||
downloadType?: DownloadType | string
|
||||
}
|
||||
|
||||
type DownloadTime = {
|
||||
elapsed: number
|
||||
remaining: number
|
||||
@ -60,7 +28,6 @@ type DownloadSize = {
|
||||
total: number
|
||||
transferred: number
|
||||
}
|
||||
|
||||
/**
|
||||
* The file metadata
|
||||
*/
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
export type FileDownloadRequest = {
|
||||
downloadId: string
|
||||
url: string
|
||||
localPath: string
|
||||
fileName: string
|
||||
displayName: string
|
||||
metadata: Record<string, string | number>
|
||||
}
|
||||
@ -1,6 +1,4 @@
|
||||
export * from './systemResourceInfo'
|
||||
export * from './promptTemplate'
|
||||
export * from './appUpdate'
|
||||
export * from './fileDownloadRequest'
|
||||
export * from './networkConfig'
|
||||
export * from './selectFiles'
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
export type NetworkConfig = {
|
||||
proxy?: string
|
||||
ignoreSSL?: boolean
|
||||
}
|
||||
@ -1,3 +1,2 @@
|
||||
export * from './threadEntity'
|
||||
export * from './threadInterface'
|
||||
export * from './threadEvent'
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
|
||||
import { ThreadEvent } from './threadEvent';
|
||||
|
||||
it('should have the correct values', () => {
|
||||
expect(ThreadEvent.OnThreadStarted).toBe('OnThreadStarted');
|
||||
});
|
||||
@ -1,4 +0,0 @@
|
||||
export enum ThreadEvent {
|
||||
/** The `OnThreadStarted` event is emitted when a thread is started. */
|
||||
OnThreadStarted = 'OnThreadStarted',
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jan",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.1740752217",
|
||||
"main": "./build/main.js",
|
||||
"author": "Jan <service@jan.ai>",
|
||||
"license": "MIT",
|
||||
@ -113,7 +113,6 @@
|
||||
"electron-store": "^8.1.0",
|
||||
"electron-updater": "^6.1.7",
|
||||
"fs-extra": "^11.2.0",
|
||||
"node-fetch": "2",
|
||||
"pacote": "^21.0.0",
|
||||
"request": "^2.88.2",
|
||||
"request-progress": "^3.0.0",
|
||||
|
||||
@ -5,11 +5,6 @@ import {
|
||||
joinPath,
|
||||
dirName,
|
||||
fs,
|
||||
ModelManager,
|
||||
abortDownload,
|
||||
DownloadState,
|
||||
events,
|
||||
DownloadEvent,
|
||||
OptionType,
|
||||
ModelSource,
|
||||
extractInferenceParams,
|
||||
@ -55,9 +50,6 @@ export default class JanModelExtension extends ModelExtension {
|
||||
this.updateCortexConfig({ huggingface_token: huggingfaceToken })
|
||||
}
|
||||
|
||||
// Listen to app download events
|
||||
this.handleDesktopEvents()
|
||||
|
||||
// Sync with cortexsohub
|
||||
this.fetchCortexsoModels()
|
||||
}
|
||||
@ -107,21 +99,6 @@ export default class JanModelExtension extends ModelExtension {
|
||||
* @returns {Promise<void>} A promise that resolves when the download has been cancelled.
|
||||
*/
|
||||
async cancelModelPull(model: string): Promise<void> {
|
||||
if (model) {
|
||||
const modelDto: Model = ModelManager.instance().get(model)
|
||||
// Clip vision model - should not be handled by cortex.cpp
|
||||
// TensorRT model - should not be handled by cortex.cpp
|
||||
if (
|
||||
modelDto &&
|
||||
(modelDto.engine === InferenceEngine.nitro_tensorrt_llm ||
|
||||
modelDto.settings.vision_model)
|
||||
) {
|
||||
for (const source of modelDto.sources) {
|
||||
const path = await joinPath(['models', modelDto.id, source.filename])
|
||||
await abortDownload(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Sending DELETE to /models/pull/{id} endpoint to cancel a model pull
|
||||
*/
|
||||
@ -362,32 +339,6 @@ export default class JanModelExtension extends ModelExtension {
|
||||
// END: - Public API
|
||||
|
||||
// BEGIN: - Private API
|
||||
/**
|
||||
* Handle download state from main app
|
||||
*/
|
||||
private handleDesktopEvents() {
|
||||
if (window && window.electronAPI) {
|
||||
window.electronAPI.onFileDownloadUpdate(
|
||||
async (_event: string, state: DownloadState | undefined) => {
|
||||
if (!state) return
|
||||
state.downloadState = 'downloading'
|
||||
events.emit(DownloadEvent.onFileDownloadUpdate, state)
|
||||
}
|
||||
)
|
||||
window.electronAPI.onFileDownloadError(
|
||||
async (_event: string, state: DownloadState) => {
|
||||
state.downloadState = 'error'
|
||||
events.emit(DownloadEvent.onFileDownloadError, state)
|
||||
}
|
||||
)
|
||||
window.electronAPI.onFileDownloadSuccess(
|
||||
async (_event: string, state: DownloadState) => {
|
||||
state.downloadState = 'end'
|
||||
events.emit(DownloadEvent.onFileDownloadSuccess, state)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform model to the expected format (e.g. parameters, settings, metadata)
|
||||
|
||||
@ -1,67 +0,0 @@
|
||||
import {
|
||||
downloadFile,
|
||||
DownloadRequest,
|
||||
fs,
|
||||
joinPath,
|
||||
Model,
|
||||
} from '@janhq/core'
|
||||
|
||||
export const downloadModel = async (
|
||||
model: Model,
|
||||
network?: { ignoreSSL?: boolean; proxy?: string }
|
||||
): Promise<void> => {
|
||||
const homedir = 'file://models'
|
||||
const supportedGpuArch = ['ampere', 'ada']
|
||||
// Create corresponding directory
|
||||
const modelDirPath = await joinPath([homedir, model.id])
|
||||
if (!(await fs.existsSync(modelDirPath))) await fs.mkdir(modelDirPath)
|
||||
|
||||
const jsonFilePath = await joinPath([modelDirPath, 'model.json'])
|
||||
// Write model.json on download
|
||||
if (!(await fs.existsSync(jsonFilePath)))
|
||||
await fs.writeFileSync(
|
||||
jsonFilePath,
|
||||
JSON.stringify(model, null, 2)
|
||||
)
|
||||
|
||||
console.debug(`Download sources: ${JSON.stringify(model.sources)}`)
|
||||
|
||||
if (model.sources.length > 1) {
|
||||
// path to model binaries
|
||||
for (const source of model.sources) {
|
||||
let path = extractFileName(source.url, '.gguf')
|
||||
if (source.filename) {
|
||||
path = await joinPath([modelDirPath, source.filename])
|
||||
}
|
||||
|
||||
const downloadRequest: DownloadRequest = {
|
||||
url: source.url,
|
||||
localPath: path,
|
||||
modelId: model.id,
|
||||
}
|
||||
downloadFile(downloadRequest, network)
|
||||
}
|
||||
} else {
|
||||
const fileName = extractFileName(model.sources[0]?.url, '.gguf')
|
||||
const path = await joinPath([modelDirPath, fileName])
|
||||
const downloadRequest: DownloadRequest = {
|
||||
url: model.sources[0]?.url,
|
||||
localPath: path,
|
||||
modelId: model.id,
|
||||
}
|
||||
downloadFile(downloadRequest, network)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* try to retrieve the download file name from the source url
|
||||
*/
|
||||
function extractFileName(url: string, fileExtension: string): string {
|
||||
if (!url) return fileExtension
|
||||
|
||||
const extractedFileName = url.split('/').pop()
|
||||
const fileName = extractedFileName.toLowerCase().endsWith(fileExtension)
|
||||
? extractedFileName
|
||||
: extractedFileName + fileExtension
|
||||
return fileName
|
||||
}
|
||||
@ -1,79 +0,0 @@
|
||||
import { useCallback, useEffect } from 'react'
|
||||
|
||||
import { abortDownload } from '@janhq/core'
|
||||
import { Button, Modal, Progress } from '@janhq/joi'
|
||||
import { atom, useAtom, useAtomValue } from 'jotai'
|
||||
|
||||
import {
|
||||
formatDownloadPercentage,
|
||||
formatExtensionsName,
|
||||
} from '@/utils/converter'
|
||||
|
||||
import {
|
||||
InstallingExtensionState,
|
||||
installingExtensionAtom,
|
||||
} from '@/helpers/atoms/Extension.atom'
|
||||
|
||||
export const showInstallingExtensionModalAtom = atom(false)
|
||||
|
||||
const InstallingExtensionModal = () => {
|
||||
const [showInstallingExtensionModal, setShowInstallingExtensionModal] =
|
||||
useAtom(showInstallingExtensionModalAtom)
|
||||
const installingExtensions = useAtomValue(installingExtensionAtom)
|
||||
|
||||
useEffect(() => {
|
||||
if (installingExtensions.length === 0) {
|
||||
setShowInstallingExtensionModal(false)
|
||||
}
|
||||
}, [installingExtensions, setShowInstallingExtensionModal])
|
||||
|
||||
const onAbortInstallingExtensionClick = useCallback(
|
||||
(item: InstallingExtensionState) => {
|
||||
if (item.localPath) {
|
||||
abortDownload(item.localPath)
|
||||
}
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Installing Extension"
|
||||
open={showInstallingExtensionModal}
|
||||
onOpenChange={() => setShowInstallingExtensionModal(false)}
|
||||
content={
|
||||
<div>
|
||||
{Object.values(installingExtensions).map((item) => (
|
||||
<div className="pt-2" key={item.extensionId}>
|
||||
<Progress
|
||||
className="mb-2"
|
||||
value={
|
||||
formatDownloadPercentage(item.percentage, {
|
||||
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">
|
||||
{formatExtensionsName(item.extensionId)}
|
||||
</p>
|
||||
<span>{formatDownloadPercentage(item.percentage)}</span>
|
||||
</div>
|
||||
<Button
|
||||
theme="ghost"
|
||||
size="small"
|
||||
onClick={() => onAbortInstallingExtensionClick(item)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default InstallingExtensionModal
|
||||
@ -1,52 +0,0 @@
|
||||
import { Fragment, useCallback } from 'react'
|
||||
|
||||
import { Progress } from '@janhq/joi'
|
||||
import { useAtomValue, useSetAtom } from 'jotai'
|
||||
|
||||
import { showInstallingExtensionModalAtom } from './InstallingExtensionModal'
|
||||
|
||||
import { installingExtensionAtom } from '@/helpers/atoms/Extension.atom'
|
||||
|
||||
const InstallingExtension = () => {
|
||||
const installingExtensions = useAtomValue(installingExtensionAtom)
|
||||
const setShowInstallingExtensionModal = useSetAtom(
|
||||
showInstallingExtensionModalAtom
|
||||
)
|
||||
const shouldShowInstalling = installingExtensions.length > 0
|
||||
|
||||
let totalPercentage = 0
|
||||
let totalExtensions = 0
|
||||
for (const installation of installingExtensions) {
|
||||
totalPercentage += installation.percentage
|
||||
totalExtensions++
|
||||
}
|
||||
const progress = (totalPercentage / totalExtensions) * 100
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
setShowInstallingExtensionModal(true)
|
||||
}, [setShowInstallingExtensionModal])
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{shouldShowInstalling ? (
|
||||
<div
|
||||
className="flex cursor-pointer flex-row items-center space-x-2"
|
||||
onClick={onClick}
|
||||
>
|
||||
<p className="font-medium text-[hsla(var(--text-secondary))]">
|
||||
Installing Additional Dependencies
|
||||
</p>
|
||||
|
||||
<div className="flex flex-row items-center justify-center space-x-2 rounded-md px-2 py-[2px]">
|
||||
<Progress size="small" className="w-20" value={progress} />
|
||||
<span className=" font-medium text-[hsla(var(--primary-bg))]">
|
||||
{progress.toFixed(2)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
export default InstallingExtension
|
||||
@ -7,7 +7,6 @@ import { twMerge } from 'tailwind-merge'
|
||||
import DownloadingState from './DownloadingState'
|
||||
|
||||
import ImportingModelState from './ImportingModelState'
|
||||
import InstallingExtension from './InstallingExtension'
|
||||
import SystemMonitor from './SystemMonitor'
|
||||
import UpdateApp from './UpdateApp'
|
||||
import UpdatedFailedModal from './UpdateFailedModal'
|
||||
@ -49,7 +48,6 @@ const BottomPanel = () => {
|
||||
<ImportingModelState />
|
||||
<DownloadingState />
|
||||
<UpdatedFailedModal />
|
||||
<InstallingExtension />
|
||||
</div>
|
||||
<div className="flex items-center gap-x-1">
|
||||
<SystemMonitor />
|
||||
|
||||
@ -35,8 +35,6 @@ import ModalAppUpdaterChangelog from '../ModalAppUpdaterChangelog'
|
||||
|
||||
import ModalAppUpdaterNotAvailable from '../ModalAppUpdaterNotAvailable'
|
||||
|
||||
import InstallingExtensionModal from './BottomPanel/InstallingExtension/InstallingExtensionModal'
|
||||
|
||||
import { mainViewStateAtom } from '@/helpers/atoms/App.atom'
|
||||
import {
|
||||
productAnalyticAtom,
|
||||
@ -167,7 +165,6 @@ const BaseLayout = () => {
|
||||
{importModelStage === 'EDIT_MODEL_INFO' && <EditModelInfoModal />}
|
||||
{importModelStage === 'CONFIRM_CANCEL' && <CancelModelImportModal />}
|
||||
<ChooseWhatToImportModal />
|
||||
<InstallingExtensionModal />
|
||||
{showProductAnalyticPrompt && (
|
||||
<div className="fixed bottom-4 z-50 m-4 max-w-full rounded-xl border border-[hsla(var(--app-border))] bg-[hsla(var(--app-bg))] p-6 shadow-2xl sm:bottom-8 sm:right-4 sm:m-0 sm:max-w-[400px]">
|
||||
<div className="mb-4 flex items-center gap-x-2">
|
||||
|
||||
@ -4,8 +4,8 @@ import React from 'react'
|
||||
|
||||
import {
|
||||
DownloadEvent,
|
||||
events,
|
||||
DownloadState,
|
||||
events,
|
||||
ModelEvent,
|
||||
ExtensionTypeEnum,
|
||||
ModelExtension,
|
||||
@ -17,23 +17,15 @@ import { useSetAtom } from 'jotai'
|
||||
|
||||
import { setDownloadStateAtom } from '@/hooks/useDownloadState'
|
||||
|
||||
import { formatExtensionsName } from '@/utils/converter'
|
||||
|
||||
import { toaster } from '../Toast'
|
||||
|
||||
import AppUpdateListener from './AppUpdateListener'
|
||||
import ClipboardListener from './ClipboardListener'
|
||||
import ModelHandler from './ModelHandler'
|
||||
|
||||
import ModelImportListener from './ModelImportListener'
|
||||
import QuickAskListener from './QuickAskListener'
|
||||
|
||||
import { extensionManager } from '@/extension'
|
||||
import {
|
||||
InstallingExtensionState,
|
||||
removeInstallingExtensionAtom,
|
||||
setInstallingExtensionAtom,
|
||||
} from '@/helpers/atoms/Extension.atom'
|
||||
import {
|
||||
addDownloadingModelAtom,
|
||||
removeDownloadingModelAtom,
|
||||
@ -41,109 +33,76 @@ import {
|
||||
|
||||
const EventListener = () => {
|
||||
const setDownloadState = useSetAtom(setDownloadStateAtom)
|
||||
const setInstallingExtension = useSetAtom(setInstallingExtensionAtom)
|
||||
const removeInstallingExtension = useSetAtom(removeInstallingExtensionAtom)
|
||||
const addDownloadingModel = useSetAtom(addDownloadingModelAtom)
|
||||
const removeDownloadingModel = useSetAtom(removeDownloadingModelAtom)
|
||||
|
||||
const onFileDownloadUpdate = useCallback(
|
||||
async (state: DownloadState) => {
|
||||
console.debug('onFileDownloadUpdate', state)
|
||||
if (state.downloadType === 'extension') {
|
||||
const installingExtensionState: InstallingExtensionState = {
|
||||
extensionId: state.extensionId!,
|
||||
percentage: state.percent,
|
||||
localPath: state.localPath,
|
||||
}
|
||||
setInstallingExtension(state.extensionId!, installingExtensionState)
|
||||
} else {
|
||||
addDownloadingModel(state.modelId)
|
||||
setDownloadState(state)
|
||||
}
|
||||
addDownloadingModel(state.modelId)
|
||||
setDownloadState(state)
|
||||
},
|
||||
[addDownloadingModel, setDownloadState, setInstallingExtension]
|
||||
[addDownloadingModel, setDownloadState]
|
||||
)
|
||||
|
||||
const onFileDownloadError = useCallback(
|
||||
(state: DownloadState) => {
|
||||
console.debug('onFileDownloadError', state)
|
||||
if (state.downloadType === 'extension') {
|
||||
removeInstallingExtension(state.extensionId!)
|
||||
} else {
|
||||
state.downloadState = 'error'
|
||||
setDownloadState(state)
|
||||
removeDownloadingModel(state.modelId)
|
||||
}
|
||||
state.downloadState = 'error'
|
||||
setDownloadState(state)
|
||||
removeDownloadingModel(state.modelId)
|
||||
},
|
||||
[removeInstallingExtension, setDownloadState, removeDownloadingModel]
|
||||
[setDownloadState, removeDownloadingModel]
|
||||
)
|
||||
|
||||
const onFileDownloadStopped = useCallback(
|
||||
(state: DownloadState) => {
|
||||
console.debug('onFileDownloadError', state)
|
||||
if (state.downloadType === 'extension') {
|
||||
removeInstallingExtension(state.extensionId!)
|
||||
} else {
|
||||
state.downloadState = 'error'
|
||||
state.error = 'aborted'
|
||||
setDownloadState(state)
|
||||
removeDownloadingModel(state.modelId)
|
||||
}
|
||||
|
||||
state.downloadState = 'error'
|
||||
state.error = 'aborted'
|
||||
setDownloadState(state)
|
||||
removeDownloadingModel(state.modelId)
|
||||
},
|
||||
[removeInstallingExtension, setDownloadState, removeDownloadingModel]
|
||||
[setDownloadState, removeDownloadingModel]
|
||||
)
|
||||
|
||||
const onFileDownloadSuccess = useCallback(
|
||||
async (state: DownloadState) => {
|
||||
console.debug('onFileDownloadSuccess', state)
|
||||
if (state.downloadType !== 'extension') {
|
||||
// Update model metadata accordingly
|
||||
const model = ModelManager.instance().models.get(state.modelId)
|
||||
if (model) {
|
||||
await extensionManager
|
||||
.get<ModelExtension>(ExtensionTypeEnum.Model)
|
||||
?.updateModel({
|
||||
id: model.id,
|
||||
...model.settings,
|
||||
...model.parameters,
|
||||
} as Partial<Model>)
|
||||
.catch((e) => console.debug(e))
|
||||
|
||||
toaster({
|
||||
title: 'Download Completed',
|
||||
description: `Download ${state.modelId} completed`,
|
||||
type: 'success',
|
||||
})
|
||||
}
|
||||
state.downloadState = 'end'
|
||||
setDownloadState(state)
|
||||
removeDownloadingModel(state.modelId)
|
||||
events.emit(ModelEvent.OnModelsUpdate, { fetch: true })
|
||||
// Update model metadata accordingly
|
||||
const model = ModelManager.instance().models.get(state.modelId)
|
||||
if (model) {
|
||||
await extensionManager
|
||||
.get<ModelExtension>(ExtensionTypeEnum.Model)
|
||||
?.updateModel({
|
||||
id: model.id,
|
||||
...model.settings,
|
||||
...model.parameters,
|
||||
} as Partial<Model>)
|
||||
.catch((e) => console.debug(e))
|
||||
|
||||
toaster({
|
||||
title: 'Download Completed',
|
||||
description: `Download ${state.modelId} completed`,
|
||||
type: 'success',
|
||||
})
|
||||
}
|
||||
state.downloadState = 'end'
|
||||
setDownloadState(state)
|
||||
removeDownloadingModel(state.modelId)
|
||||
events.emit(ModelEvent.OnModelsUpdate, { fetch: true })
|
||||
},
|
||||
[removeDownloadingModel, setDownloadState]
|
||||
)
|
||||
|
||||
const onFileUnzipSuccess = useCallback(
|
||||
(state: DownloadState) => {
|
||||
console.debug('onFileUnzipSuccess', state)
|
||||
toaster({
|
||||
title: 'Success',
|
||||
description: `Install ${formatExtensionsName(state.extensionId!)} successfully.`,
|
||||
type: 'success',
|
||||
})
|
||||
removeInstallingExtension(state.extensionId!)
|
||||
},
|
||||
[removeInstallingExtension]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
console.debug('EventListenerWrapper: registering event listeners...')
|
||||
events.on(DownloadEvent.onFileDownloadUpdate, onFileDownloadUpdate)
|
||||
events.on(DownloadEvent.onFileDownloadError, onFileDownloadError)
|
||||
events.on(DownloadEvent.onFileDownloadSuccess, onFileDownloadSuccess)
|
||||
events.on(DownloadEvent.onFileDownloadStopped, onFileDownloadStopped)
|
||||
events.on(DownloadEvent.onFileUnzipSuccess, onFileUnzipSuccess)
|
||||
|
||||
return () => {
|
||||
console.debug('EventListenerWrapper: unregistering event listeners...')
|
||||
@ -151,13 +110,11 @@ const EventListener = () => {
|
||||
events.off(DownloadEvent.onFileDownloadError, onFileDownloadError)
|
||||
events.off(DownloadEvent.onFileDownloadSuccess, onFileDownloadSuccess)
|
||||
events.off(DownloadEvent.onFileDownloadStopped, onFileDownloadStopped)
|
||||
events.off(DownloadEvent.onFileUnzipSuccess, onFileUnzipSuccess)
|
||||
}
|
||||
}, [
|
||||
onFileDownloadUpdate,
|
||||
onFileDownloadError,
|
||||
onFileDownloadSuccess,
|
||||
onFileUnzipSuccess,
|
||||
onFileDownloadStopped,
|
||||
])
|
||||
|
||||
@ -165,7 +122,6 @@ const EventListener = () => {
|
||||
<>
|
||||
<AppUpdateListener />
|
||||
<ClipboardListener />
|
||||
<ModelImportListener />
|
||||
<QuickAskListener />
|
||||
<ModelHandler />
|
||||
</>
|
||||
|
||||
@ -1,109 +0,0 @@
|
||||
import { Fragment, useCallback, useEffect } from 'react'
|
||||
|
||||
import {
|
||||
ImportingModel,
|
||||
LocalImportModelEvent,
|
||||
Model,
|
||||
ModelEvent,
|
||||
events,
|
||||
} from '@janhq/core'
|
||||
import { useSetAtom } from 'jotai'
|
||||
|
||||
import { snackbar } from '../Toast'
|
||||
|
||||
import {
|
||||
setImportingModelErrorAtom,
|
||||
setImportingModelSuccessAtom,
|
||||
updateImportingModelProgressAtom,
|
||||
} from '@/helpers/atoms/Model.atom'
|
||||
|
||||
const ModelImportListener = () => {
|
||||
const updateImportingModelProgress = useSetAtom(
|
||||
updateImportingModelProgressAtom
|
||||
)
|
||||
const setImportingModelSuccess = useSetAtom(setImportingModelSuccessAtom)
|
||||
const setImportingModelFailed = useSetAtom(setImportingModelErrorAtom)
|
||||
|
||||
const onImportModelUpdate = useCallback(
|
||||
async (state: ImportingModel) => {
|
||||
if (!state.importId) return
|
||||
updateImportingModelProgress(state.importId, state.percentage ?? 0)
|
||||
},
|
||||
[updateImportingModelProgress]
|
||||
)
|
||||
|
||||
const onImportModelFailed = useCallback(
|
||||
async (state: ImportingModel) => {
|
||||
if (!state.importId) return
|
||||
setImportingModelFailed(state.importId, state.error ?? '')
|
||||
},
|
||||
[setImportingModelFailed]
|
||||
)
|
||||
|
||||
const onImportModelSuccess = useCallback(
|
||||
(state: ImportingModel) => {
|
||||
if (!state.modelId) return
|
||||
events.emit(ModelEvent.OnModelsUpdate, { fetch: true })
|
||||
setImportingModelSuccess(state.importId, state.modelId)
|
||||
},
|
||||
[setImportingModelSuccess]
|
||||
)
|
||||
|
||||
const onImportModelFinished = useCallback((importedModels: Model[]) => {
|
||||
const modelText = importedModels.length === 1 ? 'model' : 'models'
|
||||
snackbar({
|
||||
description: `Successfully imported ${importedModels.length} ${modelText}`,
|
||||
type: 'success',
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
console.debug('ModelImportListener: registering event listeners..')
|
||||
|
||||
events.on(
|
||||
LocalImportModelEvent.onLocalImportModelUpdate,
|
||||
onImportModelUpdate
|
||||
)
|
||||
events.on(
|
||||
LocalImportModelEvent.onLocalImportModelSuccess,
|
||||
onImportModelSuccess
|
||||
)
|
||||
events.on(
|
||||
LocalImportModelEvent.onLocalImportModelFinished,
|
||||
onImportModelFinished
|
||||
)
|
||||
events.on(
|
||||
LocalImportModelEvent.onLocalImportModelFailed,
|
||||
onImportModelFailed
|
||||
)
|
||||
|
||||
return () => {
|
||||
console.debug('ModelImportListener: unregistering event listeners...')
|
||||
events.off(
|
||||
LocalImportModelEvent.onLocalImportModelUpdate,
|
||||
onImportModelUpdate
|
||||
)
|
||||
events.off(
|
||||
LocalImportModelEvent.onLocalImportModelSuccess,
|
||||
onImportModelSuccess
|
||||
)
|
||||
events.off(
|
||||
LocalImportModelEvent.onLocalImportModelFinished,
|
||||
onImportModelFinished
|
||||
)
|
||||
events.off(
|
||||
LocalImportModelEvent.onLocalImportModelFailed,
|
||||
onImportModelFailed
|
||||
)
|
||||
}
|
||||
}, [
|
||||
onImportModelUpdate,
|
||||
onImportModelSuccess,
|
||||
onImportModelFinished,
|
||||
onImportModelFailed,
|
||||
])
|
||||
|
||||
return <Fragment></Fragment>
|
||||
}
|
||||
|
||||
export default ModelImportListener
|
||||
@ -9,54 +9,6 @@ describe('Extension.atom.ts', () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('installingExtensionAtom', () => {
|
||||
it('should initialize as an empty array', () => {
|
||||
const { result } = renderHook(() => useAtomValue(ExtensionAtoms.installingExtensionAtom))
|
||||
expect(result.current).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('setInstallingExtensionAtom', () => {
|
||||
it('should add a new installing extension', () => {
|
||||
const { result: setAtom } = renderHook(() => useSetAtom(ExtensionAtoms.setInstallingExtensionAtom))
|
||||
const { result: getAtom } = renderHook(() => useAtomValue(ExtensionAtoms.installingExtensionAtom))
|
||||
|
||||
act(() => {
|
||||
setAtom.current('ext1', { extensionId: 'ext1', percentage: 0 })
|
||||
})
|
||||
|
||||
expect(getAtom.current).toEqual([{ extensionId: 'ext1', percentage: 0 }])
|
||||
})
|
||||
|
||||
it('should update an existing installing extension', () => {
|
||||
const { result: setAtom } = renderHook(() => useSetAtom(ExtensionAtoms.setInstallingExtensionAtom))
|
||||
const { result: getAtom } = renderHook(() => useAtomValue(ExtensionAtoms.installingExtensionAtom))
|
||||
|
||||
act(() => {
|
||||
setAtom.current('ext1', { extensionId: 'ext1', percentage: 0 })
|
||||
setAtom.current('ext1', { extensionId: 'ext1', percentage: 50 })
|
||||
})
|
||||
|
||||
expect(getAtom.current).toEqual([{ extensionId: 'ext1', percentage: 50 }])
|
||||
})
|
||||
})
|
||||
|
||||
describe('removeInstallingExtensionAtom', () => {
|
||||
it('should remove an installing extension', () => {
|
||||
const { result: setAtom } = renderHook(() => useSetAtom(ExtensionAtoms.setInstallingExtensionAtom))
|
||||
const { result: removeAtom } = renderHook(() => useSetAtom(ExtensionAtoms.removeInstallingExtensionAtom))
|
||||
const { result: getAtom } = renderHook(() => useAtomValue(ExtensionAtoms.installingExtensionAtom))
|
||||
|
||||
act(() => {
|
||||
setAtom.current('ext1', { extensionId: 'ext1', percentage: 0 })
|
||||
setAtom.current('ext2', { extensionId: 'ext2', percentage: 50 })
|
||||
removeAtom.current('ext1')
|
||||
})
|
||||
|
||||
expect(getAtom.current).toEqual([{ extensionId: 'ext2', percentage: 50 }])
|
||||
})
|
||||
})
|
||||
|
||||
describe('inActiveEngineProviderAtom', () => {
|
||||
it('should initialize as an empty array', () => {
|
||||
const { result } = renderHook(() => useAtomValue(ExtensionAtoms.inActiveEngineProviderAtom))
|
||||
|
||||
@ -1,45 +1,5 @@
|
||||
import { atom } from 'jotai'
|
||||
import { atomWithStorage } from 'jotai/utils'
|
||||
|
||||
type ExtensionId = string
|
||||
|
||||
export type InstallingExtensionState = {
|
||||
extensionId: ExtensionId
|
||||
percentage: number
|
||||
localPath?: string
|
||||
}
|
||||
|
||||
export const installingExtensionAtom = atom<InstallingExtensionState[]>([])
|
||||
|
||||
export const setInstallingExtensionAtom = atom(
|
||||
null,
|
||||
(get, set, extensionId: string, state: InstallingExtensionState) => {
|
||||
const current = get(installingExtensionAtom)
|
||||
|
||||
const isExists = current.some((e) => e.extensionId === extensionId)
|
||||
if (isExists) {
|
||||
const newCurrent = current.map((e) => {
|
||||
if (e.extensionId === extensionId) {
|
||||
return state
|
||||
}
|
||||
return e
|
||||
})
|
||||
set(installingExtensionAtom, newCurrent)
|
||||
} else {
|
||||
set(installingExtensionAtom, [...current, state])
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export const removeInstallingExtensionAtom = atom(
|
||||
null,
|
||||
(get, set, extensionId: string) => {
|
||||
const current = get(installingExtensionAtom)
|
||||
const newCurrent = current.filter((e) => e.extensionId !== extensionId)
|
||||
set(installingExtensionAtom, newCurrent)
|
||||
}
|
||||
)
|
||||
|
||||
const INACTIVE_ENGINE_PROVIDER = 'inActiveEngineProvider'
|
||||
export const inActiveEngineProviderAtom = atomWithStorage<string[]>(
|
||||
INACTIVE_ENGINE_PROVIDER,
|
||||
|
||||
@ -3,8 +3,8 @@ import { useCallback } from 'react'
|
||||
import {
|
||||
ExtensionTypeEnum,
|
||||
ImportingModel,
|
||||
LocalImportModelEvent,
|
||||
Model,
|
||||
ModelEvent,
|
||||
ModelExtension,
|
||||
OptionType,
|
||||
events,
|
||||
@ -25,6 +25,7 @@ import {
|
||||
downloadedModelsAtom,
|
||||
importingModelsAtom,
|
||||
removeDownloadingModelAtom,
|
||||
setImportingModelSuccessAtom,
|
||||
} from '@/helpers/atoms/Model.atom'
|
||||
|
||||
export type ImportModelStage =
|
||||
@ -59,6 +60,7 @@ const useImportModel = () => {
|
||||
const addDownloadingModel = useSetAtom(addDownloadingModelAtom)
|
||||
const removeDownloadingModel = useSetAtom(removeDownloadingModelAtom)
|
||||
const downloadedModels = useAtomValue(downloadedModelsAtom)
|
||||
const setImportingModelSuccess = useSetAtom(setImportingModelSuccessAtom)
|
||||
|
||||
const incrementalModelName = useCallback(
|
||||
(name: string, startIndex: number = 0): string => {
|
||||
@ -83,10 +85,9 @@ const useImportModel = () => {
|
||||
?.importModel(modelId, model.path, model.name, optionType)
|
||||
.finally(() => {
|
||||
removeDownloadingModel(modelId)
|
||||
events.emit(LocalImportModelEvent.onLocalImportModelSuccess, {
|
||||
importId: model.importId,
|
||||
modelId: modelId,
|
||||
})
|
||||
|
||||
events.emit(ModelEvent.OnModelsUpdate, { fetch: true })
|
||||
setImportingModelSuccess(model.importId, modelId)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@ -1,197 +0,0 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import {
|
||||
BaseExtension,
|
||||
Compatibility,
|
||||
InstallationState,
|
||||
abortDownload,
|
||||
} from '@janhq/core'
|
||||
import { Button, Progress, Tooltip } from '@janhq/joi'
|
||||
|
||||
import { InfoCircledIcon } from '@radix-ui/react-icons'
|
||||
import { useAtomValue } from 'jotai'
|
||||
|
||||
import { Marked, Renderer } from 'marked'
|
||||
|
||||
import { extensionManager } from '@/extension'
|
||||
import { installingExtensionAtom } from '@/helpers/atoms/Extension.atom'
|
||||
|
||||
type Props = {
|
||||
item: BaseExtension
|
||||
}
|
||||
|
||||
const ExtensionItem: React.FC<Props> = ({ item }) => {
|
||||
const [compatibility, setCompatibility] = useState<Compatibility | undefined>(
|
||||
undefined
|
||||
)
|
||||
const [installState, setInstallState] =
|
||||
useState<InstallationState>('NotRequired')
|
||||
const installingExtensions = useAtomValue(installingExtensionAtom)
|
||||
const isInstalling = installingExtensions.some(
|
||||
(e) => e.extensionId === item.name
|
||||
)
|
||||
|
||||
const progress = isInstalling
|
||||
? (installingExtensions.find((e) => e.extensionId === item.name)
|
||||
?.percentage ?? -1)
|
||||
: -1
|
||||
|
||||
useEffect(() => {
|
||||
const getExtensionInstallationState = async () => {
|
||||
const extension = extensionManager.getByName(item.name)
|
||||
if (!extension) return
|
||||
|
||||
if (typeof extension?.installationState === 'function') {
|
||||
const installState = await extension.installationState()
|
||||
setInstallState(installState)
|
||||
}
|
||||
}
|
||||
|
||||
getExtensionInstallationState()
|
||||
}, [item.name, isInstalling])
|
||||
|
||||
useEffect(() => {
|
||||
const extension = extensionManager.getByName(item.name)
|
||||
if (!extension) return
|
||||
setCompatibility(extension.compatibility())
|
||||
}, [setCompatibility, item.name])
|
||||
|
||||
const onInstallClick = useCallback(async () => {
|
||||
const extension = extensionManager.getByName(item.name)
|
||||
if (!extension) return
|
||||
|
||||
await extension.install()
|
||||
}, [item.name])
|
||||
|
||||
const onCancelInstallingClick = () => {
|
||||
const extension = installingExtensions.find(
|
||||
(e) => e.extensionId === item.name
|
||||
)
|
||||
if (extension?.localPath) {
|
||||
abortDownload(extension.localPath)
|
||||
}
|
||||
}
|
||||
|
||||
const description = marked.parse(item.description ?? '', { async: false })
|
||||
|
||||
return (
|
||||
<div className="mx-4 flex items-start justify-between border-b border-[hsla(var(--app-border))] py-6 first:pt-4 last:border-none">
|
||||
<div className="flex-1 flex-shrink-0 space-y-1">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<h6 className="font-semibold">Additional Dependencies</h6>
|
||||
</div>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: description }}
|
||||
className="font-medium leading-relaxed text-[hsla(var(--text-secondary))]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex min-w-[150px] flex-row justify-end">
|
||||
<InstallStateIndicator
|
||||
installProgress={progress}
|
||||
installState={installState}
|
||||
compatibility={compatibility}
|
||||
onInstallClick={onInstallClick}
|
||||
onCancelClick={onCancelInstallingClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type InstallStateProps = {
|
||||
installProgress: number
|
||||
compatibility?: Compatibility
|
||||
installState: InstallationState
|
||||
onInstallClick: () => void
|
||||
onCancelClick: () => void
|
||||
}
|
||||
|
||||
const InstallStateIndicator: React.FC<InstallStateProps> = ({
|
||||
installProgress,
|
||||
compatibility,
|
||||
installState,
|
||||
onInstallClick,
|
||||
onCancelClick,
|
||||
}) => {
|
||||
if (installProgress !== -1) {
|
||||
const progress = installProgress * 100
|
||||
return (
|
||||
<div className="text-primary dark flex h-10 flex-row items-center justify-center space-x-2 rounded-lg px-4">
|
||||
<button onClick={onCancelClick} className="text-primary font-semibold">
|
||||
Cancel
|
||||
</button>
|
||||
<div className="flex w-[113px] flex-row items-center justify-center space-x-2 rounded-md px-2 py-[2px]">
|
||||
<Progress className="h-1 w-[69px]" value={progress} />
|
||||
<span className="text-primary text-xs font-bold">
|
||||
{progress.toFixed(0)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
switch (installState) {
|
||||
case 'Installed':
|
||||
return (
|
||||
<div className="rounded-md px-3 py-1.5 font-semibold text-[hsla(var(--text-secondary))]">
|
||||
Installed
|
||||
</div>
|
||||
)
|
||||
case 'NotCompatible':
|
||||
return (
|
||||
<div className="rounded-md px-3 py-1.5 font-semibold text-[hsla(var(--text-secondary))]">
|
||||
<div className="flex flex-row items-center justify-center gap-1">
|
||||
Incompatible
|
||||
<Tooltip
|
||||
trigger={
|
||||
<InfoCircledIcon className="cursor-pointer text-[hsla(var(--text-secondary))]" />
|
||||
}
|
||||
content={
|
||||
compatibility &&
|
||||
!compatibility['platform']?.includes(PLATFORM) ? (
|
||||
<span>
|
||||
Only available on
|
||||
{compatibility?.platform
|
||||
?.map((e: string) =>
|
||||
e === 'win32'
|
||||
? 'Windows'
|
||||
: e === 'linux'
|
||||
? 'Linux'
|
||||
: 'MacOS'
|
||||
)
|
||||
.join(', ')}
|
||||
</span>
|
||||
) : (
|
||||
<span>Your GPUs are not compatible with this extension</span>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
case 'NotInstalled':
|
||||
return (
|
||||
<Button size="small" variant="soft" onClick={onInstallClick}>
|
||||
Install
|
||||
</Button>
|
||||
)
|
||||
default:
|
||||
return <div></div>
|
||||
}
|
||||
}
|
||||
|
||||
const marked: Marked = new Marked({
|
||||
renderer: {
|
||||
link: (href, title, text) => {
|
||||
return Renderer.prototype.link
|
||||
?.apply(this, [href, title, text])
|
||||
.replace(
|
||||
'<a',
|
||||
"<a class='text-[hsla(var(--app-link))]' target='_blank'"
|
||||
)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export default ExtensionItem
|
||||
@ -40,10 +40,7 @@ const ExtensionCatalog = () => {
|
||||
'provider' in extension &&
|
||||
typeof extension.provider === 'string'
|
||||
) {
|
||||
if (
|
||||
(settings && settings.length > 0) ||
|
||||
(await extension.installationState()) !== 'NotRequired'
|
||||
) {
|
||||
if (settings && settings.length > 0) {
|
||||
engineMenu.push({
|
||||
...extension,
|
||||
provider:
|
||||
|
||||
@ -1,14 +1,9 @@
|
||||
import React, { Fragment, useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import {
|
||||
BaseExtension,
|
||||
InstallationState,
|
||||
SettingComponentProps,
|
||||
} from '@janhq/core'
|
||||
import { SettingComponentProps } from '@janhq/core'
|
||||
|
||||
import { useAtomValue } from 'jotai'
|
||||
|
||||
import ExtensionItem from '../CoreExtensions/ExtensionItem'
|
||||
import SettingDetailItem from '../SettingDetail/SettingDetailItem'
|
||||
|
||||
import { extensionManager } from '@/extension'
|
||||
@ -17,11 +12,6 @@ import { selectedSettingAtom } from '@/helpers/atoms/Setting.atom'
|
||||
const ExtensionSetting = ({ extensionName }: { extensionName?: string }) => {
|
||||
const selectedExtensionName = useAtomValue(selectedSettingAtom)
|
||||
const [settings, setSettings] = useState<SettingComponentProps[]>([])
|
||||
const [installationState, setInstallationState] =
|
||||
useState<InstallationState>('NotRequired')
|
||||
const [baseExtension, setBaseExtension] = useState<BaseExtension | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
const currentExtensionName = useMemo(
|
||||
() => extensionName ?? selectedExtensionName,
|
||||
@ -35,14 +25,11 @@ const ExtensionSetting = ({ extensionName }: { extensionName?: string }) => {
|
||||
const baseExtension = extensionManager.getByName(currentExtensionName)
|
||||
if (!baseExtension) return
|
||||
|
||||
setBaseExtension(baseExtension)
|
||||
if (typeof baseExtension.getSettings === 'function') {
|
||||
const setting = await baseExtension.getSettings()
|
||||
if (setting) allSettings.push(...setting)
|
||||
}
|
||||
setSettings(allSettings)
|
||||
|
||||
setInstallationState(await baseExtension.installationState())
|
||||
}
|
||||
getExtensionSettings()
|
||||
}, [currentExtensionName])
|
||||
@ -75,9 +62,6 @@ const ExtensionSetting = ({ extensionName }: { extensionName?: string }) => {
|
||||
onValueUpdated={onValueChanged}
|
||||
/>
|
||||
)}
|
||||
{baseExtension && installationState !== 'NotRequired' && (
|
||||
<ExtensionItem item={baseExtension} />
|
||||
)}
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user