refactor: clean up deprecated components and events (#4769)

This commit is contained in:
Louis 2025-03-03 22:20:39 +07:00 committed by GitHub
parent c79c10c96b
commit a8aa938f42
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 67 additions and 1243 deletions

View File

@ -25,7 +25,6 @@ export default defineConfig([
'@types/pacote',
'@npmcli/arborist',
'ulidx',
'node-fetch',
'fs',
'request',
'crypto',

View File

@ -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'

View File

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

View File

@ -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

View File

@ -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.
*

View File

@ -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.

View File

@ -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)

View File

@ -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(),
})
)
})
})

View File

@ -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()
}
}

View File

@ -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);
});

View File

@ -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
}
}

View File

@ -1,5 +1,4 @@
export * from './config'
export * from './download'
export * from './logger'
export * from './module'
export * from './path'

View File

@ -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[]

View File

@ -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
*/

View File

@ -1,8 +0,0 @@
export type FileDownloadRequest = {
downloadId: string
url: string
localPath: string
fileName: string
displayName: string
metadata: Record<string, string | number>
}

View File

@ -1,6 +1,4 @@
export * from './systemResourceInfo'
export * from './promptTemplate'
export * from './appUpdate'
export * from './fileDownloadRequest'
export * from './networkConfig'
export * from './selectFiles'

View File

@ -1,4 +0,0 @@
export type NetworkConfig = {
proxy?: string
ignoreSSL?: boolean
}

View File

@ -1,3 +1,2 @@
export * from './threadEntity'
export * from './threadInterface'
export * from './threadEvent'

View File

@ -1,6 +0,0 @@
import { ThreadEvent } from './threadEvent';
it('should have the correct values', () => {
expect(ThreadEvent.OnThreadStarted).toBe('OnThreadStarted');
});

View File

@ -1,4 +0,0 @@
export enum ThreadEvent {
/** The `OnThreadStarted` event is emitted when a thread is started. */
OnThreadStarted = 'OnThreadStarted',
}

View File

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

View File

@ -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)

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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 />

View File

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

View File

@ -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 />
</>

View File

@ -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

View File

@ -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))

View File

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

View File

@ -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)
})
}
})

View File

@ -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&nbsp;
{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

View File

@ -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:

View File

@ -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>
)
}