refactor: file prefix replace utils & add unit test (#1676)

* refactor: file prefix replace utils

* chore: add unit tests for core module
This commit is contained in:
Louis 2024-01-22 10:05:47 +07:00 committed by GitHub
parent fc3a5c9e01
commit 99d083d84a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 204 additions and 386 deletions

View File

@ -39,6 +39,7 @@ lint: check-file-counts
# Testing # Testing
test: lint test: lint
yarn build:test yarn build:test
yarn test:unit
yarn test yarn test
# Builds and publishes the app # Builds and publishes the app

3
core/.gitignore vendored
View File

@ -6,7 +6,4 @@ coverage
.vscode .vscode
.idea .idea
dist dist
compiled
.awcache
.rpt2_cache
docs docs

7
core/jest.config.js Normal file
View File

@ -0,0 +1,7 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
moduleNameMapper: {
'@/(.*)': '<rootDir>/src/$1',
},
}

View File

@ -12,7 +12,8 @@
"module": "dist/core.es5.js", "module": "dist/core.es5.js",
"typings": "dist/types/index.d.ts", "typings": "dist/types/index.d.ts",
"files": [ "files": [
"dist" "dist",
"types"
], ],
"author": "Jan <service@jan.ai>", "author": "Jan <service@jan.ai>",
"exports": { "exports": {
@ -38,18 +39,23 @@
}, },
"scripts": { "scripts": {
"lint": "tslint --project tsconfig.json -t codeFrame 'src/**/*.ts' 'test/**/*.ts'", "lint": "tslint --project tsconfig.json -t codeFrame 'src/**/*.ts' 'test/**/*.ts'",
"test": "jest",
"prebuild": "rimraf dist", "prebuild": "rimraf dist",
"build": "tsc --module commonjs && rollup -c rollup.config.ts", "build": "tsc --module commonjs && rollup -c rollup.config.ts",
"start": "rollup -c rollup.config.ts -w" "start": "rollup -c rollup.config.ts -w"
}, },
"devDependencies": { "devDependencies": {
"jest": "^25.4.0",
"@types/jest": "^29.5.11",
"@types/node": "^12.0.2", "@types/node": "^12.0.2",
"eslint-plugin-jest": "^23.8.2",
"rollup": "^2.38.5", "rollup": "^2.38.5",
"rollup-plugin-commonjs": "^9.1.8", "rollup-plugin-commonjs": "^9.1.8",
"rollup-plugin-json": "^3.1.0", "rollup-plugin-json": "^3.1.0",
"rollup-plugin-node-resolve": "^5.2.0", "rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-sourcemaps": "^0.6.3", "rollup-plugin-sourcemaps": "^0.6.3",
"rollup-plugin-typescript2": "^0.36.0", "rollup-plugin-typescript2": "^0.36.0",
"ts-jest": "^26.1.1",
"tslib": "^2.6.2", "tslib": "^2.6.2",
"typescript": "^5.2.2" "typescript": "^5.2.2"
} }

View File

