jan/app/electron/main.ts
Louis 20dbc02c03
Refactor Jan into an Electron app (#175)
* hackathon: Refactor Jan into an Electron app

* chore: correct NextJS export output path

* chore: build electron app for all production targets

* fix: correct assetPrefix for production build

* chore: preferences shortcut

* chore: refactor

* chore: refactor into ts

* feature/#52-compile-plugin-with-webpack

* chore: introduce renderer <=> plugins <=> main invocation

* chore: suppress errors - deprecate graphql & next-auth

* chore: data plugin functions

* add llm support

Signed-off-by: James <james@jan.ai>

* chore: update plugin

* chore: introduce data-plugin

* chore: plugin invokes main with args and synchronously

* chore: install db plugin should setup db

* feature: Data Driver Plugin - Load conversations and messages from data plugin

* chore: store text message sent

* chore: shared core services

* feature: inference service

* chore: conversations ordering

* adding model management service

Signed-off-by: James <james@jan.ai>

* chore: strict type

* feature: abstract plugin preferences

* chore: abstract plugin preference

* Revert "chore: strict type"

This reverts commit 9be188d827a0b2e081e9e04b192c323799de5bb5.

* chore: base-plugin styling

* feature: create and delete conversation

* chore: fix plugin search & clean messages

* chore: typing indicator

* chore: refactor useSendChatMessage

* chore: persists inserted id to in-memory messages

* chore: search conversation history

* add delete and download model (#189)

Signed-off-by: James <james@jan.ai>
Co-authored-by: James <james@jan.ai>

* chore: add empty state for conversation list

* chore: prompt missing extension function & fix app crashes

* chore: prompt user to install required plugins

* chore: add launch background

* chore: relaunch app on model downloaded

* Jan app add installation instruction (#191)

Co-authored-by: Hien To <>

* Chore: rename folder web-client to app (#192)

* Chore: rename folder web-client to app
---------

Co-authored-by: Hien To <>

* revert: add pre-install package

* add progress for downloading model

Signed-off-by: James <james@jan.ai>

* feature: production bundle

* add download progress

Signed-off-by: James <james@jan.ai>

* chore: add new chat function

* fix: electron asar unpack modules & dynamic import

* chore: fix unpack

* chore: fix dev pack

* Add instruction to build dmg file to README.md

* init model dynamically

Signed-off-by: James <james@jan.ai>

---------

Signed-off-by: James <james@jan.ai>
Co-authored-by: James <james@jan.ai>
Co-authored-by: NamH <NamNh0122@gmail.com>
Co-authored-by: hiento09 <136591877+hiento09@users.noreply.github.com>
Co-authored-by: Hien To <>
2023-09-24 20:42:58 -07:00

222 lines
6.1 KiB
TypeScript

// @ts-nocheck
const {
app,
BrowserWindow,
screen: electronScreen,
dialog,
ipcMain,
} = require("electron");
const isDev = require("electron-is-dev");
const path = require("path");
const pe = require("pluggable-electron/main");
const fs = require("fs");
const { mkdir, writeFile } = require("fs/promises");
const { Readable } = require("stream");
const { finished } = require("stream/promises");
const request = require("request");
const progress = require("request-progress");
let modelSession = undefined;
let modelName = "llama-2-7b-chat.gguf.q4_0.bin";
let window;
const createMainWindow = () => {
window = new BrowserWindow({
width: electronScreen.getPrimaryDisplay().workArea.width,
height: electronScreen.getPrimaryDisplay().workArea.height,
show: false,
backgroundColor: "white",
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true,
preload: path.resolve(app.getAppPath(), "electron/preload.js"),
},
});
ipcMain.handle("invokePluginFunc", async (event, plugin, method, ...args) => {
const plg = pe
.getStore()
.getActivePlugins()
.filter((p) => p.name === plugin)[0];
const pluginPath = path.join(
app.getPath("userData"),
"plugins",
plg.name,
"dist/module.js",
);
return await import(
/* webpackIgnore: true */
pluginPath
)
.then((plugin) => {
if (typeof plugin[method] === "function") {
return plugin[method](...args);
} else {
console.log(plugin[method]);
console.error(`Function "${method}" does not exist in the module.`);
}
})
.then((res) => {
return res;
})
.catch((err) => console.log(err));
});
const startURL = isDev
? "http://localhost:3000"
: `file://${path.join(__dirname, "../out/index.html")}`;
window.loadURL(startURL);
window.once("ready-to-show", () => window.show());
window.on("closed", () => {
if (process.platform !== "darwin") app.quit();
});
window.webContents.openDevTools();
};
app.whenReady().then(() => {
createMainWindow();
setupPlugins();
ipcMain.handle("userData", async (event) => {
return path.resolve(__dirname, "../");
});
ipcMain.handle("downloadModel", async (event, url) => {
const userDataPath = app.getPath("userData");
const destination = path.resolve(userDataPath, modelName);
progress(request(url), {})
.on("progress", function (state) {
window.webContents.send("model-download-update", {
...state,
modelId: modelName,
});
})
.on("error", function (err) {
window.webContents.send("model-download-error", err);
})
.on("end", function () {
app.relaunch();
app.exit();
// Do something after request finishes
})
.pipe(fs.createWriteStream(destination));
});
ipcMain.handle("deleteModel", async (event, modelFileName) => {
const userDataPath = app.getPath("userData");
const fullPath = path.join(userDataPath, modelFileName);
let result = "NULL";
fs.unlink(fullPath, function (err) {
if (err && err.code == "ENOENT") {
console.info("File doesn't exist, won't remove it.");
result = "FILE_NOT_EXIST";
} else if (err) {
console.error("Error occurred while trying to remove file");
result = "ERROR";
} else {
console.info(`removed`);
result = "REMOVED";
}
});
console.log(result);
return result;
});
// TODO: add options for model configuration
ipcMain.handle("initModel", async (event, product) => {
if (!product.fileName) {
await dialog.showMessageBox({
message: "Selected model does not have file name..",
});
return;
}
console.info(`Initializing model: ${product.name}..`);
import(
isDev
? "../node_modules/node-llama-cpp/dist/index.js"
: path.resolve(
app.getAppPath(),
"./../../app.asar.unpacked/node_modules/node-llama-cpp/dist/index.js",
)
)
.then(({ LlamaContext, LlamaChatSession, LlamaModel }) => {
const modelPath = path.join(app.getPath("userData"), product.fileName);
// TODO: check if file is already there
const model = new LlamaModel({
modelPath: modelPath,
});
const context = new LlamaContext({ model });
modelSession = new LlamaChatSession({ context });
console.info(`Init model ${product.name} successfully!`);
})
.catch(async (e) => {
await dialog.showMessageBox({
message: "Failed to import LLM module",
});
});
});
ipcMain.handle("getDownloadedModels", async (event) => {
const userDataPath = app.getPath("userData");
const allBinariesName = [];
var files = fs.readdirSync(userDataPath);
for (var i = 0; i < files.length; i++) {
var filename = path.join(userDataPath, files[i]);
var stat = fs.lstatSync(filename);
if (stat.isDirectory()) {
// ignore
} else if (filename.endsWith(".bin")) {
allBinariesName.push(filename);
}
}
return allBinariesName;
});
ipcMain.handle("sendInquiry", async (event, question) => {
if (!modelSession) {
console.error("Model session has not been initialized!");
return;
}
return modelSession.prompt(question);
});
app.on("activate", () => {
if (!BrowserWindow.getAllWindows().length) {
createMainWindow();
}
});
});
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
});
function setupPlugins() {
pe.init({
// Function to check from the main process that user wants to install a plugin
confirmInstall: async (plugins) => {
const answer = await dialog.showMessageBox({
message: `Are you sure you want to install the plugin ${plugins.join(
", ",
)}`,
buttons: ["Ok", "Cancel"],
cancelId: 1,
});
return answer.response == 0;
},
// Path to install plugin to
pluginsPath: path.join(app.getPath("userData"), "plugins"),
});
}