Merge branch 'main' into add/model-list
This commit is contained in:
commit
a2a81e9639
1
Makefile
1
Makefile
@ -18,6 +18,7 @@ ifeq ($(OS),Windows_NT)
|
||||
yarn config set network-timeout 300000
|
||||
endif
|
||||
yarn build:core
|
||||
yarn build:server
|
||||
yarn install
|
||||
yarn build:extensions
|
||||
|
||||
|
||||
@ -22,6 +22,27 @@
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
},
|
||||
"exports": {
|
||||
".": "./dist/core.umd.js",
|
||||
"./sdk": "./dist/core.umd.js",
|
||||
"./node": "./dist/node/index.cjs.js"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
".": [
|
||||
"./dist/core.es5.js.map",
|
||||
"./dist/types/index.d.ts"
|
||||
],
|
||||
"sdk": [
|
||||
"./dist/core.es5.js.map",
|
||||
"./dist/types/index.d.ts"
|
||||
],
|
||||
"node": [
|
||||
"./dist/node/index.cjs.js.map",
|
||||
"./dist/types/node/index.d.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "tslint --project tsconfig.json -t codeFrame 'src/**/*.ts' 'test/**/*.ts'",
|
||||
"prebuild": "rimraf dist",
|
||||
|
||||
@ -8,14 +8,15 @@ const pkg = require('./package.json')
|
||||
|
||||
const libraryName = 'core'
|
||||
|
||||
export default {
|
||||
export default [
|
||||
{
|
||||
input: `src/index.ts`,
|
||||
output: [
|
||||
{ file: pkg.main, name: libraryName, format: 'umd', sourcemap: true },
|
||||
{ file: pkg.module, format: 'es', sourcemap: true },
|
||||
],
|
||||
// Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash')
|
||||
external: [],
|
||||
external: ['path'],
|
||||
watch: {
|
||||
include: 'src/**',
|
||||
},
|
||||
@ -34,4 +35,42 @@ export default {
|
||||
// Resolve source maps to the original source
|
||||
sourceMaps(),
|
||||
],
|
||||
}
|
||||
},
|
||||
{
|
||||
input: `src/node/index.ts`,
|
||||
output: [{ file: 'dist/node/index.cjs.js', format: 'cjs', sourcemap: true }],
|
||||
// Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash')
|
||||
external: [
|
||||
'fs/promises',
|
||||
'path',
|
||||
'pacote',
|
||||
'@types/pacote',
|
||||
'@npmcli/arborist',
|
||||
'ulid',
|
||||
'node-fetch',
|
||||
'fs',
|
||||
'request',
|
||||
'crypto',
|
||||
'url',
|
||||
'http',
|
||||
],
|
||||
watch: {
|
||||
include: 'src/node/**',
|
||||
},
|
||||
plugins: [
|
||||
// Allow json resolution
|
||||
json(),
|
||||
// Compile TypeScript files
|
||||
typescript({ useTsconfigDeclarationDir: true }),
|
||||
// Allow bundling cjs modules (unlike webpack, rollup doesn't understand cjs)
|
||||
commonjs(),
|
||||
// Allow node_modules resolution, so you can use 'external' to control
|
||||
// which external modules to include in the bundle
|
||||
// https://github.com/rollup/rollup-plugin-node-resolve#usage
|
||||
resolve(),
|
||||
|
||||
// Resolve source maps to the original source
|
||||
sourceMaps(),
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
@ -5,11 +5,11 @@
|
||||
export enum AppRoute {
|
||||
appDataPath = 'appDataPath',
|
||||
appVersion = 'appVersion',
|
||||
getResourcePath = 'getResourcePath',
|
||||
openExternalUrl = 'openExternalUrl',
|
||||
openAppDirectory = 'openAppDirectory',
|
||||
openFileExplore = 'openFileExplorer',
|
||||
relaunch = 'relaunch',
|
||||
joinPath = 'joinPath'
|
||||
}
|
||||
|
||||
export enum AppEvent {
|
||||
@ -40,20 +40,20 @@ export enum ExtensionRoute {
|
||||
uninstallExtension = 'uninstallExtension',
|
||||
}
|
||||
export enum FileSystemRoute {
|
||||
appendFile = 'appendFile',
|
||||
copyFile = 'copyFile',
|
||||
syncFile = 'syncFile',
|
||||
deleteFile = 'deleteFile',
|
||||
exists = 'exists',
|
||||
getResourcePath = 'getResourcePath',
|
||||
appendFileSync = 'appendFileSync',
|
||||
copyFileSync = 'copyFileSync',
|
||||
unlinkSync = 'unlinkSync',
|
||||
existsSync = 'existsSync',
|
||||
readdirSync = 'readdirSync',
|
||||
mkdirSync = 'mkdirSync',
|
||||
readFileSync = 'readFileSync',
|
||||
rmdirSync = 'rmdirSync',
|
||||
writeFileSync = 'writeFileSync',
|
||||
}
|
||||
export enum FileManagerRoute {
|
||||
synceFile = 'syncFile',
|
||||
getUserSpace = 'getUserSpace',
|
||||
isDirectory = 'isDirectory',
|
||||
listFiles = 'listFiles',
|
||||
mkdir = 'mkdir',
|
||||
readFile = 'readFile',
|
||||
readLineByLine = 'readLineByLine',
|
||||
rmdir = 'rmdir',
|
||||
writeFile = 'writeFile',
|
||||
getResourcePath = 'getResourcePath',
|
||||
}
|
||||
|
||||
export type ApiFunction = (...args: any[]) => any
|
||||
@ -82,17 +82,23 @@ export type FileSystemRouteFunctions = {
|
||||
[K in FileSystemRoute]: ApiFunction
|
||||
}
|
||||
|
||||
export type FileManagerRouteFunctions = {
|
||||
[K in FileManagerRoute]: ApiFunction
|
||||
}
|
||||
|
||||
export type APIFunctions = AppRouteFunctions &
|
||||
AppEventFunctions &
|
||||
DownloadRouteFunctions &
|
||||
DownloadEventFunctions &
|
||||
ExtensionRouteFunctions &
|
||||
FileSystemRouteFunctions
|
||||
FileSystemRouteFunctions &
|
||||
FileManagerRoute
|
||||
|
||||
export const APIRoutes = [
|
||||
...Object.values(AppRoute),
|
||||
...Object.values(DownloadRoute),
|
||||
...Object.values(ExtensionRoute),
|
||||
...Object.values(FileSystemRoute),
|
||||
...Object.values(FileManagerRoute),
|
||||
]
|
||||
export const APIEvents = [...Object.values(AppEvent), ...Object.values(DownloadEvent)]
|
||||
|
||||
@ -44,6 +44,13 @@ const getUserSpace = (): Promise<string> => global.core.api?.getUserSpace()
|
||||
const openFileExplorer: (path: string) => Promise<any> = (path) =>
|
||||
global.core.api?.openFileExplorer(path)
|
||||
|
||||
/**
|
||||
* Joins multiple paths together.
|
||||
* @param paths - The paths to join.
|
||||
* @returns {Promise<string>} A promise that resolves with the joined path.
|
||||
*/
|
||||
const joinPath: (paths: string[]) => Promise<string> = (paths) => global.core.api?.joinPath(paths)
|
||||
|
||||
const getResourcePath: () => Promise<string> = () => global.core.api?.getResourcePath()
|
||||
|
||||
/**
|
||||
@ -66,4 +73,5 @@ export {
|
||||
getUserSpace,
|
||||
openFileExplorer,
|
||||
getResourcePath,
|
||||
joinPath,
|
||||
}
|
||||
|
||||
@ -1,89 +1,74 @@
|
||||
/**
|
||||
* Writes data to a file at the specified path.
|
||||
* @param {string} path - The path to the file.
|
||||
* @param {string} data - The data to write to the file.
|
||||
* @returns {Promise<any>} A Promise that resolves when the file is written successfully.
|
||||
*/
|
||||
const writeFile: (path: string, data: string) => Promise<any> = (path, data) =>
|
||||
global.core.api?.writeFile(path, data)
|
||||
|
||||
/**
|
||||
* Checks whether the path is a directory.
|
||||
* @param path - The path to check.
|
||||
* @returns {boolean} A boolean indicating whether the path is a directory.
|
||||
*/
|
||||
const isDirectory = (path: string): Promise<boolean> => global.core.api?.isDirectory(path)
|
||||
const writeFileSync = (...args: any[]) => global.core.api?.writeFileSync(...args)
|
||||
|
||||
/**
|
||||
* Reads the contents of a file at the specified path.
|
||||
* @param {string} path - The path of the file to read.
|
||||
* @returns {Promise<any>} A Promise that resolves with the contents of the file.
|
||||
*/
|
||||
const readFile: (path: string) => Promise<any> = (path) => global.core.api?.readFile(path)
|
||||
const readFileSync = (...args: any[]) => global.core.api?.readFileSync(...args)
|
||||
/**
|
||||
* Check whether the file exists
|
||||
* @param {string} path
|
||||
* @returns {boolean} A boolean indicating whether the path is a file.
|
||||
*/
|
||||
const exists = (path: string): Promise<boolean> => global.core.api?.exists(path)
|
||||
const existsSync = (...args: any[]) => global.core.api?.existsSync(...args)
|
||||
/**
|
||||
* List the directory files
|
||||
* @param {string} path - The path of the directory to list files.
|
||||
* @returns {Promise<any>} A Promise that resolves with the contents of the directory.
|
||||
*/
|
||||
const listFiles: (path: string) => Promise<any> = (path) => global.core.api?.listFiles(path)
|
||||
const readdirSync = (...args: any[]) => global.core.api?.readdirSync(...args)
|
||||
/**
|
||||
* Creates a directory at the specified path.
|
||||
* @param {string} path - The path of the directory to create.
|
||||
* @returns {Promise<any>} A Promise that resolves when the directory is created successfully.
|
||||
*/
|
||||
const mkdir: (path: string) => Promise<any> = (path) => global.core.api?.mkdir(path)
|
||||
const mkdirSync = (...args: any[]) => global.core.api?.mkdirSync(...args)
|
||||
|
||||
/**
|
||||
* Removes a directory at the specified path.
|
||||
* @param {string} path - The path of the directory to remove.
|
||||
* @returns {Promise<any>} A Promise that resolves when the directory is removed successfully.
|
||||
*/
|
||||
const rmdir: (path: string) => Promise<any> = (path) => global.core.api?.rmdir(path)
|
||||
const rmdirSync = (...args: any[]) =>
|
||||
global.core.api?.rmdirSync(...args, { recursive: true, force: true })
|
||||
/**
|
||||
* Deletes a file from the local file system.
|
||||
* @param {string} path - The path of the file to delete.
|
||||
* @returns {Promise<any>} A Promise that resolves when the file is deleted.
|
||||
*/
|
||||
const deleteFile: (path: string) => Promise<any> = (path) => global.core.api?.deleteFile(path)
|
||||
const unlinkSync = (...args: any[]) => global.core.api?.unlinkSync(...args)
|
||||
|
||||
/**
|
||||
* Appends data to a file at the specified path.
|
||||
* @param path path to the file
|
||||
* @param data data to append
|
||||
*/
|
||||
const appendFile: (path: string, data: string) => Promise<any> = (path, data) =>
|
||||
global.core.api?.appendFile(path, data)
|
||||
|
||||
const copyFile: (src: string, dest: string) => Promise<any> = (src, dest) =>
|
||||
global.core.api?.copyFile(src, dest)
|
||||
const appendFileSync = (...args: any[]) => global.core.api?.appendFileSync(...args)
|
||||
|
||||
/**
|
||||
* Synchronizes a file from a source path to a destination path.
|
||||
* @param {string} src - The source path of the file to be synchronized.
|
||||
* @param {string} dest - The destination path where the file will be synchronized to.
|
||||
* @returns {Promise<any>} - A promise that resolves when the file has been successfully synchronized.
|
||||
*/
|
||||
const syncFile: (src: string, dest: string) => Promise<any> = (src, dest) =>
|
||||
global.core.api?.syncFile(src, dest)
|
||||
/**
|
||||
* Reads a file line by line.
|
||||
* @param {string} path - The path of the file to read.
|
||||
* @returns {Promise<any>} A promise that resolves to the lines of the file.
|
||||
*/
|
||||
const readLineByLine: (path: string) => Promise<any> = (path) =>
|
||||
global.core.api?.readLineByLine(path)
|
||||
|
||||
/**
|
||||
* Copy file sync.
|
||||
*/
|
||||
const copyFileSync = (...args: any[]) => global.core.api?.copyFileSync(...args)
|
||||
|
||||
// TODO: Export `dummy` fs functions automatically
|
||||
// Currently adding these manually
|
||||
export const fs = {
|
||||
isDirectory,
|
||||
writeFile,
|
||||
readFile,
|
||||
exists,
|
||||
listFiles,
|
||||
mkdir,
|
||||
rmdir,
|
||||
deleteFile,
|
||||
appendFile,
|
||||
readLineByLine,
|
||||
copyFile,
|
||||
writeFileSync,
|
||||
readFileSync,
|
||||
existsSync,
|
||||
readdirSync,
|
||||
mkdirSync,
|
||||
rmdirSync,
|
||||
unlinkSync,
|
||||
appendFileSync,
|
||||
copyFileSync,
|
||||
syncFile,
|
||||
}
|
||||
|
||||
8
core/src/node/api/HttpServer.ts
Normal file
8
core/src/node/api/HttpServer.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export interface HttpServer {
|
||||
post: (route: string, handler: (req: any, res: any) => Promise<any>) => void
|
||||
get: (route: string, handler: (req: any, res: any) => Promise<any>) => void
|
||||
patch: (route: string, handler: (req: any, res: any) => Promise<any>) => void
|
||||
put: (route: string, handler: (req: any, res: any) => Promise<any>) => void
|
||||
delete: (route: string, handler: (req: any, res: any) => Promise<any>) => void
|
||||
register: (router: any, opts?: any) => void
|
||||
}
|
||||
335
core/src/node/api/common/builder.ts
Normal file
335
core/src/node/api/common/builder.ts
Normal file
@ -0,0 +1,335 @@
|
||||
import fs from 'fs'
|
||||
import { JanApiRouteConfiguration, RouteConfiguration } from './configuration'
|
||||
import { join } from 'path'
|
||||
import { Model, ThreadMessage } from './../../../index'
|
||||
|
||||
import fetch from 'node-fetch'
|
||||
import { ulid } from 'ulid'
|
||||
import request from 'request'
|
||||
|
||||
const progress = require('request-progress')
|
||||
const os = require('os')
|
||||
|
||||
const path = join(os.homedir(), 'jan')
|
||||
|
||||
export const getBuilder = async (configuration: RouteConfiguration) => {
|
||||
const directoryPath = join(path, configuration.dirName)
|
||||
|
||||
try {
|
||||
if (!(await fs.existsSync(directoryPath))) {
|
||||
console.debug('model folder not found')
|
||||
return []
|
||||
}
|
||||
|
||||
const files: string[] = await fs.readdirSync(directoryPath)
|
||||
|
||||
const allDirectories: string[] = []
|
||||
for (const file of files) {
|
||||
if (file === '.DS_Store') continue
|
||||
allDirectories.push(file)
|
||||
}
|
||||
|
||||
const readJsonPromises = allDirectories.map(async (dirName) => {
|
||||
const jsonPath = join(directoryPath, dirName, configuration.metadataFileName)
|
||||
return await readModelMetadata(jsonPath)
|
||||
})
|
||||
|
||||
const results = await Promise.all(readJsonPromises)
|
||||
const modelData = results
|
||||
.map((result: any) => {
|
||||
try {
|
||||
return JSON.parse(result)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
})
|
||||
.filter((e: any) => !!e)
|
||||
|
||||
return modelData
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
const readModelMetadata = async (path: string) => {
|
||||
return fs.readFileSync(path, 'utf-8')
|
||||
}
|
||||
|
||||
export const retrieveBuilder = async (configuration: RouteConfiguration, id: string) => {
|
||||
const data = await getBuilder(configuration)
|
||||
const filteredData = data.filter((d: any) => d.id === id)[0]
|
||||
|
||||
if (!filteredData) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return filteredData
|
||||
}
|
||||
|
||||
export const deleteBuilder = async (configuration: RouteConfiguration, id: string) => {
|
||||
if (configuration.dirName === 'assistants' && id === 'jan') {
|
||||
return {
|
||||
message: 'Cannot delete Jan assistant',
|
||||
}
|
||||
}
|
||||
|
||||
const directoryPath = join(path, configuration.dirName)
|
||||
try {
|
||||
const data = await retrieveBuilder(configuration, id)
|
||||
if (!data || !data.keys) {
|
||||
return {
|
||||
message: 'Not found',
|
||||
}
|
||||
}
|
||||
|
||||
const myPath = join(directoryPath, id)
|
||||
fs.rmdirSync(myPath, { recursive: true })
|
||||
return {
|
||||
id: id,
|
||||
object: configuration.delete.object,
|
||||
deleted: true,
|
||||
}
|
||||
} catch (ex) {
|
||||
console.error(ex)
|
||||
}
|
||||
}
|
||||
|
||||
export const getMessages = async (threadId: string) => {
|
||||
const threadDirPath = join(path, 'threads', threadId)
|
||||
const messageFile = 'messages.jsonl'
|
||||
try {
|
||||
const files: string[] = await fs.readdirSync(threadDirPath)
|
||||
if (!files.includes(messageFile)) {
|
||||
throw Error(`${threadDirPath} not contains message file`)
|
||||
}
|
||||
|
||||
const messageFilePath = join(threadDirPath, messageFile)
|
||||
|
||||
const lines = fs
|
||||
.readFileSync(messageFilePath, 'utf-8')
|
||||
.toString()
|
||||
.split('\n')
|
||||
.filter((line: any) => line !== '')
|
||||
|
||||
const messages: ThreadMessage[] = []
|
||||
lines.forEach((line: string) => {
|
||||
messages.push(JSON.parse(line) as ThreadMessage)
|
||||
})
|
||||
return messages
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export const retrieveMesasge = async (threadId: string, messageId: string) => {
|
||||
const messages = await getMessages(threadId)
|
||||
const filteredMessages = messages.filter((m) => m.id === messageId)
|
||||
if (!filteredMessages || filteredMessages.length === 0) {
|
||||
return {
|
||||
message: 'Not found',
|
||||
}
|
||||
}
|
||||
|
||||
return filteredMessages[0]
|
||||
}
|
||||
|
||||
export const createThread = async (thread: any) => {
|
||||
const threadMetadataFileName = 'thread.json'
|
||||
// TODO: add validation
|
||||
if (!thread.assistants || thread.assistants.length === 0) {
|
||||
return {
|
||||
message: 'Thread must have at least one assistant',
|
||||
}
|
||||
}
|
||||
|
||||
const threadId = generateThreadId(thread.assistants[0].assistant_id)
|
||||
try {
|
||||
const updatedThread = {
|
||||
...thread,
|
||||
id: threadId,
|
||||
created: Date.now(),
|
||||
updated: Date.now(),
|
||||
}
|
||||
const threadDirPath = join(path, 'threads', updatedThread.id)
|
||||
const threadJsonPath = join(threadDirPath, threadMetadataFileName)
|
||||
|
||||
if (!fs.existsSync(threadDirPath)) {
|
||||
fs.mkdirSync(threadDirPath)
|
||||
}
|
||||
|
||||
await fs.writeFileSync(threadJsonPath, JSON.stringify(updatedThread, null, 2))
|
||||
return updatedThread
|
||||
} catch (err) {
|
||||
return {
|
||||
error: err,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const updateThread = async (threadId: string, thread: any) => {
|
||||
const threadMetadataFileName = 'thread.json'
|
||||
const currentThreadData = await retrieveBuilder(JanApiRouteConfiguration.threads, threadId)
|
||||
if (!currentThreadData) {
|
||||
return {
|
||||
message: 'Thread not found',
|
||||
}
|
||||
}
|
||||
// we don't want to update the id and object
|
||||
delete thread.id
|
||||
delete thread.object
|
||||
|
||||
const updatedThread = {
|
||||
...currentThreadData,
|
||||
...thread,
|
||||
updated: Date.now(),
|
||||
}
|
||||
try {
|
||||
const threadDirPath = join(path, 'threads', updatedThread.id)
|
||||
const threadJsonPath = join(threadDirPath, threadMetadataFileName)
|
||||
|
||||
await fs.writeFileSync(threadJsonPath, JSON.stringify(updatedThread, null, 2))
|
||||
return updatedThread
|
||||
} catch (err) {
|
||||
return {
|
||||
message: err,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const generateThreadId = (assistantId: string) => {
|
||||
return `${assistantId}_${(Date.now() / 1000).toFixed(0)}`
|
||||
}
|
||||
|
||||
export const createMessage = async (threadId: string, message: any) => {
|
||||
const threadMessagesFileName = 'messages.jsonl'
|
||||
// TODO: add validation
|
||||
try {
|
||||
const msgId = ulid()
|
||||
const createdAt = Date.now()
|
||||
const threadMessage: ThreadMessage = {
|
||||
...message,
|
||||
id: msgId,
|
||||
thread_id: threadId,
|
||||
created: createdAt,
|
||||
updated: createdAt,
|
||||
object: 'thread.message',
|
||||
}
|
||||
|
||||
const threadDirPath = join(path, 'threads', threadId)
|
||||
const threadMessagePath = join(threadDirPath, threadMessagesFileName)
|
||||
|
||||
if (!fs.existsSync(threadDirPath)) {
|
||||
fs.mkdirSync(threadDirPath)
|
||||
}
|
||||
fs.appendFileSync(threadMessagePath, JSON.stringify(threadMessage) + '\n')
|
||||
return threadMessage
|
||||
} catch (err) {
|
||||
return {
|
||||
message: err,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const downloadModel = async (modelId: string) => {
|
||||
const model = await retrieveBuilder(JanApiRouteConfiguration.models, modelId)
|
||||
if (!model || model.object !== 'model') {
|
||||
return {
|
||||
message: 'Model not found',
|
||||
}
|
||||
}
|
||||
|
||||
const directoryPath = join(path, 'models', modelId)
|
||||
if (!fs.existsSync(directoryPath)) {
|
||||
fs.mkdirSync(directoryPath)
|
||||
}
|
||||
|
||||
// path to model binary
|
||||
const modelBinaryPath = join(directoryPath, modelId)
|
||||
const rq = request(model.source_url)
|
||||
|
||||
progress(rq, {})
|
||||
.on('progress', function (state: any) {
|
||||
console.log('progress', JSON.stringify(state, null, 2))
|
||||
})
|
||||
.on('error', function (err: Error) {
|
||||
console.error('error', err)
|
||||
})
|
||||
.on('end', function () {
|
||||
console.log('end')
|
||||
})
|
||||
.pipe(fs.createWriteStream(modelBinaryPath))
|
||||
|
||||
return {
|
||||
message: `Starting download ${modelId}`,
|
||||
}
|
||||
}
|
||||
|
||||
export const chatCompletions = async (request: any, reply: any) => {
|
||||
const modelList = await getBuilder(JanApiRouteConfiguration.models)
|
||||
const modelId = request.body.model
|
||||
|
||||
const matchedModels = modelList.filter((model: Model) => model.id === modelId)
|
||||
if (matchedModels.length === 0) {
|
||||
const error = {
|
||||
error: {
|
||||
message: `The model ${request.body.model} does not exist`,
|
||||
type: 'invalid_request_error',
|
||||
param: null,
|
||||
code: 'model_not_found',
|
||||
},
|
||||
}
|
||||
reply.code(404).send(error)
|
||||
return
|
||||
}
|
||||
|
||||
const requestedModel = matchedModels[0]
|
||||
const engineConfiguration = await getEngineConfiguration(requestedModel.engine)
|
||||
|
||||
let apiKey: string | undefined = undefined
|
||||
let apiUrl: string = 'http://127.0.0.1:3928/inferences/llamacpp/chat_completion' // default nitro url
|
||||
|
||||
if (engineConfiguration) {
|
||||
apiKey = engineConfiguration.api_key
|
||||
apiUrl = engineConfiguration.full_url
|
||||
}
|
||||
|
||||
reply.raw.writeHead(200, {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
Connection: 'keep-alive',
|
||||
})
|
||||
|
||||
const headers: Record<string, any> = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
if (apiKey) {
|
||||
headers['Authorization'] = `Bearer ${apiKey}`
|
||||
headers['api-key'] = apiKey
|
||||
}
|
||||
console.log(apiUrl)
|
||||
console.log(JSON.stringify(headers))
|
||||
const response = await fetch(apiUrl, {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: JSON.stringify(request.body),
|
||||
})
|
||||
if (response.status !== 200) {
|
||||
console.error(response)
|
||||
return
|
||||
} else {
|
||||
response.body.pipe(reply.raw)
|
||||
}
|
||||
}
|
||||
|
||||
const getEngineConfiguration = async (engineId: string) => {
|
||||
if (engineId !== 'openai') {
|
||||
return undefined
|
||||
}
|
||||
const directoryPath = join(path, 'engines')
|
||||
const filePath = join(directoryPath, `${engineId}.json`)
|
||||
const data = await fs.readFileSync(filePath, 'utf-8')
|
||||
return JSON.parse(data)
|
||||
}
|
||||
31
core/src/node/api/common/configuration.ts
Normal file
31
core/src/node/api/common/configuration.ts
Normal file
@ -0,0 +1,31 @@
|
||||
export const JanApiRouteConfiguration: Record<string, RouteConfiguration> = {
|
||||
models: {
|
||||
dirName: 'models',
|
||||
metadataFileName: 'model.json',
|
||||
delete: {
|
||||
object: 'model',
|
||||
},
|
||||
},
|
||||
assistants: {
|
||||
dirName: 'assistants',
|
||||
metadataFileName: 'assistant.json',
|
||||
delete: {
|
||||
object: 'assistant',
|
||||
},
|
||||
},
|
||||
threads: {
|
||||
dirName: 'threads',
|
||||
metadataFileName: 'thread.json',
|
||||
delete: {
|
||||
object: 'thread',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export type RouteConfiguration = {
|
||||
dirName: string
|
||||
metadataFileName: string
|
||||
delete: {
|
||||
object: string
|
||||
}
|
||||
}
|
||||
2
core/src/node/api/index.ts
Normal file
2
core/src/node/api/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './HttpServer'
|
||||
export * from './routes'
|
||||
42
core/src/node/api/routes/common.ts
Normal file
42
core/src/node/api/routes/common.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { AppRoute } from '../../../api'
|
||||
import { HttpServer } from '../HttpServer'
|
||||
import { join } from 'path'
|
||||
import {
|
||||
chatCompletions,
|
||||
deleteBuilder,
|
||||
downloadModel,
|
||||
getBuilder,
|
||||
retrieveBuilder,
|
||||
} from '../common/builder'
|
||||
|
||||
import { JanApiRouteConfiguration } from '../common/configuration'
|
||||
|
||||
export const commonRouter = async (app: HttpServer) => {
|
||||
// Common Routes
|
||||
Object.keys(JanApiRouteConfiguration).forEach((key) => {
|
||||
app.get(`/${key}`, async (_request) => getBuilder(JanApiRouteConfiguration[key]))
|
||||
|
||||
app.get(`/${key}/:id`, async (request: any) =>
|
||||
retrieveBuilder(JanApiRouteConfiguration[key], request.params.id),
|
||||
)
|
||||
|
||||
app.delete(`/${key}/:id`, async (request: any) =>
|
||||
deleteBuilder(JanApiRouteConfiguration[key], request.params.id),
|
||||
)
|
||||
})
|
||||
|
||||
// Download Model Routes
|
||||
app.get(`/models/download/:modelId`, async (request: any) =>
|
||||
downloadModel(request.params.modelId),
|
||||
)
|
||||
|
||||
// Chat Completion Routes
|
||||
app.post(`/chat/completions`, async (request: any, reply: any) => chatCompletions(request, reply))
|
||||
|
||||
// App Routes
|
||||
app.post(`/app/${AppRoute.joinPath}`, async (request: any, reply: any) => {
|
||||
const args = JSON.parse(request.body) as any[]
|
||||
console.debug('joinPath: ', ...args[0])
|
||||
reply.send(JSON.stringify(join(...args[0])))
|
||||
})
|
||||
}
|
||||
54
core/src/node/api/routes/download.ts
Normal file
54
core/src/node/api/routes/download.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { DownloadRoute } from '../../../api'
|
||||
import { join } from 'path'
|
||||
import { userSpacePath, DownloadManager, HttpServer } from '../../index'
|
||||
import { createWriteStream } from 'fs'
|
||||
|
||||
const request = require('request')
|
||||
const progress = require('request-progress')
|
||||
|
||||
export const downloadRouter = async (app: HttpServer) => {
|
||||
app.post(`/${DownloadRoute.downloadFile}`, async (req, res) => {
|
||||
const body = JSON.parse(req.body as any)
|
||||
const normalizedArgs = body.map((arg: any) => {
|
||||
if (typeof arg === 'string' && arg.includes('file:/')) {
|
||||
return join(userSpacePath, arg.replace('file:/', ''))
|
||||
}
|
||||
return arg
|
||||
})
|
||||
|
||||
const localPath = normalizedArgs[1]
|
||||
const fileName = localPath.split('/').pop() ?? ''
|
||||
|
||||
const rq = request(normalizedArgs[0])
|
||||
progress(rq, {})
|
||||
.on('progress', function (state: any) {
|
||||
console.log('download onProgress', state)
|
||||
})
|
||||
.on('error', function (err: Error) {
|
||||
console.log('download onError', err)
|
||||
})
|
||||
.on('end', function () {
|
||||
console.log('download onEnd')
|
||||
})
|
||||
.pipe(createWriteStream(normalizedArgs[1]))
|
||||
|
||||
DownloadManager.instance.setRequest(fileName, rq)
|
||||
})
|
||||
|
||||
app.post(`/${DownloadRoute.abortDownload}`, async (req, res) => {
|
||||
const body = JSON.parse(req.body as any)
|
||||
const normalizedArgs = body.map((arg: any) => {
|
||||
if (typeof arg === 'string' && arg.includes('file:/')) {
|
||||
return join(userSpacePath, arg.replace('file:/', ''))
|
||||
}
|
||||
return arg
|
||||
})
|
||||
|
||||
const localPath = normalizedArgs[0]
|
||||
const fileName = localPath.split('/').pop() ?? ''
|
||||
console.debug('fileName', fileName)
|
||||
const rq = DownloadManager.instance.networkRequests[fileName]
|
||||
DownloadManager.instance.networkRequests[fileName] = undefined
|
||||
rq?.abort()
|
||||
})
|
||||
}
|
||||
51
core/src/node/api/routes/extension.ts
Normal file
51
core/src/node/api/routes/extension.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { join, extname } from 'path'
|
||||
import { ExtensionRoute } from '../../../api'
|
||||
import {
|
||||
userSpacePath,
|
||||
ModuleManager,
|
||||
getActiveExtensions,
|
||||
installExtensions,
|
||||
HttpServer,
|
||||
} from '../../index'
|
||||
import { readdirSync } from 'fs'
|
||||
|
||||
export const extensionRouter = async (app: HttpServer) => {
|
||||
// TODO: Share code between node projects
|
||||
app.post(`/${ExtensionRoute.getActiveExtensions}`, async (req, res) => {
|
||||
const activeExtensions = await getActiveExtensions()
|
||||
res.status(200).send(activeExtensions)
|
||||
})
|
||||
|
||||
app.post(`/${ExtensionRoute.baseExtensions}`, async (req, res) => {
|
||||
const baseExtensionPath = join(__dirname, '..', '..', '..', 'pre-install')
|
||||
const extensions = readdirSync(baseExtensionPath)
|
||||
.filter((file) => extname(file) === '.tgz')
|
||||
.map((file) => join(baseExtensionPath, file))
|
||||
|
||||
res.status(200).send(extensions)
|
||||
})
|
||||
|
||||
app.post(`/${ExtensionRoute.installExtension}`, async (req, res) => {
|
||||
const extensions = req.body as any
|
||||
const installed = await installExtensions(JSON.parse(extensions)[0])
|
||||
return JSON.parse(JSON.stringify(installed))
|
||||
})
|
||||
|
||||
app.post(`/${ExtensionRoute.invokeExtensionFunc}`, async (req, res) => {
|
||||
const args = JSON.parse(req.body as any)
|
||||
console.debug(args)
|
||||
const module = await import(join(userSpacePath, 'extensions', args[0]))
|
||||
|
||||
ModuleManager.instance.setModule(args[0], module)
|
||||
const method = args[1]
|
||||
if (typeof module[method] === 'function') {
|
||||
// remove first item from args
|
||||
const newArgs = args.slice(2)
|
||||
console.log(newArgs)
|
||||
return module[method](...args.slice(2))
|
||||
} else {
|
||||
console.debug(module[method])
|
||||
console.error(`Function "${method}" does not exist in the module.`)
|
||||
}
|
||||
})
|
||||
}
|
||||
27
core/src/node/api/routes/fs.ts
Normal file
27
core/src/node/api/routes/fs.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { FileSystemRoute } from '../../../api'
|
||||
import { join } from 'path'
|
||||
import { HttpServer, userSpacePath } from '../../index'
|
||||
|
||||
export const fsRouter = async (app: HttpServer) => {
|
||||
const moduleName = 'fs'
|
||||
// Generate handlers for each fs route
|
||||
Object.values(FileSystemRoute).forEach((route) => {
|
||||
app.post(`/${route}`, async (req, res) => {
|
||||
const body = JSON.parse(req.body as any)
|
||||
try {
|
||||
const result = await import(moduleName).then((mdl) => {
|
||||
return mdl[route](
|
||||
...body.map((arg: any) =>
|
||||
typeof arg === 'string' && arg.includes('file:/')
|
||||
? join(userSpacePath, arg.replace('file:/', ''))
|
||||
: arg,
|
||||
),
|
||||
)
|
||||
})
|
||||
res.status(200).send(result)
|
||||
} catch (ex) {
|
||||
console.log(ex)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
6
core/src/node/api/routes/index.ts
Normal file
6
core/src/node/api/routes/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export * from './download'
|
||||
export * from './extension'
|
||||
export * from './fs'
|
||||
export * from './thread'
|
||||
export * from './common'
|
||||
export * from './v1'
|
||||
30
core/src/node/api/routes/thread.ts
Normal file
30
core/src/node/api/routes/thread.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { HttpServer } from '../HttpServer'
|
||||
import {
|
||||
createMessage,
|
||||
createThread,
|
||||
getMessages,
|
||||
retrieveMesasge,
|
||||
updateThread,
|
||||
} from '../common/builder'
|
||||
|
||||
export const threadRouter = async (app: HttpServer) => {
|
||||
// create thread
|
||||
app.post(`/`, async (req, res) => createThread(req.body))
|
||||
|
||||
app.get(`/:threadId/messages`, async (req, res) => getMessages(req.params.threadId))
|
||||
|
||||
// retrieve message
|
||||
app.get(`/:threadId/messages/:messageId`, async (req, res) =>
|
||||
retrieveMesasge(req.params.threadId, req.params.messageId),
|
||||
)
|
||||
|
||||
// create message
|
||||
app.post(`/:threadId/messages`, async (req, res) =>
|
||||
createMessage(req.params.threadId as any, req.body as any),
|
||||
)
|
||||
|
||||
// modify thread
|
||||
app.patch(`/:threadId`, async (request: any) =>
|
||||
updateThread(request.params.threadId, request.body),
|
||||
)
|
||||
}
|
||||
21
core/src/node/api/routes/v1.ts
Normal file
21
core/src/node/api/routes/v1.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { HttpServer } from '../HttpServer'
|
||||
import { commonRouter, threadRouter, fsRouter, extensionRouter, downloadRouter } from './index'
|
||||
|
||||
export const v1Router = async (app: HttpServer) => {
|
||||
// MARK: External Routes
|
||||
app.register(commonRouter)
|
||||
app.register(threadRouter, {
|
||||
prefix: '/thread',
|
||||
})
|
||||
|
||||
// MARK: Internal Application Routes
|
||||
app.register(fsRouter, {
|
||||
prefix: '/fs',
|
||||
})
|
||||
app.register(extensionRouter, {
|
||||
prefix: '/extension',
|
||||
})
|
||||
app.register(downloadRouter, {
|
||||
prefix: '/download',
|
||||
})
|
||||
}
|
||||
@ -1,14 +1,14 @@
|
||||
import { rmdir } from 'fs/promises'
|
||||
import { rmdirSync } from 'fs'
|
||||
import { resolve, join } from 'path'
|
||||
import { manifest, extract } from 'pacote'
|
||||
import * as Arborist from '@npmcli/arborist'
|
||||
import { ExtensionManager } from './../managers/extension'
|
||||
import { ExtensionManager } from './manager'
|
||||
|
||||
/**
|
||||
* An NPM package that can be used as an extension.
|
||||
* Used to hold all the information and functions necessary to handle the extension lifecycle.
|
||||
*/
|
||||
class Extension {
|
||||
export default class Extension {
|
||||
/**
|
||||
* @property {string} origin Original specification provided to fetch the package.
|
||||
* @property {Object} installOptions Options provided to pacote when fetching the manifest.
|
||||
@ -56,10 +56,7 @@ class Extension {
|
||||
* @type {string}
|
||||
*/
|
||||
get specifier() {
|
||||
return (
|
||||
this.origin +
|
||||
(this.installOptions.version ? '@' + this.installOptions.version : '')
|
||||
)
|
||||
return this.origin + (this.installOptions.version ? '@' + this.installOptions.version : '')
|
||||
}
|
||||
|
||||
/**
|
||||
@ -85,9 +82,7 @@ class Extension {
|
||||
this.main = mnf.main
|
||||
this.description = mnf.description
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Package ${this.origin} does not contain a valid manifest: ${error}`
|
||||
)
|
||||
throw new Error(`Package ${this.origin} does not contain a valid manifest: ${error}`)
|
||||
}
|
||||
|
||||
return true
|
||||
@ -107,7 +102,7 @@ class Extension {
|
||||
await extract(
|
||||
this.specifier,
|
||||
join(ExtensionManager.instance.extensionsPath ?? '', this.name ?? ''),
|
||||
this.installOptions
|
||||
this.installOptions,
|
||||
)
|
||||
|
||||
// Set the url using the custom extensions protocol
|
||||
@ -180,11 +175,8 @@ class Extension {
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async uninstall() {
|
||||
const extPath = resolve(
|
||||
ExtensionManager.instance.extensionsPath ?? '',
|
||||
this.name ?? ''
|
||||
)
|
||||
await rmdir(extPath, { recursive: true })
|
||||
const extPath = resolve(ExtensionManager.instance.extensionsPath ?? '', this.name ?? '')
|
||||
await rmdirSync(extPath, { recursive: true })
|
||||
|
||||
this.emitUpdate()
|
||||
}
|
||||
@ -200,5 +192,3 @@ class Extension {
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
export default Extension
|
||||
@ -1,5 +1,5 @@
|
||||
import { readFileSync } from 'fs'
|
||||
import { protocol } from 'electron'
|
||||
|
||||
import { normalize } from 'path'
|
||||
|
||||
import Extension from './extension'
|
||||
@ -12,18 +12,8 @@ import {
|
||||
getActiveExtensions,
|
||||
addExtension,
|
||||
} from './store'
|
||||
import { ExtensionManager } from './../managers/extension'
|
||||
import { ExtensionManager } from './manager'
|
||||
|
||||
/**
|
||||
* Sets up the required communication between the main and renderer processes.
|
||||
* Additionally sets the extensions up using {@link useExtensions} if a extensionsPath is provided.
|
||||
* @param {Object} options configuration for setting up the renderer facade.
|
||||
* @param {confirmInstall} [options.confirmInstall] Function to validate that a extension should be installed.
|
||||
* @param {Boolean} [options.useFacade=true] Whether to make a facade to the extensions available in the renderer.
|
||||
* @param {string} [options.extensionsPath] Optional path to the extensions folder.
|
||||
* @returns {extensionManager|Object} A set of functions used to manage the extension lifecycle if useExtensions is provided.
|
||||
* @function
|
||||
*/
|
||||
export function init(options: any) {
|
||||
// Create extensions protocol to serve extensions to renderer
|
||||
registerExtensionProtocol()
|
||||
@ -41,13 +31,24 @@ export function init(options: any) {
|
||||
* @private
|
||||
* @returns {boolean} Whether the protocol registration was successful
|
||||
*/
|
||||
function registerExtensionProtocol() {
|
||||
return protocol.registerFileProtocol('extension', (request, callback) => {
|
||||
async function registerExtensionProtocol() {
|
||||
let electron: any = undefined
|
||||
|
||||
try {
|
||||
const moduleName = "electron"
|
||||
electron = await import(moduleName)
|
||||
} catch (err) {
|
||||
console.error('Electron is not available')
|
||||
}
|
||||
|
||||
if (electron) {
|
||||
return electron.protocol.registerFileProtocol('extension', (request: any, callback: any) => {
|
||||
const entry = request.url.substr('extension://'.length - 1)
|
||||
|
||||
const url = normalize(ExtensionManager.instance.extensionsPath + entry)
|
||||
callback({ path: url })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -57,8 +58,7 @@ function registerExtensionProtocol() {
|
||||
* @returns {extensionManager} A set of functions used to manage the extension lifecycle.
|
||||
*/
|
||||
export function useExtensions(extensionsPath: string) {
|
||||
if (!extensionsPath)
|
||||
throw Error('A path to the extensions folder is required to use extensions')
|
||||
if (!extensionsPath) throw Error('A path to the extensions folder is required to use extensions')
|
||||
// Store the path to the extensions folder
|
||||
ExtensionManager.instance.setExtensionsPath(extensionsPath)
|
||||
|
||||
@ -69,7 +69,7 @@ export function useExtensions(extensionsPath: string) {
|
||||
|
||||
// Read extension list from extensions folder
|
||||
const extensions = JSON.parse(
|
||||
readFileSync(ExtensionManager.instance.getExtensionsFile(), 'utf-8')
|
||||
readFileSync(ExtensionManager.instance.getExtensionsFile(), 'utf-8'),
|
||||
)
|
||||
try {
|
||||
// Create and store a Extension instance for each extension in list
|
||||
@ -82,7 +82,7 @@ export function useExtensions(extensionsPath: string) {
|
||||
throw new Error(
|
||||
'Could not successfully rebuild list of installed extensions.\n' +
|
||||
error +
|
||||
'\nPlease check the extensions.json file in the extensions folder.'
|
||||
'\nPlease check the extensions.json file in the extensions folder.',
|
||||
)
|
||||
}
|
||||
|
||||
@ -111,7 +111,6 @@ function loadExtension(ext: any) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
addExtension(extension, false)
|
||||
extension.subscribe('pe-persist', persistExtensions)
|
||||
}
|
||||
@ -123,7 +122,7 @@ function loadExtension(ext: any) {
|
||||
export function getStore() {
|
||||
if (!ExtensionManager.instance.extensionsPath) {
|
||||
throw new Error(
|
||||
'The extension path has not yet been set up. Please run useExtensions before accessing the store'
|
||||
'The extension path has not yet been set up. Please run useExtensions before accessing the store',
|
||||
)
|
||||
}
|
||||
|
||||
61
core/src/node/extension/manager.ts
Normal file
61
core/src/node/extension/manager.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import { join, resolve } from "path";
|
||||
|
||||
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
||||
import { init } from "./index";
|
||||
import { homedir } from "os"
|
||||
/**
|
||||
* Manages extension installation and migration.
|
||||
*/
|
||||
|
||||
export const userSpacePath = join(homedir(), "jan");
|
||||
|
||||
export class ExtensionManager {
|
||||
public static instance: ExtensionManager = new ExtensionManager();
|
||||
|
||||
extensionsPath: string | undefined = join(userSpacePath, "extensions");
|
||||
|
||||
constructor() {
|
||||
if (ExtensionManager.instance) {
|
||||
return ExtensionManager.instance;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the extensions by initializing the `extensions` module with the `confirmInstall` and `extensionsPath` options.
|
||||
* The `confirmInstall` function always returns `true` to allow extension installation.
|
||||
* The `extensionsPath` option specifies the path to install extensions to.
|
||||
*/
|
||||
setupExtensions() {
|
||||
init({
|
||||
// Function to check from the main process that user wants to install a extension
|
||||
confirmInstall: async (_extensions: string[]) => {
|
||||
return true;
|
||||
},
|
||||
// Path to install extension to
|
||||
extensionsPath: join(userSpacePath, "extensions"),
|
||||
});
|
||||
}
|
||||
|
||||
setExtensionsPath(extPath: string) {
|
||||
// Create folder if it does not exist
|
||||
let extDir;
|
||||
try {
|
||||
extDir = resolve(extPath);
|
||||
if (extDir.length < 2) throw new Error();
|
||||
|
||||
if (!existsSync(extDir)) mkdirSync(extDir);
|
||||
|
||||
const extensionsJson = join(extDir, "extensions.json");
|
||||
if (!existsSync(extensionsJson))
|
||||
writeFileSync(extensionsJson, "{}", "utf8");
|
||||
|
||||
this.extensionsPath = extDir;
|
||||
} catch (error) {
|
||||
throw new Error("Invalid path provided to the extensions folder");
|
||||
}
|
||||
}
|
||||
|
||||
getExtensionsFile() {
|
||||
return join(this.extensionsPath ?? "", "extensions.json");
|
||||
}
|
||||
}
|
||||
@ -1,16 +1,6 @@
|
||||
/**
|
||||
* Provides access to the extensions stored by Extension Store
|
||||
* @typedef {Object} extensionManager
|
||||
* @prop {getExtension} getExtension
|
||||
* @prop {getAllExtensions} getAllExtensions
|
||||
* @prop {getActiveExtensions} getActiveExtensions
|
||||
* @prop {installExtensions} installExtensions
|
||||
* @prop {removeExtension} removeExtension
|
||||
*/
|
||||
|
||||
import { writeFileSync } from 'fs'
|
||||
import Extension from './extension'
|
||||
import { ExtensionManager } from './../managers/extension'
|
||||
import { writeFileSync } from "fs";
|
||||
import Extension from "./extension";
|
||||
import { ExtensionManager } from "./manager";
|
||||
|
||||
/**
|
||||
* @module store
|
||||
@ -21,7 +11,7 @@ import { ExtensionManager } from './../managers/extension'
|
||||
* Register of installed extensions
|
||||
* @type {Object.<string, Extension>} extension - List of installed extensions
|
||||
*/
|
||||
const extensions: Record<string, Extension> = {}
|
||||
const extensions: Record<string, Extension> = {};
|
||||
|
||||
/**
|
||||
* Get a extension from the stored extensions.
|
||||
@ -31,10 +21,10 @@ const extensions: Record<string, Extension> = {}
|
||||
*/
|
||||
export function getExtension(name: string) {
|
||||
if (!Object.prototype.hasOwnProperty.call(extensions, name)) {
|
||||
throw new Error(`Extension ${name} does not exist`)
|
||||
throw new Error(`Extension ${name} does not exist`);
|
||||
}
|
||||
|
||||
return extensions[name]
|
||||
return extensions[name];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -43,7 +33,7 @@ export function getExtension(name: string) {
|
||||
* @alias extensionManager.getAllExtensions
|
||||
*/
|
||||
export function getAllExtensions() {
|
||||
return Object.values(extensions)
|
||||
return Object.values(extensions);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -52,7 +42,7 @@ export function getAllExtensions() {
|
||||
* @alias extensionManager.getActiveExtensions
|
||||
*/
|
||||
export function getActiveExtensions() {
|
||||
return Object.values(extensions).filter((extension) => extension.active)
|
||||
return Object.values(extensions).filter((extension) => extension.active);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -63,9 +53,9 @@ export function getActiveExtensions() {
|
||||
* @alias extensionManager.removeExtension
|
||||
*/
|
||||
export function removeExtension(name: string, persist = true) {
|
||||
const del = delete extensions[name]
|
||||
if (persist) persistExtensions()
|
||||
return del
|
||||
const del = delete extensions[name];
|
||||
if (persist) persistExtensions();
|
||||
return del;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -75,10 +65,10 @@ export function removeExtension(name: string, persist = true) {
|
||||
* @returns {void}
|
||||
*/
|
||||
export function addExtension(extension: Extension, persist = true) {
|
||||
if (extension.name) extensions[extension.name] = extension
|
||||
if (extension.name) extensions[extension.name] = extension;
|
||||
if (persist) {
|
||||
persistExtensions()
|
||||
extension.subscribe('pe-persist', persistExtensions)
|
||||
persistExtensions();
|
||||
extension.subscribe("pe-persist", persistExtensions);
|
||||
}
|
||||
}
|
||||
|
||||
@ -87,15 +77,15 @@ export function addExtension(extension: Extension, persist = true) {
|
||||
* @returns {void}
|
||||
*/
|
||||
export function persistExtensions() {
|
||||
const persistData: Record<string, Extension> = {}
|
||||
const persistData: Record<string, Extension> = {};
|
||||
for (const name in extensions) {
|
||||
persistData[name] = extensions[name]
|
||||
persistData[name] = extensions[name];
|
||||
}
|
||||
writeFileSync(
|
||||
ExtensionManager.instance.getExtensionsFile(),
|
||||
JSON.stringify(persistData),
|
||||
'utf8'
|
||||
)
|
||||
"utf8"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -106,25 +96,25 @@ export function persistExtensions() {
|
||||
* @alias extensionManager.installExtensions
|
||||
*/
|
||||
export async function installExtensions(extensions: any, store = true) {
|
||||
const installed: Extension[] = []
|
||||
const installed: Extension[] = [];
|
||||
for (const ext of extensions) {
|
||||
// Set install options and activation based on input type
|
||||
const isObject = typeof ext === 'object'
|
||||
const spec = isObject ? [ext.specifier, ext] : [ext]
|
||||
const activate = isObject ? ext.activate !== false : true
|
||||
const isObject = typeof ext === "object";
|
||||
const spec = isObject ? [ext.specifier, ext] : [ext];
|
||||
const activate = isObject ? ext.activate !== false : true;
|
||||
|
||||
// Install and possibly activate extension
|
||||
const extension = new Extension(...spec)
|
||||
await extension._install()
|
||||
if (activate) extension.setActive(true)
|
||||
const extension = new Extension(...spec);
|
||||
await extension._install();
|
||||
if (activate) extension.setActive(true);
|
||||
|
||||
// Add extension to store if needed
|
||||
if (store) addExtension(extension)
|
||||
installed.push(extension)
|
||||
if (store) addExtension(extension);
|
||||
installed.push(extension);
|
||||
}
|
||||
|
||||
// Return list of all installed extensions
|
||||
return installed
|
||||
return installed;
|
||||
}
|
||||
|
||||
/**
|
||||
7
core/src/node/index.ts
Normal file
7
core/src/node/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export * from './extension/index'
|
||||
export * from './extension/extension'
|
||||
export * from './extension/manager'
|
||||
export * from './extension/store'
|
||||
export * from './download'
|
||||
export * from './module'
|
||||
export * from './api'
|
||||
@ -1,16 +1,14 @@
|
||||
import { dispose } from "./../utils/disposable";
|
||||
|
||||
/**
|
||||
* Manages imported modules.
|
||||
*/
|
||||
export class ModuleManager {
|
||||
public requiredModules: Record<string, any> = {};
|
||||
public requiredModules: Record<string, any> = {}
|
||||
|
||||
public static instance: ModuleManager = new ModuleManager();
|
||||
public static instance: ModuleManager = new ModuleManager()
|
||||
|
||||
constructor() {
|
||||
if (ModuleManager.instance) {
|
||||
return ModuleManager.instance;
|
||||
return ModuleManager.instance
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,14 +18,13 @@ export class ModuleManager {
|
||||
* @param {any | undefined} nodule - The module to set, or undefined to clear the module.
|
||||
*/
|
||||
setModule(moduleName: string, nodule: any | undefined) {
|
||||
this.requiredModules[moduleName] = nodule;
|
||||
this.requiredModules[moduleName] = nodule
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all imported modules.
|
||||
*/
|
||||
clearImportedModules() {
|
||||
dispose(this.requiredModules);
|
||||
this.requiredModules = {};
|
||||
this.requiredModules = {}
|
||||
}
|
||||
}
|
||||
@ -67,13 +67,6 @@ export type Model = {
|
||||
*/
|
||||
description: string
|
||||
|
||||
/**
|
||||
* The model state.
|
||||
* Default: "to_download"
|
||||
* Enum: "to_download" "downloading" "ready" "running"
|
||||
*/
|
||||
state?: ModelState
|
||||
|
||||
/**
|
||||
* The model settings.
|
||||
*/
|
||||
@ -101,15 +94,6 @@ export type ModelMetadata = {
|
||||
cover?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* The Model transition states.
|
||||
*/
|
||||
export enum ModelState {
|
||||
Downloading = 'downloading',
|
||||
Ready = 'ready',
|
||||
Running = 'running',
|
||||
}
|
||||
|
||||
/**
|
||||
* The available model settings.
|
||||
*/
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "node",
|
||||
"target": "es5",
|
||||
"module": "es2015",
|
||||
"module": "ES2020",
|
||||
"lib": ["es2015", "es2016", "es2017", "dom"],
|
||||
"strict": true,
|
||||
"sourceMap": true,
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
import { app, ipcMain, shell, nativeTheme } from 'electron'
|
||||
import { ModuleManager } from './../managers/module'
|
||||
import { join } from 'path'
|
||||
import { ExtensionManager } from './../managers/extension'
|
||||
import { WindowManager } from './../managers/window'
|
||||
import { userSpacePath } from './../utils/path'
|
||||
import { AppRoute } from '@janhq/core'
|
||||
import { getResourcePath } from './../utils/path'
|
||||
import {
|
||||
ExtensionManager,
|
||||
ModuleManager,
|
||||
} from '@janhq/core/node'
|
||||
|
||||
export function handleAppIPCs() {
|
||||
/**
|
||||
@ -26,10 +28,6 @@ export function handleAppIPCs() {
|
||||
shell.openPath(userSpacePath)
|
||||
})
|
||||
|
||||
ipcMain.handle(AppRoute.getResourcePath, async (_event) => {
|
||||
return getResourcePath()
|
||||
})
|
||||
|
||||
/**
|
||||
* Opens a URL in the user's default browser.
|
||||
* @param _event - The IPC event object.
|
||||
@ -48,6 +46,13 @@ export function handleAppIPCs() {
|
||||
shell.openPath(url)
|
||||
})
|
||||
|
||||
/**
|
||||
* Joins multiple paths together, respect to the current OS.
|
||||
*/
|
||||
ipcMain.handle(AppRoute.joinPath, async (_event, paths: string[]) =>
|
||||
join(...paths)
|
||||
)
|
||||
|
||||
/**
|
||||
* Relaunches the app in production - reload window in development.
|
||||
* @param _event - The IPC event object.
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { app, ipcMain } from 'electron'
|
||||
import { DownloadManager } from './../managers/download'
|
||||
import { resolve, join } from 'path'
|
||||
import { WindowManager } from './../managers/window'
|
||||
import request from 'request'
|
||||
import { createWriteStream } from 'fs'
|
||||
import { createWriteStream, renameSync } from 'fs'
|
||||
import { DownloadEvent, DownloadRoute } from '@janhq/core'
|
||||
const progress = require('request-progress')
|
||||
import { DownloadManager } from '@janhq/core/node'
|
||||
|
||||
export function handleDownloaderIPCs() {
|
||||
/**
|
||||
@ -46,8 +46,13 @@ export function handleDownloaderIPCs() {
|
||||
*/
|
||||
ipcMain.handle(DownloadRoute.downloadFile, async (_event, url, fileName) => {
|
||||
const userDataPath = join(app.getPath('home'), 'jan')
|
||||
if (typeof fileName === 'string' && fileName.includes('file:/')) {
|
||||
fileName = fileName.replace('file:/', '')
|
||||
}
|
||||
const destination = resolve(userDataPath, fileName)
|
||||
const rq = request(url)
|
||||
// downloading file to a temp file first
|
||||
const downloadingTempFile = `${destination}.download`
|
||||
|
||||
progress(rq, {})
|
||||
.on('progress', function (state: any) {
|
||||
@ -70,6 +75,9 @@ export function handleDownloaderIPCs() {
|
||||
})
|
||||
.on('end', function () {
|
||||
if (DownloadManager.instance.networkRequests[fileName]) {
|
||||
// Finished downloading, rename temp file to actual file
|
||||
renameSync(downloadingTempFile, destination)
|
||||
|
||||
WindowManager?.instance.currentWindow?.webContents.send(
|
||||
DownloadEvent.onFileDownloadSuccess,
|
||||
{
|
||||
@ -87,7 +95,7 @@ export function handleDownloaderIPCs() {
|
||||
)
|
||||
}
|
||||
})
|
||||
.pipe(createWriteStream(destination))
|
||||
.pipe(createWriteStream(downloadingTempFile))
|
||||
|
||||
DownloadManager.instance.setRequest(fileName, rq)
|
||||
})
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
import { ipcMain, webContents } from 'electron'
|
||||
import { readdirSync } from 'fs'
|
||||
import { ModuleManager } from './../managers/module'
|
||||
import { join, extname } from 'path'
|
||||
|
||||
import {
|
||||
getActiveExtensions,
|
||||
getAllExtensions,
|
||||
installExtensions,
|
||||
} from './../extension/store'
|
||||
import { getExtension } from './../extension/store'
|
||||
import { removeExtension } from './../extension/store'
|
||||
import Extension from './../extension/extension'
|
||||
getExtension,
|
||||
removeExtension,
|
||||
getActiveExtensions,
|
||||
ModuleManager
|
||||
} from '@janhq/core/node'
|
||||
|
||||
import { getResourcePath, userSpacePath } from './../utils/path'
|
||||
import { ExtensionRoute } from '@janhq/core'
|
||||
|
||||
@ -81,7 +81,7 @@ export function handleExtensionIPCs() {
|
||||
ExtensionRoute.updateExtension,
|
||||
async (e, extensions, reload) => {
|
||||
// Update all provided extensions
|
||||
const updated: Extension[] = []
|
||||
const updated: any[] = []
|
||||
for (const ext of extensions) {
|
||||
const extension = getExtension(ext)
|
||||
const res = await extension.update()
|
||||
|
||||
37
electron/handlers/fileManager.ts
Normal file
37
electron/handlers/fileManager.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { ipcMain } from 'electron'
|
||||
// @ts-ignore
|
||||
import reflect from '@alumna/reflect'
|
||||
|
||||
import { FileManagerRoute, getResourcePath } from '@janhq/core'
|
||||
import { userSpacePath } from './../utils/path'
|
||||
|
||||
/**
|
||||
* Handles file system extensions operations.
|
||||
*/
|
||||
export function handleFileMangerIPCs() {
|
||||
// Handles the 'synceFile' IPC event. This event is triggered to synchronize a file from a source path to a destination path.
|
||||
ipcMain.handle(
|
||||
FileManagerRoute.synceFile,
|
||||
async (_event, src: string, dest: string) => {
|
||||
return reflect({
|
||||
src,
|
||||
dest,
|
||||
recursive: true,
|
||||
delete: false,
|
||||
overwrite: true,
|
||||
errorOnExist: false,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
// Handles the 'getUserSpace' IPC event. This event is triggered to get the user space path.
|
||||
ipcMain.handle(
|
||||
FileManagerRoute.getUserSpace,
|
||||
(): Promise<string> => Promise.resolve(userSpacePath)
|
||||
)
|
||||
|
||||
// Handles the 'getResourcePath' IPC event. This event is triggered to get the resource path.
|
||||
ipcMain.handle(FileManagerRoute.getResourcePath, async (_event) => {
|
||||
return getResourcePath()
|
||||
})
|
||||
}
|
||||
@ -1,238 +1,24 @@
|
||||
import { ipcMain } from 'electron'
|
||||
import * as fs from 'fs'
|
||||
import fse from 'fs-extra'
|
||||
import { join } from 'path'
|
||||
import readline from 'readline'
|
||||
import { userSpacePath } from './../utils/path'
|
||||
import { FileSystemRoute } from '@janhq/core'
|
||||
const reflect = require('@alumna/reflect')
|
||||
|
||||
import { FileSystemRoute } from '@janhq/core'
|
||||
import { userSpacePath } from '../utils/path'
|
||||
import { join } from 'path'
|
||||
/**
|
||||
* Handles file system operations.
|
||||
*/
|
||||
export function handleFsIPCs() {
|
||||
/**
|
||||
* Gets the path to the user data directory.
|
||||
* @param event - The event object.
|
||||
* @returns A promise that resolves with the path to the user data directory.
|
||||
*/
|
||||
ipcMain.handle(
|
||||
FileSystemRoute.getUserSpace,
|
||||
(): Promise<string> => Promise.resolve(userSpacePath)
|
||||
const moduleName = 'fs'
|
||||
Object.values(FileSystemRoute).forEach((route) => {
|
||||
ipcMain.handle(route, async (event, ...args) => {
|
||||
return import(moduleName).then((mdl) =>
|
||||
mdl[route](
|
||||
...args.map((arg) =>
|
||||
typeof arg === 'string' && arg.includes('file:/')
|
||||
? join(userSpacePath, arg.replace('file:/', ''))
|
||||
: arg
|
||||
)
|
||||
|
||||
/**
|
||||
* Checks whether the path is a directory.
|
||||
* @param event - The event object.
|
||||
* @param path - The path to check.
|
||||
* @returns A promise that resolves with a boolean indicating whether the path is a directory.
|
||||
*/
|
||||
ipcMain.handle(
|
||||
FileSystemRoute.isDirectory,
|
||||
(_event, path: string): Promise<boolean> => {
|
||||
const fullPath = join(userSpacePath, path)
|
||||
return Promise.resolve(
|
||||
fs.existsSync(fullPath) && fs.lstatSync(fullPath).isDirectory()
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Reads a file from the user data directory.
|
||||
* @param event - The event object.
|
||||
* @param path - The path of the file to read.
|
||||
* @returns A promise that resolves with the contents of the file.
|
||||
*/
|
||||
ipcMain.handle(
|
||||
FileSystemRoute.readFile,
|
||||
async (event, path: string): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readFile(join(userSpacePath, path), 'utf8', (err, data) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve(data)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Checks whether a file exists in the user data directory.
|
||||
* @param event - The event object.
|
||||
* @param path - The path of the file to check.
|
||||
* @returns A promise that resolves with a boolean indicating whether the file exists.
|
||||
*/
|
||||
ipcMain.handle(FileSystemRoute.exists, async (_event, path: string) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fullPath = join(userSpacePath, path)
|
||||
fs.existsSync(fullPath) ? resolve(true) : resolve(false)
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Writes data to a file in the user data directory.
|
||||
* @param event - The event object.
|
||||
* @param path - The path of the file to write to.
|
||||
* @param data - The data to write to the file.
|
||||
* @returns A promise that resolves when the file has been written.
|
||||
*/
|
||||
ipcMain.handle(
|
||||
FileSystemRoute.writeFile,
|
||||
async (event, path: string, data: string): Promise<void> => {
|
||||
try {
|
||||
await fs.writeFileSync(join(userSpacePath, path), data, 'utf8')
|
||||
} catch (err) {
|
||||
console.error(`writeFile ${path} result: ${err}`)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Creates a directory in the user data directory.
|
||||
* @param event - The event object.
|
||||
* @param path - The path of the directory to create.
|
||||
* @returns A promise that resolves when the directory has been created.
|
||||
*/
|
||||
ipcMain.handle(
|
||||
FileSystemRoute.mkdir,
|
||||
async (event, path: string): Promise<void> => {
|
||||
try {
|
||||
fs.mkdirSync(join(userSpacePath, path), { recursive: true })
|
||||
} catch (err) {
|
||||
console.error(`mkdir ${path} result: ${err}`)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Removes a directory in the user data directory.
|
||||
* @param event - The event object.
|
||||
* @param path - The path of the directory to remove.
|
||||
* @returns A promise that resolves when the directory is removed successfully.
|
||||
*/
|
||||
ipcMain.handle(
|
||||
FileSystemRoute.rmdir,
|
||||
async (event, path: string): Promise<void> => {
|
||||
try {
|
||||
await fs.rmSync(join(userSpacePath, path), { recursive: true })
|
||||
} catch (err) {
|
||||
console.error(`rmdir ${path} result: ${err}`)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Lists the files in a directory in the user data directory.
|
||||
* @param event - The event object.
|
||||
* @param path - The path of the directory to list files from.
|
||||
* @returns A promise that resolves with an array of file names.
|
||||
*/
|
||||
ipcMain.handle(
|
||||
FileSystemRoute.listFiles,
|
||||
async (event, path: string): Promise<string[]> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readdir(join(userSpacePath, path), (err, files) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve(files)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Deletes a file from the user data folder.
|
||||
* @param _event - The IPC event object.
|
||||
* @param filePath - The path to the file to delete.
|
||||
* @returns A string indicating the result of the operation.
|
||||
*/
|
||||
ipcMain.handle(FileSystemRoute.deleteFile, async (_event, filePath) => {
|
||||
try {
|
||||
await fs.unlinkSync(join(userSpacePath, filePath))
|
||||
} catch (err) {
|
||||
console.error(`unlink ${filePath} result: ${err}`)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Appends data to a file in the user data directory.
|
||||
* @param event - The event object.
|
||||
* @param path - The path of the file to append to.
|
||||
* @param data - The data to append to the file.
|
||||
* @returns A promise that resolves when the file has been written.
|
||||
*/
|
||||
ipcMain.handle(
|
||||
FileSystemRoute.appendFile,
|
||||
async (_event, path: string, data: string) => {
|
||||
try {
|
||||
await fs.appendFileSync(join(userSpacePath, path), data, 'utf8')
|
||||
} catch (err) {
|
||||
console.error(`appendFile ${path} result: ${err}`)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
FileSystemRoute.syncFile,
|
||||
async (_event, src: string, dest: string) => {
|
||||
console.debug(`Copying file from ${src} to ${dest}`)
|
||||
|
||||
return reflect({
|
||||
src,
|
||||
dest,
|
||||
recursive: true,
|
||||
delete: false,
|
||||
overwrite: true,
|
||||
errorOnExist: false,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
FileSystemRoute.copyFile,
|
||||
async (_event, src: string, dest: string) => {
|
||||
console.debug(`Copying file from ${src} to ${dest}`)
|
||||
|
||||
return fse.copySync(src, dest, {
|
||||
overwrite: false,
|
||||
recursive: true,
|
||||
errorOnExist: false,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Reads a file line by line.
|
||||
* @param event - The event object.
|
||||
* @param path - The path of the file to read.
|
||||
* @returns A promise that resolves with the contents of the file.
|
||||
*/
|
||||
ipcMain.handle(
|
||||
FileSystemRoute.readLineByLine,
|
||||
async (_event, path: string) => {
|
||||
const fullPath = join(userSpacePath, path)
|
||||
|
||||
return new Promise((res, rej) => {
|
||||
try {
|
||||
const readInterface = readline.createInterface({
|
||||
input: fs.createReadStream(fullPath),
|
||||
})
|
||||
const lines: any = []
|
||||
readInterface
|
||||
.on('line', function (line) {
|
||||
lines.push(line)
|
||||
})
|
||||
.on('close', function () {
|
||||
res(lines)
|
||||
})
|
||||
} catch (err) {
|
||||
rej(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@ -7,27 +7,34 @@ import { createUserSpace } from './utils/path'
|
||||
* Managers
|
||||
**/
|
||||
import { WindowManager } from './managers/window'
|
||||
import { ModuleManager } from './managers/module'
|
||||
import { ExtensionManager } from './managers/extension'
|
||||
import { ExtensionManager, ModuleManager } from '@janhq/core/node'
|
||||
|
||||
/**
|
||||
* IPC Handlers
|
||||
**/
|
||||
import { handleDownloaderIPCs } from './handlers/download'
|
||||
import { handleExtensionIPCs } from './handlers/extension'
|
||||
import { handleFileMangerIPCs } from './handlers/fileManager'
|
||||
import { handleAppIPCs } from './handlers/app'
|
||||
import { handleAppUpdates } from './handlers/update'
|
||||
import { handleFsIPCs } from './handlers/fs'
|
||||
import { migrateExtensions } from './utils/migration'
|
||||
|
||||
/**
|
||||
* Server
|
||||
*/
|
||||
import { startServer } from '@janhq/server'
|
||||
|
||||
app
|
||||
.whenReady()
|
||||
.then(createUserSpace)
|
||||
.then(ExtensionManager.instance.migrateExtensions)
|
||||
.then(migrateExtensions)
|
||||
.then(ExtensionManager.instance.setupExtensions)
|
||||
.then(setupMenu)
|
||||
.then(handleIPCs)
|
||||
.then(handleAppUpdates)
|
||||
.then(createMainWindow)
|
||||
.then(startServer)
|
||||
.then(() => {
|
||||
app.on('activate', () => {
|
||||
if (!BrowserWindow.getAllWindows().length) {
|
||||
@ -80,4 +87,5 @@ function handleIPCs() {
|
||||
handleDownloaderIPCs()
|
||||
handleExtensionIPCs()
|
||||
handleAppIPCs()
|
||||
handleFileMangerIPCs()
|
||||
}
|
||||
|
||||
@ -1,85 +0,0 @@
|
||||
import { app } from 'electron'
|
||||
import { init } from './../extension'
|
||||
import { join, resolve } from 'path'
|
||||
import { rmdir } from 'fs'
|
||||
import Store from 'electron-store'
|
||||
import { existsSync, mkdirSync, writeFileSync } from 'fs'
|
||||
import { userSpacePath } from './../utils/path'
|
||||
/**
|
||||
* Manages extension installation and migration.
|
||||
*/
|
||||
export class ExtensionManager {
|
||||
public static instance: ExtensionManager = new ExtensionManager()
|
||||
|
||||
extensionsPath: string | undefined = undefined
|
||||
|
||||
constructor() {
|
||||
if (ExtensionManager.instance) {
|
||||
return ExtensionManager.instance
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the extensions by initializing the `extensions` module with the `confirmInstall` and `extensionsPath` options.
|
||||
* The `confirmInstall` function always returns `true` to allow extension installation.
|
||||
* The `extensionsPath` option specifies the path to install extensions to.
|
||||
*/
|
||||
setupExtensions() {
|
||||
init({
|
||||
// Function to check from the main process that user wants to install a extension
|
||||
confirmInstall: async (_extensions: string[]) => {
|
||||
return true
|
||||
},
|
||||
// Path to install extension to
|
||||
extensionsPath: join(userSpacePath, 'extensions'),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates the extensions by deleting the `extensions` directory in the user data path.
|
||||
* If the `migrated_version` key in the `Store` object does not match the current app version,
|
||||
* the function deletes the `extensions` directory and sets the `migrated_version` key to the current app version.
|
||||
* @returns A Promise that resolves when the migration is complete.
|
||||
*/
|
||||
migrateExtensions() {
|
||||
return new Promise((resolve) => {
|
||||
const store = new Store()
|
||||
if (store.get('migrated_version') !== app.getVersion()) {
|
||||
console.debug('start migration:', store.get('migrated_version'))
|
||||
const fullPath = join(userSpacePath, 'extensions')
|
||||
|
||||
rmdir(fullPath, { recursive: true }, function (err) {
|
||||
if (err) console.error(err)
|
||||
store.set('migrated_version', app.getVersion())
|
||||
console.debug('migrate extensions done')
|
||||
resolve(undefined)
|
||||
})
|
||||
} else {
|
||||
resolve(undefined)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
setExtensionsPath(extPath: string) {
|
||||
// Create folder if it does not exist
|
||||
let extDir
|
||||
try {
|
||||
extDir = resolve(extPath)
|
||||
if (extDir.length < 2) throw new Error()
|
||||
|
||||
if (!existsSync(extDir)) mkdirSync(extDir)
|
||||
|
||||
const extensionsJson = join(extDir, 'extensions.json')
|
||||
if (!existsSync(extensionsJson))
|
||||
writeFileSync(extensionsJson, '{}', 'utf8')
|
||||
|
||||
this.extensionsPath = extDir
|
||||
} catch (error) {
|
||||
throw new Error('Invalid path provided to the extensions folder')
|
||||
}
|
||||
}
|
||||
|
||||
getExtensionsFile() {
|
||||
return join(this.extensionsPath ?? '', 'extensions.json')
|
||||
}
|
||||
}
|
||||
@ -72,15 +72,20 @@
|
||||
"dependencies": {
|
||||
"@alumna/reflect": "^1.1.3",
|
||||
"@janhq/core": "link:./core",
|
||||
"@janhq/server": "link:./server",
|
||||
"@npmcli/arborist": "^7.1.0",
|
||||
"@types/request": "^2.48.12",
|
||||
"@uiball/loaders": "^1.3.0",
|
||||
"electron-store": "^8.1.0",
|
||||
"electron-updater": "^6.1.7",
|
||||
"fs-extra": "^11.2.0",
|
||||
"node-fetch": "2",
|
||||
"pacote": "^17.0.4",
|
||||
"request": "^2.88.2",
|
||||
"request-progress": "^3.0.0",
|
||||
"rimraf": "^5.0.5",
|
||||
"typescript": "^5.3.3",
|
||||
"ulid": "^2.3.0",
|
||||
"use-debounce": "^9.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@ -15,6 +15,9 @@
|
||||
"paths": { "*": ["node_modules/*"] },
|
||||
"typeRoots": ["node_modules/@types"]
|
||||
},
|
||||
"ts-node": {
|
||||
"esm": true
|
||||
},
|
||||
"include": ["./**/*.ts"],
|
||||
"exclude": ["core", "build", "dist", "tests", "node_modules"]
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
// @ts-nocheck
|
||||
const { app, Menu, dialog } = require("electron");
|
||||
import { app, Menu, dialog, shell } from "electron";
|
||||
const isMac = process.platform === "darwin";
|
||||
const { autoUpdater } = require("electron-updater");
|
||||
import { compareSemanticVersions } from "./versionDiff";
|
||||
@ -97,7 +97,6 @@ const template: (Electron.MenuItemConstructorOptions | Electron.MenuItem)[] = [
|
||||
{
|
||||
label: "Learn More",
|
||||
click: async () => {
|
||||
const { shell } = require("electron");
|
||||
await shell.openExternal("https://jan.ai/");
|
||||
},
|
||||
},
|
||||
|
||||
30
electron/utils/migration.ts
Normal file
30
electron/utils/migration.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { app } from 'electron'
|
||||
import { join } from 'path'
|
||||
|
||||
import { rmdir } from 'fs'
|
||||
import Store from 'electron-store'
|
||||
import { userSpacePath } from './path'
|
||||
/**
|
||||
* Migrates the extensions by deleting the `extensions` directory in the user data path.
|
||||
* If the `migrated_version` key in the `Store` object does not match the current app version,
|
||||
* the function deletes the `extensions` directory and sets the `migrated_version` key to the current app version.
|
||||
* @returns A Promise that resolves when the migration is complete.
|
||||
*/
|
||||
export function migrateExtensions() {
|
||||
return new Promise((resolve) => {
|
||||
const store = new Store()
|
||||
if (store.get('migrated_version') !== app.getVersion()) {
|
||||
console.debug('start migration:', store.get('migrated_version'))
|
||||
const fullPath = join(userSpacePath, 'extensions')
|
||||
|
||||
rmdir(fullPath, { recursive: true }, function (err) {
|
||||
if (err) console.error(err)
|
||||
store.set('migrated_version', app.getVersion())
|
||||
console.debug('migrate extensions done')
|
||||
resolve(undefined)
|
||||
})
|
||||
} else {
|
||||
resolve(undefined)
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -3,15 +3,16 @@ import { AssistantExtension } from "@janhq/core";
|
||||
import { join } from "path";
|
||||
|
||||
export default class JanAssistantExtension implements AssistantExtension {
|
||||
private static readonly _homeDir = "assistants";
|
||||
private static readonly _homeDir = "file://assistants";
|
||||
|
||||
type(): ExtensionType {
|
||||
return ExtensionType.Assistant;
|
||||
}
|
||||
|
||||
onLoad(): void {
|
||||
async onLoad() {
|
||||
// making the assistant directory
|
||||
fs.mkdir(JanAssistantExtension._homeDir).then(() => {
|
||||
if (!(await fs.existsSync(JanAssistantExtension._homeDir)))
|
||||
fs.mkdirSync(JanAssistantExtension._homeDir).then(() => {
|
||||
this.createJanAssistant();
|
||||
});
|
||||
}
|
||||
@ -23,12 +24,12 @@ export default class JanAssistantExtension implements AssistantExtension {
|
||||
|
||||
async createAssistant(assistant: Assistant): Promise<void> {
|
||||
const assistantDir = join(JanAssistantExtension._homeDir, assistant.id);
|
||||
await fs.mkdir(assistantDir);
|
||||
if (!(await fs.existsSync(assistantDir))) await fs.mkdirSync(assistantDir);
|
||||
|
||||
// store the assistant metadata json
|
||||
const assistantMetadataPath = join(assistantDir, "assistant.json");
|
||||
try {
|
||||
await fs.writeFile(
|
||||
await fs.writeFileSync(
|
||||
assistantMetadataPath,
|
||||
JSON.stringify(assistant, null, 2)
|
||||
);
|
||||
@ -41,18 +42,14 @@ export default class JanAssistantExtension implements AssistantExtension {
|
||||
// get all the assistant directories
|
||||
// get all the assistant metadata json
|
||||
const results: Assistant[] = [];
|
||||
const allFileName: string[] = await fs.listFiles(
|
||||
const allFileName: string[] = await fs.readdirSync(
|
||||
JanAssistantExtension._homeDir
|
||||
);
|
||||
for (const fileName of allFileName) {
|
||||
const filePath = join(JanAssistantExtension._homeDir, fileName);
|
||||
const isDirectory = await fs.isDirectory(filePath);
|
||||
if (!isDirectory) {
|
||||
// if not a directory, ignore
|
||||
continue;
|
||||
}
|
||||
|
||||
const jsonFiles: string[] = (await fs.listFiles(filePath)).filter(
|
||||
if (filePath.includes(".DS_Store")) continue;
|
||||
const jsonFiles: string[] = (await fs.readdirSync(filePath)).filter(
|
||||
(file: string) => file === "assistant.json"
|
||||
);
|
||||
|
||||
@ -61,9 +58,12 @@ export default class JanAssistantExtension implements AssistantExtension {
|
||||
continue;
|
||||
}
|
||||
|
||||
const assistant: Assistant = JSON.parse(
|
||||
await fs.readFile(join(filePath, jsonFiles[0]))
|
||||
const content = await fs.readFileSync(
|
||||
join(filePath, jsonFiles[0]),
|
||||
"utf-8"
|
||||
);
|
||||
const assistant: Assistant =
|
||||
typeof content === "object" ? content : JSON.parse(content);
|
||||
|
||||
results.push(assistant);
|
||||
}
|
||||
@ -78,7 +78,7 @@ export default class JanAssistantExtension implements AssistantExtension {
|
||||
|
||||
// remove the directory
|
||||
const assistantDir = join(JanAssistantExtension._homeDir, assistant.id);
|
||||
await fs.rmdir(assistantDir);
|
||||
await fs.rmdirSync(assistantDir);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { ExtensionType, fs } from '@janhq/core'
|
||||
import { ExtensionType, fs, joinPath } from '@janhq/core'
|
||||
import { ConversationalExtension } from '@janhq/core'
|
||||
import { Thread, ThreadMessage } from '@janhq/core'
|
||||
import { join } from 'path'
|
||||
|
||||
/**
|
||||
* JSONConversationalExtension is a ConversationalExtension implementation that provides
|
||||
@ -10,7 +9,7 @@ import { join } from 'path'
|
||||
export default class JSONConversationalExtension
|
||||
implements ConversationalExtension
|
||||
{
|
||||
private static readonly _homeDir = 'threads'
|
||||
private static readonly _homeDir = 'file://threads'
|
||||
private static readonly _threadInfoFileName = 'thread.json'
|
||||
private static readonly _threadMessagesFileName = 'messages.jsonl'
|
||||
|
||||
@ -24,8 +23,9 @@ export default class JSONConversationalExtension
|
||||
/**
|
||||
* Called when the extension is loaded.
|
||||
*/
|
||||
onLoad() {
|
||||
fs.mkdir(JSONConversationalExtension._homeDir)
|
||||
async onLoad() {
|
||||
if (!(await fs.existsSync(JSONConversationalExtension._homeDir)))
|
||||
fs.mkdirSync(JSONConversationalExtension._homeDir)
|
||||
console.debug('JSONConversationalExtension loaded')
|
||||
}
|
||||
|
||||
@ -48,7 +48,9 @@ export default class JSONConversationalExtension
|
||||
const convos = promiseResults
|
||||
.map((result) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
return JSON.parse(result.value) as Thread
|
||||
return typeof result.value === 'object'
|
||||
? result.value
|
||||
: JSON.parse(result.value)
|
||||
}
|
||||
})
|
||||
.filter((convo) => convo != null)
|
||||
@ -69,16 +71,19 @@ export default class JSONConversationalExtension
|
||||
*/
|
||||
async saveThread(thread: Thread): Promise<void> {
|
||||
try {
|
||||
const threadDirPath = join(
|
||||
const threadDirPath = await joinPath([
|
||||
JSONConversationalExtension._homeDir,
|
||||
thread.id
|
||||
)
|
||||
const threadJsonPath = join(
|
||||
thread.id,
|
||||
])
|
||||
const threadJsonPath = await joinPath([
|
||||
threadDirPath,
|
||||
JSONConversationalExtension._threadInfoFileName
|
||||
)
|
||||
await fs.mkdir(threadDirPath)
|
||||
await fs.writeFile(threadJsonPath, JSON.stringify(thread, null, 2))
|
||||
JSONConversationalExtension._threadInfoFileName,
|
||||
])
|
||||
if (!(await fs.existsSync(threadDirPath))) {
|
||||
await fs.mkdirSync(threadDirPath)
|
||||
}
|
||||
|
||||
await fs.writeFileSync(threadJsonPath, JSON.stringify(thread))
|
||||
Promise.resolve()
|
||||
} catch (err) {
|
||||
Promise.reject(err)
|
||||
@ -89,22 +94,26 @@ export default class JSONConversationalExtension
|
||||
* Delete a thread with the specified ID.
|
||||
* @param threadId The ID of the thread to delete.
|
||||
*/
|
||||
deleteThread(threadId: string): Promise<void> {
|
||||
return fs.rmdir(join(JSONConversationalExtension._homeDir, `${threadId}`))
|
||||
async deleteThread(threadId: string): Promise<void> {
|
||||
return fs.rmdirSync(
|
||||
await joinPath([JSONConversationalExtension._homeDir, `${threadId}`]),
|
||||
{ recursive: true }
|
||||
)
|
||||
}
|
||||
|
||||
async addNewMessage(message: ThreadMessage): Promise<void> {
|
||||
try {
|
||||
const threadDirPath = join(
|
||||
const threadDirPath = await joinPath([
|
||||
JSONConversationalExtension._homeDir,
|
||||
message.thread_id
|
||||
)
|
||||
const threadMessagePath = join(
|
||||
message.thread_id,
|
||||
])
|
||||
const threadMessagePath = await joinPath([
|
||||
threadDirPath,
|
||||
JSONConversationalExtension._threadMessagesFileName
|
||||
)
|
||||
await fs.mkdir(threadDirPath)
|
||||
await fs.appendFile(threadMessagePath, JSON.stringify(message) + '\n')
|
||||
JSONConversationalExtension._threadMessagesFileName,
|
||||
])
|
||||
if (!(await fs.existsSync(threadDirPath)))
|
||||
await fs.mkdirSync(threadDirPath)
|
||||
await fs.appendFileSync(threadMessagePath, JSON.stringify(message) + '\n')
|
||||
Promise.resolve()
|
||||
} catch (err) {
|
||||
Promise.reject(err)
|
||||
@ -116,13 +125,17 @@ export default class JSONConversationalExtension
|
||||
messages: ThreadMessage[]
|
||||
): Promise<void> {
|
||||
try {
|
||||
const threadDirPath = join(JSONConversationalExtension._homeDir, threadId)
|
||||
const threadMessagePath = join(
|
||||
const threadDirPath = await joinPath([
|
||||
JSONConversationalExtension._homeDir,
|
||||
threadId,
|
||||
])
|
||||
const threadMessagePath = await joinPath([
|
||||
threadDirPath,
|
||||
JSONConversationalExtension._threadMessagesFileName
|
||||
)
|
||||
await fs.mkdir(threadDirPath)
|
||||
await fs.writeFile(
|
||||
JSONConversationalExtension._threadMessagesFileName,
|
||||
])
|
||||
if (!(await fs.existsSync(threadDirPath)))
|
||||
await fs.mkdirSync(threadDirPath)
|
||||
await fs.writeFileSync(
|
||||
threadMessagePath,
|
||||
messages.map((msg) => JSON.stringify(msg)).join('\n') +
|
||||
(messages.length ? '\n' : '')
|
||||
@ -139,12 +152,13 @@ export default class JSONConversationalExtension
|
||||
* @returns data of the thread
|
||||
*/
|
||||
private async readThread(threadDirName: string): Promise<any> {
|
||||
return fs.readFile(
|
||||
join(
|
||||
return fs.readFileSync(
|
||||
await joinPath([
|
||||
JSONConversationalExtension._homeDir,
|
||||
threadDirName,
|
||||
JSONConversationalExtension._threadInfoFileName
|
||||
)
|
||||
JSONConversationalExtension._threadInfoFileName,
|
||||
]),
|
||||
'utf-8'
|
||||
)
|
||||
}
|
||||
|
||||
@ -153,23 +167,19 @@ export default class JSONConversationalExtension
|
||||
* @private
|
||||
*/
|
||||
private async getValidThreadDirs(): Promise<string[]> {
|
||||
const fileInsideThread: string[] = await fs.listFiles(
|
||||
const fileInsideThread: string[] = await fs.readdirSync(
|
||||
JSONConversationalExtension._homeDir
|
||||
)
|
||||
|
||||
const threadDirs: string[] = []
|
||||
for (let i = 0; i < fileInsideThread.length; i++) {
|
||||
const path = join(
|
||||
if (fileInsideThread[i].includes('.DS_Store')) continue
|
||||
const path = await joinPath([
|
||||
JSONConversationalExtension._homeDir,
|
||||
fileInsideThread[i]
|
||||
)
|
||||
const isDirectory = await fs.isDirectory(path)
|
||||
if (!isDirectory) {
|
||||
console.debug(`Ignore ${path} because it is not a directory`)
|
||||
continue
|
||||
}
|
||||
fileInsideThread[i],
|
||||
])
|
||||
|
||||
const isHavingThreadInfo = (await fs.listFiles(path)).includes(
|
||||
const isHavingThreadInfo = (await fs.readdirSync(path)).includes(
|
||||
JSONConversationalExtension._threadInfoFileName
|
||||
)
|
||||
if (!isHavingThreadInfo) {
|
||||
@ -184,25 +194,31 @@ export default class JSONConversationalExtension
|
||||
|
||||
async getAllMessages(threadId: string): Promise<ThreadMessage[]> {
|
||||
try {
|
||||
const threadDirPath = join(JSONConversationalExtension._homeDir, threadId)
|
||||
const isDir = await fs.isDirectory(threadDirPath)
|
||||
if (!isDir) {
|
||||
throw Error(`${threadDirPath} is not directory`)
|
||||
}
|
||||
const threadDirPath = await joinPath([
|
||||
JSONConversationalExtension._homeDir,
|
||||
threadId,
|
||||
])
|
||||
|
||||
const files: string[] = await fs.listFiles(threadDirPath)
|
||||
const files: string[] = await fs.readdirSync(threadDirPath)
|
||||
if (
|
||||
!files.includes(JSONConversationalExtension._threadMessagesFileName)
|
||||
) {
|
||||
throw Error(`${threadDirPath} not contains message file`)
|
||||
}
|
||||
|
||||
const messageFilePath = join(
|
||||
const messageFilePath = await joinPath([
|
||||
threadDirPath,
|
||||
JSONConversationalExtension._threadMessagesFileName
|
||||
)
|
||||
JSONConversationalExtension._threadMessagesFileName,
|
||||
])
|
||||
|
||||
const result = await fs.readLineByLine(messageFilePath)
|
||||
const result = await fs
|
||||
.readFileSync(messageFilePath, 'utf-8')
|
||||
.then((content) =>
|
||||
content
|
||||
.toString()
|
||||
.split('\n')
|
||||
.filter((line) => line !== '')
|
||||
)
|
||||
|
||||
const messages: ThreadMessage[] = []
|
||||
result.forEach((line: string) => {
|
||||
|
||||
@ -17,9 +17,9 @@ import {
|
||||
ThreadMessage,
|
||||
events,
|
||||
executeOnMain,
|
||||
getUserSpace,
|
||||
fs,
|
||||
Model,
|
||||
joinPath,
|
||||
} from "@janhq/core";
|
||||
import { InferenceExtension } from "@janhq/core";
|
||||
import { requestInference } from "./helpers/sse";
|
||||
@ -58,8 +58,9 @@ export default class JanInferenceNitroExtension implements InferenceExtension {
|
||||
/**
|
||||
* Subscribes to events emitted by the @janhq/core package.
|
||||
*/
|
||||
onLoad(): void {
|
||||
fs.mkdir(JanInferenceNitroExtension._homeDir);
|
||||
async onLoad() {
|
||||
if (!(await fs.existsSync(JanInferenceNitroExtension._homeDir)))
|
||||
fs.mkdirSync(JanInferenceNitroExtension._homeDir);
|
||||
this.writeDefaultEngineSettings();
|
||||
|
||||
// Events subscription
|
||||
@ -91,12 +92,12 @@ export default class JanInferenceNitroExtension implements InferenceExtension {
|
||||
JanInferenceNitroExtension._homeDir,
|
||||
JanInferenceNitroExtension._engineMetadataFileName
|
||||
);
|
||||
if (await fs.exists(engineFile)) {
|
||||
JanInferenceNitroExtension._engineSettings = JSON.parse(
|
||||
await fs.readFile(engineFile)
|
||||
);
|
||||
if (await fs.existsSync(engineFile)) {
|
||||
const engine = await fs.readFileSync(engineFile, "utf-8");
|
||||
JanInferenceNitroExtension._engineSettings =
|
||||
typeof engine === "object" ? engine : JSON.parse(engine);
|
||||
} else {
|
||||
await fs.writeFile(
|
||||
await fs.writeFileSync(
|
||||
engineFile,
|
||||
JSON.stringify(JanInferenceNitroExtension._engineSettings, null, 2)
|
||||
);
|
||||
@ -110,8 +111,7 @@ export default class JanInferenceNitroExtension implements InferenceExtension {
|
||||
if (model.engine !== "nitro") {
|
||||
return;
|
||||
}
|
||||
const userSpacePath = await getUserSpace();
|
||||
const modelFullPath = join(userSpacePath, "models", model.id, model.id);
|
||||
const modelFullPath = await joinPath(["models", model.id]);
|
||||
|
||||
const nitroInitResult = await executeOnMain(MODULE, "initModel", {
|
||||
modelFullPath: modelFullPath,
|
||||
|
||||
@ -13,10 +13,11 @@ const NITRO_HTTP_LOAD_MODEL_URL = `${NITRO_HTTP_SERVER_URL}/inferences/llamacpp/
|
||||
const NITRO_HTTP_UNLOAD_MODEL_URL = `${NITRO_HTTP_SERVER_URL}/inferences/llamacpp/unloadModel`;
|
||||
const NITRO_HTTP_VALIDATE_MODEL_URL = `${NITRO_HTTP_SERVER_URL}/inferences/llamacpp/modelstatus`;
|
||||
const NITRO_HTTP_KILL_URL = `${NITRO_HTTP_SERVER_URL}/processmanager/destroy`;
|
||||
const SUPPORTED_MODEL_FORMAT = ".gguf";
|
||||
|
||||
// The subprocess instance for Nitro
|
||||
let subprocess = undefined;
|
||||
let currentModelFile = undefined;
|
||||
let currentModelFile: string = undefined;
|
||||
let currentSettings = undefined;
|
||||
|
||||
/**
|
||||
@ -37,6 +38,21 @@ function stopModel(): Promise<void> {
|
||||
*/
|
||||
async function initModel(wrapper: any): Promise<ModelOperationResponse> {
|
||||
currentModelFile = wrapper.modelFullPath;
|
||||
const janRoot = path.join(require("os").homedir(), "jan");
|
||||
if (!currentModelFile.includes(janRoot)) {
|
||||
currentModelFile = path.join(janRoot, currentModelFile);
|
||||
}
|
||||
const files: string[] = fs.readdirSync(currentModelFile);
|
||||
|
||||
// Look for GGUF model file
|
||||
const ggufBinFile = files.find(
|
||||
(file) =>
|
||||
file === path.basename(currentModelFile) ||
|
||||
file.toLowerCase().includes(SUPPORTED_MODEL_FORMAT)
|
||||
);
|
||||
|
||||
currentModelFile = path.join(currentModelFile, ggufBinFile);
|
||||
|
||||
if (wrapper.model.engine !== "nitro") {
|
||||
return Promise.resolve({ error: "Not a nitro model" });
|
||||
} else {
|
||||
@ -66,14 +82,14 @@ async function initModel(wrapper: any): Promise<ModelOperationResponse> {
|
||||
async function loadModel(nitroResourceProbe: any | undefined) {
|
||||
// Gather system information for CPU physical cores and memory
|
||||
if (!nitroResourceProbe) nitroResourceProbe = await getResourcesInfo();
|
||||
return killSubprocess()
|
||||
return (
|
||||
killSubprocess()
|
||||
.then(() => tcpPortUsed.waitUntilFree(PORT, 300, 5000))
|
||||
// wait for 500ms to make sure the port is free for windows platform
|
||||
.then(() => {
|
||||
if (process.platform === "win32") {
|
||||
return sleep(500);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return sleep(0);
|
||||
}
|
||||
})
|
||||
@ -84,7 +100,8 @@ async function loadModel(nitroResourceProbe: any | undefined) {
|
||||
console.error("error: ", err);
|
||||
// TODO: Broadcast error so app could display proper error message
|
||||
return { error: err, currentModelFile };
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Add function sleep
|
||||
@ -259,11 +276,11 @@ function spawnNitroProcess(nitroResourceProbe: any): Promise<any> {
|
||||
function getResourcesInfo(): Promise<ResourcesInfo> {
|
||||
return new Promise(async (resolve) => {
|
||||
const cpu = await si.cpu();
|
||||
const mem = await si.mem();
|
||||
// const mem = await si.mem();
|
||||
|
||||
const response = {
|
||||
const response: ResourcesInfo = {
|
||||
numCpuPhysicalCore: cpu.physicalCores,
|
||||
memAvailable: mem.available,
|
||||
memAvailable: 0,
|
||||
};
|
||||
resolve(response);
|
||||
});
|
||||
|
||||
@ -29,7 +29,7 @@ import { join } from "path";
|
||||
* It also subscribes to events emitted by the @janhq/core package and handles new message requests.
|
||||
*/
|
||||
export default class JanInferenceOpenAIExtension implements InferenceExtension {
|
||||
private static readonly _homeDir = "engines";
|
||||
private static readonly _homeDir = "file://engines";
|
||||
private static readonly _engineMetadataFileName = "openai.json";
|
||||
|
||||
private static _currentModel: OpenAIModel;
|
||||
@ -53,8 +53,9 @@ export default class JanInferenceOpenAIExtension implements InferenceExtension {
|
||||
/**
|
||||
* Subscribes to events emitted by the @janhq/core package.
|
||||
*/
|
||||
onLoad(): void {
|
||||
fs.mkdir(JanInferenceOpenAIExtension._homeDir);
|
||||
async onLoad() {
|
||||
if (!(await fs.existsSync(JanInferenceOpenAIExtension._homeDir)))
|
||||
fs.mkdirSync(JanInferenceOpenAIExtension._homeDir);
|
||||
JanInferenceOpenAIExtension.writeDefaultEngineSettings();
|
||||
|
||||
// Events subscription
|
||||
@ -85,12 +86,12 @@ export default class JanInferenceOpenAIExtension implements InferenceExtension {
|
||||
JanInferenceOpenAIExtension._homeDir,
|
||||
JanInferenceOpenAIExtension._engineMetadataFileName
|
||||
);
|
||||
if (await fs.exists(engineFile)) {
|
||||
JanInferenceOpenAIExtension._engineSettings = JSON.parse(
|
||||
await fs.readFile(engineFile)
|
||||
);
|
||||
if (await fs.existsSync(engineFile)) {
|
||||
const engine = await fs.readFileSync(engineFile, 'utf-8');
|
||||
JanInferenceOpenAIExtension._engineSettings =
|
||||
typeof engine === "object" ? engine : JSON.parse(engine);
|
||||
} else {
|
||||
await fs.writeFile(
|
||||
await fs.writeFileSync(
|
||||
engineFile,
|
||||
JSON.stringify(JanInferenceOpenAIExtension._engineSettings, null, 2)
|
||||
);
|
||||
|
||||
@ -57,8 +57,8 @@ export default class JanInferenceTritonTrtLLMExtension
|
||||
/**
|
||||
* Subscribes to events emitted by the @janhq/core package.
|
||||
*/
|
||||
onLoad(): void {
|
||||
fs.mkdir(JanInferenceTritonTrtLLMExtension._homeDir);
|
||||
async onLoad() {
|
||||
if (!(await fs.existsSync(JanInferenceTritonTrtLLMExtension._homeDir)))
|
||||
JanInferenceTritonTrtLLMExtension.writeDefaultEngineSettings();
|
||||
|
||||
// Events subscription
|
||||
@ -98,12 +98,12 @@ export default class JanInferenceTritonTrtLLMExtension
|
||||
JanInferenceTritonTrtLLMExtension._homeDir,
|
||||
JanInferenceTritonTrtLLMExtension._engineMetadataFileName
|
||||
);
|
||||
if (await fs.exists(engine_json)) {
|
||||
JanInferenceTritonTrtLLMExtension._engineSettings = JSON.parse(
|
||||
await fs.readFile(engine_json)
|
||||
);
|
||||
if (await fs.existsSync(engine_json)) {
|
||||
const engine = await fs.readFileSync(engine_json, "utf-8");
|
||||
JanInferenceTritonTrtLLMExtension._engineSettings =
|
||||
typeof engine === "object" ? engine : JSON.parse(engine);
|
||||
} else {
|
||||
await fs.writeFile(
|
||||
await fs.writeFileSync(
|
||||
engine_json,
|
||||
JSON.stringify(
|
||||
JanInferenceTritonTrtLLMExtension._engineSettings,
|
||||
|
||||
@ -5,16 +5,21 @@ import {
|
||||
abortDownload,
|
||||
getResourcePath,
|
||||
getUserSpace,
|
||||
InferenceEngine,
|
||||
joinPath,
|
||||
} from '@janhq/core'
|
||||
import { ModelExtension, Model, ModelState } from '@janhq/core'
|
||||
import { join } from 'path'
|
||||
import { basename } from 'path'
|
||||
import { ModelExtension, Model } from '@janhq/core'
|
||||
|
||||
/**
|
||||
* A extension for models
|
||||
*/
|
||||
export default class JanModelExtension implements ModelExtension {
|
||||
private static readonly _homeDir = 'models'
|
||||
private static readonly _homeDir = 'file://models'
|
||||
private static readonly _modelMetadataFileName = 'model.json'
|
||||
private static readonly _supportedModelFormat = '.gguf'
|
||||
private static readonly _incompletedModelFileName = '.download'
|
||||
private static readonly _offlineInferenceEngine = InferenceEngine.nitro
|
||||
|
||||
/**
|
||||
* Implements type from JanExtension.
|
||||
@ -41,11 +46,11 @@ export default class JanModelExtension implements ModelExtension {
|
||||
|
||||
private async copyModelsToHomeDir() {
|
||||
try {
|
||||
if (
|
||||
localStorage.getItem(`${EXTENSION_NAME}-version`) === VERSION &&
|
||||
(await fs.exists(JanModelExtension._homeDir))
|
||||
) {
|
||||
console.debug('Model already migrated')
|
||||
// list all of the files under the home directory
|
||||
|
||||
if (fs.existsSync(JanModelExtension._homeDir)) {
|
||||
// ignore if the model is already downloaded
|
||||
console.debug('Models already persisted.')
|
||||
return
|
||||
}
|
||||
|
||||
@ -54,10 +59,10 @@ export default class JanModelExtension implements ModelExtension {
|
||||
|
||||
// copy models folder from resources to home directory
|
||||
const resourePath = await getResourcePath()
|
||||
const srcPath = join(resourePath, 'models')
|
||||
const srcPath = await joinPath([resourePath, 'models'])
|
||||
|
||||
const userSpace = await getUserSpace()
|
||||
const destPath = join(userSpace, JanModelExtension._homeDir)
|
||||
const destPath = await joinPath([userSpace, JanModelExtension._homeDir])
|
||||
|
||||
await fs.syncFile(srcPath, destPath)
|
||||
|
||||
@ -88,11 +93,18 @@ export default class JanModelExtension implements ModelExtension {
|
||||
*/
|
||||
async downloadModel(model: Model): Promise<void> {
|
||||
// create corresponding directory
|
||||
const directoryPath = join(JanModelExtension._homeDir, model.id)
|
||||
await fs.mkdir(directoryPath)
|
||||
const modelDirPath = await joinPath([JanModelExtension._homeDir, model.id])
|
||||
if (!(await fs.existsSync(modelDirPath))) await fs.mkdirSync(modelDirPath)
|
||||
|
||||
// path to model binary
|
||||
const path = join(directoryPath, model.id)
|
||||
// try to retrieve the download file name from the source url
|
||||
// if it fails, use the model ID as the file name
|
||||
const extractedFileName = basename(model.source_url)
|
||||
const fileName = extractedFileName
|
||||
.toLowerCase()
|
||||
.endsWith(JanModelExtension._supportedModelFormat)
|
||||
? extractedFileName
|
||||
: model.id
|
||||
const path = await joinPath([modelDirPath, fileName])
|
||||
downloadFile(model.source_url, path)
|
||||
}
|
||||
|
||||
@ -103,9 +115,11 @@ export default class JanModelExtension implements ModelExtension {
|
||||
*/
|
||||
async cancelModelDownload(modelId: string): Promise<void> {
|
||||
return abortDownload(
|
||||
join(JanModelExtension._homeDir, modelId, modelId)
|
||||
).then(() => {
|
||||
fs.deleteFile(join(JanModelExtension._homeDir, modelId, modelId))
|
||||
await joinPath([JanModelExtension._homeDir, modelId, modelId])
|
||||
).then(async () => {
|
||||
fs.unlinkSync(
|
||||
await joinPath([JanModelExtension._homeDir, modelId, modelId])
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@ -116,27 +130,16 @@ export default class JanModelExtension implements ModelExtension {
|
||||
*/
|
||||
async deleteModel(modelId: string): Promise<void> {
|
||||
try {
|
||||
const dirPath = join(JanModelExtension._homeDir, modelId)
|
||||
const dirPath = await joinPath([JanModelExtension._homeDir, modelId])
|
||||
|
||||
// remove all files under dirPath except model.json
|
||||
const files = await fs.listFiles(dirPath)
|
||||
const deletePromises = files.map((fileName: string) => {
|
||||
const files = await fs.readdirSync(dirPath)
|
||||
const deletePromises = files.map(async (fileName: string) => {
|
||||
if (fileName !== JanModelExtension._modelMetadataFileName) {
|
||||
return fs.deleteFile(join(dirPath, fileName))
|
||||
return fs.unlinkSync(await joinPath([dirPath, fileName]))
|
||||
}
|
||||
})
|
||||
await Promise.allSettled(deletePromises)
|
||||
|
||||
// update the state as default
|
||||
const jsonFilePath = join(
|
||||
dirPath,
|
||||
JanModelExtension._modelMetadataFileName
|
||||
)
|
||||
const json = await fs.readFile(jsonFilePath)
|
||||
const model = JSON.parse(json) as Model
|
||||
delete model.state
|
||||
|
||||
await fs.writeFile(jsonFilePath, JSON.stringify(model, null, 2))
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
@ -148,24 +151,14 @@ export default class JanModelExtension implements ModelExtension {
|
||||
* @returns A Promise that resolves when the model is saved.
|
||||
*/
|
||||
async saveModel(model: Model): Promise<void> {
|
||||
const jsonFilePath = join(
|
||||
const jsonFilePath = await joinPath([
|
||||
JanModelExtension._homeDir,
|
||||
model.id,
|
||||
JanModelExtension._modelMetadataFileName
|
||||
)
|
||||
JanModelExtension._modelMetadataFileName,
|
||||
])
|
||||
|
||||
try {
|
||||
await fs.writeFile(
|
||||
jsonFilePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
...model,
|
||||
state: ModelState.Ready,
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
)
|
||||
await fs.writeFileSync(jsonFilePath, JSON.stringify(model, null, 2))
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
@ -176,43 +169,70 @@ export default class JanModelExtension implements ModelExtension {
|
||||
* @returns A Promise that resolves with an array of all models.
|
||||
*/
|
||||
async getDownloadedModels(): Promise<Model[]> {
|
||||
const models = await this.getModelsMetadata()
|
||||
return models.filter((model) => model.state === ModelState.Ready)
|
||||
return await this.getModelsMetadata(
|
||||
async (modelDir: string, model: Model) => {
|
||||
if (model.engine !== JanModelExtension._offlineInferenceEngine) {
|
||||
return true
|
||||
}
|
||||
return await fs
|
||||
.readdirSync(await joinPath([JanModelExtension._homeDir, modelDir]))
|
||||
.then((files: string[]) => {
|
||||
// or model binary exists in the directory
|
||||
// model binary name can match model ID or be a .gguf file and not be an incompleted model file
|
||||
return (
|
||||
files.includes(modelDir) ||
|
||||
files.some(
|
||||
(file) =>
|
||||
file
|
||||
.toLowerCase()
|
||||
.includes(JanModelExtension._supportedModelFormat) &&
|
||||
!file.endsWith(JanModelExtension._incompletedModelFileName)
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private async getModelsMetadata(): Promise<Model[]> {
|
||||
private async getModelsMetadata(
|
||||
selector?: (path: string, model: Model) => Promise<boolean>
|
||||
): Promise<Model[]> {
|
||||
try {
|
||||
const filesUnderJanRoot = await fs.listFiles('')
|
||||
if (!filesUnderJanRoot.includes(JanModelExtension._homeDir)) {
|
||||
if (!(await fs.existsSync(JanModelExtension._homeDir))) {
|
||||
console.debug('model folder not found')
|
||||
return []
|
||||
}
|
||||
|
||||
const files: string[] = await fs.listFiles(JanModelExtension._homeDir)
|
||||
const files: string[] = await fs.readdirSync(JanModelExtension._homeDir)
|
||||
|
||||
const allDirectories: string[] = []
|
||||
for (const file of files) {
|
||||
const isDirectory = await fs.isDirectory(
|
||||
join(JanModelExtension._homeDir, file)
|
||||
)
|
||||
if (isDirectory) {
|
||||
if (file === '.DS_Store') continue
|
||||
allDirectories.push(file)
|
||||
}
|
||||
}
|
||||
|
||||
const readJsonPromises = allDirectories.map((dirName) => {
|
||||
const jsonPath = join(
|
||||
const readJsonPromises = allDirectories.map(async (dirName) => {
|
||||
// filter out directories that don't match the selector
|
||||
|
||||
// read model.json
|
||||
const jsonPath = await joinPath([
|
||||
JanModelExtension._homeDir,
|
||||
dirName,
|
||||
JanModelExtension._modelMetadataFileName
|
||||
)
|
||||
return this.readModelMetadata(jsonPath)
|
||||
JanModelExtension._modelMetadataFileName,
|
||||
])
|
||||
let model = await this.readModelMetadata(jsonPath)
|
||||
model = typeof model === 'object' ? model : JSON.parse(model)
|
||||
|
||||
if (selector && !(await selector?.(dirName, model))) {
|
||||
return
|
||||
}
|
||||
return model
|
||||
})
|
||||
const results = await Promise.allSettled(readJsonPromises)
|
||||
const modelData = results.map((result) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
try {
|
||||
return JSON.parse(result.value) as Model
|
||||
return result.value as Model
|
||||
} catch {
|
||||
console.debug(`Unable to parse model metadata: ${result.value}`)
|
||||
return undefined
|
||||
@ -222,6 +242,7 @@ export default class JanModelExtension implements ModelExtension {
|
||||
return undefined
|
||||
}
|
||||
})
|
||||
|
||||
return modelData.filter((e) => !!e)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
@ -230,7 +251,7 @@ export default class JanModelExtension implements ModelExtension {
|
||||
}
|
||||
|
||||
private readModelMetadata(path: string) {
|
||||
return fs.readFile(join(path))
|
||||
return fs.readFileSync(path, 'utf-8')
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -4,11 +4,11 @@ const getResourcesInfo = async () =>
|
||||
new Promise(async (resolve) => {
|
||||
const cpu = await si.cpu();
|
||||
const mem = await si.mem();
|
||||
const gpu = await si.graphics();
|
||||
// const gpu = await si.graphics();
|
||||
const response = {
|
||||
cpu,
|
||||
mem,
|
||||
gpu,
|
||||
// gpu,
|
||||
};
|
||||
resolve(response);
|
||||
});
|
||||
|
||||
@ -6,7 +6,8 @@
|
||||
"uikit",
|
||||
"core",
|
||||
"electron",
|
||||
"web"
|
||||
"web",
|
||||
"server"
|
||||
],
|
||||
"nohoist": [
|
||||
"uikit",
|
||||
@ -16,7 +17,9 @@
|
||||
"electron",
|
||||
"electron/**",
|
||||
"web",
|
||||
"web/**"
|
||||
"web/**",
|
||||
"server",
|
||||
"server/**"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
@ -28,6 +31,7 @@
|
||||
"test-local": "yarn lint && yarn build:test && yarn test",
|
||||
"dev:uikit": "yarn workspace @janhq/uikit install && yarn workspace @janhq/uikit dev",
|
||||
"build:uikit": "yarn workspace @janhq/uikit install && yarn workspace @janhq/uikit build",
|
||||
"build:server": "cd server && yarn install && yarn run build",
|
||||
"build:core": "cd core && yarn install && yarn run build",
|
||||
"build:web": "yarn workspace jan-web build && cpx \"web/out/**\" \"electron/renderer/\"",
|
||||
"build:electron": "cpx \"models/**\" \"electron/models/\" && yarn workspace jan build",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 38 KiB |
43
server/index.ts
Normal file
43
server/index.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import fastify from "fastify";
|
||||
import dotenv from "dotenv";
|
||||
import { v1Router } from "@janhq/core/node";
|
||||
import path from "path";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const JAN_API_HOST = process.env.JAN_API_HOST || "0.0.0.0";
|
||||
const JAN_API_PORT = Number.parseInt(process.env.JAN_API_PORT || "1337");
|
||||
|
||||
const server = fastify();
|
||||
server.register(require("@fastify/cors"), {});
|
||||
server.register(
|
||||
(childContext, _, done) => {
|
||||
childContext.register(require("@fastify/static"), {
|
||||
root:
|
||||
process.env.EXTENSION_ROOT ||
|
||||
path.join(require("os").homedir(), "jan", "extensions"),
|
||||
wildcard: false,
|
||||
});
|
||||
|
||||
done();
|
||||
},
|
||||
{ prefix: "extensions" }
|
||||
);
|
||||
server.register(v1Router, { prefix: "/v1" });
|
||||
|
||||
export const startServer = () => {
|
||||
server
|
||||
.listen({
|
||||
port: JAN_API_PORT,
|
||||
host: JAN_API_HOST,
|
||||
})
|
||||
.then(() => {
|
||||
console.log(
|
||||
`JAN API listening at: http://${JAN_API_HOST}:${JAN_API_PORT}`
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const stopServer = () => {
|
||||
server.close();
|
||||
};
|
||||
@ -1,19 +1,3 @@
|
||||
import fastify from 'fastify'
|
||||
import dotenv from 'dotenv'
|
||||
import v1API from './v1'
|
||||
const server = fastify()
|
||||
|
||||
dotenv.config()
|
||||
server.register(v1API, {prefix: "/api/v1"})
|
||||
|
||||
|
||||
const JAN_API_PORT = Number.parseInt(process.env.JAN_API_PORT || '1337')
|
||||
const JAN_API_HOST = process.env.JAN_API_HOST || "0.0.0.0"
|
||||
|
||||
server.listen({
|
||||
port: JAN_API_PORT,
|
||||
host: JAN_API_HOST
|
||||
}).then(() => {
|
||||
console.log(`JAN API listening at: http://${JAN_API_HOST}:${JAN_API_PORT}`);
|
||||
})
|
||||
import { startServer } from "./index";
|
||||
|
||||
startServer();
|
||||
|
||||
@ -1,32 +1,37 @@
|
||||
{
|
||||
"name": "jan-server",
|
||||
"name": "@janhq/server",
|
||||
"version": "0.1.3",
|
||||
"main": "./build/main.js",
|
||||
"main": "build/index.js",
|
||||
"types": "build/index.d.ts",
|
||||
"author": "Jan <service@jan.ai>",
|
||||
"license": "AGPL-3.0",
|
||||
"homepage": "https://jan.ai",
|
||||
"description": "Use offline LLMs with your own data. Run open source models like Llama2 or Falcon on your internal computers/servers.",
|
||||
"build": "",
|
||||
"files": [
|
||||
"build/**"
|
||||
],
|
||||
"scripts": {
|
||||
"lint": "eslint . --ext \".js,.jsx,.ts,.tsx\"",
|
||||
"test:e2e": "playwright test --workers=1",
|
||||
"dev": "nodemon .",
|
||||
"dev": "tsc --watch & node --watch build/main.js",
|
||||
"build": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fastify/cors": "^8.4.2",
|
||||
"@fastify/static": "^6.12.0",
|
||||
"@janhq/core": "link:./core",
|
||||
"dotenv": "^16.3.1",
|
||||
"fastify": "^4.24.3",
|
||||
"request": "^2.88.2",
|
||||
"request-progress": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/body-parser": "^1.19.5",
|
||||
"@types/npmcli__arborist": "^5.6.4",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.3",
|
||||
"@typescript-eslint/parser": "^6.7.3",
|
||||
"dotenv": "^16.3.1",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"fastify": "^4.24.3",
|
||||
"nodemon": "^3.0.1",
|
||||
"run-script-os": "^1.1.6"
|
||||
},
|
||||
"installConfig": {
|
||||
"hoistingLimits": "workspaces"
|
||||
"run-script-os": "^1.1.6",
|
||||
"typescript": "^5.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,7 +13,9 @@
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"paths": { "*": ["node_modules/*"] },
|
||||
"typeRoots": ["node_modules/@types"]
|
||||
"typeRoots": ["node_modules/@types"],
|
||||
"ignoreDeprecations": "5.0",
|
||||
"declaration": true
|
||||
},
|
||||
// "sourceMap": true,
|
||||
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
import { FastifyInstance, FastifyPluginAsync, FastifyPluginOptions } from 'fastify'
|
||||
|
||||
const router: FastifyPluginAsync = async (app: FastifyInstance, opts: FastifyPluginOptions) => {
|
||||
//TODO: Add controllers for assistants here
|
||||
// app.get("/", controller)
|
||||
// app.post("/", controller)
|
||||
}
|
||||
export default router;
|
||||
@ -1,11 +0,0 @@
|
||||
import { FastifyInstance, FastifyPluginAsync, FastifyPluginOptions } from 'fastify'
|
||||
|
||||
const router: FastifyPluginAsync = async (app: FastifyInstance, opts: FastifyPluginOptions) => {
|
||||
//TODO: Add controllers for here
|
||||
// app.get("/", controller)
|
||||
|
||||
app.post("/", (req, res) => {
|
||||
req.body
|
||||
})
|
||||
}
|
||||
export default router;
|
||||
@ -1,37 +0,0 @@
|
||||
import assistantsAPI from './assistants'
|
||||
import chatCompletionAPI from './chat'
|
||||
import modelsAPI from './models'
|
||||
import threadsAPI from './threads'
|
||||
|
||||
import { FastifyInstance, FastifyPluginAsync } from 'fastify'
|
||||
|
||||
const router: FastifyPluginAsync = async (app: FastifyInstance, opts) => {
|
||||
app.register(
|
||||
assistantsAPI,
|
||||
{
|
||||
prefix: "/assistants"
|
||||
}
|
||||
)
|
||||
|
||||
app.register(
|
||||
chatCompletionAPI,
|
||||
{
|
||||
prefix: "/chat/completion"
|
||||
}
|
||||
)
|
||||
|
||||
app.register(
|
||||
modelsAPI,
|
||||
{
|
||||
prefix: "/models"
|
||||
}
|
||||
)
|
||||
|
||||
app.register(
|
||||
threadsAPI,
|
||||
{
|
||||
prefix: "/threads"
|
||||
}
|
||||
)
|
||||
}
|
||||
export default router;
|
||||
@ -1,23 +0,0 @@
|
||||
import { RouteHandlerMethod, FastifyRequest, FastifyReply } from 'fastify'
|
||||
import { MODEL_FOLDER_PATH } from "./index"
|
||||
import fs from 'fs/promises'
|
||||
|
||||
const controller: RouteHandlerMethod = async (req: FastifyRequest, res: FastifyReply) => {
|
||||
//TODO: download models impl
|
||||
//Mirror logic from JanModelExtension.downloadModel?
|
||||
let model = req.body.model;
|
||||
|
||||
// Fetching logic
|
||||
// const directoryPath = join(MODEL_FOLDER_PATH, model.id)
|
||||
// await fs.mkdir(directoryPath)
|
||||
|
||||
// const path = join(directoryPath, model.id)
|
||||
// downloadFile(model.source_url, path)
|
||||
// TODO: Different model downloader from different model vendor
|
||||
|
||||
res.status(200).send({
|
||||
status: "Ok"
|
||||
})
|
||||
}
|
||||
|
||||
export default controller;
|
||||
@ -1,61 +0,0 @@
|
||||
|
||||
export const MODEL_FOLDER_PATH = "./data/models"
|
||||
export const _modelMetadataFileName = 'model.json'
|
||||
|
||||
import fs from 'fs/promises'
|
||||
import { Model } from '@janhq/core'
|
||||
import { join } from 'path'
|
||||
|
||||
// map string => model object
|
||||
let modelIndex = new Map<String, Model>();
|
||||
async function buildModelIndex(){
|
||||
let modelIds = await fs.readdir(MODEL_FOLDER_PATH);
|
||||
// TODO: read modelFolders to get model info, mirror JanModelExtension?
|
||||
try{
|
||||
for(let modelId in modelIds){
|
||||
let path = join(MODEL_FOLDER_PATH, modelId)
|
||||
let fileData = await fs.readFile(join(path, _modelMetadataFileName))
|
||||
modelIndex.set(modelId, JSON.parse(fileData.toString("utf-8")) as Model)
|
||||
}
|
||||
}
|
||||
catch(err){
|
||||
console.error("build model index failed. ", err);
|
||||
}
|
||||
}
|
||||
buildModelIndex()
|
||||
|
||||
import { FastifyInstance, FastifyPluginAsync, FastifyPluginOptions } from 'fastify'
|
||||
import downloadModelController from './downloadModel'
|
||||
import { startModel, stopModel } from './modelOp'
|
||||
|
||||
const router: FastifyPluginAsync = async (app: FastifyInstance, opts: FastifyPluginOptions) => {
|
||||
//TODO: Add controllers declaration here
|
||||
|
||||
///////////// CRUD ////////////////
|
||||
// Model listing
|
||||
app.get("/", async (req, res) => {
|
||||
res.status(200).send(
|
||||
modelIndex.values()
|
||||
)
|
||||
})
|
||||
|
||||
// Retrieve model info
|
||||
app.get("/:id", (req, res) => {
|
||||
res.status(200).send(
|
||||
modelIndex.get(req.params.id)
|
||||
)
|
||||
})
|
||||
|
||||
// Delete model
|
||||
app.delete("/:id", (req, res) => {
|
||||
modelIndex.delete(req.params)
|
||||
|
||||
// TODO: delete on disk
|
||||
})
|
||||
|
||||
///////////// Other ops ////////////////
|
||||
app.post("/", downloadModelController)
|
||||
app.put("/start", startModel)
|
||||
app.put("/stop", stopModel)
|
||||
}
|
||||
export default router;
|
||||
@ -1,11 +0,0 @@
|
||||
import {FastifyRequest, FastifyReply} from 'fastify'
|
||||
|
||||
export async function startModel(req: FastifyRequest, res: FastifyReply): Promise<void> {
|
||||
|
||||
|
||||
}
|
||||
|
||||
export async function stopModel(req: FastifyRequest, res: FastifyReply): Promise<void> {
|
||||
|
||||
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
import { FastifyInstance, FastifyPluginAsync, FastifyPluginOptions } from 'fastify'
|
||||
|
||||
const router: FastifyPluginAsync = async (app: FastifyInstance, opts: FastifyPluginOptions) => {
|
||||
//TODO: Add controllers declaration here
|
||||
|
||||
// app.get()
|
||||
}
|
||||
export default router;
|
||||
@ -29,6 +29,13 @@ export default function CardSidebar({
|
||||
|
||||
useClickOutside(() => setMore(false), null, [menu, toggle])
|
||||
|
||||
let openFolderTitle: string = 'Open Containing Folder'
|
||||
if (isMac) {
|
||||
openFolderTitle = 'Reveal in Finder'
|
||||
} else if (isWindows) {
|
||||
openFolderTitle = 'Reveal in File Explorer'
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
@ -74,7 +81,7 @@ export default function CardSidebar({
|
||||
>
|
||||
<FolderOpenIcon size={16} className="text-muted-foreground" />
|
||||
<span className="text-bold text-black dark:text-muted-foreground">
|
||||
Reveal in Finder
|
||||
{openFolderTitle}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
import { Fragment } from 'react'
|
||||
|
||||
import { ExtensionType } from '@janhq/core'
|
||||
import { ModelExtension } from '@janhq/core'
|
||||
import {
|
||||
Progress,
|
||||
Modal,
|
||||
@ -12,14 +10,19 @@ import {
|
||||
ModalTrigger,
|
||||
} from '@janhq/uikit'
|
||||
|
||||
import { useAtomValue } from 'jotai'
|
||||
|
||||
import useDownloadModel from '@/hooks/useDownloadModel'
|
||||
import { useDownloadState } from '@/hooks/useDownloadState'
|
||||
|
||||
import { formatDownloadPercentage } from '@/utils/converter'
|
||||
|
||||
import { extensionManager } from '@/extension'
|
||||
import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
|
||||
|
||||
export default function DownloadingState() {
|
||||
const { downloadStates } = useDownloadState()
|
||||
const downloadingModels = useAtomValue(downloadingModelsAtom)
|
||||
const { abortModelDownload } = useDownloadModel()
|
||||
|
||||
const totalCurrentProgress = downloadStates
|
||||
.map((a) => a.size.transferred + a.size.transferred)
|
||||
@ -73,9 +76,10 @@ export default function DownloadingState() {
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (item?.modelId) {
|
||||
extensionManager
|
||||
.get<ModelExtension>(ExtensionType.Model)
|
||||
?.cancelModelDownload(item.modelId)
|
||||
const model = downloadingModels.find(
|
||||
(model) => model.id === item.modelId
|
||||
)
|
||||
if (model) abortModelDownload(model)
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { ModelExtension, ExtensionType } from '@janhq/core'
|
||||
import { Model } from '@janhq/core'
|
||||
|
||||
import {
|
||||
@ -17,11 +16,12 @@ import {
|
||||
|
||||
import { atom, useAtomValue } from 'jotai'
|
||||
|
||||
import useDownloadModel from '@/hooks/useDownloadModel'
|
||||
import { useDownloadState } from '@/hooks/useDownloadState'
|
||||
|
||||
import { formatDownloadPercentage } from '@/utils/converter'
|
||||
|
||||
import { extensionManager } from '@/extension'
|
||||
import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
|
||||
|
||||
type Props = {
|
||||
model: Model
|
||||
@ -30,6 +30,7 @@ type Props = {
|
||||
|
||||
export default function ModalCancelDownload({ model, isFromList }: Props) {
|
||||
const { modelDownloadStateAtom } = useDownloadState()
|
||||
const downloadingModels = useAtomValue(downloadingModelsAtom)
|
||||
const downloadAtom = useMemo(
|
||||
() => atom((get) => get(modelDownloadStateAtom)[model.id]),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@ -37,6 +38,7 @@ export default function ModalCancelDownload({ model, isFromList }: Props) {
|
||||
)
|
||||
const downloadState = useAtomValue(downloadAtom)
|
||||
const cancelText = `Cancel ${formatDownloadPercentage(downloadState.percent)}`
|
||||
const { abortModelDownload } = useDownloadModel()
|
||||
|
||||
return (
|
||||
<Modal>
|
||||
@ -80,9 +82,10 @@ export default function ModalCancelDownload({ model, isFromList }: Props) {
|
||||
themes="danger"
|
||||
onClick={() => {
|
||||
if (downloadState?.modelId) {
|
||||
extensionManager
|
||||
.get<ModelExtension>(ExtensionType.Model)
|
||||
?.cancelModelDownload(downloadState.modelId)
|
||||
const model = downloadingModels.find(
|
||||
(model) => model.id === downloadState.modelId
|
||||
)
|
||||
if (model) abortModelDownload(model)
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@ -1,34 +1,35 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import { PropsWithChildren, useEffect, useRef } from 'react'
|
||||
import { basename } from 'path'
|
||||
|
||||
import { ExtensionType } from '@janhq/core'
|
||||
import { ModelExtension } from '@janhq/core'
|
||||
import { PropsWithChildren, useEffect, useRef } from 'react'
|
||||
|
||||
import { useAtomValue, useSetAtom } from 'jotai'
|
||||
|
||||
import { useDownloadState } from '@/hooks/useDownloadState'
|
||||
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
|
||||
|
||||
import { modelBinFileName } from '@/utils/model'
|
||||
|
||||
import EventHandler from './EventHandler'
|
||||
|
||||
import { appDownloadProgress } from './Jotai'
|
||||
|
||||
import { extensionManager } from '@/extension/ExtensionManager'
|
||||
import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
|
||||
|
||||
export default function EventListenerWrapper({ children }: PropsWithChildren) {
|
||||
const setProgress = useSetAtom(appDownloadProgress)
|
||||
const models = useAtomValue(downloadingModelsAtom)
|
||||
const modelsRef = useRef(models)
|
||||
useEffect(() => {
|
||||
modelsRef.current = models
|
||||
}, [models])
|
||||
|
||||
const { setDownloadedModels, downloadedModels } = useGetDownloadedModels()
|
||||
const { setDownloadState, setDownloadStateSuccess, setDownloadStateFailed } =
|
||||
useDownloadState()
|
||||
const downloadedModelRef = useRef(downloadedModels)
|
||||
|
||||
useEffect(() => {
|
||||
modelsRef.current = models
|
||||
}, [models])
|
||||
useEffect(() => {
|
||||
downloadedModelRef.current = downloadedModels
|
||||
}, [downloadedModels])
|
||||
@ -38,40 +39,36 @@ export default function EventListenerWrapper({ children }: PropsWithChildren) {
|
||||
window.electronAPI.onFileDownloadUpdate(
|
||||
(_event: string, state: any | undefined) => {
|
||||
if (!state) return
|
||||
const model = modelsRef.current.find(
|
||||
(model) => modelBinFileName(model) === basename(state.fileName)
|
||||
)
|
||||
if (model)
|
||||
setDownloadState({
|
||||
...state,
|
||||
modelId: state.fileName.split('/').pop() ?? '',
|
||||
modelId: model.id,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
window.electronAPI.onFileDownloadError(
|
||||
(_event: string, callback: any) => {
|
||||
console.error('Download error', callback)
|
||||
const modelId = callback.fileName.split('/').pop() ?? ''
|
||||
setDownloadStateFailed(modelId)
|
||||
}
|
||||
window.electronAPI.onFileDownloadError((_event: string, state: any) => {
|
||||
console.error('Download error', state)
|
||||
const model = modelsRef.current.find(
|
||||
(model) => modelBinFileName(model) === basename(state.fileName)
|
||||
)
|
||||
if (model) setDownloadStateFailed(model.id)
|
||||
})
|
||||
|
||||
window.electronAPI.onFileDownloadSuccess(
|
||||
(_event: string, callback: any) => {
|
||||
if (callback && callback.fileName) {
|
||||
const modelId = callback.fileName.split('/').pop() ?? ''
|
||||
|
||||
const model = modelsRef.current.find((e) => e.id === modelId)
|
||||
|
||||
setDownloadStateSuccess(modelId)
|
||||
|
||||
if (model)
|
||||
extensionManager
|
||||
.get<ModelExtension>(ExtensionType.Model)
|
||||
?.saveModel(model)
|
||||
.then(() => {
|
||||
window.electronAPI.onFileDownloadSuccess((_event: string, state: any) => {
|
||||
if (state && state.fileName) {
|
||||
const model = modelsRef.current.find(
|
||||
(model) => modelBinFileName(model) === basename(state.fileName)
|
||||
)
|
||||
if (model) {
|
||||
setDownloadStateSuccess(model.id)
|
||||
setDownloadedModels([...downloadedModelRef.current, model])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
window.electronAPI.onAppUpdateDownloadUpdate(
|
||||
(_event: string, progress: any) => {
|
||||
|
||||
@ -81,7 +81,10 @@ export class ExtensionManager {
|
||||
*/
|
||||
async activateExtension(extension: Extension) {
|
||||
// Import class
|
||||
await import(/* webpackIgnore: true */ extension.url).then(
|
||||
const extensionUrl = window.electronAPI
|
||||
? extension.url
|
||||
: extension.url.replace('extension://', `${API_BASE_URL}/extensions/`)
|
||||
await import(/* webpackIgnore: true */ extensionUrl).then(
|
||||
(extensionClass) => {
|
||||
// Register class if it has a default export
|
||||
if (
|
||||
|
||||
@ -1,7 +1,15 @@
|
||||
import { Model, ExtensionType, ModelExtension } from '@janhq/core'
|
||||
import {
|
||||
Model,
|
||||
ExtensionType,
|
||||
ModelExtension,
|
||||
abortDownload,
|
||||
joinPath,
|
||||
} from '@janhq/core'
|
||||
|
||||
import { useSetAtom } from 'jotai'
|
||||
|
||||
import { modelBinFileName } from '@/utils/model'
|
||||
|
||||
import { useDownloadState } from './useDownloadState'
|
||||
|
||||
import { extensionManager } from '@/extension/ExtensionManager'
|
||||
@ -33,8 +41,14 @@ export default function useDownloadModel() {
|
||||
.get<ModelExtension>(ExtensionType.Model)
|
||||
?.downloadModel(model)
|
||||
}
|
||||
const abortModelDownload = async (model: Model) => {
|
||||
await abortDownload(
|
||||
await joinPath(['models', model.id, modelBinFileName(model)])
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
downloadModel,
|
||||
abortModelDownload,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
import { join } from 'path'
|
||||
|
||||
import { fs } from '@janhq/core'
|
||||
import { fs, joinPath } from '@janhq/core'
|
||||
|
||||
export const useEngineSettings = () => {
|
||||
const readOpenAISettings = async () => {
|
||||
const settings = await fs.readFile(join('engines', 'openai.json'))
|
||||
if (!fs.existsSync(await joinPath(['file://engines', 'openai.json'])))
|
||||
return {}
|
||||
const settings = await fs.readFileSync(
|
||||
await joinPath(['file://engines', 'openai.json']),
|
||||
'utf-8'
|
||||
)
|
||||
if (settings) {
|
||||
return JSON.parse(settings)
|
||||
return typeof settings === 'object' ? settings : JSON.parse(settings)
|
||||
}
|
||||
return {}
|
||||
}
|
||||
@ -17,7 +20,10 @@ export const useEngineSettings = () => {
|
||||
}) => {
|
||||
const settings = await readOpenAISettings()
|
||||
settings.api_key = apiKey
|
||||
await fs.writeFile(join('engines', 'openai.json'), JSON.stringify(settings))
|
||||
await fs.writeFileSync(
|
||||
await joinPath(['file://engines', 'openai.json']),
|
||||
JSON.stringify(settings)
|
||||
)
|
||||
}
|
||||
return { readOpenAISettings, saveOpenAISettings }
|
||||
}
|
||||
|
||||
@ -13,7 +13,6 @@ import {
|
||||
events,
|
||||
Model,
|
||||
ConversationalExtension,
|
||||
ModelRuntimeParams,
|
||||
} from '@janhq/core'
|
||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||
|
||||
@ -173,7 +172,7 @@ export default function useSendChatMessage() {
|
||||
updateThreadInitSuccess(activeThread.id)
|
||||
updateThread(updatedThread)
|
||||
|
||||
extensionManager
|
||||
await extensionManager
|
||||
.get<ConversationalExtension>(ExtensionType.Conversational)
|
||||
?.saveThread(updatedThread)
|
||||
}
|
||||
|
||||
@ -32,6 +32,10 @@ const nextConfig = {
|
||||
JSON.stringify(process.env.ANALYTICS_ID) ?? JSON.stringify('xxx'),
|
||||
ANALYTICS_HOST:
|
||||
JSON.stringify(process.env.ANALYTICS_HOST) ?? JSON.stringify('xxx'),
|
||||
API_BASE_URL: JSON.stringify('http://localhost:1337'),
|
||||
isMac: process.platform === 'darwin',
|
||||
isWindows: process.platform === 'win32',
|
||||
isLinux: process.platform === 'linux',
|
||||
}),
|
||||
]
|
||||
return config
|
||||
|
||||
@ -116,7 +116,7 @@ const ChatBody: React.FC = () => {
|
||||
) : (
|
||||
<ScrollToBottom className="flex h-full w-full flex-col">
|
||||
{messages.map((message, index) => (
|
||||
<>
|
||||
<div key={message.id}>
|
||||
<ChatItem {...message} key={message.id} />
|
||||
|
||||
{message.status === MessageStatus.Error &&
|
||||
@ -126,8 +126,8 @@ const ChatBody: React.FC = () => {
|
||||
className="mt-10 flex flex-col items-center"
|
||||
>
|
||||
<span className="mb-3 text-center text-sm font-medium text-gray-500">
|
||||
Oops! The generation was interrupted. Let's
|
||||
give it another go!
|
||||
Oops! The generation was interrupted. Let's give it
|
||||
another go!
|
||||
</span>
|
||||
<Button
|
||||
className="w-min"
|
||||
@ -140,7 +140,7 @@ const ChatBody: React.FC = () => {
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
))}
|
||||
</ScrollToBottom>
|
||||
)}
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
import { join } from 'path'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
import { getUserSpace, openFileExplorer } from '@janhq/core'
|
||||
import { getUserSpace, openFileExplorer, joinPath } from '@janhq/core'
|
||||
|
||||
import { Input, Textarea } from '@janhq/uikit'
|
||||
|
||||
@ -53,24 +51,24 @@ const Sidebar: React.FC = () => {
|
||||
let filePath = undefined
|
||||
const assistantId = activeThread.assistants[0]?.assistant_id
|
||||
switch (type) {
|
||||
case 'Engine':
|
||||
case 'Thread':
|
||||
filePath = join('threads', activeThread.id)
|
||||
filePath = await joinPath(['threads', activeThread.id])
|
||||
break
|
||||
case 'Model':
|
||||
if (!selectedModel) return
|
||||
filePath = join('models', selectedModel.id)
|
||||
filePath = await joinPath(['models', selectedModel.id])
|
||||
break
|
||||
case 'Assistant':
|
||||
if (!assistantId) return
|
||||
filePath = join('assistants', assistantId)
|
||||
filePath = await joinPath(['assistants', assistantId])
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if (!filePath) return
|
||||
|
||||
const fullPath = join(userSpace, filePath)
|
||||
const fullPath = await joinPath([userSpace, filePath])
|
||||
openFileExplorer(fullPath)
|
||||
}
|
||||
|
||||
@ -86,24 +84,24 @@ const Sidebar: React.FC = () => {
|
||||
let filePath = undefined
|
||||
const assistantId = activeThread.assistants[0]?.assistant_id
|
||||
switch (type) {
|
||||
case 'Engine':
|
||||
case 'Thread':
|
||||
filePath = join('threads', activeThread.id, 'thread.json')
|
||||
filePath = await joinPath(['threads', activeThread.id, 'thread.json'])
|
||||
break
|
||||
case 'Model':
|
||||
if (!selectedModel) return
|
||||
filePath = join('models', selectedModel.id, 'model.json')
|
||||
filePath = await joinPath(['models', selectedModel.id, 'model.json'])
|
||||
break
|
||||
case 'Assistant':
|
||||
if (!assistantId) return
|
||||
filePath = join('assistants', assistantId, 'assistant.json')
|
||||
filePath = await joinPath(['assistants', assistantId, 'assistant.json'])
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if (!filePath) return
|
||||
|
||||
const fullPath = join(userSpace, filePath)
|
||||
const fullPath = await joinPath([userSpace, filePath])
|
||||
openFileExplorer(fullPath)
|
||||
}
|
||||
|
||||
|
||||
@ -5,8 +5,6 @@ import React, { useState, useEffect, useRef, useContext } from 'react'
|
||||
|
||||
import { Button } from '@janhq/uikit'
|
||||
|
||||
import Loader from '@/containers/Loader'
|
||||
|
||||
import { FeatureToggleContext } from '@/context/FeatureToggle'
|
||||
|
||||
import { useGetAppVersion } from '@/hooks/useGetAppVersion'
|
||||
@ -18,7 +16,6 @@ import { extensionManager } from '@/extension'
|
||||
const ExtensionCatalog = () => {
|
||||
const [activeExtensions, setActiveExtensions] = useState<any[]>([])
|
||||
const [extensionCatalog, setExtensionCatalog] = useState<any[]>([])
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false)
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null)
|
||||
const { version } = useGetAppVersion()
|
||||
const { experimentalFeatureEnabed } = useContext(FeatureToggleContext)
|
||||
@ -95,8 +92,6 @@ const ExtensionCatalog = () => {
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoading) return <Loader description="Installing ..." />
|
||||
|
||||
return (
|
||||
<div className="block w-full">
|
||||
{extensionCatalog
|
||||
|
||||
@ -15,7 +15,7 @@ export default function Models() {
|
||||
const [searchValue, setsearchValue] = useState('')
|
||||
|
||||
const filteredDownloadedModels = downloadedModels.filter((x) => {
|
||||
return x.name.toLowerCase().includes(searchValue.toLowerCase())
|
||||
return x.name?.toLowerCase().includes(searchValue.toLowerCase())
|
||||
})
|
||||
|
||||
return (
|
||||
|
||||
@ -1,76 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { toast } from 'react-toastify'
|
||||
const API_BASE_PATH: string = '/api/v1'
|
||||
|
||||
export function openExternalUrl(url: string) {
|
||||
window?.open(url, '_blank')
|
||||
}
|
||||
|
||||
export async function appVersion() {
|
||||
return Promise.resolve(VERSION)
|
||||
}
|
||||
|
||||
export function invokeExtensionFunc(
|
||||
modulePath: string,
|
||||
extensionFunc: string,
|
||||
...args: any
|
||||
): Promise<any> {
|
||||
return fetchApi(modulePath, extensionFunc, args).catch((err: Error) => {
|
||||
throw err
|
||||
})
|
||||
}
|
||||
|
||||
export async function downloadFile(downloadUrl: string, fileName: string) {
|
||||
return fetchApi('', 'downloadFile', {
|
||||
downloadUrl: downloadUrl,
|
||||
fileName: fileName,
|
||||
}).catch((err: Error) => {
|
||||
throw err
|
||||
})
|
||||
}
|
||||
|
||||
export async function deleteFile(fileName: string) {
|
||||
return fetchApi('', 'deleteFile', fileName).catch((err: Error) => {
|
||||
throw err
|
||||
})
|
||||
}
|
||||
|
||||
export async function fetchApi(
|
||||
modulePath: string,
|
||||
extensionFunc: string,
|
||||
args: any
|
||||
): Promise<any> {
|
||||
const response = await fetch(API_BASE_PATH + '/invokeFunction', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
modulePath: modulePath,
|
||||
method: extensionFunc,
|
||||
args: args,
|
||||
}),
|
||||
headers: { contentType: 'application/json', Authorization: '' },
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const json = await response.json()
|
||||
if (json && json.error) {
|
||||
toast.error(json.error, {
|
||||
position: 'bottom-left',
|
||||
autoClose: 5000,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
progress: undefined,
|
||||
theme: 'light',
|
||||
})
|
||||
}
|
||||
return null
|
||||
}
|
||||
const text = await response.text()
|
||||
try {
|
||||
const json = JSON.parse(text)
|
||||
return Promise.resolve(json)
|
||||
} catch (err) {
|
||||
return Promise.resolve(text)
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import * as restAPI from './cloudNativeService'
|
||||
import { EventEmitter } from './eventsService'
|
||||
import { restAPI } from './restService'
|
||||
export const setupCoreServices = () => {
|
||||
if (typeof window === 'undefined') {
|
||||
console.debug('undefine', window)
|
||||
@ -10,9 +10,7 @@ export const setupCoreServices = () => {
|
||||
if (!window.core) {
|
||||
window.core = {
|
||||
events: new EventEmitter(),
|
||||
api: window.electronAPI ?? {
|
||||
...restAPI,
|
||||
},
|
||||
api: window.electronAPI ?? restAPI,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,13 +15,10 @@ export const isCoreExtensionInstalled = () => {
|
||||
return true
|
||||
}
|
||||
export const setupBaseExtensions = async () => {
|
||||
if (
|
||||
typeof window === 'undefined' ||
|
||||
typeof window.electronAPI === 'undefined'
|
||||
) {
|
||||
if (typeof window === 'undefined') {
|
||||
return
|
||||
}
|
||||
const baseExtensions = await window.electronAPI.baseExtensions()
|
||||
const baseExtensions = await window.core?.api.baseExtensions()
|
||||
|
||||
if (
|
||||
!extensionManager.get(ExtensionType.Conversational) ||
|
||||
|
||||
59
web/services/restService.ts
Normal file
59
web/services/restService.ts
Normal file
@ -0,0 +1,59 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import {
|
||||
AppRoute,
|
||||
DownloadRoute,
|
||||
ExtensionRoute,
|
||||
FileSystemRoute,
|
||||
} from '@janhq/core'
|
||||
|
||||
import { safeJsonParse } from '@/utils/json'
|
||||
|
||||
// Function to open an external URL in a new browser window
|
||||
export function openExternalUrl(url: string) {
|
||||
window?.open(url, '_blank')
|
||||
}
|
||||
|
||||
// Async function to get the application version
|
||||
export async function appVersion() {
|
||||
return Promise.resolve(VERSION)
|
||||
}
|
||||
|
||||
// Define API routes based on different route types
|
||||
export const APIRoutes = [
|
||||
...Object.values(AppRoute).map((r) => ({ path: 'app', route: r })),
|
||||
...Object.values(DownloadRoute).map((r) => ({ path: `download`, route: r })),
|
||||
...Object.values(ExtensionRoute).map((r) => ({
|
||||
path: `extension`,
|
||||
route: r,
|
||||
})),
|
||||
...Object.values(FileSystemRoute).map((r) => ({ path: `fs`, route: r })),
|
||||
]
|
||||
|
||||
// Define the restAPI object with methods for each API route
|
||||
export const restAPI = {
|
||||
...Object.values(APIRoutes).reduce((acc, proxy) => {
|
||||
return {
|
||||
...acc,
|
||||
[proxy.route]: (...args: any) => {
|
||||
// For each route, define a function that sends a request to the API
|
||||
return fetch(`${API_BASE_URL}/v1/${proxy.path}/${proxy.route}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(args),
|
||||
headers: { contentType: 'application/json' },
|
||||
}).then(async (res) => {
|
||||
try {
|
||||
if (proxy.path === 'fs') {
|
||||
const text = await res.text()
|
||||
return safeJsonParse(text) ?? text
|
||||
}
|
||||
return await res.json()
|
||||
} catch (err) {
|
||||
console.debug('Op: ', proxy, args, err)
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
}, {}),
|
||||
openExternalUrl,
|
||||
appVersion,
|
||||
}
|
||||
4
web/types/index.d.ts
vendored
4
web/types/index.d.ts
vendored
@ -8,6 +8,10 @@ declare global {
|
||||
declare const VERSION: string
|
||||
declare const ANALYTICS_ID: string
|
||||
declare const ANALYTICS_HOST: string
|
||||
declare const API_BASE_URL: string
|
||||
declare const isMac: boolean
|
||||
declare const isWindows: boolean
|
||||
declare const isLinux: boolean
|
||||
interface Core {
|
||||
api: APIFunctions
|
||||
events: EventEmitter
|
||||
|
||||
9
web/utils/json.ts
Normal file
9
web/utils/json.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export const safeJsonParse = <T>(str: string) => {
|
||||
try {
|
||||
const jsonValue: T = JSON.parse(str)
|
||||
|
||||
return jsonValue
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
12
web/utils/model.ts
Normal file
12
web/utils/model.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { basename } from 'path'
|
||||
|
||||
import { Model } from '@janhq/core'
|
||||
|
||||
export const modelBinFileName = (model: Model) => {
|
||||
const modelFormatExt = '.gguf'
|
||||
const extractedFileName = basename(model.source_url) ?? model.id
|
||||
const fileName = extractedFileName.toLowerCase().endsWith(modelFormatExt)
|
||||
? extractedFileName
|
||||
: model.id
|
||||
return fileName
|
||||
}
|
||||
@ -22,8 +22,7 @@ export const toRuntimeParams = (
|
||||
|
||||
for (const [key, value] of Object.entries(modelParams)) {
|
||||
if (key in defaultModelParams) {
|
||||
// @ts-ignore
|
||||
runtimeParams[key] = value
|
||||
runtimeParams[key as keyof ModelRuntimeParams] = value
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,8 +45,7 @@ export const toSettingParams = (
|
||||
|
||||
for (const [key, value] of Object.entries(modelParams)) {
|
||||
if (key in defaultSettingParams) {
|
||||
// @ts-ignore
|
||||
settingParams[key] = value
|
||||
settingParams[key as keyof ModelSettingParams] = value
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user