@ -1,58 +1,58 @@
import { DownloadRoute } from '../../../api' import { DownloadRoute } from "../../../api";
import { join } from 'path' import { join } from "path";
import { userSpacePath } from '../../extension/manager' import { userSpacePath } from "../../extension/manager";
import { DownloadManager } from '../../download' import { DownloadManager } from "../../download";
import { HttpServer } from '../HttpServer' import { HttpServer } from "../HttpServer";
import { createWriteStream } from 'fs' import { createWriteStream } from "fs";
import { normalizeFilePath } from "../../path";
export const downloadRouter = async (app: HttpServer) => { export const downloadRouter = async (app: HttpServer) => {
app.post(`/${DownloadRoute.downloadFile}`, async (req, res) => { app.post(`/${DownloadRoute.downloadFile}`, async (req, res) => {
const strictSSL = !(req.query.ignoreSSL === 'true'); const strictSSL = !(req.query.ignoreSSL === "true");
const proxy = req.query.proxy?.startsWith('http') ? req.query.proxy : undefined; const proxy = req.query.proxy?.startsWith("http") ? req.query.proxy : undefined;
const body = JSON.parse(req.body as any) const body = JSON.parse(req.body as any);
const normalizedArgs = body.map((arg: any) => { const normalizedArgs = body.map((arg: any) => {
if (typeof arg === 'string' && arg.includes('file:/')) { if (typeof arg === "string") {
return join(userSpacePath, arg.replace('file:/', '')) return join(userSpacePath, normalizeFilePath(arg));
} }
return arg return arg;
}) });
const localPath = normalizedArgs[1] const localPath = normalizedArgs[1];
const fileName = localPath.split('/').pop() ?? '' const fileName = localPath.split("/").pop() ?? "";
const request = require('request') const request = require("request");
const progress = require('request-progress') const progress = require("request-progress");
const rq = request({ url: normalizedArgs[0], strictSSL, proxy }) const rq = request({ url: normalizedArgs[0], strictSSL, proxy });
progress(rq, {}) progress(rq, {})
.on('progress', function (state: any) { .on("progress", function (state: any) {
console.log('download onProgress', state) console.log("download onProgress", state);
}) })
.on('error', function (err: Error) { .on("error", function (err: Error) {
console.log('download onError', err) console.log("download onError", err);
}) })
.on('end', function () { .on("end", function () {
console.log('download onEnd') console.log("download onEnd");
}) })
.pipe(createWriteStream(normalizedArgs[1])) .pipe(createWriteStream(normalizedArgs[1]));
DownloadManager.instance.setRequest(fileName, rq) DownloadManager.instance.setRequest(fileName, rq);
}) });
app.post(`/${DownloadRoute.abortDownload}`, async (req, res) => { app.post(`/${DownloadRoute.abortDownload}`, async (req, res) => {
const body = JSON.parse(req.body as any) const body = JSON.parse(req.body as any);
const normalizedArgs = body.map((arg: any) => { const normalizedArgs = body.map((arg: any) => {
if (typeof arg === 'string' && arg.includes('file:/')) { if (typeof arg === "string") {
return join(userSpacePath, arg.replace('file:/', '')) return join(userSpacePath, normalizeFilePath(arg));
} }
return arg return arg;
}) });
const localPath = normalizedArgs[0] const localPath = normalizedArgs[0];
const fileName = localPath.split('/').pop() ?? '' const fileName = localPath.split("/").pop() ?? "";
console.debug('fileName', fileName) const rq = DownloadManager.instance.networkRequests[fileName];
const rq = DownloadManager.instance.networkRequests[fileName] DownloadManager.instance.networkRequests[fileName] = undefined;
DownloadManager.instance.networkRequests[fileName] = undefined rq?.abort();
rq?.abort() });
}) };
}

View File

@ -6,3 +6,4 @@ export * from './download'
export * from './module' export * from './module'
export * from './api' export * from './api'
export * from './log' export * from './log'
export * from './path'

9
core/src/node/path.ts Normal file
View File

@ -0,0 +1,9 @@
/**
* Normalize file path
* Remove all file protocol prefix
* @param path
* @returns
*/
export function normalizeFilePath(path: string): string {
return path.replace(/^(file:[\\/]+)([^:\s]+)$/, "$2");
}

View File

@ -0,0 +1,12 @@
import { normalizeFilePath } from "../../src/node/path";
describe("Test file normalize", () => {
test("returns no file protocol prefix on Unix", async () => {
expect(normalizeFilePath("file://test.txt")).toBe("test.txt");
expect(normalizeFilePath("file:/test.txt")).toBe("test.txt");
});
test("returns no file protocol prefix on Windows", async () => {
expect(normalizeFilePath("file:\\\\test.txt")).toBe("test.txt");
expect(normalizeFilePath("file:\\test.txt")).toBe("test.txt");
});
});

View File

@ -13,7 +13,7 @@
"declarationDir": "dist/types", "declarationDir": "dist/types",
"outDir": "dist/lib", "outDir": "dist/lib",
"importHelpers": true, "importHelpers": true,
"typeRoots": ["node_modules/@types"] "types": ["@types/jest"]
}, },
"include": ["src"] "include": ["src"]
} }

View File

