255: Cloud native

This commit is contained in:
John 2023-10-16 12:34:39 +07:00
parent 8b10aa2c78
commit 6be67895dd
24 changed files with 528 additions and 45 deletions

1
.dockerignore Normal file
View File

@ -0,0 +1 @@
**/node_modules

39
Dockerfile Normal file
View File

@ -0,0 +1,39 @@
FROM node:20-bullseye AS base
# 1. Install dependencies only when needed
FROM base AS deps
WORKDIR /app
# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN yarn install
# # 2. Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# This will do the trick, use the corresponding env file for each environment.
RUN yarn workspace server install
RUN yarn server:prod
# 3. Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
# RUN addgroup -g 1001 -S nodejs;
COPY --from=builder /app/server/build ./
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder /app/server/node_modules ./node_modules
COPY --from=builder /app/server/package.json ./package.json
EXPOSE 4000 3928
ENV PORT 4000
ENV APPDATA /app/data
CMD ["node", "main.js"]

View File

@ -7,6 +7,7 @@
import Plugin from "./Plugin"; import Plugin from "./Plugin";
import { register } from "./activation-manager"; import { register } from "./activation-manager";
import plugins from "../../../../web/public/plugins/plugin.json"
/** /**
* @typedef {Object.<string, any>} installOptions The {@link https://www.npmjs.com/package/pacote|pacote options} * @typedef {Object.<string, any>} installOptions The {@link https://www.npmjs.com/package/pacote|pacote options}
@ -65,7 +66,7 @@ export async function getActive() {
return; return;
} }
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
const plgList = await window.pluggableElectronIpc.getActive(); const plgList = await window.pluggableElectronIpc?.getActive() ?? plugins;
return plgList.map( return plgList.map(
(plugin) => (plugin) =>
new Plugin( new Plugin(
@ -90,7 +91,7 @@ export async function registerActive() {
return; return;
} }
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
const plgList = await window.pluggableElectronIpc.getActive(); const plgList = await getActive()
plgList.forEach((plugin) => plgList.forEach((plugin) =>
register( register(
new Plugin( new Plugin(

View File

@ -4,13 +4,16 @@
"workspaces": { "workspaces": {
"packages": [ "packages": [
"electron", "electron",
"web" "web",
"server"
], ],
"nohoist": [ "nohoist": [
"electron", "electron",
"electron/**", "electron/**",
"web", "web",
"web/**" "web/**",
"server",
"server/**"
] ]
}, },
"scripts": { "scripts": {
@ -32,7 +35,10 @@
"build:publish": "yarn build:web && yarn workspace jan build:publish", "build:publish": "yarn build:web && yarn workspace jan build:publish",
"build:publish-darwin": "yarn build:web && yarn workspace jan build:publish-darwin", "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-win32": "yarn build:web && yarn workspace jan build:publish-win32",
"build:publish-linux": "yarn build:web && yarn workspace jan build:publish-linux" "build:publish-linux": "yarn build:web && yarn workspace jan build:publish-linux",
"build:web-plugins": "yarn build:web && yarn build:plugins && 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\"",
"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": { "devDependencies": {
"concurrently": "^8.2.1", "concurrently": "^8.2.1",

View File

@ -16,7 +16,7 @@ const dbs: Record<string, any> = {};
*/ */
function createCollection(name: string, schema?: { [key: string]: any }): Promise<void> { function createCollection(name: string, schema?: { [key: string]: any }): Promise<void> {
return new Promise<void>((resolve) => { return new Promise<void>((resolve) => {
const dbPath = path.join(app.getPath("userData"), "databases"); const dbPath = path.join(appPath(), "databases");
if (!fs.existsSync(dbPath)) fs.mkdirSync(dbPath); if (!fs.existsSync(dbPath)) fs.mkdirSync(dbPath);
const db = new PouchDB(`${path.join(dbPath, name)}`); const db = new PouchDB(`${path.join(dbPath, name)}`);
dbs[name] = db; dbs[name] = db;
@ -226,6 +226,13 @@ function findMany(
.then((data) => data.docs); // Return documents .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 = { module.exports = {
createCollection, createCollection,
deleteCollection, deleteCollection,

View File

@ -23,25 +23,22 @@ const initModel = (fileName) => {
let binaryFolder = path.join(__dirname, "nitro"); // Current directory by default let binaryFolder = path.join(__dirname, "nitro"); // Current directory by default
let binaryName; let binaryName;
if (process.platform === "win32") { if (process.platform === "win32") {
// Todo: Need to check for CUDA support to switch between CUDA and non-CUDA binaries // Todo: Need to check for CUDA support to switch between CUDA and non-CUDA binaries
binaryName = "nitro_start_windows.bat"; binaryName = "nitro_start_windows.bat";
} else if (process.platform === "darwin") { } else if (process.platform === "darwin") {
// Mac OS platform // Mac OS platform
binaryName = binaryName = process.arch === "arm64" ? "nitro_mac_arm64" : "nitro_mac_intel";
process.arch === "arm64" } else {
? "nitro_mac_arm64" // Linux
: "nitro_mac_intel"; // Todo: Need to check for CUDA support to switch between CUDA and non-CUDA binaries
} else { binaryName = "nitro_start_linux.sh"; // For other platforms
// Linux }
// Todo: Need to check for CUDA support to switch between CUDA and non-CUDA binaries
binaryName = "nitro_start_linux.sh"; // For other platforms
}
const binaryPath = path.join(binaryFolder, binaryName); const binaryPath = path.join(binaryFolder, binaryName);
// Execute the binary // Execute the binary
subprocess = spawn(binaryPath, { cwd: binaryFolder }); subprocess = spawn(binaryPath,["0.0.0.0", PORT], { cwd: binaryFolder });
// Handle subprocess output // Handle subprocess output
subprocess.stdout.on("data", (data) => { subprocess.stdout.on("data", (data) => {
@ -61,7 +58,7 @@ const initModel = (fileName) => {
}) })
.then(() => tcpPortUsed.waitUntilUsed(PORT, 300, 30000)) .then(() => tcpPortUsed.waitUntilUsed(PORT, 300, 30000))
.then(() => { .then(() => {
const llama_model_path = path.join(app.getPath("userData"), fileName); const llama_model_path = path.join(appPath(), fileName);
const config = { const config = {
llama_model_path, llama_model_path,
@ -107,6 +104,13 @@ function killSubprocess() {
} }
} }
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 = { module.exports = {
initModel, initModel,
killSubprocess, killSubprocess,

View File

@ -3,4 +3,4 @@
#!/bin/bash #!/bin/bash
# Attempt to run the nitro_linux_amd64_cuda file and if it fails, run nitro_linux_amd64 # Attempt to run the nitro_linux_amd64_cuda file and if it fails, run nitro_linux_amd64
./nitro_linux_amd64_cuda || (echo "nitro_linux_amd64_cuda encountered an error, attempting to run nitro_linux_amd64..." && ./nitro_linux_amd64) ./nitro_linux_amd64_cuda "$@" || (echo "nitro_linux_amd64_cuda encountered an error, attempting to run nitro_linux_amd64..." && ./nitro_linux_amd64 "$@")

View File

@ -5,11 +5,56 @@ import {
downloadFile, downloadFile,
deleteFile, deleteFile,
store, store,
EventName,
events
} from "@janhq/core"; } from "@janhq/core";
import { parseToModel } from "./helper"; import { parseToModel } from "./helper";
const downloadModel = (product) => const downloadModel = (product) => {
downloadFile(product.downloadUrl, product.fileName); 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<string> {
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); const deleteModel = (path) => deleteFile(path);
@ -87,6 +132,7 @@ function getModelById(modelId: string): Promise<any> {
function onStart() { function onStart() {
store.createCollection("models", {}); store.createCollection("models", {});
fetchDownloadProgress(null, null).then((fileName: string) => fileName && checkDownloadProgress(fileName));
} }
// Register all the above functions and objects with the relevant extension points // Register all the above functions and objects with the relevant extension points

179
server/main.ts Normal file
View File

@ -0,0 +1,179 @@
import express, { Express, Request, Response, NextFunction } from 'express'
import cors from "cors";
import { resolve } from "path";
const fs = require("fs");
const progress = require("request-progress");
const path = require("path");
const request = require("request");
// Create app dir
const userDataPath = appPath();
if (!fs.existsSync(userDataPath)) fs.mkdirSync(userDataPath);
interface ProgressState {
percent?: number;
speed?: number;
size?: {
total: number;
transferred: number;
};
time?: {
elapsed: number;
remaining: number;
};
success?: boolean | undefined;
fileName: string;
}
const options: cors.CorsOptions = { origin: "*" };
const requiredModules: Record<string, any> = {};
const port = process.env.PORT || 4000;
const dataDir = __dirname;
type DownloadProgress = Record<string, ProgressState>;
const downloadProgress: DownloadProgress = {};
const app: Express = express()
app.use(express.static(dataDir + '/renderer'))
app.use(cors(options))
app.use(express.json());
/**
* Execute a plugin module function via API call
*
* @param modulePath path to module name to import
* @param method function name to execute. The methods "deleteFile" and "downloadFile" will call the server function {@link deleteFile}, {@link downloadFile} instead of the plugin function.
* @param args arguments to pass to the function
* @returns Promise<any>
*
*/
app.post('/api/v1/invokeFunction', (req: Request, res: Response, next: NextFunction): void => {
const method = req.body["method"];
const args = req.body["args"];
switch (method) {
case "deleteFile":
deleteFile(args).then(() => res.json(Object())).catch((err: any) => next(err));
break;
case "downloadFile":
downloadFile(args.downloadUrl, args.fileName).then(() => res.json(Object())).catch((err: any) => next(err));
break;
default:
const result = invokeFunction(req.body["modulePath"], method, args)
if (typeof result === "undefined") {
res.json(Object())
} else {
result?.then((result: any) => {
res.json(result)
}).catch((err: any) => next(err));
}
}
});
app.post('/api/v1/downloadProgress', (req: Request, res: Response): void => {
const fileName = req.body["fileName"];
if (fileName && downloadProgress[fileName]) {
res.json(downloadProgress[fileName])
return;
} else {
const obj = downloadingFile();
if (obj) {
res.json(obj)
return;
}
}
res.json(Object());
});
app.use((err: Error, req: Request, res: Response, next: NextFunction): void => {
console.error("ErrorHandler", req.url, req.body, err);
res.status(500);
res.json({ error: err?.message ?? "Internal Server Error" })
});
app.listen(port, () => console.log(`Application is running on port ${port}`));
async function invokeFunction(modulePath: string, method: string, args: any): Promise<any> {
console.log(modulePath, method, args);
const module = require(/* webpackIgnore: true */ path.join(
dataDir,
"",
modulePath
));
requiredModules[modulePath] = module;
if (typeof module[method] === "function") {
return module[method](...args);
} else {
return Promise.resolve();
}
}
function downloadModel(downloadUrl: string, fileName: string): void {
const userDataPath = appPath();
const destination = resolve(userDataPath, fileName);
console.log("Download file", fileName, "to", destination);
progress(request(downloadUrl), {})
.on("progress", function (state: any) {
downloadProgress[fileName] = {
...state,
fileName,
success: undefined
};
console.log("downloading file", fileName, (state.percent * 100).toFixed(2) + '%');
})
.on("error", function (err: Error) {
downloadProgress[fileName] = {
...downloadProgress[fileName],
success: false,
fileName: fileName,
};
})
.on("end", function () {
downloadProgress[fileName] = {
success: true,
fileName: fileName,
};
})
.pipe(fs.createWriteStream(destination));
}
function deleteFile(filePath: string): Promise<void> {
const userDataPath = appPath();
const fullPath = resolve(userDataPath, filePath);
return new Promise((resolve, reject) => {
fs.unlink(fullPath, function (err: any) {
if (err && err.code === "ENOENT") {
reject(Error(`File does not exist: ${err}`));
} else if (err) {
reject(Error(`File delete error: ${err}`));
} else {
console.log(`Delete file ${filePath} from ${fullPath}`)
resolve();
}
});
})
}
function downloadingFile(): ProgressState | undefined {
const obj = Object.values(downloadProgress).find(obj => obj && typeof obj.success === "undefined")
return obj
}
async function downloadFile(downloadUrl: string, fileName: string): Promise<void> {
return new Promise((resolve, reject) => {
const obj = downloadingFile();
if (obj) {
reject(Error(obj.fileName + " is being downloaded!"))
return;
};
(async () => {
downloadModel(downloadUrl, fileName);
})().catch(e => {
console.error("downloadModel", fileName, e);
});
resolve();
});
}
function appPath(): string {
return process.env.APPDATA || (process.platform == 'darwin' ? process.env.HOME + '/Library/Preferences' : process.env.HOME + "/.local/share")
}

5
server/nodemon.json Normal file
View File

@ -0,0 +1,5 @@
{
"watch": [
"main.ts"
]
}

26
server/package.json Normal file
View File

@ -0,0 +1,26 @@
{
"name": "server",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"cors": "^2.8.5",
"electron": "^26.2.1",
"express": "^4.18.2",
"request": "^2.88.2",
"request-progress": "^3.0.0"
},
"devDependencies": {
"@types/cors": "^2.8.14",
"@types/express": "^4.17.18",
"@types/node": "^20.8.2",
"nodemon": "^3.0.1",
"ts-node": "^10.9.1",
"typescript": "^5.2.2"
},
"scripts": {
"build": "tsc --project ./",
"dev": "nodemon main.ts",
"prod": "node build/main.js"
}
}

19
server/tsconfig.json Normal file
View File

@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"noImplicitAny": true,
"sourceMap": true,
"strict": true,
"outDir": "./build",
"rootDir": "./",
"noEmitOnError": true,
"baseUrl": ".",
"allowJs": true,
"paths": { "*": ["node_modules/*"] },
"typeRoots": ["node_modules/@types"],
"esModuleInterop": true
},
"include": ["./**/*.ts"],
"exclude": ["core", "build", "dist", "tests"]
}

View File

@ -6,14 +6,14 @@ const SidebarFooter: React.FC = () => (
<SecondaryButton <SecondaryButton
title={'Discord'} title={'Discord'}
onClick={() => onClick={() =>
window.electronAPI?.openExternalUrl('https://discord.gg/AsJ8krTT3N') window.coreAPI?.openExternalUrl('https://discord.gg/AsJ8krTT3N')
} }
className="flex-1" className="flex-1"
/> />
<SecondaryButton <SecondaryButton
title={'Twitter'} title={'Twitter'}
onClick={() => onClick={() =>
window.electronAPI?.openExternalUrl('https://twitter.com/janhq_') window.coreAPI?.openExternalUrl('https://twitter.com/janhq_')
} }
className="flex-1" className="flex-1"
/> />

View File

@ -18,6 +18,9 @@ import React from 'react'
import BaseLayout from '@containers/Layout' import BaseLayout from '@containers/Layout'
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
const Page: React.FC = () => { const Page: React.FC = () => {
const viewState = useAtomValue(getMainViewStateAtom) const viewState = useAtomValue(getMainViewStateAtom)
@ -53,7 +56,10 @@ const Page: React.FC = () => {
break break
} }
return <BaseLayout>{children}</BaseLayout> return <BaseLayout>
{children}
<div><ToastContainer /></div>
</BaseLayout>
} }
export default Page export default Page

View File

@ -57,7 +57,7 @@ const Providers = (props: PropsWithChildren) => {
useEffect(() => { useEffect(() => {
if (setupCore) { if (setupCore) {
// Electron // Electron
if (window && window.electronAPI) { if (window && window.coreAPI) {
setupPE() setupPE()
} else { } else {
// Host // Host

View File

@ -170,7 +170,7 @@ export const SidebarLeft = () => {
<div className="space-y-2 rounded-lg border border-border bg-background/50 p-3"> <div className="space-y-2 rounded-lg border border-border bg-background/50 p-3">
<button <button
onClick={() => onClick={() =>
window.electronAPI?.openExternalUrl( window.coreAPI?.openExternalUrl(
'https://discord.gg/AsJ8krTT3N' 'https://discord.gg/AsJ8krTT3N'
) )
} }
@ -180,7 +180,7 @@ export const SidebarLeft = () => {
</button> </button>
<button <button
onClick={() => onClick={() =>
window.electronAPI?.openExternalUrl( window.coreAPI?.openExternalUrl(
'https://twitter.com/janhq_' 'https://twitter.com/janhq_'
) )
} }

View File

@ -11,6 +11,10 @@ import {
} from './atoms/Conversation.atom' } from './atoms/Conversation.atom'
import { executeSerial } from '../../electron/core/plugin-manager/execution/extension-manager' import { executeSerial } from '../../electron/core/plugin-manager/execution/extension-manager'
import { debounce } from 'lodash' 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";
let currentConversation: Conversation | undefined = undefined let currentConversation: Conversation | undefined = undefined
@ -21,6 +25,7 @@ const debouncedUpdateConversation = debounce(
1000 1000
) )
export default function EventHandler({ children }: { children: ReactNode }) { export default function EventHandler({ children }: { children: ReactNode }) {
const addNewMessage = useSetAtom(addNewMessageAtom) const addNewMessage = useSetAtom(addNewMessageAtom)
const updateMessage = useSetAtom(updateMessageAtom) const updateMessage = useSetAtom(updateMessageAtom)
@ -29,6 +34,9 @@ export default function EventHandler({ children }: { children: ReactNode }) {
const { getConversationById } = useGetUserConversations() const { getConversationById } = useGetUserConversations()
const updateConvWaiting = useSetAtom(updateConversationWaitingForResponseAtom) const updateConvWaiting = useSetAtom(updateConversationWaitingForResponseAtom)
const setDownloadState = useSetAtom(setDownloadStateAtom);
const setDownloadStateSuccess = useSetAtom(setDownloadStateSuccessAtom);
const setDownloadedModels = useSetAtom(downloadedModelAtom);
async function handleNewMessageResponse(message: NewMessageResponse) { async function handleNewMessageResponse(message: NewMessageResponse) {
if (message.conversationId) { if (message.conversationId) {
@ -88,6 +96,22 @@ export default function EventHandler({ children }: { children: ReactNode }) {
updateConvWaiting(messageResponse.conversationId, false) updateConvWaiting(messageResponse.conversationId, false)
} }
function handleDownloadUpdate(state: any) {
if (!state) return;
setDownloadState(state);
}
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);
});
});
}
}
useEffect(() => { useEffect(() => {
if (window.corePlugin.events) { if (window.corePlugin.events) {
events.on(EventName.OnNewMessageResponse, handleNewMessageResponse) events.on(EventName.OnNewMessageResponse, handleNewMessageResponse)
@ -97,6 +121,8 @@ export default function EventHandler({ children }: { children: ReactNode }) {
// EventName.OnMessageResponseFinished, // EventName.OnMessageResponseFinished,
handleMessageResponseFinished handleMessageResponseFinished
) )
events.on(EventName.OnDownloadUpdate, handleDownloadUpdate);
events.on(EventName.OnDownloadSuccess, handleDownloadSuccess);
} }
}, []) }, [])
@ -109,6 +135,8 @@ export default function EventHandler({ children }: { children: ReactNode }) {
// EventName.OnMessageResponseFinished, // EventName.OnMessageResponseFinished,
handleMessageResponseFinished handleMessageResponseFinished
) )
events.off(EventName.OnDownloadUpdate, handleDownloadUpdate);
events.off(EventName.OnDownloadSuccess, handleDownloadSuccess);
} }
}, []) }, [])
return <>{children}</> return <>{children}</>

View File

@ -1,5 +1,7 @@
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const { version } = require('os')
const webpack = require('webpack') const webpack = require('webpack')
const packageJson = require('./package.json')
const nextConfig = { const nextConfig = {
output: 'export', output: 'export',
@ -27,6 +29,7 @@ const nextConfig = {
PLUGIN_CATALOG: JSON.stringify( PLUGIN_CATALOG: JSON.stringify(
'https://cdn.jsdelivr.net/npm/@janhq/plugin-catalog@latest/dist/index.js' 'https://cdn.jsdelivr.net/npm/@janhq/plugin-catalog@latest/dist/index.js'
), ),
VERSION: JSON.stringify(packageJson.version)
}), }),
] ]
return config return config

View File

@ -45,6 +45,7 @@
"react-intersection-observer": "^9.5.2", "react-intersection-observer": "^9.5.2",
"sass": "^1.69.4", "sass": "^1.69.4",
"tailwind-merge": "^1.14.0", "tailwind-merge": "^1.14.0",
"react-toastify": "^9.1.3",
"tailwindcss": "3.3.3", "tailwindcss": "3.3.3",
"typescript": "5.1.6", "typescript": "5.1.6",
"uuid": "^9.0.1" "uuid": "^9.0.1"

View File

@ -0,0 +1,50 @@
[
{
"active": true,
"name": "data-plugin",
"version": "1.0.0",
"activationPoints": [
"init"
],
"main": "dist/index.js",
"description": "Jan Database Plugin efficiently stores conversation and model data using SQLite, providing accessible data management",
"icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/circle-stack.svg",
"url": "/plugins/data-plugin/index.js"
},
{
"active": true,
"name": "inference-plugin",
"version": "1.0.0",
"activationPoints": [
"init"
],
"main": "dist/index.js",
"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",
"url": "/plugins/inference-plugin/index.js"
},
{
"active": true,
"name": "model-management-plugin",
"version": "1.0.0",
"activationPoints": [
"init"
],
"main": "dist/index.js",
"description": "Model Management Plugin leverages the HuggingFace API for model exploration and seamless downloads",
"icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/queue-list.svg",
"url": "/plugins/model-management-plugin/index.js"
},
{
"active": true,
"name": "monitoring-plugin",
"version": "1.0.0",
"activationPoints": [
"init"
],
"main": "dist/bundle.js",
"description": "Utilizing systeminformation, it provides essential System and OS information retrieval",
"icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/cpu-chip.svg",
"url": "/plugins/monitoring-plugin/index.js"
}
]

View File

@ -26,6 +26,9 @@ const PluginCatalog = () => {
* Loads the plugin catalog module from a CDN and sets it as the plugin catalog state. * Loads the plugin catalog module from a CDN and sets it as the plugin catalog state.
*/ */
useEffect(() => { useEffect(() => {
if (!window.electronAPI) {
return;
}
if (!version) return if (!version) return
// Load plugin manifest from plugin if any // Load plugin manifest from plugin if any
@ -36,7 +39,7 @@ const PluginCatalog = () => {
(e: any) => (e: any) =>
!e.requiredVersion || !e.requiredVersion ||
e.requiredVersion.replace(/[.^]/g, '') <= e.requiredVersion.replace(/[.^]/g, '') <=
version.replaceAll('.', '') version.replaceAll('.', '')
) )
) )
}) })
@ -50,7 +53,7 @@ const PluginCatalog = () => {
(e: any) => (e: any) =>
!e.requiredVersion || !e.requiredVersion ||
e.requiredVersion.replace(/[.^]/g, '') <= e.requiredVersion.replace(/[.^]/g, '') <=
version.replaceAll('.', '') version.replaceAll('.', '')
) )
) )
) )

View File

@ -0,0 +1,54 @@
import { toast } from 'react-toastify';
const API_BASE_PATH: string = "/api/v1";
export function openExternalUrl(url: string) {
window?.open(url, '_blank');
}
export async function appVersion() {
return Promise.resolve(VERSION);
}
export function invokePluginFunc(modulePath: string, pluginFunc: string, ...args: any): Promise<any> {
return fetchApi(modulePath, pluginFunc, args).catch((err: Error) => { throw err });
};
export async function downloadFile(downloadUrl: string, fileName: string) {
return fetchApi("", "downloadFile", { downloadUrl: downloadUrl, fileName: fileName }).catch((err: Error) => { throw err });
}
export async function deleteFile(fileName: string) {
return fetchApi("", "deleteFile", fileName).catch((err: Error) => { throw err });
}
export async function fetchApi(modulePath: string, pluginFunc: string, args: any): Promise<any> {
const response = await fetch(API_BASE_PATH + "/invokeFunction", {
method: 'POST',
body: JSON.stringify({ "modulePath": modulePath, "method": pluginFunc, "args": args }),
headers: { 'Content-Type': 'application/json', 'Authorization': '' }
});
if (!response.ok) {
const json = await response.json();
if (json && json.error) {
toast.error(json.error, {
position: "bottom-left",
autoClose: 5000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
});
}
return null;
}
const text = await response.text();
try {
const json = JSON.parse(text)
return Promise.resolve(json);
} catch (err) {
return Promise.resolve(text);
}
}

View File

@ -1,6 +1,6 @@
import { store } from './storeService' import { store } from "./storeService";
import { EventEmitter } from './eventsService' import { EventEmitter } from "./eventsService";
import * as cn from "./cloudNativeService"
export const setupCoreServices = () => { export const setupCoreServices = () => {
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
console.log('undefine', window) console.log('undefine', window)
@ -12,10 +12,14 @@ export const setupCoreServices = () => {
window.corePlugin = { window.corePlugin = {
store, store,
events: new EventEmitter(), events: new EventEmitter(),
} };
window.coreAPI = {};
window.coreAPI = window.electronAPI ?? {
invokePluginFunc: cn.invokePluginFunc,
downloadFile: cn.downloadFile,
deleteFile: cn.deleteFile,
appVersion: cn.appVersion,
openExternalUrl: cn.openExternalUrl
};
} }
if (!window.coreAPI) { };
// fallback electron API
window.coreAPI = window.electronAPI
}
}

View File

@ -2,9 +2,10 @@ export {}
declare global { declare global {
declare const PLUGIN_CATALOG: string declare const PLUGIN_CATALOG: string
declare const VERSION: string
interface Window { interface Window {
electronAPI?: any | undefined electronAPI?: any | undefined;
corePlugin?: any | undefined corePlugin?: any | undefined;
coreAPI?: any | undefined coreAPI?: any | undefined;
} }
} }