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 { 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}
|
||||
@ -65,7 +66,7 @@ export async function getActive() {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line no-undef
|
||||
const plgList = await window.pluggableElectronIpc.getActive();
|
||||
const plgList = await window.pluggableElectronIpc?.getActive() ?? plugins;
|
||||
return plgList.map(
|
||||
(plugin) =>
|
||||
new Plugin(
|
||||
@ -90,7 +91,7 @@ export async function registerActive() {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line no-undef
|
||||
const plgList = await window.pluggableElectronIpc.getActive();
|
||||
const plgList = await getActive()
|
||||
plgList.forEach((plugin) =>
|
||||
register(
|
||||
new Plugin(
|
||||
|
||||
12
package.json
12
package.json
@ -4,13 +4,16 @@
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
"electron",
|
||||
"web"
|
||||
"web",
|
||||
"server"
|
||||
],
|
||||
"nohoist": [
|
||||
"electron",
|
||||
"electron/**",
|
||||
"web",
|
||||
"web/**"
|
||||
"web/**",
|
||||
"server",
|
||||
"server/**"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
@ -32,7 +35,10 @@
|
||||
"build:publish": "yarn build:web && yarn workspace jan build:publish",
|
||||
"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: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": {
|
||||
"concurrently": "^8.2.1",
|
||||
|
||||
@ -16,7 +16,7 @@ const dbs: Record<string, any> = {};
|
||||
*/
|
||||
function createCollection(name: string, schema?: { [key: string]: any }): Promise<void> {
|
||||
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);
|
||||
const db = new PouchDB(`${path.join(dbPath, name)}`);
|
||||
dbs[name] = db;
|
||||
@ -226,6 +226,13 @@ function findMany(
|
||||
.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,
|
||||
|
||||
@ -23,25 +23,22 @@ const initModel = (fileName) => {
|
||||
let binaryFolder = path.join(__dirname, "nitro"); // Current directory by default
|
||||
let binaryName;
|
||||
|
||||
if (process.platform === "win32") {
|
||||
// Todo: Need to check for CUDA support to switch between CUDA and non-CUDA binaries
|
||||
binaryName = "nitro_start_windows.bat";
|
||||
} else if (process.platform === "darwin") {
|
||||
// Mac OS platform
|
||||
binaryName =
|
||||
process.arch === "arm64"
|
||||
? "nitro_mac_arm64"
|
||||
: "nitro_mac_intel";
|
||||
} else {
|
||||
// Linux
|
||||
// Todo: Need to check for CUDA support to switch between CUDA and non-CUDA binaries
|
||||
binaryName = "nitro_start_linux.sh"; // For other platforms
|
||||
}
|
||||
if (process.platform === "win32") {
|
||||
// Todo: Need to check for CUDA support to switch between CUDA and non-CUDA binaries
|
||||
binaryName = "nitro_start_windows.bat";
|
||||
} else if (process.platform === "darwin") {
|
||||
// Mac OS platform
|
||||
binaryName = process.arch === "arm64" ? "nitro_mac_arm64" : "nitro_mac_intel";
|
||||
} else {
|
||||
// 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);
|
||||
|
||||
// Execute the binary
|
||||
subprocess = spawn(binaryPath, { cwd: binaryFolder });
|
||||
// Execute the binary
|
||||
subprocess = spawn(binaryPath,["0.0.0.0", PORT], { cwd: binaryFolder });
|
||||
|
||||
// Handle subprocess output
|
||||
subprocess.stdout.on("data", (data) => {
|
||||
@ -61,7 +58,7 @@ const initModel = (fileName) => {
|
||||
})
|
||||
.then(() => tcpPortUsed.waitUntilUsed(PORT, 300, 30000))
|
||||
.then(() => {
|
||||
const llama_model_path = path.join(app.getPath("userData"), fileName);
|
||||
const llama_model_path = path.join(appPath(), fileName);
|
||||
|
||||
const config = {
|
||||
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 = {
|
||||
initModel,
|
||||
killSubprocess,
|
||||
|
||||
@ -3,4 +3,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 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,
|
||||
deleteFile,
|
||||
store,
|
||||
EventName,
|
||||
events
|
||||
} from "@janhq/core";
|
||||
import { parseToModel } from "./helper";
|
||||
|
||||
const downloadModel = (product) =>
|
||||
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<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);
|
||||
|
||||
@ -87,6 +132,7 @@ function getModelById(modelId: string): Promise<any> {
|
||||
|
||||
function onStart() {
|
||||
store.createCollection("models", {});
|
||||
fetchDownloadProgress(null, null).then((fileName: string) => fileName && checkDownloadProgress(fileName));
|
||||
}
|
||||
|
||||
// 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
|
||||
title={'Discord'}
|
||||
onClick={() =>
|
||||
window.electronAPI?.openExternalUrl('https://discord.gg/AsJ8krTT3N')
|
||||
window.coreAPI?.openExternalUrl('https://discord.gg/AsJ8krTT3N')
|
||||
}
|
||||
className="flex-1"
|
||||
/>
|
||||
<SecondaryButton
|
||||
title={'Twitter'}
|
||||
onClick={() =>
|
||||
window.electronAPI?.openExternalUrl('https://twitter.com/janhq_')
|
||||
window.coreAPI?.openExternalUrl('https://twitter.com/janhq_')
|
||||
}
|
||||
className="flex-1"
|
||||
/>
|
||||
|
||||
@ -18,6 +18,9 @@ import React from 'react'
|
||||
|
||||
import BaseLayout from '@containers/Layout'
|
||||
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
|
||||
const Page: React.FC = () => {
|
||||
const viewState = useAtomValue(getMainViewStateAtom)
|
||||
|
||||
@ -53,7 +56,10 @@ const Page: React.FC = () => {
|
||||
break
|
||||
}
|
||||
|
||||
return <BaseLayout>{children}</BaseLayout>
|
||||
return <BaseLayout>
|
||||
{children}
|
||||
<div><ToastContainer /></div>
|
||||
</BaseLayout>
|
||||
}
|
||||
|
||||
export default Page
|
||||
|
||||
@ -57,7 +57,7 @@ const Providers = (props: PropsWithChildren) => {
|
||||
useEffect(() => {
|
||||
if (setupCore) {
|
||||
// Electron
|
||||
if (window && window.electronAPI) {
|
||||
if (window && window.coreAPI) {
|
||||
setupPE()
|
||||
} else {
|
||||
// Host
|
||||
|
||||
@ -170,7 +170,7 @@ export const SidebarLeft = () => {
|
||||
<div className="space-y-2 rounded-lg border border-border bg-background/50 p-3">
|
||||
<button
|
||||
onClick={() =>
|
||||
window.electronAPI?.openExternalUrl(
|
||||
window.coreAPI?.openExternalUrl(
|
||||
'https://discord.gg/AsJ8krTT3N'
|
||||
)
|
||||
}
|
||||
@ -180,7 +180,7 @@ export const SidebarLeft = () => {
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
window.electronAPI?.openExternalUrl(
|
||||
window.coreAPI?.openExternalUrl(
|
||||
'https://twitter.com/janhq_'
|
||||
)
|
||||
}
|
||||
|
||||
@ -11,6 +11,10 @@ import {
|
||||
} from './atoms/Conversation.atom'
|
||||
import { executeSerial } from '../../electron/core/plugin-manager/execution/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";
|
||||
|
||||
let currentConversation: Conversation | undefined = undefined
|
||||
|
||||
@ -21,6 +25,7 @@ const debouncedUpdateConversation = debounce(
|
||||
1000
|
||||
)
|
||||
|
||||
|
||||
export default function EventHandler({ children }: { children: ReactNode }) {
|
||||
const addNewMessage = useSetAtom(addNewMessageAtom)
|
||||
const updateMessage = useSetAtom(updateMessageAtom)
|
||||
@ -29,6 +34,9 @@ export default function EventHandler({ children }: { children: ReactNode }) {
|
||||
const { getConversationById } = useGetUserConversations()
|
||||
|
||||
const updateConvWaiting = useSetAtom(updateConversationWaitingForResponseAtom)
|
||||
const setDownloadState = useSetAtom(setDownloadStateAtom);
|
||||
const setDownloadStateSuccess = useSetAtom(setDownloadStateSuccessAtom);
|
||||
const setDownloadedModels = useSetAtom(downloadedModelAtom);
|
||||
|
||||
async function handleNewMessageResponse(message: NewMessageResponse) {
|
||||
if (message.conversationId) {
|
||||
@ -88,6 +96,22 @@ export default function EventHandler({ children }: { children: ReactNode }) {
|
||||
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(() => {
|
||||
if (window.corePlugin.events) {
|
||||
events.on(EventName.OnNewMessageResponse, handleNewMessageResponse)
|
||||
@ -97,6 +121,8 @@ export default function EventHandler({ children }: { children: ReactNode }) {
|
||||
// EventName.OnMessageResponseFinished,
|
||||
handleMessageResponseFinished
|
||||
)
|
||||
events.on(EventName.OnDownloadUpdate, handleDownloadUpdate);
|
||||
events.on(EventName.OnDownloadSuccess, handleDownloadSuccess);
|
||||
}
|
||||
}, [])
|
||||
|
||||
@ -109,6 +135,8 @@ export default function EventHandler({ children }: { children: ReactNode }) {
|
||||
// EventName.OnMessageResponseFinished,
|
||||
handleMessageResponseFinished
|
||||
)
|
||||
events.off(EventName.OnDownloadUpdate, handleDownloadUpdate);
|
||||
events.off(EventName.OnDownloadSuccess, handleDownloadSuccess);
|
||||
}
|
||||
}, [])
|
||||
return <>{children}</>
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const { version } = require('os')
|
||||
const webpack = require('webpack')
|
||||
const packageJson = require('./package.json')
|
||||
|
||||
const nextConfig = {
|
||||
output: 'export',
|
||||
@ -27,6 +29,7 @@ const nextConfig = {
|
||||
PLUGIN_CATALOG: JSON.stringify(
|
||||
'https://cdn.jsdelivr.net/npm/@janhq/plugin-catalog@latest/dist/index.js'
|
||||
),
|
||||
VERSION: JSON.stringify(packageJson.version)
|
||||
}),
|
||||
]
|
||||
return config
|
||||
|
||||
@ -45,6 +45,7 @@
|
||||
"react-intersection-observer": "^9.5.2",
|
||||
"sass": "^1.69.4",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
"react-toastify": "^9.1.3",
|
||||
"tailwindcss": "3.3.3",
|
||||
"typescript": "5.1.6",
|
||||
"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.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!window.electronAPI) {
|
||||
return;
|
||||
}
|
||||
if (!version) return
|
||||
|
||||
// Load plugin manifest from plugin if any
|
||||
@ -36,7 +39,7 @@ const PluginCatalog = () => {
|
||||
(e: any) =>
|
||||
!e.requiredVersion ||
|
||||
e.requiredVersion.replace(/[.^]/g, '') <=
|
||||
version.replaceAll('.', '')
|
||||
version.replaceAll('.', '')
|
||||
)
|
||||
)
|
||||
})
|
||||
@ -50,7 +53,7 @@ const PluginCatalog = () => {
|
||||
(e: any) =>
|
||||
!e.requiredVersion ||
|
||||
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 { EventEmitter } from './eventsService'
|
||||
|
||||
import { store } from "./storeService";
|
||||
import { EventEmitter } from "./eventsService";
|
||||
import * as cn from "./cloudNativeService"
|
||||
export const setupCoreServices = () => {
|
||||
if (typeof window === 'undefined') {
|
||||
console.log('undefine', window)
|
||||
@ -12,10 +12,14 @@ export const setupCoreServices = () => {
|
||||
window.corePlugin = {
|
||||
store,
|
||||
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 const PLUGIN_CATALOG: string
|
||||
declare const VERSION: string
|
||||
interface Window {
|
||||
electronAPI?: any | undefined
|
||||
corePlugin?: any | undefined
|
||||
coreAPI?: any | undefined
|
||||
electronAPI?: any | undefined;
|
||||
corePlugin?: any | undefined;
|
||||
coreAPI?: any | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user