@ -5,7 +5,7 @@ import request from 'request'
import { createWriteStream, renameSync } from 'fs' import { createWriteStream, renameSync } from 'fs'
import { DownloadEvent, DownloadRoute } from '@janhq/core' import { DownloadEvent, DownloadRoute } from '@janhq/core'
const progress = require('request-progress') const progress = require('request-progress')
import { DownloadManager } from '@janhq/core/node' import { DownloadManager, normalizeFilePath } from '@janhq/core/node'
export function handleDownloaderIPCs() { export function handleDownloaderIPCs() {
/** /**
@ -54,66 +54,68 @@ export function handleDownloaderIPCs() {
* @param url - The URL to download the file from. * @param url - The URL to download the file from.
* @param fileName - The name to give the downloaded file. * @param fileName - The name to give the downloaded file.
*/ */
ipcMain.handle(DownloadRoute.downloadFile, async (_event, url, fileName, network) => { ipcMain.handle(
const strictSSL = !network?.ignoreSSL; DownloadRoute.downloadFile,
const proxy = network?.proxy?.startsWith('http') ? network.proxy : undefined; async (_event, url, fileName, network) => {
const userDataPath = join(app.getPath('home'), 'jan') const strictSSL = !network?.ignoreSSL
if ( const proxy = network?.proxy?.startsWith('http')
typeof fileName === 'string' && ? network.proxy
(fileName.includes('file:/') || fileName.includes('file:\\')) : undefined
) { const userDataPath = join(app.getPath('home'), 'jan')
fileName = fileName.replace('file:/', '').replace('file:\\', '') if (typeof fileName === 'string') {
} fileName = normalizeFilePath(fileName)
const destination = resolve(userDataPath, fileName) }
const rq = request({ url, strictSSL, proxy }) const destination = resolve(userDataPath, fileName)
const rq = request({ url, strictSSL, proxy })
// Put request to download manager instance // Put request to download manager instance
DownloadManager.instance.setRequest(fileName, rq) DownloadManager.instance.setRequest(fileName, rq)
// Downloading file to a temp file first // Downloading file to a temp file first
const downloadingTempFile = `${destination}.download` const downloadingTempFile = `${destination}.download`
progress(rq, {})
.on('progress', function (state: any) {
WindowManager?.instance.currentWindow?.webContents.send(
DownloadEvent.onFileDownloadUpdate,
{
...state,
fileName,
}
)
})
.on('error', function (err: Error) {
WindowManager?.instance.currentWindow?.webContents.send(
DownloadEvent.onFileDownloadError,
{
fileName,
err,
}
)
})
.on('end', function () {
if (DownloadManager.instance.networkRequests[fileName]) {
// Finished downloading, rename temp file to actual file
renameSync(downloadingTempFile, destination)
progress(rq, {})
.on('progress', function (state: any) {
WindowManager?.instance.currentWindow?.webContents.send( WindowManager?.instance.currentWindow?.webContents.send(
DownloadEvent.onFileDownloadSuccess, DownloadEvent.onFileDownloadUpdate,
{ {
...state,
fileName, fileName,
} }
) )
DownloadManager.instance.setRequest(fileName, undefined) })
} else { .on('error', function (err: Error) {
WindowManager?.instance.currentWindow?.webContents.send( WindowManager?.instance.currentWindow?.webContents.send(
DownloadEvent.onFileDownloadError, DownloadEvent.onFileDownloadError,
{ {
fileName, fileName,
err: { message: 'aborted' }, err,
} }
) )
} })
}) .on('end', function () {
.pipe(createWriteStream(downloadingTempFile)) if (DownloadManager.instance.networkRequests[fileName]) {
}) // Finished downloading, rename temp file to actual file
renameSync(downloadingTempFile, destination)
WindowManager?.instance.currentWindow?.webContents.send(
DownloadEvent.onFileDownloadSuccess,
{
fileName,
}
)
DownloadManager.instance.setRequest(fileName, undefined)
} else {
WindowManager?.instance.currentWindow?.webContents.send(
DownloadEvent.onFileDownloadError,
{
fileName,
err: { message: 'aborted' },
}
)
}
})
.pipe(createWriteStream(downloadingTempFile))
}
)
} }

