diff --git a/.github/workflows/jan-electron-build.yml b/.github/workflows/jan-electron-build.yml index 3393656259..312ee54101 100644 --- a/.github/workflows/jan-electron-build.yml +++ b/.github/workflows/jan-electron-build.yml @@ -52,7 +52,7 @@ jobs: - name: Install yarn dependencies run: | yarn install - yarn build:pull-plugins + yarn build:plugins env: APP_PATH: "." DEVELOPER_ID: ${{ secrets.DEVELOPER_ID }} @@ -104,7 +104,7 @@ jobs: run: | yarn config set network-timeout 300000 yarn install - yarn build:pull-plugins + yarn build:plugins - name: Build and publish app run: | @@ -153,7 +153,7 @@ jobs: run: | yarn config set network-timeout 300000 yarn install - yarn build:pull-plugins + yarn build:plugins - name: Build and publish app run: | diff --git a/.github/workflows/jan-electron-linter-and-test.yml b/.github/workflows/jan-electron-linter-and-test.yml index 8c0960463f..69552f17e9 100644 --- a/.github/workflows/jan-electron-linter-and-test.yml +++ b/.github/workflows/jan-electron-linter-and-test.yml @@ -44,9 +44,10 @@ jobs: - name: Linter and test run: | yarn config set network-timeout 300000 + yarn build:core yarn install yarn lint - yarn build:pull-plugins + yarn build:plugins yarn build:test yarn test env: @@ -75,8 +76,9 @@ jobs: - name: Linter and test run: | yarn config set network-timeout 300000 + yarn build:core yarn install - yarn build:pull-plugins + yarn build:plugins yarn build:test-win32 yarn test @@ -103,7 +105,8 @@ jobs: export DISPLAY=$(w -h | awk 'NR==1 {print $2}') echo -e "Display ID: $DISPLAY" yarn config set network-timeout 300000 + yarn build:core yarn install - yarn build:pull-plugins + yarn build:plugins yarn build:test-linux yarn test \ No newline at end of file diff --git a/.github/workflows/jan-plugins.yml b/.github/workflows/jan-plugins.yml index 5f36edf17f..fee3b977d7 100644 --- a/.github/workflows/jan-plugins.yml +++ b/.github/workflows/jan-plugins.yml @@ -54,6 +54,11 @@ jobs: for dir in $(cat /tmp/change_dir.txt) do echo "$dir" + if [ ! -d "$dir" ]; then + echo "Directory $dir does not exist, plugin might be removed, skipping..." + continue + fi + # Extract current version current_version=$(jq -r '.version' $dir/package.json) @@ -80,6 +85,11 @@ jobs: with: node-version: "20.x" registry-url: "https://registry.npmjs.org" + - name: Build core module + run: | + cd core + npm install + npm run build - name: Publish npm packages run: | @@ -87,6 +97,10 @@ jobs: for dir in $(cat /tmp/change_dir.txt) do echo $dir + if [ ! -d "$dir" ]; then + echo "Directory $dir does not exist, plugin might be removed, skipping..." + continue + fi cd $dir npm install if [[ $dir == 'data-plugin' ]]; then @@ -112,6 +126,10 @@ jobs: for dir in $(cat /tmp/change_dir.txt) do echo "$dir" + if [ ! -d "$dir" ]; then + echo "Directory $dir does not exist, plugin might be removed, skipping..." + continue + fi version=$(jq -r '.version' plugins/$dir/package.json) git config --global user.email "service@jan.ai" git config --global user.name "Service Account" diff --git a/core/index.ts b/core/index.ts deleted file mode 100644 index 5d8a29c571..0000000000 --- a/core/index.ts +++ /dev/null @@ -1,304 +0,0 @@ -/** - * CoreService exports - */ - -export type CoreService = - | StoreService - | DataService - | InferenceService - | ModelManagementService - | SystemMonitoringService - | PreferenceService - | PluginService; - -/** - * Represents the available methods for the StoreService. - * @enum {string} - */ -export enum StoreService { - /** - * Creates a new collection in the database store. - */ - CreateCollection = "createCollection", - - /** - * Deletes an existing collection from the database store. - */ - DeleteCollection = "deleteCollection", - - /** - * Inserts a new value into an existing collection in the database store. - */ - InsertOne = "insertOne", - - /** - * Updates an existing value in an existing collection in the database store. - */ - UpdateOne = "updateOne", - - /** - * Updates multiple records in a collection in the database store. - */ - UpdateMany = "updateMany", - - /** - * Deletes an existing value from an existing collection in the database store. - */ - DeleteOne = "deleteOne", - - /** - * Delete multiple records in a collection in the database store. - */ - DeleteMany = "deleteMany", - - /** - * Retrieve multiple records from a collection in the data store - */ - FindMany = "findMany", - - /** - * Retrieve a record from a collection in the data store. - */ - FindOne = "findOne", -} - -/** - * DataService exports. - * @enum {string} - */ -export enum DataService { - /** - * Gets a list of conversations. - */ - GetConversations = "getConversations", - - /** - * Creates a new conversation. - */ - CreateConversation = "createConversation", - - /** - * Updates an existing conversation. - */ - UpdateConversation = "updateConversation", - - /** - * Deletes an existing conversation. - */ - DeleteConversation = "deleteConversation", - - /** - * Creates a new message in an existing conversation. - */ - CreateMessage = "createMessage", - - /** - * Updates an existing message in an existing conversation. - */ - UpdateMessage = "updateMessage", - - /** - * Gets a list of messages for an existing conversation. - */ - GetConversationMessages = "getConversationMessages", - - /** - * Gets a conversation matching an ID. - */ - GetConversationById = "getConversationById", - - /** - * Creates a new conversation using the prompt instruction. - */ - CreateBot = "createBot", - - /** - * Gets all created bots. - */ - GetBots = "getBots", - - /** - * Gets a bot matching an ID. - */ - GetBotById = "getBotById", - - /** - * Deletes a bot matching an ID. - */ - DeleteBot = "deleteBot", - - /** - * Updates a bot matching an ID. - */ - UpdateBot = "updateBot", - - /** - * Gets the plugin manifest. - */ - GetPluginManifest = "getPluginManifest", -} - -/** - * InferenceService exports. - * @enum {string} - */ -export enum InferenceService { - /** - * Initializes a model for inference. - */ - InitModel = "initModel", - - /** - * Stops a running inference model. - */ - StopModel = "stopModel", - - /** - * Single inference response. - */ - InferenceRequest = "inferenceRequest", -} - -/** - * ModelManagementService exports. - * @enum {string} - */ -export enum ModelManagementService { - /** - * Deletes a downloaded model. - */ - DeleteModel = "deleteModel", - - /** - * Downloads a model from the server. - */ - DownloadModel = "downloadModel", - - /** - * Gets configued models from the database. - */ - GetConfiguredModels = "getConfiguredModels", - - /** - * Stores a model in the database. - */ - StoreModel = "storeModel", - - /** - * Updates the finished download time for a model in the database. - */ - UpdateFinishedDownloadAt = "updateFinishedDownloadAt", - - /** - * Gets a list of finished download models from the database. - */ - GetFinishedDownloadModels = "getFinishedDownloadModels", - - /** - * Deletes a download model from the database. - */ - DeleteDownloadModel = "deleteDownloadModel", - - /** - * Gets a model by its ID from the database. - */ - GetModelById = "getModelById", -} - -/** - * PreferenceService exports. - * @enum {string} - */ -export enum PreferenceService { - /** - * The experiment component for which preferences are being managed. - */ - ExperimentComponent = "experimentComponent", - - /** - * Gets the plugin preferences. - */ - PluginPreferences = "pluginPreferences", -} - -/** - * SystemMonitoringService exports. - * @enum {string} - */ -export enum SystemMonitoringService { - /** - * Gets information about system resources. - */ - GetResourcesInfo = "getResourcesInfo", - - /** - * Gets the current system load. - */ - GetCurrentLoad = "getCurrentLoad", -} - -/** - * PluginService exports. - * @enum {string} - */ -export enum PluginService { - /** - * The plugin is being started. - */ - OnStart = "pluginOnStart", - - /** - * The plugin is being started. - */ - OnPreferencesUpdate = "pluginPreferencesUpdate", - - /** - * The plugin is being stopped. - */ - OnStop = "pluginOnStop", - - /** - * The plugin is being destroyed. - */ - OnDestroy = "pluginOnDestroy", -} - -/** - * Store module exports. - * @module - */ -export { store } from "./store"; - -/** - * @deprecated This object is deprecated and should not be used. - * Use individual functions instead. - */ -export { core } from "./core"; - -/** - * Core module exports. - * @module - */ -export { - RegisterExtensionPoint, - deleteFile, - downloadFile, - invokePluginFunc, -} from "./core"; - -/** - * Events module exports. - * @module - */ -export { - events, - EventName, - NewMessageRequest, - NewMessageResponse, -} from "./events"; - -/** - * Preferences module exports. - * @module - */ -export { preferences } from "./preferences"; diff --git a/core/package.json b/core/package.json index d80e89a6c7..fb8765c55a 100644 --- a/core/package.json +++ b/core/package.json @@ -17,7 +17,7 @@ }, "exports": { ".": "./lib/index.js", - "./store": "./lib/store.js" + "./plugin": "./lib/plugins/index.js" }, "files": [ "lib", diff --git a/core/preferences.ts b/core/preferences.ts deleted file mode 100644 index 8d3f6beb50..0000000000 --- a/core/preferences.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { store } from "./store"; - -/** - * Returns the value of the specified preference for the specified plugin. - * - * @param pluginName The name of the plugin. - * @param preferenceKey The key of the preference. - * @returns A promise that resolves to the value of the preference. - */ -function get(pluginName: string, preferenceKey: string): Promise { - return store - .createCollection("preferences", {}) - .then(() => store.findOne("preferences", `${pluginName}.${preferenceKey}`)) - .then((doc) => doc?.value ?? ""); -} - -/** - * Sets the value of the specified preference for the specified plugin. - * - * @param pluginName The name of the plugin. - * @param preferenceKey The key of the preference. - * @param value The value of the preference. - * @returns A promise that resolves when the preference has been set. - */ -function set(pluginName: string, preferenceKey: string, value: any): Promise { - return store - .createCollection("preferences", {}) - .then(() => - store - .findOne("preferences", `${pluginName}.${preferenceKey}`) - .then((doc) => - doc - ? store.updateOne("preferences", `${pluginName}.${preferenceKey}`, { value }) - : store.insertOne("preferences", { _id: `${pluginName}.${preferenceKey}`, value }) - ) - ); -} - -/** - * Clears all preferences for the specified plugin. - * - * @param pluginName The name of the plugin. - * @returns A promise that resolves when the preferences have been cleared. - */ -function clear(pluginName: string): Promise { - return Promise.resolve(); -} - -/** - * Registers a preference with the specified default value. - * - * @param register The function to use for registering the preference. - * @param pluginName The name of the plugin. - * @param preferenceKey The key of the preference. - * @param preferenceName The name of the preference. - * @param preferenceDescription The description of the preference. - * @param defaultValue The default value of the preference. - */ -function registerPreferences( - register: Function, - pluginName: string, - preferenceKey: string, - preferenceName: string, - preferenceDescription: string, - defaultValue: T -) { - register("PluginPreferences", `${pluginName}.${preferenceKey}`, () => ({ - pluginName, - preferenceKey, - preferenceName, - preferenceDescription, - defaultValue, - })); -} - -/** - * An object that provides methods for getting, setting, and clearing preferences. - */ -export const preferences = { - get, - set, - clear, - registerPreferences, -}; diff --git a/core/types/index.d.ts b/core/src/@global/index.d.ts similarity index 100% rename from core/types/index.d.ts rename to core/src/@global/index.d.ts diff --git a/core/core.ts b/core/src/core.ts similarity index 71% rename from core/core.ts rename to core/src/core.ts index 1820d5b9a0..2478434d34 100644 --- a/core/core.ts +++ b/core/src/core.ts @@ -7,7 +7,23 @@ * @returns Promise * */ -const invokePluginFunc: (plugin: string, method: string, ...args: any[]) => Promise = (plugin, method, ...args) => +const executeOnMain: ( + plugin: string, + method: string, + ...args: any[] +) => Promise = (plugin, method, ...args) => + window.coreAPI?.invokePluginFunc(plugin, method, ...args) ?? + window.electronAPI?.invokePluginFunc(plugin, method, ...args); + +/** + * @deprecated This object is deprecated and should not be used. + * Use individual functions instead. + */ +const invokePluginFunc: ( + plugin: string, + method: string, + ...args: any[] +) => Promise = (plugin, method, ...args) => window.coreAPI?.invokePluginFunc(plugin, method, ...args) ?? window.electronAPI?.invokePluginFunc(plugin, method, ...args); @@ -17,8 +33,12 @@ const invokePluginFunc: (plugin: string, method: string, ...args: any[]) => Prom * @param {string} fileName - The name to use for the downloaded file. * @returns {Promise} A promise that resolves when the file is downloaded. */ -const downloadFile: (url: string, fileName: string) => Promise = (url, fileName) => - window.coreAPI?.downloadFile(url, fileName) ?? window.electronAPI?.downloadFile(url, fileName); +const downloadFile: (url: string, fileName: string) => Promise = ( + url, + fileName +) => + window.coreAPI?.downloadFile(url, fileName) ?? + window.electronAPI?.downloadFile(url, fileName); /** * Deletes a file from the local file system. @@ -51,6 +71,7 @@ export type RegisterExtensionPoint = ( */ export const core = { invokePluginFunc, + executeOnMain, downloadFile, deleteFile, appDataPath, @@ -59,4 +80,10 @@ export const core = { /** * Functions exports */ -export { invokePluginFunc, downloadFile, deleteFile, appDataPath }; +export { + invokePluginFunc, + executeOnMain, + downloadFile, + deleteFile, + appDataPath, +}; diff --git a/core/events.ts b/core/src/events.ts similarity index 92% rename from core/events.ts rename to core/src/events.ts index ae52f297f6..b39a38408e 100644 --- a/core/events.ts +++ b/core/src/events.ts @@ -6,12 +6,16 @@ export enum EventName { OnNewMessageRequest = "onNewMessageRequest", OnNewMessageResponse = "onNewMessageResponse", OnMessageResponseUpdate = "onMessageResponseUpdate", - OnMessageResponseFinished = "OnMessageResponseFinished", + OnMessageResponseFinished = "onMessageResponseFinished", OnDownloadUpdate = "onDownloadUpdate", OnDownloadSuccess = "onDownloadSuccess", OnDownloadError = "onDownloadError", } +export type MessageHistory = { + role: string; + content: string; +}; /** * The `NewMessageRequest` type defines the shape of a new message request object. */ @@ -23,6 +27,7 @@ export type NewMessageRequest = { message?: string; createdAt?: string; updatedAt?: string; + history?: MessageHistory[]; }; /** diff --git a/core/src/fs.ts b/core/src/fs.ts new file mode 100644 index 0000000000..f3e49c5b44 --- /dev/null +++ b/core/src/fs.ts @@ -0,0 +1,49 @@ +/** + * Writes data to a file at the specified path. + * @param {string} path - The path to the file. + * @param {string} data - The data to write to the file. + * @returns {Promise} A Promise that resolves when the file is written successfully. + */ +const writeFile: (path: string, data: string) => Promise = (path, data) => + window.coreAPI?.writeFile(path, data) ?? + window.electronAPI?.writeFile(path, data); + +/** + * Reads the contents of a file at the specified path. + * @param {string} path - The path of the file to read. + * @returns {Promise} A Promise that resolves with the contents of the file. + */ +const readFile: (path: string) => Promise = (path) => + window.coreAPI?.readFile(path) ?? window.electronAPI?.readFile(path); + +/** + * List the directory files + * @param {string} path - The path of the directory to list files. + * @returns {Promise} A Promise that resolves with the contents of the directory. + */ +const listFiles: (path: string) => Promise = (path) => + window.coreAPI?.listFiles(path) ?? window.electronAPI?.listFiles(path); + +/** + * Creates a directory at the specified path. + * @param {string} path - The path of the directory to create. + * @returns {Promise} A Promise that resolves when the directory is created successfully. + */ +const mkdir: (path: string) => Promise = (path) => + window.coreAPI?.mkdir(path) ?? window.electronAPI?.mkdir(path); + +/** + * Deletes a file from the local file system. + * @param {string} path - The path of the file to delete. + * @returns {Promise} A Promise that resolves when the file is deleted. + */ +const deleteFile: (path: string) => Promise = (path) => + window.coreAPI?.deleteFile(path) ?? window.electronAPI?.deleteFile(path); + +export const fs = { + writeFile, + readFile, + listFiles, + mkdir, + deleteFile, +}; diff --git a/core/src/index.ts b/core/src/index.ts new file mode 100644 index 0000000000..cba3efe923 --- /dev/null +++ b/core/src/index.ts @@ -0,0 +1,40 @@ +/** + * @deprecated This object is deprecated and should not be used. + * Use individual functions instead. + */ +export { core, deleteFile, invokePluginFunc } from "./core"; + +/** + * Core module exports. + * @module + */ +export { downloadFile, executeOnMain } from "./core"; + +/** + * Events module exports. + * @module + */ +export { events } from "./events"; + +/** + * Events types exports. + * @module + */ +export { + EventName, + NewMessageRequest, + NewMessageResponse, + MessageHistory, +} from "./events"; + +/** + * Filesystem module exports. + * @module + */ +export { fs } from "./fs"; + +/** + * Plugin base module export. + * @module + */ +export { JanPlugin, PluginType } from "./plugin"; diff --git a/core/src/plugin.ts b/core/src/plugin.ts new file mode 100644 index 0000000000..96bfcbe94f --- /dev/null +++ b/core/src/plugin.ts @@ -0,0 +1,13 @@ +export enum PluginType { + Conversational = "conversational", + Inference = "inference", + Preference = "preference", + SystemMonitoring = "systemMonitoring", + Model = "model", +} + +export abstract class JanPlugin { + abstract type(): PluginType; + abstract onLoad(): void; + abstract onUnload(): void; +} diff --git a/core/src/plugins/conversational.ts b/core/src/plugins/conversational.ts new file mode 100644 index 0000000000..a76c41a51f --- /dev/null +++ b/core/src/plugins/conversational.ts @@ -0,0 +1,32 @@ +import { JanPlugin } from "../plugin"; +import { Conversation } from "../types/index"; + +/** + * Abstract class for conversational plugins. + * @abstract + * @extends JanPlugin + */ +export abstract class ConversationalPlugin extends JanPlugin { + /** + * Returns a list of conversations. + * @abstract + * @returns {Promise} A promise that resolves to an array of conversations. + */ + abstract getConversations(): Promise; + + /** + * Saves a conversation. + * @abstract + * @param {Conversation} conversation - The conversation to save. + * @returns {Promise} A promise that resolves when the conversation is saved. + */ + abstract saveConversation(conversation: Conversation): Promise; + + /** + * Deletes a conversation. + * @abstract + * @param {string} conversationId - The ID of the conversation to delete. + * @returns {Promise} A promise that resolves when the conversation is deleted. + */ + abstract deleteConversation(conversationId: string): Promise; +} diff --git a/core/src/plugins/index.ts b/core/src/plugins/index.ts new file mode 100644 index 0000000000..5072819d78 --- /dev/null +++ b/core/src/plugins/index.ts @@ -0,0 +1,20 @@ +/** + * Conversational plugin. Persists and retrieves conversations. + * @module + */ +export { ConversationalPlugin } from "./conversational"; + +/** + * Inference plugin. Start, stop and inference models. + */ +export { InferencePlugin } from "./inference"; + +/** + * Monitoring plugin for system monitoring. + */ +export { MonitoringPlugin } from "./monitoring"; + +/** + * Model plugin for managing models. + */ +export { ModelPlugin } from "./model"; diff --git a/core/src/plugins/inference.ts b/core/src/plugins/inference.ts new file mode 100644 index 0000000000..8da7f5059a --- /dev/null +++ b/core/src/plugins/inference.ts @@ -0,0 +1,25 @@ +import { NewMessageRequest } from "../events"; +import { JanPlugin } from "../plugin"; + +/** + * An abstract class representing an Inference Plugin for Jan. + */ +export abstract class InferencePlugin extends JanPlugin { + /** + * Initializes the model for the plugin. + * @param modelFileName - The name of the file containing the model. + */ + abstract initModel(modelFileName: string): Promise; + + /** + * Stops the model for the plugin. + */ + abstract stopModel(): Promise; + + /** + * Processes an inference request. + * @param data - The data for the inference request. + * @returns The result of the inference request. + */ + abstract inferenceRequest(data: NewMessageRequest): Promise; +} diff --git a/core/src/plugins/model.ts b/core/src/plugins/model.ts new file mode 100644 index 0000000000..03947d648d --- /dev/null +++ b/core/src/plugins/model.ts @@ -0,0 +1,44 @@ +/** + * Represents a plugin for managing machine learning models. + * @abstract + */ +import { JanPlugin } from "../plugin"; +import { Model, ModelCatalog } from "../types/index"; + +/** + * An abstract class representing a plugin for managing machine learning models. + */ +export abstract class ModelPlugin extends JanPlugin { + /** + * Downloads a model. + * @param model - The model to download. + * @returns A Promise that resolves when the model has been downloaded. + */ + abstract downloadModel(model: Model): Promise; + + /** + * Deletes a model. + * @param filePath - The file path of the model to delete. + * @returns A Promise that resolves when the model has been deleted. + */ + abstract deleteModel(filePath: string): Promise; + + /** + * Saves a model. + * @param model - The model to save. + * @returns A Promise that resolves when the model has been saved. + */ + abstract saveModel(model: Model): Promise; + + /** + * Gets a list of downloaded models. + * @returns A Promise that resolves with an array of downloaded models. + */ + abstract getDownloadedModels(): Promise; + + /** + * Gets a list of configured models. + * @returns A Promise that resolves with an array of configured models. + */ + abstract getConfiguredModels(): Promise; +} diff --git a/core/src/plugins/monitoring.ts b/core/src/plugins/monitoring.ts new file mode 100644 index 0000000000..ea608b7b28 --- /dev/null +++ b/core/src/plugins/monitoring.ts @@ -0,0 +1,19 @@ +import { JanPlugin } from "../plugin"; + +/** + * Abstract class for monitoring plugins. + * @extends JanPlugin + */ +export abstract class MonitoringPlugin extends JanPlugin { + /** + * Returns information about the system resources. + * @returns {Promise} A promise that resolves with the system resources information. + */ + abstract getResourcesInfo(): Promise; + + /** + * Returns the current system load. + * @returns {Promise} A promise that resolves with the current system load. + */ + abstract getCurrentLoad(): Promise; +} diff --git a/core/src/types/index.ts b/core/src/types/index.ts new file mode 100644 index 0000000000..c2062b5d13 --- /dev/null +++ b/core/src/types/index.ts @@ -0,0 +1,91 @@ +export interface Conversation { + _id: string; + modelId?: string; + botId?: string; + name: string; + message?: string; + summary?: string; + createdAt?: string; + updatedAt?: string; + messages: Message[]; +} +export interface Message { + message?: string; + user?: string; + _id: string; + createdAt?: string; + updatedAt?: string; +} + +export interface Model { + /** + * Combination of owner and model name. + * Being used as file name. MUST be unique. + */ + _id: string; + name: string; + quantMethod: string; + bits: number; + size: number; + maxRamRequired: number; + usecase: string; + downloadLink: string; + modelFile?: string; + /** + * For tracking download info + */ + startDownloadAt?: number; + finishDownloadAt?: number; + productId: string; + productName: string; + shortDescription: string; + longDescription: string; + avatarUrl: string; + author: string; + version: string; + modelUrl: string; + createdAt: number; + updatedAt?: number; + status: string; + releaseDate: number; + tags: string[]; +} +export interface ModelCatalog { + _id: string; + name: string; + shortDescription: string; + avatarUrl: string; + longDescription: string; + author: string; + version: string; + modelUrl: string; + createdAt: number; + updatedAt?: number; + status: string; + releaseDate: number; + tags: string[]; + availableVersions: ModelVersion[]; +} +/** + * Model type which will be stored in the database + */ +export type ModelVersion = { + /** + * Combination of owner and model name. + * Being used as file name. Should be unique. + */ + _id: string; + name: string; + quantMethod: string; + bits: number; + size: number; + maxRamRequired: number; + usecase: string; + downloadLink: string; + productId: string; + /** + * For tracking download state + */ + startDownloadAt?: number; + finishDownloadAt?: number; +}; diff --git a/core/store.ts b/core/store.ts deleted file mode 100644 index 459b9fb640..0000000000 --- a/core/store.ts +++ /dev/null @@ -1,129 +0,0 @@ -/** - * Creates, reads, updates, and deletes data in a data store. - * @module - */ - -/** - * Creates a new collection in the data store. - * @param {string} name - The name of the collection to create. - * @param { [key: string]: any } schema - schema of the collection to create, include fields and their types - * @returns {Promise} A promise that resolves when the collection is created. - */ -function createCollection( - name: string, - schema: { [key: string]: any } -): Promise { - return window.corePlugin?.store?.createCollection(name, schema); -} - -/** - * Deletes a collection from the data store. - * @param {string} name - The name of the collection to delete. - * @returns {Promise} A promise that resolves when the collection is deleted. - */ -function deleteCollection(name: string): Promise { - return window.corePlugin?.store?.deleteCollection(name); -} - -/** - * Inserts a value into a collection in the data store. - * @param {string} collectionName - The name of the collection to insert the value into. - * @param {any} value - The value to insert into the collection. - * @returns {Promise} A promise that resolves with the inserted value. - */ -function insertOne(collectionName: string, value: any): Promise { - return window.corePlugin?.store?.insertOne(collectionName, value); -} - -/** - * Retrieve a record from a collection in the data store. - * @param {string} collectionName - The name of the collection containing the record to retrieve. - * @param {string} key - The key of the record to retrieve. - * @returns {Promise} A promise that resolves when the record is retrieved. - */ -function findOne(collectionName: string, key: string): Promise { - return window.corePlugin?.store?.findOne(collectionName, key); -} - -/** - * Retrieves all records that match a selector in a collection in the data store. - * @param {string} collectionName - The name of the collection to retrieve. - * @param {{ [key: string]: any }} selector - The selector to use to get records from the collection. - * @param {[{ [key: string]: any }]} sort - The sort options to use to retrieve records. - * @returns {Promise} A promise that resolves when all records are retrieved. - */ -function findMany( - collectionName: string, - selector?: { [key: string]: any }, - sort?: [{ [key: string]: any }] -): Promise { - return window.corePlugin?.store?.findMany(collectionName, selector, sort); -} - -/** - * Updates the value of a record in a collection in the data store. - * @param {string} collectionName - The name of the collection containing the record to update. - * @param {string} key - The key of the record to update. - * @param {any} value - The new value for the record. - * @returns {Promise} A promise that resolves when the record is updated. - */ -function updateOne( - collectionName: string, - key: string, - value: any -): Promise { - return window.corePlugin?.store?.updateOne(collectionName, key, value); -} - -/** - * Updates all records that match a selector in a collection in the data store. - * @param {string} collectionName - The name of the collection containing the records to update. - * @param {{ [key: string]: any }} selector - The selector to use to get the records to update. - * @param {any} value - The new value for the records. - * @returns {Promise} A promise that resolves when the records are updated. - */ -function updateMany( - collectionName: string, - value: any, - selector?: { [key: string]: any } -): Promise { - return window.corePlugin?.store?.updateMany(collectionName, selector, value); -} - -/** - * Deletes a single record from a collection in the data store. - * @param {string} collectionName - The name of the collection containing the record to delete. - * @param {string} key - The key of the record to delete. - * @returns {Promise} A promise that resolves when the record is deleted. - */ -function deleteOne(collectionName: string, key: string): Promise { - return window.corePlugin?.store?.deleteOne(collectionName, key); -} - -/** - * Deletes all records with a matching key from a collection in the data store. - * @param {string} collectionName - The name of the collection to delete the records from. - * @param {{ [key: string]: any }} selector - The selector to use to get the records to delete. - * @returns {Promise} A promise that resolves when the records are deleted. - */ -function deleteMany( - collectionName: string, - selector?: { [key: string]: any } -): Promise { - return window.corePlugin?.store?.deleteMany(collectionName, selector); -} - -/** - * Exports the data store operations as an object. - */ -export const store = { - createCollection, - deleteCollection, - insertOne, - findOne, - findMany, - updateOne, - updateMany, - deleteOne, - deleteMany, -}; diff --git a/core/tsconfig.json b/core/tsconfig.json index 695dcbfe22..62caccdcb2 100644 --- a/core/tsconfig.json +++ b/core/tsconfig.json @@ -7,7 +7,9 @@ "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true, - "declaration": true + "declaration": true, + "rootDir": "./src" }, + "include": ["./src"], "exclude": ["lib", "node_modules", "**/*.test.ts", "**/__mocks__/*"] } diff --git a/electron/handlers/fs.ts b/electron/handlers/fs.ts new file mode 100644 index 0000000000..664833437f --- /dev/null +++ b/electron/handlers/fs.ts @@ -0,0 +1,96 @@ +import { app, ipcMain } from "electron"; +import * as fs from "fs"; +import { join } from "path"; + +/** + * Handles file system operations. + */ +export function handleFs() { + /** + * Reads a file from the user data directory. + * @param event - The event object. + * @param path - The path of the file to read. + * @returns A promise that resolves with the contents of the file. + */ + ipcMain.handle("readFile", async (event, path: string): Promise => { + return new Promise((resolve, reject) => { + fs.readFile(join(app.getPath("userData"), path), "utf8", (err, data) => { + if (err) { + reject(err); + } else { + resolve(data); + } + }); + }); + }); + + /** + * Writes data to a file in the user data directory. + * @param event - The event object. + * @param path - The path of the file to write to. + * @param data - The data to write to the file. + * @returns A promise that resolves when the file has been written. + */ + ipcMain.handle( + "writeFile", + async (event, path: string, data: string): Promise => { + return new Promise((resolve, reject) => { + fs.writeFile( + join(app.getPath("userData"), path), + data, + "utf8", + (err) => { + if (err) { + reject(err); + } else { + resolve(); + } + } + ); + }); + } + ); + + /** + * Creates a directory in the user data directory. + * @param event - The event object. + * @param path - The path of the directory to create. + * @returns A promise that resolves when the directory has been created. + */ + ipcMain.handle("mkdir", async (event, path: string): Promise => { + return new Promise((resolve, reject) => { + fs.mkdir( + join(app.getPath("userData"), path), + { recursive: true }, + (err) => { + if (err) { + reject(err); + } else { + resolve(); + } + } + ); + }); + }); + + /** + * Lists the files in a directory in the user data directory. + * @param event - The event object. + * @param path - The path of the directory to list files from. + * @returns A promise that resolves with an array of file names. + */ + ipcMain.handle( + "listFiles", + async (event, path: string): Promise => { + return new Promise((resolve, reject) => { + fs.readdir(join(app.getPath("userData"), path), (err, files) => { + if (err) { + reject(err); + } else { + resolve(files); + } + }); + }); + } + ); +} diff --git a/electron/main.ts b/electron/main.ts index b11963e671..8e175a62d0 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -12,6 +12,7 @@ import { rmdir, unlink, createWriteStream } from "fs"; import { init } from "./core/plugin/index"; import { setupMenu } from "./utils/menu"; import { dispose } from "./utils/disposable"; +import { handleFs } from "./handlers/fs"; const pacote = require("pacote"); const request = require("request"); @@ -127,6 +128,7 @@ function handleAppUpdates() { * Handles various IPC messages from the renderer process. */ function handleIPCs() { + handleFs(); /** * Handles the "setNativeThemeLight" IPC message by setting the native theme source to "light". * This will change the appearance of the app to the light theme. diff --git a/electron/preload.ts b/electron/preload.ts index e8fd723a52..2fb0ad0167 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -1,7 +1,60 @@ +/** + * Exposes a set of APIs to the renderer process via the contextBridge object. + * @remarks + * This module is used to make Pluggable Electron's facade available to the renderer on window.plugins. + * @module preload + */ + +/** + * Exposes a set of APIs to the renderer process via the contextBridge object. + * @remarks + * This module is used to make Pluggable Electron's facade available to the renderer on window.plugins. + * @function useFacade + * @memberof module:preload + * @returns {void} + */ + +/** + * Exposes a set of APIs to the renderer process via the contextBridge object. + * @remarks + * This module is used to make Pluggable Electron's facade available to the renderer on window.plugins. + * @namespace electronAPI + * @memberof module:preload + * @property {Function} invokePluginFunc - Invokes a plugin function with the given arguments. + * @property {Function} setNativeThemeLight - Sets the native theme to light. + * @property {Function} setNativeThemeDark - Sets the native theme to dark. + * @property {Function} setNativeThemeSystem - Sets the native theme to system. + * @property {Function} basePlugins - Returns the base plugins. + * @property {Function} pluginPath - Returns the plugin path. + * @property {Function} appDataPath - Returns the app data path. + * @property {Function} reloadPlugins - Reloads the plugins. + * @property {Function} appVersion - Returns the app version. + * @property {Function} openExternalUrl - Opens the given URL in the default browser. + * @property {Function} relaunch - Relaunches the app. + * @property {Function} openAppDirectory - Opens the app directory. + * @property {Function} deleteFile - Deletes the file at the given path. + * @property {Function} readFile - Reads the file at the given path. + * @property {Function} writeFile - Writes the given data to the file at the given path. + * @property {Function} listFiles - Lists the files in the directory at the given path. + * @property {Function} mkdir - Creates a directory at the given path. + * @property {Function} installRemotePlugin - Installs the remote plugin with the given name. + * @property {Function} downloadFile - Downloads the file at the given URL to the given path. + * @property {Function} pauseDownload - Pauses the download of the file with the given name. + * @property {Function} resumeDownload - Resumes the download of the file with the given name. + * @property {Function} abortDownload - Aborts the download of the file with the given name. + * @property {Function} onFileDownloadUpdate - Registers a callback to be called when a file download is updated. + * @property {Function} onFileDownloadError - Registers a callback to be called when a file download encounters an error. + * @property {Function} onFileDownloadSuccess - Registers a callback to be called when a file download is completed successfully. + * @property {Function} onAppUpdateDownloadUpdate - Registers a callback to be called when an app update download is updated. + * @property {Function} onAppUpdateDownloadError - Registers a callback to be called when an app update download encounters an error. + * @property {Function} onAppUpdateDownloadSuccess - Registers a callback to be called when an app update download is completed successfully. + */ + // Make Pluggable Electron's facade available to the renderer on window.plugins import { useFacade } from "./core/plugin/facade"; + useFacade(); -//@ts-ignore + const { contextBridge, ipcRenderer } = require("electron"); contextBridge.exposeInMainWorld("electronAPI", { @@ -32,6 +85,15 @@ contextBridge.exposeInMainWorld("electronAPI", { deleteFile: (filePath: string) => ipcRenderer.invoke("deleteFile", filePath), + readFile: (path: string) => ipcRenderer.invoke("readFile", path), + + writeFile: (path: string, data: string) => + ipcRenderer.invoke("writeFile", path, data), + + listFiles: (path: string) => ipcRenderer.invoke("listFiles", path), + + mkdir: (path: string) => ipcRenderer.invoke("mkdir", path), + installRemotePlugin: (pluginName: string) => ipcRenderer.invoke("installRemotePlugin", pluginName), diff --git a/package.json b/package.json index 52ec0191e5..2a489d9a06 100644 --- a/package.json +++ b/package.json @@ -27,9 +27,9 @@ "build:web": "yarn workspace jan-web build && cpx \"web/out/**\" \"electron/renderer/\"", "build:electron": "yarn workspace jan build", "build:electron:test": "yarn workspace jan build:test", - "build:pull-plugins": "rimraf ./electron/core/pre-install/*.tgz && cd ./electron/core/pre-install && npm pack @janhq/inference-plugin @janhq/data-plugin @janhq/model-management-plugin @janhq/monitoring-plugin", - "build:plugins": "rimraf ./electron/core/pre-install/*.tgz && concurrently --kill-others-on-fail \"cd ./plugins/data-plugin && npm install && npm run postinstall\" \"cd ./plugins/inference-plugin && npm install --ignore-scripts && npm run postinstall:dev\" \"cd ./plugins/model-management-plugin && npm install && npm run postinstall\" \"cd ./plugins/monitoring-plugin && npm install && npm run postinstall\" && concurrently --kill-others-on-fail \"cd ./plugins/data-plugin && npm run build:publish\" \"cd ./plugins/inference-plugin && npm run build:publish\" \"cd ./plugins/model-management-plugin && npm run build:publish\" \"cd ./plugins/monitoring-plugin && npm run build:publish\"", - "build:plugins-web": "rimraf ./electron/core/pre-install/*.tgz && concurrently --kill-others-on-fail \"cd ./plugins/data-plugin && npm install && npm run build:deps && npm run postinstall\" \"cd ./plugins/inference-plugin && npm install && npm run postinstall\" \"cd ./plugins/model-management-plugin && npm install && npm run postinstall\" \"cd ./plugins/monitoring-plugin && npm install && npm run postinstall\" && concurrently --kill-others-on-fail \"cd ./plugins/data-plugin && npm run build:publish\" \"cd ./plugins/inference-plugin && npm run build:publish\" \"cd ./plugins/model-management-plugin && npm run build:publish\" \"cd ./plugins/monitoring-plugin && npm run build:publish\"", + "build:pull-plugins": "rimraf ./electron/core/pre-install/*.tgz && cd ./electron/core/pre-install && npm pack @janhq/inference-plugin @janhq/monitoring-plugin", + "build:plugins": "rimraf ./electron/core/pre-install/*.tgz && concurrently --kill-others-on-fail \"cd ./plugins/conversational-plugin && npm install && npm run postinstall && npm run build:publish\" \"cd ./plugins/inference-plugin && npm install --ignore-scripts && npm run postinstall:dev && npm run build:publish\" \"cd ./plugins/model-plugin && npm install && npm run postinstall && npm run build:publish\" \"cd ./plugins/monitoring-plugin && npm install && npm run postinstall && npm run build:publish\"", + "build:plugins-web": "rimraf ./electron/core/pre-install/*.tgz && concurrently --kill-others-on-fail \"cd ./plugins/conversational-plugin && npm install && npm run build:deps && npm run postinstall\" \"cd ./plugins/inference-plugin && npm install && npm run postinstall\" \"cd ./plugins/model-plugin && npm install && npm run postinstall\" \"cd ./plugins/monitoring-plugin && npm install && npm run postinstall\" && concurrently --kill-others-on-fail \"cd ./plugins/conversational-plugin && npm run build:publish\" \"cd ./plugins/inference-plugin && npm run build:publish\" \"cd ./plugins/model-plugin && npm run build:publish\" \"cd ./plugins/monitoring-plugin && npm run build:publish\"", "build": "yarn build:web && yarn build:electron", "build:test": "yarn build:web && yarn build:electron:test", "build:test-darwin": "yarn build:web && yarn workspace jan build:test-darwin", @@ -42,15 +42,18 @@ "build:publish-darwin": "yarn build:web && yarn workspace jan build:publish-darwin", "build:publish-win32": "yarn build:web && yarn workspace jan build:publish-win32", "build:publish-linux": "yarn build:web && yarn workspace jan build:publish-linux", - "build:web-plugins": "yarn build:web && yarn build:plugins-web && mkdir -p \"./web/out/plugins/data-plugin\" && cp \"./plugins/data-plugin/dist/esm/index.js\" \"./web/out/plugins/data-plugin\" && mkdir -p \"./web/out/plugins/inference-plugin\" && cp \"./plugins/inference-plugin/dist/index.js\" \"./web/out/plugins/inference-plugin\" && mkdir -p \"./web/out/plugins/model-management-plugin\" && cp \"./plugins/model-management-plugin/dist/index.js\" \"./web/out/plugins/model-management-plugin\" && mkdir -p \"./web/out/plugins/monitoring-plugin\" && cp \"./plugins/monitoring-plugin/dist/index.js\" \"./web/out/plugins/monitoring-plugin\"", + "build:web-plugins": "yarn build:web && yarn build:plugins-web && mkdir -p \"./web/out/plugins/conversational-plugin\" && cp \"./plugins/conversational-plugin/dist/index.js\" \"./web/out/plugins/conversational-plugin\" && mkdir -p \"./web/out/plugins/inference-plugin\" && cp \"./plugins/inference-plugin/dist/index.js\" \"./web/out/plugins/inference-plugin\" && mkdir -p \"./web/out/plugins/model-plugin\" && cp \"./plugins/model-plugin/dist/index.js\" \"./web/out/plugins/model-plugin\" && mkdir -p \"./web/out/plugins/monitoring-plugin\" && cp \"./plugins/monitoring-plugin/dist/index.js\" \"./web/out/plugins/monitoring-plugin\"", "server:prod": "yarn workspace server build && yarn build:web-plugins && cpx \"web/out/**\" \"server/build/renderer/\" && mkdir -p ./server/build/@janhq && cp -r ./plugins/* ./server/build/@janhq", "start:server": "yarn server:prod && node server/build/main.js" }, "devDependencies": { "concurrently": "^8.2.1", "cpx": "^1.5.0", - "wait-on": "^7.0.1", - "rimraf": "^3.0.2" + "rimraf": "^3.0.2", + "wait-on": "^7.0.1" }, - "version": "0.0.0" + "version": "0.0.0", + "dependencies": { + "@janhq/core": "file:core" + } } diff --git a/plugins/openai-plugin/package.json b/plugins/conversational-plugin/package.json similarity index 58% rename from plugins/openai-plugin/package.json rename to plugins/conversational-plugin/package.json index 3f5505f245..3566ef04bc 100644 --- a/plugins/openai-plugin/package.json +++ b/plugins/conversational-plugin/package.json @@ -1,10 +1,8 @@ { - "name": "@janhq/azure-openai-plugin", + "name": "@janhq/conversational-plugin", "version": "1.0.7", - "description": "Inference plugin for Azure OpenAI", - "icon": "https://static-assets.jan.ai/openai-icon.jpg", + "description": "Conversational Plugin - Stores jan app conversations", "main": "dist/index.js", - "module": "dist/module.js", "author": "Jan ", "requiredVersion": "^0.3.1", "license": "MIT", @@ -13,7 +11,7 @@ ], "scripts": { "build": "tsc -b . && webpack --config webpack.config.js", - "postinstall": "rimraf *.tgz --glob && npm run build && rimraf dist/nitro/* && cpx \"nitro/**\" \"dist/nitro\"", + "postinstall": "rimraf *.tgz --glob && npm run build", "build:publish": "npm pack && cpx *.tgz ../../electron/core/pre-install" }, "exports": { @@ -27,16 +25,9 @@ "webpack-cli": "^5.1.4" }, "dependencies": { - "@janhq/core": "^0.1.6", - "azure-openai": "^0.9.4", - "kill-port-process": "^3.2.0", - "tcp-port-used": "^1.0.2", + "@janhq/core": "file:../../core", "ts-loader": "^9.5.0" }, - "bundledDependencies": [ - "tcp-port-used", - "kill-port-process" - ], "engines": { "node": ">=18.0.0" }, @@ -44,5 +35,6 @@ "dist/*", "package.json", "README.md" - ] + ], + "bundleDependencies": [] } diff --git a/plugins/conversational-plugin/src/index.ts b/plugins/conversational-plugin/src/index.ts new file mode 100644 index 0000000000..24902973f9 --- /dev/null +++ b/plugins/conversational-plugin/src/index.ts @@ -0,0 +1,211 @@ +import { PluginType, fs } from "@janhq/core"; +import { ConversationalPlugin } from "@janhq/core/lib/plugins"; +import { Message, Conversation } from "@janhq/core/lib/types"; + +/** + * JanConversationalPlugin is a ConversationalPlugin implementation that provides + * functionality for managing conversations in a Jan bot. + */ +export default class JanConversationalPlugin implements ConversationalPlugin { + /** + * Returns the type of the plugin. + */ + type(): PluginType { + return PluginType.Conversational; + } + + /** + * Called when the plugin is loaded. + */ + onLoad() { + console.debug("JanConversationalPlugin loaded"); + } + + /** + * Called when the plugin is unloaded. + */ + onUnload() { + console.debug("JanConversationalPlugin unloaded"); + } + + /** + * Returns a Promise that resolves to an array of Conversation objects. + */ + getConversations(): Promise { + return this.getConversationDocs().then((conversationIds) => + Promise.all( + conversationIds.map((conversationId) => + this.loadConversationFromMarkdownFile( + `conversations/${conversationId}` + ) + ) + ).then((conversations) => + conversations.sort( + (a, b) => + new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime() + ) + ) + ); + } + + /** + * Saves a Conversation object to a Markdown file. + * @param conversation The Conversation object to save. + */ + saveConversation(conversation: Conversation): Promise { + return this.writeMarkdownToFile(conversation); + } + + /** + * Deletes a conversation with the specified ID. + * @param conversationId The ID of the conversation to delete. + */ + deleteConversation(conversationId: string): Promise { + return fs.deleteFile(`conversations/${conversationId}.md`); + } + + /** + * Returns a Promise that resolves to an array of conversation IDs. + * The conversation IDs are the names of the Markdown files in the "conversations" directory. + * @private + */ + private async getConversationDocs(): Promise { + return fs.listFiles("conversations").then((files: string[]) => { + return Promise.all( + files.filter((file) => file.startsWith("jan-")) + ); + }); + } + + /** + * Parses a Markdown string and returns a Conversation object. + * @param markdown The Markdown string to parse. + * @private + */ + private parseConversationMarkdown(markdown: string): Conversation { + const conversation: Conversation = { + _id: "", + name: "", + messages: [], + }; + var currentMessage: Message | undefined = undefined; + for (const line of markdown.split("\n")) { + const trimmedLine = line.trim(); + if (trimmedLine.startsWith("- _id:")) { + conversation._id = trimmedLine.replace("- _id:", "").trim(); + } else if (trimmedLine.startsWith("- modelId:")) { + conversation.modelId = trimmedLine.replace("- modelId:", "").trim(); + } else if (trimmedLine.startsWith("- name:")) { + conversation.name = trimmedLine.replace("- name:", "").trim(); + } else if (trimmedLine.startsWith("- lastMessage:")) { + conversation.message = trimmedLine.replace("- lastMessage:", "").trim(); + } else if (trimmedLine.startsWith("- summary:")) { + conversation.summary = trimmedLine.replace("- summary:", "").trim(); + } else if ( + trimmedLine.startsWith("- createdAt:") && + currentMessage === undefined + ) { + conversation.createdAt = trimmedLine.replace("- createdAt:", "").trim(); + } else if (trimmedLine.startsWith("- updatedAt:")) { + conversation.updatedAt = trimmedLine.replace("- updatedAt:", "").trim(); + } else if (trimmedLine.startsWith("- botId:")) { + conversation.botId = trimmedLine.replace("- botId:", "").trim(); + } else if (trimmedLine.startsWith("- user:")) { + if (currentMessage) + currentMessage.user = trimmedLine.replace("- user:", "").trim(); + } else if (trimmedLine.startsWith("- createdAt:")) { + if (currentMessage) + currentMessage.createdAt = trimmedLine + .replace("- createdAt:", "") + .trim(); + + currentMessage.updatedAt = currentMessage.createdAt; + } else if (trimmedLine.startsWith("- message:")) { + if (currentMessage) + currentMessage.message = trimmedLine.replace("- message:", "").trim(); + } else if (trimmedLine.startsWith("- Message ")) { + const messageMatch = trimmedLine.match(/- Message (message-\d+):/); + if (messageMatch) { + if (currentMessage) { + conversation.messages.push(currentMessage); + } + currentMessage = { _id: messageMatch[1] }; + } + } else if ( + currentMessage?.message && + !trimmedLine.startsWith("## Messages") + ) { + currentMessage.message = currentMessage.message + "\n" + line.trim(); + } else if (trimmedLine.startsWith("## Messages")) { + currentMessage = undefined; + } else { + console.log("missing field processing: ", trimmedLine); + } + } + + return conversation; + } + + /** + * Loads a Conversation object from a Markdown file. + * @param filePath The path to the Markdown file. + * @private + */ + private async loadConversationFromMarkdownFile( + filePath: string + ): Promise { + try { + const markdown: string = await fs.readFile(filePath); + return this.parseConversationMarkdown(markdown); + } catch (err) { + return undefined; + } + } + + /** + * Generates a Markdown string from a Conversation object. + * @param conversation The Conversation object to generate Markdown from. + * @private + */ + private generateMarkdown(conversation: Conversation): string { + // Generate the Markdown content based on the Conversation object + const conversationMetadata = ` + - _id: ${conversation._id} + - modelId: ${conversation.modelId} + - name: ${conversation.name} + - lastMessage: ${conversation.message} + - summary: ${conversation.summary} + - createdAt: ${conversation.createdAt} + - updatedAt: ${conversation.updatedAt} + - botId: ${conversation.botId} + `; + + const messages = conversation.messages.map( + (message) => ` + - Message ${message._id}: + - createdAt: ${message.createdAt} + - user: ${message.user} + - message: ${message.message?.trim()} + ` + ); + + return `## Conversation Metadata + ${conversationMetadata} +## Messages + ${messages.map((msg) => msg.trim()).join("\n")} + `; + } + + /** + * Writes a Conversation object to a Markdown file. + * @param conversation The Conversation object to write to a Markdown file. + * @private + */ + private async writeMarkdownToFile(conversation: Conversation) { + await fs.mkdir("conversations"); + // Generate the Markdown content + const markdownContent = this.generateMarkdown(conversation); + // Write the content to a Markdown file + await fs.writeFile(`conversations/${conversation._id}.md`, markdownContent); + } +} diff --git a/plugins/openai-plugin/tsconfig.json b/plugins/conversational-plugin/tsconfig.json similarity index 74% rename from plugins/openai-plugin/tsconfig.json rename to plugins/conversational-plugin/tsconfig.json index 3b321034a8..2477d58ce5 100644 --- a/plugins/openai-plugin/tsconfig.json +++ b/plugins/conversational-plugin/tsconfig.json @@ -7,6 +7,8 @@ "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": false, - "skipLibCheck": true - } + "skipLibCheck": true, + "rootDir": "./src" + }, + "include": ["./src"] } diff --git a/plugins/openai-plugin/webpack.config.js b/plugins/conversational-plugin/webpack.config.js similarity index 65% rename from plugins/openai-plugin/webpack.config.js rename to plugins/conversational-plugin/webpack.config.js index 4583497e7f..d4b0db2bd5 100644 --- a/plugins/openai-plugin/webpack.config.js +++ b/plugins/conversational-plugin/webpack.config.js @@ -1,10 +1,9 @@ const path = require("path"); const webpack = require("webpack"); -const packageJson = require("./package.json"); module.exports = { experiments: { outputModule: true }, - entry: "./index.ts", // Adjust the entry point to match your project's main file + entry: "./src/index.ts", // Adjust the entry point to match your project's main file mode: "production", module: { rules: [ @@ -20,14 +19,13 @@ module.exports = { path: path.resolve(__dirname, "dist"), library: { type: "module" }, // Specify ESM output format }, - plugins: [ - new webpack.DefinePlugin({ - PLUGIN_NAME: JSON.stringify(packageJson.name), - MODULE_PATH: JSON.stringify(`${packageJson.name}/${packageJson.module}`), - }), - ], + plugins: [new webpack.DefinePlugin({})], resolve: { extensions: [".ts", ".js"], }, + // Do not minify the output, otherwise it breaks the class registration + optimization: { + minimize: false, + }, // Add loaders and other configuration as needed for your project }; diff --git a/plugins/data-plugin/@types/global.d.ts b/plugins/data-plugin/@types/global.d.ts deleted file mode 100644 index d9ac971729..0000000000 --- a/plugins/data-plugin/@types/global.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -declare const PLUGIN_NAME: string; -declare const MODULE_PATH: string; -declare const PLUGIN_CATALOG: string; diff --git a/plugins/data-plugin/README.md b/plugins/data-plugin/README.md deleted file mode 100644 index 802883fab9..0000000000 --- a/plugins/data-plugin/README.md +++ /dev/null @@ -1,6 +0,0 @@ -## Jan data handler plugin - -- index.ts: Main entry point for the plugin. -- module.ts: Defines the plugin module which would be executed by the main node process. -- package.json: Plugin & npm module manifest. - diff --git a/plugins/data-plugin/config/tsconfig.cjs.json b/plugins/data-plugin/config/tsconfig.cjs.json deleted file mode 100644 index 46377e6e25..0000000000 --- a/plugins/data-plugin/config/tsconfig.cjs.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./../tsconfig.json", - "compilerOptions": { - "outDir": "./../dist/cjs", - "module": "commonjs" - }, - "files": ["../module.ts"] -} diff --git a/plugins/data-plugin/config/tsconfig.esm.json b/plugins/data-plugin/config/tsconfig.esm.json deleted file mode 100644 index 4cddff8063..0000000000 --- a/plugins/data-plugin/config/tsconfig.esm.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "./../tsconfig.json", - "compilerOptions": { - "outDir": "./../dist/esm", - "module": "esnext" - }, - "include": ["@types/*"], - "files": ["../@types/global.d.ts", "../index.ts"] -} diff --git a/plugins/data-plugin/index.ts b/plugins/data-plugin/index.ts deleted file mode 100644 index 0ff0fcd681..0000000000 --- a/plugins/data-plugin/index.ts +++ /dev/null @@ -1,345 +0,0 @@ -import { - invokePluginFunc, - store, - RegisterExtensionPoint, - StoreService, - DataService, - PluginService, -} from "@janhq/core"; - -/** - * Create a collection on data store - * - * @param name name of the collection to create - * @param schema schema of the collection to create, include fields and their types - * @returns Promise - * - */ -function createCollection({ - name, - schema, -}: { - name: string; - schema?: { [key: string]: any }; -}): Promise { - return invokePluginFunc(MODULE_PATH, "createCollection", name, schema); -} - -/** - * Delete a collection - * - * @param name name of the collection to delete - * @returns Promise - * - */ -function deleteCollection(name: string): Promise { - return invokePluginFunc(MODULE_PATH, "deleteCollection", name); -} - -/** - * Insert a value to a collection - * - * @param collectionName name of the collection - * @param value value to insert - * @returns Promise - * - */ -function insertOne({ - collectionName, - value, -}: { - collectionName: string; - value: any; -}): Promise { - return invokePluginFunc(MODULE_PATH, "insertOne", collectionName, value); -} - -/** - * Update value of a collection's record - * - * @param collectionName name of the collection - * @param key key of the record to update - * @param value value to update - * @returns Promise - * - */ -function updateOne({ - collectionName, - key, - value, -}: { - collectionName: string; - key: string; - value: any; -}): Promise { - return invokePluginFunc(MODULE_PATH, "updateOne", collectionName, key, value); -} - -/** - * Updates all records that match a selector in a collection in the data store. - * @param collectionName - The name of the collection containing the records to update. - * @param selector - The selector to use to get the records to update. - * @param value - The new value for the records. - * @returns {Promise} A promise that resolves when the records are updated. - */ -function updateMany({ - collectionName, - value, - selector, -}: { - collectionName: string; - value: any; - selector?: { [key: string]: any }; -}): Promise { - return invokePluginFunc( - MODULE_PATH, - "updateMany", - collectionName, - value, - selector - ); -} - -/** - * Delete a collection's record - * - * @param collectionName name of the collection - * @param key key of the record to delete - * @returns Promise - * - */ -function deleteOne({ - collectionName, - key, -}: { - collectionName: string; - key: string; -}): Promise { - return invokePluginFunc(MODULE_PATH, "deleteOne", collectionName, key); -} - -/** - * Deletes all records with a matching key from a collection in the data store. - * - * @param collectionName name of the collection - * @param selector selector to use to get the records to delete. - * @returns {Promise} - * - */ -function deleteMany({ - collectionName, - selector, -}: { - collectionName: string; - selector?: { [key: string]: any }; -}): Promise { - return invokePluginFunc(MODULE_PATH, "deleteMany", collectionName, selector); -} - -/** - * Retrieve a record from a collection in the data store. - * @param {string} collectionName - The name of the collection containing the record to retrieve. - * @param {string} key - The key of the record to retrieve. - * @returns {Promise} A promise that resolves when the record is retrieved. - */ -function findOne({ - collectionName, - key, -}: { - collectionName: string; - key: string; -}): Promise { - return invokePluginFunc(MODULE_PATH, "findOne", collectionName, key); -} - -/** - * Gets records in a collection in the data store using a selector. - * @param {string} collectionName - The name of the collection containing the record to get the value from. - * @param {{ [key: string]: any }} selector - The selector to use to get the value from the record. - * @param {[{ [key: string]: any }]} sort - The sort options to use to retrieve records. - * @returns {Promise} A promise that resolves with the selected value. - */ -function findMany({ - collectionName, - selector, - sort, -}: { - collectionName: string; - selector: { [key: string]: any }; - sort?: [{ [key: string]: any }]; -}): Promise { - return invokePluginFunc( - MODULE_PATH, - "findMany", - collectionName, - selector, - sort - ); -} - -function onStart() { - createCollection({ name: "conversations", schema: {} }); - createCollection({ name: "messages", schema: {} }); - createCollection({ name: "bots", schema: {} }); -} - -// Register all the above functions and objects with the relevant extension points -// prettier-ignore -export function init({ register }: { register: RegisterExtensionPoint }) { - register(PluginService.OnStart, PLUGIN_NAME, onStart); - register(StoreService.CreateCollection, createCollection.name, createCollection); - register(StoreService.DeleteCollection, deleteCollection.name, deleteCollection); - - register(StoreService.InsertOne, insertOne.name, insertOne); - register(StoreService.UpdateOne, updateOne.name, updateOne); - register(StoreService.UpdateMany, updateMany.name, updateMany); - register(StoreService.DeleteOne, deleteOne.name, deleteOne); - register(StoreService.DeleteMany, deleteMany.name, deleteMany); - register(StoreService.FindOne, findOne.name, findOne); - register(StoreService.FindMany, findMany.name, findMany); - - // for conversations management - register(DataService.GetConversations, getConversations.name, getConversations); - register(DataService.GetConversationById,getConversationById.name,getConversationById); - register(DataService.CreateConversation, createConversation.name, createConversation); - register(DataService.UpdateConversation, updateConversation.name, updateConversation); - register(DataService.DeleteConversation, deleteConversation.name, deleteConversation); - - // for messages management - register(DataService.UpdateMessage, updateMessage.name, updateMessage); - register(DataService.CreateMessage, createMessage.name, createMessage); - register(DataService.GetConversationMessages, getConversationMessages.name, getConversationMessages); - - // for bots management - register(DataService.CreateBot, createBot.name, createBot); - register(DataService.GetBots, getBots.name, getBots); - register(DataService.GetBotById, getBotById.name, getBotById); - register(DataService.DeleteBot, deleteBot.name, deleteBot); - register(DataService.UpdateBot, updateBot.name, updateBot); - - // for plugin manifest - register(DataService.GetPluginManifest, getPluginManifest.name, getPluginManifest) -} - -function getConversations(): Promise { - return store.findMany("conversations", {}, [{ updatedAt: "desc" }]); -} - -function getConversationById(id: string): Promise { - return store.findOne("conversations", id); -} - -function createConversation(conversation: any): Promise { - return store.insertOne("conversations", conversation); -} - -function updateConversation(conversation: any): Promise { - return store.updateOne("conversations", conversation._id, conversation); -} - -function createMessage(message: any): Promise { - return store.insertOne("messages", message); -} - -function updateMessage(message: any): Promise { - return store.updateOne("messages", message._id, message); -} - -function deleteConversation(id: any) { - return store - .deleteOne("conversations", id) - .then(() => store.deleteMany("messages", { conversationId: id })); -} - -function getConversationMessages(conversationId: any) { - return store.findMany("messages", { conversationId }, [ - { createdAt: "desc" }, - ]); -} - -function createBot(bot: any): Promise { - console.debug("Creating bot", JSON.stringify(bot, null, 2)); - return store - .insertOne("bots", bot) - .then(() => { - console.debug("Bot created", JSON.stringify(bot, null, 2)); - return Promise.resolve(); - }) - .catch((err) => { - console.error("Error creating bot", err); - return Promise.reject(err); - }); -} - -function getBots(): Promise { - console.debug("Getting bots"); - return store - .findMany("bots", { name: { $gt: null } }) - .then((bots) => { - console.debug("Bots retrieved", JSON.stringify(bots, null, 2)); - return Promise.resolve(bots); - }) - .catch((err) => { - console.error("Error getting bots", err); - return Promise.reject(err); - }); -} - -function deleteBot(id: string): Promise { - console.debug("Deleting bot", id); - return store - .deleteOne("bots", id) - .then(() => { - console.debug("Bot deleted", id); - return Promise.resolve(); - }) - .catch((err) => { - console.error("Error deleting bot", err); - return Promise.reject(err); - }); -} - -function updateBot(bot: any): Promise { - console.debug("Updating bot", JSON.stringify(bot, null, 2)); - return store - .updateOne("bots", bot._id, bot) - .then(() => { - console.debug("Bot updated"); - return Promise.resolve(); - }) - .catch((err) => { - console.error("Error updating bot", err); - return Promise.reject(err); - }); -} - -function getBotById(botId: string): Promise { - console.debug("Getting bot", botId); - return store - .findOne("bots", botId) - .then((bot) => { - console.debug("Bot retrieved", JSON.stringify(bot, null, 2)); - return Promise.resolve(bot); - }) - .catch((err) => { - console.error("Error getting bot", err); - return Promise.reject(err); - }); -} - -/** - * Retrieves the plugin manifest by importing the remote model catalog and clearing the cache to get the latest version. - * A timestamp is added to the URL to prevent caching. - * @returns A Promise that resolves with the plugin manifest. - */ -function getPluginManifest(): Promise { - // Clear cache to get the latest model catalog - delete require.cache[ - require.resolve(/* webpackIgnore: true */ PLUGIN_CATALOG) - ]; - // Import the remote model catalog - // Add a timestamp to the URL to prevent caching - return import( - /* webpackIgnore: true */ PLUGIN_CATALOG + `?t=${Date.now()}` - ).then((module) => module.default); -} diff --git a/plugins/data-plugin/module.ts b/plugins/data-plugin/module.ts deleted file mode 100644 index 21878d0f77..0000000000 --- a/plugins/data-plugin/module.ts +++ /dev/null @@ -1,246 +0,0 @@ -var PouchDB = require("pouchdb-node"); -PouchDB.plugin(require("pouchdb-find")); -var path = require("path"); -var { app } = require("electron"); -var fs = require("fs"); - -const dbs: Record = {}; - -/** - * Create a collection on data store - * - * @param name name of the collection to create - * @param schema schema of the collection to create, include fields and their types - * @returns Promise - * - */ -function createCollection(name: string, schema?: { [key: string]: any }): Promise { - return new Promise((resolve) => { - const dbPath = path.join(appPath(), "databases"); - if (!fs.existsSync(dbPath)) fs.mkdirSync(dbPath); - const db = new PouchDB(`${path.join(dbPath, name)}`); - dbs[name] = db; - resolve(); - }); -} - -/** - * Delete a collection - * - * @param name name of the collection to delete - * @returns Promise - * - */ -function deleteCollection(name: string): Promise { - // Do nothing with Unstructured Database - return dbs[name].destroy(); -} - -/** - * Insert a value to a collection - * - * @param collectionName name of the collection - * @param value value to insert - * @returns Promise - * - */ -function insertOne(collectionName: string, value: any): Promise { - if (!value._id) return dbs[collectionName].post(value).then((doc) => doc.id); - return dbs[collectionName].put(value).then((doc) => doc.id); -} - -/** - * Update value of a collection's record - * - * @param collectionName name of the collection - * @param key key of the record to update - * @param value value to update - * @returns Promise - * - */ -function updateOne(collectionName: string, key: string, value: any): Promise { - console.debug(`updateOne ${collectionName}: ${key} - ${JSON.stringify(value)}`); - return dbs[collectionName].get(key).then((doc) => { - return dbs[collectionName].put({ - _id: key, - _rev: doc._rev, - ...value, - }, - { force: true }); - }).then((res: any) => { - console.info(`updateOne ${collectionName} result: ${JSON.stringify(res)}`); - }).catch((err: any) => { - console.error(`updateOne ${collectionName} error: ${err}`); - }); -} - -/** - * Update value of a collection's records - * - * @param collectionName name of the collection - * @param selector selector of records to update - * @param value value to update - * @returns Promise - * - */ -function updateMany(collectionName: string, value: any, selector?: { [key: string]: any }): Promise { - // Creates keys from selector for indexing - const keys = selector ? Object.keys(selector) : []; - - // At a basic level, there are two steps to running a query: createIndex() - // (to define which fields to index) and find() (to query the index). - return ( - keys.length > 0 - ? dbs[collectionName].createIndex({ - // There is selector so we need to create index - index: { fields: keys }, - }) - : Promise.resolve() - ) // No selector, so no need to create index - .then(() => - dbs[collectionName].find({ - // Find documents using Mango queries - selector, - }) - ) - .then((data) => { - const docs = data.docs.map((doc) => { - // Update doc with new value - return (doc = { - ...doc, - ...value, - }); - }); - return dbs[collectionName].bulkDocs(docs); - }); -} - -/** - * Delete a collection's record - * - * @param collectionName name of the collection - * @param key key of the record to delete - * @returns Promise - * - */ -function deleteOne(collectionName: string, key: string): Promise { - return findOne(collectionName, key).then((doc) => dbs[collectionName].remove(doc)); -} - -/** - * Delete a collection records by selector - * - * @param {string} collectionName name of the collection - * @param {{ [key: string]: any }} selector selector for retrieving records. - * @returns Promise - * - */ -function deleteMany(collectionName: string, selector?: { [key: string]: any }): Promise { - // Creates keys from selector for indexing - const keys = selector ? Object.keys(selector) : []; - - // At a basic level, there are two steps to running a query: createIndex() - // (to define which fields to index) and find() (to query the index). - return ( - keys.length > 0 - ? dbs[collectionName].createIndex({ - // There is selector so we need to create index - index: { fields: keys }, - }) - : Promise.resolve() - ) // No selector, so no need to create index - .then(() => - dbs[collectionName].find({ - // Find documents using Mango queries - selector, - }) - ) - .then((data) => { - return Promise.all( - // Remove documents - data.docs.map((doc) => { - return dbs[collectionName].remove(doc); - }) - ); - }); -} - -/** - * Retrieve a record from a collection in the data store. - * @param {string} collectionName - The name of the collection containing the record to retrieve. - * @param {string} key - The key of the record to retrieve. - * @returns {Promise} A promise that resolves when the record is retrieved. - */ -function findOne(collectionName: string, key: string): Promise { - return dbs[collectionName].get(key).catch(() => undefined); -} - -/** - * Gets records in a collection in the data store using a selector. - * @param {string} collectionName - The name of the collection containing records to retrieve. - * @param {{ [key: string]: any }} selector - The selector to use to retrieve records. - * @param {[{ [key: string]: any }]} sort - The sort options to use to retrieve records. - * @returns {Promise} A promise that resolves with the selected records. - */ -function findMany( - collectionName: string, - selector?: { [key: string]: any }, - sort?: [{ [key: string]: any }] -): Promise { - const keys = selector ? Object.keys(selector) : []; - const sortKeys = sort ? sort.flatMap((e) => (e ? Object.keys(e) : undefined)) : []; - - // Note that we are specifying that the field must be greater than or equal to null - // which is a workaround for the fact that the Mango query language requires us to have a selector. - // In CouchDB collation order, null is the "lowest" value, and so this will return all documents regardless of their field value. - sortKeys.forEach((key) => { - if (!keys.includes(key)) { - selector = { ...selector, [key]: { $gt: null } }; - } - }); - - // There is no selector & sort, so we can just use allDocs() to get all the documents. - if (sortKeys.concat(keys).length === 0) { - return dbs[collectionName] - .allDocs({ - include_docs: true, - endkey: "_design", - inclusive_end: false, - }) - .then((data) => data.rows.map((row) => row.doc)); - } - // At a basic level, there are two steps to running a query: createIndex() - // (to define which fields to index) and find() (to query the index). - return dbs[collectionName] - .createIndex({ - // Create index for selector & sort - index: { fields: sortKeys.concat(keys) }, - }) - .then(() => { - // Find documents using Mango queries - return dbs[collectionName].find({ - selector, - sort, - }); - }) - .then((data) => data.docs); // Return documents -} - -function appPath() { - if (app) { - return app.getPath("userData"); - } - return process.env.APPDATA || (process.platform == 'darwin' ? process.env.HOME + '/Library/Preferences' : process.env.HOME + "/.local/share"); -} - -module.exports = { - createCollection, - deleteCollection, - insertOne, - findOne, - findMany, - updateOne, - updateMany, - deleteOne, - deleteMany, -}; diff --git a/plugins/data-plugin/package.json b/plugins/data-plugin/package.json deleted file mode 100644 index dbdd31bbc9..0000000000 --- a/plugins/data-plugin/package.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "name": "@janhq/data-plugin", - "version": "1.0.19", - "description": "The Data Connector provides easy access to a data API using the PouchDB engine. It offers accessible data management capabilities.", - "icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/circle-stack.svg", - "main": "dist/esm/index.js", - "module": "dist/cjs/module.js", - "author": "Jan ", - "license": "AGPL-3.0", - "supportCloudNative": true, - "url": "/plugins/data-plugin/index.js", - "activationPoints": [ - "init" - ], - "scripts": { - "build": "tsc -b ./config/tsconfig.esm.json && tsc -b ./config/tsconfig.cjs.json && webpack --config webpack.config.js", - "build:deps": "electron-rebuild -f -w leveldown@5.6.0 --arch=arm64 -v 26.2.1 && node-gyp -C ./node_modules/leveldown clean && mkdir -p ./node_modules/leveldown/prebuilds/darwin-arm64 && cp ./node_modules/leveldown/bin/darwin-arm64-116/leveldown.node ./node_modules/leveldown/prebuilds/darwin-arm64/node.napi.node", - "postinstall": "rimraf *.tgz --glob && npm run build", - "build:publish": "npm pack && cpx *.tgz ../../electron/core/pre-install" - }, - "exports": { - "import": "./dist/esm/index.js", - "require": "./dist/cjs/module.js", - "default": "./dist/esm/index.js" - }, - "devDependencies": { - "cpx": "^1.5.0", - "node-pre-gyp": "^0.17.0", - "rimraf": "^3.0.2", - "ts-loader": "^9.4.4", - "ts-node": "^10.9.1", - "typescript": "^5.2.2", - "webpack": "^5.88.2", - "webpack-cli": "^5.1.4" - }, - "files": [ - "dist/**", - "package.json", - "node_modules" - ], - "dependencies": { - "@janhq/core": "^0.1.7", - "electron": "26.2.1", - "electron-rebuild": "^3.2.9", - "node-gyp": "^9.4.1", - "pouchdb-find": "^8.0.1", - "pouchdb-node": "^8.0.1" - }, - "bundleDependencies": [ - "pouchdb-node", - "pouchdb-find" - ] -} diff --git a/plugins/data-plugin/webpack.config.js b/plugins/data-plugin/webpack.config.js deleted file mode 100644 index affb5950a8..0000000000 --- a/plugins/data-plugin/webpack.config.js +++ /dev/null @@ -1,39 +0,0 @@ -const path = require("path"); -const webpack = require("webpack"); -const packageJson = require("./package.json"); - -module.exports = { - experiments: { outputModule: true }, - entry: "./index.ts", // Adjust the entry point to match your project's main file - mode: "production", - module: { - rules: [ - { - test: /\.tsx?$/, - use: "ts-loader", - exclude: /node_modules/, - }, - ], - }, - plugins: [ - new webpack.DefinePlugin({ - PLUGIN_NAME: JSON.stringify(packageJson.name), - MODULE_PATH: JSON.stringify(`${packageJson.name}/${packageJson.module}`), - PLUGIN_CATALOG: JSON.stringify( - "https://cdn.jsdelivr.net/npm/@janhq/plugin-catalog@latest/dist/index.js" - ), - }), - ], - output: { - filename: "esm/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"], - }, - optimization: { - minimize: false, - }, - // Add loaders and other configuration as needed for your project -}; diff --git a/plugins/inference-plugin/@types/global.d.ts b/plugins/inference-plugin/@types/global.d.ts deleted file mode 100644 index a0c04db1b0..0000000000 --- a/plugins/inference-plugin/@types/global.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -declare const PLUGIN_NAME: string; -declare const MODULE_PATH: string; -declare const INFERENCE_URL: string; diff --git a/plugins/inference-plugin/index.ts b/plugins/inference-plugin/index.ts deleted file mode 100644 index 27acbc8215..0000000000 --- a/plugins/inference-plugin/index.ts +++ /dev/null @@ -1,206 +0,0 @@ -import { - EventName, - InferenceService, - NewMessageRequest, - PluginService, - events, - store, - invokePluginFunc, -} from "@janhq/core"; -import { Observable } from "rxjs"; - -const initModel = async (product) => - invokePluginFunc(MODULE_PATH, "initModel", product); - -const stopModel = () => { - invokePluginFunc(MODULE_PATH, "killSubprocess"); -}; - -function requestInference( - recentMessages: any[], - bot?: any -): Observable { - return new Observable((subscriber) => { - const requestBody = JSON.stringify({ - messages: recentMessages, - stream: true, - model: "gpt-3.5-turbo", - max_tokens: bot?.maxTokens ?? 2048, - frequency_penalty: bot?.frequencyPenalty ?? 0, - presence_penalty: bot?.presencePenalty ?? 0, - temperature: bot?.customTemperature ?? 0, - }); - console.debug(`Request body: ${requestBody}`); - fetch(INFERENCE_URL, { - method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "text/event-stream", - "Access-Control-Allow-Origin": "*", - }, - body: requestBody, - }) - .then(async (response) => { - 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(); - if (done) { - console.log("SSE stream closed"); - break; - } - 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: ", ""); - } - subscriber.next(content); - } - } - } - subscriber.complete(); - }) - .catch((err) => subscriber.error(err)); - }); -} - -async function retrieveLastTenMessages(conversationId: string, bot?: any) { - // TODO: Common collections should be able to access via core functions instead of store - const messageHistory = - (await store.findMany("messages", { conversationId }, [ - { createdAt: "asc" }, - ])) ?? []; - - let recentMessages = messageHistory - .filter( - (e) => e.message !== "" && (e.user === "user" || e.user === "assistant") - ) - .slice(-9) - .map((message) => ({ - content: message.message.trim(), - role: message.user === "user" ? "user" : "assistant", - })); - - if (bot && bot.systemPrompt) { - // append bot's system prompt - recentMessages = [ - { - content: `[INST] ${bot.systemPrompt}`, - role: "system", - }, - ...recentMessages, - ]; - } - - console.debug(`Last 10 messages: ${JSON.stringify(recentMessages, null, 2)}`); - - return recentMessages; -} - -async function handleMessageRequest(data: NewMessageRequest) { - const conversation = await store.findOne( - "conversations", - data.conversationId - ); - let bot = undefined; - if (conversation.botId != null) { - bot = await store.findOne("bots", conversation.botId); - } - - const recentMessages = await retrieveLastTenMessages( - data.conversationId, - bot - ); - const message = { - ...data, - message: "", - user: "assistant", - createdAt: new Date().toISOString(), - _id: undefined, - }; - // TODO: Common collections should be able to access via core functions instead of store - const id = await store.insertOne("messages", message); - - message._id = id; - events.emit(EventName.OnNewMessageResponse, message); - - requestInference(recentMessages, bot).subscribe({ - next: (content) => { - message.message = content; - events.emit(EventName.OnMessageResponseUpdate, message); - }, - complete: async () => { - message.message = message.message.trim(); - // TODO: Common collections should be able to access via core functions instead of store - await store.updateOne("messages", message._id, message); - events.emit("OnMessageResponseFinished", message); - // events.emit(EventName.OnMessageResponseFinished, message); - }, - error: async (err) => { - message.message = - message.message.trim() + "\n" + "Error occurred: " + err.message; - events.emit(EventName.OnMessageResponseUpdate, message); - // TODO: Common collections should be able to access via core functions instead of store - await store.updateOne("messages", message._id, message); - }, - }); -} - -async function inferenceRequest(data: NewMessageRequest): Promise { - const message = { - ...data, - message: "", - user: "assistant", - createdAt: new Date().toISOString(), - }; - return new Promise(async (resolve, reject) => { - const recentMessages = await retrieveLastTenMessages(data.conversationId); - requestInference([ - ...recentMessages, - { role: "user", content: data.message }, - ]).subscribe({ - next: (content) => { - message.message = content; - }, - complete: async () => { - resolve(message); - }, - error: async (err) => { - reject(err); - }, - }); - }); -} - -const registerListener = () => { - events.on(EventName.OnNewMessageRequest, handleMessageRequest); -}; - -const killSubprocess = () => { - invokePluginFunc(MODULE_PATH, "killSubprocess"); -}; - -const onStart = async () => { - // Try killing any existing subprocesses related to Nitro - killSubprocess(); - - registerListener(); -}; -// Register all the above functions and objects with the relevant extension points -export function init({ register }) { - register(PluginService.OnStart, PLUGIN_NAME, onStart); - register(InferenceService.InitModel, initModel.name, initModel); - register(InferenceService.StopModel, stopModel.name, stopModel); - register( - InferenceService.InferenceRequest, - inferenceRequest.name, - inferenceRequest - ); -} diff --git a/plugins/inference-plugin/package.json b/plugins/inference-plugin/package.json index c169386276..f6b56e40d8 100644 --- a/plugins/inference-plugin/package.json +++ b/plugins/inference-plugin/package.json @@ -2,7 +2,6 @@ "name": "@janhq/inference-plugin", "version": "1.0.21", "description": "Inference Plugin, powered by @janhq/nitro, bring a high-performance Llama model inference in pure C++.", - "icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/command-line.svg", "main": "dist/index.js", "module": "dist/module.js", "author": "Jan ", @@ -35,7 +34,7 @@ "webpack-cli": "^5.1.4" }, "dependencies": { - "@janhq/core": "^0.1.6", + "@janhq/core": "file:../../core", "download-cli": "^1.1.1", "kill-port": "^2.0.1", "rxjs": "^7.8.1", diff --git a/plugins/inference-plugin/src/@types/global.d.ts b/plugins/inference-plugin/src/@types/global.d.ts new file mode 100644 index 0000000000..7267f09400 --- /dev/null +++ b/plugins/inference-plugin/src/@types/global.d.ts @@ -0,0 +1,2 @@ +declare const MODULE: string; +declare const INFERENCE_URL: string; diff --git a/plugins/inference-plugin/src/helpers/sse.ts b/plugins/inference-plugin/src/helpers/sse.ts new file mode 100644 index 0000000000..f63cc027b7 --- /dev/null +++ b/plugins/inference-plugin/src/helpers/sse.ts @@ -0,0 +1,52 @@ +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. + * @returns An Observable that emits the generated response as a string. + */ +export function requestInference(recentMessages: any[]): Observable { + return new Observable((subscriber) => { + const requestBody = JSON.stringify({ + messages: recentMessages, + stream: true, + model: "gpt-3.5-turbo", + max_tokens: 2048, + }); + fetch(INFERENCE_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "text/event-stream", + "Access-Control-Allow-Origin": "*", + }, + body: requestBody, + }) + .then(async (response) => { + 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(); + if (done) { + break; + } + 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: ", ""); + } + subscriber.next(content); + } + } + } + subscriber.complete(); + }) + .catch((err) => subscriber.error(err)); + }); +} diff --git a/plugins/inference-plugin/src/index.ts b/plugins/inference-plugin/src/index.ts new file mode 100644 index 0000000000..b8a3cf2abe --- /dev/null +++ b/plugins/inference-plugin/src/index.ts @@ -0,0 +1,140 @@ +/** + * @file This file exports a class that implements the InferencePlugin 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. + * @version 1.0.0 + * @module inference-plugin/src/index + */ + +import { + EventName, + MessageHistory, + NewMessageRequest, + PluginType, + events, + executeOnMain, +} from "@janhq/core"; +import { InferencePlugin } from "@janhq/core/lib/plugins"; +import { requestInference } from "./helpers/sse"; + +/** + * A class that implements the InferencePlugin 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 JanInferencePlugin implements InferencePlugin { + /** + * Returns the type of the plugin. + * @returns {PluginType} The type of the plugin. + */ + type(): PluginType { + return PluginType.Inference; + } + + /** + * Subscribes to events emitted by the @janhq/core package. + */ + onLoad(): void { + events.on(EventName.OnNewMessageRequest, this.handleMessageRequest); + } + + /** + * Stops the model inference. + */ + onUnload(): void { + this.stopModel(); + } + + /** + * Initializes the model with the specified file name. + * @param {string} modelFileName - The name of the model file. + * @returns {Promise} A promise that resolves when the model is initialized. + */ + initModel(modelFileName: string): Promise { + return executeOnMain(MODULE, "initModel", modelFileName); + } + + /** + * Stops the model. + * @returns {Promise} A promise that resolves when the model is stopped. + */ + stopModel(): Promise { + return executeOnMain(MODULE, "killSubprocess"); + } + + /** + * Makes a single response inference request. + * @param {NewMessageRequest} data - The data for the inference request. + * @returns {Promise} A promise that resolves with the inference response. + */ + async inferenceRequest(data: NewMessageRequest): Promise { + const message = { + ...data, + message: "", + user: "assistant", + createdAt: new Date().toISOString(), + }; + const prompts: [MessageHistory] = [ + { + role: "user", + content: data.message, + }, + ]; + const recentMessages = await (data.history ?? prompts); + + return new Promise(async (resolve, reject) => { + requestInference([ + ...recentMessages, + { role: "user", content: data.message }, + ]).subscribe({ + next: (content) => { + message.message = content; + }, + complete: async () => { + resolve(message); + }, + error: async (err) => { + reject(err); + }, + }); + }); + } + + /** + * Handles a new message request by making an inference request and emitting events. + * @param {NewMessageRequest} data - The data for the new message request. + */ + private async handleMessageRequest(data: NewMessageRequest) { + const prompts: [MessageHistory] = [ + { + role: "user", + content: data.message, + }, + ]; + const recentMessages = await (data.history ?? prompts); + const message = { + ...data, + message: "", + user: "assistant", + createdAt: new Date().toISOString(), + _id: `message-${Date.now()}`, + }; + events.emit(EventName.OnNewMessageResponse, message); + + requestInference(recentMessages).subscribe({ + next: (content) => { + message.message = content; + events.emit(EventName.OnMessageResponseUpdate, message); + }, + complete: async () => { + message.message = message.message.trim(); + events.emit(EventName.OnMessageResponseFinished, message); + }, + error: async (err) => { + message.message = + message.message.trim() + "\n" + "Error occurred: " + err.message; + events.emit(EventName.OnMessageResponseUpdate, message); + }, + }); + } +} diff --git a/plugins/inference-plugin/module.ts b/plugins/inference-plugin/src/module.ts similarity index 100% rename from plugins/inference-plugin/module.ts rename to plugins/inference-plugin/src/module.ts diff --git a/plugins/inference-plugin/tsconfig.json b/plugins/inference-plugin/tsconfig.json index 3399507196..b48175a169 100644 --- a/plugins/inference-plugin/tsconfig.json +++ b/plugins/inference-plugin/tsconfig.json @@ -8,6 +8,8 @@ "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": false, - "skipLibCheck": true - } + "skipLibCheck": true, + "rootDir": "./src" + }, + "include": ["./src"] } diff --git a/plugins/inference-plugin/webpack.config.js b/plugins/inference-plugin/webpack.config.js index 0f5f59d7fc..f6f32a263a 100644 --- a/plugins/inference-plugin/webpack.config.js +++ b/plugins/inference-plugin/webpack.config.js @@ -4,7 +4,7 @@ const packageJson = require("./package.json"); module.exports = { experiments: { outputModule: true }, - entry: "./index.ts", // Adjust the entry point to match your project's main file + entry: "./src/index.ts", // Adjust the entry point to match your project's main file mode: "production", module: { rules: [ @@ -17,8 +17,7 @@ module.exports = { }, plugins: [ new webpack.DefinePlugin({ - PLUGIN_NAME: JSON.stringify(packageJson.name), - MODULE_PATH: JSON.stringify(`${packageJson.name}/${packageJson.module}`), + MODULE: JSON.stringify(`${packageJson.name}/${packageJson.module}`), INFERENCE_URL: JSON.stringify(process.env.INFERENCE_URL || "http://127.0.0.1:3928/inferences/llamacpp/chat_completion"), }), ], diff --git a/plugins/model-management-plugin/index.ts b/plugins/model-management-plugin/index.ts deleted file mode 100644 index b0fee76303..0000000000 --- a/plugins/model-management-plugin/index.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { - ModelManagementService, - PluginService, - RegisterExtensionPoint, - downloadFile, - deleteFile, - store, - EventName, - events -} from "@janhq/core"; -import { parseToModel } from "./helper"; - -const downloadModel = (product) => { - downloadFile(product.downloadUrl, product.fileName); - checkDownloadProgress(product.fileName); -} - -async function checkDownloadProgress(fileName: string) { - if (typeof window !== "undefined" && typeof (window as any).electronAPI === "undefined") { - const intervalId = setInterval(() => { - fetchDownloadProgress(fileName, intervalId); - }, 3000); - } -} - -async function fetchDownloadProgress(fileName: string, intervalId: NodeJS.Timeout): Promise { - const response = await fetch("/api/v1/downloadProgress", { - method: 'POST', - body: JSON.stringify({ fileName: fileName }), - headers: { 'Content-Type': 'application/json', 'Authorization': '' } - }); - - if (!response.ok) { - events.emit(EventName.OnDownloadError, null); - clearInterval(intervalId); - return; - } - const json = await response.json(); - if (isEmptyObject(json)) { - if (!fileName && intervalId) { - clearInterval(intervalId); - } - return Promise.resolve(""); - } - if (json.success === true) { - events.emit(EventName.OnDownloadSuccess, json); - clearInterval(intervalId); - return Promise.resolve(""); - } else { - events.emit(EventName.OnDownloadUpdate, json); - return Promise.resolve(json.fileName); - } -} - -function isEmptyObject(ojb: any): boolean { - return Object.keys(ojb).length === 0; -} - -const deleteModel = (path) => deleteFile(path); - -/** - * Retrieves a list of configured models from the model catalog URL. - * @returns A Promise that resolves to an array of configured models. - */ -async function getConfiguredModels(): Promise { - // Add a timestamp to the URL to prevent caching - return import( - /* webpackIgnore: true */ MODEL_CATALOG_URL + `?t=${Date.now()}` - ).then((module) => - module.default.map((e) => { - return parseToModel(e); - }) - ); -} - -/** - * Store a model in the database when user start downloading it - * - * @param model Product - */ -function storeModel(model: any) { - return store.findOne("models", model._id).then((doc) => { - if (doc) { - return store.updateOne("models", model._id, model); - } else { - return store.insertOne("models", model); - } - }); -} - -/** - * Update the finished download time of a model - * - * @param model Product - */ -function updateFinishedDownloadAt(_id: string): Promise { - return store.updateMany( - "models", - { _id }, - { time: Date.now(), finishDownloadAt: 1 } - ); -} - -/** - * Retrieves all finished models from the database. - * - * @returns A promise that resolves with an array of finished models. - */ -function getFinishedDownloadModels(): Promise { - return store.findMany("models", { finishDownloadAt: 1 }); -} - -/** - * Deletes a model from the database. - * - * @param modelId The ID of the model to delete. - * @returns A promise that resolves when the model is deleted. - */ -function deleteDownloadModel(modelId: string): Promise { - return store.deleteOne("models", modelId); -} - -/** - * Retrieves a model from the database by ID. - * - * @param modelId The ID of the model to retrieve. - * @returns A promise that resolves with the model. - */ -function getModelById(modelId: string): Promise { - return store.findOne("models", modelId); -} - -function onStart() { - store.createCollection("models", {}); - if (!(window as any)?.electronAPI) { - fetchDownloadProgress(null, null).then((fileName: string) => fileName && checkDownloadProgress(fileName)); - } -} - -// Register all the above functions and objects with the relevant extension points -export function init({ register }: { register: RegisterExtensionPoint }) { - register(PluginService.OnStart, PLUGIN_NAME, onStart); - - register( - ModelManagementService.DownloadModel, - downloadModel.name, - downloadModel - ); - register(ModelManagementService.DeleteModel, deleteModel.name, deleteModel); - register( - ModelManagementService.GetConfiguredModels, - getConfiguredModels.name, - getConfiguredModels - ); - - register(ModelManagementService.StoreModel, storeModel.name, storeModel); - register( - ModelManagementService.UpdateFinishedDownloadAt, - updateFinishedDownloadAt.name, - updateFinishedDownloadAt - ); - - register( - ModelManagementService.DeleteDownloadModel, - deleteDownloadModel.name, - deleteDownloadModel - ); - register( - ModelManagementService.GetModelById, - getModelById.name, - getModelById - ); - register( - ModelManagementService.GetFinishedDownloadModels, - getFinishedDownloadModels.name, - getFinishedDownloadModels - ); -} diff --git a/plugins/model-management-plugin/module.ts b/plugins/model-management-plugin/module.ts deleted file mode 100644 index f053ebf797..0000000000 --- a/plugins/model-management-plugin/module.ts +++ /dev/null @@ -1 +0,0 @@ -module.exports = {}; diff --git a/plugins/model-management-plugin/tsconfig.json b/plugins/model-management-plugin/tsconfig.json deleted file mode 100644 index 3a82721e66..0000000000 --- a/plugins/model-management-plugin/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "compilerOptions": { - "target": "es2016", - "module": "esnext", - "moduleResolution": "node", - "outDir": "./dist", - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "strict": false, - "skipLibCheck": true - } -} diff --git a/plugins/model-management-plugin/README.md b/plugins/model-plugin/README.md similarity index 100% rename from plugins/model-management-plugin/README.md rename to plugins/model-plugin/README.md diff --git a/plugins/model-management-plugin/package.json b/plugins/model-plugin/package.json similarity index 87% rename from plugins/model-management-plugin/package.json rename to plugins/model-plugin/package.json index faa8495065..1ee8e5496e 100644 --- a/plugins/model-management-plugin/package.json +++ b/plugins/model-plugin/package.json @@ -1,5 +1,5 @@ { - "name": "@janhq/model-management-plugin", + "name": "@janhq/model-plugin", "version": "1.0.13", "description": "Model Management Plugin provides model exploration and seamless downloads", "icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/queue-list.svg", @@ -8,7 +8,7 @@ "author": "Jan ", "license": "AGPL-3.0", "supportCloudNative": true, - "url": "/plugins/model-management-plugin/index.js", + "url": "/plugins/model-plugin/index.js", "activationPoints": [ "init" ], @@ -29,7 +29,7 @@ "README.md" ], "dependencies": { - "@janhq/core": "^0.1.6", + "@janhq/core": "file:../../core", "ts-loader": "^9.5.0" } } diff --git a/plugins/model-management-plugin/@types/global.d.ts b/plugins/model-plugin/src/@types/global.d.ts similarity index 100% rename from plugins/model-management-plugin/@types/global.d.ts rename to plugins/model-plugin/src/@types/global.d.ts diff --git a/plugins/model-plugin/src/helpers/cloudNative.ts b/plugins/model-plugin/src/helpers/cloudNative.ts new file mode 100644 index 0000000000..90c6d3f1ef --- /dev/null +++ b/plugins/model-plugin/src/helpers/cloudNative.ts @@ -0,0 +1,48 @@ +import { EventName, events } from "@janhq/core"; + +export async function pollDownloadProgress(fileName: string) { + if ( + typeof window !== "undefined" && + typeof (window as any).electronAPI === "undefined" + ) { + const intervalId = setInterval(() => { + notifyProgress(fileName, intervalId); + }, 3000); + } +} + +export async function notifyProgress( + fileName: string, + intervalId: NodeJS.Timeout +): Promise { + const response = await fetch("/api/v1/downloadProgress", { + method: "POST", + body: JSON.stringify({ fileName: fileName }), + headers: { "Content-Type": "application/json", Authorization: "" }, + }); + + if (!response.ok) { + events.emit(EventName.OnDownloadError, null); + clearInterval(intervalId); + return; + } + const json = await response.json(); + if (isEmptyObject(json)) { + if (!fileName && intervalId) { + clearInterval(intervalId); + } + return Promise.resolve(""); + } + if (json.success === true) { + events.emit(EventName.OnDownloadSuccess, json); + clearInterval(intervalId); + return Promise.resolve(""); + } else { + events.emit(EventName.OnDownloadUpdate, json); + return Promise.resolve(json.fileName); + } +} + +function isEmptyObject(ojb: any): boolean { + return Object.keys(ojb).length === 0; +} diff --git a/plugins/model-management-plugin/helper.ts b/plugins/model-plugin/src/helpers/modelParser.ts similarity index 100% rename from plugins/model-management-plugin/helper.ts rename to plugins/model-plugin/src/helpers/modelParser.ts diff --git a/plugins/model-plugin/src/index.ts b/plugins/model-plugin/src/index.ts new file mode 100644 index 0000000000..ccfed6bfea --- /dev/null +++ b/plugins/model-plugin/src/index.ts @@ -0,0 +1,105 @@ +import { PluginType, fs, downloadFile } from "@janhq/core"; +import { ModelPlugin } from "@janhq/core/lib/plugins"; +import { Model, ModelCatalog } from "@janhq/core/lib/types"; +import { pollDownloadProgress } from "./helpers/cloudNative"; +import { parseToModel } from "./helpers/modelParser"; + +/** + * A plugin for managing machine learning models. + */ +export default class JanModelPlugin implements ModelPlugin { + /** + * Implements type from JanPlugin. + * @override + * @returns The type of the plugin. + */ + type(): PluginType { + return PluginType.Model; + } + + /** + * Called when the plugin is loaded. + * @override + */ + onLoad(): void { + /** Cloud Native + * TODO: Fetch all downloading progresses? + **/ + } + + /** + * Called when the plugin is unloaded. + * @override + */ + onUnload(): void {} + + /** + * Downloads a machine learning model. + * @param model - The model to download. + * @returns A Promise that resolves when the model is downloaded. + */ + async downloadModel(model: Model): Promise { + await fs.mkdir("models"); + downloadFile(model.downloadLink, `models/${model._id}`); + /** Cloud Native + * MARK: Poll Downloading Progress + **/ + pollDownloadProgress(model._id); + } + + /** + * Deletes a machine learning model. + * @param filePath - The path to the model file to delete. + * @returns A Promise that resolves when the model is deleted. + */ + deleteModel(filePath: string): Promise { + return fs + .deleteFile(`models/${filePath}`) + .then(() => fs.deleteFile(`models/m-${filePath}.json`)); + } + + /** + * Saves a machine learning model. + * @param model - The model to save. + * @returns A Promise that resolves when the model is saved. + */ + async saveModel(model: Model): Promise { + await fs.writeFile(`models/m-${model._id}.json`, JSON.stringify(model)); + } + + /** + * Gets all downloaded models. + * @returns A Promise that resolves with an array of all models. + */ + getDownloadedModels(): Promise { + return fs + .listFiles("models") + .then((files: string[]) => { + return Promise.all( + files + .filter((file) => /^m-.*\.json$/.test(file)) + .map(async (file) => { + const model: Model = JSON.parse( + await fs.readFile(`models/${file}`) + ); + return model; + }) + ); + }) + .catch((e) => fs.mkdir("models").then(() => [])); + } + /** + * Gets all available models. + * @returns A Promise that resolves with an array of all models. + */ + getConfiguredModels(): Promise { + // Add a timestamp to the URL to prevent caching + return import( + /* webpackIgnore: true */ MODEL_CATALOG_URL + `?t=${Date.now()}` + ).then((module) => + module.default.map((e) => { + return parseToModel(e); + }) + ); + } +} diff --git a/plugins/data-plugin/tsconfig.json b/plugins/model-plugin/tsconfig.json similarity index 74% rename from plugins/data-plugin/tsconfig.json rename to plugins/model-plugin/tsconfig.json index 3a82721e66..addd8e1274 100644 --- a/plugins/data-plugin/tsconfig.json +++ b/plugins/model-plugin/tsconfig.json @@ -7,6 +7,8 @@ "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": false, - "skipLibCheck": true - } + "skipLibCheck": true, + "rootDir": "./src" + }, + "include": ["./src"] } diff --git a/plugins/model-management-plugin/webpack.config.js b/plugins/model-plugin/webpack.config.js similarity index 91% rename from plugins/model-management-plugin/webpack.config.js rename to plugins/model-plugin/webpack.config.js index 1cef734baf..60fa1a9b0f 100644 --- a/plugins/model-management-plugin/webpack.config.js +++ b/plugins/model-plugin/webpack.config.js @@ -4,7 +4,7 @@ const packageJson = require("./package.json"); module.exports = { experiments: { outputModule: true }, - entry: "./index.ts", // Adjust the entry point to match your project's main file + entry: "./src/index.ts", // Adjust the entry point to match your project's main file mode: "production", module: { rules: [ diff --git a/plugins/monitoring-plugin/@types/global.d.ts b/plugins/monitoring-plugin/@types/global.d.ts deleted file mode 100644 index 87105f0994..0000000000 --- a/plugins/monitoring-plugin/@types/global.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -declare const PLUGIN_NAME: string; -declare const MODULE_PATH: string; diff --git a/plugins/monitoring-plugin/index.ts b/plugins/monitoring-plugin/index.ts deleted file mode 100644 index c7c7697bbf..0000000000 --- a/plugins/monitoring-plugin/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { core, SystemMonitoringService } from "@janhq/core"; - -// Provide an async method to manipulate the price provided by the extension point -const getResourcesInfo = () => core.invokePluginFunc(MODULE_PATH, "getResourcesInfo"); - -const getCurrentLoad = () => core.invokePluginFunc(MODULE_PATH, "getCurrentLoad"); - -// Register all the above functions and objects with the relevant extension points -export function init({ register }) { - register(SystemMonitoringService.GetResourcesInfo, getResourcesInfo.name, getResourcesInfo); - register(SystemMonitoringService.GetCurrentLoad, getCurrentLoad.name, getCurrentLoad); -} diff --git a/plugins/monitoring-plugin/package.json b/plugins/monitoring-plugin/package.json index e33408d685..a60c910c66 100644 --- a/plugins/monitoring-plugin/package.json +++ b/plugins/monitoring-plugin/package.json @@ -23,16 +23,16 @@ "webpack-cli": "^5.1.4" }, "dependencies": { - "@janhq/core": "^0.1.6", + "@janhq/core": "file:../../core", "systeminformation": "^5.21.8", "ts-loader": "^9.5.0" }, - "bundledDependencies": [ - "systeminformation" - ], "files": [ "dist/*", "package.json", "README.md" + ], + "bundleDependencies": [ + "systeminformation" ] } diff --git a/plugins/monitoring-plugin/src/@types/global.d.ts b/plugins/monitoring-plugin/src/@types/global.d.ts new file mode 100644 index 0000000000..3b45ccc5ad --- /dev/null +++ b/plugins/monitoring-plugin/src/@types/global.d.ts @@ -0,0 +1 @@ +declare const MODULE: string; diff --git a/plugins/monitoring-plugin/src/index.ts b/plugins/monitoring-plugin/src/index.ts new file mode 100644 index 0000000000..4b392596ce --- /dev/null +++ b/plugins/monitoring-plugin/src/index.ts @@ -0,0 +1,43 @@ +import { PluginType } from "@janhq/core"; +import { MonitoringPlugin } from "@janhq/core/lib/plugins"; +import { executeOnMain } from "@janhq/core"; + +/** + * JanMonitoringPlugin is a plugin that provides system monitoring functionality. + * It implements the MonitoringPlugin interface from the @janhq/core package. + */ +export default class JanMonitoringPlugin implements MonitoringPlugin { + /** + * Returns the type of the plugin. + * @returns The PluginType.SystemMonitoring value. + */ + type(): PluginType { + return PluginType.SystemMonitoring; + } + + /** + * Called when the plugin is loaded. + */ + onLoad(): void {} + + /** + * Called when the plugin is unloaded. + */ + onUnload(): void {} + + /** + * Returns information about the system resources. + * @returns A Promise that resolves to an object containing information about the system resources. + */ + getResourcesInfo(): Promise { + return executeOnMain(MODULE, "getResourcesInfo"); + } + + /** + * Returns information about the current system load. + * @returns A Promise that resolves to an object containing information about the current system load. + */ + getCurrentLoad(): Promise { + return executeOnMain(MODULE, "getCurrentLoad"); + } +} diff --git a/plugins/monitoring-plugin/module.ts b/plugins/monitoring-plugin/src/module.ts similarity index 100% rename from plugins/monitoring-plugin/module.ts rename to plugins/monitoring-plugin/src/module.ts diff --git a/plugins/monitoring-plugin/tsconfig.json b/plugins/monitoring-plugin/tsconfig.json index 3b321034a8..2477d58ce5 100644 --- a/plugins/monitoring-plugin/tsconfig.json +++ b/plugins/monitoring-plugin/tsconfig.json @@ -7,6 +7,8 @@ "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": false, - "skipLibCheck": true - } + "skipLibCheck": true, + "rootDir": "./src" + }, + "include": ["./src"] } diff --git a/plugins/monitoring-plugin/webpack.config.js b/plugins/monitoring-plugin/webpack.config.js index 25d51ffa0a..f54059222f 100644 --- a/plugins/monitoring-plugin/webpack.config.js +++ b/plugins/monitoring-plugin/webpack.config.js @@ -4,7 +4,7 @@ const packageJson = require("./package.json"); module.exports = { experiments: { outputModule: true }, - entry: "./index.ts", // Adjust the entry point to match your project's main file + entry: "./src/index.ts", // Adjust the entry point to match your project's main file mode: "production", module: { rules: [ @@ -22,8 +22,7 @@ module.exports = { }, plugins: [ new webpack.DefinePlugin({ - PLUGIN_NAME: JSON.stringify(packageJson.name), - MODULE_PATH: JSON.stringify(`${packageJson.name}/${packageJson.module}`), + MODULE: JSON.stringify(`${packageJson.name}/${packageJson.module}`), }), ], resolve: { diff --git a/plugins/openai-plugin/@types/global.d.ts b/plugins/openai-plugin/@types/global.d.ts deleted file mode 100644 index 87105f0994..0000000000 --- a/plugins/openai-plugin/@types/global.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -declare const PLUGIN_NAME: string; -declare const MODULE_PATH: string; diff --git a/plugins/openai-plugin/index.ts b/plugins/openai-plugin/index.ts deleted file mode 100644 index 040d3d08a9..0000000000 --- a/plugins/openai-plugin/index.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { - PluginService, - EventName, - NewMessageRequest, - events, - store, - preferences, - RegisterExtensionPoint, -} from "@janhq/core"; -import { Configuration, OpenAIApi } from "azure-openai"; - -const setRequestHeader = XMLHttpRequest.prototype.setRequestHeader; -XMLHttpRequest.prototype.setRequestHeader = function newSetRequestHeader(key: string, val: string) { - if (key.toLocaleLowerCase() === "user-agent") { - return; - } - setRequestHeader.apply(this, [key, val]); -}; - -var openai: OpenAIApi | undefined = undefined; - -const setup = async () => { - const apiKey: string = (await preferences.get(PLUGIN_NAME, "apiKey")) ?? ""; - const endpoint: string = (await preferences.get(PLUGIN_NAME, "endpoint")) ?? ""; - const deploymentName: string = (await preferences.get(PLUGIN_NAME, "deploymentName")) ?? ""; - try { - openai = new OpenAIApi( - new Configuration({ - azure: { - apiKey, //Your API key goes here - endpoint, //Your endpoint goes here. It is like: "https://endpointname.openai.azure.com/" - deploymentName, //Your deployment name goes here. It is like "chatgpt" - }, - }) - ); - } catch (err) { - openai = undefined; - console.log(err); - } -}; - -async function onStart() { - setup(); - registerListener(); -} - -async function handleMessageRequest(data: NewMessageRequest) { - if (!openai) { - const message = { - ...data, - message: "Your API key is not set. Please set it in the plugin preferences.", - user: "GPT-3", - avatar: "https://static-assets.jan.ai/openai-icon.jpg", - createdAt: new Date().toISOString(), - _id: undefined, - }; - const id = await store.insertOne("messages", message); - message._id = id; - events.emit(EventName.OnNewMessageResponse, message); - return; - } - - const message = { - ...data, - message: "", - user: "GPT-3", - avatar: "https://static-assets.jan.ai/openai-icon.jpg", - createdAt: new Date().toISOString(), - _id: undefined, - }; - const id = await store.insertOne("messages", message); - - message._id = id; - events.emit(EventName.OnNewMessageResponse, message); - const response = await openai.createChatCompletion({ - messages: [{ role: "user", content: data.message }], - model: "gpt-3.5-turbo", - }); - message.message = response.data.choices[0].message.content; - events.emit(EventName.OnMessageResponseUpdate, message); - await store.updateOne("messages", message._id, message); -} - -const registerListener = () => { - events.on(EventName.OnNewMessageRequest, handleMessageRequest); -}; - -// Preference update - reconfigure OpenAI -const onPreferencesUpdate = () => { - setup(); -}; -// Register all the above functions and objects with the relevant extension points -export function init({ register }: { register: RegisterExtensionPoint }) { - register(PluginService.OnStart, PLUGIN_NAME, onStart); - register(PluginService.OnPreferencesUpdate, PLUGIN_NAME, onPreferencesUpdate); - - preferences.registerPreferences(register, PLUGIN_NAME, "apiKey", "API Key", "Azure Project API Key", ""); - preferences.registerPreferences( - register, - PLUGIN_NAME, - "endpoint", - "API Endpoint", - "Azure Deployment Endpoint API", - "" - ); - preferences.registerPreferences( - register, - PLUGIN_NAME, - "deploymentName", - "Deployment Name", - "The deployment name you chose when you deployed the model", - "" - ); -} diff --git a/plugins/retrieval-plugin/@types/global.d.ts b/plugins/retrieval-plugin/@types/global.d.ts deleted file mode 100644 index 87105f0994..0000000000 --- a/plugins/retrieval-plugin/@types/global.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -declare const PLUGIN_NAME: string; -declare const MODULE_PATH: string; diff --git a/plugins/retrieval-plugin/README.md b/plugins/retrieval-plugin/README.md deleted file mode 100644 index 5f43031598..0000000000 --- a/plugins/retrieval-plugin/README.md +++ /dev/null @@ -1,75 +0,0 @@ -# Create a Jan Plugin using Typescript - -Use this template to bootstrap the creation of a TypeScript Jan plugin. 🚀 - -## Create Your Own Plugin - -To create your own plugin, you can use this repository as a template! Just follow the below instructions: - -1. Click the Use this template button at the top of the repository -2. Select Create a new repository -3. Select an owner and name for your new repository -4. Click Create repository -5. Clone your new repository - -## Initial Setup - -After you've cloned the repository to your local machine or codespace, you'll need to perform some initial setup steps before you can develop your plugin. - -> [!NOTE] -> -> You'll need to have a reasonably modern version of -> [Node.js](https://nodejs.org) handy. If you are using a version manager like -> [`nodenv`](https://github.com/nodenv/nodenv) or -> [`nvm`](https://github.com/nvm-sh/nvm), you can run `nodenv install` in the -> root of your repository to install the version specified in -> [`package.json`](./package.json). Otherwise, 20.x or later should work! - -1. :hammer_and_wrench: Install the dependencies - - ```bash - npm install - ``` - -1. :building_construction: Package the TypeScript for distribution - - ```bash - npm run bundle - ``` - -1. :white_check_mark: Check your artifact - - There will be a tgz file in your plugin directory now - -## Update the Plugin Metadata - -The [`package.json`](package.json) file defines metadata about your plugin, such as -plugin name, main entry, description and version. - -When you copy this repository, update `package.json` with the name, description for your plugin. - -## Update the Plugin Code - -The [`src/`](./src/) directory is the heart of your plugin! This contains the -source code that will be run when your plugin extension functions are invoked. You can replace the -contents of this directory with your own code. - -There are a few things to keep in mind when writing your plugin code: - -- Most Jan Plugin Extension functions are processed asynchronously. - In `index.ts`, you will see that the extension function will return a `Promise`. - - ```typescript - import { core } from "@janhq/core"; - - function onStart(): Promise { - return core.invokePluginFunc(MODULE_PATH, "run", 0); - } - ``` - - For more information about the Jan Plugin Core module, see the - [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/plugins/retrieval-plugin/package.json b/plugins/retrieval-plugin/package.json deleted file mode 100644 index b58ea3e110..0000000000 --- a/plugins/retrieval-plugin/package.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "name": "retrieval-plugin", - "version": "1.0.3", - "description": "Retrieval plugin for Jan app (experimental)", - "icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/circle-stack.svg", - "main": "dist/index.js", - "module": "dist/module.js", - "requiredVersion": "^0.3.1", - "author": "Jan ", - "license": "MIT", - "activationPoints": [ - "init" - ], - "scripts": { - "build": "tsc -b . && webpack --config webpack.config.js", - "bundle": "npm pack" - }, - "devDependencies": { - "webpack": "^5.88.2", - "webpack-cli": "^5.1.4" - }, - "dependencies": { - "@janhq/core": "^0.1.1", - "faiss-node": "^0.5.1", - "install": "^0.13.0", - "langchain": "^0.0.169", - "npm": "^10.2.0", - "pdf-parse": "^1.1.1", - "ts-loader": "^9.5.0" - }, - "engines": { - "node": ">=18.0.0" - }, - "files": [ - "dist/*", - "package.json", - "README.md" - ], - "bundleDependencies": [ - "pdf-parse", - "langchain", - "faiss-node" - ] -} diff --git a/plugins/retrieval-plugin/src/index.ts b/plugins/retrieval-plugin/src/index.ts deleted file mode 100644 index ca8b9daae9..0000000000 --- a/plugins/retrieval-plugin/src/index.ts +++ /dev/null @@ -1,172 +0,0 @@ -/** - * The entrypoint for the plugin. - */ - -import { - EventName, - NewMessageRequest, - PluginService, - RegisterExtensionPoint, - invokePluginFunc, - events, - preferences, - store, -} from "@janhq/core"; - -/** - * Register event listener. - */ -const registerListener = () => { - events.on(EventName.OnNewMessageRequest, inferenceRequest); -}; - -/** - * Invokes the `ingest` function from the `module.js` file using the `invokePluginFunc` method. - * "ingest" is the name of the function to invoke. - * @returns {Promise} A promise that resolves with the result of the `run` function. - */ -function onStart(): Promise { - registerListener(); - ingest(); - return Promise.resolve(); -} - -/** - * Retrieves the document ingestion directory path from the `preferences` module and invokes the `ingest` function - * from the specified module with the directory path and additional options. - * The additional options are retrieved from the `preferences` module using the `PLUGIN_NAME` constant. - */ -async function ingest() { - const path = await preferences.get(PLUGIN_NAME, "ingestDocumentDirectoryPath"); - - // TODO: Hiro - Add support for custom embeddings - const customizedEmbedding = undefined; - - if (path && path.length > 0) { - const openAPIKey = await preferences.get(PLUGIN_NAME, "openAIApiKey"); - const azureOpenAIBasePath = await preferences.get(PLUGIN_NAME, "azureOpenAIBasePath"); - const azureOpenAIApiInstanceName = await preferences.get(PLUGIN_NAME, "azureOpenAIApiInstanceName"); - invokePluginFunc(MODULE_PATH, "ingest", path, customizedEmbedding, { - openAIApiKey: openAPIKey?.length > 0 ? openAPIKey : undefined, - azureOpenAIApiKey: await preferences.get(PLUGIN_NAME, "azureOpenAIApiKey"), - azureOpenAIApiVersion: await preferences.get(PLUGIN_NAME, "azureOpenAIApiVersion"), - azureOpenAIApiInstanceName: azureOpenAIApiInstanceName?.length > 0 ? azureOpenAIApiInstanceName : undefined, - azureOpenAIApiDeploymentName: await preferences.get(PLUGIN_NAME, "azureOpenAIApiDeploymentNameRag"), - azureOpenAIBasePath: azureOpenAIBasePath?.length > 0 ? azureOpenAIBasePath : undefined, - }); - } -} - -/** - * Retrieves the document ingestion directory path from the `preferences` module and invokes the `ingest` function - * from the specified module with the directory path and additional options. - * The additional options are retrieved from the `preferences` module using the `PLUGIN_NAME` constant. - */ -async function inferenceRequest(data: NewMessageRequest): Promise { - // TODO: Hiro - Add support for custom embeddings - const customLLM = undefined; - const message = { - ...data, - message: "", - user: "RAG", - createdAt: new Date().toISOString(), - _id: undefined, - }; - const id = await store.insertOne("messages", message); - message._id = id; - events.emit(EventName.OnNewMessageResponse, message); - - const openAPIKey = await preferences.get(PLUGIN_NAME, "openAIApiKey"); - const azureOpenAIBasePath = await preferences.get(PLUGIN_NAME, "azureOpenAIBasePath"); - const azureOpenAIApiInstanceName = await preferences.get(PLUGIN_NAME, "azureOpenAIApiInstanceName"); - invokePluginFunc(MODULE_PATH, "chatWithDocs", data.message, customLLM, { - openAIApiKey: openAPIKey?.length > 0 ? openAPIKey : undefined, - azureOpenAIApiKey: await preferences.get(PLUGIN_NAME, "azureOpenAIApiKey"), - azureOpenAIApiVersion: await preferences.get(PLUGIN_NAME, "azureOpenAIApiVersion"), - azureOpenAIApiInstanceName: azureOpenAIApiInstanceName?.length > 0 ? azureOpenAIApiInstanceName : undefined, - azureOpenAIApiDeploymentName: await preferences.get(PLUGIN_NAME, "azureOpenAIApiDeploymentNameChat"), - azureOpenAIBasePath: azureOpenAIBasePath?.length > 0 ? azureOpenAIBasePath : undefined, - modelName: "gpt-3.5-turbo-16k", - temperature: 0.2, - }).then(async (text) => { - console.log("RAG Response:", text); - message.message = text; - - events.emit(EventName.OnMessageResponseUpdate, message); - }); -} -/** - * Initializes the plugin by registering the extension functions with the given register function. - * @param {Function} options.register - The function to use for registering the extension functions - */ -export function init({ register }: { register: RegisterExtensionPoint }) { - register(PluginService.OnStart, PLUGIN_NAME, onStart); - register(PluginService.OnPreferencesUpdate, PLUGIN_NAME, ingest); - - preferences.registerPreferences( - register, - PLUGIN_NAME, - "ingestDocumentDirectoryPath", - "Document Ingest Directory Path", - "The URL of the directory containing the documents to ingest", - undefined - ); - - preferences.registerPreferences( - register, - PLUGIN_NAME, - "openAIApiKey", - "Open API Key", - "OpenAI API Key", - undefined - ); - - preferences.registerPreferences( - register, - PLUGIN_NAME, - "azureOpenAIApiKey", - "Azure API Key", - "Azure Project API Key", - undefined - ); - preferences.registerPreferences( - register, - PLUGIN_NAME, - "azureOpenAIApiVersion", - "Azure API Version", - "Azure Project API Version", - undefined - ); - preferences.registerPreferences( - register, - PLUGIN_NAME, - "azureOpenAIApiInstanceName", - "Azure Instance Name", - "Azure Project Instance Name", - undefined - ); - preferences.registerPreferences( - register, - PLUGIN_NAME, - "azureOpenAIApiDeploymentNameChat", - "Azure Chat Model Deployment Name", - "Azure Project Chat Model Deployment Name (e.g. gpt-3.5-turbo-16k)", - undefined - ); - preferences.registerPreferences( - register, - PLUGIN_NAME, - "azureOpenAIApiDeploymentNameRag", - "Azure Text Embedding Model Deployment Name", - "Azure Project Text Embedding Model Deployment Name (e.g. text-embedding-ada-002)", - undefined - ); - preferences.registerPreferences( - register, - PLUGIN_NAME, - "azureOpenAIBasePath", - "Azure Base Path", - "Azure Project Base Path", - undefined - ); -} diff --git a/plugins/retrieval-plugin/src/module.ts b/plugins/retrieval-plugin/src/module.ts deleted file mode 100644 index dbbd5bd0f2..0000000000 --- a/plugins/retrieval-plugin/src/module.ts +++ /dev/null @@ -1,58 +0,0 @@ -const path = require("path"); -const { app } = require("electron"); -const { DirectoryLoader } = require("langchain/document_loaders/fs/directory"); -const { OpenAIEmbeddings } = require("langchain/embeddings/openai"); -const { PDFLoader } = require("langchain/document_loaders/fs/pdf"); -const { CharacterTextSplitter } = require("langchain/text_splitter"); -const { FaissStore } = require("langchain/vectorstores/faiss"); -const { ChatOpenAI } = require("langchain/chat_models/openai"); -const { RetrievalQAChain } = require("langchain/chains"); - -var db: any | undefined = undefined; - -/** - * Ingests documents from the specified directory - * If an `embedding` object is not provided, uses OpenAIEmbeddings. - * The resulting embeddings are stored in the database using Faiss. - * @param docDir - The directory containing the documents to ingest. - * @param embedding - An optional object used to generate embeddings for the documents. - * @param config - An optional configuration object used to create a new `OpenAIEmbeddings` object. - */ -async function ingest(docDir: string, embedding?: any, config?: any) { - const loader = new DirectoryLoader(docDir, { - ".pdf": (path) => new PDFLoader(path), - }); - const docs = await loader.load(); - const textSplitter = new CharacterTextSplitter(); - const docsQA = await textSplitter.splitDocuments(docs); - const embeddings = embedding ?? new OpenAIEmbeddings({ ...config }); - db = await FaissStore.fromDocuments(await docsQA, embeddings); - console.log("Documents are ingested"); -} - -/** - * Generates an answer to a given question using the specified `llm` or a new `ChatOpenAI`. - * The function uses the `RetrievalQAChain` class to retrieve the most relevant document from the database and generate an answer. - * @param question - The question to generate an answer for. - * @param llm - An optional object used to generate the answer. - * @param config - An optional configuration object used to create a new `ChatOpenAI` object, can be ignored if llm is specified. - * @returns A Promise that resolves with the generated answer. - */ -async function chatWithDocs(question: string, llm?: any, config?: any): Promise { - const llm_question_answer = - llm ?? - new ChatOpenAI({ - temperature: 0.2, - ...config, - }); - const qa = RetrievalQAChain.fromLLM(llm_question_answer, db.asRetriever(), { - verbose: true, - }); - const answer = await qa.run(question); - return answer; -} - -module.exports = { - ingest, - chatWithDocs, -}; diff --git a/plugins/retrieval-plugin/tsconfig.json b/plugins/retrieval-plugin/tsconfig.json deleted file mode 100644 index 73b76094a4..0000000000 --- a/plugins/retrieval-plugin/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "compilerOptions": { - "target": "es2016", - "module": "ES6", - "moduleResolution": "node", - "outDir": "./dist", - "rootDir": "./src", - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "strict": false, - "skipLibCheck": true - } -} diff --git a/plugins/retrieval-plugin/webpack.config.js b/plugins/retrieval-plugin/webpack.config.js deleted file mode 100644 index 0f78724bb6..0000000000 --- a/plugins/retrieval-plugin/webpack.config.js +++ /dev/null @@ -1,35 +0,0 @@ -const path = require("path"); -const webpack = require("webpack"); -const packageJson = require("./package.json"); - -module.exports = { - experiments: { outputModule: true }, - entry: "./src/index.ts", - mode: "production", - module: { - rules: [ - { - test: /\.tsx?$/, - use: "ts-loader", - exclude: /node_modules/, - }, - ], - }, - plugins: [ - new webpack.DefinePlugin({ - PLUGIN_NAME: JSON.stringify(packageJson.name), - MODULE_PATH: JSON.stringify(`${packageJson.name}/${packageJson.module}`), - }), - ], - output: { - filename: "index.js", - path: path.resolve(__dirname, "dist"), - library: { type: "module" }, - }, - resolve: { - extensions: [".ts", ".js"], - }, - optimization: { - minimize: false, - }, -}; diff --git a/web/app/_components/ActiveModelTable/index.tsx b/web/app/_components/ActiveModelTable/index.tsx index 3a477ba5c4..1fa6c0bd53 100644 --- a/web/app/_components/ActiveModelTable/index.tsx +++ b/web/app/_components/ActiveModelTable/index.tsx @@ -1,10 +1,10 @@ import { useAtomValue } from 'jotai' import React from 'react' import ModelTable from '../ModelTable' -import { activeAssistantModelAtom } from '@helpers/atoms/Model.atom' +import { activeModelAtom } from '@helpers/atoms/Model.atom' const ActiveModelTable: React.FC = () => { - const activeModel = useAtomValue(activeAssistantModelAtom) + const activeModel = useAtomValue(activeModelAtom) if (!activeModel) return null diff --git a/web/app/_components/AvailableModelCard/index.tsx b/web/app/_components/AvailableModelCard/index.tsx index 9773dfb859..792b712a0f 100644 --- a/web/app/_components/AvailableModelCard/index.tsx +++ b/web/app/_components/AvailableModelCard/index.tsx @@ -3,12 +3,13 @@ import ModelDownloadButton from '../ModelDownloadButton' import ModelDownloadingButton from '../ModelDownloadingButton' import { useAtomValue } from 'jotai' import { modelDownloadStateAtom } from '@helpers/atoms/DownloadState.atom' +import { Model } from '@janhq/core/lib/types' type Props = { - model: AssistantModel + model: Model isRecommend: boolean required?: string - onDownloadClick?: (model: AssistantModel) => void + onDownloadClick?: (model: Model) => void } const AvailableModelCard: React.FC = ({ @@ -53,7 +54,7 @@ const AvailableModelCard: React.FC = ({ description={model.shortDescription} isRecommend={isRecommend} name={model.name} - type={model.type} + type={'LLM'} /> {downloadButton} diff --git a/web/app/_components/ChatBody/index.tsx b/web/app/_components/ChatBody/index.tsx index 3d759d388b..78ec7341e4 100644 --- a/web/app/_components/ChatBody/index.tsx +++ b/web/app/_components/ChatBody/index.tsx @@ -2,10 +2,11 @@ import React from 'react' import ChatItem from '../ChatItem' -import useChatMessages from '@hooks/useChatMessages' +import { useAtomValue } from 'jotai' +import { getCurrentChatMessagesAtom } from '@helpers/atoms/ChatMessage.atom' const ChatBody: React.FC = () => { - const { messages } = useChatMessages() + const messages = useAtomValue(getCurrentChatMessagesAtom) return (
diff --git a/web/app/_components/ConversationalCard/index.tsx b/web/app/_components/ConversationalCard/index.tsx index 9cc550accf..6f0360caf2 100644 --- a/web/app/_components/ConversationalCard/index.tsx +++ b/web/app/_components/ConversationalCard/index.tsx @@ -2,9 +2,10 @@ import React from 'react' import Image from 'next/image' import useCreateConversation from '@hooks/useCreateConversation' import { PlayIcon } from '@heroicons/react/24/outline' +import { Model } from '@janhq/core/lib/types' type Props = { - model: AssistantModel + model: Model } const ConversationalCard: React.FC = ({ model }) => { diff --git a/web/app/_components/ConversationalList/index.tsx b/web/app/_components/ConversationalList/index.tsx index 827874923b..70ded146ae 100644 --- a/web/app/_components/ConversationalList/index.tsx +++ b/web/app/_components/ConversationalList/index.tsx @@ -1,8 +1,9 @@ +import { Model } from '@janhq/core/lib/types' import ConversationalCard from '../ConversationalCard' import { ChatBubbleBottomCenterTextIcon } from '@heroicons/react/24/outline' type Props = { - models: AssistantModel[] + models: Model[] } const ConversationalList: React.FC = ({ models }) => ( diff --git a/web/app/_components/CreateBotContainer/index.tsx b/web/app/_components/CreateBotContainer/index.tsx index 2ba2946adc..d4b36de897 100644 --- a/web/app/_components/CreateBotContainer/index.tsx +++ b/web/app/_components/CreateBotContainer/index.tsx @@ -12,15 +12,11 @@ import { v4 as uuidv4 } from 'uuid' import DraggableProgressBar from '../DraggableProgressBar' import { useSetAtom } from 'jotai' import { activeBotAtom } from '@helpers/atoms/Bot.atom' -import { - rightSideBarExpandStateAtom, -} from '@helpers/atoms/SideBarExpand.atom' +import { rightSideBarExpandStateAtom } from '@helpers/atoms/SideBarExpand.atom' import { MainViewState, setMainViewStateAtom, } from '@helpers/atoms/MainView.atom' -import { DataService } from '@janhq/core' -import { executeSerial } from '@services/pluginService' const CreateBotContainer: React.FC = () => { const { downloadedModels } = useGetDownloadedModels() @@ -30,7 +26,7 @@ const CreateBotContainer: React.FC = () => { const createBot = async (bot: Bot) => { try { - await executeSerial(DataService.CreateBot, bot) + // await executeSerial(DataService.CreateBot, bot) } catch (err) { alert(err) console.error(err) diff --git a/web/app/_components/DownloadedModelCard/index.tsx b/web/app/_components/DownloadedModelCard/index.tsx index 2a0a86ebec..08c9d4f26e 100644 --- a/web/app/_components/DownloadedModelCard/index.tsx +++ b/web/app/_components/DownloadedModelCard/index.tsx @@ -1,11 +1,12 @@ +import { Model } from '@janhq/core/lib/types' import DownloadModelContent from '../DownloadModelContent' type Props = { - model: AssistantModel + model: Model isRecommend: boolean required?: string transferred?: number - onDeleteClick?: (model: AssistantModel) => void + onDeleteClick?: (model: Model) => void } const DownloadedModelCard: React.FC = ({ @@ -22,7 +23,7 @@ const DownloadedModelCard: React.FC = ({ description={model.shortDescription} isRecommend={isRecommend} name={model.name} - type={model.type} + type={'LLM'} />
diff --git a/web/app/_components/DownloadedModelTable/index.tsx b/web/app/_components/DownloadedModelTable/index.tsx index c9c2f0ad68..bc9c351183 100644 --- a/web/app/_components/DownloadedModelTable/index.tsx +++ b/web/app/_components/DownloadedModelTable/index.tsx @@ -1,5 +1,4 @@ import React from 'react' -import SearchBar from '../SearchBar' import ModelTable from '../ModelTable' import { useGetDownloadedModels } from '@hooks/useGetDownloadedModels' diff --git a/web/app/_components/ExploreModelItem/index.tsx b/web/app/_components/ExploreModelItem/index.tsx index fa4558a20b..eb98a0eaea 100644 --- a/web/app/_components/ExploreModelItem/index.tsx +++ b/web/app/_components/ExploreModelItem/index.tsx @@ -5,7 +5,7 @@ import ExploreModelItemHeader from '../ExploreModelItemHeader' import { Button } from '@uikit' import ModelVersionList from '../ModelVersionList' -import { Fragment, forwardRef, useEffect, useState } from 'react' +import { forwardRef, useEffect, useState } from 'react' import SimpleTag from '../SimpleTag' import { MiscellanousTag, @@ -18,9 +18,10 @@ import { import { displayDate } from '@utils/datetime' import useGetMostSuitableModelVersion from '@hooks/useGetMostSuitableModelVersion' import { toGigabytes } from '@utils/converter' +import { ModelCatalog } from '@janhq/core/lib/types' type Props = { - model: Product + model: ModelCatalog } const ExploreModelItem = forwardRef(({ model }, ref) => { diff --git a/web/app/_components/ExploreModelItemHeader/index.tsx b/web/app/_components/ExploreModelItemHeader/index.tsx index 81228d0daf..e8287db8d9 100644 --- a/web/app/_components/ExploreModelItemHeader/index.tsx +++ b/web/app/_components/ExploreModelItemHeader/index.tsx @@ -13,10 +13,11 @@ import { } from '@helpers/atoms/MainView.atom' import ConfirmationModal from '../ConfirmationModal' import { showingCancelDownloadModalAtom } from '@helpers/atoms/Modal.atom' +import { ModelCatalog, ModelVersion } from '@janhq/core/lib/types' type Props = { suitableModel: ModelVersion - exploreModel: Product + exploreModel: ModelCatalog } const ExploreModelItemHeader: React.FC = ({ diff --git a/web/app/_components/HistoryItem/index.tsx b/web/app/_components/HistoryItem/index.tsx index 19c95dd14b..59edf796e4 100644 --- a/web/app/_components/HistoryItem/index.tsx +++ b/web/app/_components/HistoryItem/index.tsx @@ -10,9 +10,9 @@ import { } from '@helpers/atoms/MainView.atom' import { displayDate } from '@utils/datetime' import { twMerge } from 'tailwind-merge' -import { activeAssistantModelAtom } from '@helpers/atoms/Model.atom' +import { activeModelAtom } from '@helpers/atoms/Model.atom' import useStartStopModel from '@hooks/useStartStopModel' -import useGetModelById from '@hooks/useGetModelById' +import { downloadedModelAtom } from '@helpers/atoms/DownloadedModel.atom' type Props = { conversation: Conversation @@ -29,12 +29,12 @@ const HistoryItem: React.FC = ({ }) => { const activeConvoId = useAtomValue(getActiveConvoIdAtom) const isSelected = activeConvoId === conversation._id - const activeModel = useAtomValue(activeAssistantModelAtom) + const activeModel = useAtomValue(activeModelAtom) const { startModel } = useStartStopModel() - const { getModelById } = useGetModelById() const setMainViewState = useSetAtom(setMainViewStateAtom) const setActiveConvoId = useSetAtom(setActiveConvoIdAtom) + const models = useAtomValue(downloadedModelAtom) const onClick = async () => { if (conversation.modelId == null) { @@ -42,7 +42,7 @@ const HistoryItem: React.FC = ({ return } - const model = await getModelById(conversation.modelId) + const model = models.find((e) => e._id === conversation.modelId) if (model != null) { if (activeModel == null) { // if there's no active model, we simply load conversation's model diff --git a/web/app/_components/InputToolbar/index.tsx b/web/app/_components/InputToolbar/index.tsx index 7220734553..b6607b1bca 100644 --- a/web/app/_components/InputToolbar/index.tsx +++ b/web/app/_components/InputToolbar/index.tsx @@ -7,7 +7,7 @@ import { useAtomValue, useSetAtom } from 'jotai' import SecondaryButton from '../SecondaryButton' import { PlusIcon } from '@heroicons/react/24/outline' import useCreateConversation from '@hooks/useCreateConversation' -import { activeAssistantModelAtom, stateModel } from '@helpers/atoms/Model.atom' +import { activeModelAtom, stateModel } from '@helpers/atoms/Model.atom' import { currentConvoStateAtom, getActiveConvoIdAtom, @@ -18,7 +18,7 @@ import { userConversationsAtom } from '@helpers/atoms/Conversation.atom' import { showingModalNoActiveModel } from '@helpers/atoms/Modal.atom' const InputToolbar: React.FC = () => { - const activeModel = useAtomValue(activeAssistantModelAtom) + const activeModel = useAtomValue(activeModelAtom) const currentConvoState = useAtomValue(currentConvoStateAtom) const { inputState, currentConvo } = useGetInputState() const { requestCreateConvo } = useCreateConversation() diff --git a/web/app/_components/LeftHeaderAction/index.tsx b/web/app/_components/LeftHeaderAction/index.tsx index f24f411227..e0648e5821 100644 --- a/web/app/_components/LeftHeaderAction/index.tsx +++ b/web/app/_components/LeftHeaderAction/index.tsx @@ -11,7 +11,7 @@ import { MagnifyingGlassIcon, PlusIcon } from '@heroicons/react/24/outline' import useCreateConversation from '@hooks/useCreateConversation' import { useGetDownloadedModels } from '@hooks/useGetDownloadedModels' import { Button } from '@uikit' -import { activeAssistantModelAtom } from '@helpers/atoms/Model.atom' +import { activeModelAtom } from '@helpers/atoms/Model.atom' import { showingModalNoActiveModel } from '@helpers/atoms/Modal.atom' import { FeatureToggleContext, @@ -20,7 +20,7 @@ import { const LeftHeaderAction: React.FC = () => { const setMainView = useSetAtom(setMainViewStateAtom) const { downloadedModels } = useGetDownloadedModels() - const activeModel = useAtomValue(activeAssistantModelAtom) + const activeModel = useAtomValue(activeModelAtom) const { requestCreateConvo } = useCreateConversation() const setShowModalNoActiveModel = useSetAtom(showingModalNoActiveModel) const { experimentalFeatureEnabed } = useContext(FeatureToggleContext) diff --git a/web/app/_components/ModelRow/index.tsx b/web/app/_components/ModelRow/index.tsx index fd7479982f..371709e2ff 100644 --- a/web/app/_components/ModelRow/index.tsx +++ b/web/app/_components/ModelRow/index.tsx @@ -4,16 +4,17 @@ import { useAtomValue } from 'jotai' import ModelActionButton, { ModelActionType } from '../ModelActionButton' import useStartStopModel from '@hooks/useStartStopModel' import useDeleteModel from '@hooks/useDeleteModel' -import { activeAssistantModelAtom, stateModel } from '@helpers/atoms/Model.atom' +import { activeModelAtom, stateModel } from '@helpers/atoms/Model.atom' import { toGigabytes } from '@utils/converter' +import { Model } from '@janhq/core/lib/types' type Props = { - model: AssistantModel + model: Model } const ModelRow: React.FC = ({ model }) => { const { startModel, stopModel } = useStartStopModel() - const activeModel = useAtomValue(activeAssistantModelAtom) + const activeModel = useAtomValue(activeModelAtom) const { deleteModel } = useDeleteModel() const { loading, model: currentModelState } = useAtomValue(stateModel) diff --git a/web/app/_components/ModelSelector/index.tsx b/web/app/_components/ModelSelector/index.tsx index b3864d2381..7c3f75d4e1 100644 --- a/web/app/_components/ModelSelector/index.tsx +++ b/web/app/_components/ModelSelector/index.tsx @@ -4,6 +4,7 @@ import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/20/solid' import { useAtom, useAtomValue } from 'jotai' import { selectedModelAtom } from '@helpers/atoms/Model.atom' import { downloadedModelAtom } from '@helpers/atoms/DownloadedModel.atom' +import { Model } from '@janhq/core/lib/types' function classNames(...classes: any) { return classes.filter(Boolean).join(' ') @@ -19,7 +20,7 @@ const SelectModels: React.FC = () => { } }, [downloadedModels]) - const onModelSelected = (model: AssistantModel) => { + const onModelSelected = (model: Model) => { setSelectedModel(model) } diff --git a/web/app/_components/ModelTable/index.tsx b/web/app/_components/ModelTable/index.tsx index 0f3646509a..eb6d891b4b 100644 --- a/web/app/_components/ModelTable/index.tsx +++ b/web/app/_components/ModelTable/index.tsx @@ -1,9 +1,10 @@ import React from 'react' import ModelRow from '../ModelRow' import ModelTableHeader from '../ModelTableHeader' +import { Model } from '@janhq/core/lib/types' type Props = { - models: AssistantModel[] + models: Model[] } const tableHeaders = ['MODEL', 'FORMAT', 'SIZE', 'STATUS', 'ACTIONS'] diff --git a/web/app/_components/ModelVersionItem/index.tsx b/web/app/_components/ModelVersionItem/index.tsx index 788c48b39b..9a431eecdc 100644 --- a/web/app/_components/ModelVersionItem/index.tsx +++ b/web/app/_components/ModelVersionItem/index.tsx @@ -1,15 +1,15 @@ import React, { useMemo } from 'react' import { formatDownloadPercentage, toGigabytes } from '@utils/converter' -import Image from 'next/image' import useDownloadModel from '@hooks/useDownloadModel' import { modelDownloadStateAtom } from '@helpers/atoms/DownloadState.atom' import { atom, useAtomValue } from 'jotai' import { useGetDownloadedModels } from '@hooks/useGetDownloadedModels' import SimpleTag from '../SimpleTag' import { RamRequired, UsecaseTag } from '../SimpleTag/TagType' +import { ModelCatalog, ModelVersion } from '@janhq/core/lib/types' type Props = { - model: Product + model: ModelCatalog modelVersion: ModelVersion isRecommended: boolean } diff --git a/web/app/_components/ModelVersionList/index.tsx b/web/app/_components/ModelVersionList/index.tsx index 133f694091..8364cd0883 100644 --- a/web/app/_components/ModelVersionList/index.tsx +++ b/web/app/_components/ModelVersionList/index.tsx @@ -1,8 +1,9 @@ import React from 'react' import ModelVersionItem from '../ModelVersionItem' +import { ModelCatalog, ModelVersion } from '@janhq/core/lib/types' type Props = { - model: Product + model: ModelCatalog versions: ModelVersion[] recommendedVersion: string } diff --git a/web/app/_components/MonitorBar/index.tsx b/web/app/_components/MonitorBar/index.tsx index e0302cc36f..6dc9d58a74 100644 --- a/web/app/_components/MonitorBar/index.tsx +++ b/web/app/_components/MonitorBar/index.tsx @@ -6,11 +6,11 @@ import useGetAppVersion from '@hooks/useGetAppVersion' import useGetSystemResources from '@hooks/useGetSystemResources' import { modelDownloadStateAtom } from '@helpers/atoms/DownloadState.atom' import { formatDownloadPercentage } from '@utils/converter' -import { activeAssistantModelAtom } from '@helpers/atoms/Model.atom' +import { activeModelAtom } from '@helpers/atoms/Model.atom' const MonitorBar: React.FC = () => { const progress = useAtomValue(appDownloadProgress) - const activeModel = useAtomValue(activeAssistantModelAtom) + const activeModel = useAtomValue(activeModelAtom) const { version } = useGetAppVersion() const { ram, cpu } = useGetSystemResources() const modelDownloadStates = useAtomValue(modelDownloadStateAtom) diff --git a/web/app/_components/Preferences.tsx b/web/app/_components/Preferences.tsx index 5cb10fc775..7cb326f216 100644 --- a/web/app/_components/Preferences.tsx +++ b/web/app/_components/Preferences.tsx @@ -1,6 +1,5 @@ 'use client' -import { useEffect, useRef, useState } from 'react' -import { plugins, extensionPoints } from '@plugin' +import { useContext, useEffect, useRef, useState } from 'react' import { ChartPieIcon, CommandLineIcon, @@ -9,10 +8,9 @@ import { import { MagnifyingGlassIcon } from '@heroicons/react/20/solid' import classNames from 'classnames' -import { DataService, PluginService, preferences } from '@janhq/core' -import { execute } from '@plugin/extension-manager' import LoadingIndicator from './LoadingIndicator' -import { executeSerial } from '@services/pluginService' +import { FeatureToggleContext } from '@helpers/FeatureToggleWrapper' +import { pluginManager } from '@plugin/PluginManager' export const Preferences = () => { const [search, setSearch] = useState('') @@ -25,14 +23,22 @@ export const Preferences = () => { const [isLoading, setIsLoading] = useState(false) const experimentRef = useRef(null) const preferenceRef = useRef(null) - + const { experimentalFeatureEnabed } = useContext(FeatureToggleContext) /** * Loads the plugin catalog module from a CDN and sets it as the plugin catalog state. */ useEffect(() => { - executeSerial(DataService.GetPluginManifest).then((data: any) => { - setPluginCatalog(data) - }) + if (!window.electronAPI) { + return + } + + // Get plugin manifest + import(/* webpackIgnore: true */ PLUGIN_CATALOG + `?t=${Date.now()}`).then( + (data) => { + if (Array.isArray(data.default) && experimentalFeatureEnabed) + setPluginCatalog(data.default) + } + ) }, []) /** @@ -44,39 +50,8 @@ export const Preferences = () => { */ useEffect(() => { const getActivePlugins = async () => { - const plgs = await plugins.getActive() + const plgs = await pluginManager.getActive() setActivePlugins(plgs) - - if (extensionPoints.get('experimentComponent')) { - const components = await Promise.all( - extensionPoints.execute('experimentComponent', {}) - ) - if (components.length > 0) { - setIsTestAvailable(true) - } - components.forEach((e) => { - if (experimentRef.current) { - // @ts-ignore - experimentRef.current.appendChild(e) - } - }) - } - - if (extensionPoints.get('PluginPreferences')) { - const data = await Promise.all( - extensionPoints.execute('PluginPreferences', {}) - ) - setPreferenceItems(Array.isArray(data) ? data : []) - Promise.all( - (Array.isArray(data) ? data : []).map((e) => - preferences - .get(e.pluginName, e.preferenceKey) - .then((k) => ({ key: e.preferenceKey, value: k })) - ) - ).then((data) => { - setPreferenceValues(data) - }) - } } getActivePlugins() }, []) @@ -93,7 +68,7 @@ export const Preferences = () => { // Send the filename of the to be installed plugin // to the main process for installation - const installed = await plugins.install([pluginFile]) + const installed = await pluginManager.install([pluginFile]) if (installed) window.coreAPI?.relaunch() } @@ -105,7 +80,7 @@ export const Preferences = () => { const uninstall = async (name: string) => { // Send the filename of the to be uninstalled plugin // to the main process for removal - const res = await plugins.uninstall([name]) + const res = await pluginManager.uninstall([name]) if (res) window.coreAPI?.relaunch() } @@ -131,7 +106,7 @@ export const Preferences = () => { const downloadTarball = async (pluginName: string) => { setIsLoading(true) const pluginPath = await window.coreAPI?.installRemotePlugin(pluginName) - const installed = await plugins.install([pluginPath]) + const installed = await pluginManager.install([pluginPath]) setIsLoading(false) if (installed) window.coreAPI.relaunch() } @@ -144,11 +119,6 @@ export const Preferences = () => { if (timeout) { clearTimeout(timeout) } - if (extensionPoints.get(PluginService.OnPreferencesUpdate)) - timeout = setTimeout( - () => execute(PluginService.OnPreferencesUpdate, {}), - 100 - ) } /** @@ -408,11 +378,7 @@ export const Preferences = () => { (v) => v.key === e.preferenceKey )[0]?.value } - onChange={(event) => { - preferences - .set(e.pluginName, e.preferenceKey, event.target.value) - .then(() => notifyPreferenceUpdate()) - }} + onChange={(event) => {}} >
diff --git a/web/app/_components/SidebarEmptyHistory/index.tsx b/web/app/_components/SidebarEmptyHistory/index.tsx index 1cb4e3a6c2..ae688b2f59 100644 --- a/web/app/_components/SidebarEmptyHistory/index.tsx +++ b/web/app/_components/SidebarEmptyHistory/index.tsx @@ -5,7 +5,7 @@ import { MainViewState, setMainViewStateAtom, } from '@helpers/atoms/MainView.atom' -import { activeAssistantModelAtom } from '@helpers/atoms/Model.atom' +import { activeModelAtom } from '@helpers/atoms/Model.atom' import { useGetDownloadedModels } from '@hooks/useGetDownloadedModels' import { Button } from '@uikit' import { MessageCircle } from 'lucide-react' @@ -18,7 +18,7 @@ enum ActionButton { const SidebarEmptyHistory: React.FC = () => { const { downloadedModels } = useGetDownloadedModels() - const activeModel = useAtomValue(activeAssistantModelAtom) + const activeModel = useAtomValue(activeModelAtom) const setMainView = useSetAtom(setMainViewStateAtom) const { requestCreateConvo } = useCreateConversation() const [action, setAction] = useState(ActionButton.DownloadModel) diff --git a/web/app/_components/SwitchingModelConfirmationModal/index.tsx b/web/app/_components/SwitchingModelConfirmationModal/index.tsx index 9c0ca91759..b75b19b4a2 100644 --- a/web/app/_components/SwitchingModelConfirmationModal/index.tsx +++ b/web/app/_components/SwitchingModelConfirmationModal/index.tsx @@ -3,16 +3,17 @@ import { Dialog, Transition } from '@headlessui/react' import { ExclamationTriangleIcon, XMarkIcon } from '@heroicons/react/24/outline' import { switchingModelConfirmationModalPropsAtom } from '@helpers/atoms/Modal.atom' import { useAtom, useAtomValue } from 'jotai' -import { activeAssistantModelAtom } from '@helpers/atoms/Model.atom' +import { activeModelAtom } from '@helpers/atoms/Model.atom' import useStartStopModel from '@hooks/useStartStopModel' +import { Model } from '@janhq/core/lib/types' export type SwitchingModelConfirmationModalProps = { - replacingModel: AssistantModel + replacingModel: Model } const SwitchingModelConfirmationModal: React.FC = () => { const [props, setProps] = useAtom(switchingModelConfirmationModalPropsAtom) - const activeModel = useAtomValue(activeAssistantModelAtom) + const activeModel = useAtomValue(activeModelAtom) const { startModel } = useStartStopModel() const onConfirmSwitchModelClick = () => { diff --git a/web/containers/BottomBar/index.tsx b/web/containers/BottomBar/index.tsx index fedddcfdbe..622c5a5a3f 100644 --- a/web/containers/BottomBar/index.tsx +++ b/web/containers/BottomBar/index.tsx @@ -4,13 +4,13 @@ import useGetSystemResources from '@hooks/useGetSystemResources' import { useAtomValue } from 'jotai' import { modelDownloadStateAtom } from '@helpers/atoms/DownloadState.atom' import { formatDownloadPercentage } from '@utils/converter' -import { activeAssistantModelAtom, stateModel } from '@helpers/atoms/Model.atom' +import { activeModelAtom, stateModel } from '@helpers/atoms/Model.atom' import useGetAppVersion from '@hooks/useGetAppVersion' import ProgressBar from '@/_components/ProgressBar' import { appDownloadProgress } from '@helpers/JotaiWrapper' const BottomBar = () => { - const activeModel = useAtomValue(activeAssistantModelAtom) + const activeModel = useAtomValue(activeModelAtom) const stateModelStartStop = useAtomValue(stateModel) const { ram, cpu } = useGetSystemResources() const modelDownloadStates = useAtomValue(modelDownloadStateAtom) diff --git a/web/containers/Providers/index.tsx b/web/containers/Providers/index.tsx index 897e5370b3..d6a2540a7a 100644 --- a/web/containers/Providers/index.tsx +++ b/web/containers/Providers/index.tsx @@ -1,21 +1,19 @@ 'use client' import { PropsWithChildren } from 'react' -import { PluginService } from '@janhq/core' import { ThemeWrapper } from '@helpers/ThemeWrapper' import JotaiWrapper from '@helpers/JotaiWrapper' import { ModalWrapper } from '@helpers/ModalWrapper' import { useEffect, useState } from 'react' import CompactLogo from '@containers/Logo/CompactLogo' -import { setup, plugins, activationPoints, extensionPoints } from '@plugin' import EventListenerWrapper from '@helpers/EventListenerWrapper' import { setupCoreServices } from '@services/coreService' import { - executeSerial, isCorePluginInstalled, setupBasePlugins, } from '@services/pluginService' import { FeatureToggleWrapper } from '@helpers/FeatureToggleWrapper' +import { pluginManager } from '../../plugin/PluginManager' const Providers = (props: PropsWithChildren) => { const [setupCore, setSetupCore] = useState(false) @@ -24,26 +22,16 @@ const Providers = (props: PropsWithChildren) => { const { children } = props async function setupPE() { - // Enable activation point management - setup({ - importer: (plugin: string) => - import(/* webpackIgnore: true */ plugin).catch((err) => { - console.log(err) - }), - }) - // Register all active plugins with their activation points - await plugins.registerActive() + await pluginManager.registerActive() + setTimeout(async () => { - // Trigger activation points - await activationPoints.trigger('init') if (!isCorePluginInstalled()) { setupBasePlugins() return } - if (extensionPoints.get(PluginService.OnStart)) { - await executeSerial(PluginService.OnStart) - } + + pluginManager.load() setActivated(true) }, 500) } diff --git a/web/helpers/EventHandler.tsx b/web/helpers/EventHandler.tsx index 7c8fc1c3f9..2316f64076 100644 --- a/web/helpers/EventHandler.tsx +++ b/web/helpers/EventHandler.tsx @@ -1,50 +1,59 @@ -import { addNewMessageAtom, updateMessageAtom } from './atoms/ChatMessage.atom' +import { + addNewMessageAtom, + chatMessages, + updateMessageAtom, +} from './atoms/ChatMessage.atom' import { toChatMessage } from '@models/ChatMessage' -import { events, EventName, NewMessageResponse, DataService } from '@janhq/core' -import { useSetAtom } from 'jotai' -import { ReactNode, useEffect } from 'react' +import { events, EventName, NewMessageResponse, PluginType } from '@janhq/core' +import { useAtomValue, useSetAtom } from 'jotai' +import { ReactNode, useEffect, useRef } from 'react' import useGetBots from '@hooks/useGetBots' -import useGetUserConversations from '@hooks/useGetUserConversations' import { updateConversationAtom, updateConversationWaitingForResponseAtom, + userConversationsAtom, } from './atoms/Conversation.atom' -import { executeSerial } from '@plugin/extension-manager' -import { debounce } from 'lodash' import { setDownloadStateAtom, setDownloadStateSuccessAtom, } from './atoms/DownloadState.atom' import { downloadedModelAtom } from './atoms/DownloadedModel.atom' -import { ModelManagementService } from '@janhq/core' import { getDownloadedModels } from '../hooks/useGetDownloadedModels' +import { pluginManager } from '../plugin/PluginManager' +import { Message } from '@janhq/core/lib/types' +import { ConversationalPlugin, ModelPlugin } from '@janhq/core/lib/plugins' +import { downloadingModelsAtom } from './atoms/Model.atom' let currentConversation: Conversation | undefined = undefined -const debouncedUpdateConversation = debounce( - async (updatedConv: Conversation) => { - await executeSerial(DataService.UpdateConversation, updatedConv) - }, - 1000 -) - export default function EventHandler({ children }: { children: ReactNode }) { const addNewMessage = useSetAtom(addNewMessageAtom) const updateMessage = useSetAtom(updateMessageAtom) const updateConversation = useSetAtom(updateConversationAtom) const { getBotById } = useGetBots() - const { getConversationById } = useGetUserConversations() const updateConvWaiting = useSetAtom(updateConversationWaitingForResponseAtom) const setDownloadState = useSetAtom(setDownloadStateAtom) const setDownloadStateSuccess = useSetAtom(setDownloadStateSuccessAtom) const setDownloadedModels = useSetAtom(downloadedModelAtom) + const models = useAtomValue(downloadingModelsAtom) + const messages = useAtomValue(chatMessages) + const conversations = useAtomValue(userConversationsAtom) + const messagesRef = useRef(messages) + const convoRef = useRef(conversations) + + useEffect(() => { + messagesRef.current = messages + convoRef.current = conversations + }, [messages, conversations]) async function handleNewMessageResponse(message: NewMessageResponse) { if (message.conversationId) { - const convo = await getConversationById(message.conversationId) + const convo = convoRef.current.find( + (e) => e._id == message.conversationId + ) + if (!convo) return const botId = convo?.botId - console.debug('botId', botId) if (botId) { const bot = await getBotById(botId) const newResponse = toChatMessage(message, bot) @@ -75,27 +84,53 @@ export default function EventHandler({ children }: { children: ReactNode }) { !currentConversation || currentConversation._id !== messageResponse.conversationId ) { - currentConversation = await getConversationById( - messageResponse.conversationId - ) + if (convoRef.current && messageResponse.conversationId) + currentConversation = convoRef.current.find( + (e) => e._id == messageResponse.conversationId + ) } - const updatedConv: Conversation = { - ...currentConversation, - lastMessage: messageResponse.message, - } + if (currentConversation) { + const updatedConv: Conversation = { + ...currentConversation, + lastMessage: messageResponse.message, + } - updateConversation(updatedConv) - debouncedUpdateConversation(updatedConv) + updateConversation(updatedConv) + } } } async function handleMessageResponseFinished( messageResponse: NewMessageResponse ) { - if (!messageResponse.conversationId) return - console.debug('handleMessageResponseFinished', messageResponse) + if (!messageResponse.conversationId || !convoRef.current) return updateConvWaiting(messageResponse.conversationId, false) + + const convo = convoRef.current.find( + (e) => e._id == messageResponse.conversationId + ) + if (convo) { + const messagesData = (messagesRef.current ?? [])[convo._id].map( + (e: ChatMessage) => { + return { + _id: e.id, + message: e.text, + user: e.senderUid, + updatedAt: new Date(e.createdAt).toISOString(), + createdAt: new Date(e.createdAt).toISOString(), + } + } + ) + pluginManager + .get(PluginType.Conversational) + ?.saveConversation({ + ...convo, + _id: convo._id ?? '', + name: convo.name ?? '', + messages: messagesData, + }) + } } function handleDownloadUpdate(state: any) { @@ -106,14 +141,16 @@ export default function EventHandler({ children }: { children: ReactNode }) { function handleDownloadSuccess(state: any) { if (state && state.fileName && state.success === true) { setDownloadStateSuccess(state.fileName) - executeSerial( - ModelManagementService.UpdateFinishedDownloadAt, - state.fileName - ).then(() => { - getDownloadedModels().then((models) => { - setDownloadedModels(models) - }) - }) + const model = models.find((e) => e._id === state.fileName) + if (model) + pluginManager + .get(PluginType.Model) + ?.saveModel(model) + .then(() => { + getDownloadedModels().then((models) => { + setDownloadedModels(models) + }) + }) } } @@ -122,8 +159,7 @@ export default function EventHandler({ children }: { children: ReactNode }) { events.on(EventName.OnNewMessageResponse, handleNewMessageResponse) events.on(EventName.OnMessageResponseUpdate, handleMessageResponseUpdate) events.on( - 'OnMessageResponseFinished', - // EventName.OnMessageResponseFinished, + EventName.OnMessageResponseFinished, handleMessageResponseFinished ) events.on(EventName.OnDownloadUpdate, handleDownloadUpdate) @@ -136,8 +172,7 @@ export default function EventHandler({ children }: { children: ReactNode }) { events.off(EventName.OnNewMessageResponse, handleNewMessageResponse) events.off(EventName.OnMessageResponseUpdate, handleMessageResponseUpdate) events.off( - 'OnMessageResponseFinished', - // EventName.OnMessageResponseFinished, + EventName.OnMessageResponseFinished, handleMessageResponseFinished ) events.off(EventName.OnDownloadUpdate, handleDownloadUpdate) diff --git a/web/helpers/EventListenerWrapper.tsx b/web/helpers/EventListenerWrapper.tsx index f248bf0209..2bb7ed31fe 100644 --- a/web/helpers/EventListenerWrapper.tsx +++ b/web/helpers/EventListenerWrapper.tsx @@ -1,9 +1,9 @@ 'use client' -import { useSetAtom } from 'jotai' -import { ReactNode, useEffect } from 'react' +import { useAtomValue, useSetAtom } from 'jotai' +import { ReactNode, useEffect, useRef } from 'react' import { appDownloadProgress } from './JotaiWrapper' -import { ModelManagementService } from '@janhq/core' +import { PluginType } from '@janhq/core' import { setDownloadStateAtom, setDownloadStateSuccessAtom, @@ -11,7 +11,9 @@ import { import { getDownloadedModels } from '../hooks/useGetDownloadedModels' import { downloadedModelAtom } from './atoms/DownloadedModel.atom' import EventHandler from './EventHandler' -import { executeSerial } from '@services/pluginService' +import { pluginManager } from '@plugin/PluginManager' +import { ModelPlugin } from '@janhq/core/lib/plugins' +import { downloadingModelsAtom } from './atoms/Model.atom' type Props = { children: ReactNode @@ -22,6 +24,11 @@ export default function EventListenerWrapper({ children }: Props) { const setDownloadStateSuccess = useSetAtom(setDownloadStateSuccessAtom) const setProgress = useSetAtom(appDownloadProgress) const setDownloadedModels = useSetAtom(downloadedModelAtom) + const models = useAtomValue(downloadingModelsAtom) + const modelsRef = useRef(models) + useEffect(() => { + modelsRef.current = models + }, [models]) useEffect(() => { if (window && window.electronAPI) { @@ -42,16 +49,19 @@ export default function EventListenerWrapper({ children }: Props) { window.electronAPI.onFileDownloadSuccess( (_event: string, callback: any) => { if (callback && callback.fileName) { - setDownloadStateSuccess(callback.fileName) + const fileName = callback.fileName.replace('models/', '') + setDownloadStateSuccess(fileName) - executeSerial( - ModelManagementService.UpdateFinishedDownloadAt, - callback.fileName - ).then(() => { - getDownloadedModels().then((models) => { - setDownloadedModels(models) - }) - }) + const model = modelsRef.current.find((e) => e._id === fileName) + if (model) + pluginManager + .get(PluginType.Model) + ?.saveModel(model) + .then(() => { + getDownloadedModels().then((models) => { + setDownloadedModels(models) + }) + }) } } ) diff --git a/web/helpers/atoms/ChatMessage.atom.ts b/web/helpers/atoms/ChatMessage.atom.ts index b61103479c..92ce0ab31b 100644 --- a/web/helpers/atoms/ChatMessage.atom.ts +++ b/web/helpers/atoms/ChatMessage.atom.ts @@ -4,7 +4,7 @@ import { getActiveConvoIdAtom } from './Conversation.atom' /** * Stores all chat messages for all conversations */ -const chatMessages = atom>({}) +export const chatMessages = atom>({}) /** * Return the chat messages for the current active conversation @@ -12,7 +12,8 @@ const chatMessages = atom>({}) export const getCurrentChatMessagesAtom = atom((get) => { const activeConversationId = get(getActiveConvoIdAtom) if (!activeConversationId) return [] - return get(chatMessages)[activeConversationId] ?? [] + const messages = get(chatMessages)[activeConversationId] + return messages ?? [] }) export const setCurrentChatMessagesAtom = atom( @@ -29,6 +30,17 @@ export const setCurrentChatMessagesAtom = atom( } ) +export const setConvoMessagesAtom = atom( + null, + (get, set, messages: ChatMessage[], convoId: string) => { + const newData: Record = { + ...get(chatMessages), + } + newData[convoId] = messages + set(chatMessages, newData) + } +) + /** * Used for pagination. Add old messages to the current conversation */ diff --git a/web/helpers/atoms/DownloadState.atom.ts b/web/helpers/atoms/DownloadState.atom.ts index 8db01d810d..d61f13f599 100644 --- a/web/helpers/atoms/DownloadState.atom.ts +++ b/web/helpers/atoms/DownloadState.atom.ts @@ -10,6 +10,8 @@ export const setDownloadStateAtom = atom( console.debug( `current download state for ${state.fileName} is ${JSON.stringify(state)}` ) + state.fileName = state.fileName.replace('models/', '') + // TODO: Need somehow to not depend on filename currentState[state.fileName] = state set(modelDownloadStateAtom, currentState) } @@ -19,6 +21,7 @@ export const setDownloadStateSuccessAtom = atom( null, (get, set, fileName: string) => { const currentState = { ...get(modelDownloadStateAtom) } + fileName = fileName.replace('models/', '') const state = currentState[fileName] if (!state) { console.error(`Cannot find download state for ${fileName}`) @@ -28,4 +31,4 @@ export const setDownloadStateSuccessAtom = atom( delete currentState[fileName] set(modelDownloadStateAtom, currentState) } -) \ No newline at end of file +) diff --git a/web/helpers/atoms/DownloadedModel.atom.ts b/web/helpers/atoms/DownloadedModel.atom.ts index c7e4aaf314..bf36a103c0 100644 --- a/web/helpers/atoms/DownloadedModel.atom.ts +++ b/web/helpers/atoms/DownloadedModel.atom.ts @@ -1,6 +1,7 @@ +import { Model } from '@janhq/core/lib/types' import { atom } from 'jotai' /** * @description: This atom is used to store the downloaded models */ -export const downloadedModelAtom = atom([]) +export const downloadedModelAtom = atom([]) diff --git a/web/helpers/atoms/Model.atom.ts b/web/helpers/atoms/Model.atom.ts index 21dcd67810..75d4be9723 100644 --- a/web/helpers/atoms/Model.atom.ts +++ b/web/helpers/atoms/Model.atom.ts @@ -1,7 +1,9 @@ +import { Model } from '@janhq/core/lib/types' import { atom } from 'jotai' export const stateModel = atom({ state: 'start', loading: false, model: '' }) -export const selectedModelAtom = atom(undefined) -export const activeAssistantModelAtom = atom( +export const selectedModelAtom = atom(undefined) +export const activeModelAtom = atom( undefined ) +export const downloadingModelsAtom = atom([]) \ No newline at end of file diff --git a/web/hooks/useChatMessages.ts b/web/hooks/useChatMessages.ts deleted file mode 100644 index 28dfb21f92..0000000000 --- a/web/hooks/useChatMessages.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { toChatMessage } from '@models/ChatMessage' -import { executeSerial } from '@services/pluginService' -import { useAtomValue, useSetAtom } from 'jotai' -import { useEffect } from 'react' -import { DataService } from '@janhq/core' -import { - getActiveConvoIdAtom, - userConversationsAtom, -} from '@helpers/atoms/Conversation.atom' -import { - getCurrentChatMessagesAtom, - setCurrentChatMessagesAtom, -} from '@helpers/atoms/ChatMessage.atom' -import useGetBots from './useGetBots' - -/** - * Custom hooks to get chat messages for current(active) conversation - */ -const useChatMessages = () => { - const setMessages = useSetAtom(setCurrentChatMessagesAtom) - const messages = useAtomValue(getCurrentChatMessagesAtom) - const activeConvoId = useAtomValue(getActiveConvoIdAtom) - const userConversations = useAtomValue(userConversationsAtom) - const { getBotById } = useGetBots() - - const getMessages = async (convoId: string) => { - const data: any = await executeSerial( - DataService.GetConversationMessages, - convoId - ) - if (!data) { - return [] - } - - const convo = userConversations.find((c) => c._id === convoId) - if (convo && convo.botId) { - const bot = await getBotById(convo.botId) - return parseMessages(data, bot) - } - - return parseMessages(data) - } - - useEffect(() => { - if (!activeConvoId) { - console.error('active convo is undefined') - return - } - - getMessages(activeConvoId) - .then((messages) => { - setMessages(messages) - }) - .catch((err) => { - console.error(err) - }) - }, [activeConvoId]) - - return { messages } -} - -function parseMessages(messages: RawMessage[], bot?: Bot): ChatMessage[] { - const newMessages: ChatMessage[] = [] - for (const m of messages) { - const chatMessage = toChatMessage(m, bot) - newMessages.push(chatMessage) - } - return newMessages -} - -export default useChatMessages diff --git a/web/hooks/useCreateBot.ts b/web/hooks/useCreateBot.ts index 46503de7b4..16cf1bb6e1 100644 --- a/web/hooks/useCreateBot.ts +++ b/web/hooks/useCreateBot.ts @@ -1,10 +1,7 @@ -import { DataService } from '@janhq/core' -import { executeSerial } from '@services/pluginService' - export default function useCreateBot() { const createBot = async (bot: Bot) => { try { - await executeSerial(DataService.CreateBot, bot) + // await executeSerial(DataService.CreateBot, bot) } catch (err) { alert(err) console.error(err) diff --git a/web/hooks/useCreateConversation.ts b/web/hooks/useCreateConversation.ts index 54f233c831..a8d615755d 100644 --- a/web/hooks/useCreateConversation.ts +++ b/web/hooks/useCreateConversation.ts @@ -1,23 +1,22 @@ -import { useAtom, useSetAtom } from 'jotai' -import { executeSerial } from '@services/pluginService' -import { DataService, ModelManagementService } from '@janhq/core' +import { useAtom, useAtomValue, useSetAtom } from 'jotai' import { userConversationsAtom, setActiveConvoIdAtom, addNewConversationStateAtom, } from '@helpers/atoms/Conversation.atom' -import useGetModelById from './useGetModelById' +import { Model } from '@janhq/core/lib/types' +import { downloadedModelAtom } from '@helpers/atoms/DownloadedModel.atom' const useCreateConversation = () => { const [userConversations, setUserConversations] = useAtom( userConversationsAtom ) - const { getModelById } = useGetModelById() const setActiveConvoId = useSetAtom(setActiveConvoIdAtom) const addNewConvoState = useSetAtom(addNewConversationStateAtom) + const models = useAtomValue(downloadedModelAtom) const createConvoByBot = async (bot: Bot) => { - const model = await getModelById(bot.modelId) + const model = models.find((e) => e._id === bot.modelId) if (!model) { alert( @@ -29,19 +28,10 @@ const useCreateConversation = () => { return requestCreateConvo(model, bot) } - const requestCreateConvo = async (model: AssistantModel, bot?: Bot) => { + const requestCreateConvo = async (model: Model, bot?: Bot) => { const conversationName = model.name - const conv: Conversation = { - modelId: model._id, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - name: conversationName, - botId: bot?._id ?? undefined, - } - const id = await executeSerial(DataService.CreateConversation, conv) - const mappedConvo: Conversation = { - _id: id, + _id: `jan-${Date.now()}`, modelId: model._id, name: conversationName, createdAt: new Date().toISOString(), @@ -49,12 +39,12 @@ const useCreateConversation = () => { botId: bot?._id ?? undefined, } - addNewConvoState(id ?? '', { + addNewConvoState(mappedConvo._id, { hasMore: true, waitingForResponse: false, }) setUserConversations([mappedConvo, ...userConversations]) - setActiveConvoId(id) + setActiveConvoId(mappedConvo._id) } return { diff --git a/web/hooks/useDeleteBot.ts b/web/hooks/useDeleteBot.ts index 8d7f6abed2..4fcf4ac26a 100644 --- a/web/hooks/useDeleteBot.ts +++ b/web/hooks/useDeleteBot.ts @@ -1,8 +1,6 @@ import { useSetAtom } from 'jotai' import { activeBotAtom } from '@helpers/atoms/Bot.atom' import { rightSideBarExpandStateAtom } from '@helpers/atoms/SideBarExpand.atom' -import { executeSerial } from '@services/pluginService' -import { DataService } from '@janhq/core' export default function useDeleteBot() { const setActiveBot = useSetAtom(activeBotAtom) @@ -10,7 +8,7 @@ export default function useDeleteBot() { const deleteBot = async (botId: string): Promise<'success' | 'failed'> => { try { - await executeSerial(DataService.DeleteBot, botId) + // await executeSerial(DataService.DeleteBot, botId) setRightPanelVisibility(false) setActiveBot(undefined) return 'success' diff --git a/web/hooks/useDeleteConversation.ts b/web/hooks/useDeleteConversation.ts index d78acb48ad..98cb73b7a4 100644 --- a/web/hooks/useDeleteConversation.ts +++ b/web/hooks/useDeleteConversation.ts @@ -1,7 +1,6 @@ import { currentPromptAtom } from '@helpers/JotaiWrapper' -import { executeSerial } from '@services/pluginService' import { useAtom, useAtomValue, useSetAtom } from 'jotai' -import { DataService } from '@janhq/core' +import { PluginType } from '@janhq/core' import { deleteConversationMessage } from '@helpers/atoms/ChatMessage.atom' import { userConversationsAtom, @@ -16,6 +15,8 @@ import { MainViewState, setMainViewStateAtom, } from '@helpers/atoms/MainView.atom' +import { pluginManager } from '../plugin/PluginManager' +import { ConversationalPlugin } from '@janhq/core/lib/plugins' export default function useDeleteConversation() { const [userConversations, setUserConversations] = useAtom( @@ -33,7 +34,9 @@ export default function useDeleteConversation() { const deleteConvo = async () => { if (activeConvoId) { try { - await executeSerial(DataService.DeleteConversation, activeConvoId) + await pluginManager + .get(PluginType.Conversational) + ?.deleteConversation(activeConvoId) const currentConversations = userConversations.filter( (c) => c._id !== activeConvoId ) diff --git a/web/hooks/useDeleteModel.ts b/web/hooks/useDeleteModel.ts index 295120fc8d..6623be06f8 100644 --- a/web/hooks/useDeleteModel.ts +++ b/web/hooks/useDeleteModel.ts @@ -1,15 +1,18 @@ -import { execute, executeSerial } from '@services/pluginService' -import { ModelManagementService } from '@janhq/core' +import { PluginType } from '@janhq/core' import { useSetAtom } from 'jotai' import { downloadedModelAtom } from '@helpers/atoms/DownloadedModel.atom' import { getDownloadedModels } from './useGetDownloadedModels' +import { pluginManager } from '@plugin/PluginManager' +import { ModelPlugin } from '@janhq/core/lib/plugins' +import { Model } from '@janhq/core/lib/types' export default function useDeleteModel() { const setDownloadedModels = useSetAtom(downloadedModelAtom) - const deleteModel = async (model: AssistantModel) => { - execute(ModelManagementService.DeleteDownloadModel, model._id) - await executeSerial(ModelManagementService.DeleteModel, model._id) + const deleteModel = async (model: Model) => { + await pluginManager + .get(PluginType.Model) + ?.deleteModel(model._id) // reload models const downloadedModels = await getDownloadedModels() diff --git a/web/hooks/useDownloadModel.ts b/web/hooks/useDownloadModel.ts index 78a82aed98..ee071dc8b9 100644 --- a/web/hooks/useDownloadModel.ts +++ b/web/hooks/useDownloadModel.ts @@ -1,15 +1,19 @@ -import { executeSerial } from '@services/pluginService' -import { ModelManagementService } from '@janhq/core' -import { useSetAtom } from 'jotai' +import { PluginType } from '@janhq/core' +import { useAtom, useSetAtom } from 'jotai' import { setDownloadStateAtom } from '@helpers/atoms/DownloadState.atom' +import { Model, ModelCatalog, ModelVersion } from '@janhq/core/lib/types' +import { pluginManager } from '@plugin/PluginManager' +import { ModelPlugin } from '@janhq/core/lib/plugins' +import { downloadingModelsAtom } from '@helpers/atoms/Model.atom' export default function useDownloadModel() { const setDownloadState = useSetAtom(setDownloadStateAtom) + const [models, setModelsAtom] = useAtom(downloadingModelsAtom) const assistanModel = ( - model: Product, + model: ModelCatalog, modelVersion: ModelVersion - ): AssistantModel => { + ): Model => { return { _id: modelVersion._id, name: modelVersion.name, @@ -29,9 +33,6 @@ export default function useDownloadModel() { author: model.author, version: model.version, modelUrl: model.modelUrl, - nsfw: model.nsfw === true ? false : true, - greeting: model.greeting, - type: model.type, createdAt: new Date(model.createdAt).getTime(), updatedAt: new Date(model.updatedAt ?? '').getTime(), status: '', @@ -40,7 +41,10 @@ export default function useDownloadModel() { } } - const downloadModel = async (model: Product, modelVersion: ModelVersion) => { + const downloadModel = async ( + model: ModelCatalog, + modelVersion: ModelVersion + ) => { // set an initial download state setDownloadState({ modelId: modelVersion._id, @@ -59,11 +63,10 @@ export default function useDownloadModel() { modelVersion.startDownloadAt = Date.now() const assistantModel = assistanModel(model, modelVersion) - await executeSerial(ModelManagementService.StoreModel, assistantModel) - await executeSerial(ModelManagementService.DownloadModel, { - downloadUrl: modelVersion.downloadLink, - fileName: modelVersion._id, - }) + setModelsAtom([...models, assistantModel]) + await pluginManager + .get(PluginType.Model) + ?.downloadModel(assistantModel) } return { diff --git a/web/hooks/useGetBots.ts b/web/hooks/useGetBots.ts index e35e76478c..ccf54f7b30 100644 --- a/web/hooks/useGetBots.ts +++ b/web/hooks/useGetBots.ts @@ -1,11 +1,8 @@ -import { DataService } from '@janhq/core' -import { executeSerial } from '@services/pluginService' - export default function useGetBots() { const getAllBots = async (): Promise => { try { - const bots = await executeSerial(DataService.GetBots) - return bots + // const bots = await executeSerial(DataService.GetBots) + return [] } catch (err) { alert(`Failed to get bots: ${err}`) console.error(err) @@ -15,8 +12,8 @@ export default function useGetBots() { const getBotById = async (botId: string): Promise => { try { - const bot: Bot = await executeSerial(DataService.GetBotById, botId) - return bot + // const bot: Bot = await executeSerial(DataService.GetBotById, botId) + return undefined } catch (err) { alert(`Failed to get bot ${botId}: ${err}`) console.error(err) diff --git a/web/hooks/useGetConfiguredModels.ts b/web/hooks/useGetConfiguredModels.ts index 6046fdb106..17094f9465 100644 --- a/web/hooks/useGetConfiguredModels.ts +++ b/web/hooks/useGetConfiguredModels.ts @@ -1,13 +1,28 @@ import { useEffect, useState } from 'react' -import { getConfiguredModels } from './useGetDownloadedModels' +import { ModelCatalog } from '@janhq/core/lib/types' +import { pluginManager } from '@plugin/PluginManager' +import { ModelPlugin } from '@janhq/core/lib/plugins' +import { PluginType } from '@janhq/core' +import { dummyModel } from '@utils/dummy' export default function useGetConfiguredModels() { const [loading, setLoading] = useState(false) - const [models, setModels] = useState([]) + const [models, setModels] = useState([]) + + async function getConfiguredModels(): Promise { + return ( + ((await pluginManager + .get(PluginType.Model) + ?.getConfiguredModels()) as ModelCatalog[]) ?? [] + ) + } const fetchModels = async () => { setLoading(true) - const models = await getConfiguredModels() + let models = await getConfiguredModels() + if (process.env.NODE_ENV === 'development') { + models = [dummyModel, ...models] + } setLoading(false) setModels(models) } diff --git a/web/hooks/useGetDownloadedModels.ts b/web/hooks/useGetDownloadedModels.ts index 49703bbab8..9d5b80b3e4 100644 --- a/web/hooks/useGetDownloadedModels.ts +++ b/web/hooks/useGetDownloadedModels.ts @@ -1,9 +1,10 @@ import { useEffect } from 'react' -import { ModelManagementService } from '@janhq/core' +import { PluginType } from '@janhq/core' import { useAtom } from 'jotai' import { downloadedModelAtom } from '@helpers/atoms/DownloadedModel.atom' -import { extensionPoints } from '@plugin' -import { executeSerial } from '@services/pluginService' +import { pluginManager } from '@plugin/PluginManager' +import { ModelPlugin } from '@janhq/core/lib/plugins' +import { Model } from '@janhq/core/lib/types' export function useGetDownloadedModels() { const [downloadedModels, setDownloadedModels] = useAtom(downloadedModelAtom) @@ -17,16 +18,10 @@ export function useGetDownloadedModels() { return { downloadedModels } } -export async function getDownloadedModels(): Promise { - if (!extensionPoints.get(ModelManagementService.GetFinishedDownloadModels)) { - return [] - } - const downloadedModels: AssistantModel[] = await executeSerial( - ModelManagementService.GetFinishedDownloadModels - ) - return downloadedModels ?? [] -} - -export async function getConfiguredModels(): Promise { - return executeSerial(ModelManagementService.GetConfiguredModels) +export async function getDownloadedModels(): Promise { + const models = + ((await pluginManager + .get(PluginType.Model) + ?.getDownloadedModels()) as Model[]) ?? [] + return models } diff --git a/web/hooks/useGetHuggingFaceModel.ts b/web/hooks/useGetHuggingFaceModel.ts index 9edd84192e..498fb9b28b 100644 --- a/web/hooks/useGetHuggingFaceModel.ts +++ b/web/hooks/useGetHuggingFaceModel.ts @@ -1,10 +1,11 @@ import { useState } from 'react' import { useSetAtom } from 'jotai' import { modelLoadMoreAtom } from '@helpers/atoms/ExploreModelLoading.atom' +import { ModelCatalog } from '@janhq/core/lib/types' export default function useGetHuggingFaceModel() { const setLoadMoreInProgress = useSetAtom(modelLoadMoreAtom) - const [modelList, setModelList] = useState([]) + const [modelList, setModelList] = useState([]) const [currentOwner, setCurrentOwner] = useState( undefined ) diff --git a/web/hooks/useGetInputState.ts b/web/hooks/useGetInputState.ts index d112255976..05506e0c80 100644 --- a/web/hooks/useGetInputState.ts +++ b/web/hooks/useGetInputState.ts @@ -1,19 +1,20 @@ import { currentConversationAtom } from '@helpers/atoms/Conversation.atom' -import { activeAssistantModelAtom } from '@helpers/atoms/Model.atom' +import { activeModelAtom } from '@helpers/atoms/Model.atom' import { useAtomValue } from 'jotai' import { useEffect, useState } from 'react' import { useGetDownloadedModels } from './useGetDownloadedModels' +import { Model } from '@janhq/core/lib/types' export default function useGetInputState() { const [inputState, setInputState] = useState('loading') const currentConvo = useAtomValue(currentConversationAtom) - const activeModel = useAtomValue(activeAssistantModelAtom) + const activeModel = useAtomValue(activeModelAtom) const { downloadedModels } = useGetDownloadedModels() const handleInputState = ( convo: Conversation | undefined, - currentModel: AssistantModel | undefined, - models: AssistantModel[] + currentModel: Model | undefined, + models: Model[] ) => { if (convo == null) return if (currentModel == null) { diff --git a/web/hooks/useGetModelById.ts b/web/hooks/useGetModelById.ts deleted file mode 100644 index 65990b1982..0000000000 --- a/web/hooks/useGetModelById.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ModelManagementService } from '@janhq/core' -import { executeSerial } from '@plugin/extension-manager' - -export default function useGetModelById() { - const getModelById = async ( - modelId: string - ): Promise => { - return queryModelById(modelId) - } - - return { getModelById } -} - -const queryModelById = async ( - modelId: string -): Promise => { - const model = await executeSerial( - ModelManagementService.GetModelById, - modelId - ) - - return model -} diff --git a/web/hooks/useGetMostSuitableModelVersion.ts b/web/hooks/useGetMostSuitableModelVersion.ts index 5dd95d1e69..be99401db6 100644 --- a/web/hooks/useGetMostSuitableModelVersion.ts +++ b/web/hooks/useGetMostSuitableModelVersion.ts @@ -1,6 +1,7 @@ import { useState } from 'react' import { useAtomValue } from 'jotai' import { totalRamAtom } from '@helpers/atoms/SystemBar.atom' +import { ModelVersion } from '@janhq/core/lib/types' export default function useGetMostSuitableModelVersion() { const [suitableModel, setSuitableModel] = useState() diff --git a/web/hooks/useGetPerformanceTag.ts b/web/hooks/useGetPerformanceTag.ts index b5122fc69b..6d0cbe3b0e 100644 --- a/web/hooks/useGetPerformanceTag.ts +++ b/web/hooks/useGetPerformanceTag.ts @@ -3,6 +3,7 @@ import { useState } from 'react' import { ModelPerformance, TagType } from '@/_components/SimpleTag/TagType' import { useAtomValue } from 'jotai' import { totalRamAtom } from '@helpers/atoms/SystemBar.atom' +import { ModelVersion } from '@janhq/core/lib/types' // Recommendation: // `Recommended (green)`: "Max RAM required" is 80% of users max RAM. diff --git a/web/hooks/useGetSystemResources.ts b/web/hooks/useGetSystemResources.ts index 77a91bca1e..94ceb56877 100644 --- a/web/hooks/useGetSystemResources.ts +++ b/web/hooks/useGetSystemResources.ts @@ -1,24 +1,24 @@ import { useEffect, useState } from 'react' -import { extensionPoints } from '@plugin' -import { SystemMonitoringService } from '@janhq/core' +import { PluginType } from '@janhq/core' import { useSetAtom } from 'jotai' import { totalRamAtom } from '@helpers/atoms/SystemBar.atom' -import { executeSerial } from '@services/pluginService' +import { pluginManager } from '@plugin/PluginManager' +import { MonitoringPlugin } from '@janhq/core/lib/plugins' export default function useGetSystemResources() { const [ram, setRam] = useState(0) const [cpu, setCPU] = useState(0) const setTotalRam = useSetAtom(totalRamAtom) const getSystemResources = async () => { - if (!extensionPoints.get(SystemMonitoringService.GetResourcesInfo)) { + if (!pluginManager.get(PluginType.SystemMonitoring)) { return } - const resourceInfor = await executeSerial( - SystemMonitoringService.GetResourcesInfo - ) - const currentLoadInfor = await executeSerial( - SystemMonitoringService.GetCurrentLoad + const monitoring = pluginManager.get( + PluginType.SystemMonitoring ) + const resourceInfor = await monitoring?.getResourcesInfo() + const currentLoadInfor = await monitoring?.getCurrentLoad() + const ram = (resourceInfor?.mem?.active ?? 0) / (resourceInfor?.mem?.total ?? 1) if (resourceInfor?.mem?.total) setTotalRam(resourceInfor.mem.total) diff --git a/web/hooks/useGetUserConversations.ts b/web/hooks/useGetUserConversations.ts index 6055b69622..65dc04d240 100644 --- a/web/hooks/useGetUserConversations.ts +++ b/web/hooks/useGetUserConversations.ts @@ -1,26 +1,35 @@ import { useSetAtom } from 'jotai' -import { executeSerial } from '@services/pluginService' -import { DataService } from '@janhq/core' import { conversationStatesAtom, userConversationsAtom, } from '@helpers/atoms/Conversation.atom' +import { pluginManager } from '../plugin/PluginManager' +import { PluginType } from '@janhq/core' +import { setConvoMessagesAtom } from '@helpers/atoms/ChatMessage.atom' +import { toChatMessage } from '@models/ChatMessage' +import { ConversationalPlugin } from '@janhq/core/lib/plugins' +import { Conversation } from "@janhq/core/lib/types" const useGetUserConversations = () => { const setConversationStates = useSetAtom(conversationStatesAtom) const setConversations = useSetAtom(userConversationsAtom) + const setConvoMessages = useSetAtom(setConvoMessagesAtom) const getUserConversations = async () => { try { - const convos: Conversation[] | undefined = await executeSerial( - DataService.GetConversations - ) + const convos: Conversation[] | undefined = await pluginManager + .get(PluginType.Conversational) + ?.getConversations() const convoStates: Record = {} convos?.forEach((convo) => { convoStates[convo._id ?? ''] = { hasMore: true, waitingForResponse: false, } + setConvoMessages( + convo.messages.map((msg) => toChatMessage(msg)), + convo._id ?? '' + ) }) setConversationStates(convoStates) setConversations(convos ?? []) @@ -29,14 +38,7 @@ const useGetUserConversations = () => { } } - const getConversationById = async ( - id: string - ): Promise => { - return await executeSerial(DataService.GetConversationById, id) - } - return { - getConversationById, getUserConversations, } } diff --git a/web/hooks/useSendChatMessage.ts b/web/hooks/useSendChatMessage.ts index 3b8b4fcefb..c6dbf9e114 100644 --- a/web/hooks/useSendChatMessage.ts +++ b/web/hooks/useSendChatMessage.ts @@ -1,20 +1,24 @@ import { currentPromptAtom } from '@helpers/JotaiWrapper' import { useAtom, useAtomValue, useSetAtom } from 'jotai' import { - DataService, EventName, - InferenceService, + MessageHistory, + NewMessageRequest, + PluginType, events, - store, } from '@janhq/core' import { toChatMessage } from '@models/ChatMessage' -import { executeSerial } from '@services/pluginService' -import { addNewMessageAtom } from '@helpers/atoms/ChatMessage.atom' +import { + addNewMessageAtom, + getCurrentChatMessagesAtom, +} from '@helpers/atoms/ChatMessage.atom' import { currentConversationAtom, updateConversationAtom, updateConversationWaitingForResponseAtom, } from '@helpers/atoms/Conversation.atom' +import { pluginManager } from '@plugin/PluginManager' +import { InferencePlugin } from '@janhq/core/lib/plugins' export default function useSendChatMessage() { const currentConvo = useAtomValue(currentConversationAtom) @@ -22,6 +26,7 @@ export default function useSendChatMessage() { const updateConversation = useSetAtom(updateConversationAtom) const updateConvWaiting = useSetAtom(updateConversationWaitingForResponseAtom) const [currentPrompt, setCurrentPrompt] = useAtom(currentPromptAtom) + const currentMessages = useAtomValue(getCurrentChatMessagesAtom) let timeout: any | undefined = undefined @@ -40,10 +45,9 @@ export default function useSendChatMessage() { setTimeout(async () => { newMessage.message = 'summary this conversation in 5 words, the response should just include the summary' - const result = await executeSerial( - InferenceService.InferenceRequest, - newMessage - ) + const result = await pluginManager + .get(PluginType.Inference) + ?.inferenceRequest(newMessage) if ( result?.message && @@ -55,7 +59,6 @@ export default function useSendChatMessage() { summary: result.message, } updateConversation(updatedConv) - await executeSerial(DataService.UpdateConversation, updatedConv) } }, 1000) } @@ -70,14 +73,28 @@ export default function useSendChatMessage() { updateConvWaiting(convoId, true) const prompt = currentPrompt.trim() - const newMessage: RawMessage = { + const messageHistory: MessageHistory[] = currentMessages + .map((msg) => { + return { + role: msg.senderUid === 'user' ? 'user' : 'assistant', + content: msg.text ?? '', + } + }) + .reverse() + .concat([ + { + role: 'user', + content: prompt, + } as MessageHistory, + ]) + const newMessage: NewMessageRequest = { + _id: `message-${Date.now()}`, conversationId: convoId, message: prompt, user: 'user', createdAt: new Date().toISOString(), + history: messageHistory, } - const id = await executeSerial(DataService.CreateMessage, newMessage) - newMessage._id = id const newChatMessage = toChatMessage(newMessage) addNewMessage(newChatMessage) @@ -92,7 +109,6 @@ export default function useSendChatMessage() { } updateConversation(updatedConv) - await executeSerial(DataService.UpdateConversation, updatedConv) } else { const updatedConv: Conversation = { ...currentConvo, @@ -100,7 +116,6 @@ export default function useSendChatMessage() { } updateConversation(updatedConv) - await executeSerial(DataService.UpdateConversation, updatedConv) } updateConvSummary(newMessage) diff --git a/web/hooks/useStartStopModel.ts b/web/hooks/useStartStopModel.ts index 574e000fc6..dd9451aac4 100644 --- a/web/hooks/useStartStopModel.ts +++ b/web/hooks/useStartStopModel.ts @@ -1,13 +1,14 @@ -import { executeSerial } from '@services/pluginService' -import { InferenceService } from '@janhq/core' -import { useAtom, useSetAtom } from 'jotai' -import { activeAssistantModelAtom, stateModel } from '@helpers/atoms/Model.atom' -import useGetModelById from './useGetModelById' +import { PluginType } from '@janhq/core' +import { InferencePlugin } from '@janhq/core/lib/plugins' +import { useAtom, useAtomValue, useSetAtom } from 'jotai' +import { activeModelAtom, stateModel } from '@helpers/atoms/Model.atom' +import { pluginManager } from '@plugin/PluginManager' +import { downloadedModelAtom } from '@helpers/atoms/DownloadedModel.atom' export default function useStartStopModel() { - const [activeModel, setActiveModel] = useAtom(activeAssistantModelAtom) - const { getModelById } = useGetModelById() + const [activeModel, setActiveModel] = useAtom(activeModelAtom) const setStateModel = useSetAtom(stateModel) + const models = useAtomValue(downloadedModelAtom) const startModel = async (modelId: string) => { if (activeModel && activeModel._id === modelId) { @@ -17,7 +18,7 @@ export default function useStartStopModel() { setStateModel({ state: 'start', loading: true, model: modelId }) - const model = await getModelById(modelId) + const model = await models.find((model) => model._id == modelId) if (!model) { alert(`Model ${modelId} not found! Please re-download the model first.`) @@ -28,7 +29,7 @@ export default function useStartStopModel() { const currentTime = Date.now() console.debug('Init model: ', model._id) - const res = await initModel(model._id) + const res = await initModel(`models/${model._id}`) if (res?.error) { const errorMessage = `Failed to init model: ${res.error}` console.error(errorMessage) @@ -47,7 +48,9 @@ export default function useStartStopModel() { const stopModel = async (modelId: string) => { setStateModel({ state: 'stop', loading: true, model: modelId }) setTimeout(async () => { - await executeSerial(InferenceService.StopModel, modelId) + await pluginManager + .get(PluginType.Inference) + ?.stopModel() setActiveModel(undefined) setStateModel({ state: 'stop', loading: false, model: modelId }) }, 500) @@ -57,5 +60,7 @@ export default function useStartStopModel() { } const initModel = async (modelId: string): Promise => { - return executeSerial(InferenceService.InitModel, modelId) + await pluginManager + .get(PluginType.Inference) + ?.initModel(modelId) } diff --git a/web/hooks/useUpdateBot.ts b/web/hooks/useUpdateBot.ts index b913ab3940..c2dbc9104e 100644 --- a/web/hooks/useUpdateBot.ts +++ b/web/hooks/useUpdateBot.ts @@ -1,6 +1,3 @@ -import { DataService } from '@janhq/core' -import { executeSerial } from '@services/pluginService' - export default function useUpdateBot() { const updateBot = async ( bot: Bot, @@ -16,7 +13,7 @@ export default function useUpdateBot() { } } - await executeSerial(DataService.UpdateBot, bot) + // await executeSerial(DataService.UpdateBot, bot) console.debug('Bot updated', JSON.stringify(bot, null, 2)) } catch (err) { alert(`Update bot error: ${err}`) diff --git a/web/models/ChatMessage.ts b/web/models/ChatMessage.ts index e5763c984d..9d3f0f4ddd 100644 --- a/web/models/ChatMessage.ts +++ b/web/models/ChatMessage.ts @@ -1,4 +1,5 @@ import { NewMessageResponse } from '@janhq/core' +import { Message } from '@janhq/core/lib/types' export enum MessageType { Text = 'Text', Image = 'Image', @@ -41,8 +42,9 @@ export interface RawMessage { } export const toChatMessage = ( - m: RawMessage | NewMessageResponse, - bot?: Bot + m: RawMessage | Message | NewMessageResponse, + bot?: Bot, + conversationId?: string ): ChatMessage => { const createdAt = new Date(m.createdAt ?? '').getTime() const imageUrls: string[] = [] @@ -64,16 +66,17 @@ export const toChatMessage = ( return { id: (m._id ?? 0).toString(), - conversationId: (m.conversationId ?? 0).toString(), + conversationId: ( + (m as RawMessage | NewMessageResponse)?.conversationId ?? + conversationId ?? + 0 + ).toString(), messageType: messageType, messageSenderType: messageSenderType, senderUid: m.user?.toString() || '0', senderName: senderName, - senderAvatarUrl: m.avatar - ? m.avatar - : m.user === 'user' - ? 'icons/avatar.svg' - : 'icons/app_icon.svg', + senderAvatarUrl: + m.user === 'user' ? 'icons/avatar.svg' : 'icons/app_icon.svg', text: content, imageUrls: imageUrls, createdAt: createdAt, diff --git a/web/plugin/Activation.ts b/web/plugin/Activation.ts deleted file mode 100644 index fe517d4f91..0000000000 --- a/web/plugin/Activation.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { callExport } from "./import-manager" - -class Activation { - /** @type {string} Name of the registered plugin. */ - plugin - - /** @type {string} Name of the activation point that is registered to. */ - activationPoint - - /** @type {string} location of the file containing the activation function. */ - url - - /** @type {boolean} Whether the activation has been activated. */ - activated - - constructor(plugin: string, activationPoint: string, url: string) { - this.plugin = plugin - this.activationPoint = activationPoint - this.url = url - this.activated = false - } - - /** - * Trigger the activation function in the plugin once, - * providing the list of extension points or an object with the extension point's register, execute and executeSerial functions. - * @returns {boolean} Whether the activation has already been activated. - */ - async trigger() { - if (!this.activated) { - await callExport(this.url, this.activationPoint, this.plugin) - this.activated = true - } - return this.activated - } -} - -export default Activation diff --git a/web/plugin/ExtensionPoint.ts b/web/plugin/ExtensionPoint.ts deleted file mode 100644 index be97702321..0000000000 --- a/web/plugin/ExtensionPoint.ts +++ /dev/null @@ -1,146 +0,0 @@ -/** - * @typedef {Object} Extension An extension registered to an extension point - * @property {string} name Unique name for the extension. - * @property {Object|Callback} response Object to be returned or function to be called by the extension point. - * @property {number} [priority] Order priority for execution used for executing in serial. - */ - -/** - * Represents a point in the consumer's code that can be extended by a plugin. - * The plugin can register a callback or object to the extension point. - * When the extension point is triggered, the provided function will then be called or object will be returned. - */ -class ExtensionPoint { - /** @type {string} Name of the extension point */ - name - - /** - * @type {Array.} The list of all extensions registered with this extension point. - * @private - */ - _extensions: any[] = [] - - /** - * @type {Array.} A list of functions to be executed when the list of extensions changes. - * @private - */ - changeListeners: any[] = [] - - constructor(name: string) { - this.name = name - } - - /** - * Register new extension with this extension point. - * The registered response will be executed (if callback) or returned (if object) - * when the extension point is executed (see below). - * @param {string} name Unique name for the extension. - * @param {Object|Callback} response Object to be returned or function to be called by the extension point. - * @param {number} [priority] Order priority for execution used for executing in serial. - * @returns {void} - */ - register(name: string, response: any, priority: number = 0) { - const index = this._extensions.findIndex((p) => p.priority > priority) - const newExt = { name, response, priority } - if (index > -1) { - this._extensions.splice(index, 0, newExt) - } else { - this._extensions.push(newExt) - } - - this.emitChange() - } - - /** - * Remove an extension from the registry. It will no longer be part of the extension point execution. - * @param {RegExp } name Matcher for the name of the extension to remove. - * @returns {void} - */ - unregister(name: string) { - const index = this._extensions.findIndex((ext) => ext.name.match(name)) - if (index > -1) this._extensions.splice(index, 1) - - this.emitChange() - } - - /** - * Empty the registry of all extensions. - * @returns {void} - */ - clear() { - this._extensions = [] - this.emitChange() - } - - /** - * Get a specific extension registered with the extension point - * @param {string} name Name of the extension to return - * @returns {Object|Callback|undefined} The response of the extension. If this is a function the function is returned, not its response. - */ - get(name: string) { - const ep = this._extensions.find((ext) => ext.name === name) - return ep && ep.response - } - - /** - * Execute (if callback) and return or just return (if object) the response for each extension registered to this extension point. - * Any asynchronous responses will be executed in parallel and the returned array will contain a promise for each of these responses. - * @param {*} input Input to be provided as a parameter to each response if response is a callback. - * @returns {Array} List of responses from the extensions. - */ - execute(input: any) { - return this._extensions.map((p) => { - if (typeof p.response === 'function') { - return p.response(input) - } else { - return p.response - } - }) - } - - /** - * Execute (if callback) and return the response, or push it to the array if the previous response is an array - * for each extension registered to this extension point in serial, - * feeding the result from the last response as input to the next. - * @param {*} input Input to be provided as a parameter to the 1st callback - * @returns {Promise.<*>} Result of the last extension that was called - */ - async executeSerial(input: any) { - return await this._extensions.reduce(async (throughput, p) => { - let tp = await throughput - if (typeof p.response === 'function') { - tp = await p.response(tp) - } else if (Array.isArray(tp)) { - tp.push(p.response) - } - return tp - }, input) - } - - /** - * Register a callback to be executed if the list of extensions changes. - * @param {string} name Name of the listener needed if it is to be removed. - * @param {Function} callback The callback function to trigger on a change. - */ - onRegister(name: string, callback: any) { - if (typeof callback === 'function') - this.changeListeners.push({ name, callback }) - } - - /** - * Unregister a callback from the extension list changes. - * @param {string} name The name of the listener to remove. - */ - offRegister(name: string) { - const index = this.changeListeners.findIndex((l) => l.name === name) - if (index > -1) this.changeListeners.splice(index, 1) - } - - emitChange() { - for (const l of this.changeListeners) { - l.callback(this) - } - } -} - -export default ExtensionPoint diff --git a/web/plugin/Plugin.ts b/web/plugin/Plugin.ts index bf24c4f2e9..fc97b01623 100644 --- a/web/plugin/Plugin.ts +++ b/web/plugin/Plugin.ts @@ -1,5 +1,3 @@ -import { callExport } from './import-manager' - /** * A slimmed down representation of a plugin for the renderer. */ @@ -42,14 +40,6 @@ class Plugin { this.version = version this.icon = icon } - - /** - * Trigger an exported callback on the plugin's main file. - * @param {string} exp exported callback to trigger. - */ - triggerExport(exp: string) { - if (this.url && this.name) callExport(this.url, exp, this.name) - } } export default Plugin diff --git a/web/plugin/PluginManager.ts b/web/plugin/PluginManager.ts new file mode 100644 index 0000000000..5f1b69b99b --- /dev/null +++ b/web/plugin/PluginManager.ts @@ -0,0 +1,150 @@ +import { JanPlugin, PluginType } from '@janhq/core' +import Plugin from './Plugin' + +/** + * Manages the registration and retrieval of plugins. + */ +export class PluginManager { + // MARK: - Plugin Manager + private plugins = new Map() + + /** + * Registers a plugin. + * @param plugin - The plugin to register. + */ + register(plugin: T) { + this.plugins.set(plugin.type(), plugin) + } + + /** + * Retrieves a plugin by its type. + * @param type - The type of the plugin to retrieve. + * @returns The plugin, if found. + */ + get(type: PluginType): T | undefined { + return this.plugins.get(type) as T | undefined + } + + /** + * Loads all registered plugins. + */ + load() { + this.listPlugins().forEach((plugin) => { + plugin.onLoad() + }) + } + + /** + * Unloads all registered plugins. + */ + unload() { + this.listPlugins().forEach((plugin) => { + plugin.onUnload() + }) + } + + /** + * Retrieves a list of all registered plugins. + * @returns An array of all registered plugins. + */ + listPlugins() { + return [...this.plugins.values()] + } + + /** + * Retrieves a list of all registered plugins. + * @returns An array of all registered plugins. + */ + async getActive(): Promise { + const plgList = await window.pluggableElectronIpc?.getActive() + let plugins: Plugin[] = plgList.map( + (plugin: any) => + new Plugin( + plugin.name, + plugin.url, + plugin.activationPoints, + plugin.active, + plugin.description, + plugin.version, + plugin.icon + ) + ) + return plugins + } + + /** + * Register a plugin with its class. + * @param {Plugin} plugin plugin object as provided by the main process. + * @returns {void} + */ + async activatePlugin(plugin: Plugin) { + if (plugin.url) + // Import class + await import(/* webpackIgnore: true */ plugin.url).then((pluginClass) => { + // Register class if it has a default export + if ( + typeof pluginClass.default === 'function' && + pluginClass.default.prototype + ) { + this.register(new pluginClass.default()) + } + }) + } + + // MARK: - Plugin Facades + /** + * Registers all active plugins. + * @returns {void} + */ + async registerActive() { + // Get active plugins + const plugins = await this.getActive() + // Activate all + await Promise.all( + plugins.map((plugin: Plugin) => this.activatePlugin(plugin)) + ) + } + + /** + * Install a new plugin. + * @param {Array.} plugins A list of NPM specifiers, or installation configuration objects. + * @returns {Promise. | false>} plugin as defined by the main process. Has property cancelled set to true if installation was cancelled in the main process. + * @alias plugins.install + */ + async install(plugins: any[]) { + if (typeof window === 'undefined') { + return + } + const plgList = await window.pluggableElectronIpc?.install(plugins) + if (plgList.cancelled) return false + return plgList.map(async (plg: any) => { + const plugin = new Plugin( + plg.name, + plg.url, + plg.activationPoints, + plg.active + ) + await this.activatePlugin(plugin) + return plugin + }) + } + + /** + * Uninstall provided plugins + * @param {Array.} plugins List of names of plugins to uninstall. + * @param {boolean} reload Whether to reload all renderers after updating the plugins. + * @returns {Promise.} Whether uninstalling the plugins was successful. + * @alias plugins.uninstall + */ + uninstall(plugins: string[], reload = true) { + if (typeof window === 'undefined') { + return + } + return window.pluggableElectronIpc?.uninstall(plugins, reload) + } +} + +/** + * The singleton instance of the PluginManager. + */ +export const pluginManager = new PluginManager() diff --git a/web/plugin/activation-manager.ts b/web/plugin/activation-manager.ts deleted file mode 100644 index f1fd1d2880..0000000000 --- a/web/plugin/activation-manager.ts +++ /dev/null @@ -1,92 +0,0 @@ -import Activation from './Activation' -import Plugin from './Plugin' -/** - * This object contains a register of plugin registrations to an activation points, and the means to work with them. - * @namespace activationPoints - */ - -/** - * @constant {Array.} activationRegister - * @private - * Store of activations used by the consumer - */ -const activationRegister: any[] = [] - -/** - * Register a plugin with its activation points (as defined in its manifest). - * @param {Plugin} plugin plugin object as provided by the main process. - * @returns {void} - * @alias activationPoints.register - */ -export function register(plugin: Plugin) { - if (!Array.isArray(plugin.activationPoints)) - throw new Error( - `Plugin ${ - plugin.name || 'without name' - } does not have any activation points set up in its manifest.` - ) - for (const ap of plugin.activationPoints) { - // Ensure plugin is not already registered to activation point - const duplicate = activationRegister.findIndex( - (act) => act.plugin === plugin.name && act.activationPoint === ap - ) - - // Create new activation and add it to the register - if (duplicate < 0 && plugin.name && plugin.url) - activationRegister.push(new Activation(plugin.name, ap, plugin.url)) - } -} - -/** - * Trigger all activations registered to the given activation point. See {@link Plugin}. - * This will call the function with the same name as the activation point on the path specified in the plugin. - * @param {string} activationPoint Name of the activation to trigger - * @returns {Promise.} Resolves to true when the activations are complete. - * @alias activationPoints.trigger - */ -export async function trigger(activationPoint: string) { - // Make sure all triggers are complete before returning - await Promise.all( - // Trigger each relevant activation point from the register and return an array of trigger promises - activationRegister.reduce((triggered, act) => { - if (act.activationPoint === activationPoint) { - triggered.push(act.trigger()) - } - return triggered - }, []) - ) - return true -} - -/** - * Remove a plugin from the activations register. - * @param {string} plugin Name of the plugin to remove. - * @returns {void} - * @alias activationPoints.remove - */ -export function remove(plugin: any) { - let i = activationRegister.length - while (i--) { - if (activationRegister[i].plugin === plugin) { - activationRegister.splice(i, 1) - } - } -} - -/** - * Remove all activations from the activation register. - * @returns {void} - * @alias activationPoints.clear - */ -export function clear() { - activationRegister.length = 0 -} - -/** - * Fetch all activations. - * @returns {Array.} Found extension points - * @alias activationPoints.get - */ -export function get() { - return [...activationRegister] -} diff --git a/web/plugin/extension-manager.ts b/web/plugin/extension-manager.ts deleted file mode 100644 index 790840f01c..0000000000 --- a/web/plugin/extension-manager.ts +++ /dev/null @@ -1,100 +0,0 @@ -/** - * This object contains a register of {@link ExtensionPoint|extension points} and the means to work with them. - * @namespace extensionPoints - */ - -import ExtensionPoint from './ExtensionPoint' - -/** - * @constant {Object.} extensionPoints - * @private - * Register of extension points created by the consumer - */ -const _extensionPoints: Record = {} - -/** - * Create new extension point and add it to the registry. - * @param {string} name Name of the extension point. - * @returns {void} - * @alias extensionPoints.add - */ -export function add(name: string) { - _extensionPoints[name] = new ExtensionPoint(name) -} - -/** - * Remove an extension point from the registry. - * @param {string} name Name of the extension point - * @returns {void} - * @alias extensionPoints.remove - */ -export function remove(name: string) { - delete _extensionPoints[name] -} - -/** - * Create extension point if it does not exist and then register the given extension to it. - * @param {string} ep Name of the extension point. - * @param {string} extension Unique name for the extension. - * @param {Object|Callback} response Object to be returned or function to be called by the extension point. - * @param {number} [priority=0] Order priority for execution used for executing in serial. - * @returns {void} - * @alias extensionPoints.register - */ -export function register(ep: string, extension: string, response: any, priority: number) { - if (!_extensionPoints[ep]) add(ep) - if (_extensionPoints[ep].register) { - _extensionPoints[ep].register(extension, response, priority) - } -} - -/** - * Remove extensions matching regular expression from all extension points. - * @param {RegExp} name Matcher for the name of the extension to remove. - * @alias extensionPoints.unregisterAll - */ -export function unregisterAll(name: string) { - for (const ep in _extensionPoints) _extensionPoints[ep].unregister(name) -} - -/** - * Fetch extension point by name. or all extension points if no name is given. - * @param {string} [ep] Extension point to return - * @returns {Object. | ExtensionPoint} Found extension points - * @alias extensionPoints.get - */ -export function get(ep?: string) { - return ep ? _extensionPoints[ep] : { ..._extensionPoints } -} - -/** - * Call all the extensions registered to an extension point synchronously. See execute on {@link ExtensionPoint}. - * Call this at the point in the base code where you want it to be extended. - * @param {string} name Name of the extension point to call - * @param {*} [input] Parameter to provide to the extensions if they are a function - * @returns {Array} Result of Promise.all or Promise.allSettled depending on exitOnError - * @alias extensionPoints.execute - */ -export function execute(name: string, input: any) { - if (!_extensionPoints[name] || !_extensionPoints[name].execute) - throw new Error( - `The extension point "${name}" is not a valid extension point` - ) - return _extensionPoints[name].execute(input) -} - -/** - * Calls all the extensions registered to the extension point in serial. See executeSerial on {@link ExtensionPoint} - * Call this at the point in the base code where you want it to be extended. - * @param {string} name Name of the extension point to call - * @param {*} [input] Parameter to provide to the extensions if they are a function - * @returns {Promise.<*>} Result of the last extension that was called - * @alias extensionPoints.executeSerial - */ -export function executeSerial(name: string, input: any) { - if (!_extensionPoints[name] || !_extensionPoints[name].executeSerial) - throw new Error( - `The extension point "${name}" is not a valid extension point` - ) - return _extensionPoints[name].executeSerial(input) -} diff --git a/web/plugin/facade.ts b/web/plugin/facade.ts deleted file mode 100644 index 1ccdb428ce..0000000000 --- a/web/plugin/facade.ts +++ /dev/null @@ -1,163 +0,0 @@ -/** - * Helper functions to access the plugin management in the main process. - * Note that the facade needs to be imported separately as "pluggable-electron/facade" as described above. - * It is then available on the global window object as describe in the {@link https://www.electronjs.org/docs/api/context-bridge|Electron documentation} - * @namespace plugins - */ - -import Plugin from './Plugin' -import { register } from './activation-manager' - -/** - * @typedef {Object.} installOptions The {@link https://www.npmjs.com/package/pacote|pacote options} - * used to install the plugin. - * @param {string} specifier the NPM specifier that identifies the package. - * @param {boolean} [activate=true] Whether this plugin should be activated after installation. - */ - -/** - * Install a new plugin. - * @param {Array.} plugins A list of NPM specifiers, or installation configuration objects. - * @returns {Promise. | false>} plugin as defined by the main process. Has property cancelled set to true if installation was cancelled in the main process. - * @alias plugins.install - */ -export async function install(plugins: any[]) { - if (typeof window === 'undefined') { - return - } - // eslint-disable-next-line no-undef - const plgList = await window.pluggableElectronIpc.install(plugins) - if (plgList.cancelled) return false - return plgList.map((plg: any) => { - const plugin = new Plugin( - plg.name, - plg.url, - plg.activationPoints, - plg.active - ) - register(plugin) - return plugin - }) -} - -/** - * Uninstall provided plugins - * @param {Array.} plugins List of names of plugins to uninstall. - * @param {boolean} reload Whether to reload all renderers after updating the plugins. - * @returns {Promise.} Whether uninstalling the plugins was successful. - * @alias plugins.uninstall - */ -export function uninstall(plugins: string[], reload = true) { - if (typeof window === 'undefined') { - return - } - // eslint-disable-next-line no-undef - return window.pluggableElectronIpc.uninstall(plugins, reload) -} - -/** - * Fetch a list of all the active plugins. - * @returns {Promise.>} List of plugins as defined by the main process. - * @alias plugins.getActive - */ -export async function getActive() { - if (typeof window === 'undefined') { - return [] - } - // eslint-disable-next-line no-undef - const plgList = - (await window.pluggableElectronIpc?.getActive()) ?? - (await import( - // eslint-disable-next-line no-undef - /* webpackIgnore: true */ PLUGIN_CATALOG + `?t=${Date.now()}` - ).then((data) => data.default.filter((e: any) => e.supportCloudNative))) - return plgList.map( - (plugin: any) => - new Plugin( - plugin.name, - plugin.url, - plugin.activationPoints, - plugin.active, - plugin.description, - plugin.version, - plugin.icon - ) - ) -} - -/** - * Register all the active plugins. - * @returns {Promise.>} List of plugins as defined by the main process. - * @alias plugins.registerActive - */ -export async function registerActive() { - if (typeof window === 'undefined') { - return - } - // eslint-disable-next-line no-undef - const plgList = await getActive() - plgList.forEach((plugin: Plugin) => - register( - new Plugin( - plugin.name, - plugin.url, - plugin.activationPoints, - plugin.active - ) - ) - ) -} - -/** - * Update provided plugins to its latest version. - * @param {Array.} plugins List of plugins to update by name. - * @param {boolean} reload Whether to reload all renderers after updating the plugins. - * @returns {Promise.>} Updated plugin as defined by the main process. - * @alias plugins.update - */ -export async function update(plugins: Plugin[], reload = true) { - if (typeof window === 'undefined') { - return - } - // eslint-disable-next-line no-undef - const plgList = await window.pluggableElectronIpc.update(plugins, reload) - return plgList.map( - (plugin: any) => - new Plugin( - plugin.name, - plugin.url, - plugin.activationPoints, - plugin.active - ) - ) -} - -/** - * Check if an update is available for provided plugins. - * @param {Array.} plugin List of plugin names to check for available updates. - * @returns {Object.} Object with plugins as keys and new version if update is available or false as values. - * @alias plugins.updatesAvailable - */ -export function updatesAvailable(plugin: Plugin) { - if (typeof window === 'undefined') { - return - } - // eslint-disable-next-line no-undef - return window.pluggableElectronIpc.updatesAvailable(plugin) -} - -/** - * Toggle a plugin's active state. This determines if a plugin should be loaded in initialisation. - * @param {String} plugin Plugin to toggle. - * @param {boolean} active Whether plugin should be activated (true) or deactivated (false). - * @returns {Promise.} Updated plugin as defined by the main process. - * @alias plugins.toggleActive - */ -export async function toggleActive(plugin: Plugin, active: boolean) { - if (typeof window === 'undefined') { - return - } - // eslint-disable-next-line no-undef - const plg = await window.pluggableElectronIpc.toggleActive(plugin, active) - return new Plugin(plg.name, plg.url, plg.activationPoints, plg.active) -} diff --git a/web/plugin/import-manager.ts b/web/plugin/import-manager.ts deleted file mode 100644 index b9b9fb9e48..0000000000 --- a/web/plugin/import-manager.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { - get as getEPs, - register, - execute, - executeSerial, -} from "./extension-manager"; -/** - * Used to import a plugin entry point. - * Ensure your bundler does no try to resolve this import as the plugins are not known at build time. - * @callback importer - * @param {string} entryPoint File to be imported. - * @returns {module} The module containing the entry point function. - */ - -/** - * @private - * @type {importer} - */ -export let importer: any; - -/** - * @private - * Set the plugin importer function. - * @param {importer} callback Callback to import plugins. - */ -export function setImporter(callback: any) { - importer = callback; -} - -/** - * @private - * @type {Boolean|null} - */ -export let presetEPs: boolean|null; - -/** - * @private - * Define how extension points are accessed. - * @param {Boolean|null} peps Whether extension points are predefined. - */ -export function definePresetEps(peps: boolean|null) { - presetEPs = peps === null || peps === true ? peps : false; -} - -/** - * @private - * Call exported function on imported module. - * @param {string} url @see Activation - * @param {string} exp Export to call - * @param {string} [plugin] @see Activation - */ -export async function callExport(url: string, exp: string, plugin: string) { - if (!importer) throw new Error("Importer callback has not been set"); - - const main = await importer(url); - if (!main || typeof main[exp] !== "function") { - throw new Error( - `Activation point "${exp}" was triggered but does not exist on ${ - plugin ? "plugin " + plugin : "unknown plugin" - }` - ); - } - const activate = main[exp]; - switch (presetEPs) { - case true: - activate(getEPs()); - break; - - case null: - activate(); - break; - - default: - activate({ register, execute, executeSerial }); - break; - } -} diff --git a/web/plugin/index.ts b/web/plugin/index.ts deleted file mode 100644 index 49aede97a9..0000000000 --- a/web/plugin/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { definePresetEps, setImporter } from "./import-manager"; - -export * as extensionPoints from "./extension-manager"; -export * as activationPoints from "./activation-manager"; -export * as plugins from "./facade"; -export { default as ExtensionPoint } from "./ExtensionPoint"; - -// eslint-disable-next-line no-undef -if (typeof window !== "undefined" && !window.pluggableElectronIpc) - console.warn( - "Facade is not registered in preload. Facade functions will throw an error if used." - ); - -/** - * Set the renderer options for Pluggable Electron. Should be called before any other Pluggable Electron function in the renderer - * @param {Object} options - * @param {importer} options.importer The callback function used to import the plugin entry points. - * @param {Boolean|null} [options.presetEPs=false] Whether the Extension Points have been predefined (true), - * can be created on the fly(false) or should not be provided through the input at all (null). - * @returns {void} - */ -export function setup(options: any) { - setImporter(options.importer); - definePresetEps(options.presetEPs); -} diff --git a/web/screens/ExploreModels/ExploreModelList/index.tsx b/web/screens/ExploreModels/ExploreModelList/index.tsx index 0f5f08da83..95d084df9a 100644 --- a/web/screens/ExploreModels/ExploreModelList/index.tsx +++ b/web/screens/ExploreModels/ExploreModelList/index.tsx @@ -1,20 +1,13 @@ import React, { useEffect } from 'react' import ExploreModelItem from '@/_components/ExploreModelItem' -import { getConfiguredModels } from '@hooks/useGetDownloadedModels' import useGetConfiguredModels from '@hooks/useGetConfiguredModels' const ExploreModelList: React.FC = () => { - const { loading, models } = useGetConfiguredModels() - - useEffect(() => { - getConfiguredModels() - }, []) + const { models } = useGetConfiguredModels() return (
- {models?.map((item) => ( - - ))} + {models?.map((item) => )}
) } diff --git a/web/screens/ExploreModels/index.tsx b/web/screens/ExploreModels/index.tsx index 6b7f6286ba..634cc63788 100644 --- a/web/screens/ExploreModels/index.tsx +++ b/web/screens/ExploreModels/index.tsx @@ -1,15 +1,10 @@ import React, { Fragment, useEffect } from 'react' import ExploreModelList from '@screens/ExploreModels/ExploreModelList' import useGetConfiguredModels from '@hooks/useGetConfiguredModels' -import { getConfiguredModels } from '@hooks/useGetDownloadedModels' import Loader from '@containers/Loader' const ExploreModelsScreen = () => { - const { loading, models } = useGetConfiguredModels() - - useEffect(() => { - getConfiguredModels() - }, []) + const { loading } = useGetConfiguredModels() return (
diff --git a/web/screens/MyModels/index.tsx b/web/screens/MyModels/index.tsx index dddb5ad850..984f149877 100644 --- a/web/screens/MyModels/index.tsx +++ b/web/screens/MyModels/index.tsx @@ -3,8 +3,6 @@ import { useSetAtom } from 'jotai' import { Button } from '@uikit' import { modelDownloadStateAtom } from '@helpers/atoms/DownloadState.atom' import DownloadedModelTable from '@/_components/DownloadedModelTable' -import ActiveModelTable from '@/_components/ActiveModelTable' -import DownloadingModelTable from '@/_components/DownloadingModelTable' import { useAtomValue } from 'jotai' import { useGetDownloadedModels } from '@hooks/useGetDownloadedModels' import { formatDownloadPercentage } from '@utils/converter' diff --git a/web/screens/Settings/CorePlugins/PluginsCatalog.tsx b/web/screens/Settings/CorePlugins/PluginsCatalog.tsx index a6ba4f0f6d..b566c3f6a9 100644 --- a/web/screens/Settings/CorePlugins/PluginsCatalog.tsx +++ b/web/screens/Settings/CorePlugins/PluginsCatalog.tsx @@ -5,9 +5,9 @@ import { Button, Switch } from '@uikit' import Loader from '@containers/Loader' import { formatPluginsName } from '@utils/converter' -import { plugins, extensionPoints } from '@plugin' import useGetAppVersion from '@hooks/useGetAppVersion' import { FeatureToggleContext } from '@helpers/FeatureToggleWrapper' +import { pluginManager } from '@plugin/PluginManager' const PluginCatalog = () => { const [activePlugins, setActivePlugins] = useState([]) @@ -44,20 +44,8 @@ const PluginCatalog = () => { */ useEffect(() => { const getActivePlugins = async () => { - const plgs = await plugins.getActive() + const plgs = await pluginManager.getActive() if (Array.isArray(plgs)) setActivePlugins(plgs) - - if (extensionPoints.get('experimentComponent')) { - const components = await Promise.all( - extensionPoints.execute('experimentComponent', {}) - ) - components.forEach((e) => { - if (experimentRef.current) { - // @ts-ignore - experimentRef.current.appendChild(e) - } - }) - } } getActivePlugins() }, []) @@ -73,7 +61,7 @@ const PluginCatalog = () => { // Send the filename of the to be installed plugin // to the main process for installation - const installed = await plugins.install([pluginFile]) + const installed = await pluginManager.install([pluginFile]) if (installed) window.coreAPI?.relaunch() } @@ -85,7 +73,7 @@ const PluginCatalog = () => { const uninstall = async (name: string) => { // Send the filename of the to be uninstalled plugin // to the main process for removal - const res = await plugins.uninstall([name]) + const res = await pluginManager.uninstall([name]) if (res) window.coreAPI?.relaunch() } @@ -97,7 +85,7 @@ const PluginCatalog = () => { const downloadTarball = async (pluginName: string) => { setIsLoading(true) const pluginPath = await window.coreAPI?.installRemotePlugin(pluginName) - const installed = await plugins.install([pluginPath]) + const installed = await pluginManager.install([pluginPath]) setIsLoading(false) if (installed) window.coreAPI.relaunch() } diff --git a/web/screens/Settings/CorePlugins/PreferencePlugins/index.tsx b/web/screens/Settings/CorePlugins/PreferencePlugins/index.tsx index 43aba559a2..446b329883 100644 --- a/web/screens/Settings/CorePlugins/PreferencePlugins/index.tsx +++ b/web/screens/Settings/CorePlugins/PreferencePlugins/index.tsx @@ -1,5 +1,4 @@ import React from 'react' -import { execute } from '@plugin/extension-manager' type Props = { pluginName: string @@ -8,26 +7,10 @@ type Props = { } import { formatPluginsName } from '@utils/converter' -import { PluginService, preferences } from '@janhq/core' const PreferencePlugins = (props: Props) => { const { pluginName, preferenceValues, preferenceItems } = props - /** - * Notifies plugins of a preference update by executing the `PluginService.OnPreferencesUpdate` event. - * If a timeout is already set, it is cleared before setting a new timeout to execute the event. - */ - let timeout: any | undefined = undefined - function notifyPreferenceUpdate() { - if (timeout) { - clearTimeout(timeout) - } - timeout = setTimeout( - () => execute(PluginService.OnPreferencesUpdate, {}), - 100 - ) - } - return (
@@ -53,11 +36,7 @@ const PreferencePlugins = (props: Props) => { (v: any) => v.key === e.preferenceKey )[0]?.value } - onChange={(event) => { - preferences - .set(e.pluginName, e.preferenceKey, event.target.value) - .then(() => notifyPreferenceUpdate()) - }} + onChange={(event) => {}} >
diff --git a/web/screens/Settings/index.tsx b/web/screens/Settings/index.tsx index eb800ca192..cc852a6a5e 100644 --- a/web/screens/Settings/index.tsx +++ b/web/screens/Settings/index.tsx @@ -8,12 +8,10 @@ import AppearanceOptions from './Appearance' import PluginCatalog from './CorePlugins/PluginsCatalog' import PreferencePlugins from './CorePlugins/PreferencePlugins' -import { preferences } from '@janhq/core' import { twMerge } from 'tailwind-merge' import { formatPluginsName } from '@utils/converter' -import { extensionPoints } from '@plugin' import Advanced from './Advanced' const SettingsScreen = () => { @@ -41,21 +39,17 @@ const SettingsScreen = () => { */ useEffect(() => { const getActivePluginPreferences = async () => { - if (extensionPoints.get('PluginPreferences')) { - const data = await Promise.all( - extensionPoints.execute('PluginPreferences', {}) - ) - setPreferenceItems(Array.isArray(data) ? data : []) - Promise.all( - (Array.isArray(data) ? data : []).map((e) => - preferences - .get(e.pluginName, e.preferenceKey) - .then((k) => ({ key: e.preferenceKey, value: k })) - ) - ).then((data) => { - setPreferenceValues(data) - }) - } + // setPreferenceItems(Array.isArray(data) ? data : []) + // TODO: Add back with new preferences mechanism + // Promise.all( + // (Array.isArray(data) ? data : []).map((e) => + // preferences + // .get(e.pluginName, e.preferenceKey) + // .then((k) => ({ key: e.preferenceKey, value: k })) + // ) + // ).then((data) => { + // setPreferenceValues(data) + // }) } getActivePluginPreferences() }, []) diff --git a/web/services/coreService.ts b/web/services/coreService.ts index 7310c3df81..90b2d51830 100644 --- a/web/services/coreService.ts +++ b/web/services/coreService.ts @@ -1,6 +1,5 @@ -import { store } from "./storeService"; -import { EventEmitter } from "./eventsService"; -import * as cn from "./cloudNativeService" +import { EventEmitter } from './eventsService' +import * as cn from './cloudNativeService' export const setupCoreServices = () => { if (typeof window === 'undefined') { console.log('undefine', window) @@ -10,16 +9,15 @@ export const setupCoreServices = () => { } if (!window.corePlugin) { window.corePlugin = { - store, events: new EventEmitter(), - }; - window.coreAPI = {}; + } + window.coreAPI = {} window.coreAPI = window.electronAPI ?? { invokePluginFunc: cn.invokePluginFunc, downloadFile: cn.downloadFile, deleteFile: cn.deleteFile, appVersion: cn.appVersion, - openExternalUrl: cn.openExternalUrl - }; + openExternalUrl: cn.openExternalUrl, + } } -}; +} diff --git a/web/services/pluginService.ts b/web/services/pluginService.ts index 8250776a66..06e3c9db7d 100644 --- a/web/services/pluginService.ts +++ b/web/services/pluginService.ts @@ -1,23 +1,13 @@ 'use client' -import { - extensionPoints, - plugins, -} from '@plugin' -import { - CoreService, - InferenceService, - ModelManagementService, - StoreService, -} from '@janhq/core' +import { PluginType } from '@janhq/core' +import { pluginManager } from '@plugin/PluginManager' export const isCorePluginInstalled = () => { - if (!extensionPoints.get(StoreService.CreateCollection)) { + if (!pluginManager.get(PluginType.Conversational)) { return false } - if (!extensionPoints.get(InferenceService.InitModel)) { - return false - } - if (!extensionPoints.get(ModelManagementService.DownloadModel)) { + if (!pluginManager.get(PluginType.Inference)) return false + if (!pluginManager.get(PluginType.Model)) { return false } return true @@ -32,29 +22,13 @@ export const setupBasePlugins = async () => { const basePlugins = await window.electronAPI.basePlugins() if ( - !extensionPoints.get(StoreService.CreateCollection) || - !extensionPoints.get(InferenceService.InitModel) || - !extensionPoints.get(ModelManagementService.DownloadModel) + !pluginManager.get(PluginType.Conversational) || + !pluginManager.get(PluginType.Inference) || + !pluginManager.get(PluginType.Model) ) { - const installed = await plugins.install(basePlugins) + const installed = await pluginManager.install(basePlugins) if (installed) { window.location.reload() } } } - -export const execute = (name: CoreService, args?: any) => { - if (!extensionPoints.get(name)) { - // alert('Missing extension for function: ' + name) - return undefined - } - return extensionPoints.execute(name, args) -} - -export const executeSerial = (name: CoreService, args?: any) => { - if (!extensionPoints.get(name)) { - // alert('Missing extension for function: ' + name) - return Promise.resolve(undefined) - } - return extensionPoints.executeSerial(name, args) -} diff --git a/web/services/storeService.ts b/web/services/storeService.ts deleted file mode 100644 index c70c3177fc..0000000000 --- a/web/services/storeService.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { StoreService } from '@janhq/core' -import { executeSerial } from './pluginService' - -/** - * Create a collection on data store - * - * @param name name of the collection to create - * @param schema schema of the collection to create, include fields and their types, optional for relational database engine - * @returns {Promise} - * - */ -function createCollection( - name: string, - schema?: { [key: string]: any } -): Promise { - return executeSerial(StoreService.CreateCollection, { name, schema }) -} - -/** - * Delete a collection - * - * @param name name of the collection to delete - * @returns {Promise} - * - */ -function deleteCollection(name: string): Promise { - return executeSerial(StoreService.DeleteCollection, name) -} - -/** - * Insert a value to a collection - * - * @param collectionName name of the collection - * @param value value to insert - * @returns {Promise} - * - */ -function insertOne(collectionName: string, value: any): Promise { - return executeSerial(StoreService.InsertOne, { - collectionName, - value, - }) -} - -/** - * Retrieve a record from a collection in the data store. - * @param {string} collectionName - The name of the collection containing the record to retrieve. - * @param {string} key - The key of the record to retrieve. - * @returns {Promise} A promise that resolves when the record is retrieved. - */ -function findOne(collectionName: string, key: string): Promise { - return executeSerial(StoreService.FindOne, { collectionName, key }) -} - -/** - * Retrieves all records that match a selector in a collection in the data store. - * @param collectionName - The name of the collection to retrieve. - * @param selector - The selector to use to get records from the collection. - * @param sort - The sort options to use to retrieve records. - * @returns {Promise} - */ -function findMany( - collectionName: string, - selector?: { [key: string]: any }, - sort?: [{ [key: string]: any }] -): Promise { - return executeSerial(StoreService.FindMany, { - collectionName, - selector, - sort, - }) -} - -/** - * Update value of a collection's record - * - * @param collectionName name of the collection - * @param key key of the record to update - * @param value value to update - * @returns Promise - * - */ -function updateOne( - collectionName: string, - key: string, - value: any -): Promise { - return executeSerial(StoreService.UpdateOne, { - collectionName, - key, - value, - }) -} - -/** - * Updates all records that match a selector in a collection in the data store. - * @param collectionName - The name of the collection containing the records to update. - * @param selector - The selector to use to get the records to update. - * @param value - The new value for the records. - * @returns {Promise} A promise that resolves when the records are updated. - */ -function updateMany( - collectionName: string, - value: any, - selector?: { [key: string]: any } -): Promise { - return executeSerial(StoreService.UpdateMany, { - collectionName, - value, - selector, - }) -} - -/** - * Delete a collection's record - * - * @param collectionName name of the collection - * @param key key of the record to delete - * @returns Promise - * - */ -function deleteOne(collectionName: string, key: string): Promise { - return executeSerial(StoreService.DeleteOne, { collectionName, key }) -} - -/** - * Deletes all records with a matching key from a collection in the data store. - * @param {string} collectionName - The name of the collection to delete the records from. - * @param {{ [key: string]: any }} selector - The selector to use to get the records to delete. - * @returns {Promise} A promise that resolves when the records are deleted. - */ -function deleteMany( - collectionName: string, - selector?: { [key: string]: any } -): Promise { - return executeSerial(StoreService.DeleteMany, { - collectionName, - selector, - }) -} - -export const store = { - createCollection, - deleteCollection, - insertOne, - updateOne, - updateMany, - deleteOne, - deleteMany, - findOne, - findMany, -} diff --git a/web/tsconfig.json b/web/tsconfig.json index 42170f12a8..bd8b3acc1a 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es5", + "target": "ES2015", "lib": ["dom", "dom.iterable", "esnext"], "typeRoots": ["node_modules/@types", "./src/types"], "allowJs": true, diff --git a/web/types/chatMessage.d.ts b/web/types/chatMessage.d.ts index dff6f05d8c..b1796aef5f 100644 --- a/web/types/chatMessage.d.ts +++ b/web/types/chatMessage.d.ts @@ -40,7 +40,7 @@ interface RawMessage { } interface Conversation { - _id?: string + _id: string modelId?: string name?: string image?: string diff --git a/web/types/models.d.ts b/web/types/models.d.ts deleted file mode 100644 index 09df752ac6..0000000000 --- a/web/types/models.d.ts +++ /dev/null @@ -1,59 +0,0 @@ -type AssistantModel = { - /** - * Combination of owner and model name. - * Being used as file name. MUST be unique. - */ - _id: string - name: string - quantMethod: string - bits: number - size: number - maxRamRequired: number - usecase: string - downloadLink: string - /** - * For tracking download info - */ - startDownloadAt?: number - finishDownloadAt?: number - productId: string - productName: string - shortDescription: string - longDescription: string - avatarUrl: string - author: string - version: string - modelUrl: string - nsfw: boolean - greeting: string - type: ProductType - createdAt: number - updatedAt?: number - status: string - releaseDate: number - tags: string[] -} - -/** - * Model type which will be stored in the database - */ -type ModelVersion = { - /** - * Combination of owner and model name. - * Being used as file name. Should be unique. - */ - _id: string - name: string - quantMethod: string - bits: number - size: number - maxRamRequired: number - usecase: string - downloadLink: string - productId: string - /** - * For tracking download state - */ - startDownloadAt?: number - finishDownloadAt?: number -} diff --git a/web/types/products.d.ts b/web/types/products.d.ts deleted file mode 100644 index 31054821b1..0000000000 --- a/web/types/products.d.ts +++ /dev/null @@ -1,58 +0,0 @@ -type ItemProperties = { - name: string - type: string - items?: ProductBodyItem[] - example?: unknown - description?: string -} - -type ProductInput = { - body: ItemProperties[] - slug: string - headers: ProductHeader -} - -interface ProductOutput { - slug: string - type: string - properties: ItemProperties[] - description: string -} - -type ProductHeader = { - accept: string - contentType: string -} - -type ProductBodyItem = { - type: string - properties: ItemProperties[] -} - -enum ProductType { - LLM = 'LLM', - GenerativeArt = 'GenerativeArt', - ControlNet = 'ControlNet', -} - -interface Product { - _id: string - name: string - shortDescription: string - avatarUrl: string - longDescription: string - author: string - version: string - modelUrl: string - nsfw: boolean - greeting: string - type: ProductType - inputs?: ProductInput - outputs?: ProductOutput - createdAt: number - updatedAt?: number - status: string - releaseDate: number - tags: string[] - availableVersions: ModelVersion[] -} diff --git a/web/utils/dummy.ts b/web/utils/dummy.ts new file mode 100644 index 0000000000..de0e2eb449 --- /dev/null +++ b/web/utils/dummy.ts @@ -0,0 +1,30 @@ +import { ModelCatalog, ModelVersion } from '@janhq/core/lib/types' + +export const dummyModel: ModelCatalog = { + _id: 'aladar/TinyLLama-v0-GGUF', + name: 'TinyLLama-v0-GGUF', + shortDescription: 'TinyLlama-1.1B-Chat-v0.3-GGUF', + longDescription: 'https://huggingface.co/aladar/TinyLLama-v0-GGUF/tree/main', + avatarUrl: '', + status: '', + releaseDate: Date.now(), + author: 'aladar', + version: '1.0.0', + modelUrl: 'aladar/TinyLLama-v0-GGUF', + tags: ['freeform', 'tags'], + createdAt: 0, + availableVersions: [ + { + _id: 'tinyllama-1.1b-chat-v0.3.Q2_K.gguf', + name: 'tinyllama-1.1b-chat-v0.3.Q2_K.gguf', + quantMethod: '', + bits: 2, + size: 19660000, + maxRamRequired: 256000000, + usecase: + 'smallest, significant quality loss - not recommended for most purposes', + downloadLink: + 'https://huggingface.co/aladar/TinyLLama-v0-GGUF/resolve/main/TinyLLama-v0.f32.gguf', + } as ModelVersion, + ], +}