diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index f980b9df7..db1eed38d 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -1,4 +1,4 @@
{
- "name": "jan",
- "image": "node:20"
-}
\ No newline at end of file
+ "name": "jan",
+ "image": "node:20"
+}
diff --git a/.github/workflows/jan-electron-linter-and-test.yml b/.github/workflows/jan-electron-linter-and-test.yml
index 6d5aaf150..40085391f 100644
--- a/.github/workflows/jan-electron-linter-and-test.yml
+++ b/.github/workflows/jan-electron-linter-and-test.yml
@@ -1,5 +1,6 @@
name: Jan Electron Linter & Test
on:
+ workflow_dispatch:
push:
branches:
- main
diff --git a/core/.prettierignore b/.prettierignore
similarity index 100%
rename from core/.prettierignore
rename to .prettierignore
diff --git a/core/.prettierrc b/.prettierrc
similarity index 100%
rename from core/.prettierrc
rename to .prettierrc
diff --git a/Dockerfile b/Dockerfile
index 82c657604..913a93a11 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -32,7 +32,10 @@ COPY --from=builder /app/node_modules ./node_modules/
COPY --from=builder /app/yarn.lock ./yarn.lock
# Copy the package.json, yarn.lock, and build output of server yarn space to leverage Docker cache
+COPY --from=builder /app/core ./core/
COPY --from=builder /app/server ./server/
+RUN cd core && yarn install && yarn run build
+RUN yarn workspace @janhq/server install && yarn workspace @janhq/server build
COPY --from=builder /app/docs/openapi ./docs/openapi/
# Copy pre-install dependencies
diff --git a/Dockerfile.gpu b/Dockerfile.gpu
index f67990afd..d5ea70499 100644
--- a/Dockerfile.gpu
+++ b/Dockerfile.gpu
@@ -56,7 +56,10 @@ COPY --from=builder /app/node_modules ./node_modules/
COPY --from=builder /app/yarn.lock ./yarn.lock
# Copy the package.json, yarn.lock, and build output of server yarn space to leverage Docker cache
+COPY --from=builder /app/core ./core/
COPY --from=builder /app/server ./server/
+RUN cd core && yarn install && yarn run build
+RUN yarn workspace @janhq/server install && yarn workspace @janhq/server build
COPY --from=builder /app/docs/openapi ./docs/openapi/
# Copy pre-install dependencies
diff --git a/README.md b/README.md
index 5b5263ed1..f8ae4069c 100644
--- a/README.md
+++ b/README.md
@@ -76,31 +76,31 @@ Jan is an open-source ChatGPT alternative that runs 100% offline on your compute
| Experimental (Nightly Build) |
-
+
jan.exe
|
-
+
Intel
|
-
+
M1/M2
|
-
+
jan.deb
|
-
+
jan.AppImage
@@ -167,6 +167,7 @@ To reset your installation:
- Clear Application cache in `~/Library/Caches/jan`
## Requirements for running Jan
+
- MacOS: 13 or higher
- Windows:
- Windows 10 or higher
@@ -194,17 +195,17 @@ Contributions are welcome! Please read the [CONTRIBUTING.md](CONTRIBUTING.md) fi
1. **Clone the repository and prepare:**
- ```bash
- git clone https://github.com/janhq/jan
- cd jan
- git checkout -b DESIRED_BRANCH
- ```
+ ```bash
+ git clone https://github.com/janhq/jan
+ cd jan
+ git checkout -b DESIRED_BRANCH
+ ```
2. **Run development and use Jan Desktop**
- ```bash
- make dev
- ```
+ ```bash
+ make dev
+ ```
This will start the development server and open the desktop app.
@@ -222,14 +223,15 @@ This will build the app MacOS m1/m2 for production (with code signing already do
- Supported OS: Linux, WSL2 Docker
- Pre-requisites:
- - `docker` and `docker compose`, follow instruction [here](https://docs.docker.com/engine/install/ubuntu/)
+
+ - Docker Engine and Docker Compose are required to run Jan in Docker mode. Follow the [instructions](https://docs.docker.com/engine/install/ubuntu/) below to get started with Docker Engine on Ubuntu.
```bash
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh ./get-docker.sh --dry-run
```
- - `nvidia-driver` and `nvidia-docker2`, follow instruction [here](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) (If you want to run with GPU mode)
+ - If you intend to run Jan in GPU mode, you need to install `nvidia-driver` and `nvidia-docker2`. Follow the instruction [here](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) for installation.
- Run Jan in Docker mode
@@ -241,7 +243,7 @@ This will build the app MacOS m1/m2 for production (with code signing already do
- **Option 2**: Run Jan in GPU mode
- - **Step 1**: Check cuda compatibility with your nvidia driver by running `nvidia-smi` and check the cuda version in the output
+ - **Step 1**: Check CUDA compatibility with your NVIDIA driver by running `nvidia-smi` and check the CUDA version in the output
```bash
nvidia-smi
@@ -274,7 +276,7 @@ This will build the app MacOS m1/m2 for production (with code signing already do
|=======================================================================================|
```
- - **Step 2**: Go to https://catalog.ngc.nvidia.com/orgs/nvidia/containers/cuda/tags and find the smallest minor version of image tag that matches the cuda version from the output of `nvidia-smi` (e.g. 12.1 -> 12.1.0)
+ - **Step 2**: Visit [NVIDIA NGC Catalog ](https://catalog.ngc.nvidia.com/orgs/nvidia/containers/cuda/tags) and find the smallest minor version of image tag that matches your CUDA version (e.g., 12.1 -> 12.1.0)
- **Step 3**: Update the `Dockerfile.gpu` line number 5 with the latest minor version of the image tag from step 2 (e.g. change `FROM nvidia/cuda:12.2.0-runtime-ubuntu22.04 AS base` to `FROM nvidia/cuda:12.1.0-runtime-ubuntu22.04 AS base`)
@@ -286,6 +288,7 @@ This will build the app MacOS m1/m2 for production (with code signing already do
```
This will start the web server and you can access Jan at `http://localhost:3000`.
+
> Note: Currently, Docker mode is only work for development and localhost, production is not supported yet. RAG feature is not supported in Docker mode yet.
## Acknowledgements
diff --git a/core/jest.config.js b/core/jest.config.js
index fb03768fe..c18f55091 100644
--- a/core/jest.config.js
+++ b/core/jest.config.js
@@ -4,4 +4,4 @@ module.exports = {
moduleNameMapper: {
'@/(.*)': '/src/$1',
},
-}
\ No newline at end of file
+}
diff --git a/core/rollup.config.ts b/core/rollup.config.ts
index d78130a4d..ebea8e237 100644
--- a/core/rollup.config.ts
+++ b/core/rollup.config.ts
@@ -54,7 +54,8 @@ export default [
'url',
'http',
'os',
- 'util'
+ 'util',
+ 'child_process',
],
watch: {
include: 'src/node/**',
diff --git a/core/src/api/index.ts b/core/src/api/index.ts
index f4ec3cd7e..676020758 100644
--- a/core/src/api/index.ts
+++ b/core/src/api/index.ts
@@ -1,15 +1,22 @@
+/**
+ * Native Route APIs
+ * @description Enum of all the routes exposed by the app
+ */
+export enum NativeRoute {
+ openExternalUrl = 'openExternalUrl',
+ openAppDirectory = 'openAppDirectory',
+ openFileExplore = 'openFileExplorer',
+ selectDirectory = 'selectDirectory',
+ relaunch = 'relaunch',
+}
+
/**
* App Route APIs
* @description Enum of all the routes exposed by the app
*/
export enum AppRoute {
- openExternalUrl = 'openExternalUrl',
- openAppDirectory = 'openAppDirectory',
- openFileExplore = 'openFileExplorer',
- selectDirectory = 'selectDirectory',
getAppConfigurations = 'getAppConfigurations',
updateAppConfiguration = 'updateAppConfiguration',
- relaunch = 'relaunch',
joinPath = 'joinPath',
isSubdirectory = 'isSubdirectory',
baseName = 'baseName',
@@ -69,6 +76,10 @@ export enum FileManagerRoute {
export type ApiFunction = (...args: any[]) => any
+export type NativeRouteFunctions = {
+ [K in NativeRoute]: ApiFunction
+}
+
export type AppRouteFunctions = {
[K in AppRoute]: ApiFunction
}
@@ -97,7 +108,8 @@ export type FileManagerRouteFunctions = {
[K in FileManagerRoute]: ApiFunction
}
-export type APIFunctions = AppRouteFunctions &
+export type APIFunctions = NativeRouteFunctions &
+ AppRouteFunctions &
AppEventFunctions &
DownloadRouteFunctions &
DownloadEventFunctions &
@@ -105,11 +117,13 @@ export type APIFunctions = AppRouteFunctions &
FileSystemRouteFunctions &
FileManagerRoute
-export const APIRoutes = [
+export const CoreRoutes = [
...Object.values(AppRoute),
...Object.values(DownloadRoute),
...Object.values(ExtensionRoute),
...Object.values(FileSystemRoute),
...Object.values(FileManagerRoute),
]
+
+export const APIRoutes = [...CoreRoutes, ...Object.values(NativeRoute)]
export const APIEvents = [...Object.values(AppEvent), ...Object.values(DownloadEvent)]
diff --git a/core/src/extension.ts b/core/src/extension.ts
index 0b7f9b7fc..3528f581c 100644
--- a/core/src/extension.ts
+++ b/core/src/extension.ts
@@ -1,13 +1,13 @@
export enum ExtensionTypeEnum {
- Assistant = "assistant",
- Conversational = "conversational",
- Inference = "inference",
- Model = "model",
- SystemMonitoring = "systemMonitoring",
+ Assistant = 'assistant',
+ Conversational = 'conversational',
+ Inference = 'inference',
+ Model = 'model',
+ SystemMonitoring = 'systemMonitoring',
}
export interface ExtensionType {
- type(): ExtensionTypeEnum | undefined;
+ type(): ExtensionTypeEnum | undefined
}
/**
* Represents a base extension.
@@ -20,16 +20,16 @@ export abstract class BaseExtension implements ExtensionType {
* Undefined means its not extending any known extension by the application.
*/
type(): ExtensionTypeEnum | undefined {
- return undefined;
+ return undefined
}
/**
* Called when the extension is loaded.
* Any initialization logic for the extension should be put here.
*/
- abstract onLoad(): void;
+ abstract onLoad(): void
/**
* Called when the extension is unloaded.
* Any cleanup logic for the extension should be put here.
*/
- abstract onUnload(): void;
+ abstract onUnload(): void
}
diff --git a/core/src/extensions/assistant.ts b/core/src/extensions/assistant.ts
index ba345711a..5c3114f41 100644
--- a/core/src/extensions/assistant.ts
+++ b/core/src/extensions/assistant.ts
@@ -1,5 +1,5 @@
-import { Assistant, AssistantInterface } from "../index";
-import { BaseExtension, ExtensionTypeEnum } from "../extension";
+import { Assistant, AssistantInterface } from '../index'
+import { BaseExtension, ExtensionTypeEnum } from '../extension'
/**
* Assistant extension for managing assistants.
@@ -10,10 +10,10 @@ export abstract class AssistantExtension extends BaseExtension implements Assist
* Assistant extension type.
*/
type(): ExtensionTypeEnum | undefined {
- return ExtensionTypeEnum.Assistant;
+ return ExtensionTypeEnum.Assistant
}
- abstract createAssistant(assistant: Assistant): Promise;
- abstract deleteAssistant(assistant: Assistant): Promise;
- abstract getAssistants(): Promise;
+ abstract createAssistant(assistant: Assistant): Promise
+ abstract deleteAssistant(assistant: Assistant): Promise
+ abstract getAssistants(): Promise
}
diff --git a/core/src/extensions/conversational.ts b/core/src/extensions/conversational.ts
index 4319784c3..a49a4e689 100644
--- a/core/src/extensions/conversational.ts
+++ b/core/src/extensions/conversational.ts
@@ -14,7 +14,7 @@ export abstract class ConversationalExtension
* Conversation extension type.
*/
type(): ExtensionTypeEnum | undefined {
- return ExtensionTypeEnum.Conversational;
+ return ExtensionTypeEnum.Conversational
}
abstract getThreads(): Promise
diff --git a/core/src/extensions/index.ts b/core/src/extensions/index.ts
index 1796c1618..522334548 100644
--- a/core/src/extensions/index.ts
+++ b/core/src/extensions/index.ts
@@ -2,24 +2,24 @@
* Conversational extension. Persists and retrieves conversations.
* @module
*/
-export { ConversationalExtension } from "./conversational";
+export { ConversationalExtension } from './conversational'
/**
* Inference extension. Start, stop and inference models.
*/
-export { InferenceExtension } from "./inference";
+export { InferenceExtension } from './inference'
/**
* Monitoring extension for system monitoring.
*/
-export { MonitoringExtension } from "./monitoring";
+export { MonitoringExtension } from './monitoring'
/**
* Assistant extension for managing assistants.
*/
-export { AssistantExtension } from "./assistant";
+export { AssistantExtension } from './assistant'
/**
* Model extension for managing models.
*/
-export { ModelExtension } from "./model";
+export { ModelExtension } from './model'
diff --git a/core/src/extensions/inference.ts b/core/src/extensions/inference.ts
index c551d108f..e8e51f9eb 100644
--- a/core/src/extensions/inference.ts
+++ b/core/src/extensions/inference.ts
@@ -1,5 +1,5 @@
-import { InferenceInterface, MessageRequest, ThreadMessage } from "../index";
-import { BaseExtension, ExtensionTypeEnum } from "../extension";
+import { InferenceInterface, MessageRequest, ThreadMessage } from '../index'
+import { BaseExtension, ExtensionTypeEnum } from '../extension'
/**
* Inference extension. Start, stop and inference models.
@@ -9,8 +9,8 @@ export abstract class InferenceExtension extends BaseExtension implements Infere
* Inference extension type.
*/
type(): ExtensionTypeEnum | undefined {
- return ExtensionTypeEnum.Inference;
+ return ExtensionTypeEnum.Inference
}
- abstract inference(data: MessageRequest): Promise;
+ abstract inference(data: MessageRequest): Promise
}
diff --git a/core/src/extensions/model.ts b/core/src/extensions/model.ts
index 30aa5b6ba..df7d14f42 100644
--- a/core/src/extensions/model.ts
+++ b/core/src/extensions/model.ts
@@ -1,5 +1,5 @@
-import { BaseExtension, ExtensionTypeEnum } from "../extension";
-import { Model, ModelInterface } from "../index";
+import { BaseExtension, ExtensionTypeEnum } from '../extension'
+import { Model, ModelInterface } from '../index'
/**
* Model extension for managing models.
@@ -9,16 +9,16 @@ export abstract class ModelExtension extends BaseExtension implements ModelInter
* Model extension type.
*/
type(): ExtensionTypeEnum | undefined {
- return ExtensionTypeEnum.Model;
+ return ExtensionTypeEnum.Model
}
abstract downloadModel(
model: Model,
- network?: { proxy: string; ignoreSSL?: boolean },
- ): Promise;
- abstract cancelModelDownload(modelId: string): Promise;
- abstract deleteModel(modelId: string): Promise;
- abstract saveModel(model: Model): Promise;
- abstract getDownloadedModels(): Promise;
- abstract getConfiguredModels(): Promise;
+ network?: { proxy: string; ignoreSSL?: boolean }
+ ): Promise
+ abstract cancelModelDownload(modelId: string): Promise
+ abstract deleteModel(modelId: string): Promise
+ abstract saveModel(model: Model): Promise
+ abstract getDownloadedModels(): Promise
+ abstract getConfiguredModels(): Promise
}
diff --git a/core/src/extensions/monitoring.ts b/core/src/extensions/monitoring.ts
index 2de9b9ae5..ba193f0f4 100644
--- a/core/src/extensions/monitoring.ts
+++ b/core/src/extensions/monitoring.ts
@@ -1,5 +1,5 @@
-import { BaseExtension, ExtensionTypeEnum } from "../extension";
-import { MonitoringInterface } from "../index";
+import { BaseExtension, ExtensionTypeEnum } from '../extension'
+import { MonitoringInterface } from '../index'
/**
* Monitoring extension for system monitoring.
@@ -10,9 +10,9 @@ export abstract class MonitoringExtension extends BaseExtension implements Monit
* Monitoring extension type.
*/
type(): ExtensionTypeEnum | undefined {
- return ExtensionTypeEnum.SystemMonitoring;
+ return ExtensionTypeEnum.SystemMonitoring
}
- abstract getResourcesInfo(): Promise;
- abstract getCurrentLoad(): Promise;
+ abstract getResourcesInfo(): Promise
+ abstract getCurrentLoad(): Promise
}
diff --git a/core/src/index.ts b/core/src/index.ts
index a56b6f0e1..3505797b1 100644
--- a/core/src/index.ts
+++ b/core/src/index.ts
@@ -38,3 +38,10 @@ export * from './extension'
* @module
*/
export * from './extensions/index'
+
+/**
+ * Declare global object
+ */
+declare global {
+ var core: any | undefined
+}
diff --git a/core/src/node/api/common/adapter.ts b/core/src/node/api/common/adapter.ts
new file mode 100644
index 000000000..56f4cedb3
--- /dev/null
+++ b/core/src/node/api/common/adapter.ts
@@ -0,0 +1,43 @@
+import {
+ AppRoute,
+ DownloadRoute,
+ ExtensionRoute,
+ FileManagerRoute,
+ FileSystemRoute,
+} from '../../../api'
+import { Downloader } from '../processors/download'
+import { FileSystem } from '../processors/fs'
+import { Extension } from '../processors/extension'
+import { FSExt } from '../processors/fsExt'
+import { App } from '../processors/app'
+
+export class RequestAdapter {
+ downloader: Downloader
+ fileSystem: FileSystem
+ extension: Extension
+ fsExt: FSExt
+ app: App
+
+ constructor(observer?: Function) {
+ this.downloader = new Downloader(observer)
+ this.fileSystem = new FileSystem()
+ this.extension = new Extension()
+ this.fsExt = new FSExt()
+ this.app = new App()
+ }
+
+ // TODO: Clearer Factory pattern here
+ process(route: string, ...args: any) {
+ if (route in DownloadRoute) {
+ return this.downloader.process(route, ...args)
+ } else if (route in FileSystemRoute) {
+ return this.fileSystem.process(route, ...args)
+ } else if (route in ExtensionRoute) {
+ return this.extension.process(route, ...args)
+ } else if (route in FileManagerRoute) {
+ return this.fsExt.process(route, ...args)
+ } else if (route in AppRoute) {
+ return this.app.process(route, ...args)
+ }
+ }
+}
diff --git a/core/src/node/api/common/handler.ts b/core/src/node/api/common/handler.ts
new file mode 100644
index 000000000..4a39ae52a
--- /dev/null
+++ b/core/src/node/api/common/handler.ts
@@ -0,0 +1,23 @@
+import { CoreRoutes } from '../../../api'
+import { RequestAdapter } from './adapter'
+
+export type Handler = (route: string, args: any) => any
+
+export class RequestHandler {
+ handler: Handler
+ adataper: RequestAdapter
+
+ constructor(handler: Handler, observer?: Function) {
+ this.handler = handler
+ this.adataper = new RequestAdapter(observer)
+ }
+
+ handle() {
+ CoreRoutes.map((route) => {
+ this.handler(route, async (...args: any[]) => {
+ const values = await this.adataper.process(route, ...args)
+ return values
+ })
+ })
+ }
+}
diff --git a/core/src/node/api/index.ts b/core/src/node/api/index.ts
index 4c3041ba3..ab0c51656 100644
--- a/core/src/node/api/index.ts
+++ b/core/src/node/api/index.ts
@@ -1,2 +1,3 @@
export * from './HttpServer'
-export * from './routes'
+export * from './restful/v1'
+export * from './common/handler'
diff --git a/core/src/node/api/processors/Processor.ts b/core/src/node/api/processors/Processor.ts
new file mode 100644
index 000000000..8ef0c6e19
--- /dev/null
+++ b/core/src/node/api/processors/Processor.ts
@@ -0,0 +1,3 @@
+export abstract class Processor {
+ abstract process(key: string, ...args: any[]): any
+}
diff --git a/core/src/node/api/processors/app.ts b/core/src/node/api/processors/app.ts
new file mode 100644
index 000000000..a4b1a5a06
--- /dev/null
+++ b/core/src/node/api/processors/app.ts
@@ -0,0 +1,97 @@
+import { basename, isAbsolute, join, relative } from 'path'
+
+import { AppRoute } from '../../../api'
+import { Processor } from './Processor'
+import { getAppConfigurations as appConfiguration, updateAppConfiguration } from '../../helper'
+import { log as writeLog, logServer as writeServerLog } from '../../helper/log'
+import { appResourcePath } from '../../helper/path'
+
+export class App implements Processor {
+ observer?: Function
+
+ constructor(observer?: Function) {
+ this.observer = observer
+ }
+
+ process(key: string, ...args: any[]): any {
+ const instance = this as any
+ const func = instance[key]
+ return func(...args)
+ }
+
+ /**
+ * Joins multiple paths together, respect to the current OS.
+ */
+ joinPath(args: any[]) {
+ return join(...args)
+ }
+
+ /**
+ * Checks if the given path is a subdirectory of the given directory.
+ *
+ * @param _event - The IPC event object.
+ * @param from - The path to check.
+ * @param to - The directory to check against.
+ *
+ * @returns {Promise} - A promise that resolves with the result.
+ */
+ isSubdirectory(from: any, to: any) {
+ const rel = relative(from, to)
+ const isSubdir = rel && !rel.startsWith('..') && !isAbsolute(rel)
+
+ if (isSubdir === '') return false
+ else return isSubdir
+ }
+
+ /**
+ * Retrieve basename from given path, respect to the current OS.
+ */
+ baseName(args: any) {
+ return basename(args)
+ }
+
+ /**
+ * Log message to log file.
+ */
+ log(args: any) {
+ writeLog(args)
+ }
+
+ /**
+ * Log message to log file.
+ */
+ logServer(args: any) {
+ writeServerLog(args)
+ }
+
+ getAppConfigurations() {
+ return appConfiguration()
+ }
+
+ async updateAppConfiguration(args: any) {
+ await updateAppConfiguration(args)
+ }
+
+ /**
+ * Start Jan API Server.
+ */
+ async startServer(args?: any) {
+ const { startServer } = require('@janhq/server')
+ return startServer({
+ host: args?.host,
+ port: args?.port,
+ isCorsEnabled: args?.isCorsEnabled,
+ isVerboseEnabled: args?.isVerboseEnabled,
+ schemaPath: join(await appResourcePath(), 'docs', 'openapi', 'jan.yaml'),
+ baseDir: join(await appResourcePath(), 'docs', 'openapi'),
+ })
+ }
+
+ /**
+ * Stop Jan API Server.
+ */
+ stopServer() {
+ const { stopServer } = require('@janhq/server')
+ return stopServer()
+ }
+}
diff --git a/core/src/node/api/processors/download.ts b/core/src/node/api/processors/download.ts
new file mode 100644
index 000000000..686ba58a1
--- /dev/null
+++ b/core/src/node/api/processors/download.ts
@@ -0,0 +1,106 @@
+import { resolve, sep } from 'path'
+import { DownloadEvent } from '../../../api'
+import { normalizeFilePath } from '../../helper/path'
+import { getJanDataFolderPath } from '../../helper'
+import { DownloadManager } from '../../helper/download'
+import { createWriteStream, renameSync } from 'fs'
+import { Processor } from './Processor'
+import { DownloadState } from '../../../types'
+
+export class Downloader implements Processor {
+ observer?: Function
+
+ constructor(observer?: Function) {
+ this.observer = observer
+ }
+
+ process(key: string, ...args: any[]): any {
+ const instance = this as any
+ const func = instance[key]
+ return func(this.observer, ...args)
+ }
+
+ downloadFile(observer: any, url: string, localPath: string, network: any) {
+ const request = require('request')
+ const progress = require('request-progress')
+
+ const strictSSL = !network?.ignoreSSL
+ const proxy = network?.proxy?.startsWith('http') ? network.proxy : undefined
+ if (typeof localPath === 'string') {
+ localPath = normalizeFilePath(localPath)
+ }
+ const array = localPath.split(sep)
+ const fileName = array.pop() ?? ''
+ const modelId = array.pop() ?? ''
+
+ const destination = resolve(getJanDataFolderPath(), localPath)
+ const rq = request({ url, strictSSL, proxy })
+
+ // Put request to download manager instance
+ DownloadManager.instance.setRequest(localPath, rq)
+
+ // Downloading file to a temp file first
+ const downloadingTempFile = `${destination}.download`
+
+ progress(rq, {})
+ .on('progress', (state: any) => {
+ const downloadState: DownloadState = {
+ ...state,
+ modelId,
+ fileName,
+ downloadState: 'downloading',
+ }
+ console.log('progress: ', downloadState)
+ observer?.(DownloadEvent.onFileDownloadUpdate, downloadState)
+ DownloadManager.instance.downloadProgressMap[modelId] = downloadState
+ })
+ .on('error', (error: Error) => {
+ const currentDownloadState = DownloadManager.instance.downloadProgressMap[modelId]
+ const downloadState: DownloadState = {
+ ...currentDownloadState,
+ error: error.message,
+ downloadState: 'error',
+ }
+ if (currentDownloadState) {
+ DownloadManager.instance.downloadProgressMap[modelId] = downloadState
+ }
+
+ observer?.(DownloadEvent.onFileDownloadError, downloadState)
+ })
+ .on('end', () => {
+ const currentDownloadState = DownloadManager.instance.downloadProgressMap[modelId]
+ if (currentDownloadState && DownloadManager.instance.networkRequests[localPath]) {
+ // Finished downloading, rename temp file to actual file
+ renameSync(downloadingTempFile, destination)
+ const downloadState: DownloadState = {
+ ...currentDownloadState,
+ downloadState: 'end',
+ }
+ observer?.(DownloadEvent.onFileDownloadSuccess, downloadState)
+ DownloadManager.instance.downloadProgressMap[modelId] = downloadState
+ }
+ })
+ .pipe(createWriteStream(downloadingTempFile))
+ }
+
+ abortDownload(observer: any, fileName: string) {
+ const rq = DownloadManager.instance.networkRequests[fileName]
+ if (rq) {
+ DownloadManager.instance.networkRequests[fileName] = undefined
+ rq?.abort()
+ } else {
+ observer?.(DownloadEvent.onFileDownloadError, {
+ fileName,
+ error: 'aborted',
+ })
+ }
+ }
+
+ resumeDownload(observer: any, fileName: any) {
+ DownloadManager.instance.networkRequests[fileName]?.resume()
+ }
+
+ pauseDownload(observer: any, fileName: any) {
+ DownloadManager.instance.networkRequests[fileName]?.pause()
+ }
+}
diff --git a/core/src/node/api/processors/extension.ts b/core/src/node/api/processors/extension.ts
new file mode 100644
index 000000000..df5d2d945
--- /dev/null
+++ b/core/src/node/api/processors/extension.ts
@@ -0,0 +1,88 @@
+import { readdirSync } from 'fs'
+import { join, extname } from 'path'
+
+import { Processor } from './Processor'
+import { ModuleManager } from '../../helper/module'
+import { getJanExtensionsPath as getPath } from '../../helper'
+import {
+ getActiveExtensions as getExtensions,
+ getExtension,
+ removeExtension,
+ installExtensions,
+} from '../../extension/store'
+import { appResourcePath } from '../../helper/path'
+
+export class Extension implements Processor {
+ observer?: Function
+
+ constructor(observer?: Function) {
+ this.observer = observer
+ }
+
+ process(key: string, ...args: any[]): any {
+ const instance = this as any
+ const func = instance[key]
+ return func(...args)
+ }
+
+ invokeExtensionFunc(modulePath: string, method: string, ...params: any[]) {
+ const module = require(join(getPath(), modulePath))
+ ModuleManager.instance.setModule(modulePath, module)
+
+ if (typeof module[method] === 'function') {
+ return module[method](...params)
+ } else {
+ console.debug(module[method])
+ console.error(`Function "${method}" does not exist in the module.`)
+ }
+ }
+
+ /**
+ * Returns the paths of the base extensions.
+ * @returns An array of paths to the base extensions.
+ */
+ async baseExtensions() {
+ const baseExtensionPath = join(await appResourcePath(), 'pre-install')
+ return readdirSync(baseExtensionPath)
+ .filter((file) => extname(file) === '.tgz')
+ .map((file) => join(baseExtensionPath, file))
+ }
+
+ /**MARK: Extension Manager handlers */
+ async installExtension(extensions: any) {
+ // Install and activate all provided extensions
+ const installed = await installExtensions(extensions)
+ return JSON.parse(JSON.stringify(installed))
+ }
+
+ // Register IPC route to uninstall a extension
+ async uninstallExtension(extensions: any) {
+ // Uninstall all provided extensions
+ for (const ext of extensions) {
+ const extension = getExtension(ext)
+ await extension.uninstall()
+ if (extension.name) removeExtension(extension.name)
+ }
+
+ // Reload all renderer pages if needed
+ return true
+ }
+
+ // Register IPC route to update a extension
+ async updateExtension(extensions: any) {
+ // Update all provided extensions
+ const updated: any[] = []
+ for (const ext of extensions) {
+ const extension = getExtension(ext)
+ const res = await extension.update()
+ if (res) updated.push(extension)
+ }
+
+ // Reload all renderer pages if needed
+ return JSON.parse(JSON.stringify(updated))
+ }
+
+ getActiveExtensions() {
+ return JSON.parse(JSON.stringify(getExtensions()))
+ }
+}
diff --git a/core/src/node/api/processors/fs.ts b/core/src/node/api/processors/fs.ts
new file mode 100644
index 000000000..93a5f1905
--- /dev/null
+++ b/core/src/node/api/processors/fs.ts
@@ -0,0 +1,25 @@
+import { join } from 'path'
+import { normalizeFilePath } from '../../helper/path'
+import { getJanDataFolderPath } from '../../helper'
+import { Processor } from './Processor'
+
+export class FileSystem implements Processor {
+ observer?: Function
+ private static moduleName = 'fs'
+
+ constructor(observer?: Function) {
+ this.observer = observer
+ }
+
+ process(route: string, ...args: any[]): any {
+ return import(FileSystem.moduleName).then((mdl) =>
+ mdl[route](
+ ...args.map((arg: any) =>
+ typeof arg === 'string' && (arg.startsWith(`file:/`) || arg.startsWith(`file:\\`))
+ ? join(getJanDataFolderPath(), normalizeFilePath(arg))
+ : arg
+ )
+ )
+ )
+ }
+}
diff --git a/core/src/node/api/processors/fsExt.ts b/core/src/node/api/processors/fsExt.ts
new file mode 100644
index 000000000..71e07ae57
--- /dev/null
+++ b/core/src/node/api/processors/fsExt.ts
@@ -0,0 +1,78 @@
+import { join } from 'path'
+import fs from 'fs'
+import { FileManagerRoute } from '../../../api'
+import { appResourcePath, normalizeFilePath } from '../../helper/path'
+import { getJanDataFolderPath, getJanDataFolderPath as getPath } from '../../helper'
+import { Processor } from './Processor'
+import { FileStat } from '../../../types'
+
+export class FSExt implements Processor {
+ observer?: Function
+
+ constructor(observer?: Function) {
+ this.observer = observer
+ }
+
+ process(key: string, ...args: any): any {
+ const instance = this as any
+ const func = instance[key]
+ return func(...args)
+ }
+
+ // Handles the 'syncFile' IPC event. This event is triggered to synchronize a file from a source path to a destination path.
+ syncFile(src: string, dest: string) {
+ const reflect = require('@alumna/reflect')
+ return reflect({
+ src,
+ dest,
+ recursive: true,
+ delete: false,
+ overwrite: true,
+ errorOnExist: false,
+ })
+ }
+
+ // Handles the 'getJanDataFolderPath' IPC event. This event is triggered to get the user space path.
+ getJanDataFolderPath() {
+ return Promise.resolve(getPath())
+ }
+
+ // Handles the 'getResourcePath' IPC event. This event is triggered to get the resource path.
+ getResourcePath() {
+ return appResourcePath()
+ }
+
+ // Handles the 'getUserHomePath' IPC event. This event is triggered to get the user home path.
+ getUserHomePath() {
+ return process.env[process.platform == 'win32' ? 'USERPROFILE' : 'HOME']
+ }
+
+ // handle fs is directory here
+ fileStat(path: string) {
+ const normalizedPath = normalizeFilePath(path)
+
+ const fullPath = join(getJanDataFolderPath(), normalizedPath)
+ const isExist = fs.existsSync(fullPath)
+ if (!isExist) return undefined
+
+ const isDirectory = fs.lstatSync(fullPath).isDirectory()
+ const size = fs.statSync(fullPath).size
+
+ const fileStat: FileStat = {
+ isDirectory,
+ size,
+ }
+
+ return fileStat
+ }
+
+ writeBlob(path: string, data: any) {
+ try {
+ const normalizedPath = normalizeFilePath(path)
+ const dataBuffer = Buffer.from(data, 'base64')
+ fs.writeFileSync(join(getJanDataFolderPath(), normalizedPath), dataBuffer)
+ } catch (err) {
+ console.error(`writeFile ${path} result: ${err}`)
+ }
+ }
+}
diff --git a/core/src/node/api/restful/app/download.ts b/core/src/node/api/restful/app/download.ts
new file mode 100644
index 000000000..b5919659b
--- /dev/null
+++ b/core/src/node/api/restful/app/download.ts
@@ -0,0 +1,23 @@
+import { DownloadRoute } from '../../../../api'
+import { DownloadManager } from '../../../helper/download'
+import { HttpServer } from '../../HttpServer'
+
+export const downloadRouter = async (app: HttpServer) => {
+ app.get(`/download/${DownloadRoute.getDownloadProgress}/:modelId`, async (req, res) => {
+ const modelId = req.params.modelId
+
+ console.debug(`Getting download progress for model ${modelId}`)
+ console.debug(
+ `All Download progress: ${JSON.stringify(DownloadManager.instance.downloadProgressMap)}`
+ )
+
+ // check if null DownloadManager.instance.downloadProgressMap
+ if (!DownloadManager.instance.downloadProgressMap[modelId]) {
+ return res.status(404).send({
+ message: 'Download progress not found',
+ })
+ } else {
+ return res.status(200).send(DownloadManager.instance.downloadProgressMap[modelId])
+ }
+ })
+}
diff --git a/core/src/node/api/restful/app/handlers.ts b/core/src/node/api/restful/app/handlers.ts
new file mode 100644
index 000000000..43c3f7add
--- /dev/null
+++ b/core/src/node/api/restful/app/handlers.ts
@@ -0,0 +1,13 @@
+import { HttpServer } from '../../HttpServer'
+import { Handler, RequestHandler } from '../../common/handler'
+
+export function handleRequests(app: HttpServer) {
+ const restWrapper: Handler = (route: string, listener: (...args: any[]) => any) => {
+ app.post(`/app/${route}`, async (request: any, reply: any) => {
+ const args = JSON.parse(request.body) as any[]
+ reply.send(JSON.stringify(await listener(...args)))
+ })
+ }
+ const handler = new RequestHandler(restWrapper)
+ handler.handle()
+}
diff --git a/core/src/node/api/routes/common.ts b/core/src/node/api/restful/common.ts
similarity index 58%
rename from core/src/node/api/routes/common.ts
rename to core/src/node/api/restful/common.ts
index 8887755fe..b87bc946d 100644
--- a/core/src/node/api/routes/common.ts
+++ b/core/src/node/api/restful/common.ts
@@ -1,22 +1,24 @@
-import { AppRoute } from '../../../api'
import { HttpServer } from '../HttpServer'
-import { basename, join } from 'path'
import {
chatCompletions,
deleteBuilder,
downloadModel,
getBuilder,
retrieveBuilder,
-} from '../common/builder'
+ createMessage,
+ createThread,
+ getMessages,
+ retrieveMesasge,
+ updateThread,
+} from './helper/builder'
-import { JanApiRouteConfiguration } from '../common/configuration'
-import { startModel, stopModel } from '../common/startStopModel'
+import { JanApiRouteConfiguration } from './helper/configuration'
+import { startModel, stopModel } from './helper/startStopModel'
import { ModelSettingParams } from '../../../types'
-import { getJanDataFolderPath } from '../../utils'
-import { normalizeFilePath } from '../../path'
export const commonRouter = async (app: HttpServer) => {
// Common Routes
+ // Read & Delete :: Threads | Models | Assistants
Object.keys(JanApiRouteConfiguration).forEach((key) => {
app.get(`/${key}`, async (_request) => getBuilder(JanApiRouteConfiguration[key]))
@@ -29,7 +31,24 @@ export const commonRouter = async (app: HttpServer) => {
)
})
- // Download Model Routes
+ // Threads
+ app.post(`/threads/`, async (req, res) => createThread(req.body))
+
+ app.get(`/threads/:threadId/messages`, async (req, res) => getMessages(req.params.threadId))
+
+ app.get(`/threads/:threadId/messages/:messageId`, async (req, res) =>
+ retrieveMesasge(req.params.threadId, req.params.messageId)
+ )
+
+ app.post(`/threads/:threadId/messages`, async (req, res) =>
+ createMessage(req.params.threadId as any, req.body as any)
+ )
+
+ app.patch(`/threads/:threadId`, async (request: any) =>
+ updateThread(request.params.threadId, request.body)
+ )
+
+ // Models
app.get(`/models/download/:modelId`, async (request: any) =>
downloadModel(request.params.modelId, {
ignoreSSL: request.query.ignoreSSL === 'true',
@@ -48,24 +67,6 @@ export const commonRouter = async (app: HttpServer) => {
app.put(`/models/:modelId/stop`, async (request: any) => stopModel(request.params.modelId))
- // Chat Completion Routes
+ // Chat Completion
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[]
-
- const paths = args[0].map((arg: string) =>
- typeof arg === 'string' && (arg.startsWith(`file:/`) || arg.startsWith(`file:\\`))
- ? join(getJanDataFolderPath(), normalizeFilePath(arg))
- : arg
- )
-
- reply.send(JSON.stringify(join(...paths)))
- })
-
- app.post(`/app/${AppRoute.baseName}`, async (request: any, reply: any) => {
- const args = JSON.parse(request.body) as any[]
- reply.send(JSON.stringify(basename(args[0])))
- })
}
diff --git a/core/src/node/api/common/builder.ts b/core/src/node/api/restful/helper/builder.ts
similarity index 99%
rename from core/src/node/api/common/builder.ts
rename to core/src/node/api/restful/helper/builder.ts
index 5c99cf4d8..a8124a74a 100644
--- a/core/src/node/api/common/builder.ts
+++ b/core/src/node/api/restful/helper/builder.ts
@@ -1,10 +1,11 @@
import fs from 'fs'
import { JanApiRouteConfiguration, RouteConfiguration } from './configuration'
import { join } from 'path'
-import { ContentType, MessageStatus, Model, ThreadMessage } from './../../../index'
-import { getEngineConfiguration, getJanDataFolderPath } from '../../utils'
+import { ContentType, MessageStatus, Model, ThreadMessage } from '../../../../index'
+import { getEngineConfiguration, getJanDataFolderPath } from '../../../helper'
import { DEFAULT_CHAT_COMPLETION_URL } from './consts'
+// TODO: Refactor these
export const getBuilder = async (configuration: RouteConfiguration) => {
const directoryPath = join(getJanDataFolderPath(), configuration.dirName)
try {
diff --git a/core/src/node/api/common/configuration.ts b/core/src/node/api/restful/helper/configuration.ts
similarity index 100%
rename from core/src/node/api/common/configuration.ts
rename to core/src/node/api/restful/helper/configuration.ts
diff --git a/core/src/node/api/common/consts.ts b/core/src/node/api/restful/helper/consts.ts
similarity index 100%
rename from core/src/node/api/common/consts.ts
rename to core/src/node/api/restful/helper/consts.ts
diff --git a/core/src/node/api/common/startStopModel.ts b/core/src/node/api/restful/helper/startStopModel.ts
similarity index 99%
rename from core/src/node/api/common/startStopModel.ts
rename to core/src/node/api/restful/helper/startStopModel.ts
index 0d4934e1c..0e6972b0b 100644
--- a/core/src/node/api/common/startStopModel.ts
+++ b/core/src/node/api/restful/helper/startStopModel.ts
@@ -1,9 +1,9 @@
import fs from 'fs'
import { join } from 'path'
-import { getJanDataFolderPath, getJanExtensionsPath, getSystemResourceInfo } from '../../utils'
-import { logServer } from '../../log'
+import { getJanDataFolderPath, getJanExtensionsPath, getSystemResourceInfo } from '../../../helper'
+import { logServer } from '../../../helper/log'
import { ChildProcessWithoutNullStreams, spawn } from 'child_process'
-import { Model, ModelSettingParams, PromptTemplate } from '../../../types'
+import { Model, ModelSettingParams, PromptTemplate } from '../../../../types'
import {
LOCAL_HOST,
NITRO_DEFAULT_PORT,
diff --git a/core/src/node/api/restful/v1.ts b/core/src/node/api/restful/v1.ts
new file mode 100644
index 000000000..5eb8f5067
--- /dev/null
+++ b/core/src/node/api/restful/v1.ts
@@ -0,0 +1,16 @@
+import { HttpServer } from '../HttpServer'
+import { commonRouter } from './common'
+import { downloadRouter } from './app/download'
+import { handleRequests } from './app/handlers'
+
+export const v1Router = async (app: HttpServer) => {
+ // MARK: Public API Routes
+ app.register(commonRouter)
+
+ // MARK: Internal Application Routes
+ handleRequests(app)
+
+ // Expanded route for tracking download progress
+ // TODO: Replace by Observer Wrapper (ZeroMQ / Vanilla Websocket)
+ app.register(downloadRouter)
+}
diff --git a/core/src/node/api/routes/download.ts b/core/src/node/api/routes/download.ts
deleted file mode 100644
index 7fb05daee..000000000
--- a/core/src/node/api/routes/download.ts
+++ /dev/null
@@ -1,112 +0,0 @@
-import { DownloadRoute } from '../../../api'
-import { join } from 'path'
-import { DownloadManager } from '../../download'
-import { HttpServer } from '../HttpServer'
-import { createWriteStream } from 'fs'
-import { getJanDataFolderPath } from '../../utils'
-import { normalizeFilePath } from '../../path'
-import { DownloadState } from '../../../types'
-
-export const downloadRouter = async (app: HttpServer) => {
- app.get(`/${DownloadRoute.getDownloadProgress}/:modelId`, async (req, res) => {
- const modelId = req.params.modelId
-
- console.debug(`Getting download progress for model ${modelId}`)
- console.debug(
- `All Download progress: ${JSON.stringify(DownloadManager.instance.downloadProgressMap)}`
- )
-
- // check if null DownloadManager.instance.downloadProgressMap
- if (!DownloadManager.instance.downloadProgressMap[modelId]) {
- return res.status(404).send({
- message: 'Download progress not found',
- })
- } else {
- return res.status(200).send(DownloadManager.instance.downloadProgressMap[modelId])
- }
- })
-
- app.post(`/${DownloadRoute.downloadFile}`, async (req, res) => {
- const strictSSL = !(req.query.ignoreSSL === 'true')
- const proxy = req.query.proxy?.startsWith('http') ? req.query.proxy : undefined
- const body = JSON.parse(req.body as any)
- const normalizedArgs = body.map((arg: any) => {
- if (typeof arg === 'string' && arg.startsWith('file:')) {
- return join(getJanDataFolderPath(), normalizeFilePath(arg))
- }
- return arg
- })
-
- const localPath = normalizedArgs[1]
- const array = localPath.split('/')
- const fileName = array.pop() ?? ''
- const modelId = array.pop() ?? ''
- console.debug('downloadFile', normalizedArgs, fileName, modelId)
-
- const request = require('request')
- const progress = require('request-progress')
-
- const rq = request({ url: normalizedArgs[0], strictSSL, proxy })
- progress(rq, {})
- .on('progress', function (state: any) {
- const downloadProps: DownloadState = {
- ...state,
- modelId,
- fileName,
- downloadState: 'downloading',
- }
- console.debug(`Download ${modelId} onProgress`, downloadProps)
- DownloadManager.instance.downloadProgressMap[modelId] = downloadProps
- })
- .on('error', function (err: Error) {
- console.debug(`Download ${modelId} onError`, err.message)
-
- const currentDownloadState = DownloadManager.instance.downloadProgressMap[modelId]
- if (currentDownloadState) {
- DownloadManager.instance.downloadProgressMap[modelId] = {
- ...currentDownloadState,
- downloadState: 'error',
- }
- }
- })
- .on('end', function () {
- console.debug(`Download ${modelId} onEnd`)
-
- const currentDownloadState = DownloadManager.instance.downloadProgressMap[modelId]
- if (currentDownloadState) {
- if (currentDownloadState.downloadState === 'downloading') {
- // if the previous state is downloading, then set the state to end (success)
- DownloadManager.instance.downloadProgressMap[modelId] = {
- ...currentDownloadState,
- downloadState: 'end',
- }
- }
- }
- })
- .pipe(createWriteStream(normalizedArgs[1]))
-
- DownloadManager.instance.setRequest(localPath, rq)
- res.status(200).send({ message: 'Download started' })
- })
-
- 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.startsWith('file:')) {
- return join(getJanDataFolderPath(), normalizeFilePath(arg))
- }
- return arg
- })
-
- const localPath = normalizedArgs[0]
- const fileName = localPath.split('/').pop() ?? ''
- const rq = DownloadManager.instance.networkRequests[fileName]
- DownloadManager.instance.networkRequests[fileName] = undefined
- rq?.abort()
- if (rq) {
- res.status(200).send({ message: 'Download aborted' })
- } else {
- res.status(404).send({ message: 'Download not found' })
- }
- })
-}
diff --git a/core/src/node/api/routes/extension.ts b/core/src/node/api/routes/extension.ts
deleted file mode 100644
index 02bc54eb3..000000000
--- a/core/src/node/api/routes/extension.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import { join, extname } from 'path'
-import { ExtensionRoute } from '../../../api/index'
-import { ModuleManager } from '../../module'
-import { getActiveExtensions, installExtensions } from '../../extension/store'
-import { HttpServer } from '../HttpServer'
-
-import { readdirSync } from 'fs'
-import { getJanExtensionsPath } from '../../utils'
-
-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) => {
- 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(getJanExtensionsPath(), 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.`)
- }
- })
-}
diff --git a/core/src/node/api/routes/fileManager.ts b/core/src/node/api/routes/fileManager.ts
deleted file mode 100644
index b4c73dda1..000000000
--- a/core/src/node/api/routes/fileManager.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { FileManagerRoute } from '../../../api'
-import { HttpServer } from '../../index'
-import { join } from 'path'
-
-export const fileManagerRouter = async (app: HttpServer) => {
- app.post(`/fs/${FileManagerRoute.syncFile}`, async (request: any, reply: any) => {
- const reflect = require('@alumna/reflect')
- const args = JSON.parse(request.body)
- return reflect({
- src: args[0],
- dest: args[1],
- recursive: true,
- delete: false,
- overwrite: true,
- errorOnExist: false,
- })
- })
-
- app.post(`/fs/${FileManagerRoute.getJanDataFolderPath}`, async (request: any, reply: any) =>
- global.core.appPath()
- )
-
- app.post(`/fs/${FileManagerRoute.getResourcePath}`, async (request: any, reply: any) =>
- join(global.core.appPath(), '../../..')
- )
-
- app.post(`/app/${FileManagerRoute.getUserHomePath}`, async (request: any, reply: any) => {})
- app.post(`/fs/${FileManagerRoute.fileStat}`, async (request: any, reply: any) => {})
-}
diff --git a/core/src/node/api/routes/fs.ts b/core/src/node/api/routes/fs.ts
deleted file mode 100644
index 9535418a0..000000000
--- a/core/src/node/api/routes/fs.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import { FileManagerRoute, FileSystemRoute } from '../../../api'
-import { join } from 'path'
-import { HttpServer } from '../HttpServer'
-import { getJanDataFolderPath } from '../../utils'
-import { normalizeFilePath } from '../../path'
-import { writeFileSync } from 'fs'
-
-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.startsWith(`file:/`) || arg.startsWith(`file:\\`))
- ? join(getJanDataFolderPath(), normalizeFilePath(arg))
- : arg
- )
- )
- })
- res.status(200).send(result)
- } catch (ex) {
- console.log(ex)
- }
- })
- })
- app.post(`/${FileManagerRoute.writeBlob}`, async (request: any, reply: any) => {
- try {
- const args = JSON.parse(request.body) as any[]
- console.log('writeBlob:', args[0])
- const dataBuffer = Buffer.from(args[1], 'base64')
- writeFileSync(args[0], dataBuffer)
- } catch (err) {
- console.error(`writeFile ${request.body} result: ${err}`)
- }
- })
-}
diff --git a/core/src/node/api/routes/index.ts b/core/src/node/api/routes/index.ts
deleted file mode 100644
index e6edc62f7..000000000
--- a/core/src/node/api/routes/index.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export * from './download'
-export * from './extension'
-export * from './fs'
-export * from './thread'
-export * from './common'
-export * from './v1'
diff --git a/core/src/node/api/routes/thread.ts b/core/src/node/api/routes/thread.ts
deleted file mode 100644
index 4066d2716..000000000
--- a/core/src/node/api/routes/thread.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-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),
- )
-}
diff --git a/core/src/node/api/routes/v1.ts b/core/src/node/api/routes/v1.ts
deleted file mode 100644
index 301c41ac0..000000000
--- a/core/src/node/api/routes/v1.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { HttpServer } from '../HttpServer'
-import { commonRouter } from './common'
-import { threadRouter } from './thread'
-import { fsRouter } from './fs'
-import { extensionRouter } from './extension'
-import { downloadRouter } from './download'
-import { fileManagerRouter } from './fileManager'
-
-export const v1Router = async (app: HttpServer) => {
- // MARK: External Routes
- app.register(commonRouter)
- app.register(threadRouter, {
- prefix: '/threads',
- })
-
- // MARK: Internal Application Routes
- app.register(fsRouter, {
- prefix: '/fs',
- })
- app.register(fileManagerRouter)
-
- app.register(extensionRouter, {
- prefix: '/extension',
- })
- app.register(downloadRouter, {
- prefix: '/download',
- })
-}
diff --git a/core/src/node/extension/extension.ts b/core/src/node/extension/extension.ts
index aeb0277c0..1f8dfa3ec 100644
--- a/core/src/node/extension/extension.ts
+++ b/core/src/node/extension/extension.ts
@@ -104,7 +104,7 @@ export default class Extension {
await pacote.extract(
this.specifier,
join(ExtensionManager.instance.getExtensionsPath() ?? '', this.name ?? ''),
- this.installOptions,
+ this.installOptions
)
// Set the url using the custom extensions protocol
diff --git a/core/src/node/extension/store.ts b/core/src/node/extension/store.ts
index 84b1f9caf..93b1aeb2b 100644
--- a/core/src/node/extension/store.ts
+++ b/core/src/node/extension/store.ts
@@ -1,6 +1,6 @@
-import { writeFileSync } from "fs";
-import Extension from "./extension";
-import { ExtensionManager } from "./manager";
+import { writeFileSync } from 'fs'
+import Extension from './extension'
+import { ExtensionManager } from './manager'
/**
* @module store
@@ -11,7 +11,7 @@ import { ExtensionManager } from "./manager";
* Register of installed extensions
* @type {Object.} extension - List of installed extensions
*/
-const extensions: Record = {};
+const extensions: Record = {}
/**
* Get a extension from the stored extensions.
@@ -21,10 +21,10 @@ const extensions: Record = {};
*/
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]
}
/**
@@ -33,7 +33,7 @@ export function getExtension(name: string) {
* @alias extensionManager.getAllExtensions
*/
export function getAllExtensions() {
- return Object.values(extensions);
+ return Object.values(extensions)
}
/**
@@ -42,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)
}
/**
@@ -53,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
}
/**
@@ -65,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)
}
}
@@ -77,14 +77,11 @@ export function addExtension(extension: Extension, persist = true) {
* @returns {void}
*/
export function persistExtensions() {
- const persistData: Record = {};
+ const persistData: Record = {}
for (const name in extensions) {
- persistData[name] = extensions[name];
+ persistData[name] = extensions[name]
}
- writeFileSync(
- ExtensionManager.instance.getExtensionsFile(),
- JSON.stringify(persistData),
- );
+ writeFileSync(ExtensionManager.instance.getExtensionsFile(), JSON.stringify(persistData))
}
/**
@@ -94,26 +91,29 @@ export function persistExtensions() {
* @returns {Promise.>} New extension
* @alias extensionManager.installExtensions
*/
-export async function installExtensions(extensions: any, store = true) {
- const installed: Extension[] = [];
+export async function installExtensions(extensions: any) {
+ 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)
+ if (!extension.origin) {
+ continue
+ }
+ await extension._install()
+ if (activate) extension.setActive(true)
// Add extension to store if needed
- if (store) addExtension(extension);
- installed.push(extension);
+ addExtension(extension)
+ installed.push(extension)
}
// Return list of all installed extensions
- return installed;
+ return installed
}
/**
diff --git a/core/src/node/utils/index.ts b/core/src/node/helper/config.ts
similarity index 93%
rename from core/src/node/utils/index.ts
rename to core/src/node/helper/config.ts
index 4bcbf13b1..a47875e68 100644
--- a/core/src/node/utils/index.ts
+++ b/core/src/node/helper/config.ts
@@ -2,7 +2,7 @@ import { AppConfiguration, SystemResourceInfo } from '../../types'
import { join } from 'path'
import fs from 'fs'
import os from 'os'
-import { log, logServer } from '../log'
+import { log, logServer } from './log'
import childProcess from 'child_process'
// TODO: move this to core
@@ -56,34 +56,6 @@ export const updateAppConfiguration = (configuration: AppConfiguration): Promise
return Promise.resolve()
}
-/**
- * Utility function to get server log path
- *
- * @returns {string} The log path.
- */
-export const getServerLogPath = (): string => {
- const appConfigurations = getAppConfigurations()
- const logFolderPath = join(appConfigurations.data_folder, 'logs')
- if (!fs.existsSync(logFolderPath)) {
- fs.mkdirSync(logFolderPath, { recursive: true })
- }
- return join(logFolderPath, 'server.log')
-}
-
-/**
- * Utility function to get app log path
- *
- * @returns {string} The log path.
- */
-export const getAppLogPath = (): string => {
- const appConfigurations = getAppConfigurations()
- const logFolderPath = join(appConfigurations.data_folder, 'logs')
- if (!fs.existsSync(logFolderPath)) {
- fs.mkdirSync(logFolderPath, { recursive: true })
- }
- return join(logFolderPath, 'app.log')
-}
-
/**
* Utility function to get data folder path
*
@@ -146,18 +118,6 @@ const exec = async (command: string): Promise => {
})
}
-export const getSystemResourceInfo = async (): Promise => {
- const cpu = await physicalCpuCount()
- const message = `[NITRO]::CPU informations - ${cpu}`
- log(message)
- logServer(message)
-
- return {
- numCpuPhysicalCore: cpu,
- memAvailable: 0, // TODO: this should not be 0
- }
-}
-
export const getEngineConfiguration = async (engineId: string) => {
if (engineId !== 'openai') {
return undefined
@@ -167,3 +127,31 @@ export const getEngineConfiguration = async (engineId: string) => {
const data = fs.readFileSync(filePath, 'utf-8')
return JSON.parse(data)
}
+
+/**
+ * Utility function to get server log path
+ *
+ * @returns {string} The log path.
+ */
+export const getServerLogPath = (): string => {
+ const appConfigurations = getAppConfigurations()
+ const logFolderPath = join(appConfigurations.data_folder, 'logs')
+ if (!fs.existsSync(logFolderPath)) {
+ fs.mkdirSync(logFolderPath, { recursive: true })
+ }
+ return join(logFolderPath, 'server.log')
+}
+
+/**
+ * Utility function to get app log path
+ *
+ * @returns {string} The log path.
+ */
+export const getAppLogPath = (): string => {
+ const appConfigurations = getAppConfigurations()
+ const logFolderPath = join(appConfigurations.data_folder, 'logs')
+ if (!fs.existsSync(logFolderPath)) {
+ fs.mkdirSync(logFolderPath, { recursive: true })
+ }
+ return join(logFolderPath, 'app.log')
+}
diff --git a/core/src/node/download.ts b/core/src/node/helper/download.ts
similarity index 94%
rename from core/src/node/download.ts
rename to core/src/node/helper/download.ts
index b3f284440..b9fb88bb5 100644
--- a/core/src/node/download.ts
+++ b/core/src/node/helper/download.ts
@@ -1,4 +1,4 @@
-import { DownloadState } from '../types'
+import { DownloadState } from '../../types'
/**
* Manages file downloads and network requests.
diff --git a/core/src/node/helper/index.ts b/core/src/node/helper/index.ts
new file mode 100644
index 000000000..6fc54fc6b
--- /dev/null
+++ b/core/src/node/helper/index.ts
@@ -0,0 +1,6 @@
+export * from './config'
+export * from './download'
+export * from './log'
+export * from './module'
+export * from './path'
+export * from './resource'
diff --git a/core/src/node/log.ts b/core/src/node/helper/log.ts
similarity index 93%
rename from core/src/node/log.ts
rename to core/src/node/helper/log.ts
index 6f2c2f80f..8ff196943 100644
--- a/core/src/node/log.ts
+++ b/core/src/node/helper/log.ts
@@ -1,6 +1,6 @@
import fs from 'fs'
import util from 'util'
-import { getAppLogPath, getServerLogPath } from './utils'
+import { getAppLogPath, getServerLogPath } from './config'
export const log = (message: string) => {
const path = getAppLogPath()
diff --git a/core/src/node/module.ts b/core/src/node/helper/module.ts
similarity index 100%
rename from core/src/node/module.ts
rename to core/src/node/helper/module.ts
diff --git a/core/src/node/helper/path.ts b/core/src/node/helper/path.ts
new file mode 100644
index 000000000..c20889f4c
--- /dev/null
+++ b/core/src/node/helper/path.ts
@@ -0,0 +1,35 @@
+import { join } from 'path'
+
+/**
+ * Normalize file path
+ * Remove all file protocol prefix
+ * @param path
+ * @returns
+ */
+export function normalizeFilePath(path: string): string {
+ return path.replace(/^(file:[\\/]+)([^:\s]+)$/, '$2')
+}
+
+export async function appResourcePath(): Promise {
+ let electron: any = undefined
+
+ try {
+ const moduleName = 'electron'
+ electron = await import(moduleName)
+ } catch (err) {
+ console.error('Electron is not available')
+ }
+
+ // electron
+ if (electron && electron.protocol) {
+ let appPath = join(electron.app.getAppPath(), '..', 'app.asar.unpacked')
+
+ if (!electron.app.isPackaged) {
+ // for development mode
+ appPath = join(electron.app.getAppPath())
+ }
+ return appPath
+ }
+ // server
+ return join(global.core.appPath(), '../../..')
+}
diff --git a/core/src/node/helper/resource.ts b/core/src/node/helper/resource.ts
new file mode 100644
index 000000000..04a7d512a
--- /dev/null
+++ b/core/src/node/helper/resource.ts
@@ -0,0 +1,15 @@
+import { SystemResourceInfo } from '../../types'
+import { physicalCpuCount } from './config'
+import { log, logServer } from './log'
+
+export const getSystemResourceInfo = async (): Promise => {
+ const cpu = await physicalCpuCount()
+ const message = `[NITRO]::CPU informations - ${cpu}`
+ log(message)
+ logServer(message)
+
+ return {
+ numCpuPhysicalCore: cpu,
+ memAvailable: 0, // TODO: this should not be 0
+ }
+}
diff --git a/core/src/node/index.ts b/core/src/node/index.ts
index 10385ecfc..31f2f076e 100644
--- a/core/src/node/index.ts
+++ b/core/src/node/index.ts
@@ -2,9 +2,5 @@ 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'
-export * from './log'
-export * from './utils'
-export * from './path'
+export * from './helper'
diff --git a/core/src/node/path.ts b/core/src/node/path.ts
deleted file mode 100644
index adbc38c6c..000000000
--- a/core/src/node/path.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-/**
- * Normalize file path
- * Remove all file protocol prefix
- * @param path
- * @returns
- */
-export function normalizeFilePath(path: string): string {
- return path.replace(/^(file:[\\/]+)([^:\s]+)$/, "$2");
-}
diff --git a/core/src/types/assistant/assistantEvent.ts b/core/src/types/assistant/assistantEvent.ts
index f8f3e6ad0..8c32f5d37 100644
--- a/core/src/types/assistant/assistantEvent.ts
+++ b/core/src/types/assistant/assistantEvent.ts
@@ -2,7 +2,6 @@
* The `EventName` enumeration contains the names of all the available events in the Jan platform.
*/
export enum AssistantEvent {
- /** The `OnAssistantsUpdate` event is emitted when the assistant list is updated. */
- OnAssistantsUpdate = 'OnAssistantsUpdate',
- }
-
\ No newline at end of file
+ /** The `OnAssistantsUpdate` event is emitted when the assistant list is updated. */
+ OnAssistantsUpdate = 'OnAssistantsUpdate',
+}
diff --git a/core/src/types/file/index.ts b/core/src/types/file/index.ts
index 57d687d2f..cc7274a28 100644
--- a/core/src/types/file/index.ts
+++ b/core/src/types/file/index.ts
@@ -5,7 +5,7 @@ export type FileStat = {
export type DownloadState = {
modelId: string
- filename: string
+ fileName: string
time: DownloadTime
speed: number
percent: number
diff --git a/core/src/types/message/index.ts b/core/src/types/message/index.ts
index e8d78deda..ebb4c363d 100644
--- a/core/src/types/message/index.ts
+++ b/core/src/types/message/index.ts
@@ -1,3 +1,4 @@
export * from './messageEntity'
export * from './messageInterface'
export * from './messageEvent'
+export * from './messageRequestType'
diff --git a/core/src/types/message/messageEntity.ts b/core/src/types/message/messageEntity.ts
index 87e4b1997..e9211d550 100644
--- a/core/src/types/message/messageEntity.ts
+++ b/core/src/types/message/messageEntity.ts
@@ -27,6 +27,8 @@ export type ThreadMessage = {
updated: number
/** The additional metadata of this message. **/
metadata?: Record
+
+ type?: string
}
/**
@@ -56,6 +58,8 @@ export type MessageRequest = {
/** The thread of this message is belong to. **/
// TODO: deprecate threadId field
thread?: Thread
+
+ type?: string
}
/**
diff --git a/core/src/types/message/messageRequestType.ts b/core/src/types/message/messageRequestType.ts
new file mode 100644
index 000000000..cbb4cf421
--- /dev/null
+++ b/core/src/types/message/messageRequestType.ts
@@ -0,0 +1,5 @@
+export enum MessageRequestType {
+ Thread = 'Thread',
+ Assistant = 'Assistant',
+ Summary = 'Summary',
+}
diff --git a/core/src/types/model/modelInterface.ts b/core/src/types/model/modelInterface.ts
index 74a479f3c..93d5867ee 100644
--- a/core/src/types/model/modelInterface.ts
+++ b/core/src/types/model/modelInterface.ts
@@ -10,7 +10,7 @@ export interface ModelInterface {
* @param network - Optional object to specify proxy/whether to ignore SSL certificates.
* @returns A Promise that resolves when the model has been downloaded.
*/
- downloadModel(model: Model, network?: { ignoreSSL?: boolean, proxy?: string }): Promise
+ downloadModel(model: Model, network?: { ignoreSSL?: boolean; proxy?: string }): Promise
/**
* Cancels the download of a specific model.
diff --git a/core/tests/node/path.test.ts b/core/tests/node/path.test.ts
index 9f8a557bb..5390df119 100644
--- a/core/tests/node/path.test.ts
+++ b/core/tests/node/path.test.ts
@@ -1,4 +1,4 @@
-import { normalizeFilePath } from "../../src/node/path";
+import { normalizeFilePath } from "../../src/node/helper/path";
describe("Test file normalize", () => {
test("returns no file protocol prefix on Unix", async () => {
diff --git a/core/tslint.json b/core/tslint.json
index 398a41670..6543a641a 100644
--- a/core/tslint.json
+++ b/core/tslint.json
@@ -1,6 +1,3 @@
{
- "extends": [
- "tslint-config-standard",
- "tslint-config-prettier"
- ]
-}
\ No newline at end of file
+ "extends": ["tslint-config-standard", "tslint-config-prettier"]
+}
diff --git a/docs/blog/authors.yml b/docs/blog/authors.yml
index 4c1dc521e..ec58002e4 100644
--- a/docs/blog/authors.yml
+++ b/docs/blog/authors.yml
@@ -11,7 +11,7 @@ namchuai:
url: https://github.com/namchuai
image_url: https://avatars.githubusercontent.com/u/10397206?v=4
email: james@jan.ai
-
+
hiro-v:
name: Hiro Vuong
title: MLE
@@ -60,4 +60,17 @@ automaticcat:
url: https://github.com/tikikun
image_url: https://avatars.githubusercontent.com/u/22268502?v=4
email: alan@jan.ai
-
+
+hieu-jan:
+ name: Henry Ho
+ title: Software Engineer
+ url: https://github.com/hieu-jan
+ image_url: https://avatars.githubusercontent.com/u/150573299?v=4
+ email: hieu@jan.ai
+
+0xsage:
+ name: Nicole Zhu
+ title: Co-Founder
+ url: https://github.com/0xsage
+ image_url: https://avatars.githubusercontent.com/u/69952136?v=4
+ email: nicole@jan.ai
diff --git a/docs/docs/guides/02-installation/05-docker.md b/docs/docs/guides/02-installation/05-docker.md
new file mode 100644
index 000000000..6236ed92e
--- /dev/null
+++ b/docs/docs/guides/02-installation/05-docker.md
@@ -0,0 +1,102 @@
+---
+title: Docker
+slug: /install/docker
+description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server.
+keywords:
+ [
+ Jan AI,
+ Jan,
+ ChatGPT alternative,
+ local AI,
+ private AI,
+ conversational AI,
+ no-subscription fee,
+ large language model,
+ docker installation,
+ ]
+---
+
+# Installing Jan using Docker
+
+## Installation
+
+### Pre-requisites
+
+:::note
+
+**Supported OS**: Linux, WSL2 Docker
+
+:::
+
+- Docker Engine and Docker Compose are required to run Jan in Docker mode. Follow the [instructions](https://docs.docker.com/engine/install/ubuntu/) below to get started with Docker Engine on Ubuntu.
+
+```bash
+curl -fsSL https://get.docker.com -o get-docker.sh
+sudo sh ./get-docker.sh --dry-run
+```
+
+- If you intend to run Jan in GPU mode, you need to install `nvidia-driver` and `nvidia-docker2`. Follow the instruction [here](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) for installation.
+
+### Instructions
+
+- Run Jan in Docker mode
+
+ - **Option 1**: Run Jan in CPU mode
+
+ ```bash
+ docker compose --profile cpu up -d
+ ```
+
+ - **Option 2**: Run Jan in GPU mode
+
+ - **Step 1**: Check CUDA compatibility with your NVIDIA driver by running `nvidia-smi` and check the CUDA version in the output
+
+ ```bash
+ nvidia-smi
+
+ # Output
+ +---------------------------------------------------------------------------------------+
+ | NVIDIA-SMI 531.18 Driver Version: 531.18 CUDA Version: 12.1 |
+ |-----------------------------------------+----------------------+----------------------+
+ | GPU Name TCC/WDDM | Bus-Id Disp.A | Volatile Uncorr. ECC |
+ | Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
+ | | | MIG M. |
+ |=========================================+======================+======================|
+ | 0 NVIDIA GeForce RTX 4070 Ti WDDM | 00000000:01:00.0 On | N/A |
+ | 0% 44C P8 16W / 285W| 1481MiB / 12282MiB | 2% Default |
+ | | | N/A |
+ +-----------------------------------------+----------------------+----------------------+
+ | 1 NVIDIA GeForce GTX 1660 Ti WDDM | 00000000:02:00.0 Off | N/A |
+ | 0% 49C P8 14W / 120W| 0MiB / 6144MiB | 0% Default |
+ | | | N/A |
+ +-----------------------------------------+----------------------+----------------------+
+ | 2 NVIDIA GeForce GTX 1660 Ti WDDM | 00000000:05:00.0 Off | N/A |
+ | 29% 38C P8 11W / 120W| 0MiB / 6144MiB | 0% Default |
+ | | | N/A |
+ +-----------------------------------------+----------------------+----------------------+
+
+ +---------------------------------------------------------------------------------------+
+ | Processes: |
+ | GPU GI CI PID Type Process name GPU Memory |
+ | ID ID Usage |
+ |=======================================================================================|
+ ```
+
+ - **Step 2**: Visit [NVIDIA NGC Catalog ](https://catalog.ngc.nvidia.com/orgs/nvidia/containers/cuda/tags) and find the smallest minor version of image tag that matches your CUDA version (e.g., 12.1 -> 12.1.0)
+
+ - **Step 3**: Update the `Dockerfile.gpu` line number 5 with the latest minor version of the image tag from step 2 (e.g. change `FROM nvidia/cuda:12.2.0-runtime-ubuntu22.04 AS base` to `FROM nvidia/cuda:12.1.0-runtime-ubuntu22.04 AS base`)
+
+ - **Step 4**: Run command to start Jan in GPU mode
+
+ ```bash
+ # GPU mode
+ docker compose --profile gpu up -d
+ ```
+
+ This will start the web server and you can access Jan at `http://localhost:3000`.
+
+:::warning
+
+- Docker mode is currently only suitable for development and localhost. Production is not supported yet, and the RAG feature is not available in Docker mode.
+
+:::
diff --git a/docs/docs/guides/02-installation/05-nightly-build.md b/docs/docs/guides/02-installation/07-nightly-build.md
similarity index 100%
rename from docs/docs/guides/02-installation/05-nightly-build.md
rename to docs/docs/guides/02-installation/07-nightly-build.md
diff --git a/docs/docs/guides/02-installation/07-antivirus-compatibility-testing.md b/docs/docs/guides/02-installation/08-antivirus-compatibility-testing.md
similarity index 100%
rename from docs/docs/guides/02-installation/07-antivirus-compatibility-testing.md
rename to docs/docs/guides/02-installation/08-antivirus-compatibility-testing.md
diff --git a/docs/docs/guides/08-troubleshooting/03-gpu-not-used.mdx b/docs/docs/guides/08-troubleshooting/03-gpu-not-used.mdx
index d35993ab6..53638027b 100644
--- a/docs/docs/guides/08-troubleshooting/03-gpu-not-used.mdx
+++ b/docs/docs/guides/08-troubleshooting/03-gpu-not-used.mdx
@@ -188,4 +188,6 @@ Troubleshooting tips:
2. If the issue persists, ensure your (V)RAM is accessible by the application. Some folks have virtual RAM and need additional configuration.
-3. Get help in [Jan Discord](https://discord.gg/mY69SZaMaC).
+3. If you are facing issues with the installation of RTX issues, please update the NVIDIA driver that supports CUDA 11.7 or higher. Ensure that the CUDA path is added to the environment variable.
+
+4. Get help in [Jan Discord](https://discord.gg/mY69SZaMaC).
diff --git a/electron/.prettierrc b/electron/.prettierrc
deleted file mode 100644
index 46f1abcb0..000000000
--- a/electron/.prettierrc
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "semi": false,
- "singleQuote": true,
- "quoteProps": "consistent",
- "trailingComma": "es5",
- "endOfLine": "auto",
- "plugins": ["prettier-plugin-tailwindcss"]
-}
diff --git a/electron/handlers/app.ts b/electron/handlers/app.ts
deleted file mode 100644
index c1f431ef3..000000000
--- a/electron/handlers/app.ts
+++ /dev/null
@@ -1,173 +0,0 @@
-import { app, ipcMain, dialog, shell } from 'electron'
-import { join, basename, relative as getRelative, isAbsolute } from 'path'
-import { WindowManager } from './../managers/window'
-import { getResourcePath } from './../utils/path'
-import { AppRoute, AppConfiguration } from '@janhq/core'
-import { ServerConfig, startServer, stopServer } from '@janhq/server'
-import {
- ModuleManager,
- getJanDataFolderPath,
- getJanExtensionsPath,
- init,
- log,
- logServer,
- getAppConfigurations,
- updateAppConfiguration,
-} from '@janhq/core/node'
-
-export function handleAppIPCs() {
- /**
- * Handles the "openAppDirectory" IPC message by opening the app's user data directory.
- * The `shell.openPath` method is used to open the directory in the user's default file explorer.
- * @param _event - The IPC event object.
- */
- ipcMain.handle(AppRoute.openAppDirectory, async (_event) => {
- shell.openPath(getJanDataFolderPath())
- })
-
- /**
- * Opens a URL in the user's default browser.
- * @param _event - The IPC event object.
- * @param url - The URL to open.
- */
- ipcMain.handle(AppRoute.openExternalUrl, async (_event, url) => {
- shell.openExternal(url)
- })
-
- /**
- * Opens a URL in the user's default browser.
- * @param _event - The IPC event object.
- * @param url - The URL to open.
- */
- ipcMain.handle(AppRoute.openFileExplore, async (_event, url) => {
- shell.openPath(url)
- })
-
- /**
- * Joins multiple paths together, respect to the current OS.
- */
- ipcMain.handle(AppRoute.joinPath, async (_event, paths: string[]) =>
- join(...paths)
- )
-
- /**
- * Checks if the given path is a subdirectory of the given directory.
- *
- * @param _event - The IPC event object.
- * @param from - The path to check.
- * @param to - The directory to check against.
- *
- * @returns {Promise} - A promise that resolves with the result.
- */
- ipcMain.handle(
- AppRoute.isSubdirectory,
- async (_event, from: string, to: string) => {
- const relative = getRelative(from, to)
- const isSubdir =
- relative && !relative.startsWith('..') && !isAbsolute(relative)
-
- if (isSubdir === '') return false
- else return isSubdir
- }
- )
-
- /**
- * Retrieve basename from given path, respect to the current OS.
- */
- ipcMain.handle(AppRoute.baseName, async (_event, path: string) =>
- basename(path)
- )
-
- /**
- * Start Jan API Server.
- */
- ipcMain.handle(AppRoute.startServer, async (_event, configs?: ServerConfig) =>
- startServer({
- host: configs?.host,
- port: configs?.port,
- isCorsEnabled: configs?.isCorsEnabled,
- isVerboseEnabled: configs?.isVerboseEnabled,
- schemaPath: app.isPackaged
- ? join(getResourcePath(), 'docs', 'openapi', 'jan.yaml')
- : undefined,
- baseDir: app.isPackaged
- ? join(getResourcePath(), 'docs', 'openapi')
- : undefined,
- })
- )
-
- /**
- * Stop Jan API Server.
- */
- ipcMain.handle(AppRoute.stopServer, stopServer)
-
- /**
- * Relaunches the app in production - reload window in development.
- * @param _event - The IPC event object.
- * @param url - The URL to reload.
- */
- ipcMain.handle(AppRoute.relaunch, async (_event) => {
- ModuleManager.instance.clearImportedModules()
-
- if (app.isPackaged) {
- app.relaunch()
- app.exit()
- } else {
- for (const modulePath in ModuleManager.instance.requiredModules) {
- delete require.cache[
- require.resolve(join(getJanExtensionsPath(), modulePath))
- ]
- }
- 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: getJanExtensionsPath(),
- })
- WindowManager.instance.currentWindow?.reload()
- }
- })
-
- /**
- * Log message to log file.
- */
- ipcMain.handle(AppRoute.log, async (_event, message) => log(message))
-
- /**
- * Log message to log file.
- */
- ipcMain.handle(AppRoute.logServer, async (_event, message) =>
- logServer(message)
- )
-
- ipcMain.handle(AppRoute.selectDirectory, async () => {
- const mainWindow = WindowManager.instance.currentWindow
- if (!mainWindow) {
- console.error('No main window found')
- return
- }
- const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow, {
- title: 'Select a folder',
- buttonLabel: 'Select Folder',
- properties: ['openDirectory', 'createDirectory'],
- })
- if (canceled) {
- return
- } else {
- return filePaths[0]
- }
- })
-
- ipcMain.handle(AppRoute.getAppConfigurations, async () =>
- getAppConfigurations()
- )
-
- ipcMain.handle(
- AppRoute.updateAppConfiguration,
- async (_event, appConfiguration: AppConfiguration) => {
- await updateAppConfiguration(appConfiguration)
- }
- )
-}
diff --git a/electron/handlers/common.ts b/electron/handlers/common.ts
new file mode 100644
index 000000000..5a54a92bd
--- /dev/null
+++ b/electron/handlers/common.ts
@@ -0,0 +1,25 @@
+import { Handler, RequestHandler } from '@janhq/core/node'
+import { ipcMain } from 'electron'
+import { WindowManager } from '../managers/window'
+
+export function injectHandler() {
+ const ipcWrapper: Handler = (
+ route: string,
+ listener: (...args: any[]) => any
+ ) => {
+ return ipcMain.handle(route, async (event, ...args: any[]) => {
+ return listener(...args)
+ })
+ }
+
+ const handler = new RequestHandler(
+ ipcWrapper,
+ (channel: string, args: any) => {
+ return WindowManager.instance.currentWindow?.webContents.send(
+ channel,
+ args
+ )
+ }
+ )
+ handler.handle()
+}
diff --git a/electron/handlers/download.ts b/electron/handlers/download.ts
deleted file mode 100644
index 5f1d8371e..000000000
--- a/electron/handlers/download.ts
+++ /dev/null
@@ -1,132 +0,0 @@
-import { ipcMain } from 'electron'
-import { resolve } from 'path'
-import { WindowManager } from './../managers/window'
-import request from 'request'
-import { createWriteStream, renameSync } from 'fs'
-import { DownloadEvent, DownloadRoute } from '@janhq/core'
-const progress = require('request-progress')
-import {
- DownloadManager,
- getJanDataFolderPath,
- normalizeFilePath,
-} from '@janhq/core/node'
-
-export function handleDownloaderIPCs() {
- /**
- * Handles the "pauseDownload" IPC message by pausing the download associated with the provided fileName.
- * @param _event - The IPC event object.
- * @param fileName - The name of the file being downloaded.
- */
- ipcMain.handle(DownloadRoute.pauseDownload, async (_event, fileName) => {
- DownloadManager.instance.networkRequests[fileName]?.pause()
- })
-
- /**
- * Handles the "resumeDownload" IPC message by resuming the download associated with the provided fileName.
- * @param _event - The IPC event object.
- * @param fileName - The name of the file being downloaded.
- */
- ipcMain.handle(DownloadRoute.resumeDownload, async (_event, fileName) => {
- DownloadManager.instance.networkRequests[fileName]?.resume()
- })
-
- /**
- * Handles the "abortDownload" IPC message by aborting the download associated with the provided fileName.
- * The network request associated with the fileName is then removed from the networkRequests object.
- * @param _event - The IPC event object.
- * @param fileName - The name of the file being downloaded.
- */
- ipcMain.handle(DownloadRoute.abortDownload, async (_event, fileName) => {
- const rq = DownloadManager.instance.networkRequests[fileName]
- if (rq) {
- DownloadManager.instance.networkRequests[fileName] = undefined
- rq?.abort()
- } else {
- WindowManager?.instance.currentWindow?.webContents.send(
- DownloadEvent.onFileDownloadError,
- {
- fileName,
- err: { message: 'aborted' },
- }
- )
- }
- })
-
- /**
- * Downloads a file from a given URL.
- * @param _event - The IPC event object.
- * @param url - The URL to download the file from.
- * @param fileName - The name to give the downloaded file.
- */
- ipcMain.handle(
- DownloadRoute.downloadFile,
- async (_event, url, localPath, network) => {
- const strictSSL = !network?.ignoreSSL
- const proxy = network?.proxy?.startsWith('http')
- ? network.proxy
- : undefined
- if (typeof localPath === 'string') {
- localPath = normalizeFilePath(localPath)
- }
- const array = localPath.split('/')
- const fileName = array.pop() ?? ''
- const modelId = array.pop() ?? ''
-
- const destination = resolve(getJanDataFolderPath(), localPath)
- const rq = request({ url, strictSSL, proxy })
-
- // Put request to download manager instance
- DownloadManager.instance.setRequest(localPath, rq)
-
- // Downloading file to a temp file first
- const downloadingTempFile = `${destination}.download`
-
- progress(rq, {})
- .on('progress', function (state: any) {
- WindowManager?.instance.currentWindow?.webContents.send(
- DownloadEvent.onFileDownloadUpdate,
- {
- ...state,
- fileName,
- modelId,
- }
- )
- })
- .on('error', function (err: Error) {
- WindowManager?.instance.currentWindow?.webContents.send(
- DownloadEvent.onFileDownloadError,
- {
- fileName,
- err,
- modelId,
- }
- )
- })
- .on('end', function () {
- if (DownloadManager.instance.networkRequests[localPath]) {
- // Finished downloading, rename temp file to actual file
- renameSync(downloadingTempFile, destination)
-
- WindowManager?.instance.currentWindow?.webContents.send(
- DownloadEvent.onFileDownloadSuccess,
- {
- fileName,
- modelId,
- }
- )
- DownloadManager.instance.setRequest(localPath, undefined)
- } else {
- WindowManager?.instance.currentWindow?.webContents.send(
- DownloadEvent.onFileDownloadError,
- {
- fileName,
- modelId,
- err: { message: 'aborted' },
- }
- )
- }
- })
- .pipe(createWriteStream(downloadingTempFile))
- }
- )
-}
diff --git a/electron/handlers/extension.ts b/electron/handlers/extension.ts
deleted file mode 100644
index 763c4cdec..000000000
--- a/electron/handlers/extension.ts
+++ /dev/null
@@ -1,104 +0,0 @@
-import { ipcMain, webContents } from 'electron'
-import { readdirSync } from 'fs'
-import { join, extname } from 'path'
-
-import {
- installExtensions,
- getExtension,
- removeExtension,
- getActiveExtensions,
- ModuleManager,
- getJanExtensionsPath,
-} from '@janhq/core/node'
-
-import { getResourcePath } from './../utils/path'
-import { ExtensionRoute } from '@janhq/core'
-
-export function handleExtensionIPCs() {
- /**MARK: General handlers */
- /**
- * Invokes a function from a extension module in main node process.
- * @param _event - The IPC event object.
- * @param modulePath - The path to the extension module.
- * @param method - The name of the function to invoke.
- * @param args - The arguments to pass to the function.
- * @returns The result of the invoked function.
- */
- ipcMain.handle(
- ExtensionRoute.invokeExtensionFunc,
- async (_event, modulePath, method, ...args) => {
- const module = require(
- /* webpackIgnore: true */ join(getJanExtensionsPath(), modulePath)
- )
- ModuleManager.instance.setModule(modulePath, module)
-
- if (typeof module[method] === 'function') {
- return module[method](...args)
- } else {
- console.debug(module[method])
- console.error(`Function "${method}" does not exist in the module.`)
- }
- }
- )
-
- /**
- * Returns the paths of the base extensions.
- * @param _event - The IPC event object.
- * @returns An array of paths to the base extensions.
- */
- ipcMain.handle(ExtensionRoute.baseExtensions, async (_event) => {
- const baseExtensionPath = join(getResourcePath(), 'pre-install')
- return readdirSync(baseExtensionPath)
- .filter((file) => extname(file) === '.tgz')
- .map((file) => join(baseExtensionPath, file))
- })
-
- /**MARK: Extension Manager handlers */
- ipcMain.handle(ExtensionRoute.installExtension, async (e, extensions) => {
- // Install and activate all provided extensions
- const installed = await installExtensions(extensions)
- return JSON.parse(JSON.stringify(installed))
- })
-
- // Register IPC route to uninstall a extension
- ipcMain.handle(
- ExtensionRoute.uninstallExtension,
- async (e, extensions, reload) => {
- // Uninstall all provided extensions
- for (const ext of extensions) {
- const extension = getExtension(ext)
- await extension.uninstall()
- if (extension.name) removeExtension(extension.name)
- }
-
- // Reload all renderer pages if needed
- reload && webContents.getAllWebContents().forEach((wc) => wc.reload())
- return true
- }
- )
-
- // Register IPC route to update a extension
- ipcMain.handle(
- ExtensionRoute.updateExtension,
- async (e, extensions, reload) => {
- // Update all provided extensions
- const updated: any[] = []
- for (const ext of extensions) {
- const extension = getExtension(ext)
- const res = await extension.update()
- if (res) updated.push(extension)
- }
-
- // Reload all renderer pages if needed
- if (updated.length && reload)
- webContents.getAllWebContents().forEach((wc) => wc.reload())
-
- return JSON.parse(JSON.stringify(updated))
- }
- )
-
- // Register IPC route to get the list of active extensions
- ipcMain.handle(ExtensionRoute.getActiveExtensions, () => {
- return JSON.parse(JSON.stringify(getActiveExtensions()))
- })
-}
diff --git a/electron/handlers/fileManager.ts b/electron/handlers/fileManager.ts
deleted file mode 100644
index 15c371d34..000000000
--- a/electron/handlers/fileManager.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import { ipcMain, app } from 'electron'
-// @ts-ignore
-import reflect from '@alumna/reflect'
-
-import { FileManagerRoute, FileStat } from '@janhq/core'
-import { getResourcePath } from './../utils/path'
-import fs from 'fs'
-import { join } from 'path'
-import { getJanDataFolderPath, normalizeFilePath } from '@janhq/core/node'
-
-/**
- * Handles file system extensions operations.
- */
-export function handleFileMangerIPCs() {
- // Handles the 'syncFile' IPC event. This event is triggered to synchronize a file from a source path to a destination path.
- ipcMain.handle(
- FileManagerRoute.syncFile,
- async (_event, src: string, dest: string) => {
- return reflect({
- src,
- dest,
- recursive: true,
- delete: false,
- overwrite: true,
- errorOnExist: false,
- })
- }
- )
-
- // Handles the 'getJanDataFolderPath' IPC event. This event is triggered to get the user space path.
- ipcMain.handle(
- FileManagerRoute.getJanDataFolderPath,
- (): Promise => Promise.resolve(getJanDataFolderPath())
- )
-
- // Handles the 'getResourcePath' IPC event. This event is triggered to get the resource path.
- ipcMain.handle(FileManagerRoute.getResourcePath, async (_event) =>
- getResourcePath()
- )
-
- // Handles the 'getUserHomePath' IPC event. This event is triggered to get the user home path.
- ipcMain.handle(FileManagerRoute.getUserHomePath, async (_event) =>
- app.getPath('home')
- )
-
- // handle fs is directory here
- ipcMain.handle(
- FileManagerRoute.fileStat,
- async (_event, path: string): Promise => {
- const normalizedPath = normalizeFilePath(path)
-
- const fullPath = join(getJanDataFolderPath(), normalizedPath)
- const isExist = fs.existsSync(fullPath)
- if (!isExist) return undefined
-
- const isDirectory = fs.lstatSync(fullPath).isDirectory()
- const size = fs.statSync(fullPath).size
-
- const fileStat: FileStat = {
- isDirectory,
- size,
- }
-
- return fileStat
- }
- )
-
- ipcMain.handle(
- FileManagerRoute.writeBlob,
- async (_event, path: string, data: string): Promise => {
- try {
- const normalizedPath = normalizeFilePath(path)
- const dataBuffer = Buffer.from(data, 'base64')
- fs.writeFileSync(
- join(getJanDataFolderPath(), normalizedPath),
- dataBuffer
- )
- } catch (err) {
- console.error(`writeFile ${path} result: ${err}`)
- }
- }
- )
-}
diff --git a/electron/handlers/fs.ts b/electron/handlers/fs.ts
deleted file mode 100644
index 8ac575cb2..000000000
--- a/electron/handlers/fs.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { ipcMain } from 'electron'
-
-import { getJanDataFolderPath, normalizeFilePath } from '@janhq/core/node'
-import { FileSystemRoute } from '@janhq/core'
-import { join } from 'path'
-/**
- * Handles file system operations.
- */
-export function handleFsIPCs() {
- 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.startsWith(`file:/`) || arg.startsWith(`file:\\`))
- ? join(getJanDataFolderPath(), normalizeFilePath(arg))
- : arg
- )
- )
- )
- })
- })
-}
diff --git a/electron/handlers/native.ts b/electron/handlers/native.ts
new file mode 100644
index 000000000..14ead07bd
--- /dev/null
+++ b/electron/handlers/native.ts
@@ -0,0 +1,86 @@
+import { app, ipcMain, dialog, shell } from 'electron'
+import { join } from 'path'
+import { WindowManager } from '../managers/window'
+import {
+ ModuleManager,
+ getJanDataFolderPath,
+ getJanExtensionsPath,
+ init,
+} from '@janhq/core/node'
+import { NativeRoute } from '@janhq/core'
+
+export function handleAppIPCs() {
+ /**
+ * Handles the "openAppDirectory" IPC message by opening the app's user data directory.
+ * The `shell.openPath` method is used to open the directory in the user's default file explorer.
+ * @param _event - The IPC event object.
+ */
+ ipcMain.handle(NativeRoute.openAppDirectory, async (_event) => {
+ shell.openPath(getJanDataFolderPath())
+ })
+
+ /**
+ * Opens a URL in the user's default browser.
+ * @param _event - The IPC event object.
+ * @param url - The URL to open.
+ */
+ ipcMain.handle(NativeRoute.openExternalUrl, async (_event, url) => {
+ shell.openExternal(url)
+ })
+
+ /**
+ * Opens a URL in the user's default browser.
+ * @param _event - The IPC event object.
+ * @param url - The URL to open.
+ */
+ ipcMain.handle(NativeRoute.openFileExplore, async (_event, url) => {
+ shell.openPath(url)
+ })
+
+ /**
+ * Relaunches the app in production - reload window in development.
+ * @param _event - The IPC event object.
+ * @param url - The URL to reload.
+ */
+ ipcMain.handle(NativeRoute.relaunch, async (_event) => {
+ ModuleManager.instance.clearImportedModules()
+
+ if (app.isPackaged) {
+ app.relaunch()
+ app.exit()
+ } else {
+ for (const modulePath in ModuleManager.instance.requiredModules) {
+ delete require.cache[
+ require.resolve(join(getJanExtensionsPath(), modulePath))
+ ]
+ }
+ 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: getJanExtensionsPath(),
+ })
+ WindowManager.instance.currentWindow?.reload()
+ }
+ })
+
+ ipcMain.handle(NativeRoute.selectDirectory, async () => {
+ const mainWindow = WindowManager.instance.currentWindow
+ if (!mainWindow) {
+ console.error('No main window found')
+ return
+ }
+ const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow, {
+ title: 'Select a folder',
+ buttonLabel: 'Select Folder',
+ properties: ['openDirectory', 'createDirectory'],
+ })
+ if (canceled) {
+ return
+ } else {
+ return filePaths[0]
+ }
+ })
+}
diff --git a/electron/handlers/update.ts b/electron/handlers/update.ts
index cbb34c22b..cfd738f78 100644
--- a/electron/handlers/update.ts
+++ b/electron/handlers/update.ts
@@ -36,7 +36,7 @@ export function handleAppUpdates() {
autoUpdater.on('error', (info: any) => {
WindowManager.instance.currentWindow?.webContents.send(
AppEvent.onAppUpdateDownloadError,
- {}
+ info
)
})
diff --git a/electron/main.ts b/electron/main.ts
index 5d7e59c0f..13e181cdf 100644
--- a/electron/main.ts
+++ b/electron/main.ts
@@ -1,4 +1,4 @@
-import { app, BrowserWindow } from 'electron'
+import { app, BrowserWindow, shell } from 'electron'
import { join } from 'path'
/**
* Managers
@@ -9,12 +9,9 @@ import { log } 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 { injectHandler } from './handlers/common'
import { handleAppUpdates } from './handlers/update'
-import { handleFsIPCs } from './handlers/fs'
+import { handleAppIPCs } from './handlers/native'
/**
* Utils
@@ -25,25 +22,12 @@ import { migrateExtensions } from './utils/migration'
import { cleanUpAndQuit } from './utils/clean'
import { setupExtensions } from './utils/extension'
import { setupCore } from './utils/setup'
+import { setupReactDevTool } from './utils/dev'
+import { cleanLogs } from './utils/log'
app
.whenReady()
- .then(async () => {
- if (!app.isPackaged) {
- // Which means you're running from source code
- const { default: installExtension, REACT_DEVELOPER_TOOLS } = await import(
- 'electron-devtools-installer'
- ) // Don't use import on top level, since the installer package is dev-only
- try {
- const name = installExtension(REACT_DEVELOPER_TOOLS)
- console.log(`Added Extension: ${name}`)
- } catch (err) {
- console.log('An error occurred while installing devtools:')
- console.error(err)
- // Only log the error and don't throw it because it's not critical
- }
- }
- })
+ .then(setupReactDevTool)
.then(setupCore)
.then(createUserSpace)
.then(migrateExtensions)
@@ -59,6 +43,7 @@ app
}
})
})
+ .then(() => cleanLogs())
app.once('window-all-closed', () => {
cleanUpAndQuit()
@@ -92,7 +77,7 @@ function createMainWindow() {
/* Open external links in the default browser */
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
- require('electron').shell.openExternal(url)
+ shell.openExternal(url)
return { action: 'deny' }
})
@@ -104,11 +89,11 @@ function createMainWindow() {
* Handles various IPC messages from the renderer process.
*/
function handleIPCs() {
- handleFsIPCs()
- handleDownloaderIPCs()
- handleExtensionIPCs()
+ // Inject core handlers for IPCs
+ injectHandler()
+
+ // Handle native IPCs
handleAppIPCs()
- handleFileMangerIPCs()
}
/*
diff --git a/electron/merge-latest-ymls.js b/electron/merge-latest-ymls.js
index 8172a3176..ee8caf825 100644
--- a/electron/merge-latest-ymls.js
+++ b/electron/merge-latest-ymls.js
@@ -9,7 +9,9 @@ const file3 = args[2]
// check that all arguments are present and throw error instead
if (!file1 || !file2 || !file3) {
- throw new Error('Please provide 3 file paths as arguments: path to file1, to file2 and destination path')
+ throw new Error(
+ 'Please provide 3 file paths as arguments: path to file1, to file2 and destination path'
+ )
}
const doc1 = yaml.load(fs.readFileSync(file1, 'utf8'))
diff --git a/electron/package.json b/electron/package.json
index 229979b41..deff3826a 100644
--- a/electron/package.json
+++ b/electron/package.json
@@ -11,7 +11,6 @@
"productName": "Jan",
"files": [
"renderer/**/*",
- "build/*.{js,map}",
"build/**/*.{js,map}",
"pre-install",
"models/**/*",
@@ -64,11 +63,11 @@
"build:test:darwin": "tsc -p . && electron-builder -p never -m --dir",
"build:test:win32": "tsc -p . && electron-builder -p never -w --dir",
"build:test:linux": "tsc -p . && electron-builder -p never -l --dir",
- "build:darwin": "tsc -p . && electron-builder -p never -m --x64 --arm64",
+ "build:darwin": "tsc -p . && electron-builder -p never -m",
"build:win32": "tsc -p . && electron-builder -p never -w",
"build:linux": "tsc -p . && electron-builder -p never -l deb -l AppImage",
"build:publish": "yarn copy:assets && run-script-os",
- "build:publish:darwin": "tsc -p . && electron-builder -p always -m --x64 --arm64",
+ "build:publish:darwin": "tsc -p . && electron-builder -p always -m",
"build:publish:win32": "tsc -p . && electron-builder -p always -w",
"build:publish:linux": "tsc -p . && electron-builder -p always -l deb -l AppImage"
},
@@ -77,7 +76,6 @@
"@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",
@@ -86,8 +84,6 @@
"pacote": "^17.0.4",
"request": "^2.88.2",
"request-progress": "^3.0.0",
- "rimraf": "^5.0.5",
- "typescript": "^5.2.2",
"ulid": "^2.3.0",
"use-debounce": "^9.0.4"
},
@@ -96,6 +92,7 @@
"@playwright/test": "^1.38.1",
"@types/npmcli__arborist": "^5.6.4",
"@types/pacote": "^11.1.7",
+ "@types/request": "^2.48.12",
"@typescript-eslint/eslint-plugin": "^6.7.3",
"@typescript-eslint/parser": "^6.7.3",
"electron": "28.0.0",
@@ -103,7 +100,9 @@
"electron-devtools-installer": "^3.2.0",
"electron-playwright-helpers": "^1.6.0",
"eslint-plugin-react": "^7.33.2",
- "run-script-os": "^1.1.6"
+ "rimraf": "^5.0.5",
+ "run-script-os": "^1.1.6",
+ "typescript": "^5.2.2"
},
"installConfig": {
"hoistingLimits": "workspaces"
diff --git a/electron/playwright.config.ts b/electron/playwright.config.ts
index 8047b7513..d3dff40c6 100644
--- a/electron/playwright.config.ts
+++ b/electron/playwright.config.ts
@@ -3,14 +3,12 @@ import { PlaywrightTestConfig } from '@playwright/test'
const config: PlaywrightTestConfig = {
testDir: './tests/e2e',
retries: 0,
- globalTimeout: 300000,
+ globalTimeout: 350000,
use: {
screenshot: 'only-on-failure',
video: 'retain-on-failure',
trace: 'retain-on-failure',
},
-
reporter: [['html', { outputFolder: './playwright-report' }]],
}
-
export default config
diff --git a/electron/sign.js b/electron/sign.js
index 6e973eb6e..73afedc4e 100644
--- a/electron/sign.js
+++ b/electron/sign.js
@@ -1,44 +1,48 @@
-const { exec } = require('child_process');
+const { exec } = require('child_process')
+function sign({
+ path,
+ name,
+ certUrl,
+ clientId,
+ tenantId,
+ clientSecret,
+ certName,
+ timestampServer,
+ version,
+}) {
+ return new Promise((resolve, reject) => {
+ const command = `azuresigntool.exe sign -kvu "${certUrl}" -kvi "${clientId}" -kvt "${tenantId}" -kvs "${clientSecret}" -kvc "${certName}" -tr "${timestampServer}" -v "${path}"`
-function sign({ path, name, certUrl, clientId, tenantId, clientSecret, certName, timestampServer, version }) {
- return new Promise((resolve, reject) => {
-
- const command = `azuresigntool.exe sign -kvu "${certUrl}" -kvi "${clientId}" -kvt "${tenantId}" -kvs "${clientSecret}" -kvc "${certName}" -tr "${timestampServer}" -v "${path}"`;
-
-
- exec(command, (error, stdout, stderr) => {
- if (error) {
- console.error(`Error: ${error}`);
- return reject(error);
- }
- console.log(`stdout: ${stdout}`);
- console.error(`stderr: ${stderr}`);
- resolve();
- });
- });
+ exec(command, (error, stdout, stderr) => {
+ if (error) {
+ console.error(`Error: ${error}`)
+ return reject(error)
+ }
+ console.log(`stdout: ${stdout}`)
+ console.error(`stderr: ${stderr}`)
+ resolve()
+ })
+ })
}
+exports.default = async function (options) {
+ const certUrl = process.env.AZURE_KEY_VAULT_URI
+ const clientId = process.env.AZURE_CLIENT_ID
+ const tenantId = process.env.AZURE_TENANT_ID
+ const clientSecret = process.env.AZURE_CLIENT_SECRET
+ const certName = process.env.AZURE_CERT_NAME
+ const timestampServer = 'http://timestamp.globalsign.com/tsa/r6advanced1'
-exports.default = async function(options) {
-
- const certUrl = process.env.AZURE_KEY_VAULT_URI;
- const clientId = process.env.AZURE_CLIENT_ID;
- const tenantId = process.env.AZURE_TENANT_ID;
- const clientSecret = process.env.AZURE_CLIENT_SECRET;
- const certName = process.env.AZURE_CERT_NAME;
- const timestampServer = 'http://timestamp.globalsign.com/tsa/r6advanced1';
-
-
- await sign({
- path: options.path,
- name: "jan-win-x64",
- certUrl,
- clientId,
- tenantId,
- clientSecret,
- certName,
- timestampServer,
- version: options.version
- });
-};
+ await sign({
+ path: options.path,
+ name: 'jan-win-x64',
+ certUrl,
+ clientId,
+ tenantId,
+ clientSecret,
+ certName,
+ timestampServer,
+ version: options.version,
+ })
+}
diff --git a/electron/tests/config/constants.ts b/electron/tests/config/constants.ts
new file mode 100644
index 000000000..7039ad58c
--- /dev/null
+++ b/electron/tests/config/constants.ts
@@ -0,0 +1,4 @@
+export const Constants = {
+ VIDEO_DIR: './playwright-video',
+ TIMEOUT: '300000',
+}
diff --git a/electron/tests/config/fixtures.ts b/electron/tests/config/fixtures.ts
new file mode 100644
index 000000000..680b09785
--- /dev/null
+++ b/electron/tests/config/fixtures.ts
@@ -0,0 +1,119 @@
+import {
+ _electron as electron,
+ BrowserContext,
+ ElectronApplication,
+ expect,
+ Page,
+ test as base,
+} from '@playwright/test'
+import {
+ ElectronAppInfo,
+ findLatestBuild,
+ parseElectronApp,
+ stubDialog,
+} from 'electron-playwright-helpers'
+import { Constants } from './constants'
+import { HubPage } from '../pages/hubPage'
+import { CommonActions } from '../pages/commonActions'
+
+export let electronApp: ElectronApplication
+export let page: Page
+export let appInfo: ElectronAppInfo
+export const TIMEOUT = parseInt(process.env.TEST_TIMEOUT || Constants.TIMEOUT)
+
+export async function setupElectron() {
+ process.env.CI = 'e2e'
+
+ const latestBuild = findLatestBuild('dist')
+ expect(latestBuild).toBeTruthy()
+
+ // parse the packaged Electron app and find paths and other info
+ appInfo = parseElectronApp(latestBuild)
+ expect(appInfo).toBeTruthy()
+
+ electronApp = await electron.launch({
+ args: [appInfo.main], // main file from package.json
+ executablePath: appInfo.executable, // path to the Electron executable
+ // recordVideo: { dir: Constants.VIDEO_DIR }, // Specify the directory for video recordings
+ })
+ await stubDialog(electronApp, 'showMessageBox', { response: 1 })
+
+ page = await electronApp.firstWindow({
+ timeout: TIMEOUT,
+ })
+}
+
+export async function teardownElectron() {
+ await page.close()
+ await electronApp.close()
+}
+
+/**
+ * this fixture is needed to record and attach videos / screenshot on failed tests when
+ * tests are run in serial mode (i.e. browser is not closed between tests)
+ */
+export const test = base.extend<
+ {
+ commonActions: CommonActions
+ hubPage: HubPage
+ attachVideoPage: Page
+ attachScreenshotsToReport: void
+ },
+ { createVideoContext: BrowserContext }
+>({
+ commonActions: async ({ request }, use, testInfo) => {
+ await use(new CommonActions(page, testInfo))
+ },
+ hubPage: async ({ commonActions }, use) => {
+ await use(new HubPage(page, commonActions))
+ },
+ createVideoContext: [
+ async ({ playwright }, use) => {
+ const context = electronApp.context()
+ await use(context)
+ },
+ { scope: 'worker' },
+ ],
+
+ attachVideoPage: [
+ async ({ createVideoContext }, use, testInfo) => {
+ await use(page)
+
+ if (testInfo.status !== testInfo.expectedStatus) {
+ const path = await createVideoContext.pages()[0].video()?.path()
+ await createVideoContext.close()
+ await testInfo.attach('video', {
+ path: path,
+ })
+ }
+ },
+ { scope: 'test', auto: true },
+ ],
+
+ attachScreenshotsToReport: [
+ async ({ commonActions }, use, testInfo) => {
+ await use()
+
+ // After the test, we can check whether the test passed or failed.
+ if (testInfo.status !== testInfo.expectedStatus) {
+ await commonActions.takeScreenshot('')
+ }
+ },
+ { auto: true },
+ ],
+})
+
+test.setTimeout(TIMEOUT)
+
+test.beforeAll(async () => {
+ await setupElectron()
+ await page.waitForSelector('img[alt="Jan - Logo"]', {
+ state: 'visible',
+ timeout: TIMEOUT,
+ })
+})
+
+test.afterAll(async () => {
+ // temporally disabling this due to the config for parallel testing WIP
+ // teardownElectron()
+})
diff --git a/electron/tests/e2e/hub.e2e.spec.ts b/electron/tests/e2e/hub.e2e.spec.ts
index 68632058e..d968e7641 100644
--- a/electron/tests/e2e/hub.e2e.spec.ts
+++ b/electron/tests/e2e/hub.e2e.spec.ts
@@ -1,34 +1,19 @@
-import {
- page,
- test,
- setupElectron,
- teardownElectron,
- TIMEOUT,
-} from '../pages/basePage'
+import { test, appInfo } from '../config/fixtures'
import { expect } from '@playwright/test'
test.beforeAll(async () => {
- const appInfo = await setupElectron()
- expect(appInfo.asar).toBe(true)
- expect(appInfo.executable).toBeTruthy()
- expect(appInfo.main).toBeTruthy()
- expect(appInfo.name).toBe('jan')
- expect(appInfo.packageJson).toBeTruthy()
- expect(appInfo.packageJson.name).toBe('jan')
- expect(appInfo.platform).toBeTruthy()
- expect(appInfo.platform).toBe(process.platform)
- expect(appInfo.resourcesDir).toBeTruthy()
-})
-
-test.afterAll(async () => {
- await teardownElectron()
-})
-
-test('explores hub', async () => {
- await page.getByTestId('Hub').first().click({
- timeout: TIMEOUT,
- })
- await page.getByTestId('hub-container-test-id').isVisible({
- timeout: TIMEOUT,
+ expect(appInfo).toMatchObject({
+ asar: true,
+ executable: expect.anything(),
+ main: expect.anything(),
+ name: 'jan',
+ packageJson: expect.objectContaining({ name: 'jan' }),
+ platform: process.platform,
+ resourcesDir: expect.anything(),
})
})
+
+test('explores hub', async ({ hubPage }) => {
+ await hubPage.navigateByMenu()
+ await hubPage.verifyContainerVisible()
+})
diff --git a/electron/tests/e2e/navigation.e2e.spec.ts b/electron/tests/e2e/navigation.e2e.spec.ts
index 2da59953c..66924ce78 100644
--- a/electron/tests/e2e/navigation.e2e.spec.ts
+++ b/electron/tests/e2e/navigation.e2e.spec.ts
@@ -1,19 +1,5 @@
import { expect } from '@playwright/test'
-import {
- page,
- setupElectron,
- TIMEOUT,
- test,
- teardownElectron,
-} from '../pages/basePage'
-
-test.beforeAll(async () => {
- await setupElectron()
-})
-
-test.afterAll(async () => {
- await teardownElectron()
-})
+import { page, test, TIMEOUT } from '../config/fixtures'
test('renders left navigation panel', async () => {
const systemMonitorBtn = await page
diff --git a/electron/tests/e2e/settings.e2e.spec.ts b/electron/tests/e2e/settings.e2e.spec.ts
index 54215d9b1..06b4d1acc 100644
--- a/electron/tests/e2e/settings.e2e.spec.ts
+++ b/electron/tests/e2e/settings.e2e.spec.ts
@@ -1,23 +1,11 @@
import { expect } from '@playwright/test'
-import {
- setupElectron,
- teardownElectron,
- test,
- page,
- TIMEOUT,
-} from '../pages/basePage'
-
-test.beforeAll(async () => {
- await setupElectron()
-})
-
-test.afterAll(async () => {
- await teardownElectron()
-})
+import { test, page, TIMEOUT } from '../config/fixtures'
test('shows settings', async () => {
- await page.getByTestId('Settings').first().click({ timeout: TIMEOUT })
+ await page.getByTestId('Settings').first().click({
+ timeout: TIMEOUT,
+ })
const settingDescription = page.getByTestId('testid-setting-description')
await expect(settingDescription).toBeVisible({ timeout: TIMEOUT })
})
diff --git a/electron/tests/pages/basePage.ts b/electron/tests/pages/basePage.ts
index 5f1a6fca1..4e16a3c23 100644
--- a/electron/tests/pages/basePage.ts
+++ b/electron/tests/pages/basePage.ts
@@ -1,67 +1,49 @@
-import {
- expect,
- test as base,
- _electron as electron,
- ElectronApplication,
- Page,
-} from '@playwright/test'
-import {
- findLatestBuild,
- parseElectronApp,
- stubDialog,
-} from 'electron-playwright-helpers'
+import { Page, expect } from '@playwright/test'
+import { CommonActions } from './commonActions'
+import { TIMEOUT } from '../config/fixtures'
-export const TIMEOUT: number = parseInt(process.env.TEST_TIMEOUT || '300000')
+export class BasePage {
+ menuId: string
-export let electronApp: ElectronApplication
-export let page: Page
+ constructor(
+ protected readonly page: Page,
+ readonly action: CommonActions,
+ protected containerId: string
+ ) {}
-export async function setupElectron() {
- process.env.CI = 'e2e'
+ public getValue(key: string) {
+ return this.action.getValue(key)
+ }
- const latestBuild = findLatestBuild('dist')
- expect(latestBuild).toBeTruthy()
+ public setValue(key: string, value: string) {
+ this.action.setValue(key, value)
+ }
- // parse the packaged Electron app and find paths and other info
- const appInfo = parseElectronApp(latestBuild)
- expect(appInfo).toBeTruthy()
+ async takeScreenshot(name: string = '') {
+ await this.action.takeScreenshot(name)
+ }
- electronApp = await electron.launch({
- args: [appInfo.main], // main file from package.json
- executablePath: appInfo.executable, // path to the Electron executable
- })
- await stubDialog(electronApp, 'showMessageBox', { response: 1 })
+ async navigateByMenu() {
+ await this.page.getByTestId(this.menuId).first().click()
+ }
- page = await electronApp.firstWindow({
- timeout: TIMEOUT,
- })
- // Return appInfo for future use
- return appInfo
+ async verifyContainerVisible() {
+ const container = this.page.getByTestId(this.containerId)
+ expect(container.isVisible()).toBeTruthy()
+ }
+
+ async waitUpdateLoader() {
+ await this.isElementVisible('img[alt="Jan - Logo"]')
+ }
+
+ //wait and find a specific element with it's selector and return Visible
+ async isElementVisible(selector: any) {
+ let isVisible = true
+ await this.page
+ .waitForSelector(selector, { state: 'visible', timeout: TIMEOUT })
+ .catch(() => {
+ isVisible = false
+ })
+ return isVisible
+ }
}
-
-export async function teardownElectron() {
- await page.close()
- await electronApp.close()
-}
-
-export const test = base.extend<{
- attachScreenshotsToReport: void
-}>({
- attachScreenshotsToReport: [
- async ({ request }, use, testInfo) => {
- await use()
-
- // After the test, we can check whether the test passed or failed.
- if (testInfo.status !== testInfo.expectedStatus) {
- const screenshot = await page.screenshot()
- await testInfo.attach('screenshot', {
- body: screenshot,
- contentType: 'image/png',
- })
- }
- },
- { auto: true },
- ],
-})
-
-test.setTimeout(TIMEOUT)
diff --git a/electron/tests/pages/commonActions.ts b/electron/tests/pages/commonActions.ts
new file mode 100644
index 000000000..08ea15f92
--- /dev/null
+++ b/electron/tests/pages/commonActions.ts
@@ -0,0 +1,34 @@
+import { Page, TestInfo } from '@playwright/test'
+import { page } from '../config/fixtures'
+
+export class CommonActions {
+ private testData = new Map()
+
+ constructor(
+ public page: Page,
+ public testInfo: TestInfo
+ ) {}
+
+ async takeScreenshot(name: string) {
+ const screenshot = await page.screenshot({
+ fullPage: true,
+ })
+ const attachmentName = `${this.testInfo.title}_${name || new Date().toISOString().slice(5, 19).replace(/[-:]/g, '').replace('T', '_')}`
+ await this.testInfo.attach(attachmentName.replace(/\s+/g, ''), {
+ body: screenshot,
+ contentType: 'image/png',
+ })
+ }
+
+ async hooks() {
+ console.log('hook from the scenario page')
+ }
+
+ setValue(key: string, value: string) {
+ this.testData.set(key, value)
+ }
+
+ getValue(key: string) {
+ return this.testData.get(key)
+ }
+}
diff --git a/electron/tests/pages/hubPage.ts b/electron/tests/pages/hubPage.ts
new file mode 100644
index 000000000..0299ab15d
--- /dev/null
+++ b/electron/tests/pages/hubPage.ts
@@ -0,0 +1,15 @@
+import { Page } from '@playwright/test'
+import { BasePage } from './basePage'
+import { CommonActions } from './commonActions'
+
+export class HubPage extends BasePage {
+ readonly menuId: string = 'Hub'
+ static readonly containerId: string = 'hub-container-test-id'
+
+ constructor(
+ public page: Page,
+ readonly action: CommonActions
+ ) {
+ super(page, action, HubPage.containerId)
+ }
+}
diff --git a/electron/utils/dev.ts b/electron/utils/dev.ts
new file mode 100644
index 000000000..b2a492886
--- /dev/null
+++ b/electron/utils/dev.ts
@@ -0,0 +1,18 @@
+import { app } from 'electron'
+
+export const setupReactDevTool = async () => {
+ if (!app.isPackaged) {
+ // Which means you're running from source code
+ const { default: installExtension, REACT_DEVELOPER_TOOLS } = await import(
+ 'electron-devtools-installer'
+ ) // Don't use import on top level, since the installer package is dev-only
+ try {
+ const name = await installExtension(REACT_DEVELOPER_TOOLS)
+ console.log(`Added Extension: ${name}`)
+ } catch (err) {
+ console.log('An error occurred while installing devtools:')
+ console.error(err)
+ // Only log the error and don't throw it because it's not critical
+ }
+ }
+}
diff --git a/electron/utils/disposable.ts b/electron/utils/disposable.ts
index 462f7e3e5..59018a775 100644
--- a/electron/utils/disposable.ts
+++ b/electron/utils/disposable.ts
@@ -1,8 +1,8 @@
export function dispose(requiredModules: Record) {
for (const key in requiredModules) {
- const module = requiredModules[key];
- if (typeof module["dispose"] === "function") {
- module["dispose"]();
+ const module = requiredModules[key]
+ if (typeof module['dispose'] === 'function') {
+ module['dispose']()
}
}
}
diff --git a/electron/utils/log.ts b/electron/utils/log.ts
new file mode 100644
index 000000000..84c185d75
--- /dev/null
+++ b/electron/utils/log.ts
@@ -0,0 +1,67 @@
+import { getJanDataFolderPath } from '@janhq/core/node'
+import * as fs from 'fs'
+import * as path from 'path'
+
+export function cleanLogs(
+ maxFileSizeBytes?: number | undefined,
+ daysToKeep?: number | undefined,
+ delayMs?: number | undefined
+): void {
+ const size = maxFileSizeBytes ?? 1 * 1024 * 1024 // 1 MB
+ const days = daysToKeep ?? 7 // 7 days
+ const delays = delayMs ?? 10000 // 10 seconds
+ const logDirectory = path.join(getJanDataFolderPath(), 'logs')
+
+ // Perform log cleaning
+ const currentDate = new Date()
+ fs.readdir(logDirectory, (err, files) => {
+ if (err) {
+ console.error('Error reading log directory:', err)
+ return
+ }
+
+ files.forEach((file) => {
+ const filePath = path.join(logDirectory, file)
+ fs.stat(filePath, (err, stats) => {
+ if (err) {
+ console.error('Error getting file stats:', err)
+ return
+ }
+
+ // Check size
+ if (stats.size > size) {
+ fs.unlink(filePath, (err) => {
+ if (err) {
+ console.error('Error deleting log file:', err)
+ return
+ }
+ console.log(
+ `Deleted log file due to exceeding size limit: ${filePath}`
+ )
+ })
+ } else {
+ // Check age
+ const creationDate = new Date(stats.ctime)
+ const daysDifference = Math.floor(
+ (currentDate.getTime() - creationDate.getTime()) /
+ (1000 * 3600 * 24)
+ )
+ if (daysDifference > days) {
+ fs.unlink(filePath, (err) => {
+ if (err) {
+ console.error('Error deleting log file:', err)
+ return
+ }
+ console.log(`Deleted old log file: ${filePath}`)
+ })
+ }
+ }
+ })
+ })
+ })
+
+ // Schedule the next execution with doubled delays
+ setTimeout(() => {
+ cleanLogs(maxFileSizeBytes, daysToKeep, delays * 2)
+ }, delays)
+}
diff --git a/electron/utils/menu.ts b/electron/utils/menu.ts
index 7721b7c78..4825991ee 100644
--- a/electron/utils/menu.ts
+++ b/electron/utils/menu.ts
@@ -1,8 +1,7 @@
// @ts-nocheck
-import { app, Menu, dialog, shell } from 'electron'
+import { app, Menu, shell } from 'electron'
const isMac = process.platform === 'darwin'
import { autoUpdater } from 'electron-updater'
-import { compareSemanticVersions } from './versionDiff'
const template: (Electron.MenuItemConstructorOptions | Electron.MenuItem)[] = [
{
diff --git a/electron/utils/path.ts b/electron/utils/path.ts
index 4e47cc312..4438156bc 100644
--- a/electron/utils/path.ts
+++ b/electron/utils/path.ts
@@ -1,5 +1,3 @@
-import { join } from 'path'
-import { app } from 'electron'
import { mkdir } from 'fs-extra'
import { existsSync } from 'fs'
import { getJanDataFolderPath } from '@janhq/core/node'
@@ -16,13 +14,3 @@ export async function createUserSpace(): Promise {
}
}
}
-
-export function getResourcePath() {
- let appPath = join(app.getAppPath(), '..', 'app.asar.unpacked')
-
- if (!app.isPackaged) {
- // for development mode
- appPath = join(__dirname, '..', '..')
- }
- return appPath
-}
diff --git a/electron/utils/setup.ts b/electron/utils/setup.ts
index 887c3c2b7..01b0b31da 100644
--- a/electron/utils/setup.ts
+++ b/electron/utils/setup.ts
@@ -1,9 +1,9 @@
import { app } from 'electron'
export const setupCore = async () => {
- // Setup core api for main process
- global.core = {
- // Define appPath function for app to retrieve app path globaly
- appPath: () => app.getPath('userData')
- }
-}
\ No newline at end of file
+ // Setup core api for main process
+ global.core = {
+ // Define appPath function for app to retrieve app path globaly
+ appPath: () => app.getPath('userData'),
+ }
+}
diff --git a/electron/utils/versionDiff.ts b/electron/utils/versionDiff.ts
deleted file mode 100644
index 25934e87f..000000000
--- a/electron/utils/versionDiff.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-export const compareSemanticVersions = (a: string, b: string) => {
-
- // 1. Split the strings into their parts.
- const a1 = a.split('.');
- const b1 = b.split('.');
- // 2. Contingency in case there's a 4th or 5th version
- const len = Math.min(a1.length, b1.length);
- // 3. Look through each version number and compare.
- for (let i = 0; i < len; i++) {
- const a2 = +a1[ i ] || 0;
- const b2 = +b1[ i ] || 0;
-
- if (a2 !== b2) {
- return a2 > b2 ? 1 : -1;
- }
- }
-
- // 4. We hit this if the all checked versions so far are equal
- //
- return b1.length - a1.length;
-};
\ No newline at end of file
diff --git a/extensions/assistant-extension/package.json b/extensions/assistant-extension/package.json
index 5f45ecabe..baa858655 100644
--- a/extensions/assistant-extension/package.json
+++ b/extensions/assistant-extension/package.json
@@ -1,13 +1,14 @@
{
"name": "@janhq/assistant-extension",
- "version": "1.0.0",
+ "version": "1.0.1",
"description": "This extension enables assistants, including Jan, a default assistant that can call all downloaded models",
"main": "dist/index.js",
"node": "dist/node/index.js",
"author": "Jan ",
"license": "AGPL-3.0",
"scripts": {
- "build": "tsc --module commonjs && rollup -c rollup.config.ts",
+ "clean:modules": "rimraf node_modules/pdf-parse/test && cd node_modules/pdf-parse/lib/pdf.js && rimraf v1.9.426 v1.10.88 v2.0.550",
+ "build": "yarn clean:modules && tsc --module commonjs && rollup -c rollup.config.ts",
"build:publish:linux": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../pre-install",
"build:publish:darwin": "rimraf *.tgz --glob && npm run build && ../../.github/scripts/auto-sign.sh && npm pack && cpx *.tgz ../../pre-install",
"build:publish:win32": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../pre-install",
@@ -25,7 +26,7 @@
"rollup-plugin-define": "^1.0.1",
"rollup-plugin-sourcemaps": "^0.6.3",
"rollup-plugin-typescript2": "^0.36.0",
- "typescript": "^5.3.3",
+ "typescript": "^5.2.2",
"run-script-os": "^1.1.6"
},
"dependencies": {
@@ -44,9 +45,6 @@
],
"bundleDependencies": [
"@janhq/core",
- "@langchain/community",
- "hnswlib-node",
- "langchain",
- "pdf-parse"
+ "hnswlib-node"
]
}
diff --git a/extensions/assistant-extension/rollup.config.ts b/extensions/assistant-extension/rollup.config.ts
index 7916ef9c8..d3c39cab2 100644
--- a/extensions/assistant-extension/rollup.config.ts
+++ b/extensions/assistant-extension/rollup.config.ts
@@ -1,22 +1,22 @@
-import resolve from "@rollup/plugin-node-resolve";
-import commonjs from "@rollup/plugin-commonjs";
-import sourceMaps from "rollup-plugin-sourcemaps";
-import typescript from "rollup-plugin-typescript2";
-import json from "@rollup/plugin-json";
-import replace from "@rollup/plugin-replace";
+import resolve from '@rollup/plugin-node-resolve'
+import commonjs from '@rollup/plugin-commonjs'
+import sourceMaps from 'rollup-plugin-sourcemaps'
+import typescript from 'rollup-plugin-typescript2'
+import json from '@rollup/plugin-json'
+import replace from '@rollup/plugin-replace'
-const packageJson = require("./package.json");
+const packageJson = require('./package.json')
-const pkg = require("./package.json");
+const pkg = require('./package.json')
export default [
{
input: `src/index.ts`,
- output: [{ file: pkg.main, format: "es", sourcemap: true }],
+ output: [{ file: pkg.main, format: 'es', sourcemap: true }],
// Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash')
external: [],
watch: {
- include: "src/**",
+ include: 'src/**',
},
plugins: [
replace({
@@ -35,7 +35,7 @@ export default [
// which external modules to include in the bundle
// https://github.com/rollup/rollup-plugin-node-resolve#usage
resolve({
- extensions: [".js", ".ts", ".svelte"],
+ extensions: ['.js', '.ts', '.svelte'],
}),
// Resolve source maps to the original source
@@ -44,18 +44,11 @@ export default [
},
{
input: `src/node/index.ts`,
- output: [{ dir: "dist/node", format: "cjs", sourcemap: false }],
+ output: [{ dir: 'dist/node', format: 'cjs', sourcemap: false }],
// Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash')
- external: [
- "@janhq/core/node",
- "@langchain/community",
- "langchain",
- "langsmith",
- "path",
- "hnswlib-node",
- ],
+ external: ['@janhq/core/node', 'path', 'hnswlib-node'],
watch: {
- include: "src/node/**",
+ include: 'src/node/**',
},
// inlineDynamicImports: true,
plugins: [
@@ -71,11 +64,11 @@ export default [
// which external modules to include in the bundle
// https://github.com/rollup/rollup-plugin-node-resolve#usage
resolve({
- extensions: [".ts", ".js", ".json"],
+ extensions: ['.ts', '.js', '.json'],
}),
// Resolve source maps to the original source
// sourceMaps(),
],
},
-];
+]
diff --git a/extensions/assistant-extension/src/@types/global.d.ts b/extensions/assistant-extension/src/@types/global.d.ts
index dc11709a4..bc97157cd 100644
--- a/extensions/assistant-extension/src/@types/global.d.ts
+++ b/extensions/assistant-extension/src/@types/global.d.ts
@@ -1,3 +1,3 @@
-declare const NODE: string;
-declare const EXTENSION_NAME: string;
-declare const VERSION: string;
+declare const NODE: string
+declare const EXTENSION_NAME: string
+declare const VERSION: string
diff --git a/extensions/assistant-extension/src/index.ts b/extensions/assistant-extension/src/index.ts
index 8bc8cafdc..0a5319c8a 100644
--- a/extensions/assistant-extension/src/index.ts
+++ b/extensions/assistant-extension/src/index.ts
@@ -10,145 +10,168 @@ import {
executeOnMain,
AssistantExtension,
AssistantEvent,
-} from "@janhq/core";
+} from '@janhq/core'
export default class JanAssistantExtension extends AssistantExtension {
- private static readonly _homeDir = "file://assistants";
+ private static readonly _homeDir = 'file://assistants'
+ private static readonly _threadDir = 'file://threads'
- controller = new AbortController();
- isCancelled = false;
- retrievalThreadId: string | undefined = undefined;
+ controller = new AbortController()
+ isCancelled = false
+ retrievalThreadId: string | undefined = undefined
async onLoad() {
// making the assistant directory
const assistantDirExist = await fs.existsSync(
JanAssistantExtension._homeDir
- );
+ )
if (
localStorage.getItem(`${EXTENSION_NAME}-version`) !== VERSION ||
!assistantDirExist
) {
- if (!assistantDirExist)
- await fs.mkdirSync(JanAssistantExtension._homeDir);
+ if (!assistantDirExist) await fs.mkdirSync(JanAssistantExtension._homeDir)
// Write assistant metadata
- await this.createJanAssistant();
+ await this.createJanAssistant()
// Finished migration
- localStorage.setItem(`${EXTENSION_NAME}-version`, VERSION);
+ localStorage.setItem(`${EXTENSION_NAME}-version`, VERSION)
// Update the assistant list
- events.emit(AssistantEvent.OnAssistantsUpdate, {});
+ events.emit(AssistantEvent.OnAssistantsUpdate, {})
}
// Events subscription
events.on(MessageEvent.OnMessageSent, (data: MessageRequest) =>
JanAssistantExtension.handleMessageRequest(data, this)
- );
+ )
events.on(InferenceEvent.OnInferenceStopped, () => {
- JanAssistantExtension.handleInferenceStopped(this);
- });
+ JanAssistantExtension.handleInferenceStopped(this)
+ })
}
private static async handleInferenceStopped(instance: JanAssistantExtension) {
- instance.isCancelled = true;
- instance.controller?.abort();
+ instance.isCancelled = true
+ instance.controller?.abort()
}
private static async handleMessageRequest(
data: MessageRequest,
instance: JanAssistantExtension
) {
- instance.isCancelled = false;
- instance.controller = new AbortController();
+ instance.isCancelled = false
+ instance.controller = new AbortController()
if (
data.model?.engine !== InferenceEngine.tool_retrieval_enabled ||
!data.messages ||
+ // TODO: Since the engine is defined, its unsafe to assume that assistant tools are defined
+ // That could lead to an issue where thread stuck at generating response
!data.thread?.assistants[0]?.tools
) {
- return;
+ return
}
- const latestMessage = data.messages[data.messages.length - 1];
+ const latestMessage = data.messages[data.messages.length - 1]
- // Ingest the document if needed
+ // 1. Ingest the document if needed
if (
latestMessage &&
latestMessage.content &&
- typeof latestMessage.content !== "string"
+ typeof latestMessage.content !== 'string' &&
+ latestMessage.content.length > 1
) {
- const docFile = latestMessage.content[1]?.doc_url?.url;
+ const docFile = latestMessage.content[1]?.doc_url?.url
if (docFile) {
await executeOnMain(
NODE,
- "toolRetrievalIngestNewDocument",
+ 'toolRetrievalIngestNewDocument',
docFile,
data.model?.proxyEngine
- );
+ )
}
+ } else if (
+ // Check whether we need to ingest document or not
+ // Otherwise wrong context will be sent
+ !(await fs.existsSync(
+ await joinPath([
+ JanAssistantExtension._threadDir,
+ data.threadId,
+ 'memory',
+ ])
+ ))
+ ) {
+ // No document ingested, reroute the result to inference engine
+ const output = {
+ ...data,
+ model: {
+ ...data.model,
+ engine: data.model.proxyEngine,
+ },
+ }
+ events.emit(MessageEvent.OnMessageSent, output)
+ return
}
-
- // Load agent on thread changed
+ // 2. Load agent on thread changed
if (instance.retrievalThreadId !== data.threadId) {
- await executeOnMain(NODE, "toolRetrievalLoadThreadMemory", data.threadId);
+ await executeOnMain(NODE, 'toolRetrievalLoadThreadMemory', data.threadId)
- instance.retrievalThreadId = data.threadId;
+ instance.retrievalThreadId = data.threadId
// Update the text splitter
await executeOnMain(
NODE,
- "toolRetrievalUpdateTextSplitter",
+ 'toolRetrievalUpdateTextSplitter',
data.thread.assistants[0].tools[0]?.settings?.chunk_size ?? 4000,
data.thread.assistants[0].tools[0]?.settings?.chunk_overlap ?? 200
- );
+ )
}
+ // 3. Using the retrieval template with the result and query
if (latestMessage.content) {
const prompt =
- typeof latestMessage.content === "string"
+ typeof latestMessage.content === 'string'
? latestMessage.content
- : latestMessage.content[0].text;
+ : latestMessage.content[0].text
// Retrieve the result
- console.debug("toolRetrievalQuery", latestMessage.content);
const retrievalResult = await executeOnMain(
NODE,
- "toolRetrievalQueryResult",
+ 'toolRetrievalQueryResult',
prompt
- );
+ )
+ console.debug('toolRetrievalQueryResult', retrievalResult)
- // Update the message content
- // Using the retrieval template with the result and query
- if (data.thread?.assistants[0].tools)
+ // Update message content
+ if (data.thread?.assistants[0]?.tools && retrievalResult)
data.messages[data.messages.length - 1].content =
data.thread.assistants[0].tools[0].settings?.retrieval_template
- ?.replace("{CONTEXT}", retrievalResult)
- .replace("{QUESTION}", prompt);
+ ?.replace('{CONTEXT}', retrievalResult)
+ .replace('{QUESTION}', prompt)
}
// Filter out all the messages that are not text
data.messages = data.messages.map((message) => {
if (
message.content &&
- typeof message.content !== "string" &&
+ typeof message.content !== 'string' &&
(message.content.length ?? 0) > 0
) {
return {
...message,
content: [message.content[0]],
- };
+ }
}
- return message;
- });
+ return message
+ })
- // Reroute the result to inference engine
+ // 4. Reroute the result to inference engine
const output = {
...data,
model: {
...data.model,
engine: data.model.proxyEngine,
},
- };
- events.emit(MessageEvent.OnMessageSent, output);
+ }
+ events.emit(MessageEvent.OnMessageSent, output)
}
/**
@@ -160,107 +183,107 @@ export default class JanAssistantExtension extends AssistantExtension {
const assistantDir = await joinPath([
JanAssistantExtension._homeDir,
assistant.id,
- ]);
- if (!(await fs.existsSync(assistantDir))) await fs.mkdirSync(assistantDir);
+ ])
+ if (!(await fs.existsSync(assistantDir))) await fs.mkdirSync(assistantDir)
// store the assistant metadata json
const assistantMetadataPath = await joinPath([
assistantDir,
- "assistant.json",
- ]);
+ 'assistant.json',
+ ])
try {
await fs.writeFileSync(
assistantMetadataPath,
JSON.stringify(assistant, null, 2)
- );
+ )
} catch (err) {
- console.error(err);
+ console.error(err)
}
}
async getAssistants(): Promise {
// get all the assistant directories
// get all the assistant metadata json
- const results: Assistant[] = [];
+ const results: Assistant[] = []
const allFileName: string[] = await fs.readdirSync(
JanAssistantExtension._homeDir
- );
+ )
for (const fileName of allFileName) {
const filePath = await joinPath([
JanAssistantExtension._homeDir,
fileName,
- ]);
+ ])
- if (filePath.includes(".DS_Store")) continue;
+ if (filePath.includes('.DS_Store')) continue
const jsonFiles: string[] = (await fs.readdirSync(filePath)).filter(
- (file: string) => file === "assistant.json"
- );
+ (file: string) => file === 'assistant.json'
+ )
if (jsonFiles.length !== 1) {
// has more than one assistant file -> ignore
- continue;
+ continue
}
const content = await fs.readFileSync(
await joinPath([filePath, jsonFiles[0]]),
- "utf-8"
- );
+ 'utf-8'
+ )
const assistant: Assistant =
- typeof content === "object" ? content : JSON.parse(content);
+ typeof content === 'object' ? content : JSON.parse(content)
- results.push(assistant);
+ results.push(assistant)
}
- return results;
+ return results
}
async deleteAssistant(assistant: Assistant): Promise {
- if (assistant.id === "jan") {
- return Promise.reject("Cannot delete Jan Assistant");
+ if (assistant.id === 'jan') {
+ return Promise.reject('Cannot delete Jan Assistant')
}
// remove the directory
const assistantDir = await joinPath([
JanAssistantExtension._homeDir,
assistant.id,
- ]);
- await fs.rmdirSync(assistantDir);
- return Promise.resolve();
+ ])
+ await fs.rmdirSync(assistantDir)
+ return Promise.resolve()
}
private async createJanAssistant(): Promise {
const janAssistant: Assistant = {
- avatar: "",
+ avatar: '',
thread_location: undefined,
- id: "jan",
- object: "assistant",
+ id: 'jan',
+ object: 'assistant',
created_at: Date.now(),
- name: "Jan",
- description: "A default assistant that can use all downloaded models",
- model: "*",
- instructions: "",
+ name: 'Jan',
+ description: 'A default assistant that can use all downloaded models',
+ model: '*',
+ instructions: '',
tools: [
{
- type: "retrieval",
+ type: 'retrieval',
enabled: false,
settings: {
top_k: 2,
chunk_size: 1024,
chunk_overlap: 64,
retrieval_template: `Use the following pieces of context to answer the question at the end. If you don't know the answer, just say that you don't know, don't try to make up an answer.
- ----------------
- CONTEXT: {CONTEXT}
- ----------------
- QUESTION: {QUESTION}
- ----------------
- Helpful Answer:`,
+----------------
+CONTEXT: {CONTEXT}
+----------------
+QUESTION: {QUESTION}
+----------------
+Helpful Answer:`,
},
},
],
file_ids: [],
metadata: undefined,
- };
+ }
- await this.createAssistant(janAssistant);
+ await this.createAssistant(janAssistant)
}
}
diff --git a/extensions/assistant-extension/src/node/engine.ts b/extensions/assistant-extension/src/node/engine.ts
index 54b2a6ba1..70d02af1f 100644
--- a/extensions/assistant-extension/src/node/engine.ts
+++ b/extensions/assistant-extension/src/node/engine.ts
@@ -1,13 +1,13 @@
-import fs from "fs";
-import path from "path";
-import { getJanDataFolderPath } from "@janhq/core/node";
+import fs from 'fs'
+import path from 'path'
+import { getJanDataFolderPath } from '@janhq/core/node'
// Sec: Do not send engine settings over requests
// Read it manually instead
export const readEmbeddingEngine = (engineName: string) => {
const engineSettings = fs.readFileSync(
- path.join(getJanDataFolderPath(), "engines", `${engineName}.json`),
- "utf-8",
- );
- return JSON.parse(engineSettings);
-};
+ path.join(getJanDataFolderPath(), 'engines', `${engineName}.json`),
+ 'utf-8'
+ )
+ return JSON.parse(engineSettings)
+}
diff --git a/extensions/assistant-extension/src/node/index.ts b/extensions/assistant-extension/src/node/index.ts
index 95a7243a4..d52a4b23e 100644
--- a/extensions/assistant-extension/src/node/index.ts
+++ b/extensions/assistant-extension/src/node/index.ts
@@ -1,39 +1,39 @@
-import { getJanDataFolderPath, normalizeFilePath } from "@janhq/core/node";
-import { Retrieval } from "./tools/retrieval";
-import path from "path";
+import { getJanDataFolderPath, normalizeFilePath } from '@janhq/core/node'
+import { retrieval } from './tools/retrieval'
+import path from 'path'
-const retrieval = new Retrieval();
-
-export async function toolRetrievalUpdateTextSplitter(
+export function toolRetrievalUpdateTextSplitter(
chunkSize: number,
- chunkOverlap: number,
+ chunkOverlap: number
) {
- retrieval.updateTextSplitter(chunkSize, chunkOverlap);
- return Promise.resolve();
+ retrieval.updateTextSplitter(chunkSize, chunkOverlap)
}
export async function toolRetrievalIngestNewDocument(
file: string,
- engine: string,
+ engine: string
) {
- const filePath = path.join(getJanDataFolderPath(), normalizeFilePath(file));
- const threadPath = path.dirname(filePath.replace("files", ""));
- retrieval.updateEmbeddingEngine(engine);
- await retrieval.ingestAgentKnowledge(filePath, `${threadPath}/memory`);
- return Promise.resolve();
+ const filePath = path.join(getJanDataFolderPath(), normalizeFilePath(file))
+ const threadPath = path.dirname(filePath.replace('files', ''))
+ retrieval.updateEmbeddingEngine(engine)
+ return retrieval
+ .ingestAgentKnowledge(filePath, `${threadPath}/memory`)
+ .catch((err) => {
+ console.error(err)
+ })
}
export async function toolRetrievalLoadThreadMemory(threadId: string) {
- try {
- await retrieval.loadRetrievalAgent(
- path.join(getJanDataFolderPath(), "threads", threadId, "memory"),
- );
- return Promise.resolve();
- } catch (err) {
- console.debug(err);
- }
+ return retrieval
+ .loadRetrievalAgent(
+ path.join(getJanDataFolderPath(), 'threads', threadId, 'memory')
+ )
+ .catch((err) => {
+ console.error(err)
+ })
}
export async function toolRetrievalQueryResult(query: string) {
- const res = await retrieval.generateResult(query);
- return Promise.resolve(res);
+ return retrieval.generateResult(query).catch((err) => {
+ console.error(err)
+ })
}
diff --git a/extensions/assistant-extension/src/node/tools/retrieval/index.ts b/extensions/assistant-extension/src/node/tools/retrieval/index.ts
index 8c7a6aa2b..e58ec0c46 100644
--- a/extensions/assistant-extension/src/node/tools/retrieval/index.ts
+++ b/extensions/assistant-extension/src/node/tools/retrieval/index.ts
@@ -1,77 +1,80 @@
-import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
-import { formatDocumentsAsString } from "langchain/util/document";
-import { PDFLoader } from "langchain/document_loaders/fs/pdf";
+import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter'
+import { formatDocumentsAsString } from 'langchain/util/document'
+import { PDFLoader } from 'langchain/document_loaders/fs/pdf'
-import { HNSWLib } from "langchain/vectorstores/hnswlib";
+import { HNSWLib } from 'langchain/vectorstores/hnswlib'
-import { OpenAIEmbeddings } from "langchain/embeddings/openai";
-import { readEmbeddingEngine } from "../../engine";
+import { OpenAIEmbeddings } from 'langchain/embeddings/openai'
+import { readEmbeddingEngine } from '../../engine'
export class Retrieval {
- public chunkSize: number = 100;
- public chunkOverlap?: number = 0;
- private retriever: any;
+ public chunkSize: number = 100
+ public chunkOverlap?: number = 0
+ private retriever: any
- private embeddingModel?: OpenAIEmbeddings = undefined;
- private textSplitter?: RecursiveCharacterTextSplitter;
+ private embeddingModel?: OpenAIEmbeddings = undefined
+ private textSplitter?: RecursiveCharacterTextSplitter
constructor(chunkSize: number = 4000, chunkOverlap: number = 200) {
- this.updateTextSplitter(chunkSize, chunkOverlap);
+ this.updateTextSplitter(chunkSize, chunkOverlap)
}
public updateTextSplitter(chunkSize: number, chunkOverlap: number): void {
- this.chunkSize = chunkSize;
- this.chunkOverlap = chunkOverlap;
+ this.chunkSize = chunkSize
+ this.chunkOverlap = chunkOverlap
this.textSplitter = new RecursiveCharacterTextSplitter({
chunkSize: chunkSize,
chunkOverlap: chunkOverlap,
- });
+ })
}
public updateEmbeddingEngine(engine: string): void {
// Engine settings are not compatible with the current embedding model params
// Switch case manually for now
- const settings = readEmbeddingEngine(engine);
- if (engine === "nitro") {
+ const settings = readEmbeddingEngine(engine)
+ if (engine === 'nitro') {
this.embeddingModel = new OpenAIEmbeddings(
- { openAIApiKey: "nitro-embedding" },
- { basePath: "http://127.0.0.1:3928/v1" },
- );
+ { openAIApiKey: 'nitro-embedding' },
+ // TODO: Raw settings
+ { basePath: 'http://127.0.0.1:3928/v1' }
+ )
} else {
// Fallback to OpenAI Settings
this.embeddingModel = new OpenAIEmbeddings({
openAIApiKey: settings.api_key,
- });
+ })
}
}
public ingestAgentKnowledge = async (
filePath: string,
- memoryPath: string,
+ memoryPath: string
): Promise => {
const loader = new PDFLoader(filePath, {
splitPages: true,
- });
- if (!this.embeddingModel) return Promise.reject();
- const doc = await loader.load();
- const docs = await this.textSplitter!.splitDocuments(doc);
- const vectorStore = await HNSWLib.fromDocuments(docs, this.embeddingModel);
- return vectorStore.save(memoryPath);
- };
+ })
+ if (!this.embeddingModel) return Promise.reject()
+ const doc = await loader.load()
+ const docs = await this.textSplitter!.splitDocuments(doc)
+ const vectorStore = await HNSWLib.fromDocuments(docs, this.embeddingModel)
+ return vectorStore.save(memoryPath)
+ }
public loadRetrievalAgent = async (memoryPath: string): Promise => {
- if (!this.embeddingModel) return Promise.reject();
- const vectorStore = await HNSWLib.load(memoryPath, this.embeddingModel);
- this.retriever = vectorStore.asRetriever(2);
- return Promise.resolve();
- };
+ if (!this.embeddingModel) return Promise.reject()
+ const vectorStore = await HNSWLib.load(memoryPath, this.embeddingModel)
+ this.retriever = vectorStore.asRetriever(2)
+ return Promise.resolve()
+ }
public generateResult = async (query: string): Promise => {
if (!this.retriever) {
- return Promise.resolve(" ");
+ return Promise.resolve(' ')
}
- const relevantDocs = await this.retriever.getRelevantDocuments(query);
- const serializedDoc = formatDocumentsAsString(relevantDocs);
- return Promise.resolve(serializedDoc);
- };
+ const relevantDocs = await this.retriever.getRelevantDocuments(query)
+ const serializedDoc = formatDocumentsAsString(relevantDocs)
+ return Promise.resolve(serializedDoc)
+ }
}
+
+export const retrieval = new Retrieval()
diff --git a/extensions/assistant-extension/tsconfig.json b/extensions/assistant-extension/tsconfig.json
index d3794cace..e425358c3 100644
--- a/extensions/assistant-extension/tsconfig.json
+++ b/extensions/assistant-extension/tsconfig.json
@@ -14,7 +14,7 @@
"outDir": "dist",
"importHelpers": true,
"typeRoots": ["node_modules/@types"],
- "skipLibCheck": true,
+ "skipLibCheck": true
},
- "include": ["src"],
+ "include": ["src"]
}
diff --git a/extensions/conversational-extension/.prettierrc b/extensions/conversational-extension/.prettierrc
deleted file mode 100644
index 46f1abcb0..000000000
--- a/extensions/conversational-extension/.prettierrc
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "semi": false,
- "singleQuote": true,
- "quoteProps": "consistent",
- "trailingComma": "es5",
- "endOfLine": "auto",
- "plugins": ["prettier-plugin-tailwindcss"]
-}
diff --git a/extensions/conversational-extension/package.json b/extensions/conversational-extension/package.json
index b84c75d3d..8a6da14e5 100644
--- a/extensions/conversational-extension/package.json
+++ b/extensions/conversational-extension/package.json
@@ -17,12 +17,12 @@
"cpx": "^1.5.0",
"rimraf": "^3.0.2",
"webpack": "^5.88.2",
- "webpack-cli": "^5.1.4"
+ "webpack-cli": "^5.1.4",
+ "ts-loader": "^9.5.0"
},
"dependencies": {
"@janhq/core": "file:../../core",
- "path-browserify": "^1.0.1",
- "ts-loader": "^9.5.0"
+ "path-browserify": "^1.0.1"
},
"engines": {
"node": ">=18.0.0"
diff --git a/extensions/conversational-extension/webpack.config.js b/extensions/conversational-extension/webpack.config.js
index 36e338295..a3eb873d7 100644
--- a/extensions/conversational-extension/webpack.config.js
+++ b/extensions/conversational-extension/webpack.config.js
@@ -1,27 +1,27 @@
-const path = require("path");
-const webpack = require("webpack");
+const path = require('path')
+const webpack = require('webpack')
module.exports = {
experiments: { outputModule: true },
- entry: "./src/index.ts", // Adjust the entry point to match your project's main file
- mode: "production",
+ entry: './src/index.ts', // Adjust the entry point to match your project's main file
+ mode: 'production',
module: {
rules: [
{
test: /\.tsx?$/,
- use: "ts-loader",
+ use: 'ts-loader',
exclude: /node_modules/,
},
],
},
output: {
- filename: "index.js", // Adjust the output file name as needed
- path: path.resolve(__dirname, "dist"),
- library: { type: "module" }, // Specify ESM output format
+ filename: 'index.js', // Adjust the output file name as needed
+ path: path.resolve(__dirname, 'dist'),
+ library: { type: 'module' }, // Specify ESM output format
},
plugins: [new webpack.DefinePlugin({})],
resolve: {
- extensions: [".ts", ".js"],
+ extensions: ['.ts', '.js'],
fallback: {
path: require.resolve('path-browserify'),
},
@@ -31,4 +31,4 @@ module.exports = {
minimize: false,
},
// Add loaders and other configuration as needed for your project
-};
+}
diff --git a/extensions/inference-nitro-extension/README.md b/extensions/inference-nitro-extension/README.md
index 455783efb..f499e0b9c 100644
--- a/extensions/inference-nitro-extension/README.md
+++ b/extensions/inference-nitro-extension/README.md
@@ -64,10 +64,10 @@ There are a few things to keep in mind when writing your plugin code:
In `index.ts`, you will see that the extension function will return a `Promise`.
```typescript
- import { core } from "@janhq/core";
+ import { core } from '@janhq/core'
function onStart(): Promise {
- return core.invokePluginFunc(MODULE_PATH, "run", 0);
+ return core.invokePluginFunc(MODULE_PATH, 'run', 0)
}
```
@@ -75,4 +75,3 @@ There are a few things to keep in mind when writing your plugin code:
[documentation](https://github.com/janhq/jan/blob/main/core/README.md).
So, what are you waiting for? Go ahead and start customizing your plugin!
-
diff --git a/extensions/inference-nitro-extension/bin/version.txt b/extensions/inference-nitro-extension/bin/version.txt
index c2c0004f0..940ac09aa 100644
--- a/extensions/inference-nitro-extension/bin/version.txt
+++ b/extensions/inference-nitro-extension/bin/version.txt
@@ -1 +1 @@
-0.3.5
+0.3.9
diff --git a/extensions/inference-nitro-extension/package.json b/extensions/inference-nitro-extension/package.json
index cccfbefd0..b65cf445f 100644
--- a/extensions/inference-nitro-extension/package.json
+++ b/extensions/inference-nitro-extension/package.json
@@ -35,12 +35,12 @@
"rollup-plugin-sourcemaps": "^0.6.3",
"rollup-plugin-typescript2": "^0.36.0",
"run-script-os": "^1.1.6",
- "typescript": "^5.2.2"
+ "typescript": "^5.2.2",
+ "@types/os-utils": "^0.0.4",
+ "@rollup/plugin-replace": "^5.0.5"
},
"dependencies": {
"@janhq/core": "file:../../core",
- "@rollup/plugin-replace": "^5.0.5",
- "@types/os-utils": "^0.0.4",
"fetch-retry": "^5.0.6",
"path-browserify": "^1.0.1",
"rxjs": "^7.8.1",
diff --git a/extensions/inference-nitro-extension/rollup.config.ts b/extensions/inference-nitro-extension/rollup.config.ts
index 77a9fb208..ec8943f9c 100644
--- a/extensions/inference-nitro-extension/rollup.config.ts
+++ b/extensions/inference-nitro-extension/rollup.config.ts
@@ -1,34 +1,34 @@
-import resolve from "@rollup/plugin-node-resolve";
-import commonjs from "@rollup/plugin-commonjs";
-import sourceMaps from "rollup-plugin-sourcemaps";
-import typescript from "rollup-plugin-typescript2";
-import json from "@rollup/plugin-json";
-import replace from "@rollup/plugin-replace";
-const packageJson = require("./package.json");
+import resolve from '@rollup/plugin-node-resolve'
+import commonjs from '@rollup/plugin-commonjs'
+import sourceMaps from 'rollup-plugin-sourcemaps'
+import typescript from 'rollup-plugin-typescript2'
+import json from '@rollup/plugin-json'
+import replace from '@rollup/plugin-replace'
+const packageJson = require('./package.json')
-const pkg = require("./package.json");
+const pkg = require('./package.json')
export default [
{
input: `src/index.ts`,
- output: [{ file: pkg.main, format: "es", sourcemap: true }],
+ output: [{ file: pkg.main, format: 'es', sourcemap: true }],
// Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash')
external: [],
watch: {
- include: "src/**",
+ include: 'src/**',
},
plugins: [
replace({
NODE: JSON.stringify(`${packageJson.name}/${packageJson.node}`),
INFERENCE_URL: JSON.stringify(
process.env.INFERENCE_URL ||
- "http://127.0.0.1:3928/inferences/llamacpp/chat_completion"
+ 'http://127.0.0.1:3928/inferences/llamacpp/chat_completion'
),
TROUBLESHOOTING_URL: JSON.stringify(
- "https://jan.ai/guides/troubleshooting"
+ 'https://jan.ai/guides/troubleshooting'
),
JAN_SERVER_INFERENCE_URL: JSON.stringify(
- "http://localhost:1337/v1/chat/completions"
+ 'http://localhost:1337/v1/chat/completions'
),
}),
// Allow json resolution
@@ -42,7 +42,7 @@ export default [
// which external modules to include in the bundle
// https://github.com/rollup/rollup-plugin-node-resolve#usage
resolve({
- extensions: [".js", ".ts", ".svelte"],
+ extensions: ['.js', '.ts', '.svelte'],
}),
// Resolve source maps to the original source
@@ -52,12 +52,12 @@ export default [
{
input: `src/node/index.ts`,
output: [
- { file: "dist/node/index.cjs.js", format: "cjs", sourcemap: true },
+ { 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: ["@janhq/core/node"],
+ external: ['@janhq/core/node'],
watch: {
- include: "src/node/**",
+ include: 'src/node/**',
},
plugins: [
// Allow json resolution
@@ -70,11 +70,11 @@ export default [
// which external modules to include in the bundle
// https://github.com/rollup/rollup-plugin-node-resolve#usage
resolve({
- extensions: [".ts", ".js", ".json"],
+ extensions: ['.ts', '.js', '.json'],
}),
// Resolve source maps to the original source
sourceMaps(),
],
},
-];
+]
diff --git a/extensions/inference-nitro-extension/src/@types/global.d.ts b/extensions/inference-nitro-extension/src/@types/global.d.ts
index 7a4fb4805..3a3d2aa32 100644
--- a/extensions/inference-nitro-extension/src/@types/global.d.ts
+++ b/extensions/inference-nitro-extension/src/@types/global.d.ts
@@ -1,13 +1,13 @@
-declare const NODE: string;
-declare const INFERENCE_URL: string;
-declare const TROUBLESHOOTING_URL: string;
-declare const JAN_SERVER_INFERENCE_URL: string;
+declare const NODE: string
+declare const INFERENCE_URL: string
+declare const TROUBLESHOOTING_URL: string
+declare const JAN_SERVER_INFERENCE_URL: string
/**
* The response from the initModel function.
* @property error - An error message if the model fails to load.
*/
interface ModelOperationResponse {
- error?: any;
- modelFile?: string;
+ error?: any
+ modelFile?: string
}
diff --git a/extensions/inference-nitro-extension/src/helpers/sse.ts b/extensions/inference-nitro-extension/src/helpers/sse.ts
index aab260828..06176c9b9 100644
--- a/extensions/inference-nitro-extension/src/helpers/sse.ts
+++ b/extensions/inference-nitro-extension/src/helpers/sse.ts
@@ -1,5 +1,5 @@
-import { Model } from "@janhq/core";
-import { Observable } from "rxjs";
+import { Model } from '@janhq/core'
+import { Observable } from 'rxjs'
/**
* Sends a request to the inference server to generate a response based on the recent messages.
* @param recentMessages - An array of recent messages to use as context for the inference.
@@ -17,50 +17,50 @@ export function requestInference(
model: model.id,
stream: true,
...model.parameters,
- });
+ })
fetch(inferenceUrl, {
- method: "POST",
+ method: 'POST',
headers: {
- "Content-Type": "application/json",
- "Access-Control-Allow-Origin": "*",
- Accept: model.parameters.stream
- ? "text/event-stream"
- : "application/json",
+ 'Content-Type': 'application/json',
+ 'Access-Control-Allow-Origin': '*',
+ 'Accept': model.parameters.stream
+ ? 'text/event-stream'
+ : 'application/json',
},
body: requestBody,
signal: controller?.signal,
})
.then(async (response) => {
if (model.parameters.stream === false) {
- const data = await response.json();
- subscriber.next(data.choices[0]?.message?.content ?? "");
+ const data = await response.json()
+ subscriber.next(data.choices[0]?.message?.content ?? '')
} else {
- const stream = response.body;
- const decoder = new TextDecoder("utf-8");
- const reader = stream?.getReader();
- let content = "";
+ const stream = response.body
+ const decoder = new TextDecoder('utf-8')
+ const reader = stream?.getReader()
+ let content = ''
while (true && reader) {
- const { done, value } = await reader.read();
+ const { done, value } = await reader.read()
if (done) {
- break;
+ break
}
- const text = decoder.decode(value);
- const lines = text.trim().split("\n");
+ const text = decoder.decode(value)
+ const lines = text.trim().split('\n')
for (const line of lines) {
- if (line.startsWith("data: ") && !line.includes("data: [DONE]")) {
- const data = JSON.parse(line.replace("data: ", ""));
- content += data.choices[0]?.delta?.content ?? "";
- if (content.startsWith("assistant: ")) {
- content = content.replace("assistant: ", "");
+ if (line.startsWith('data: ') && !line.includes('data: [DONE]')) {
+ const data = JSON.parse(line.replace('data: ', ''))
+ content += data.choices[0]?.delta?.content ?? ''
+ if (content.startsWith('assistant: ')) {
+ content = content.replace('assistant: ', '')
}
- subscriber.next(content);
+ subscriber.next(content)
}
}
}
}
- subscriber.complete();
+ subscriber.complete()
})
- .catch((err) => subscriber.error(err));
- });
+ .catch((err) => subscriber.error(err))
+ })
}
diff --git a/extensions/inference-nitro-extension/src/index.ts b/extensions/inference-nitro-extension/src/index.ts
index 9e96ad93f..b88501936 100644
--- a/extensions/inference-nitro-extension/src/index.ts
+++ b/extensions/inference-nitro-extension/src/index.ts
@@ -10,6 +10,7 @@ import {
ChatCompletionRole,
ContentType,
MessageRequest,
+ MessageRequestType,
MessageStatus,
ThreadContent,
ThreadMessage,
@@ -25,9 +26,10 @@ import {
ModelEvent,
InferenceEvent,
ModelSettingParams,
-} from "@janhq/core";
-import { requestInference } from "./helpers/sse";
-import { ulid } from "ulid";
+ getJanDataFolderPath,
+} from '@janhq/core'
+import { requestInference } from './helpers/sse'
+import { ulid } from 'ulid'
/**
* A class that implements the InferenceExtension interface from the @janhq/core package.
@@ -35,16 +37,16 @@ import { ulid } from "ulid";
* It also subscribes to events emitted by the @janhq/core package and handles new message requests.
*/
export default class JanInferenceNitroExtension extends InferenceExtension {
- private static readonly _homeDir = "file://engines";
- private static readonly _settingsDir = "file://settings";
- private static readonly _engineMetadataFileName = "nitro.json";
+ private static readonly _homeDir = 'file://engines'
+ private static readonly _settingsDir = 'file://settings'
+ private static readonly _engineMetadataFileName = 'nitro.json'
/**
* Checking the health for Nitro's process each 5 secs.
*/
- private static readonly _intervalHealthCheck = 5 * 1000;
+ private static readonly _intervalHealthCheck = 5 * 1000
- private _currentModel: Model | undefined;
+ private _currentModel: Model | undefined
private _engineSettings: ModelSettingParams = {
ctx_len: 2048,
@@ -52,23 +54,22 @@ export default class JanInferenceNitroExtension extends InferenceExtension {
cpu_threads: 1,
cont_batching: false,
embedding: true,
- };
+ }
- controller = new AbortController();
- isCancelled = false;
+ controller = new AbortController()
+ isCancelled = false
/**
* The interval id for the health check. Used to stop the health check.
*/
- private getNitroProcesHealthIntervalId: NodeJS.Timeout | undefined =
- undefined;
+ private getNitroProcesHealthIntervalId: NodeJS.Timeout | undefined = undefined
/**
* Tracking the current state of nitro process.
*/
- private nitroProcessInfo: any = undefined;
+ private nitroProcessInfo: any = undefined
- private inferenceUrl = "";
+ private inferenceUrl = ''
/**
* Subscribes to events emitted by the @janhq/core package.
@@ -76,44 +77,40 @@ export default class JanInferenceNitroExtension extends InferenceExtension {
async onLoad() {
if (!(await fs.existsSync(JanInferenceNitroExtension._homeDir))) {
try {
- await fs.mkdirSync(JanInferenceNitroExtension._homeDir);
+ await fs.mkdirSync(JanInferenceNitroExtension._homeDir)
} catch (e) {
- console.debug(e);
+ console.debug(e)
}
}
// init inference url
// @ts-ignore
- const electronApi = window?.electronAPI;
- this.inferenceUrl = INFERENCE_URL;
+ const electronApi = window?.electronAPI
+ this.inferenceUrl = INFERENCE_URL
if (!electronApi) {
- this.inferenceUrl = JAN_SERVER_INFERENCE_URL;
+ this.inferenceUrl = JAN_SERVER_INFERENCE_URL
}
- console.debug("Inference url: ", this.inferenceUrl);
+ console.debug('Inference url: ', this.inferenceUrl)
if (!(await fs.existsSync(JanInferenceNitroExtension._settingsDir)))
- await fs.mkdirSync(JanInferenceNitroExtension._settingsDir);
- this.writeDefaultEngineSettings();
+ await fs.mkdirSync(JanInferenceNitroExtension._settingsDir)
+ this.writeDefaultEngineSettings()
// Events subscription
events.on(MessageEvent.OnMessageSent, (data: MessageRequest) =>
this.onMessageRequest(data)
- );
+ )
- events.on(ModelEvent.OnModelInit, (model: Model) =>
- this.onModelInit(model)
- );
+ events.on(ModelEvent.OnModelInit, (model: Model) => this.onModelInit(model))
- events.on(ModelEvent.OnModelStop, (model: Model) =>
- this.onModelStop(model)
- );
+ events.on(ModelEvent.OnModelStop, (model: Model) => this.onModelStop(model))
events.on(InferenceEvent.OnInferenceStopped, () =>
this.onInferenceStopped()
- );
+ )
// Attempt to fetch nvidia info
- await executeOnMain(NODE, "updateNvidiaInfo", {});
+ await executeOnMain(NODE, 'updateNvidiaInfo', {})
}
/**
@@ -126,56 +123,59 @@ export default class JanInferenceNitroExtension extends InferenceExtension {
const engineFile = await joinPath([
JanInferenceNitroExtension._homeDir,
JanInferenceNitroExtension._engineMetadataFileName,
- ]);
+ ])
if (await fs.existsSync(engineFile)) {
- const engine = await fs.readFileSync(engineFile, "utf-8");
+ const engine = await fs.readFileSync(engineFile, 'utf-8')
this._engineSettings =
- typeof engine === "object" ? engine : JSON.parse(engine);
+ typeof engine === 'object' ? engine : JSON.parse(engine)
} else {
await fs.writeFileSync(
engineFile,
JSON.stringify(this._engineSettings, null, 2)
- );
+ )
}
} catch (err) {
- console.error(err);
+ console.error(err)
}
}
private async onModelInit(model: Model) {
- if (model.engine !== InferenceEngine.nitro) return;
+ if (model.engine !== InferenceEngine.nitro) return
- const modelFullPath = await joinPath(["models", model.id]);
-
- this._currentModel = model;
- const nitroInitResult = await executeOnMain(NODE, "runModel", {
- modelFullPath,
+ const modelFolder = await joinPath([
+ await getJanDataFolderPath(),
+ 'models',
+ model.id,
+ ])
+ this._currentModel = model
+ const nitroInitResult = await executeOnMain(NODE, 'runModel', {
+ modelFolder,
model,
- });
+ })
if (nitroInitResult?.error) {
- events.emit(ModelEvent.OnModelFail, model);
- return;
+ events.emit(ModelEvent.OnModelFail, model)
+ return
}
- events.emit(ModelEvent.OnModelReady, model);
+ events.emit(ModelEvent.OnModelReady, model)
this.getNitroProcesHealthIntervalId = setInterval(
() => this.periodicallyGetNitroHealth(),
JanInferenceNitroExtension._intervalHealthCheck
- );
+ )
}
private async onModelStop(model: Model) {
- if (model.engine !== "nitro") return;
+ if (model.engine !== 'nitro') return
- await executeOnMain(NODE, "stopModel");
- events.emit(ModelEvent.OnModelStopped, {});
+ await executeOnMain(NODE, 'stopModel')
+ events.emit(ModelEvent.OnModelStopped, {})
// stop the periocally health check
if (this.getNitroProcesHealthIntervalId) {
- clearInterval(this.getNitroProcesHealthIntervalId);
- this.getNitroProcesHealthIntervalId = undefined;
+ clearInterval(this.getNitroProcesHealthIntervalId)
+ this.getNitroProcesHealthIntervalId = undefined
}
}
@@ -183,19 +183,19 @@ export default class JanInferenceNitroExtension extends InferenceExtension {
* Periodically check for nitro process's health.
*/
private async periodicallyGetNitroHealth(): Promise {
- const health = await executeOnMain(NODE, "getCurrentNitroProcessInfo");
+ const health = await executeOnMain(NODE, 'getCurrentNitroProcessInfo')
- const isRunning = this.nitroProcessInfo?.isRunning ?? false;
+ const isRunning = this.nitroProcessInfo?.isRunning ?? false
if (isRunning && health.isRunning === false) {
- console.debug("Nitro process is stopped");
- events.emit(ModelEvent.OnModelStopped, {});
+ console.debug('Nitro process is stopped')
+ events.emit(ModelEvent.OnModelStopped, {})
}
- this.nitroProcessInfo = health;
+ this.nitroProcessInfo = health
}
private async onInferenceStopped() {
- this.isCancelled = true;
- this.controller?.abort();
+ this.isCancelled = true
+ this.controller?.abort()
}
/**
@@ -204,20 +204,20 @@ export default class JanInferenceNitroExtension extends InferenceExtension {
* @returns {Promise} A promise that resolves with the inference response.
*/
async inference(data: MessageRequest): Promise {
- const timestamp = Date.now();
+ const timestamp = Date.now()
const message: ThreadMessage = {
thread_id: data.threadId,
created: timestamp,
updated: timestamp,
status: MessageStatus.Ready,
- id: "",
+ id: '',
role: ChatCompletionRole.Assistant,
- object: "thread.message",
+ object: 'thread.message',
content: [],
- };
+ }
return new Promise(async (resolve, reject) => {
- if (!this._currentModel) return Promise.reject("No model loaded");
+ if (!this._currentModel) return Promise.reject('No model loaded')
requestInference(
this.inferenceUrl,
@@ -226,13 +226,13 @@ export default class JanInferenceNitroExtension extends InferenceExtension {
).subscribe({
next: (_content: any) => {},
complete: async () => {
- resolve(message);
+ resolve(message)
},
error: async (err: any) => {
- reject(err);
+ reject(err)
},
- });
- });
+ })
+ })
}
/**
@@ -243,31 +243,35 @@ export default class JanInferenceNitroExtension extends InferenceExtension {
*/
private async onMessageRequest(data: MessageRequest) {
if (data.model?.engine !== InferenceEngine.nitro || !this._currentModel) {
- return;
+ return
}
- const timestamp = Date.now();
+ const timestamp = Date.now()
const message: ThreadMessage = {
id: ulid(),
thread_id: data.threadId,
+ type: data.type,
assistant_id: data.assistantId,
role: ChatCompletionRole.Assistant,
content: [],
status: MessageStatus.Pending,
created: timestamp,
updated: timestamp,
- object: "thread.message",
- };
- events.emit(MessageEvent.OnMessageResponse, message);
+ object: 'thread.message',
+ }
- this.isCancelled = false;
- this.controller = new AbortController();
+ if (data.type !== MessageRequestType.Summary) {
+ events.emit(MessageEvent.OnMessageResponse, message)
+ }
+
+ this.isCancelled = false
+ this.controller = new AbortController()
// @ts-ignore
const model: Model = {
...(this._currentModel || {}),
...(data.model || {}),
- };
+ }
requestInference(
this.inferenceUrl,
data.messages ?? [],
@@ -281,26 +285,26 @@ export default class JanInferenceNitroExtension extends InferenceExtension {
value: content.trim(),
annotations: [],
},
- };
- message.content = [messageContent];
- events.emit(MessageEvent.OnMessageUpdate, message);
+ }
+ message.content = [messageContent]
+ events.emit(MessageEvent.OnMessageUpdate, message)
},
complete: async () => {
message.status = message.content.length
? MessageStatus.Ready
- : MessageStatus.Error;
- events.emit(MessageEvent.OnMessageUpdate, message);
+ : MessageStatus.Error
+ events.emit(MessageEvent.OnMessageUpdate, message)
},
error: async (err: any) => {
if (this.isCancelled || message.content.length) {
- message.status = MessageStatus.Stopped;
- events.emit(MessageEvent.OnMessageUpdate, message);
- return;
+ message.status = MessageStatus.Stopped
+ events.emit(MessageEvent.OnMessageUpdate, message)
+ return
}
- message.status = MessageStatus.Error;
- events.emit(MessageEvent.OnMessageUpdate, message);
- log(`[APP]::Error: ${err.message}`);
+ message.status = MessageStatus.Error
+ events.emit(MessageEvent.OnMessageUpdate, message)
+ log(`[APP]::Error: ${err.message}`)
},
- });
+ })
}
}
diff --git a/extensions/inference-nitro-extension/src/node/execute.ts b/extensions/inference-nitro-extension/src/node/execute.ts
index 83b5226d4..795c83ded 100644
--- a/extensions/inference-nitro-extension/src/node/execute.ts
+++ b/extensions/inference-nitro-extension/src/node/execute.ts
@@ -1,65 +1,65 @@
-import { readFileSync } from "fs";
-import * as path from "path";
-import { NVIDIA_INFO_FILE } from "./nvidia";
+import { readFileSync } from 'fs'
+import * as path from 'path'
+import { NVIDIA_INFO_FILE } from './nvidia'
export interface NitroExecutableOptions {
- executablePath: string;
- cudaVisibleDevices: string;
+ executablePath: string
+ cudaVisibleDevices: string
}
/**
* Find which executable file to run based on the current platform.
* @returns The name of the executable file to run.
*/
export const executableNitroFile = (): NitroExecutableOptions => {
- let binaryFolder = path.join(__dirname, "..", "bin"); // Current directory by default
- let cudaVisibleDevices = "";
- let binaryName = "nitro";
+ let binaryFolder = path.join(__dirname, '..', 'bin') // Current directory by default
+ let cudaVisibleDevices = ''
+ let binaryName = 'nitro'
/**
* The binary folder is different for each platform.
*/
- if (process.platform === "win32") {
+ if (process.platform === 'win32') {
/**
* For Windows: win-cpu, win-cuda-11-7, win-cuda-12-0
*/
- let nvidiaInfo = JSON.parse(readFileSync(NVIDIA_INFO_FILE, "utf-8"));
- if (nvidiaInfo["run_mode"] === "cpu") {
- binaryFolder = path.join(binaryFolder, "win-cpu");
+ let nvidiaInfo = JSON.parse(readFileSync(NVIDIA_INFO_FILE, 'utf-8'))
+ if (nvidiaInfo['run_mode'] === 'cpu') {
+ binaryFolder = path.join(binaryFolder, 'win-cpu')
} else {
- if (nvidiaInfo["cuda"].version === "11") {
- binaryFolder = path.join(binaryFolder, "win-cuda-11-7");
+ if (nvidiaInfo['cuda'].version === '11') {
+ binaryFolder = path.join(binaryFolder, 'win-cuda-11-7')
} else {
- binaryFolder = path.join(binaryFolder, "win-cuda-12-0");
+ binaryFolder = path.join(binaryFolder, 'win-cuda-12-0')
}
- cudaVisibleDevices = nvidiaInfo["gpus_in_use"].join(",");
+ cudaVisibleDevices = nvidiaInfo['gpus_in_use'].join(',')
}
- binaryName = "nitro.exe";
- } else if (process.platform === "darwin") {
+ binaryName = 'nitro.exe'
+ } else if (process.platform === 'darwin') {
/**
* For MacOS: mac-arm64 (Silicon), mac-x64 (InteL)
*/
- if (process.arch === "arm64") {
- binaryFolder = path.join(binaryFolder, "mac-arm64");
+ if (process.arch === 'arm64') {
+ binaryFolder = path.join(binaryFolder, 'mac-arm64')
} else {
- binaryFolder = path.join(binaryFolder, "mac-x64");
+ binaryFolder = path.join(binaryFolder, 'mac-x64')
}
} else {
/**
* For Linux: linux-cpu, linux-cuda-11-7, linux-cuda-12-0
*/
- let nvidiaInfo = JSON.parse(readFileSync(NVIDIA_INFO_FILE, "utf-8"));
- if (nvidiaInfo["run_mode"] === "cpu") {
- binaryFolder = path.join(binaryFolder, "linux-cpu");
+ let nvidiaInfo = JSON.parse(readFileSync(NVIDIA_INFO_FILE, 'utf-8'))
+ if (nvidiaInfo['run_mode'] === 'cpu') {
+ binaryFolder = path.join(binaryFolder, 'linux-cpu')
} else {
- if (nvidiaInfo["cuda"].version === "11") {
- binaryFolder = path.join(binaryFolder, "linux-cuda-11-7");
+ if (nvidiaInfo['cuda'].version === '11') {
+ binaryFolder = path.join(binaryFolder, 'linux-cuda-11-7')
} else {
- binaryFolder = path.join(binaryFolder, "linux-cuda-12-0");
+ binaryFolder = path.join(binaryFolder, 'linux-cuda-12-0')
}
- cudaVisibleDevices = nvidiaInfo["gpus_in_use"].join(",");
+ cudaVisibleDevices = nvidiaInfo['gpus_in_use'].join(',')
}
}
return {
executablePath: path.join(binaryFolder, binaryName),
cudaVisibleDevices,
- };
-};
+ }
+}
diff --git a/extensions/inference-nitro-extension/src/node/index.ts b/extensions/inference-nitro-extension/src/node/index.ts
index 7ba90b556..25f571c81 100644
--- a/extensions/inference-nitro-extension/src/node/index.ts
+++ b/extensions/inference-nitro-extension/src/node/index.ts
@@ -1,55 +1,50 @@
-import fs from "fs";
-import path from "path";
-import { ChildProcessWithoutNullStreams, spawn } from "child_process";
-import tcpPortUsed from "tcp-port-used";
-import fetchRT from "fetch-retry";
-import {
- log,
- getJanDataFolderPath,
- getSystemResourceInfo,
-} from "@janhq/core/node";
-import { getNitroProcessInfo, updateNvidiaInfo } from "./nvidia";
+import fs from 'fs'
+import path from 'path'
+import { ChildProcessWithoutNullStreams, spawn } from 'child_process'
+import tcpPortUsed from 'tcp-port-used'
+import fetchRT from 'fetch-retry'
+import { log, getSystemResourceInfo } from '@janhq/core/node'
+import { getNitroProcessInfo, updateNvidiaInfo } from './nvidia'
import {
Model,
InferenceEngine,
ModelSettingParams,
PromptTemplate,
-} from "@janhq/core";
-import { executableNitroFile } from "./execute";
+} from '@janhq/core'
+import { executableNitroFile } from './execute'
// Polyfill fetch with retry
-const fetchRetry = fetchRT(fetch);
+const fetchRetry = fetchRT(fetch)
/**
* The response object for model init operation.
*/
interface ModelInitOptions {
- modelFullPath: string;
- model: Model;
+ modelFolder: string
+ model: Model
}
// The PORT to use for the Nitro subprocess
-const PORT = 3928;
+const PORT = 3928
// The HOST address to use for the Nitro subprocess
-const LOCAL_HOST = "127.0.0.1";
+const LOCAL_HOST = '127.0.0.1'
// The URL for the Nitro subprocess
-const NITRO_HTTP_SERVER_URL = `http://${LOCAL_HOST}:${PORT}`;
+const NITRO_HTTP_SERVER_URL = `http://${LOCAL_HOST}:${PORT}`
// The URL for the Nitro subprocess to load a model
-const NITRO_HTTP_LOAD_MODEL_URL = `${NITRO_HTTP_SERVER_URL}/inferences/llamacpp/loadmodel`;
+const NITRO_HTTP_LOAD_MODEL_URL = `${NITRO_HTTP_SERVER_URL}/inferences/llamacpp/loadmodel`
// The URL for the Nitro subprocess to validate a model
-const NITRO_HTTP_VALIDATE_MODEL_URL = `${NITRO_HTTP_SERVER_URL}/inferences/llamacpp/modelstatus`;
+const NITRO_HTTP_VALIDATE_MODEL_URL = `${NITRO_HTTP_SERVER_URL}/inferences/llamacpp/modelstatus`
// The URL for the Nitro subprocess to kill itself
-const NITRO_HTTP_KILL_URL = `${NITRO_HTTP_SERVER_URL}/processmanager/destroy`;
+const NITRO_HTTP_KILL_URL = `${NITRO_HTTP_SERVER_URL}/processmanager/destroy`
// The supported model format
// TODO: Should be an array to support more models
-const SUPPORTED_MODEL_FORMAT = ".gguf";
+const SUPPORTED_MODEL_FORMAT = '.gguf'
// The subprocess instance for Nitro
-let subprocess: ChildProcessWithoutNullStreams | undefined = undefined;
-// The current model file url
-let currentModelFile: string = "";
+let subprocess: ChildProcessWithoutNullStreams | undefined = undefined
+
// The current model settings
-let currentSettings: ModelSettingParams | undefined = undefined;
+let currentSettings: ModelSettingParams | undefined = undefined
/**
* Stops a Nitro subprocess.
@@ -57,7 +52,7 @@ let currentSettings: ModelSettingParams | undefined = undefined;
* @returns A Promise that resolves when the subprocess is terminated successfully, or rejects with an error message if the subprocess fails to terminate.
*/
function stopModel(): Promise {
- return killSubprocess();
+ return killSubprocess()
}
/**
@@ -67,62 +62,79 @@ function stopModel(): Promise {
* TODO: Should pass absolute of the model file instead of just the name - So we can modurize the module.ts to npm package
*/
async function runModel(
- wrapper: ModelInitOptions,
+ wrapper: ModelInitOptions
): Promise {
if (wrapper.model.engine !== InferenceEngine.nitro) {
// Not a nitro model
- return Promise.resolve();
+ return Promise.resolve()
}
- currentModelFile = wrapper.modelFullPath;
- const janRoot = await getJanDataFolderPath();
- 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),
- );
-
- if (!ggufBinFile) return Promise.reject("No GGUF model file found");
-
- currentModelFile = path.join(currentModelFile, ggufBinFile);
-
if (wrapper.model.engine !== InferenceEngine.nitro) {
- return Promise.reject("Not a nitro model");
+ return Promise.reject('Not a nitro model')
} else {
- const nitroResourceProbe = await getSystemResourceInfo();
+ const nitroResourceProbe = await getSystemResourceInfo()
// Convert settings.prompt_template to system_prompt, user_prompt, ai_prompt
if (wrapper.model.settings.prompt_template) {
- const promptTemplate = wrapper.model.settings.prompt_template;
- const prompt = promptTemplateConverter(promptTemplate);
+ const promptTemplate = wrapper.model.settings.prompt_template
+ const prompt = promptTemplateConverter(promptTemplate)
if (prompt?.error) {
- return Promise.reject(prompt.error);
+ return Promise.reject(prompt.error)
}
- wrapper.model.settings.system_prompt = prompt.system_prompt;
- wrapper.model.settings.user_prompt = prompt.user_prompt;
- wrapper.model.settings.ai_prompt = prompt.ai_prompt;
+ wrapper.model.settings.system_prompt = prompt.system_prompt
+ wrapper.model.settings.user_prompt = prompt.user_prompt
+ wrapper.model.settings.ai_prompt = prompt.ai_prompt
}
- const modelFolderPath = path.join(janRoot, "models", wrapper.model.id);
- const modelPath = wrapper.model.settings.llama_model_path
- ? path.join(modelFolderPath, wrapper.model.settings.llama_model_path)
- : currentModelFile;
+ // modelFolder is the absolute path to the running model folder
+ // e.g. ~/jan/models/llama-2
+ let modelFolder = wrapper.modelFolder
+
+ let llama_model_path = wrapper.model.settings.llama_model_path
+
+ // Absolute model path support
+ if (
+ wrapper.model?.sources.length &&
+ wrapper.model.sources.every((e) => fs.existsSync(e.url))
+ ) {
+ llama_model_path =
+ wrapper.model.sources.length === 1
+ ? wrapper.model.sources[0].url
+ : wrapper.model.sources.find((e) =>
+ e.url.includes(llama_model_path ?? wrapper.model.id)
+ )?.url
+ }
+
+ if (!llama_model_path || !path.isAbsolute(llama_model_path)) {
+ // Look for GGUF model file
+ const modelFiles: string[] = fs.readdirSync(modelFolder)
+ const ggufBinFile = modelFiles.find(
+ (file) =>
+ // 1. Prioritize llama_model_path (predefined)
+ (llama_model_path && file === llama_model_path) ||
+ // 2. Prioritize GGUF File (manual import)
+ file.toLowerCase().includes(SUPPORTED_MODEL_FORMAT) ||
+ // 3. Fallback Model ID (for backward compatibility)
+ file === wrapper.model.id
+ )
+ if (ggufBinFile) llama_model_path = path.join(modelFolder, ggufBinFile)
+ }
+
+ // Look for absolute source path for single model
+
+ if (!llama_model_path) return Promise.reject('No GGUF model file found')
currentSettings = {
...wrapper.model.settings,
- llama_model_path: modelPath,
+ llama_model_path,
// This is critical and requires real CPU physical core count (or performance core)
cpu_threads: Math.max(1, nitroResourceProbe.numCpuPhysicalCore),
...(wrapper.model.settings.mmproj && {
- mmproj: path.join(modelFolderPath, wrapper.model.settings.mmproj),
+ mmproj: path.isAbsolute(wrapper.model.settings.mmproj)
+ ? wrapper.model.settings.mmproj
+ : path.join(modelFolder, wrapper.model.settings.mmproj),
}),
- };
- return runNitroAndLoadModel();
+ }
+ return runNitroAndLoadModel()
}
}
@@ -142,10 +154,10 @@ async function runNitroAndLoadModel() {
* Should wait for awhile to make sure the port is free and subprocess is killed
* The tested threshold is 500ms
**/
- if (process.platform === "win32") {
- return new Promise((resolve) => setTimeout(resolve, 500));
+ if (process.platform === 'win32') {
+ return new Promise((resolve) => setTimeout(resolve, 500))
} else {
- return Promise.resolve();
+ return Promise.resolve()
}
})
.then(spawnNitroProcess)
@@ -153,9 +165,9 @@ async function runNitroAndLoadModel() {
.then(validateModelStatus)
.catch((err) => {
// TODO: Broadcast error so app could display proper error message
- log(`[NITRO]::Error: ${err}`);
- return { error: err };
- });
+ log(`[NITRO]::Error: ${err}`)
+ return { error: err }
+ })
}
/**
@@ -165,43 +177,43 @@ async function runNitroAndLoadModel() {
*/
function promptTemplateConverter(promptTemplate: string): PromptTemplate {
// Split the string using the markers
- const systemMarker = "{system_message}";
- const promptMarker = "{prompt}";
+ const systemMarker = '{system_message}'
+ const promptMarker = '{prompt}'
if (
promptTemplate.includes(systemMarker) &&
promptTemplate.includes(promptMarker)
) {
// Find the indices of the markers
- const systemIndex = promptTemplate.indexOf(systemMarker);
- const promptIndex = promptTemplate.indexOf(promptMarker);
+ const systemIndex = promptTemplate.indexOf(systemMarker)
+ const promptIndex = promptTemplate.indexOf(promptMarker)
// Extract the parts of the string
- const system_prompt = promptTemplate.substring(0, systemIndex);
+ const system_prompt = promptTemplate.substring(0, systemIndex)
const user_prompt = promptTemplate.substring(
systemIndex + systemMarker.length,
- promptIndex,
- );
+ promptIndex
+ )
const ai_prompt = promptTemplate.substring(
- promptIndex + promptMarker.length,
- );
+ promptIndex + promptMarker.length
+ )
// Return the split parts
- return { system_prompt, user_prompt, ai_prompt };
+ return { system_prompt, user_prompt, ai_prompt }
} else if (promptTemplate.includes(promptMarker)) {
// Extract the parts of the string for the case where only promptMarker is present
- const promptIndex = promptTemplate.indexOf(promptMarker);
- const user_prompt = promptTemplate.substring(0, promptIndex);
+ const promptIndex = promptTemplate.indexOf(promptMarker)
+ const user_prompt = promptTemplate.substring(0, promptIndex)
const ai_prompt = promptTemplate.substring(
- promptIndex + promptMarker.length,
- );
+ promptIndex + promptMarker.length
+ )
// Return the split parts
- return { user_prompt, ai_prompt };
+ return { user_prompt, ai_prompt }
}
// Return an error if none of the conditions are met
- return { error: "Cannot split prompt template" };
+ return { error: 'Cannot split prompt template' }
}
/**
@@ -210,13 +222,13 @@ function promptTemplateConverter(promptTemplate: string): PromptTemplate {
*/
function loadLLMModel(settings: any): Promise {
if (!settings?.ngl) {
- settings.ngl = 100;
+ settings.ngl = 100
}
- log(`[NITRO]::Debug: Loading model with params ${JSON.stringify(settings)}`);
+ log(`[NITRO]::Debug: Loading model with params ${JSON.stringify(settings)}`)
return fetchRetry(NITRO_HTTP_LOAD_MODEL_URL, {
- method: "POST",
+ method: 'POST',
headers: {
- "Content-Type": "application/json",
+ 'Content-Type': 'application/json',
},
body: JSON.stringify(settings),
retries: 3,
@@ -225,15 +237,15 @@ function loadLLMModel(settings: any): Promise {
.then((res) => {
log(
`[NITRO]::Debug: Load model success with response ${JSON.stringify(
- res,
- )}`,
- );
- return Promise.resolve(res);
+ res
+ )}`
+ )
+ return Promise.resolve(res)
})
.catch((err) => {
- log(`[NITRO]::Error: Load model failed with error ${err}`);
- return Promise.reject(err);
- });
+ log(`[NITRO]::Error: Load model failed with error ${err}`)
+ return Promise.reject(err)
+ })
}
/**
@@ -246,9 +258,9 @@ async function validateModelStatus(): Promise {
// Send a GET request to the validation URL.
// Retry the request up to 3 times if it fails, with a delay of 500 milliseconds between retries.
return fetchRetry(NITRO_HTTP_VALIDATE_MODEL_URL, {
- method: "GET",
+ method: 'GET',
headers: {
- "Content-Type": "application/json",
+ 'Content-Type': 'application/json',
},
retries: 5,
retryDelay: 500,
@@ -257,10 +269,10 @@ async function validateModelStatus(): Promise {
`[NITRO]::Debug: Validate model state with response ${JSON.stringify(
res.status
)}`
- );
+ )
// If the response is OK, check model_loaded status.
if (res.ok) {
- const body = await res.json();
+ const body = await res.json()
// If the model is loaded, return an empty object.
// Otherwise, return an object with an error message.
if (body.model_loaded) {
@@ -268,17 +280,17 @@ async function validateModelStatus(): Promise {
`[NITRO]::Debug: Validate model state success with response ${JSON.stringify(
body
)}`
- );
- return Promise.resolve();
+ )
+ return Promise.resolve()
}
}
log(
`[NITRO]::Debug: Validate model state failed with response ${JSON.stringify(
res.statusText
)}`
- );
- return Promise.reject("Validate model status failed");
- });
+ )
+ return Promise.reject('Validate model status failed')
+ })
}
/**
@@ -286,21 +298,21 @@ async function validateModelStatus(): Promise {
* @returns A Promise that resolves when the subprocess is terminated successfully, or rejects with an error message if the subprocess fails to terminate.
*/
async function killSubprocess(): Promise {
- const controller = new AbortController();
- setTimeout(() => controller.abort(), 5000);
- log(`[NITRO]::Debug: Request to kill Nitro`);
+ const controller = new AbortController()
+ setTimeout(() => controller.abort(), 5000)
+ log(`[NITRO]::Debug: Request to kill Nitro`)
return fetch(NITRO_HTTP_KILL_URL, {
- method: "DELETE",
+ method: 'DELETE',
signal: controller.signal,
})
.then(() => {
- subprocess?.kill();
- subprocess = undefined;
+ subprocess?.kill()
+ subprocess = undefined
})
.catch(() => {})
.then(() => tcpPortUsed.waitUntilFree(PORT, 300, 5000))
- .then(() => log(`[NITRO]::Debug: Nitro process is terminated`));
+ .then(() => log(`[NITRO]::Debug: Nitro process is terminated`))
}
/**
@@ -308,49 +320,49 @@ async function killSubprocess(): Promise {
* @returns A promise that resolves when the Nitro subprocess is started.
*/
function spawnNitroProcess(): Promise {
- log(`[NITRO]::Debug: Spawning Nitro subprocess...`);
+ log(`[NITRO]::Debug: Spawning Nitro subprocess...`)
return new Promise(async (resolve, reject) => {
- let binaryFolder = path.join(__dirname, "..", "bin"); // Current directory by default
- let executableOptions = executableNitroFile();
+ let binaryFolder = path.join(__dirname, '..', 'bin') // Current directory by default
+ let executableOptions = executableNitroFile()
- const args: string[] = ["1", LOCAL_HOST, PORT.toString()];
+ const args: string[] = ['1', LOCAL_HOST, PORT.toString()]
// Execute the binary
log(
- `[NITRO]::Debug: Spawn nitro at path: ${executableOptions.executablePath}, and args: ${args}`,
- );
+ `[NITRO]::Debug: Spawn nitro at path: ${executableOptions.executablePath}, and args: ${args}`
+ )
subprocess = spawn(
executableOptions.executablePath,
- ["1", LOCAL_HOST, PORT.toString()],
+ ['1', LOCAL_HOST, PORT.toString()],
{
cwd: binaryFolder,
env: {
...process.env,
CUDA_VISIBLE_DEVICES: executableOptions.cudaVisibleDevices,
},
- },
- );
+ }
+ )
// Handle subprocess output
- subprocess.stdout.on("data", (data: any) => {
- log(`[NITRO]::Debug: ${data}`);
- });
+ subprocess.stdout.on('data', (data: any) => {
+ log(`[NITRO]::Debug: ${data}`)
+ })
- subprocess.stderr.on("data", (data: any) => {
- log(`[NITRO]::Error: ${data}`);
- });
+ subprocess.stderr.on('data', (data: any) => {
+ log(`[NITRO]::Error: ${data}`)
+ })
- subprocess.on("close", (code: any) => {
- log(`[NITRO]::Debug: Nitro exited with code: ${code}`);
- subprocess = undefined;
- reject(`child process exited with code ${code}`);
- });
+ subprocess.on('close', (code: any) => {
+ log(`[NITRO]::Debug: Nitro exited with code: ${code}`)
+ subprocess = undefined
+ reject(`child process exited with code ${code}`)
+ })
tcpPortUsed.waitUntilUsed(PORT, 300, 30000).then(() => {
- log(`[NITRO]::Debug: Nitro is ready`);
- resolve();
- });
- });
+ log(`[NITRO]::Debug: Nitro is ready`)
+ resolve()
+ })
+ })
}
/**
@@ -360,7 +372,7 @@ function spawnNitroProcess(): Promise {
*/
function dispose() {
// clean other registered resources here
- killSubprocess();
+ killSubprocess()
}
export default {
@@ -370,4 +382,4 @@ export default {
dispose,
updateNvidiaInfo,
getCurrentNitroProcessInfo: () => getNitroProcessInfo(subprocess),
-};
+}
diff --git a/extensions/inference-nitro-extension/src/node/nvidia.ts b/extensions/inference-nitro-extension/src/node/nvidia.ts
index bed2856a1..60468f0c9 100644
--- a/extensions/inference-nitro-extension/src/node/nvidia.ts
+++ b/extensions/inference-nitro-extension/src/node/nvidia.ts
@@ -1,47 +1,47 @@
-import { writeFileSync, existsSync, readFileSync } from "fs";
-import { exec } from "child_process";
-import path from "path";
-import { getJanDataFolderPath } from "@janhq/core/node";
+import { writeFileSync, existsSync, readFileSync } from 'fs'
+import { exec } from 'child_process'
+import path from 'path'
+import { getJanDataFolderPath } from '@janhq/core/node'
/**
* Default GPU settings
**/
const DEFALT_SETTINGS = {
notify: true,
- run_mode: "cpu",
+ run_mode: 'cpu',
nvidia_driver: {
exist: false,
- version: "",
+ version: '',
},
cuda: {
exist: false,
- version: "",
+ version: '',
},
gpus: [],
- gpu_highest_vram: "",
+ gpu_highest_vram: '',
gpus_in_use: [],
is_initial: true,
-};
+}
/**
* Path to the settings file
**/
export const NVIDIA_INFO_FILE = path.join(
getJanDataFolderPath(),
- "settings",
- "settings.json"
-);
+ 'settings',
+ 'settings.json'
+)
/**
* Current nitro process
*/
-let nitroProcessInfo: NitroProcessInfo | undefined = undefined;
+let nitroProcessInfo: NitroProcessInfo | undefined = undefined
/**
* Nitro process info
*/
export interface NitroProcessInfo {
- isRunning: boolean;
+ isRunning: boolean
}
/**
@@ -49,16 +49,16 @@ export interface NitroProcessInfo {
* Will be called when the extension is loaded to turn on GPU acceleration if supported
*/
export async function updateNvidiaInfo() {
- if (process.platform !== "darwin") {
- let data;
+ if (process.platform !== 'darwin') {
+ let data
try {
- data = JSON.parse(readFileSync(NVIDIA_INFO_FILE, "utf-8"));
+ data = JSON.parse(readFileSync(NVIDIA_INFO_FILE, 'utf-8'))
} catch (error) {
- data = DEFALT_SETTINGS;
- writeFileSync(NVIDIA_INFO_FILE, JSON.stringify(data, null, 2));
+ data = DEFALT_SETTINGS
+ writeFileSync(NVIDIA_INFO_FILE, JSON.stringify(data, null, 2))
}
- updateNvidiaDriverInfo();
- updateGpuInfo();
+ updateNvidiaDriverInfo()
+ updateGpuInfo()
}
}
@@ -68,31 +68,31 @@ export async function updateNvidiaInfo() {
export const getNitroProcessInfo = (subprocess: any): NitroProcessInfo => {
nitroProcessInfo = {
isRunning: subprocess != null,
- };
- return nitroProcessInfo;
-};
+ }
+ return nitroProcessInfo
+}
/**
* Validate nvidia and cuda for linux and windows
*/
export async function updateNvidiaDriverInfo(): Promise {
exec(
- "nvidia-smi --query-gpu=driver_version --format=csv,noheader",
+ 'nvidia-smi --query-gpu=driver_version --format=csv,noheader',
(error, stdout) => {
- let data = JSON.parse(readFileSync(NVIDIA_INFO_FILE, "utf-8"));
+ let data = JSON.parse(readFileSync(NVIDIA_INFO_FILE, 'utf-8'))
if (!error) {
- const firstLine = stdout.split("\n")[0].trim();
- data["nvidia_driver"].exist = true;
- data["nvidia_driver"].version = firstLine;
+ const firstLine = stdout.split('\n')[0].trim()
+ data['nvidia_driver'].exist = true
+ data['nvidia_driver'].version = firstLine
} else {
- data["nvidia_driver"].exist = false;
+ data['nvidia_driver'].exist = false
}
- writeFileSync(NVIDIA_INFO_FILE, JSON.stringify(data, null, 2));
- Promise.resolve();
+ writeFileSync(NVIDIA_INFO_FILE, JSON.stringify(data, null, 2))
+ Promise.resolve()
}
- );
+ )
}
/**
@@ -102,54 +102,56 @@ export function checkFileExistenceInPaths(
file: string,
paths: string[]
): boolean {
- return paths.some((p) => existsSync(path.join(p, file)));
+ return paths.some((p) => existsSync(path.join(p, file)))
}
/**
* Validate cuda for linux and windows
*/
-export function updateCudaExistence(data: Record = DEFALT_SETTINGS): Record {
- let filesCuda12: string[];
- let filesCuda11: string[];
- let paths: string[];
- let cudaVersion: string = "";
+export function updateCudaExistence(
+ data: Record = DEFALT_SETTINGS
+): Record {
+ let filesCuda12: string[]
+ let filesCuda11: string[]
+ let paths: string[]
+ let cudaVersion: string = ''
- if (process.platform === "win32") {
- filesCuda12 = ["cublas64_12.dll", "cudart64_12.dll", "cublasLt64_12.dll"];
- filesCuda11 = ["cublas64_11.dll", "cudart64_11.dll", "cublasLt64_11.dll"];
- paths = process.env.PATH ? process.env.PATH.split(path.delimiter) : [];
+ if (process.platform === 'win32') {
+ filesCuda12 = ['cublas64_12.dll', 'cudart64_12.dll', 'cublasLt64_12.dll']
+ filesCuda11 = ['cublas64_11.dll', 'cudart64_11.dll', 'cublasLt64_11.dll']
+ paths = process.env.PATH ? process.env.PATH.split(path.delimiter) : []
} else {
- filesCuda12 = ["libcudart.so.12", "libcublas.so.12", "libcublasLt.so.12"];
- filesCuda11 = ["libcudart.so.11.0", "libcublas.so.11", "libcublasLt.so.11"];
+ filesCuda12 = ['libcudart.so.12', 'libcublas.so.12', 'libcublasLt.so.12']
+ filesCuda11 = ['libcudart.so.11.0', 'libcublas.so.11', 'libcublasLt.so.11']
paths = process.env.LD_LIBRARY_PATH
? process.env.LD_LIBRARY_PATH.split(path.delimiter)
- : [];
- paths.push("/usr/lib/x86_64-linux-gnu/");
+ : []
+ paths.push('/usr/lib/x86_64-linux-gnu/')
}
let cudaExists = filesCuda12.every(
(file) => existsSync(file) || checkFileExistenceInPaths(file, paths)
- );
+ )
if (!cudaExists) {
cudaExists = filesCuda11.every(
(file) => existsSync(file) || checkFileExistenceInPaths(file, paths)
- );
+ )
if (cudaExists) {
- cudaVersion = "11";
+ cudaVersion = '11'
}
} else {
- cudaVersion = "12";
+ cudaVersion = '12'
}
- data["cuda"].exist = cudaExists;
- data["cuda"].version = cudaVersion;
- console.log(data["is_initial"], data["gpus_in_use"]);
- if (cudaExists && data["is_initial"] && data["gpus_in_use"].length > 0) {
- data.run_mode = "gpu";
+ data['cuda'].exist = cudaExists
+ data['cuda'].version = cudaVersion
+ console.log(data['is_initial'], data['gpus_in_use'])
+ if (cudaExists && data['is_initial'] && data['gpus_in_use'].length > 0) {
+ data.run_mode = 'gpu'
}
- data.is_initial = false;
- return data;
+ data.is_initial = false
+ return data
}
/**
@@ -157,41 +159,41 @@ export function updateCudaExistence(data: Record = DEFALT_SETTINGS)
*/
export async function updateGpuInfo(): Promise {
exec(
- "nvidia-smi --query-gpu=index,memory.total,name --format=csv,noheader,nounits",
+ 'nvidia-smi --query-gpu=index,memory.total,name --format=csv,noheader,nounits',
(error, stdout) => {
- let data = JSON.parse(readFileSync(NVIDIA_INFO_FILE, "utf-8"));
+ let data = JSON.parse(readFileSync(NVIDIA_INFO_FILE, 'utf-8'))
if (!error) {
// Get GPU info and gpu has higher memory first
- let highestVram = 0;
- let highestVramId = "0";
+ let highestVram = 0
+ let highestVramId = '0'
let gpus = stdout
.trim()
- .split("\n")
+ .split('\n')
.map((line) => {
- let [id, vram, name] = line.split(", ");
- vram = vram.replace(/\r/g, "");
+ let [id, vram, name] = line.split(', ')
+ vram = vram.replace(/\r/g, '')
if (parseFloat(vram) > highestVram) {
- highestVram = parseFloat(vram);
- highestVramId = id;
+ highestVram = parseFloat(vram)
+ highestVramId = id
}
- return { id, vram, name };
- });
+ return { id, vram, name }
+ })
- data.gpus = gpus;
- data.gpu_highest_vram = highestVramId;
+ data.gpus = gpus
+ data.gpu_highest_vram = highestVramId
} else {
- data.gpus = [];
- data.gpu_highest_vram = "";
+ data.gpus = []
+ data.gpu_highest_vram = ''
}
- if (!data["gpus_in_use"] || data["gpus_in_use"].length === 0) {
- data.gpus_in_use = [data["gpu_highest_vram"]];
+ if (!data['gpus_in_use'] || data['gpus_in_use'].length === 0) {
+ data.gpus_in_use = [data['gpu_highest_vram']]
}
- data = updateCudaExistence(data);
- writeFileSync(NVIDIA_INFO_FILE, JSON.stringify(data, null, 2));
- Promise.resolve();
+ data = updateCudaExistence(data)
+ writeFileSync(NVIDIA_INFO_FILE, JSON.stringify(data, null, 2))
+ Promise.resolve()
}
- );
+ )
}
diff --git a/extensions/inference-openai-extension/package.json b/extensions/inference-openai-extension/package.json
index 0ba6f18db..5efdbf874 100644
--- a/extensions/inference-openai-extension/package.json
+++ b/extensions/inference-openai-extension/package.json
@@ -18,13 +18,13 @@
"cpx": "^1.5.0",
"rimraf": "^3.0.2",
"webpack": "^5.88.2",
- "webpack-cli": "^5.1.4"
+ "webpack-cli": "^5.1.4",
+ "ts-loader": "^9.5.0"
},
"dependencies": {
"@janhq/core": "file:../../core",
"fetch-retry": "^5.0.6",
"path-browserify": "^1.0.1",
- "ts-loader": "^9.5.0",
"ulid": "^2.3.0"
},
"engines": {
diff --git a/extensions/inference-openai-extension/src/@types/global.d.ts b/extensions/inference-openai-extension/src/@types/global.d.ts
index 84f86c145..a49bb5a2f 100644
--- a/extensions/inference-openai-extension/src/@types/global.d.ts
+++ b/extensions/inference-openai-extension/src/@types/global.d.ts
@@ -1,26 +1,26 @@
-declare const MODULE: string;
-declare const OPENAI_DOMAIN: string;
+declare const MODULE: string
+declare const OPENAI_DOMAIN: string
declare interface EngineSettings {
- full_url?: string;
- api_key?: string;
+ full_url?: string
+ api_key?: string
}
enum OpenAIChatCompletionModelName {
- "gpt-3.5-turbo-instruct" = "gpt-3.5-turbo-instruct",
- "gpt-3.5-turbo-instruct-0914" = "gpt-3.5-turbo-instruct-0914",
- "gpt-4-1106-preview" = "gpt-4-1106-preview",
- "gpt-3.5-turbo-0613" = "gpt-3.5-turbo-0613",
- "gpt-3.5-turbo-0301" = "gpt-3.5-turbo-0301",
- "gpt-3.5-turbo" = "gpt-3.5-turbo",
- "gpt-3.5-turbo-16k-0613" = "gpt-3.5-turbo-16k-0613",
- "gpt-3.5-turbo-1106" = "gpt-3.5-turbo-1106",
- "gpt-4-vision-preview" = "gpt-4-vision-preview",
- "gpt-4" = "gpt-4",
- "gpt-4-0314" = "gpt-4-0314",
- "gpt-4-0613" = "gpt-4-0613",
+ 'gpt-3.5-turbo-instruct' = 'gpt-3.5-turbo-instruct',
+ 'gpt-3.5-turbo-instruct-0914' = 'gpt-3.5-turbo-instruct-0914',
+ 'gpt-4-1106-preview' = 'gpt-4-1106-preview',
+ 'gpt-3.5-turbo-0613' = 'gpt-3.5-turbo-0613',
+ 'gpt-3.5-turbo-0301' = 'gpt-3.5-turbo-0301',
+ 'gpt-3.5-turbo' = 'gpt-3.5-turbo',
+ 'gpt-3.5-turbo-16k-0613' = 'gpt-3.5-turbo-16k-0613',
+ 'gpt-3.5-turbo-1106' = 'gpt-3.5-turbo-1106',
+ 'gpt-4-vision-preview' = 'gpt-4-vision-preview',
+ 'gpt-4' = 'gpt-4',
+ 'gpt-4-0314' = 'gpt-4-0314',
+ 'gpt-4-0613' = 'gpt-4-0613',
}
-declare type OpenAIModel = Omit & {
- id: OpenAIChatCompletionModelName;
-};
+declare type OpenAIModel = Omit & {
+ id: OpenAIChatCompletionModelName
+}
diff --git a/extensions/inference-openai-extension/src/helpers/sse.ts b/extensions/inference-openai-extension/src/helpers/sse.ts
index fb75816e7..11db38282 100644
--- a/extensions/inference-openai-extension/src/helpers/sse.ts
+++ b/extensions/inference-openai-extension/src/helpers/sse.ts
@@ -1,4 +1,4 @@
-import { Observable } from "rxjs";
+import { Observable } from 'rxjs'
/**
* Sends a request to the inference server to generate a response based on the recent messages.
@@ -14,26 +14,26 @@ export function requestInference(
controller?: AbortController
): Observable {
return new Observable((subscriber) => {
- let model_id: string = model.id;
+ let model_id: string = model.id
if (engine.full_url.includes(OPENAI_DOMAIN)) {
- model_id = engine.full_url.split("/")[5];
+ model_id = engine.full_url.split('/')[5]
}
const requestBody = JSON.stringify({
messages: recentMessages,
stream: true,
model: model_id,
...model.parameters,
- });
+ })
fetch(`${engine.full_url}`, {
- method: "POST",
+ method: 'POST',
headers: {
- "Content-Type": "application/json",
- Accept: model.parameters.stream
- ? "text/event-stream"
- : "application/json",
- "Access-Control-Allow-Origin": "*",
- Authorization: `Bearer ${engine.api_key}`,
- "api-key": `${engine.api_key}`,
+ 'Content-Type': 'application/json',
+ 'Accept': model.parameters.stream
+ ? 'text/event-stream'
+ : 'application/json',
+ 'Access-Control-Allow-Origin': '*',
+ 'Authorization': `Bearer ${engine.api_key}`,
+ 'api-key': `${engine.api_key}`,
},
body: requestBody,
signal: controller?.signal,
@@ -41,41 +41,41 @@ export function requestInference(
.then(async (response) => {
if (!response.ok) {
subscriber.next(
- (await response.json()).error?.message ?? "Error occured"
- );
- subscriber.complete();
- return;
+ (await response.json()).error?.message ?? 'Error occurred.'
+ )
+ subscriber.complete()
+ return
}
if (model.parameters.stream === false) {
- const data = await response.json();
- subscriber.next(data.choices[0]?.message?.content ?? "");
+ const data = await response.json()
+ subscriber.next(data.choices[0]?.message?.content ?? '')
} else {
- const stream = response.body;
- const decoder = new TextDecoder("utf-8");
- const reader = stream?.getReader();
- let content = "";
+ const stream = response.body
+ const decoder = new TextDecoder('utf-8')
+ const reader = stream?.getReader()
+ let content = ''
while (true && reader) {
- const { done, value } = await reader.read();
+ const { done, value } = await reader.read()
if (done) {
- break;
+ break
}
- const text = decoder.decode(value);
- const lines = text.trim().split("\n");
+ const text = decoder.decode(value)
+ const lines = text.trim().split('\n')
for (const line of lines) {
- if (line.startsWith("data: ") && !line.includes("data: [DONE]")) {
- const data = JSON.parse(line.replace("data: ", ""));
- content += data.choices[0]?.delta?.content ?? "";
- if (content.startsWith("assistant: ")) {
- content = content.replace("assistant: ", "");
+ if (line.startsWith('data: ') && !line.includes('data: [DONE]')) {
+ const data = JSON.parse(line.replace('data: ', ''))
+ content += data.choices[0]?.delta?.content ?? ''
+ if (content.startsWith('assistant: ')) {
+ content = content.replace('assistant: ', '')
}
- subscriber.next(content);
+ subscriber.next(content)
}
}
}
}
- subscriber.complete();
+ subscriber.complete()
})
- .catch((err) => subscriber.error(err));
- });
+ .catch((err) => subscriber.error(err))
+ })
}
diff --git a/extensions/inference-openai-extension/src/index.ts b/extensions/inference-openai-extension/src/index.ts
index fd1230bc7..481171742 100644
--- a/extensions/inference-openai-extension/src/index.ts
+++ b/extensions/inference-openai-extension/src/index.ts
@@ -18,14 +18,15 @@ import {
InferenceEngine,
BaseExtension,
MessageEvent,
+ MessageRequestType,
ModelEvent,
InferenceEvent,
AppConfigurationEventName,
joinPath,
-} from "@janhq/core";
-import { requestInference } from "./helpers/sse";
-import { ulid } from "ulid";
-import { join } from "path";
+} from '@janhq/core'
+import { requestInference } from './helpers/sse'
+import { ulid } from 'ulid'
+import { join } from 'path'
/**
* A class that implements the InferenceExtension interface from the @janhq/core package.
@@ -33,18 +34,18 @@ import { join } from "path";
* It also subscribes to events emitted by the @janhq/core package and handles new message requests.
*/
export default class JanInferenceOpenAIExtension extends BaseExtension {
- private static readonly _engineDir = "file://engines";
- private static readonly _engineMetadataFileName = "openai.json";
+ private static readonly _engineDir = 'file://engines'
+ private static readonly _engineMetadataFileName = 'openai.json'
- private static _currentModel: OpenAIModel;
+ private static _currentModel: OpenAIModel
private static _engineSettings: EngineSettings = {
- full_url: "https://api.openai.com/v1/chat/completions",
- api_key: "sk-",
- };
+ full_url: 'https://api.openai.com/v1/chat/completions',
+ api_key: 'sk-',
+ }
- controller = new AbortController();
- isCancelled = false;
+ controller = new AbortController()
+ isCancelled = false
/**
* Subscribes to events emitted by the @janhq/core package.
@@ -53,40 +54,40 @@ export default class JanInferenceOpenAIExtension extends BaseExtension {
if (!(await fs.existsSync(JanInferenceOpenAIExtension._engineDir))) {
await fs
.mkdirSync(JanInferenceOpenAIExtension._engineDir)
- .catch((err) => console.debug(err));
+ .catch((err) => console.debug(err))
}
- JanInferenceOpenAIExtension.writeDefaultEngineSettings();
+ JanInferenceOpenAIExtension.writeDefaultEngineSettings()
// Events subscription
events.on(MessageEvent.OnMessageSent, (data) =>
- JanInferenceOpenAIExtension.handleMessageRequest(data, this),
- );
+ JanInferenceOpenAIExtension.handleMessageRequest(data, this)
+ )
events.on(ModelEvent.OnModelInit, (model: OpenAIModel) => {
- JanInferenceOpenAIExtension.handleModelInit(model);
- });
+ JanInferenceOpenAIExtension.handleModelInit(model)
+ })
events.on(ModelEvent.OnModelStop, (model: OpenAIModel) => {
- JanInferenceOpenAIExtension.handleModelStop(model);
- });
+ JanInferenceOpenAIExtension.handleModelStop(model)
+ })
events.on(InferenceEvent.OnInferenceStopped, () => {
- JanInferenceOpenAIExtension.handleInferenceStopped(this);
- });
+ JanInferenceOpenAIExtension.handleInferenceStopped(this)
+ })
const settingsFilePath = await joinPath([
JanInferenceOpenAIExtension._engineDir,
JanInferenceOpenAIExtension._engineMetadataFileName,
- ]);
+ ])
events.on(
AppConfigurationEventName.OnConfigurationUpdate,
(settingsKey: string) => {
// Update settings on changes
if (settingsKey === settingsFilePath)
- JanInferenceOpenAIExtension.writeDefaultEngineSettings();
- },
- );
+ JanInferenceOpenAIExtension.writeDefaultEngineSettings()
+ }
+ )
}
/**
@@ -98,45 +99,45 @@ export default class JanInferenceOpenAIExtension extends BaseExtension {
try {
const engineFile = join(
JanInferenceOpenAIExtension._engineDir,
- JanInferenceOpenAIExtension._engineMetadataFileName,
- );
+ JanInferenceOpenAIExtension._engineMetadataFileName
+ )
if (await fs.existsSync(engineFile)) {
- const engine = await fs.readFileSync(engineFile, "utf-8");
+ const engine = await fs.readFileSync(engineFile, 'utf-8')
JanInferenceOpenAIExtension._engineSettings =
- typeof engine === "object" ? engine : JSON.parse(engine);
+ typeof engine === 'object' ? engine : JSON.parse(engine)
} else {
await fs.writeFileSync(
engineFile,
- JSON.stringify(JanInferenceOpenAIExtension._engineSettings, null, 2),
- );
+ JSON.stringify(JanInferenceOpenAIExtension._engineSettings, null, 2)
+ )
}
} catch (err) {
- console.error(err);
+ console.error(err)
}
}
private static async handleModelInit(model: OpenAIModel) {
if (model.engine !== InferenceEngine.openai) {
- return;
+ return
} else {
- JanInferenceOpenAIExtension._currentModel = model;
- JanInferenceOpenAIExtension.writeDefaultEngineSettings();
+ JanInferenceOpenAIExtension._currentModel = model
+ JanInferenceOpenAIExtension.writeDefaultEngineSettings()
// Todo: Check model list with API key
- events.emit(ModelEvent.OnModelReady, model);
+ events.emit(ModelEvent.OnModelReady, model)
}
}
private static async handleModelStop(model: OpenAIModel) {
- if (model.engine !== "openai") {
- return;
+ if (model.engine !== 'openai') {
+ return
}
- events.emit(ModelEvent.OnModelStopped, model);
+ events.emit(ModelEvent.OnModelStopped, model)
}
private static async handleInferenceStopped(
- instance: JanInferenceOpenAIExtension,
+ instance: JanInferenceOpenAIExtension
) {
- instance.isCancelled = true;
- instance.controller?.abort();
+ instance.isCancelled = true
+ instance.controller?.abort()
}
/**
@@ -147,28 +148,32 @@ export default class JanInferenceOpenAIExtension extends BaseExtension {
*/
private static async handleMessageRequest(
data: MessageRequest,
- instance: JanInferenceOpenAIExtension,
+ instance: JanInferenceOpenAIExtension
) {
- if (data.model.engine !== "openai") {
- return;
+ if (data.model.engine !== 'openai') {
+ return
}
- const timestamp = Date.now();
+ const timestamp = Date.now()
const message: ThreadMessage = {
id: ulid(),
thread_id: data.threadId,
+ type: data.type,
assistant_id: data.assistantId,
role: ChatCompletionRole.Assistant,
content: [],
status: MessageStatus.Pending,
created: timestamp,
updated: timestamp,
- object: "thread.message",
- };
- events.emit(MessageEvent.OnMessageResponse, message);
+ object: 'thread.message',
+ }
- instance.isCancelled = false;
- instance.controller = new AbortController();
+ if (data.type !== MessageRequestType.Summary) {
+ events.emit(MessageEvent.OnMessageResponse, message)
+ }
+
+ instance.isCancelled = false
+ instance.controller = new AbortController()
requestInference(
data?.messages ?? [],
@@ -177,7 +182,7 @@ export default class JanInferenceOpenAIExtension extends BaseExtension {
...JanInferenceOpenAIExtension._currentModel,
parameters: data.model.parameters,
},
- instance.controller,
+ instance.controller
).subscribe({
next: (content) => {
const messageContent: ThreadContent = {
@@ -186,33 +191,33 @@ export default class JanInferenceOpenAIExtension extends BaseExtension {
value: content.trim(),
annotations: [],
},
- };
- message.content = [messageContent];
- events.emit(MessageEvent.OnMessageUpdate, message);
+ }
+ message.content = [messageContent]
+ events.emit(MessageEvent.OnMessageUpdate, message)
},
complete: async () => {
message.status = message.content.length
? MessageStatus.Ready
- : MessageStatus.Error;
- events.emit(MessageEvent.OnMessageUpdate, message);
+ : MessageStatus.Error
+ events.emit(MessageEvent.OnMessageUpdate, message)
},
error: async (err) => {
if (instance.isCancelled || message.content.length > 0) {
- message.status = MessageStatus.Stopped;
- events.emit(MessageEvent.OnMessageUpdate, message);
- return;
+ message.status = MessageStatus.Stopped
+ events.emit(MessageEvent.OnMessageUpdate, message)
+ return
}
const messageContent: ThreadContent = {
type: ContentType.Text,
text: {
- value: "Error occurred: " + err.message,
+ value: 'Error occurred: ' + err.message,
annotations: [],
},
- };
- message.content = [messageContent];
- message.status = MessageStatus.Error;
- events.emit(MessageEvent.OnMessageUpdate, message);
+ }
+ message.content = [messageContent]
+ message.status = MessageStatus.Error
+ events.emit(MessageEvent.OnMessageUpdate, message)
},
- });
+ })
}
}
diff --git a/extensions/inference-openai-extension/tsconfig.json b/extensions/inference-openai-extension/tsconfig.json
index 7bfdd9009..2477d58ce 100644
--- a/extensions/inference-openai-extension/tsconfig.json
+++ b/extensions/inference-openai-extension/tsconfig.json
@@ -8,7 +8,7 @@
"forceConsistentCasingInFileNames": true,
"strict": false,
"skipLibCheck": true,
- "rootDir": "./src",
+ "rootDir": "./src"
},
- "include": ["./src"],
+ "include": ["./src"]
}
diff --git a/extensions/inference-openai-extension/webpack.config.js b/extensions/inference-openai-extension/webpack.config.js
index 72b7d90c1..ee2e3b624 100644
--- a/extensions/inference-openai-extension/webpack.config.js
+++ b/extensions/inference-openai-extension/webpack.config.js
@@ -1,16 +1,16 @@
-const path = require("path");
-const webpack = require("webpack");
-const packageJson = require("./package.json");
+const path = require('path')
+const webpack = require('webpack')
+const packageJson = require('./package.json')
module.exports = {
experiments: { outputModule: true },
- entry: "./src/index.ts", // Adjust the entry point to match your project's main file
- mode: "production",
+ entry: './src/index.ts', // Adjust the entry point to match your project's main file
+ mode: 'production',
module: {
rules: [
{
test: /\.tsx?$/,
- use: "ts-loader",
+ use: 'ts-loader',
exclude: /node_modules/,
},
],
@@ -18,22 +18,22 @@ module.exports = {
plugins: [
new webpack.DefinePlugin({
MODULE: JSON.stringify(`${packageJson.name}/${packageJson.module}`),
- OPENAI_DOMAIN: JSON.stringify("openai.azure.com"),
+ OPENAI_DOMAIN: JSON.stringify('openai.azure.com'),
}),
],
output: {
- filename: "index.js", // Adjust the output file name as needed
- path: path.resolve(__dirname, "dist"),
- library: { type: "module" }, // Specify ESM output format
+ filename: 'index.js', // Adjust the output file name as needed
+ path: path.resolve(__dirname, 'dist'),
+ library: { type: 'module' }, // Specify ESM output format
},
resolve: {
- extensions: [".ts", ".js"],
+ extensions: ['.ts', '.js'],
fallback: {
- path: require.resolve("path-browserify"),
+ path: require.resolve('path-browserify'),
},
},
optimization: {
minimize: false,
},
// Add loaders and other configuration as needed for your project
-};
+}
diff --git a/extensions/inference-triton-trtllm-extension/package.json b/extensions/inference-triton-trtllm-extension/package.json
index 0f4c2de23..455f8030e 100644
--- a/extensions/inference-triton-trtllm-extension/package.json
+++ b/extensions/inference-triton-trtllm-extension/package.json
@@ -18,13 +18,13 @@
"cpx": "^1.5.0",
"rimraf": "^3.0.2",
"webpack": "^5.88.2",
- "webpack-cli": "^5.1.4"
+ "webpack-cli": "^5.1.4",
+ "ts-loader": "^9.5.0"
},
"dependencies": {
"@janhq/core": "file:../../core",
"fetch-retry": "^5.0.6",
"path-browserify": "^1.0.1",
- "ts-loader": "^9.5.0",
"ulid": "^2.3.0",
"rxjs": "^7.8.1"
},
diff --git a/extensions/inference-triton-trtllm-extension/src/@types/global.d.ts b/extensions/inference-triton-trtllm-extension/src/@types/global.d.ts
index 6224b8e68..c834feba0 100644
--- a/extensions/inference-triton-trtllm-extension/src/@types/global.d.ts
+++ b/extensions/inference-triton-trtllm-extension/src/@types/global.d.ts
@@ -1,5 +1,5 @@
-import { Model } from "@janhq/core";
+import { Model } from '@janhq/core'
declare interface EngineSettings {
- base_url?: string;
+ base_url?: string
}
diff --git a/extensions/inference-triton-trtllm-extension/src/helpers/sse.ts b/extensions/inference-triton-trtllm-extension/src/helpers/sse.ts
index da20fa32d..9aff61265 100644
--- a/extensions/inference-triton-trtllm-extension/src/helpers/sse.ts
+++ b/extensions/inference-triton-trtllm-extension/src/helpers/sse.ts
@@ -1,6 +1,6 @@
-import { Observable } from "rxjs";
-import { EngineSettings } from "../@types/global";
-import { Model } from "@janhq/core";
+import { Observable } from 'rxjs'
+import { EngineSettings } from '../@types/global'
+import { Model } from '@janhq/core'
/**
* Sends a request to the inference server to generate a response based on the recent messages.
@@ -16,48 +16,48 @@ export function requestInference(
controller?: AbortController
): Observable {
return new Observable((subscriber) => {
- const text_input = recentMessages.map((message) => message.text).join("\n");
+ const text_input = recentMessages.map((message) => message.text).join('\n')
const requestBody = JSON.stringify({
text_input: text_input,
max_tokens: 4096,
temperature: 0,
- bad_words: "",
- stop_words: "[DONE]",
- stream: true
- });
+ bad_words: '',
+ stop_words: '[DONE]',
+ stream: true,
+ })
fetch(`${engine.base_url}/v2/models/ensemble/generate_stream`, {
- method: "POST",
+ method: 'POST',
headers: {
- "Content-Type": "application/json",
- Accept: "text/event-stream",
- "Access-Control-Allow-Origin": "*",
+ 'Content-Type': 'application/json',
+ 'Accept': 'text/event-stream',
+ 'Access-Control-Allow-Origin': '*',
},
body: requestBody,
signal: controller?.signal,
})
.then(async (response) => {
- const stream = response.body;
- const decoder = new TextDecoder("utf-8");
- const reader = stream?.getReader();
- let content = "";
+ const stream = response.body
+ const decoder = new TextDecoder('utf-8')
+ const reader = stream?.getReader()
+ let content = ''
while (true && reader) {
- const { done, value } = await reader.read();
+ const { done, value } = await reader.read()
if (done) {
- break;
+ break
}
- const text = decoder.decode(value);
- const lines = text.trim().split("\n");
+ const text = decoder.decode(value)
+ const lines = text.trim().split('\n')
for (const line of lines) {
- if (line.startsWith("data: ") && !line.includes("data: [DONE]")) {
- const data = JSON.parse(line.replace("data: ", ""));
- content += data.choices[0]?.delta?.content ?? "";
- subscriber.next(content);
+ if (line.startsWith('data: ') && !line.includes('data: [DONE]')) {
+ const data = JSON.parse(line.replace('data: ', ''))
+ content += data.choices[0]?.delta?.content ?? ''
+ subscriber.next(content)
}
}
}
- subscriber.complete();
+ subscriber.complete()
})
- .catch((err) => subscriber.error(err));
- });
+ .catch((err) => subscriber.error(err))
+ })
}
diff --git a/extensions/inference-triton-trtllm-extension/src/index.ts b/extensions/inference-triton-trtllm-extension/src/index.ts
index 11ddf7893..f009a81e0 100644
--- a/extensions/inference-triton-trtllm-extension/src/index.ts
+++ b/extensions/inference-triton-trtllm-extension/src/index.ts
@@ -20,51 +20,49 @@ import {
BaseExtension,
MessageEvent,
ModelEvent,
-} from "@janhq/core";
-import { requestInference } from "./helpers/sse";
-import { ulid } from "ulid";
-import { join } from "path";
-import { EngineSettings } from "./@types/global";
+} from '@janhq/core'
+import { requestInference } from './helpers/sse'
+import { ulid } from 'ulid'
+import { join } from 'path'
+import { EngineSettings } from './@types/global'
/**
* A class that implements the InferenceExtension interface from the @janhq/core package.
* The class provides methods for initializing and stopping a model, and for making inference requests.
* It also subscribes to events emitted by the @janhq/core package and handles new message requests.
*/
-export default class JanInferenceTritonTrtLLMExtension
- extends BaseExtension
-{
- private static readonly _homeDir = "file://engines";
- private static readonly _engineMetadataFileName = "triton_trtllm.json";
+export default class JanInferenceTritonTrtLLMExtension extends BaseExtension {
+ private static readonly _homeDir = 'file://engines'
+ private static readonly _engineMetadataFileName = 'triton_trtllm.json'
- static _currentModel: Model;
+ static _currentModel: Model
static _engineSettings: EngineSettings = {
- base_url: "",
- };
+ base_url: '',
+ }
- controller = new AbortController();
- isCancelled = false;
+ controller = new AbortController()
+ isCancelled = false
/**
* Subscribes to events emitted by the @janhq/core package.
*/
async onLoad() {
if (!(await fs.existsSync(JanInferenceTritonTrtLLMExtension._homeDir)))
- JanInferenceTritonTrtLLMExtension.writeDefaultEngineSettings();
+ JanInferenceTritonTrtLLMExtension.writeDefaultEngineSettings()
// Events subscription
events.on(MessageEvent.OnMessageSent, (data) =>
JanInferenceTritonTrtLLMExtension.handleMessageRequest(data, this)
- );
+ )
events.on(ModelEvent.OnModelInit, (model: Model) => {
- JanInferenceTritonTrtLLMExtension.handleModelInit(model);
- });
+ JanInferenceTritonTrtLLMExtension.handleModelInit(model)
+ })
events.on(ModelEvent.OnModelStop, (model: Model) => {
- JanInferenceTritonTrtLLMExtension.handleModelStop(model);
- });
+ JanInferenceTritonTrtLLMExtension.handleModelStop(model)
+ })
}
/**
@@ -81,7 +79,7 @@ export default class JanInferenceTritonTrtLLMExtension
modelId: string,
settings?: ModelSettingParams
): Promise {
- return;
+ return
}
static async writeDefaultEngineSettings() {
@@ -89,11 +87,11 @@ export default class JanInferenceTritonTrtLLMExtension
const engine_json = join(
JanInferenceTritonTrtLLMExtension._homeDir,
JanInferenceTritonTrtLLMExtension._engineMetadataFileName
- );
+ )
if (await fs.existsSync(engine_json)) {
- const engine = await fs.readFileSync(engine_json, "utf-8");
+ const engine = await fs.readFileSync(engine_json, 'utf-8')
JanInferenceTritonTrtLLMExtension._engineSettings =
- typeof engine === "object" ? engine : JSON.parse(engine);
+ typeof engine === 'object' ? engine : JSON.parse(engine)
} else {
await fs.writeFileSync(
engine_json,
@@ -102,10 +100,10 @@ export default class JanInferenceTritonTrtLLMExtension
null,
2
)
- );
+ )
}
} catch (err) {
- console.error(err);
+ console.error(err)
}
}
/**
@@ -119,26 +117,26 @@ export default class JanInferenceTritonTrtLLMExtension
* @returns {Promise} A promise that resolves when the streaming is stopped.
*/
async stopInference(): Promise {
- this.isCancelled = true;
- this.controller?.abort();
+ this.isCancelled = true
+ this.controller?.abort()
}
private static async handleModelInit(model: Model) {
- if (model.engine !== "triton_trtllm") {
- return;
+ if (model.engine !== 'triton_trtllm') {
+ return
} else {
- JanInferenceTritonTrtLLMExtension._currentModel = model;
- JanInferenceTritonTrtLLMExtension.writeDefaultEngineSettings();
+ JanInferenceTritonTrtLLMExtension._currentModel = model
+ JanInferenceTritonTrtLLMExtension.writeDefaultEngineSettings()
// Todo: Check model list with API key
- events.emit(ModelEvent.OnModelReady, model);
+ events.emit(ModelEvent.OnModelReady, model)
}
}
private static async handleModelStop(model: Model) {
- if (model.engine !== "triton_trtllm") {
- return;
+ if (model.engine !== 'triton_trtllm') {
+ return
}
- events.emit(ModelEvent.OnModelStopped, model);
+ events.emit(ModelEvent.OnModelStopped, model)
}
/**
@@ -151,11 +149,11 @@ export default class JanInferenceTritonTrtLLMExtension
data: MessageRequest,
instance: JanInferenceTritonTrtLLMExtension
) {
- if (data.model.engine !== "triton_trtllm") {
- return;
+ if (data.model.engine !== 'triton_trtllm') {
+ return
}
- const timestamp = Date.now();
+ const timestamp = Date.now()
const message: ThreadMessage = {
id: ulid(),
thread_id: data.threadId,
@@ -165,12 +163,12 @@ export default class JanInferenceTritonTrtLLMExtension
status: MessageStatus.Pending,
created: timestamp,
updated: timestamp,
- object: "thread.message",
- };
- events.emit(MessageEvent.OnMessageResponse, message);
+ object: 'thread.message',
+ }
+ events.emit(MessageEvent.OnMessageResponse, message)
- instance.isCancelled = false;
- instance.controller = new AbortController();
+ instance.isCancelled = false
+ instance.controller = new AbortController()
requestInference(
data?.messages ?? [],
@@ -188,33 +186,33 @@ export default class JanInferenceTritonTrtLLMExtension
value: content.trim(),
annotations: [],
},
- };
- message.content = [messageContent];
- events.emit(MessageEvent.OnMessageUpdate, message);
+ }
+ message.content = [messageContent]
+ events.emit(MessageEvent.OnMessageUpdate, message)
},
complete: async () => {
message.status = message.content.length
? MessageStatus.Ready
- : MessageStatus.Error;
- events.emit(MessageEvent.OnMessageUpdate, message);
+ : MessageStatus.Error
+ events.emit(MessageEvent.OnMessageUpdate, message)
},
error: async (err) => {
if (instance.isCancelled || message.content.length) {
- message.status = MessageStatus.Error;
- events.emit(MessageEvent.OnMessageUpdate, message);
- return;
+ message.status = MessageStatus.Error
+ events.emit(MessageEvent.OnMessageUpdate, message)
+ return
}
const messageContent: ThreadContent = {
type: ContentType.Text,
text: {
- value: "Error occurred: " + err.message,
+ value: 'Error occurred: ' + err.message,
annotations: [],
},
- };
- message.content = [messageContent];
- message.status = MessageStatus.Ready;
- events.emit(MessageEvent.OnMessageUpdate, message);
+ }
+ message.content = [messageContent]
+ message.status = MessageStatus.Ready
+ events.emit(MessageEvent.OnMessageUpdate, message)
},
- });
+ })
}
}
diff --git a/extensions/inference-triton-trtllm-extension/tsconfig.json b/extensions/inference-triton-trtllm-extension/tsconfig.json
index 7bfdd9009..2477d58ce 100644
--- a/extensions/inference-triton-trtllm-extension/tsconfig.json
+++ b/extensions/inference-triton-trtllm-extension/tsconfig.json
@@ -8,7 +8,7 @@
"forceConsistentCasingInFileNames": true,
"strict": false,
"skipLibCheck": true,
- "rootDir": "./src",
+ "rootDir": "./src"
},
- "include": ["./src"],
+ "include": ["./src"]
}
diff --git a/extensions/inference-triton-trtllm-extension/webpack.config.js b/extensions/inference-triton-trtllm-extension/webpack.config.js
index 57a0adb0a..e83370a1a 100644
--- a/extensions/inference-triton-trtllm-extension/webpack.config.js
+++ b/extensions/inference-triton-trtllm-extension/webpack.config.js
@@ -1,16 +1,16 @@
-const path = require("path");
-const webpack = require("webpack");
-const packageJson = require("./package.json");
+const path = require('path')
+const webpack = require('webpack')
+const packageJson = require('./package.json')
module.exports = {
experiments: { outputModule: true },
- entry: "./src/index.ts", // Adjust the entry point to match your project's main file
- mode: "production",
+ entry: './src/index.ts', // Adjust the entry point to match your project's main file
+ mode: 'production',
module: {
rules: [
{
test: /\.tsx?$/,
- use: "ts-loader",
+ use: 'ts-loader',
exclude: /node_modules/,
},
],
@@ -21,18 +21,18 @@ module.exports = {
}),
],
output: {
- filename: "index.js", // Adjust the output file name as needed
- path: path.resolve(__dirname, "dist"),
- library: { type: "module" }, // Specify ESM output format
+ filename: 'index.js', // Adjust the output file name as needed
+ path: path.resolve(__dirname, 'dist'),
+ library: { type: 'module' }, // Specify ESM output format
},
resolve: {
- extensions: [".ts", ".js"],
+ extensions: ['.ts', '.js'],
fallback: {
- path: require.resolve("path-browserify"),
+ path: require.resolve('path-browserify'),
},
},
optimization: {
minimize: false,
},
// Add loaders and other configuration as needed for your project
-};
+}
diff --git a/extensions/model-extension/.prettierrc b/extensions/model-extension/.prettierrc
deleted file mode 100644
index 46f1abcb0..000000000
--- a/extensions/model-extension/.prettierrc
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "semi": false,
- "singleQuote": true,
- "quoteProps": "consistent",
- "trailingComma": "es5",
- "endOfLine": "auto",
- "plugins": ["prettier-plugin-tailwindcss"]
-}
diff --git a/extensions/model-extension/package.json b/extensions/model-extension/package.json
index 1af5d38cb..5d1674007 100644
--- a/extensions/model-extension/package.json
+++ b/extensions/model-extension/package.json
@@ -14,7 +14,8 @@
"cpx": "^1.5.0",
"rimraf": "^3.0.2",
"webpack": "^5.88.2",
- "webpack-cli": "^5.1.4"
+ "webpack-cli": "^5.1.4",
+ "ts-loader": "^9.5.0"
},
"files": [
"dist/*",
@@ -23,7 +24,6 @@
],
"dependencies": {
"@janhq/core": "file:../../core",
- "path-browserify": "^1.0.1",
- "ts-loader": "^9.5.0"
+ "path-browserify": "^1.0.1"
}
}
diff --git a/extensions/model-extension/src/index.ts b/extensions/model-extension/src/index.ts
index e26fd4929..61a7db005 100644
--- a/extensions/model-extension/src/index.ts
+++ b/extensions/model-extension/src/index.ts
@@ -219,15 +219,20 @@ export default class JanModelExtension extends ModelExtension {
async getDownloadedModels(): Promise {
return await this.getModelsMetadata(
async (modelDir: string, model: Model) => {
- if (model.engine !== JanModelExtension._offlineInferenceEngine) {
+ if (model.engine !== JanModelExtension._offlineInferenceEngine)
return true
- }
+
+ // model binaries (sources) are absolute path & exist
+ const existFiles = await Promise.all(
+ model.sources.map((source) => fs.existsSync(source.url))
+ )
+ if (existFiles.every((exist) => exist)) 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
- // TODO: Check diff between urls, filenames
+ // 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.filter(
@@ -273,8 +278,19 @@ export default class JanModelExtension extends ModelExtension {
if (await fs.existsSync(jsonPath)) {
// if we have the model.json file, read it
let model = await this.readModelMetadata(jsonPath)
+
model = typeof model === 'object' ? model : JSON.parse(model)
+ // This to ensure backward compatibility with `model.json` with `source_url`
+ if (model['source_url'] != null) {
+ model['sources'] = [
+ {
+ filename: model.id,
+ url: model['source_url'],
+ },
+ ]
+ }
+
if (selector && !(await selector?.(dirName, model))) {
return
}
@@ -288,31 +304,18 @@ export default class JanModelExtension extends ModelExtension {
})
const results = await Promise.allSettled(readJsonPromises)
const modelData = results.map((result) => {
- if (result.status === 'fulfilled') {
+ if (result.status === 'fulfilled' && result.value) {
try {
- // This to ensure backward compatibility with `model.json` with `source_url`
- const tmpModel =
+ const model =
typeof result.value === 'object'
? result.value
: JSON.parse(result.value)
- if (tmpModel['source_url'] != null) {
- tmpModel['source'] = [
- {
- filename: tmpModel.id,
- url: tmpModel['source_url'],
- },
- ]
- }
-
- return tmpModel as Model
+ return model as Model
} catch {
console.debug(`Unable to parse model metadata: ${result.value}`)
- return undefined
}
- } else {
- console.error(result.reason)
- return undefined
}
+ return undefined
})
return modelData.filter((e) => !!e)
diff --git a/extensions/model-extension/tsconfig.json b/extensions/model-extension/tsconfig.json
index c175d9437..addd8e127 100644
--- a/extensions/model-extension/tsconfig.json
+++ b/extensions/model-extension/tsconfig.json
@@ -8,7 +8,7 @@
"forceConsistentCasingInFileNames": true,
"strict": false,
"skipLibCheck": true,
- "rootDir": "./src",
+ "rootDir": "./src"
},
- "include": ["./src"],
+ "include": ["./src"]
}
diff --git a/extensions/model-extension/webpack.config.js b/extensions/model-extension/webpack.config.js
index c67bf8dc0..347719f91 100644
--- a/extensions/model-extension/webpack.config.js
+++ b/extensions/model-extension/webpack.config.js
@@ -19,7 +19,7 @@ module.exports = {
new webpack.DefinePlugin({
EXTENSION_NAME: JSON.stringify(packageJson.name),
MODULE_PATH: JSON.stringify(`${packageJson.name}/${packageJson.module}`),
- VERSION: JSON.stringify(packageJson.version)
+ VERSION: JSON.stringify(packageJson.version),
}),
],
output: {
diff --git a/extensions/monitoring-extension/package.json b/extensions/monitoring-extension/package.json
index 538f6bdee..582f7cd7b 100644
--- a/extensions/monitoring-extension/package.json
+++ b/extensions/monitoring-extension/package.json
@@ -13,12 +13,12 @@
"devDependencies": {
"rimraf": "^3.0.2",
"webpack": "^5.88.2",
- "webpack-cli": "^5.1.4"
+ "webpack-cli": "^5.1.4",
+ "ts-loader": "^9.5.0"
},
"dependencies": {
"@janhq/core": "file:../../core",
- "node-os-utils": "^1.3.7",
- "ts-loader": "^9.5.0"
+ "node-os-utils": "^1.3.7"
},
"files": [
"dist/*",
diff --git a/extensions/monitoring-extension/src/@types/global.d.ts b/extensions/monitoring-extension/src/@types/global.d.ts
index 3b45ccc5a..8106353cf 100644
--- a/extensions/monitoring-extension/src/@types/global.d.ts
+++ b/extensions/monitoring-extension/src/@types/global.d.ts
@@ -1 +1 @@
-declare const MODULE: string;
+declare const MODULE: string
diff --git a/extensions/monitoring-extension/src/index.ts b/extensions/monitoring-extension/src/index.ts
index 9297a770f..ce9b2fc14 100644
--- a/extensions/monitoring-extension/src/index.ts
+++ b/extensions/monitoring-extension/src/index.ts
@@ -1,4 +1,4 @@
-import { MonitoringExtension, executeOnMain } from "@janhq/core";
+import { MonitoringExtension, executeOnMain } from '@janhq/core'
/**
* JanMonitoringExtension is a extension that provides system monitoring functionality.
@@ -20,7 +20,7 @@ export default class JanMonitoringExtension extends MonitoringExtension {
* @returns A Promise that resolves to an object containing information about the system resources.
*/
getResourcesInfo(): Promise {
- return executeOnMain(MODULE, "getResourcesInfo");
+ return executeOnMain(MODULE, 'getResourcesInfo')
}
/**
@@ -28,6 +28,6 @@ export default class JanMonitoringExtension extends MonitoringExtension {
* @returns A Promise that resolves to an object containing information about the current system load.
*/
getCurrentLoad(): Promise {
- return executeOnMain(MODULE, "getCurrentLoad");
+ return executeOnMain(MODULE, 'getCurrentLoad')
}
}
diff --git a/extensions/monitoring-extension/src/module.ts b/extensions/monitoring-extension/src/module.ts
index 2c1b14343..ea7319b47 100644
--- a/extensions/monitoring-extension/src/module.ts
+++ b/extensions/monitoring-extension/src/module.ts
@@ -1,73 +1,92 @@
-const nodeOsUtils = require("node-os-utils");
-const getJanDataFolderPath = require("@janhq/core/node").getJanDataFolderPath;
-const path = require("path");
-const { readFileSync } = require("fs");
-const exec = require("child_process").exec;
+const nodeOsUtils = require('node-os-utils')
+const getJanDataFolderPath = require('@janhq/core/node').getJanDataFolderPath
+const path = require('path')
+const { readFileSync } = require('fs')
+const exec = require('child_process').exec
const NVIDIA_INFO_FILE = path.join(
getJanDataFolderPath(),
- "settings",
- "settings.json"
-);
+ 'settings',
+ 'settings.json'
+)
const getResourcesInfo = () =>
new Promise((resolve) => {
nodeOsUtils.mem.used().then((ramUsedInfo) => {
- const totalMemory = ramUsedInfo.totalMemMb * 1024 * 1024;
- const usedMemory = ramUsedInfo.usedMemMb * 1024 * 1024;
+ const totalMemory = ramUsedInfo.totalMemMb * 1024 * 1024
+ const usedMemory = ramUsedInfo.usedMemMb * 1024 * 1024
const response = {
mem: {
totalMemory,
usedMemory,
},
- };
- resolve(response);
- });
- });
+ }
+ resolve(response)
+ })
+ })
const getCurrentLoad = () =>
new Promise((resolve, reject) => {
nodeOsUtils.cpu.usage().then((cpuPercentage) => {
let data = {
- run_mode: "cpu",
+ run_mode: 'cpu',
gpus_in_use: [],
- };
- if (process.platform !== "darwin") {
- data = JSON.parse(readFileSync(NVIDIA_INFO_FILE, "utf-8"));
}
- if (data.run_mode === "gpu" && data.gpus_in_use.length > 0) {
- const gpuIds = data["gpus_in_use"].join(",");
- if (gpuIds !== "") {
+ if (process.platform !== 'darwin') {
+ data = JSON.parse(readFileSync(NVIDIA_INFO_FILE, 'utf-8'))
+ }
+ if (data.run_mode === 'gpu' && data.gpus_in_use.length > 0) {
+ const gpuIds = data['gpus_in_use'].join(',')
+ if (gpuIds !== '') {
exec(
`nvidia-smi --query-gpu=index,name,temperature.gpu,utilization.gpu,memory.total,memory.free,utilization.memory --format=csv,noheader,nounits --id=${gpuIds}`,
(error, stdout, stderr) => {
if (error) {
- console.error(`exec error: ${error}`);
- reject(error);
- return;
+ console.error(`exec error: ${error}`)
+ reject(error)
+ return
}
- const gpuInfo = stdout.trim().split("\n").map((line) => {
- const [id, name, temperature, utilization, memoryTotal, memoryFree, memoryUtilization] = line.split(", ").map(item => item.replace(/\r/g, ""));
- return { id, name, temperature, utilization, memoryTotal, memoryFree, memoryUtilization };
- });
+ const gpuInfo = stdout
+ .trim()
+ .split('\n')
+ .map((line) => {
+ const [
+ id,
+ name,
+ temperature,
+ utilization,
+ memoryTotal,
+ memoryFree,
+ memoryUtilization,
+ ] = line.split(', ').map((item) => item.replace(/\r/g, ''))
+ return {
+ id,
+ name,
+ temperature,
+ utilization,
+ memoryTotal,
+ memoryFree,
+ memoryUtilization,
+ }
+ })
resolve({
cpu: { usage: cpuPercentage },
- gpu: gpuInfo
- });
+ gpu: gpuInfo,
+ })
}
- );
+ )
} else {
// Handle the case where gpuIds is empty
- resolve({ cpu: { usage: cpuPercentage }, gpu: [] });
+ resolve({ cpu: { usage: cpuPercentage }, gpu: [] })
}
} else {
// Handle the case where run_mode is not 'gpu' or no GPUs are in use
- resolve({ cpu: { usage: cpuPercentage }, gpu: [] });
+ resolve({ cpu: { usage: cpuPercentage }, gpu: [] })
}
- });
- });
+ })
+ })
module.exports = {
getResourcesInfo,
getCurrentLoad,
-};
\ No newline at end of file
+}
diff --git a/extensions/monitoring-extension/webpack.config.js b/extensions/monitoring-extension/webpack.config.js
index f54059222..c8c3a34f7 100644
--- a/extensions/monitoring-extension/webpack.config.js
+++ b/extensions/monitoring-extension/webpack.config.js
@@ -1,24 +1,24 @@
-const path = require("path");
-const webpack = require("webpack");
-const packageJson = require("./package.json");
+const path = require('path')
+const webpack = require('webpack')
+const packageJson = require('./package.json')
module.exports = {
experiments: { outputModule: true },
- entry: "./src/index.ts", // Adjust the entry point to match your project's main file
- mode: "production",
+ entry: './src/index.ts', // Adjust the entry point to match your project's main file
+ mode: 'production',
module: {
rules: [
{
test: /\.tsx?$/,
- use: "ts-loader",
+ use: 'ts-loader',
exclude: /node_modules/,
},
],
},
output: {
- filename: "index.js", // Adjust the output file name as needed
- path: path.resolve(__dirname, "dist"),
- library: { type: "module" }, // Specify ESM output format
+ filename: 'index.js', // Adjust the output file name as needed
+ path: path.resolve(__dirname, 'dist'),
+ library: { type: 'module' }, // Specify ESM output format
},
plugins: [
new webpack.DefinePlugin({
@@ -26,10 +26,10 @@ module.exports = {
}),
],
resolve: {
- extensions: [".ts", ".js"],
+ extensions: ['.ts', '.js'],
},
optimization: {
minimize: false,
},
// Add loaders and other configuration as needed for your project
-};
+}
diff --git a/models/mistral-ins-7b-q4/cover.png b/models/mistral-ins-7b-q4/cover.png
index 000445ecb..73b82e599 100644
Binary files a/models/mistral-ins-7b-q4/cover.png and b/models/mistral-ins-7b-q4/cover.png differ
diff --git a/models/openhermes-neural-7b/cover.png b/models/openhermes-neural-7b/cover.png
index 5b9da0aef..8976d8449 100644
Binary files a/models/openhermes-neural-7b/cover.png and b/models/openhermes-neural-7b/cover.png differ
diff --git a/models/trinity-v1.2-7b/cover.png b/models/trinity-v1.2-7b/cover.png
index a548e3c17..fbef0bb56 100644
Binary files a/models/trinity-v1.2-7b/cover.png and b/models/trinity-v1.2-7b/cover.png differ
diff --git a/server/helpers/setup.ts b/server/helpers/setup.ts
index 51d8eebe5..e6fab7a25 100644
--- a/server/helpers/setup.ts
+++ b/server/helpers/setup.ts
@@ -1,47 +1,47 @@
-import { join, extname } from "path";
-import { existsSync, readdirSync, writeFileSync, mkdirSync } from "fs";
-import { init, installExtensions } from "@janhq/core/node";
+import { join, extname } from 'path'
+import { existsSync, readdirSync, writeFileSync, mkdirSync } from 'fs'
+import { init, installExtensions } from '@janhq/core/node'
export async function setup() {
/**
* Setup Jan Data Directory
*/
- const appDir = process.env.JAN_DATA_DIRECTORY ?? join(__dirname, "..", "jan");
+ const appDir = process.env.JAN_DATA_DIRECTORY ?? join(__dirname, '..', 'jan')
- console.debug(`Create app data directory at ${appDir}...`);
- if (!existsSync(appDir)) mkdirSync(appDir);
+ console.debug(`Create app data directory at ${appDir}...`)
+ if (!existsSync(appDir)) mkdirSync(appDir)
//@ts-ignore
global.core = {
// Define appPath function for app to retrieve app path globaly
appPath: () => appDir,
- };
+ }
init({
- extensionsPath: join(appDir, "extensions"),
- });
+ extensionsPath: join(appDir, 'extensions'),
+ })
/**
* Write app configurations. See #1619
*/
- console.debug("Writing config file...");
+ console.debug('Writing config file...')
writeFileSync(
- join(appDir, "settings.json"),
+ join(appDir, 'settings.json'),
JSON.stringify({
data_folder: appDir,
}),
- "utf-8"
- );
+ 'utf-8'
+ )
/**
* Install extensions
*/
- console.debug("Installing extensions...");
+ console.debug('Installing extensions...')
- const baseExtensionPath = join(__dirname, "../../..", "pre-install");
+ const baseExtensionPath = join(__dirname, '../../..', 'pre-install')
const extensions = readdirSync(baseExtensionPath)
- .filter((file) => extname(file) === ".tgz")
- .map((file) => join(baseExtensionPath, file));
+ .filter((file) => extname(file) === '.tgz')
+ .map((file) => join(baseExtensionPath, file))
- await installExtensions(extensions);
- console.debug("Extensions installed");
+ await installExtensions(extensions)
+ console.debug('Extensions installed')
}
diff --git a/server/index.ts b/server/index.ts
index 91349a81f..dc518781f 100644
--- a/server/index.ts
+++ b/server/index.ts
@@ -1,26 +1,26 @@
-import fastify from "fastify";
-import dotenv from "dotenv";
+import fastify from 'fastify'
+import dotenv from 'dotenv'
import {
getServerLogPath,
v1Router,
logServer,
getJanExtensionsPath,
-} from "@janhq/core/node";
-import { join } from "path";
+} from '@janhq/core/node'
+import { join } from 'path'
// Load environment variables
-dotenv.config();
+dotenv.config()
// Define default settings
-const JAN_API_HOST = process.env.JAN_API_HOST || "127.0.0.1";
-const JAN_API_PORT = Number.parseInt(process.env.JAN_API_PORT || "1337");
+const JAN_API_HOST = process.env.JAN_API_HOST || '127.0.0.1'
+const JAN_API_PORT = Number.parseInt(process.env.JAN_API_PORT || '1337')
// Initialize server settings
-let server: any | undefined = undefined;
-let hostSetting: string = JAN_API_HOST;
-let portSetting: number = JAN_API_PORT;
-let corsEnabled: boolean = true;
-let isVerbose: boolean = true;
+let server: any | undefined = undefined
+let hostSetting: string = JAN_API_HOST
+let portSetting: number = JAN_API_PORT
+let corsEnabled: boolean = true
+let isVerbose: boolean = true
/**
* Server configurations
@@ -32,13 +32,13 @@ let isVerbose: boolean = true;
* @param baseDir - Base directory for the OpenAPI schema file
*/
export interface ServerConfig {
- host?: string;
- port?: number;
- isCorsEnabled?: boolean;
- isVerboseEnabled?: boolean;
- schemaPath?: string;
- baseDir?: string;
- storageAdataper?: any;
+ host?: string
+ port?: number
+ isCorsEnabled?: boolean
+ isVerboseEnabled?: boolean
+ schemaPath?: string
+ baseDir?: string
+ storageAdataper?: any
}
/**
@@ -47,69 +47,69 @@ export interface ServerConfig {
*/
export const startServer = async (configs?: ServerConfig) => {
// Update server settings
- isVerbose = configs?.isVerboseEnabled ?? true;
- hostSetting = configs?.host ?? JAN_API_HOST;
- portSetting = configs?.port ?? JAN_API_PORT;
- corsEnabled = configs?.isCorsEnabled ?? true;
- const serverLogPath = getServerLogPath();
+ isVerbose = configs?.isVerboseEnabled ?? true
+ hostSetting = configs?.host ?? JAN_API_HOST
+ portSetting = configs?.port ?? JAN_API_PORT
+ corsEnabled = configs?.isCorsEnabled ?? true
+ const serverLogPath = getServerLogPath()
// Start the server
try {
// Log server start
- if (isVerbose) logServer(`Debug: Starting JAN API server...`);
+ if (isVerbose) logServer(`Debug: Starting JAN API server...`)
// Initialize Fastify server with logging
server = fastify({
logger: {
- level: "info",
+ level: 'info',
file: serverLogPath,
},
- });
+ })
// Register CORS if enabled
- if (corsEnabled) await server.register(require("@fastify/cors"), {});
+ if (corsEnabled) await server.register(require('@fastify/cors'), {})
// Register Swagger for API documentation
- await server.register(require("@fastify/swagger"), {
- mode: "static",
+ await server.register(require('@fastify/swagger'), {
+ mode: 'static',
specification: {
- path: configs?.schemaPath ?? "./../docs/openapi/jan.yaml",
- baseDir: configs?.baseDir ?? "./../docs/openapi",
+ path: configs?.schemaPath ?? './../docs/openapi/jan.yaml',
+ baseDir: configs?.baseDir ?? './../docs/openapi',
},
- });
+ })
// Register Swagger UI
- await server.register(require("@fastify/swagger-ui"), {
- routePrefix: "/",
- baseDir: configs?.baseDir ?? join(__dirname, "../..", "./docs/openapi"),
+ await server.register(require('@fastify/swagger-ui'), {
+ routePrefix: '/',
+ baseDir: configs?.baseDir ?? join(__dirname, '../..', './docs/openapi'),
uiConfig: {
- docExpansion: "full",
+ docExpansion: 'full',
deepLinking: false,
},
staticCSP: false,
transformSpecificationClone: true,
- });
+ })
// Register static file serving for extensions
// TODO: Watch extension files changes and reload
await server.register(
(childContext: any, _: any, done: any) => {
- childContext.register(require("@fastify/static"), {
+ childContext.register(require('@fastify/static'), {
root: getJanExtensionsPath(),
wildcard: false,
- });
+ })
- done();
+ done()
},
- { prefix: "extensions" }
- );
+ { prefix: 'extensions' }
+ )
// Register proxy middleware
if (configs?.storageAdataper)
- server.addHook("preHandler", configs.storageAdataper);
+ server.addHook('preHandler', configs.storageAdataper)
// Register API routes
- await server.register(v1Router, { prefix: "/v1" });
+ await server.register(v1Router, { prefix: '/v1' })
// Start listening for requests
await server
.listen({
@@ -121,13 +121,13 @@ export const startServer = async (configs?: ServerConfig) => {
if (isVerbose)
logServer(
`Debug: JAN API listening at: http://${hostSetting}:${portSetting}`
- );
- });
+ )
+ })
} catch (e) {
// Log any errors
- if (isVerbose) logServer(`Error: ${e}`);
+ if (isVerbose) logServer(`Error: ${e}`)
}
-};
+}
/**
* Function to stop the server
@@ -135,11 +135,11 @@ export const startServer = async (configs?: ServerConfig) => {
export const stopServer = async () => {
try {
// Log server stop
- if (isVerbose) logServer(`Debug: Server stopped`);
+ if (isVerbose) logServer(`Debug: Server stopped`)
// Stop the server
- await server.close();
+ await server.close()
} catch (e) {
// Log any errors
- if (isVerbose) logServer(`Error: ${e}`);
+ if (isVerbose) logServer(`Error: ${e}`)
}
-};
+}
diff --git a/server/main.ts b/server/main.ts
index 3be397e6f..71fb11106 100644
--- a/server/main.ts
+++ b/server/main.ts
@@ -1,7 +1,7 @@
-import { s3 } from "./middleware/s3";
-import { setup } from "./helpers/setup";
-import { startServer as start } from "./index";
+import { s3 } from './middleware/s3'
+import { setup } from './helpers/setup'
+import { startServer as start } from './index'
/**
* Setup extensions and start the server
*/
-setup().then(() => start({ storageAdataper: s3 }));
+setup().then(() => start({ storageAdataper: s3 }))
diff --git a/server/middleware/s3.ts b/server/middleware/s3.ts
index 624865222..28971a42b 100644
--- a/server/middleware/s3.ts
+++ b/server/middleware/s3.ts
@@ -1,4 +1,4 @@
-import { join } from "path";
+import { join } from 'path'
// Middleware to intercept requests and proxy if certain conditions are met
const config = {
@@ -8,63 +8,63 @@ const config = {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
},
-};
+}
-const S3_BUCKET_NAME = process.env.S3_BUCKET_NAME;
+const S3_BUCKET_NAME = process.env.S3_BUCKET_NAME
-const fs = require("@cyclic.sh/s3fs")(S3_BUCKET_NAME, config);
-const PROXY_PREFIX = "/v1/fs";
-const PROXY_ROUTES = ["/threads", "/messages"];
+const fs = require('@cyclic.sh/s3fs')(S3_BUCKET_NAME, config)
+const PROXY_PREFIX = '/v1/fs'
+const PROXY_ROUTES = ['/threads', '/messages']
export const s3 = (req: any, reply: any, done: any) => {
// Proxy FS requests to S3 using S3FS
if (req.url.startsWith(PROXY_PREFIX)) {
- const route = req.url.split("/").pop();
- const args = parseRequestArgs(req);
+ const route = req.url.split('/').pop()
+ const args = parseRequestArgs(req)
// Proxy matched requests to the s3fs module
if (args.length && PROXY_ROUTES.some((route) => args[0].includes(route))) {
try {
// Handle customized route
// S3FS does not handle appendFileSync
- if (route === "appendFileSync") {
- let result = handAppendFileSync(args);
+ if (route === 'appendFileSync') {
+ let result = handAppendFileSync(args)
- reply.status(200).send(result);
- return;
+ reply.status(200).send(result)
+ return
}
// Reroute the other requests to the s3fs module
- const result = fs[route](...args);
- reply.status(200).send(result);
- return;
+ const result = fs[route](...args)
+ reply.status(200).send(result)
+ return
} catch (ex) {
- console.log(ex);
+ console.log(ex)
}
}
}
// Let other requests go through
- done();
-};
+ done()
+}
const parseRequestArgs = (req: Request) => {
const {
getJanDataFolderPath,
normalizeFilePath,
- } = require("@janhq/core/node");
+ } = require('@janhq/core/node')
return JSON.parse(req.body as any).map((arg: any) =>
- typeof arg === "string" &&
+ typeof arg === 'string' &&
(arg.startsWith(`file:/`) || arg.startsWith(`file:\\`))
? join(getJanDataFolderPath(), normalizeFilePath(arg))
: arg
- );
-};
+ )
+}
const handAppendFileSync = (args: any[]) => {
if (fs.existsSync(args[0])) {
- const data = fs.readFileSync(args[0], "utf-8");
- return fs.writeFileSync(args[0], data + args[1]);
+ const data = fs.readFileSync(args[0], 'utf-8')
+ return fs.writeFileSync(args[0], data + args[1])
} else {
- return fs.writeFileSync(args[0], args[1]);
+ return fs.writeFileSync(args[0], args[1])
}
-};
+}
diff --git a/server/package.json b/server/package.json
index c1a104506..a7cc09b4f 100644
--- a/server/package.json
+++ b/server/package.json
@@ -13,9 +13,8 @@
"scripts": {
"lint": "eslint . --ext \".js,.jsx,.ts,.tsx\"",
"test:e2e": "playwright test --workers=1",
- "build:core": "cd node_modules/@janhq/core && yarn install && yarn build",
- "dev": "yarn build:core && tsc --watch & node --watch build/main.js",
- "build": "yarn build:core && tsc"
+ "dev": "tsc --watch & node --watch build/main.js",
+ "build": "tsc"
},
"dependencies": {
"@alumna/reflect": "^1.1.3",
@@ -24,7 +23,8 @@
"@fastify/static": "^6.12.0",
"@fastify/swagger": "^8.13.0",
"@fastify/swagger-ui": "2.0.1",
- "@janhq/core": "file:../core",
+ "@janhq/core": "link:./core",
+ "@npmcli/arborist": "^7.3.1",
"dotenv": "^16.3.1",
"fastify": "^4.24.3",
"request": "^2.88.2",
@@ -41,8 +41,5 @@
"run-script-os": "^1.1.6",
"@types/tcp-port-used": "^1.0.4",
"typescript": "^5.2.2"
- },
- "bundleDependencies": [
- "@janhq/core"
- ]
+ }
}
diff --git a/uikit/src/select/styles.scss b/uikit/src/select/styles.scss
index 6f6cd5800..90485723a 100644
--- a/uikit/src/select/styles.scss
+++ b/uikit/src/select/styles.scss
@@ -21,6 +21,7 @@
&-item {
@apply hover:bg-secondary relative my-1 block w-full cursor-pointer select-none items-center rounded-sm px-4 py-2 text-sm data-[disabled]:pointer-events-none data-[disabled]:opacity-50;
+ @apply focus:outline-none focus-visible:outline-0;
}
&-trigger-viewport {
diff --git a/uikit/types/declaration.d.ts b/uikit/types/declaration.d.ts
index 85b1a7136..f8e975fa5 100644
--- a/uikit/types/declaration.d.ts
+++ b/uikit/types/declaration.d.ts
@@ -1,4 +1,4 @@
declare module '*.scss' {
- const content: Record;
- export default content;
-}
\ No newline at end of file
+ const content: Record
+ export default content
+}
diff --git a/web/.prettierignore b/web/.prettierignore
deleted file mode 100644
index 02d9145c1..000000000
--- a/web/.prettierignore
+++ /dev/null
@@ -1,5 +0,0 @@
-.next/
-node_modules/
-dist/
-*.hbs
-*.mdx
\ No newline at end of file
diff --git a/web/.prettierrc b/web/.prettierrc
deleted file mode 100644
index 46f1abcb0..000000000
--- a/web/.prettierrc
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "semi": false,
- "singleQuote": true,
- "quoteProps": "consistent",
- "trailingComma": "es5",
- "endOfLine": "auto",
- "plugins": ["prettier-plugin-tailwindcss"]
-}
diff --git a/web/app/error.tsx b/web/app/error.tsx
new file mode 100644
index 000000000..25b24b9ef
--- /dev/null
+++ b/web/app/error.tsx
@@ -0,0 +1,89 @@
+'use client' // Error components must be Client Components
+
+import { useEffect, useState } from 'react'
+
+export default function Error({
+ error,
+}: {
+ error: Error & { digest?: string }
+ reset: () => void
+}) {
+ const [showFull, setShowFull] = useState(false)
+ useEffect(() => {
+ // Log the error to an error reporting service
+ console.error(error)
+ }, [error])
+
+ return (
+ <>
+
+
+
+
+ Oops! Unexpected error occurred.
+
+
+ Something went wrong. Try to{' '}
+ {' '}
+ or feel free to{' '}
+
+ contact us
+ {' '}
+ if the problem presists.
+
+
+ Error:
+ {error.message}
+
+
+ {showFull ? error.stack : error.stack?.slice(0, 200)}
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/web/containers/CardSidebar/index.tsx b/web/containers/CardSidebar/index.tsx
index cb9942087..f500141f0 100644
--- a/web/containers/CardSidebar/index.tsx
+++ b/web/containers/CardSidebar/index.tsx
@@ -31,7 +31,7 @@ export default function CardSidebar({
rightAction,
hideMoreVerticalAction,
}: Props) {
- const [show, setShow] = useState(true)
+ const [show, setShow] = useState(false)
const [more, setMore] = useState(false)
const [menu, setMenu] = useState(null)
const [toggle, setToggle] = useState(null)
diff --git a/web/containers/DropdownListSidebar/index.tsx b/web/containers/DropdownListSidebar/index.tsx
index 140a1aba1..75bbe073c 100644
--- a/web/containers/DropdownListSidebar/index.tsx
+++ b/web/containers/DropdownListSidebar/index.tsx
@@ -14,7 +14,14 @@ import {
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'
-import { MonitorIcon } from 'lucide-react'
+import {
+ MonitorIcon,
+ LayoutGridIcon,
+ FoldersIcon,
+ GlobeIcon,
+ CheckIcon,
+ CopyIcon,
+} from 'lucide-react'
import { twMerge } from 'tailwind-merge'
@@ -22,6 +29,7 @@ import { MainViewState } from '@/constants/screens'
import { useActiveModel } from '@/hooks/useActiveModel'
+import { useClipboard } from '@/hooks/useClipboard'
import { useMainViewState } from '@/hooks/useMainViewState'
import useRecommendedModel from '@/hooks/useRecommendedModel'
@@ -42,6 +50,8 @@ import {
export const selectedModelAtom = atom(undefined)
+const engineOptions = ['Local', 'Remote']
+
// TODO: Move all of the unscoped logics outside of the component
const DropdownListSidebar = ({
strictedThread = true,
@@ -51,13 +61,24 @@ const DropdownListSidebar = ({
const activeThread = useAtomValue(activeThreadAtom)
const [selectedModel, setSelectedModel] = useAtom(selectedModelAtom)
const setThreadModelParams = useSetAtom(setThreadModelParamsAtom)
-
+ const [isTabActive, setIsTabActive] = useState(0)
const { stateModel } = useActiveModel()
const [serverEnabled, setServerEnabled] = useAtom(serverEnabledAtom)
const { setMainViewState } = useMainViewState()
const [loader, setLoader] = useState(0)
const { recommendedModel, downloadedModels } = useRecommendedModel()
const { updateModelParameter } = useUpdateModelParameters()
+ const clipboard = useClipboard({ timeout: 1000 })
+ const [copyId, setCopyId] = useState('')
+
+ const localModel = downloadedModels.filter(
+ (model) => model.engine === InferenceEngine.nitro
+ )
+ const remoteModel = downloadedModels.filter(
+ (model) => model.engine === InferenceEngine.openai
+ )
+
+ const modelOptions = isTabActive === 0 ? localModel : remoteModel
useEffect(() => {
if (!activeThread) return
@@ -73,7 +94,7 @@ const DropdownListSidebar = ({
// This is fake loader please fix this when we have realtime percentage when load model
useEffect(() => {
- if (stateModel.loading) {
+ if (stateModel.model === selectedModel?.id && stateModel.loading) {
if (loader === 24) {
setTimeout(() => {
setLoader(loader + 1)
@@ -94,7 +115,7 @@ const DropdownListSidebar = ({
} else {
setLoader(0)
}
- }, [stateModel.loading, loader])
+ }, [stateModel.loading, loader, selectedModel, stateModel.model])
const onValueSelected = useCallback(
async (modelId: string) => {
@@ -138,12 +159,16 @@ const DropdownListSidebar = ({
return null
}
+ const selectedModelLoading =
+ stateModel.model === selectedModel?.id && stateModel.loading
+
return (
<>
|