View File

@ -7,6 +7,7 @@ import { userSpacePath, getResourcePath } from './../utils/path'
import fs from 'fs' import fs from 'fs'
import { join } from 'path' import { join } from 'path'
import { FileStat } from '@janhq/core' import { FileStat } from '@janhq/core'
import { normalizeFilePath } from '@janhq/core/node'
/** /**
* Handles file system extensions operations. * Handles file system extensions operations.
@ -42,11 +43,7 @@ export function handleFileMangerIPCs() {
ipcMain.handle( ipcMain.handle(
FileManagerRoute.fileStat, FileManagerRoute.fileStat,
async (_event, path: string): Promise<FileStat | undefined> => { async (_event, path: string): Promise<FileStat | undefined> => {
const normalizedPath = path const normalizedPath = normalizeFilePath(path)
.replace(`file://`, '')
.replace(`file:/`, '')
.replace(`file:\\\\`, '')
.replace(`file:\\`, '')
const fullPath = join(userSpacePath, normalizedPath) const fullPath = join(userSpacePath, normalizedPath)
const isExist = fs.existsSync(fullPath) const isExist = fs.existsSync(fullPath)

View File

@ -3,6 +3,7 @@ import { ipcMain } from 'electron'
import { FileSystemRoute } from '@janhq/core' import { FileSystemRoute } from '@janhq/core'
import { userSpacePath } from '../utils/path' import { userSpacePath } from '../utils/path'
import { join } from 'path' import { join } from 'path'
import { normalizeFilePath } from '@janhq/core/node'
/** /**
* Handles file system operations. * Handles file system operations.
*/ */
@ -15,14 +16,7 @@ export function handleFsIPCs() {
...args.map((arg) => ...args.map((arg) =>
typeof arg === 'string' && typeof arg === 'string' &&
(arg.includes(`file:/`) || arg.includes(`file:\\`)) (arg.includes(`file:/`) || arg.includes(`file:\\`))
? join( ? join(userSpacePath, normalizeFilePath(arg))
userSpacePath,
arg
.replace(`file://`, '')
.replace(`file:/`, '')
.replace(`file:\\\\`, '')
.replace(`file:\\`, '')
)
: arg : arg
) )
) )

View File

