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',
|
'@types/pacote',
|
||||||
'@npmcli/arborist',
|
'@npmcli/arborist',
|
||||||
'ulidx',
|
'ulidx',
|
||||||
'node-fetch',
|
|
||||||
'fs',
|
'fs',
|
||||||
'request',
|
'request',
|
||||||
'crypto',
|
'crypto',
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import { openExternalUrl } from './core'
|
|||||||
import { joinPath } from './core'
|
import { joinPath } from './core'
|
||||||
import { openFileExplorer } from './core'
|
import { openFileExplorer } from './core'
|
||||||
import { getJanDataFolderPath } from './core'
|
import { getJanDataFolderPath } from './core'
|
||||||
import { abortDownload } from './core'
|
|
||||||
import { executeOnMain } from './core'
|
import { executeOnMain } from './core'
|
||||||
|
|
||||||
describe('test core apis', () => {
|
describe('test core apis', () => {
|
||||||
@ -53,18 +52,6 @@ describe('test core apis', () => {
|
|||||||
expect(result).toBe('/path/to/jan/data')
|
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 () => {
|
it('should execute function on main process', async () => {
|
||||||
const extension = 'testExtension'
|
const extension = 'testExtension'
|
||||||
const method = 'testMethod'
|
const method = 'testMethod'
|
||||||
|
|||||||
@ -1,9 +1,4 @@
|
|||||||
import {
|
import { SystemInformation } from '../types'
|
||||||
DownloadRequest,
|
|
||||||
FileStat,
|
|
||||||
NetworkConfig,
|
|
||||||
SystemInformation,
|
|
||||||
} from '../types'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute a extension module function in main process
|
* Execute a extension module function in main process
|
||||||
@ -14,42 +9,19 @@ import {
|
|||||||
* @returns Promise<any>
|
* @returns Promise<any>
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
const executeOnMain: (
|
const executeOnMain: (extension: string, method: string, ...args: any[]) => Promise<any> = (
|
||||||
extension: string,
|
extension,
|
||||||
method: string,
|
method,
|
||||||
...args: any[]
|
...args
|
||||||
) => Promise<any> = (extension, method, ...args) =>
|
) => globalThis.core?.api?.invokeExtensionFunc(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.
|
* Gets Jan's data folder path.
|
||||||
*
|
*
|
||||||
* @returns {Promise<string>} A Promise that resolves with Jan's data folder path.
|
* @returns {Promise<string>} A Promise that resolves with Jan's data folder path.
|
||||||
*/
|
*/
|
||||||
const getJanDataFolderPath = (): Promise<string> =>
|
const getJanDataFolderPath = (): Promise<string> => globalThis.core.api?.getJanDataFolderPath()
|
||||||
globalThis.core.api?.getJanDataFolderPath()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens the file explorer at a specific path.
|
* 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.
|
* @param path - The file path to retrieve dirname.
|
||||||
* @returns {Promise<string>} A promise that resolves the dirname.
|
* @returns {Promise<string>} A promise that resolves the dirname.
|
||||||
*/
|
*/
|
||||||
const dirName: (path: string) => Promise<string> = (path) =>
|
const dirName: (path: string) => Promise<string> = (path) => globalThis.core.api?.dirName(path)
|
||||||
globalThis.core.api?.dirName(path)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the basename from an url.
|
* Retrieve the basename from an url.
|
||||||
* @param path - The path to retrieve.
|
* @param path - The path to retrieve.
|
||||||
* @returns {Promise<string>} A promise that resolves with the basename.
|
* @returns {Promise<string>} A promise that resolves with the basename.
|
||||||
*/
|
*/
|
||||||
const baseName: (paths: string) => Promise<string> = (path) =>
|
const baseName: (paths: string) => Promise<string> = (path) => globalThis.core.api?.baseName(path)
|
||||||
globalThis.core.api?.baseName(path)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens an external URL in the default web browser.
|
* 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.
|
* @returns {Promise<string>} - A promise that resolves with the resource path.
|
||||||
*/
|
*/
|
||||||
const getResourcePath: () => Promise<string> = () =>
|
const getResourcePath: () => Promise<string> = () => globalThis.core.api?.getResourcePath()
|
||||||
globalThis.core.api?.getResourcePath()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the user's home path.
|
* Gets the user's home path.
|
||||||
* @returns return user's home path
|
* @returns return user's home path
|
||||||
*/
|
*/
|
||||||
const getUserHomePath = (): Promise<string> =>
|
const getUserHomePath = (): Promise<string> => globalThis.core.api?.getUserHomePath()
|
||||||
globalThis.core.api?.getUserHomePath()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log to file from browser processes.
|
* 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.
|
* @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> = (
|
const isSubdirectory: (from: string, to: string) => Promise<boolean> = (from: string, to: string) =>
|
||||||
from: string,
|
globalThis.core.api?.isSubdirectory(from, to)
|
||||||
to: string
|
|
||||||
) => globalThis.core.api?.isSubdirectory(from, to)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get system information
|
* Get system information
|
||||||
@ -159,8 +125,6 @@ export type RegisterExtensionPoint = (
|
|||||||
*/
|
*/
|
||||||
export {
|
export {
|
||||||
executeOnMain,
|
executeOnMain,
|
||||||
downloadFile,
|
|
||||||
abortDownload,
|
|
||||||
getJanDataFolderPath,
|
getJanDataFolderPath,
|
||||||
openFileExplorer,
|
openFileExplorer,
|
||||||
getResourcePath,
|
getResourcePath,
|
||||||
|
|||||||
@ -39,11 +39,6 @@ describe('BaseExtension', () => {
|
|||||||
expect(baseExtension.onUnload).toBeDefined()
|
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 () => {
|
it('should install the extension', async () => {
|
||||||
await baseExtension.install()
|
await baseExtension.install()
|
||||||
// Add your assertions here
|
// Add your assertions here
|
||||||
@ -84,11 +79,6 @@ describe('BaseExtension', () => {
|
|||||||
expect(baseExtension.onUnload).toBeDefined()
|
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 () => {
|
it('should install the extension', async () => {
|
||||||
await baseExtension.install()
|
await baseExtension.install()
|
||||||
// Add your assertions here
|
// Add your assertions here
|
||||||
|
|||||||
@ -24,17 +24,6 @@ export interface Compatibility {
|
|||||||
version: string
|
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.
|
* Represents a base extension.
|
||||||
* This class should be extended by any class that represents an extension.
|
* This class should be extended by any class that represents an extension.
|
||||||
@ -175,15 +164,6 @@ export abstract class BaseExtension implements ExtensionType {
|
|||||||
return
|
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.
|
* Install the prerequisites for the extension.
|
||||||
*
|
*
|
||||||
|
|||||||
@ -55,6 +55,12 @@ const unlinkSync = (...args: any[]) => globalThis.core.api?.unlinkSync(...args)
|
|||||||
*/
|
*/
|
||||||
const appendFileSync = (...args: any[]) => globalThis.core.api?.appendFileSync(...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) =>
|
const copyFile: (src: string, dest: string) => Promise<void> = (src, dest) =>
|
||||||
globalThis.core.api?.copyFile(src, dest)
|
globalThis.core.api?.copyFile(src, dest)
|
||||||
|
|
||||||
@ -64,8 +70,8 @@ const copyFile: (src: string, dest: string) => Promise<void> = (src, dest) =>
|
|||||||
* @param path - The paths to the file.
|
* @param path - The paths to the file.
|
||||||
* @returns {Promise<{any}>} - A promise that resolves with the list of gguf and non-gguf files
|
* @returns {Promise<{any}>} - A promise that resolves with the list of gguf and non-gguf files
|
||||||
*/
|
*/
|
||||||
const getGgufFiles: (paths: string[]) => Promise<any> = (
|
const getGgufFiles: (paths: string[]) => Promise<any> = (paths) =>
|
||||||
paths) => globalThis.core.api?.getGgufFiles(paths)
|
globalThis.core.api?.getGgufFiles(paths)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the file's stats.
|
* Gets the file's stats.
|
||||||
|
|||||||
@ -1,25 +1,21 @@
|
|||||||
import {
|
import {
|
||||||
AppRoute,
|
AppRoute,
|
||||||
DownloadRoute,
|
|
||||||
ExtensionRoute,
|
ExtensionRoute,
|
||||||
FileManagerRoute,
|
FileManagerRoute,
|
||||||
FileSystemRoute,
|
FileSystemRoute,
|
||||||
} from '../../../types/api'
|
} from '../../../types/api'
|
||||||
import { Downloader } from '../processors/download'
|
|
||||||
import { FileSystem } from '../processors/fs'
|
import { FileSystem } from '../processors/fs'
|
||||||
import { Extension } from '../processors/extension'
|
import { Extension } from '../processors/extension'
|
||||||
import { FSExt } from '../processors/fsExt'
|
import { FSExt } from '../processors/fsExt'
|
||||||
import { App } from '../processors/app'
|
import { App } from '../processors/app'
|
||||||
|
|
||||||
export class RequestAdapter {
|
export class RequestAdapter {
|
||||||
downloader: Downloader
|
|
||||||
fileSystem: FileSystem
|
fileSystem: FileSystem
|
||||||
extension: Extension
|
extension: Extension
|
||||||
fsExt: FSExt
|
fsExt: FSExt
|
||||||
app: App
|
app: App
|
||||||
|
|
||||||
constructor(observer?: Function) {
|
constructor(observer?: Function) {
|
||||||
this.downloader = new Downloader(observer)
|
|
||||||
this.fileSystem = new FileSystem()
|
this.fileSystem = new FileSystem()
|
||||||
this.extension = new Extension()
|
this.extension = new Extension()
|
||||||
this.fsExt = new FSExt()
|
this.fsExt = new FSExt()
|
||||||
@ -28,9 +24,7 @@ export class RequestAdapter {
|
|||||||
|
|
||||||
// TODO: Clearer Factory pattern here
|
// TODO: Clearer Factory pattern here
|
||||||
process(route: string, ...args: any) {
|
process(route: string, ...args: any) {
|
||||||
if (route in DownloadRoute) {
|
if (route in FileSystemRoute) {
|
||||||
return this.downloader.process(route, ...args)
|
|
||||||
} else if (route in FileSystemRoute) {
|
|
||||||
return this.fileSystem.process(route, ...args)
|
return this.fileSystem.process(route, ...args)
|
||||||
} else if (route in ExtensionRoute) {
|
} else if (route in ExtensionRoute) {
|
||||||
return this.extension.process(route, ...args)
|
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 './config'
|
||||||
export * from './download'
|
|
||||||
export * from './logger'
|
export * from './logger'
|
||||||
export * from './module'
|
export * from './module'
|
||||||
export * from './path'
|
export * from './path'
|
||||||
|
|||||||
@ -65,30 +65,13 @@ export enum AppEvent {
|
|||||||
onMainViewStateChange = 'onMainViewStateChange',
|
onMainViewStateChange = 'onMainViewStateChange',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum DownloadRoute {
|
|
||||||
abortDownload = 'abortDownload',
|
|
||||||
downloadFile = 'downloadFile',
|
|
||||||
pauseDownload = 'pauseDownload',
|
|
||||||
resumeDownload = 'resumeDownload',
|
|
||||||
getDownloadProgress = 'getDownloadProgress',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum DownloadEvent {
|
export enum DownloadEvent {
|
||||||
onFileDownloadUpdate = 'onFileDownloadUpdate',
|
onFileDownloadUpdate = 'onFileDownloadUpdate',
|
||||||
onFileDownloadError = 'onFileDownloadError',
|
onFileDownloadError = 'onFileDownloadError',
|
||||||
onFileDownloadSuccess = 'onFileDownloadSuccess',
|
onFileDownloadSuccess = 'onFileDownloadSuccess',
|
||||||
onFileDownloadStopped = 'onFileDownloadStopped',
|
onFileDownloadStopped = 'onFileDownloadStopped',
|
||||||
onFileDownloadStarted = 'onFileDownloadStarted',
|
onFileDownloadStarted = 'onFileDownloadStarted',
|
||||||
onFileUnzipSuccess = 'onFileUnzipSuccess',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum LocalImportModelEvent {
|
|
||||||
onLocalImportModelUpdate = 'onLocalImportModelUpdate',
|
|
||||||
onLocalImportModelFailed = 'onLocalImportModelFailed',
|
|
||||||
onLocalImportModelSuccess = 'onLocalImportModelSuccess',
|
|
||||||
onLocalImportModelFinished = 'onLocalImportModelFinished',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum ExtensionRoute {
|
export enum ExtensionRoute {
|
||||||
baseExtensions = 'baseExtensions',
|
baseExtensions = 'baseExtensions',
|
||||||
getActiveExtensions = 'getActiveExtensions',
|
getActiveExtensions = 'getActiveExtensions',
|
||||||
@ -131,10 +114,6 @@ export type AppEventFunctions = {
|
|||||||
[K in AppEvent]: ApiFunction
|
[K in AppEvent]: ApiFunction
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DownloadRouteFunctions = {
|
|
||||||
[K in DownloadRoute]: ApiFunction
|
|
||||||
}
|
|
||||||
|
|
||||||
export type DownloadEventFunctions = {
|
export type DownloadEventFunctions = {
|
||||||
[K in DownloadEvent]: ApiFunction
|
[K in DownloadEvent]: ApiFunction
|
||||||
}
|
}
|
||||||
@ -154,7 +133,6 @@ export type FileManagerRouteFunctions = {
|
|||||||
export type APIFunctions = NativeRouteFunctions &
|
export type APIFunctions = NativeRouteFunctions &
|
||||||
AppRouteFunctions &
|
AppRouteFunctions &
|
||||||
AppEventFunctions &
|
AppEventFunctions &
|
||||||
DownloadRouteFunctions &
|
|
||||||
DownloadEventFunctions &
|
DownloadEventFunctions &
|
||||||
ExtensionRouteFunctions &
|
ExtensionRouteFunctions &
|
||||||
FileSystemRouteFunctions &
|
FileSystemRouteFunctions &
|
||||||
@ -162,7 +140,6 @@ export type APIFunctions = NativeRouteFunctions &
|
|||||||
|
|
||||||
export const CoreRoutes = [
|
export const CoreRoutes = [
|
||||||
...Object.values(AppRoute),
|
...Object.values(AppRoute),
|
||||||
...Object.values(DownloadRoute),
|
|
||||||
...Object.values(ExtensionRoute),
|
...Object.values(ExtensionRoute),
|
||||||
...Object.values(FileSystemRoute),
|
...Object.values(FileSystemRoute),
|
||||||
...Object.values(FileManagerRoute),
|
...Object.values(FileManagerRoute),
|
||||||
@ -172,7 +149,6 @@ export const APIRoutes = [...CoreRoutes, ...Object.values(NativeRoute)]
|
|||||||
export const APIEvents = [
|
export const APIEvents = [
|
||||||
...Object.values(AppEvent),
|
...Object.values(AppEvent),
|
||||||
...Object.values(DownloadEvent),
|
...Object.values(DownloadEvent),
|
||||||
...Object.values(LocalImportModelEvent),
|
|
||||||
]
|
]
|
||||||
export type PayloadType = {
|
export type PayloadType = {
|
||||||
messages: ChatCompletionMessage[]
|
messages: ChatCompletionMessage[]
|
||||||
|
|||||||
@ -16,41 +16,9 @@ export type DownloadState = {
|
|||||||
|
|
||||||
error?: string
|
error?: string
|
||||||
extensionId?: string
|
extensionId?: string
|
||||||
downloadType?: DownloadType | string
|
|
||||||
localPath?: 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 = {
|
type DownloadTime = {
|
||||||
elapsed: number
|
elapsed: number
|
||||||
remaining: number
|
remaining: number
|
||||||
@ -60,7 +28,6 @@ type DownloadSize = {
|
|||||||
total: number
|
total: number
|
||||||
transferred: number
|
transferred: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The file metadata
|
* 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 './systemResourceInfo'
|
||||||
export * from './promptTemplate'
|
export * from './promptTemplate'
|
||||||
export * from './appUpdate'
|
export * from './appUpdate'
|
||||||
export * from './fileDownloadRequest'
|
|
||||||
export * from './networkConfig'
|
|
||||||
export * from './selectFiles'
|
export * from './selectFiles'
|
||||||
|
|||||||
@ -1,4 +0,0 @@
|
|||||||
export type NetworkConfig = {
|
|
||||||
proxy?: string
|
|
||||||
ignoreSSL?: boolean
|
|
||||||
}
|
|
||||||
@ -1,3 +1,2 @@
|
|||||||
export * from './threadEntity'
|
export * from './threadEntity'
|
||||||
export * from './threadInterface'
|
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",
|
"name": "jan",
|
||||||
"version": "0.1.1",
|
"version": "0.1.1740752217",
|
||||||
"main": "./build/main.js",
|
"main": "./build/main.js",
|
||||||
"author": "Jan <service@jan.ai>",
|
"author": "Jan <service@jan.ai>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -113,7 +113,6 @@
|
|||||||
"electron-store": "^8.1.0",
|
"electron-store": "^8.1.0",
|
||||||
"electron-updater": "^6.1.7",
|
"electron-updater": "^6.1.7",
|
||||||
"fs-extra": "^11.2.0",
|
"fs-extra": "^11.2.0",
|
||||||
"node-fetch": "2",
|
|
||||||
"pacote": "^21.0.0",
|
"pacote": "^21.0.0",
|
||||||
"request": "^2.88.2",
|
"request": "^2.88.2",
|
||||||
"request-progress": "^3.0.0",
|
"request-progress": "^3.0.0",
|
||||||
|
|||||||
@ -5,11 +5,6 @@ import {
|
|||||||
joinPath,
|
joinPath,
|
||||||
dirName,
|
dirName,
|
||||||
fs,
|
fs,
|
||||||
ModelManager,
|
|
||||||
abortDownload,
|
|
||||||
DownloadState,
|
|
||||||
events,
|
|
||||||
DownloadEvent,
|
|
||||||
OptionType,
|
OptionType,
|
||||||
ModelSource,
|
ModelSource,
|
||||||
extractInferenceParams,
|
extractInferenceParams,
|
||||||
@ -55,9 +50,6 @@ export default class JanModelExtension extends ModelExtension {
|
|||||||
this.updateCortexConfig({ huggingface_token: huggingfaceToken })
|
this.updateCortexConfig({ huggingface_token: huggingfaceToken })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen to app download events
|
|
||||||
this.handleDesktopEvents()
|
|
||||||
|
|
||||||
// Sync with cortexsohub
|
// Sync with cortexsohub
|
||||||
this.fetchCortexsoModels()
|
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.
|
* @returns {Promise<void>} A promise that resolves when the download has been cancelled.
|
||||||
*/
|
*/
|
||||||
async cancelModelPull(model: string): Promise<void> {
|
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
|
* 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
|
// END: - Public API
|
||||||
|
|
||||||
// BEGIN: - Private 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)
|
* 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 DownloadingState from './DownloadingState'
|
||||||
|
|
||||||
import ImportingModelState from './ImportingModelState'
|
import ImportingModelState from './ImportingModelState'
|
||||||
import InstallingExtension from './InstallingExtension'
|
|
||||||
import SystemMonitor from './SystemMonitor'
|
import SystemMonitor from './SystemMonitor'
|
||||||
import UpdateApp from './UpdateApp'
|
import UpdateApp from './UpdateApp'
|
||||||
import UpdatedFailedModal from './UpdateFailedModal'
|
import UpdatedFailedModal from './UpdateFailedModal'
|
||||||
@ -49,7 +48,6 @@ const BottomPanel = () => {
|
|||||||
<ImportingModelState />
|
<ImportingModelState />
|
||||||
<DownloadingState />
|
<DownloadingState />
|
||||||
<UpdatedFailedModal />
|
<UpdatedFailedModal />
|
||||||
<InstallingExtension />
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-x-1">
|
<div className="flex items-center gap-x-1">
|
||||||
<SystemMonitor />
|
<SystemMonitor />
|
||||||
|
|||||||
@ -35,8 +35,6 @@ import ModalAppUpdaterChangelog from '../ModalAppUpdaterChangelog'
|
|||||||
|
|
||||||
import ModalAppUpdaterNotAvailable from '../ModalAppUpdaterNotAvailable'
|
import ModalAppUpdaterNotAvailable from '../ModalAppUpdaterNotAvailable'
|
||||||
|
|
||||||
import InstallingExtensionModal from './BottomPanel/InstallingExtension/InstallingExtensionModal'
|
|
||||||
|
|
||||||
import { mainViewStateAtom } from '@/helpers/atoms/App.atom'
|
import { mainViewStateAtom } from '@/helpers/atoms/App.atom'
|
||||||
import {
|
import {
|
||||||
productAnalyticAtom,
|
productAnalyticAtom,
|
||||||
@ -167,7 +165,6 @@ const BaseLayout = () => {
|
|||||||
{importModelStage === 'EDIT_MODEL_INFO' && <EditModelInfoModal />}
|
{importModelStage === 'EDIT_MODEL_INFO' && <EditModelInfoModal />}
|
||||||
{importModelStage === 'CONFIRM_CANCEL' && <CancelModelImportModal />}
|
{importModelStage === 'CONFIRM_CANCEL' && <CancelModelImportModal />}
|
||||||
<ChooseWhatToImportModal />
|
<ChooseWhatToImportModal />
|
||||||
<InstallingExtensionModal />
|
|
||||||
{showProductAnalyticPrompt && (
|
{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="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">
|
<div className="mb-4 flex items-center gap-x-2">
|
||||||
|
|||||||
@ -4,8 +4,8 @@ import React from 'react'
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
DownloadEvent,
|
DownloadEvent,
|
||||||
events,
|
|
||||||
DownloadState,
|
DownloadState,
|
||||||
|
events,
|
||||||
ModelEvent,
|
ModelEvent,
|
||||||
ExtensionTypeEnum,
|
ExtensionTypeEnum,
|
||||||
ModelExtension,
|
ModelExtension,
|
||||||
@ -17,23 +17,15 @@ import { useSetAtom } from 'jotai'
|
|||||||
|
|
||||||
import { setDownloadStateAtom } from '@/hooks/useDownloadState'
|
import { setDownloadStateAtom } from '@/hooks/useDownloadState'
|
||||||
|
|
||||||
import { formatExtensionsName } from '@/utils/converter'
|
|
||||||
|
|
||||||
import { toaster } from '../Toast'
|
import { toaster } from '../Toast'
|
||||||
|
|
||||||
import AppUpdateListener from './AppUpdateListener'
|
import AppUpdateListener from './AppUpdateListener'
|
||||||
import ClipboardListener from './ClipboardListener'
|
import ClipboardListener from './ClipboardListener'
|
||||||
import ModelHandler from './ModelHandler'
|
import ModelHandler from './ModelHandler'
|
||||||
|
|
||||||
import ModelImportListener from './ModelImportListener'
|
|
||||||
import QuickAskListener from './QuickAskListener'
|
import QuickAskListener from './QuickAskListener'
|
||||||
|
|
||||||
import { extensionManager } from '@/extension'
|
import { extensionManager } from '@/extension'
|
||||||
import {
|
|
||||||
InstallingExtensionState,
|
|
||||||
removeInstallingExtensionAtom,
|
|
||||||
setInstallingExtensionAtom,
|
|
||||||
} from '@/helpers/atoms/Extension.atom'
|
|
||||||
import {
|
import {
|
||||||
addDownloadingModelAtom,
|
addDownloadingModelAtom,
|
||||||
removeDownloadingModelAtom,
|
removeDownloadingModelAtom,
|
||||||
@ -41,62 +33,44 @@ import {
|
|||||||
|
|
||||||
const EventListener = () => {
|
const EventListener = () => {
|
||||||
const setDownloadState = useSetAtom(setDownloadStateAtom)
|
const setDownloadState = useSetAtom(setDownloadStateAtom)
|
||||||
const setInstallingExtension = useSetAtom(setInstallingExtensionAtom)
|
|
||||||
const removeInstallingExtension = useSetAtom(removeInstallingExtensionAtom)
|
|
||||||
const addDownloadingModel = useSetAtom(addDownloadingModelAtom)
|
const addDownloadingModel = useSetAtom(addDownloadingModelAtom)
|
||||||
const removeDownloadingModel = useSetAtom(removeDownloadingModelAtom)
|
const removeDownloadingModel = useSetAtom(removeDownloadingModelAtom)
|
||||||
|
|
||||||
const onFileDownloadUpdate = useCallback(
|
const onFileDownloadUpdate = useCallback(
|
||||||
async (state: DownloadState) => {
|
async (state: DownloadState) => {
|
||||||
console.debug('onFileDownloadUpdate', state)
|
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)
|
addDownloadingModel(state.modelId)
|
||||||
setDownloadState(state)
|
setDownloadState(state)
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[addDownloadingModel, setDownloadState, setInstallingExtension]
|
[addDownloadingModel, setDownloadState]
|
||||||
)
|
)
|
||||||
|
|
||||||
const onFileDownloadError = useCallback(
|
const onFileDownloadError = useCallback(
|
||||||
(state: DownloadState) => {
|
(state: DownloadState) => {
|
||||||
console.debug('onFileDownloadError', state)
|
console.debug('onFileDownloadError', state)
|
||||||
if (state.downloadType === 'extension') {
|
|
||||||
removeInstallingExtension(state.extensionId!)
|
|
||||||
} else {
|
|
||||||
state.downloadState = 'error'
|
state.downloadState = 'error'
|
||||||
setDownloadState(state)
|
setDownloadState(state)
|
||||||
removeDownloadingModel(state.modelId)
|
removeDownloadingModel(state.modelId)
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[removeInstallingExtension, setDownloadState, removeDownloadingModel]
|
[setDownloadState, removeDownloadingModel]
|
||||||
)
|
)
|
||||||
|
|
||||||
const onFileDownloadStopped = useCallback(
|
const onFileDownloadStopped = useCallback(
|
||||||
(state: DownloadState) => {
|
(state: DownloadState) => {
|
||||||
console.debug('onFileDownloadError', state)
|
console.debug('onFileDownloadError', state)
|
||||||
if (state.downloadType === 'extension') {
|
|
||||||
removeInstallingExtension(state.extensionId!)
|
|
||||||
} else {
|
|
||||||
state.downloadState = 'error'
|
state.downloadState = 'error'
|
||||||
state.error = 'aborted'
|
state.error = 'aborted'
|
||||||
setDownloadState(state)
|
setDownloadState(state)
|
||||||
removeDownloadingModel(state.modelId)
|
removeDownloadingModel(state.modelId)
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[removeInstallingExtension, setDownloadState, removeDownloadingModel]
|
[setDownloadState, removeDownloadingModel]
|
||||||
)
|
)
|
||||||
|
|
||||||
const onFileDownloadSuccess = useCallback(
|
const onFileDownloadSuccess = useCallback(
|
||||||
async (state: DownloadState) => {
|
async (state: DownloadState) => {
|
||||||
console.debug('onFileDownloadSuccess', state)
|
console.debug('onFileDownloadSuccess', state)
|
||||||
if (state.downloadType !== 'extension') {
|
|
||||||
// Update model metadata accordingly
|
// Update model metadata accordingly
|
||||||
const model = ModelManager.instance().models.get(state.modelId)
|
const model = ModelManager.instance().models.get(state.modelId)
|
||||||
if (model) {
|
if (model) {
|
||||||
@ -119,31 +93,16 @@ const EventListener = () => {
|
|||||||
setDownloadState(state)
|
setDownloadState(state)
|
||||||
removeDownloadingModel(state.modelId)
|
removeDownloadingModel(state.modelId)
|
||||||
events.emit(ModelEvent.OnModelsUpdate, { fetch: true })
|
events.emit(ModelEvent.OnModelsUpdate, { fetch: true })
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[removeDownloadingModel, setDownloadState]
|
[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(() => {
|
useEffect(() => {
|
||||||
console.debug('EventListenerWrapper: registering event listeners...')
|
console.debug('EventListenerWrapper: registering event listeners...')
|
||||||
events.on(DownloadEvent.onFileDownloadUpdate, onFileDownloadUpdate)
|
events.on(DownloadEvent.onFileDownloadUpdate, onFileDownloadUpdate)
|
||||||
events.on(DownloadEvent.onFileDownloadError, onFileDownloadError)
|
events.on(DownloadEvent.onFileDownloadError, onFileDownloadError)
|
||||||
events.on(DownloadEvent.onFileDownloadSuccess, onFileDownloadSuccess)
|
events.on(DownloadEvent.onFileDownloadSuccess, onFileDownloadSuccess)
|
||||||
events.on(DownloadEvent.onFileDownloadStopped, onFileDownloadStopped)
|
events.on(DownloadEvent.onFileDownloadStopped, onFileDownloadStopped)
|
||||||
events.on(DownloadEvent.onFileUnzipSuccess, onFileUnzipSuccess)
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
console.debug('EventListenerWrapper: unregistering event listeners...')
|
console.debug('EventListenerWrapper: unregistering event listeners...')
|
||||||
@ -151,13 +110,11 @@ const EventListener = () => {
|
|||||||
events.off(DownloadEvent.onFileDownloadError, onFileDownloadError)
|
events.off(DownloadEvent.onFileDownloadError, onFileDownloadError)
|
||||||
events.off(DownloadEvent.onFileDownloadSuccess, onFileDownloadSuccess)
|
events.off(DownloadEvent.onFileDownloadSuccess, onFileDownloadSuccess)
|
||||||
events.off(DownloadEvent.onFileDownloadStopped, onFileDownloadStopped)
|
events.off(DownloadEvent.onFileDownloadStopped, onFileDownloadStopped)
|
||||||
events.off(DownloadEvent.onFileUnzipSuccess, onFileUnzipSuccess)
|
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
onFileDownloadUpdate,
|
onFileDownloadUpdate,
|
||||||
onFileDownloadError,
|
onFileDownloadError,
|
||||||
onFileDownloadSuccess,
|
onFileDownloadSuccess,
|
||||||
onFileUnzipSuccess,
|
|
||||||
onFileDownloadStopped,
|
onFileDownloadStopped,
|
||||||
])
|
])
|
||||||
|
|
||||||
@ -165,7 +122,6 @@ const EventListener = () => {
|
|||||||
<>
|
<>
|
||||||
<AppUpdateListener />
|
<AppUpdateListener />
|
||||||
<ClipboardListener />
|
<ClipboardListener />
|
||||||
<ModelImportListener />
|
|
||||||
<QuickAskListener />
|
<QuickAskListener />
|
||||||
<ModelHandler />
|
<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()
|
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', () => {
|
describe('inActiveEngineProviderAtom', () => {
|
||||||
it('should initialize as an empty array', () => {
|
it('should initialize as an empty array', () => {
|
||||||
const { result } = renderHook(() => useAtomValue(ExtensionAtoms.inActiveEngineProviderAtom))
|
const { result } = renderHook(() => useAtomValue(ExtensionAtoms.inActiveEngineProviderAtom))
|
||||||
|
|||||||
@ -1,45 +1,5 @@
|
|||||||
import { atom } from 'jotai'
|
|
||||||
import { atomWithStorage } from 'jotai/utils'
|
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'
|
const INACTIVE_ENGINE_PROVIDER = 'inActiveEngineProvider'
|
||||||
export const inActiveEngineProviderAtom = atomWithStorage<string[]>(
|
export const inActiveEngineProviderAtom = atomWithStorage<string[]>(
|
||||||
INACTIVE_ENGINE_PROVIDER,
|
INACTIVE_ENGINE_PROVIDER,
|
||||||
|
|||||||
@ -3,8 +3,8 @@ import { useCallback } from 'react'
|
|||||||
import {
|
import {
|
||||||
ExtensionTypeEnum,
|
ExtensionTypeEnum,
|
||||||
ImportingModel,
|
ImportingModel,
|
||||||
LocalImportModelEvent,
|
|
||||||
Model,
|
Model,
|
||||||
|
ModelEvent,
|
||||||
ModelExtension,
|
ModelExtension,
|
||||||
OptionType,
|
OptionType,
|
||||||
events,
|
events,
|
||||||
@ -25,6 +25,7 @@ import {
|
|||||||
downloadedModelsAtom,
|
downloadedModelsAtom,
|
||||||
importingModelsAtom,
|
importingModelsAtom,
|
||||||
removeDownloadingModelAtom,
|
removeDownloadingModelAtom,
|
||||||
|
setImportingModelSuccessAtom,
|
||||||
} from '@/helpers/atoms/Model.atom'
|
} from '@/helpers/atoms/Model.atom'
|
||||||
|
|
||||||
export type ImportModelStage =
|
export type ImportModelStage =
|
||||||
@ -59,6 +60,7 @@ const useImportModel = () => {
|
|||||||
const addDownloadingModel = useSetAtom(addDownloadingModelAtom)
|
const addDownloadingModel = useSetAtom(addDownloadingModelAtom)
|
||||||
const removeDownloadingModel = useSetAtom(removeDownloadingModelAtom)
|
const removeDownloadingModel = useSetAtom(removeDownloadingModelAtom)
|
||||||
const downloadedModels = useAtomValue(downloadedModelsAtom)
|
const downloadedModels = useAtomValue(downloadedModelsAtom)
|
||||||
|
const setImportingModelSuccess = useSetAtom(setImportingModelSuccessAtom)
|
||||||
|
|
||||||
const incrementalModelName = useCallback(
|
const incrementalModelName = useCallback(
|
||||||
(name: string, startIndex: number = 0): string => {
|
(name: string, startIndex: number = 0): string => {
|
||||||
@ -83,10 +85,9 @@ const useImportModel = () => {
|
|||||||
?.importModel(modelId, model.path, model.name, optionType)
|
?.importModel(modelId, model.path, model.name, optionType)
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
removeDownloadingModel(modelId)
|
removeDownloadingModel(modelId)
|
||||||
events.emit(LocalImportModelEvent.onLocalImportModelSuccess, {
|
|
||||||
importId: model.importId,
|
events.emit(ModelEvent.OnModelsUpdate, { fetch: true })
|
||||||
modelId: modelId,
|
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 &&
|
'provider' in extension &&
|
||||||
typeof extension.provider === 'string'
|
typeof extension.provider === 'string'
|
||||||
) {
|
) {
|
||||||
if (
|
if (settings && settings.length > 0) {
|
||||||
(settings && settings.length > 0) ||
|
|
||||||
(await extension.installationState()) !== 'NotRequired'
|
|
||||||
) {
|
|
||||||
engineMenu.push({
|
engineMenu.push({
|
||||||
...extension,
|
...extension,
|
||||||
provider:
|
provider:
|
||||||
|
|||||||
@ -1,14 +1,9 @@
|
|||||||
import React, { Fragment, useEffect, useMemo, useState } from 'react'
|
import React, { Fragment, useEffect, useMemo, useState } from 'react'
|
||||||
|
|
||||||
import {
|
import { SettingComponentProps } from '@janhq/core'
|
||||||
BaseExtension,
|
|
||||||
InstallationState,
|
|
||||||
SettingComponentProps,
|
|
||||||
} from '@janhq/core'
|
|
||||||
|
|
||||||
import { useAtomValue } from 'jotai'
|
import { useAtomValue } from 'jotai'
|
||||||
|
|
||||||
import ExtensionItem from '../CoreExtensions/ExtensionItem'
|
|
||||||
import SettingDetailItem from '../SettingDetail/SettingDetailItem'
|
import SettingDetailItem from '../SettingDetail/SettingDetailItem'
|
||||||
|
|
||||||
import { extensionManager } from '@/extension'
|
import { extensionManager } from '@/extension'
|
||||||
@ -17,11 +12,6 @@ import { selectedSettingAtom } from '@/helpers/atoms/Setting.atom'
|
|||||||
const ExtensionSetting = ({ extensionName }: { extensionName?: string }) => {
|
const ExtensionSetting = ({ extensionName }: { extensionName?: string }) => {
|
||||||
const selectedExtensionName = useAtomValue(selectedSettingAtom)
|
const selectedExtensionName = useAtomValue(selectedSettingAtom)
|
||||||
const [settings, setSettings] = useState<SettingComponentProps[]>([])
|
const [settings, setSettings] = useState<SettingComponentProps[]>([])
|
||||||
const [installationState, setInstallationState] =
|
|
||||||
useState<InstallationState>('NotRequired')
|
|
||||||
const [baseExtension, setBaseExtension] = useState<BaseExtension | undefined>(
|
|
||||||
undefined
|
|
||||||
)
|
|
||||||
|
|
||||||
const currentExtensionName = useMemo(
|
const currentExtensionName = useMemo(
|
||||||
() => extensionName ?? selectedExtensionName,
|
() => extensionName ?? selectedExtensionName,
|
||||||
@ -35,14 +25,11 @@ const ExtensionSetting = ({ extensionName }: { extensionName?: string }) => {
|
|||||||
const baseExtension = extensionManager.getByName(currentExtensionName)
|
const baseExtension = extensionManager.getByName(currentExtensionName)
|
||||||
if (!baseExtension) return
|
if (!baseExtension) return
|
||||||
|
|
||||||
setBaseExtension(baseExtension)
|
|
||||||
if (typeof baseExtension.getSettings === 'function') {
|
if (typeof baseExtension.getSettings === 'function') {
|
||||||
const setting = await baseExtension.getSettings()
|
const setting = await baseExtension.getSettings()
|
||||||
if (setting) allSettings.push(...setting)
|
if (setting) allSettings.push(...setting)
|
||||||
}
|
}
|
||||||
setSettings(allSettings)
|
setSettings(allSettings)
|
||||||
|
|
||||||
setInstallationState(await baseExtension.installationState())
|
|
||||||
}
|
}
|
||||||
getExtensionSettings()
|
getExtensionSettings()
|
||||||
}, [currentExtensionName])
|
}, [currentExtensionName])
|
||||||
@ -75,9 +62,6 @@ const ExtensionSetting = ({ extensionName }: { extensionName?: string }) => {
|
|||||||
onValueUpdated={onValueChanged}
|
onValueUpdated={onValueChanged}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{baseExtension && installationState !== 'NotRequired' && (
|
|
||||||
<ExtensionItem item={baseExtension} />
|
|
||||||
)}
|
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user