255: Cloud native
This commit is contained in:
parent
8b10aa2c78
commit
6be67895dd
1
.dockerignore
Normal file
1
.dockerignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
**/node_modules
|
||||||
39
Dockerfile
Normal file
39
Dockerfile
Normal 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"]
|
||||||
@ -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(
|
||||||
|
|||||||
12
package.json
12
package.json
@ -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",
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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 "$@")
|
||||||
|
|||||||
@ -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
179
server/main.ts
Normal 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
5
server/nodemon.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"watch": [
|
||||||
|
"main.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
26
server/package.json
Normal file
26
server/package.json
Normal 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
19
server/tsconfig.json
Normal 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"]
|
||||||
|
}
|
||||||
@ -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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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_'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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}</>
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
50
web/public/plugins/plugin.json
Normal file
50
web/public/plugins/plugin.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
@ -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('.', '')
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
54
web/services/cloudNativeService.ts
Normal file
54
web/services/cloudNativeService.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
7
web/types/index.d.ts
vendored
7
web/types/index.d.ts
vendored
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user