@ -11,19 +11,15 @@
], ],
"nohoist": [ "nohoist": [
"uikit", "uikit",
"uikit/*",
"core", "core",
"core/*",
"electron", "electron",
"electron/**",
"web", "web",
"web/**", "server"
"server",
"server/**"
] ]
}, },
"scripts": { "scripts": {
"lint": "yarn workspace jan lint && yarn workspace jan-web lint", "lint": "yarn workspace jan lint && yarn workspace jan-web lint",
"test:unit": "yarn workspace @janhq/core test",
"test": "yarn workspace jan test:e2e", "test": "yarn workspace jan test:e2e",
"copy:assets": "cpx \"models/**\" \"electron/models/\" && cpx \"docs/openapi/**\" \"electron/docs/openapi\"", "copy:assets": "cpx \"models/**\" \"electron/models/\" && cpx \"docs/openapi/**\" \"electron/docs/openapi\"",
"dev:electron": "yarn copy:assets && yarn workspace jan dev", "dev:electron": "yarn copy:assets && yarn workspace jan dev",

View File

@ -29,7 +29,8 @@ export default function FeatureToggleWrapper({
const EXPERIMENTAL_FEATURE = 'experimentalFeature' const EXPERIMENTAL_FEATURE = 'experimentalFeature'
const IGNORE_SSL = 'ignoreSSLFeature' const IGNORE_SSL = 'ignoreSSLFeature'
const HTTPS_PROXY_FEATURE = 'httpsProxyFeature' const HTTPS_PROXY_FEATURE = 'httpsProxyFeature'
const [experimentalFeature, directSetExperimentalFeature] = useState<boolean>(false) const [experimentalFeature, directSetExperimentalFeature] =
useState<boolean>(false)
const [ignoreSSL, directSetIgnoreSSL] = useState<boolean>(false) const [ignoreSSL, directSetIgnoreSSL] = useState<boolean>(false)
const [proxy, directSetProxy] = useState<string>('') const [proxy, directSetProxy] = useState<string>('')
@ -37,12 +38,8 @@ export default function FeatureToggleWrapper({
directSetExperimentalFeature( directSetExperimentalFeature(
localStorage.getItem(EXPERIMENTAL_FEATURE) === 'true' localStorage.getItem(EXPERIMENTAL_FEATURE) === 'true'
) )
directSetIgnoreSSL( directSetIgnoreSSL(localStorage.getItem(IGNORE_SSL) === 'true')
localStorage.getItem(IGNORE_SSL) === 'true' directSetProxy(localStorage.getItem(HTTPS_PROXY_FEATURE) ?? '')
)
directSetProxy(
localStorage.getItem(HTTPS_PROXY_FEATURE) ?? ""
)
}, []) }, [])
const setExperimentalFeature = (on: boolean) => { const setExperimentalFeature = (on: boolean) => {

View File

@ -104,8 +104,8 @@ export const useCreateNewThread = () => {
} }
extensionManager extensionManager
.get<ConversationalExtension>(ExtensionType.Conversational) .get<ConversationalExtension>(ExtensionType.Conversational)
?.saveThread(thread) ?.saveThread(thread)
} }
return { return {

View File

@ -1,3 +1,5 @@
import { useContext } from 'react'
import { import {
Model, Model,
ExtensionType, ExtensionType,
@ -8,14 +10,14 @@ import {
import { useSetAtom } from 'jotai' import { useSetAtom } from 'jotai'
import { FeatureToggleContext } from '@/context/FeatureToggle'
import { modelBinFileName } from '@/utils/model' import { modelBinFileName } from '@/utils/model'
import { useDownloadState } from './useDownloadState' import { useDownloadState } from './useDownloadState'
import { extensionManager } from '@/extension/ExtensionManager' import { extensionManager } from '@/extension/ExtensionManager'
import { addNewDownloadingModelAtom } from '@/helpers/atoms/Model.atom' import { addNewDownloadingModelAtom } from '@/helpers/atoms/Model.atom'
import { useContext } from 'react'
import { FeatureToggleContext } from '@/context/FeatureToggle'
export default function useDownloadModel() { export default function useDownloadModel() {
const { ignoreSSL, proxy } = useContext(FeatureToggleContext) const { ignoreSSL, proxy } = useContext(FeatureToggleContext)

View File

@ -7,7 +7,7 @@
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "eslint .",
"format": "prettier --write \"**/*.{js,jsx,ts,tsx}\"", "format": "prettier --write \"**/*.{js,jsx,ts,tsx}\"",
"compile": "tsc --noEmit -p . --pretty" "compile": "tsc --noEmit -p . --pretty"
}, },

View File

@ -1,19 +1,16 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import { useContext, useEffect, useState, useCallback, ChangeEvent } from 'react' import {
useContext,
useEffect,
useState,
useCallback,
ChangeEvent,
} from 'react'
import { fs } from '@janhq/core' import { fs } from '@janhq/core'
import { import { Switch, Button, Input } from '@janhq/uikit'
Switch,
Button,
Input,
Modal,
ModalContent,
ModalHeader,
ModalTitle,
ModalTrigger,
} from '@janhq/uikit'
import ShortcutModal from '@/containers/ShortcutModal' import ShortcutModal from '@/containers/ShortcutModal'
@ -24,22 +21,30 @@ import { FeatureToggleContext } from '@/context/FeatureToggle'
import { useSettings } from '@/hooks/useSettings' import { useSettings } from '@/hooks/useSettings'
const Advanced = () => { const Advanced = () => {
const { experimentalFeature, setExperimentalFeature, ignoreSSL, setIgnoreSSL, proxy, setProxy } = const {
useContext(FeatureToggleContext) experimentalFeature,
setExperimentalFeature,
ignoreSSL,
setIgnoreSSL,
proxy,
setProxy,
} = useContext(FeatureToggleContext)
const [partialProxy, setPartialProxy] = useState<string>(proxy) const [partialProxy, setPartialProxy] = useState<string>(proxy)
const [gpuEnabled, setGpuEnabled] = useState<boolean>(false) const [gpuEnabled, setGpuEnabled] = useState<boolean>(false)
const { readSettings, saveSettings, validateSettings, setShowNotification } = const { readSettings, saveSettings, validateSettings, setShowNotification } =
useSettings() useSettings()
const onProxyChange = useCallback((event: ChangeEvent<HTMLInputElement>) => { const onProxyChange = useCallback(
const value = event.target.value || '' (event: ChangeEvent<HTMLInputElement>) => {
setPartialProxy(value) const value = event.target.value || ''
if (value.trim().startsWith('http')) { setPartialProxy(value)
setProxy(value.trim()) if (value.trim().startsWith('http')) {
} setProxy(value.trim())
else { } else {
setProxy('') setProxy('')
} }
}, [setPartialProxy, setProxy]) },
[setPartialProxy, setProxy]
)
useEffect(() => { useEffect(() => {
readSettings().then((settings) => { readSettings().then((settings) => {
@ -115,15 +120,14 @@ const Advanced = () => {
<div className="flex w-full items-start justify-between border-b border-border py-4 first:pt-0 last:border-none"> <div className="flex w-full items-start justify-between border-b border-border py-4 first:pt-0 last:border-none">
<div className="w-4/5 flex-shrink-0 space-y-1.5"> <div className="w-4/5 flex-shrink-0 space-y-1.5">
<div className="flex gap-x-2"> <div className="flex gap-x-2">
<h6 className="text-sm font-semibold capitalize"> <h6 className="text-sm font-semibold capitalize">HTTPS Proxy</h6>
HTTPS Proxy
</h6>
</div> </div>
<p className="whitespace-pre-wrap leading-relaxed"> <p className="whitespace-pre-wrap leading-relaxed">
Specify the HTTPS proxy or leave blank (proxy auto-configuration and SOCKS not supported). Specify the HTTPS proxy or leave blank (proxy auto-configuration and
SOCKS not supported).
</p> </p>
<Input <Input
placeholder={"http://<user>:<password>@<domain or IP>:<port>"} placeholder={'http://<user>:<password>@<domain or IP>:<port>'}
value={partialProxy} value={partialProxy}
onChange={onProxyChange} onChange={onProxyChange}
/> />
@ -138,7 +142,8 @@ const Advanced = () => {
</h6> </h6>
</div> </div>
<p className="whitespace-pre-wrap leading-relaxed"> <p className="whitespace-pre-wrap leading-relaxed">
Allow self-signed or unverified certificates - may be required for certain proxies. Allow self-signed or unverified certificates - may be required for
certain proxies.
</p> </p>
</div> </div>
<Switch <Switch

View File

@ -1,101 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
type Props = {
extensionName: string
preferenceValues: any
preferenceItems: any
}
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
Input,
FormLabel,
FormMessage,
Button,
} from '@janhq/uikit'
import * as z from 'zod'
import { toaster } from '@/containers/Toast'
import { formatExtensionsName } from '@/utils/converter'
const PreferenceExtensions = (props: Props) => {
const { extensionName, preferenceValues, preferenceItems } = props
const FormSchema = z.record(
z
.string({ required_error: 'Field is Required' })
.min(1, { message: 'Field is Required' })
)
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: preferenceValues.reduce(
(obj: any, item: { key: any; value: any }) =>
Object.assign(obj, { [item.key]: item.value }),
{}
),
})
const onSubmit = async (values: z.infer<typeof FormSchema>) => {
for (const [key, value] of Object.entries(values)) {
// await preferences.set(extensionName, key, value)
// await execute(ExtensionService.OnPreferencesUpdate, {})
}
toaster({
title: formatExtensionsName(extensionName),
description: 'Successfully updated preferences',
})
}
return (
<div className="mx-auto w-full lg:mt-10 lg:w-1/2">
<h6 className="mb-6 text-lg font-semibold capitalize">
{formatExtensionsName(extensionName)}
</h6>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
{preferenceItems
.filter((x: any) => x.extensionName === extensionName)
?.map((e: any) => (
<FormField
key={e.preferenceKey}
control={form.control}
name={e.preferenceKey}
render={({ field }) => (
<FormItem>
<FormLabel>{e.preferenceName}</FormLabel>
<FormDescription className="mb-2">
{e.preferenceDescription}
</FormDescription>
<FormControl>
<Input
placeholder={`Enter your ${e.preferenceName}`}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
))}
<div className="pt-4">
<Button type="submit" block>
Submit
</Button>
</div>
</form>
</Form>
</div>
)
}
export default PreferenceExtensions

View File

@ -1,38 +1,17 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/ban-ts-comment */
import React, { useState, useEffect, useRef, useContext } from 'react' import React, { useState, useEffect, useRef } from 'react'
import { Button } from '@janhq/uikit' import { Button } from '@janhq/uikit'
import { FeatureToggleContext } from '@/context/FeatureToggle'
import { formatExtensionsName } from '@/utils/converter' import { formatExtensionsName } from '@/utils/converter'
import { extensionManager } from '@/extension' import { extensionManager } from '@/extension'
import Extension from '@/extension/Extension'
const ExtensionCatalog = () => { const ExtensionCatalog = () => {
const [activeExtensions, setActiveExtensions] = useState<any[]>([]) const [activeExtensions, setActiveExtensions] = useState<Extension[]>([])
const [extensionCatalog, setExtensionCatalog] = useState<any[]>([])
const fileInputRef = useRef<HTMLInputElement | null>(null) const fileInputRef = useRef<HTMLInputElement | null>(null)
const { experimentalFeature } = useContext(FeatureToggleContext)
/**
* Loads the extension catalog module from a CDN and sets it as the extension catalog state.
*/
useEffect(() => {
if (!window.electronAPI) {
return
}
// Get extension manifest
import(/* webpackIgnore: true */ PLUGIN_CATALOG + `?t=${Date.now()}`).then(
(data) => {
if (Array.isArray(data.default) && experimentalFeature)
setExtensionCatalog(data.default)
}
)
}, [experimentalFeature])
/** /**
* Fetches the active extensions and their preferences from the `extensions` and `preferences` modules. * Fetches the active extensions and their preferences from the `extensions` and `preferences` modules.
* If the `experimentComponent` extension point is available, it executes the extension point and * If the `experimentComponent` extension point is available, it executes the extension point and
@ -90,57 +69,28 @@ const ExtensionCatalog = () => {
return ( return (
<div className="block w-full"> <div className="block w-full">
{extensionCatalog {activeExtensions.map((item, i) => {
.concat( return (
activeExtensions.filter( <div
(e) => !(extensionCatalog ?? []).some((p) => p.name === e.name) key={i}
) ?? [] className="flex w-full items-start justify-between border-b border-border py-4 first:pt-0 last:border-none"
) >
.map((item, i) => { <div className="w-4/5 flex-shrink-0 space-y-1.5">
const isActiveExtension = activeExtensions.some( <div className="flex gap-x-2">
(x) => x.name === item.name <h6 className="text-sm font-semibold capitalize">
) {formatExtensionsName(item.name ?? item.description ?? '')}
const installedExtension = activeExtensions.filter( </h6>
(p) => p.name === item.name <p className="whitespace-pre-wrap font-semibold leading-relaxed ">
)[0] v{item.version}
const updateVersionExtensions = Number(
installedExtension?.version.replaceAll('.', '')
)
const hasUpdateVersionExtensions =
item.version.replaceAll('.', '') > updateVersionExtensions
return (
<div
key={i}
className="flex w-full items-start justify-between border-b border-border py-4 first:pt-0 last:border-none"
>
<div className="w-4/5 flex-shrink-0 space-y-1.5">
<div className="flex gap-x-2">
<h6 className="text-sm font-semibold capitalize">
{formatExtensionsName(item.name)}
</h6>
<p className="whitespace-pre-wrap font-semibold leading-relaxed ">
v{item.version}
</p>
</div>
<p className="whitespace-pre-wrap leading-relaxed ">
{item.description}
</p> </p>
{isActiveExtension && (
<div className="flex items-center gap-x-2">
<p className="whitespace-pre-wrap leading-relaxed ">
Installed{' '}
{hasUpdateVersionExtensions
? `v${installedExtension.version}`
: 'the latest version'}
</p>
</div>
)}
</div> </div>
<p className="whitespace-pre-wrap leading-relaxed ">
{item.description}
</p>
</div> </div>
) </div>
})} )
})}
{/* Manual Installation */} {/* Manual Installation */}
<div className="flex w-full items-start justify-between border-b border-border py-4 first:pt-0 last:border-none"> <div className="flex w-full items-start justify-between border-b border-border py-4 first:pt-0 last:border-none">
<div className="w-4/5 flex-shrink-0 space-y-1.5"> <div className="w-4/5 flex-shrink-0 space-y-1.5">

View File

@ -9,8 +9,7 @@ import { twMerge } from 'tailwind-merge'
import Advanced from '@/screens/Settings/Advanced' import Advanced from '@/screens/Settings/Advanced'
import AppearanceOptions from '@/screens/Settings/Appearance' import AppearanceOptions from '@/screens/Settings/Appearance'
import ExtensionCatalog from '@/screens/Settings/CoreExtensions/ExtensionsCatalog' import ExtensionCatalog from '@/screens/Settings/CoreExtensions'
import PreferenceExtensions from '@/screens/Settings/CoreExtensions/PreferenceExtensions'
import Models from '@/screens/Settings/Models' import Models from '@/screens/Settings/Models'
@ -19,8 +18,6 @@ import { formatExtensionsName } from '@/utils/converter'
const SettingsScreen = () => { const SettingsScreen = () => {
const [activeStaticMenu, setActiveStaticMenu] = useState('My Models') const [activeStaticMenu, setActiveStaticMenu] = useState('My Models')
const [menus, setMenus] = useState<any[]>([]) const [menus, setMenus] = useState<any[]>([])
const [preferenceItems, setPreferenceItems] = useState<any[]>([])
const [preferenceValues, setPreferenceValues] = useState<any[]>([])
useEffect(() => { useEffect(() => {
const menu = ['My Models', 'My Settings', 'Advanced Settings'] const menu = ['My Models', 'My Settings', 'Advanced Settings']
@ -31,12 +28,6 @@ const SettingsScreen = () => {
setMenus(menu) setMenus(menu)
}, []) }, [])
const preferenceExtensions = preferenceItems
.map((x) => x.extensionnName)
.filter((x, i) => {
// return prefere/nceItems.map((x) => x.extensionName).indexOf(x) === i
})
const [activePreferenceExtension, setActivePreferenceExtension] = useState('') const [activePreferenceExtension, setActivePreferenceExtension] = useState('')
const handleShowOptions = (menu: string) => { const handleShowOptions = (menu: string) => {
@ -52,15 +43,6 @@ const SettingsScreen = () => {
case 'My Models': case 'My Models':
return <Models /> return <Models />
default:
return (
<PreferenceExtensions
extensionName={menu}
preferenceItems={preferenceItems}
preferenceValues={preferenceValues}
/>
)
} }
} }
@ -97,45 +79,6 @@ const SettingsScreen = () => {
})} })}
</div> </div>
</div> </div>
<div className="mt-5 flex-shrink-0">
{preferenceExtensions.length > 0 && (
<label className="font-bold uppercase text-muted-foreground">
Core Extensions
</label>
)}
<div className="mt-2 font-medium">
{preferenceExtensions.map((menu, i) => {
const isActive = activePreferenceExtension === menu
return (
<div key={i} className="relative my-0.5 block py-1.5">
<div
onClick={() => {
setActivePreferenceExtension(menu)
setActiveStaticMenu('')
}}
className="block w-full cursor-pointer"
>
<span
className={twMerge(
'capitalize',
isActive && 'relative z-10'
)}
>
{formatExtensionsName(String(menu))}
</span>
</div>
{isActive ? (
<m.div
className="absolute inset-0 -left-3 h-full w-[calc(100%+24px)] rounded-md bg-primary/50"
layoutId="active-static-menu"
/>
) : null}
</div>
)
})}
</div>
</div>
</div> </div>
</ScrollArea> </ScrollArea>
</div> </div>