refactor: move file to jan root (#598)

* feat: move necessary files to jan root

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

* chore: check model dir

---------

Signed-off-by: James <james@jan.ai>
Co-authored-by: James <james@jan.ai>
Co-authored-by: Louis <louis@jan.ai>
This commit is contained in:
NamH 2023-11-16 12:09:09 +07:00 committed by GitHub
parent 9c5c03b6bc
commit 52d56a8ae1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 608 additions and 658 deletions

View File

@ -20,7 +20,7 @@ export type MessageHistory = {
* The `NewMessageRequest` type defines the shape of a new message request object. * The `NewMessageRequest` type defines the shape of a new message request object.
*/ */
export type NewMessageRequest = { export type NewMessageRequest = {
_id?: string; id?: string;
conversationId?: string; conversationId?: string;
user?: string; user?: string;
avatar?: string; avatar?: string;
@ -34,7 +34,7 @@ export type NewMessageRequest = {
* The `NewMessageRequest` type defines the shape of a new message request object. * The `NewMessageRequest` type defines the shape of a new message request object.
*/ */
export type NewMessageResponse = { export type NewMessageResponse = {
_id?: string; id?: string;
conversationId?: string; conversationId?: string;
user?: string; user?: string;
avatar?: string; avatar?: string;

View File

@ -8,6 +8,21 @@ const writeFile: (path: string, data: string) => Promise<any> = (path, data) =>
window.coreAPI?.writeFile(path, data) ?? window.coreAPI?.writeFile(path, data) ??
window.electronAPI?.writeFile(path, data); window.electronAPI?.writeFile(path, data);
/**
* Gets the user space path.
* @returns {Promise<any>} A Promise that resolves with the user space path.
*/
const getUserSpace = (): Promise<string> =>
window.coreAPI?.getUserSpace() ?? window.electronAPI?.getUserSpace();
/**
* Checks whether the path is a directory.
* @param path - The path to check.
* @returns {boolean} A boolean indicating whether the path is a directory.
*/
const isDirectory = (path: string): Promise<boolean> =>
window.coreAPI?.isDirectory(path) ?? window.electronAPI?.isDirectory(path);
/** /**
* Reads the contents of a file at the specified path. * Reads the contents of a file at the specified path.
* @param {string} path - The path of the file to read. * @param {string} path - The path of the file to read.
@ -48,6 +63,8 @@ const deleteFile: (path: string) => Promise<any> = (path) =>
window.coreAPI?.deleteFile(path) ?? window.electronAPI?.deleteFile(path); window.coreAPI?.deleteFile(path) ?? window.electronAPI?.deleteFile(path);
export const fs = { export const fs = {
isDirectory,
getUserSpace,
writeFile, writeFile,
readFile, readFile,
listFiles, listFiles,

View File

@ -20,12 +20,9 @@ export { events } from "./events";
* Events types exports. * Events types exports.
* @module * @module
*/ */
export { export * from "./events";
EventName,
NewMessageRequest, export * from "./types/index";
NewMessageResponse,
MessageHistory,
} from "./events";
/** /**
* Filesystem module exports. * Filesystem module exports.

View File

@ -1,5 +1,5 @@
export interface Conversation { export interface Conversation {
_id: string; id: string;
modelId?: string; modelId?: string;
botId?: string; botId?: string;
name: string; name: string;
@ -8,11 +8,23 @@ export interface Conversation {
createdAt?: string; createdAt?: string;
updatedAt?: string; updatedAt?: string;
messages: Message[]; messages: Message[];
lastMessage?: string;
} }
export interface Message { export interface Message {
id: string;
message?: string; message?: string;
user?: string; user?: string;
_id: string; createdAt?: string;
updatedAt?: string;
}
export interface RawMessage {
id?: string;
conversationId?: string;
user?: string;
avatar?: string;
message?: string;
createdAt?: string; createdAt?: string;
updatedAt?: string; updatedAt?: string;
} }
@ -22,7 +34,7 @@ export interface Model {
* Combination of owner and model name. * Combination of owner and model name.
* Being used as file name. MUST be unique. * Being used as file name. MUST be unique.
*/ */
_id: string; id: string;
name: string; name: string;
quantMethod: string; quantMethod: string;
bits: number; bits: number;
@ -51,7 +63,7 @@ export interface Model {
tags: string[]; tags: string[];
} }
export interface ModelCatalog { export interface ModelCatalog {
_id: string; id: string;
name: string; name: string;
shortDescription: string; shortDescription: string;
avatarUrl: string; avatarUrl: string;
@ -74,7 +86,7 @@ export type ModelVersion = {
* Combination of owner and model name. * Combination of owner and model name.
* Being used as file name. Should be unique. * Being used as file name. Should be unique.
*/ */
_id: string; id: string;
name: string; name: string;
quantMethod: string; quantMethod: string;
bits: number; bits: number;
@ -89,3 +101,40 @@ export type ModelVersion = {
startDownloadAt?: number; startDownloadAt?: number;
finishDownloadAt?: number; finishDownloadAt?: number;
}; };
export interface ChatMessage {
id: string;
conversationId: string;
messageType: MessageType;
messageSenderType: MessageSenderType;
senderUid: string;
senderName: string;
senderAvatarUrl: string;
text: string | undefined;
imageUrls?: string[] | undefined;
createdAt: number;
status: MessageStatus;
}
export enum MessageType {
Text = "Text",
Image = "Image",
ImageWithText = "ImageWithText",
Error = "Error",
}
export enum MessageSenderType {
Ai = "assistant",
User = "user",
}
export enum MessageStatus {
Ready = "ready",
Pending = "pending",
}
export type ConversationState = {
hasMore: boolean;
waitingForResponse: boolean;
error?: Error;
};

8
electron/.prettierrc Normal file
View File

@ -0,0 +1,8 @@
{
"semi": false,
"singleQuote": true,
"quoteProps": "consistent",
"trailingComma": "es5",
"endOfLine": "auto",
"plugins": ["prettier-plugin-tailwindcss"]
}

View File

@ -1,10 +1,10 @@
import { app, ipcMain } from "electron"; import { app, ipcMain } from 'electron'
import { DownloadManager } from "../managers/download"; import { DownloadManager } from '../managers/download'
import { resolve, join } from "path"; import { resolve, join } from 'path'
import { WindowManager } from "../managers/window"; import { WindowManager } from '../managers/window'
import request from "request"; import request from 'request'
import { createWriteStream, unlink } from "fs"; import { createWriteStream, unlink } from 'fs'
const progress = require("request-progress"); const progress = require('request-progress')
export function handleDownloaderIPCs() { export function handleDownloaderIPCs() {
/** /**
@ -12,18 +12,18 @@ export function handleDownloaderIPCs() {
* @param _event - The IPC event object. * @param _event - The IPC event object.
* @param fileName - The name of the file being downloaded. * @param fileName - The name of the file being downloaded.
*/ */
ipcMain.handle("pauseDownload", async (_event, fileName) => { ipcMain.handle('pauseDownload', async (_event, fileName) => {
DownloadManager.instance.networkRequests[fileName]?.pause(); DownloadManager.instance.networkRequests[fileName]?.pause()
}); })
/** /**
* Handles the "resumeDownload" IPC message by resuming the download associated with the provided fileName. * Handles the "resumeDownload" IPC message by resuming the download associated with the provided fileName.
* @param _event - The IPC event object. * @param _event - The IPC event object.
* @param fileName - The name of the file being downloaded. * @param fileName - The name of the file being downloaded.
*/ */
ipcMain.handle("resumeDownload", async (_event, fileName) => { ipcMain.handle('resumeDownload', async (_event, fileName) => {
DownloadManager.instance.networkRequests[fileName]?.resume(); DownloadManager.instance.networkRequests[fileName]?.resume()
}); })
/** /**
* Handles the "abortDownload" IPC message by aborting the download associated with the provided fileName. * Handles the "abortDownload" IPC message by aborting the download associated with the provided fileName.
@ -31,24 +31,26 @@ export function handleDownloaderIPCs() {
* @param _event - The IPC event object. * @param _event - The IPC event object.
* @param fileName - The name of the file being downloaded. * @param fileName - The name of the file being downloaded.
*/ */
ipcMain.handle("abortDownload", async (_event, fileName) => { ipcMain.handle('abortDownload', async (_event, fileName) => {
const rq = DownloadManager.instance.networkRequests[fileName]; const rq = DownloadManager.instance.networkRequests[fileName]
DownloadManager.instance.networkRequests[fileName] = undefined; DownloadManager.instance.networkRequests[fileName] = undefined
const userDataPath = app.getPath("userData"); const userDataPath = app.getPath('userData')
const fullPath = join(userDataPath, fileName); const fullPath = join(userDataPath, fileName)
rq?.abort(); rq?.abort()
let result = "NULL"; let result = 'NULL'
unlink(fullPath, function (err) { unlink(fullPath, function (err) {
if (err && err.code == "ENOENT") { if (err && err.code == 'ENOENT') {
result = `File not exist: ${err}`; result = `File not exist: ${err}`
} else if (err) { } else if (err) {
result = `File delete error: ${err}`; result = `File delete error: ${err}`
} else { } else {
result = "File deleted successfully"; result = 'File deleted successfully'
} }
console.log(`Delete file ${fileName} from ${fullPath} result: ${result}`); console.debug(
}); `Delete file ${fileName} from ${fullPath} result: ${result}`
}); )
})
})
/** /**
* Downloads a file from a given URL. * Downloads a file from a given URL.
@ -56,51 +58,51 @@ export function handleDownloaderIPCs() {
* @param url - The URL to download the file from. * @param url - The URL to download the file from.
* @param fileName - The name to give the downloaded file. * @param fileName - The name to give the downloaded file.
*/ */
ipcMain.handle("downloadFile", async (_event, url, fileName) => { ipcMain.handle('downloadFile', async (_event, url, fileName) => {
const userDataPath = app.getPath("userData"); const userDataPath = join(app.getPath('home'), 'jan')
const destination = resolve(userDataPath, fileName); const destination = resolve(userDataPath, fileName)
const rq = request(url); const rq = request(url)
progress(rq, {}) progress(rq, {})
.on("progress", function (state: any) { .on('progress', function (state: any) {
WindowManager?.instance.currentWindow?.webContents.send( WindowManager?.instance.currentWindow?.webContents.send(
"FILE_DOWNLOAD_UPDATE", 'FILE_DOWNLOAD_UPDATE',
{ {
...state, ...state,
fileName, fileName,
} }
); )
}) })
.on("error", function (err: Error) { .on('error', function (err: Error) {
WindowManager?.instance.currentWindow?.webContents.send( WindowManager?.instance.currentWindow?.webContents.send(
"FILE_DOWNLOAD_ERROR", 'FILE_DOWNLOAD_ERROR',
{ {
fileName, fileName,
err, err,
} }
); )
}) })
.on("end", function () { .on('end', function () {
if (DownloadManager.instance.networkRequests[fileName]) { if (DownloadManager.instance.networkRequests[fileName]) {
WindowManager?.instance.currentWindow?.webContents.send( WindowManager?.instance.currentWindow?.webContents.send(
"FILE_DOWNLOAD_COMPLETE", 'FILE_DOWNLOAD_COMPLETE',
{ {
fileName, fileName,
} }
); )
DownloadManager.instance.setRequest(fileName, undefined); DownloadManager.instance.setRequest(fileName, undefined)
} else { } else {
WindowManager?.instance.currentWindow?.webContents.send( WindowManager?.instance.currentWindow?.webContents.send(
"FILE_DOWNLOAD_ERROR", 'FILE_DOWNLOAD_ERROR',
{ {
fileName, fileName,
err: "Download cancelled", err: 'Download cancelled',
} }
); )
} }
}) })
.pipe(createWriteStream(destination)); .pipe(createWriteStream(destination))
DownloadManager.instance.setRequest(fileName, rq); DownloadManager.instance.setRequest(fileName, rq)
}); })
} }

View File

@ -1,28 +1,53 @@
import { app, ipcMain } from "electron"; import { app, ipcMain } from 'electron'
import * as fs from "fs"; import * as fs from 'fs'
import { join } from "path"; import { join } from 'path'
/** /**
* Handles file system operations. * Handles file system operations.
*/ */
export function handleFsIPCs() { export function handleFsIPCs() {
const userSpacePath = join(app.getPath('home'), 'jan')
/**
* Gets the path to the user data directory.
* @param event - The event object.
* @returns A promise that resolves with the path to the user data directory.
*/
ipcMain.handle(
'getUserSpace',
(): Promise<string> => Promise.resolve(userSpacePath)
)
/**
* Checks whether the path is a directory.
* @param event - The event object.
* @param path - The path to check.
* @returns A promise that resolves with a boolean indicating whether the path is a directory.
*/
ipcMain.handle('isDirectory', (_event, path: string): Promise<boolean> => {
const fullPath = join(userSpacePath, path)
return Promise.resolve(
fs.existsSync(fullPath) && fs.lstatSync(fullPath).isDirectory()
)
})
/** /**
* Reads a file from the user data directory. * Reads a file from the user data directory.
* @param event - The event object. * @param event - The event object.
* @param path - The path of the file to read. * @param path - The path of the file to read.
* @returns A promise that resolves with the contents of the file. * @returns A promise that resolves with the contents of the file.
*/ */
ipcMain.handle("readFile", async (event, path: string): Promise<string> => { ipcMain.handle('readFile', async (event, path: string): Promise<string> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fs.readFile(join(app.getPath("userData"), path), "utf8", (err, data) => { fs.readFile(join(userSpacePath, path), 'utf8', (err, data) => {
if (err) { if (err) {
reject(err); reject(err)
} else { } else {
resolve(data); resolve(data)
} }
}); })
}); })
}); })
/** /**
* Writes data to a file in the user data directory. * Writes data to a file in the user data directory.
@ -32,24 +57,19 @@ export function handleFsIPCs() {
* @returns A promise that resolves when the file has been written. * @returns A promise that resolves when the file has been written.
*/ */
ipcMain.handle( ipcMain.handle(
"writeFile", 'writeFile',
async (event, path: string, data: string): Promise<void> => { async (event, path: string, data: string): Promise<void> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fs.writeFile( fs.writeFile(join(userSpacePath, path), data, 'utf8', (err) => {
join(app.getPath("userData"), path), if (err) {
data, reject(err)
"utf8", } else {
(err) => { resolve()
if (err) {
reject(err);
} else {
resolve();
}
} }
); })
}); })
} }
); )
/** /**
* Creates a directory in the user data directory. * Creates a directory in the user data directory.
@ -57,21 +77,17 @@ export function handleFsIPCs() {
* @param path - The path of the directory to create. * @param path - The path of the directory to create.
* @returns A promise that resolves when the directory has been created. * @returns A promise that resolves when the directory has been created.
*/ */
ipcMain.handle("mkdir", async (event, path: string): Promise<void> => { ipcMain.handle('mkdir', async (event, path: string): Promise<void> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fs.mkdir( fs.mkdir(join(userSpacePath, path), { recursive: true }, (err) => {
join(app.getPath("userData"), path), if (err) {
{ recursive: true }, reject(err)
(err) => { } else {
if (err) { resolve()
reject(err);
} else {
resolve();
}
} }
); })
}); })
}); })
/** /**
* Removes a directory in the user data directory. * Removes a directory in the user data directory.
@ -79,21 +95,17 @@ export function handleFsIPCs() {
* @param path - The path of the directory to remove. * @param path - The path of the directory to remove.
* @returns A promise that resolves when the directory is removed successfully. * @returns A promise that resolves when the directory is removed successfully.
*/ */
ipcMain.handle("rmdir", async (event, path: string): Promise<void> => { ipcMain.handle('rmdir', async (event, path: string): Promise<void> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fs.rmdir( fs.rmdir(join(userSpacePath, path), { recursive: true }, (err) => {
join(app.getPath("userData"), path), if (err) {
{ recursive: true }, reject(err)
(err) => { } else {
if (err) { resolve()
reject(err);
} else {
resolve();
}
} }
); })
}); })
}); })
/** /**
* Lists the files in a directory in the user data directory. * Lists the files in a directory in the user data directory.
@ -102,19 +114,19 @@ export function handleFsIPCs() {
* @returns A promise that resolves with an array of file names. * @returns A promise that resolves with an array of file names.
*/ */
ipcMain.handle( ipcMain.handle(
"listFiles", 'listFiles',
async (event, path: string): Promise<string[]> => { async (event, path: string): Promise<string[]> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fs.readdir(join(app.getPath("userData"), path), (err, files) => { fs.readdir(join(userSpacePath, path), (err, files) => {
if (err) { if (err) {
reject(err); reject(err)
} else { } else {
resolve(files); resolve(files)
} }
}); })
}); })
} }
); )
/** /**
* Deletes a file from the user data folder. * Deletes a file from the user data folder.
@ -122,22 +134,23 @@ export function handleFsIPCs() {
* @param filePath - The path to the file to delete. * @param filePath - The path to the file to delete.
* @returns A string indicating the result of the operation. * @returns A string indicating the result of the operation.
*/ */
ipcMain.handle("deleteFile", async (_event, filePath) => { ipcMain.handle('deleteFile', async (_event, filePath) => {
const userDataPath = app.getPath("userData"); const fullPath = join(userSpacePath, filePath)
const fullPath = join(userDataPath, filePath);
let result = "NULL"; let result = 'NULL'
fs.unlink(fullPath, function (err) { fs.unlink(fullPath, function (err) {
if (err && err.code == "ENOENT") { if (err && err.code == 'ENOENT') {
result = `File not exist: ${err}`; result = `File not exist: ${err}`
} else if (err) { } else if (err) {
result = `File delete error: ${err}`; result = `File delete error: ${err}`
} else { } else {
result = "File deleted successfully"; result = 'File deleted successfully'
} }
console.log(`Delete file ${filePath} from ${fullPath} result: ${result}`); console.debug(
}); `Delete file ${filePath} from ${fullPath} result: ${result}`
)
})
return result; return result
}); })
} }

View File

@ -30,7 +30,7 @@ export function handlePluginIPCs() {
if (typeof module[method] === "function") { if (typeof module[method] === "function") {
return module[method](...args); return module[method](...args);
} else { } else {
console.log(module[method]); console.debug(module[method]);
console.error(`Function "${method}" does not exist in the module.`); console.error(`Function "${method}" does not exist in the module.`);
} }
} }
@ -75,7 +75,7 @@ export function handlePluginIPCs() {
const fullPath = join(userDataPath, "plugins"); const fullPath = join(userDataPath, "plugins");
rmdir(fullPath, { recursive: true }, function (err) { rmdir(fullPath, { recursive: true }, function (err) {
if (err) console.log(err); if (err) console.error(err);
ModuleManager.instance.clearImportedModules(); ModuleManager.instance.clearImportedModules();
// just relaunch if packaged, should launch manually in development mode // just relaunch if packaged, should launch manually in development mode

View File

@ -42,7 +42,7 @@ export function handleAppUpdates() {
/* App Update Progress */ /* App Update Progress */
autoUpdater.on("download-progress", (progress: any) => { autoUpdater.on("download-progress", (progress: any) => {
console.log("app update progress: ", progress.percent); console.debug("app update progress: ", progress.percent);
WindowManager.instance.currentWindow?.webContents.send( WindowManager.instance.currentWindow?.webContents.send(
"APP_UPDATE_PROGRESS", "APP_UPDATE_PROGRESS",
{ {

View File

@ -1,23 +1,23 @@
import { app, BrowserWindow } from "electron"; import { app, BrowserWindow } from 'electron'
import { join } from "path"; import { join } from 'path'
import { setupMenu } from "./utils/menu"; import { setupMenu } from './utils/menu'
import { handleFsIPCs } from "./handlers/fs"; import { handleFsIPCs } from './handlers/fs'
/** /**
* Managers * Managers
**/ **/
import { WindowManager } from "./managers/window"; import { WindowManager } from './managers/window'
import { ModuleManager } from "./managers/module"; import { ModuleManager } from './managers/module'
import { PluginManager } from "./managers/plugin"; import { PluginManager } from './managers/plugin'
/** /**
* IPC Handlers * IPC Handlers
**/ **/
import { handleDownloaderIPCs } from "./handlers/download"; import { handleDownloaderIPCs } from './handlers/download'
import { handleThemesIPCs } from "./handlers/theme"; import { handleThemesIPCs } from './handlers/theme'
import { handlePluginIPCs } from "./handlers/plugin"; import { handlePluginIPCs } from './handlers/plugin'
import { handleAppIPCs } from "./handlers/app"; import { handleAppIPCs } from './handlers/app'
import { handleAppUpdates } from "./handlers/update"; import { handleAppUpdates } from './handlers/update'
app app
.whenReady() .whenReady()
@ -28,56 +28,56 @@ app
.then(handleAppUpdates) .then(handleAppUpdates)
.then(createMainWindow) .then(createMainWindow)
.then(() => { .then(() => {
app.on("activate", () => { app.on('activate', () => {
if (!BrowserWindow.getAllWindows().length) { if (!BrowserWindow.getAllWindows().length) {
createMainWindow(); createMainWindow()
} }
}); })
}); })
app.on("window-all-closed", () => { app.on('window-all-closed', () => {
ModuleManager.instance.clearImportedModules(); ModuleManager.instance.clearImportedModules()
app.quit(); app.quit()
}); })
app.on("quit", () => { app.on('quit', () => {
ModuleManager.instance.clearImportedModules(); ModuleManager.instance.clearImportedModules()
app.quit(); app.quit()
}); })
function createMainWindow() { function createMainWindow() {
/* Create main window */ /* Create main window */
const mainWindow = WindowManager.instance.createWindow({ const mainWindow = WindowManager.instance.createWindow({
webPreferences: { webPreferences: {
nodeIntegration: true, nodeIntegration: true,
preload: join(__dirname, "preload.js"), preload: join(__dirname, 'preload.js'),
webSecurity: false, webSecurity: false,
}, },
}); })
const startURL = app.isPackaged const startURL = app.isPackaged
? `file://${join(__dirname, "../renderer/index.html")}` ? `file://${join(__dirname, '../renderer/index.html')}`
: "http://localhost:3000"; : 'http://localhost:3000'
/* Load frontend app to the window */ /* Load frontend app to the window */
mainWindow.loadURL(startURL); mainWindow.loadURL(startURL)
mainWindow.once("ready-to-show", () => mainWindow?.show()); mainWindow.once('ready-to-show', () => mainWindow?.show())
mainWindow.on("closed", () => { mainWindow.on('closed', () => {
if (process.platform !== "darwin") app.quit(); if (process.platform !== 'darwin') app.quit()
}); })
/* Enable dev tools for development */ /* Enable dev tools for development */
if (!app.isPackaged) mainWindow.webContents.openDevTools(); if (!app.isPackaged) mainWindow.webContents.openDevTools()
} }
/** /**
* Handles various IPC messages from the renderer process. * Handles various IPC messages from the renderer process.
*/ */
function handleIPCs() { function handleIPCs() {
handleFsIPCs(); handleFsIPCs()
handleDownloaderIPCs(); handleDownloaderIPCs()
handleThemesIPCs(); handleThemesIPCs()
handlePluginIPCs(); handlePluginIPCs()
handleAppIPCs(); handleAppIPCs()
} }

View File

@ -42,14 +42,14 @@ export class PluginManager {
return new Promise((resolve) => { return new Promise((resolve) => {
const store = new Store(); const store = new Store();
if (store.get("migrated_version") !== app.getVersion()) { if (store.get("migrated_version") !== app.getVersion()) {
console.log("start migration:", store.get("migrated_version")); console.debug("start migration:", store.get("migrated_version"));
const userDataPath = app.getPath("userData"); const userDataPath = app.getPath("userData");
const fullPath = join(userDataPath, "plugins"); const fullPath = join(userDataPath, "plugins");
rmdir(fullPath, { recursive: true }, function (err) { rmdir(fullPath, { recursive: true }, function (err) {
if (err) console.log(err); if (err) console.error(err);
store.set("migrated_version", app.getVersion()); store.set("migrated_version", app.getVersion());
console.log("migrate plugins done"); console.debug("migrate plugins done");
resolve(undefined); resolve(undefined);
}); });
} else { } else {

View File

@ -33,6 +33,8 @@
* @property {Function} relaunch - Relaunches the app. * @property {Function} relaunch - Relaunches the app.
* @property {Function} openAppDirectory - Opens the app directory. * @property {Function} openAppDirectory - Opens the app directory.
* @property {Function} deleteFile - Deletes the file at the given path. * @property {Function} deleteFile - Deletes the file at the given path.
* @property {Function} isDirectory - Returns true if the file at the given path is a directory.
* @property {Function} getUserSpace - Returns the user space.
* @property {Function} readFile - Reads the file at the given path. * @property {Function} readFile - Reads the file at the given path.
* @property {Function} writeFile - Writes the given data to the file at the given path. * @property {Function} writeFile - Writes the given data to the file at the given path.
* @property {Function} listFiles - Lists the files in the directory at the given path. * @property {Function} listFiles - Lists the files in the directory at the given path.
@ -52,81 +54,85 @@
*/ */
// Make Pluggable Electron's facade available to the renderer on window.plugins // Make Pluggable Electron's facade available to the renderer on window.plugins
import { useFacade } from "./core/plugin/facade"; import { useFacade } from './core/plugin/facade'
useFacade(); useFacade()
const { contextBridge, ipcRenderer } = require("electron"); const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld("electronAPI", { contextBridge.exposeInMainWorld('electronAPI', {
invokePluginFunc: (plugin: any, method: any, ...args: any[]) => invokePluginFunc: (plugin: any, method: any, ...args: any[]) =>
ipcRenderer.invoke("invokePluginFunc", plugin, method, ...args), ipcRenderer.invoke('invokePluginFunc', plugin, method, ...args),
setNativeThemeLight: () => ipcRenderer.invoke("setNativeThemeLight"), setNativeThemeLight: () => ipcRenderer.invoke('setNativeThemeLight'),
setNativeThemeDark: () => ipcRenderer.invoke("setNativeThemeDark"), setNativeThemeDark: () => ipcRenderer.invoke('setNativeThemeDark'),
setNativeThemeSystem: () => ipcRenderer.invoke("setNativeThemeSystem"), setNativeThemeSystem: () => ipcRenderer.invoke('setNativeThemeSystem'),
basePlugins: () => ipcRenderer.invoke("basePlugins"), basePlugins: () => ipcRenderer.invoke('basePlugins'),
pluginPath: () => ipcRenderer.invoke("pluginPath"), pluginPath: () => ipcRenderer.invoke('pluginPath'),
appDataPath: () => ipcRenderer.invoke("appDataPath"), appDataPath: () => ipcRenderer.invoke('appDataPath'),
reloadPlugins: () => ipcRenderer.invoke("reloadPlugins"), reloadPlugins: () => ipcRenderer.invoke('reloadPlugins'),
appVersion: () => ipcRenderer.invoke("appVersion"), appVersion: () => ipcRenderer.invoke('appVersion'),
openExternalUrl: (url: string) => ipcRenderer.invoke("openExternalUrl", url), openExternalUrl: (url: string) => ipcRenderer.invoke('openExternalUrl', url),
relaunch: () => ipcRenderer.invoke("relaunch"), relaunch: () => ipcRenderer.invoke('relaunch'),
openAppDirectory: () => ipcRenderer.invoke("openAppDirectory"), openAppDirectory: () => ipcRenderer.invoke('openAppDirectory'),
deleteFile: (filePath: string) => ipcRenderer.invoke("deleteFile", filePath), deleteFile: (filePath: string) => ipcRenderer.invoke('deleteFile', filePath),
readFile: (path: string) => ipcRenderer.invoke("readFile", path), isDirectory: (filePath: string) => ipcRenderer.invoke('isDirectory', filePath),
getUserSpace: () => ipcRenderer.invoke('getUserSpace'),
readFile: (path: string) => ipcRenderer.invoke('readFile', path),
writeFile: (path: string, data: string) => writeFile: (path: string, data: string) =>
ipcRenderer.invoke("writeFile", path, data), ipcRenderer.invoke('writeFile', path, data),
listFiles: (path: string) => ipcRenderer.invoke("listFiles", path), listFiles: (path: string) => ipcRenderer.invoke('listFiles', path),
mkdir: (path: string) => ipcRenderer.invoke("mkdir", path), mkdir: (path: string) => ipcRenderer.invoke('mkdir', path),
rmdir: (path: string) => ipcRenderer.invoke("rmdir", path), rmdir: (path: string) => ipcRenderer.invoke('rmdir', path),
installRemotePlugin: (pluginName: string) => installRemotePlugin: (pluginName: string) =>
ipcRenderer.invoke("installRemotePlugin", pluginName), ipcRenderer.invoke('installRemotePlugin', pluginName),
downloadFile: (url: string, path: string) => downloadFile: (url: string, path: string) =>
ipcRenderer.invoke("downloadFile", url, path), ipcRenderer.invoke('downloadFile', url, path),
pauseDownload: (fileName: string) => pauseDownload: (fileName: string) =>
ipcRenderer.invoke("pauseDownload", fileName), ipcRenderer.invoke('pauseDownload', fileName),
resumeDownload: (fileName: string) => resumeDownload: (fileName: string) =>
ipcRenderer.invoke("resumeDownload", fileName), ipcRenderer.invoke('resumeDownload', fileName),
abortDownload: (fileName: string) => abortDownload: (fileName: string) =>
ipcRenderer.invoke("abortDownload", fileName), ipcRenderer.invoke('abortDownload', fileName),
onFileDownloadUpdate: (callback: any) => onFileDownloadUpdate: (callback: any) =>
ipcRenderer.on("FILE_DOWNLOAD_UPDATE", callback), ipcRenderer.on('FILE_DOWNLOAD_UPDATE', callback),
onFileDownloadError: (callback: any) => onFileDownloadError: (callback: any) =>
ipcRenderer.on("FILE_DOWNLOAD_ERROR", callback), ipcRenderer.on('FILE_DOWNLOAD_ERROR', callback),
onFileDownloadSuccess: (callback: any) => onFileDownloadSuccess: (callback: any) =>
ipcRenderer.on("FILE_DOWNLOAD_COMPLETE", callback), ipcRenderer.on('FILE_DOWNLOAD_COMPLETE', callback),
onAppUpdateDownloadUpdate: (callback: any) => onAppUpdateDownloadUpdate: (callback: any) =>
ipcRenderer.on("APP_UPDATE_PROGRESS", callback), ipcRenderer.on('APP_UPDATE_PROGRESS', callback),
onAppUpdateDownloadError: (callback: any) => onAppUpdateDownloadError: (callback: any) =>
ipcRenderer.on("APP_UPDATE_ERROR", callback), ipcRenderer.on('APP_UPDATE_ERROR', callback),
onAppUpdateDownloadSuccess: (callback: any) => onAppUpdateDownloadSuccess: (callback: any) =>
ipcRenderer.on("APP_UPDATE_COMPLETE", callback), ipcRenderer.on('APP_UPDATE_COMPLETE', callback),
}); })

View File

@ -24,6 +24,7 @@
}, },
"dependencies": { "dependencies": {
"@janhq/core": "file:../../core", "@janhq/core": "file:../../core",
"path-browserify": "^1.0.1",
"ts-loader": "^9.5.0" "ts-loader": "^9.5.0"
}, },
"engines": { "engines": {

View File

@ -1,12 +1,15 @@
import { PluginType, fs } from '@janhq/core' import { PluginType, fs } from '@janhq/core'
import { ConversationalPlugin } from '@janhq/core/lib/plugins' import { ConversationalPlugin } from '@janhq/core/lib/plugins'
import { Conversation } from '@janhq/core/lib/types' import { Conversation } from '@janhq/core/lib/types'
import { join } from 'path'
/** /**
* JSONConversationalPlugin is a ConversationalPlugin implementation that provides * JSONConversationalPlugin is a ConversationalPlugin implementation that provides
* functionality for managing conversations. * functionality for managing conversations.
*/ */
export default class JSONConversationalPlugin implements ConversationalPlugin { export default class JSONConversationalPlugin implements ConversationalPlugin {
private static readonly _homeDir = 'threads'
/** /**
* Returns the type of the plugin. * Returns the type of the plugin.
*/ */
@ -18,7 +21,7 @@ export default class JSONConversationalPlugin implements ConversationalPlugin {
* Called when the plugin is loaded. * Called when the plugin is loaded.
*/ */
onLoad() { onLoad() {
fs.mkdir('conversations') fs.mkdir(JSONConversationalPlugin._homeDir)
console.debug('JSONConversationalPlugin loaded') console.debug('JSONConversationalPlugin loaded')
} }
@ -65,10 +68,14 @@ export default class JSONConversationalPlugin implements ConversationalPlugin {
*/ */
saveConversation(conversation: Conversation): Promise<void> { saveConversation(conversation: Conversation): Promise<void> {
return fs return fs
.mkdir(`conversations/${conversation._id}`) .mkdir(`${JSONConversationalPlugin._homeDir}/${conversation.id}`)
.then(() => .then(() =>
fs.writeFile( fs.writeFile(
`conversations/${conversation._id}/${conversation._id}.json`, join(
JSONConversationalPlugin._homeDir,
conversation.id,
`${conversation.id}.json`
),
JSON.stringify(conversation) JSON.stringify(conversation)
) )
) )
@ -79,7 +86,9 @@ export default class JSONConversationalPlugin implements ConversationalPlugin {
* @param conversationId The ID of the conversation to delete. * @param conversationId The ID of the conversation to delete.
*/ */
deleteConversation(conversationId: string): Promise<void> { deleteConversation(conversationId: string): Promise<void> {
return fs.rmdir(`conversations/${conversationId}`) return fs.rmdir(
join(JSONConversationalPlugin._homeDir, `${conversationId}`)
)
} }
/** /**
@ -88,7 +97,9 @@ export default class JSONConversationalPlugin implements ConversationalPlugin {
* @returns data of the conversation * @returns data of the conversation
*/ */
private async readConvo(convoId: string): Promise<any> { private async readConvo(convoId: string): Promise<any> {
return fs.readFile(`conversations/${convoId}/${convoId}.json`) return fs.readFile(
join(JSONConversationalPlugin._homeDir, convoId, `${convoId}.json`)
)
} }
/** /**
@ -97,8 +108,10 @@ export default class JSONConversationalPlugin implements ConversationalPlugin {
* @private * @private
*/ */
private async getConversationDocs(): Promise<string[]> { private async getConversationDocs(): Promise<string[]> {
return fs.listFiles(`conversations`).then((files: string[]) => { return fs
return Promise.all(files.filter((file) => file.startsWith('jan-'))) .listFiles(JSONConversationalPlugin._homeDir)
}) .then((files: string[]) => {
return Promise.all(files.filter((file) => file.startsWith('jan-')))
})
} }
} }

View File

@ -22,6 +22,9 @@ module.exports = {
plugins: [new webpack.DefinePlugin({})], plugins: [new webpack.DefinePlugin({})],
resolve: { resolve: {
extensions: [".ts", ".js"], extensions: [".ts", ".js"],
fallback: {
path: require.resolve('path-browserify'),
},
}, },
// Do not minify the output, otherwise it breaks the class registration // Do not minify the output, otherwise it breaks the class registration
optimization: { optimization: {

View File

@ -83,15 +83,15 @@ export default class JanConversationalPlugin implements ConversationalPlugin {
*/ */
private parseConversationMarkdown(markdown: string): Conversation { private parseConversationMarkdown(markdown: string): Conversation {
const conversation: Conversation = { const conversation: Conversation = {
_id: "", id: "",
name: "", name: "",
messages: [], messages: [],
}; };
var currentMessage: Message | undefined = undefined; var currentMessage: Message | undefined = undefined;
for (const line of markdown.split("\n")) { for (const line of markdown.split("\n")) {
const trimmedLine = line.trim(); const trimmedLine = line.trim();
if (trimmedLine.startsWith("- _id:")) { if (trimmedLine.startsWith("- id:")) {
conversation._id = trimmedLine.replace("- _id:", "").trim(); conversation.id = trimmedLine.replace("- id:", "").trim();
} else if (trimmedLine.startsWith("- modelId:")) { } else if (trimmedLine.startsWith("- modelId:")) {
conversation.modelId = trimmedLine.replace("- modelId:", "").trim(); conversation.modelId = trimmedLine.replace("- modelId:", "").trim();
} else if (trimmedLine.startsWith("- name:")) { } else if (trimmedLine.startsWith("- name:")) {
@ -128,7 +128,7 @@ export default class JanConversationalPlugin implements ConversationalPlugin {
if (currentMessage) { if (currentMessage) {
conversation.messages.push(currentMessage); conversation.messages.push(currentMessage);
} }
currentMessage = { _id: messageMatch[1] }; currentMessage = { id: messageMatch[1] };
} }
} else if ( } else if (
currentMessage?.message && currentMessage?.message &&
@ -170,7 +170,7 @@ export default class JanConversationalPlugin implements ConversationalPlugin {
private generateMarkdown(conversation: Conversation): string { private generateMarkdown(conversation: Conversation): string {
// Generate the Markdown content based on the Conversation object // Generate the Markdown content based on the Conversation object
const conversationMetadata = ` const conversationMetadata = `
- _id: ${conversation._id} - id: ${conversation.id}
- modelId: ${conversation.modelId} - modelId: ${conversation.modelId}
- name: ${conversation.name} - name: ${conversation.name}
- lastMessage: ${conversation.message} - lastMessage: ${conversation.message}
@ -182,7 +182,7 @@ export default class JanConversationalPlugin implements ConversationalPlugin {
const messages = conversation.messages.map( const messages = conversation.messages.map(
(message) => ` (message) => `
- Message ${message._id}: - Message ${message.id}:
- createdAt: ${message.createdAt} - createdAt: ${message.createdAt}
- user: ${message.user} - user: ${message.user}
- message: ${message.message?.trim()} - message: ${message.message?.trim()}
@ -204,10 +204,10 @@ export default class JanConversationalPlugin implements ConversationalPlugin {
private async writeMarkdownToFile(conversation: Conversation) { private async writeMarkdownToFile(conversation: Conversation) {
// Generate the Markdown content // Generate the Markdown content
const markdownContent = this.generateMarkdown(conversation); const markdownContent = this.generateMarkdown(conversation);
await fs.mkdir(`conversations/${conversation._id}`); await fs.mkdir(`conversations/${conversation.id}`);
// Write the content to a Markdown file // Write the content to a Markdown file
await fs.writeFile( await fs.writeFile(
`conversations/${conversation._id}/${conversation._id}.md`, `conversations/${conversation.id}/${conversation.id}.md`,
markdownContent markdownContent
); );
} }

View File

@ -18,7 +18,7 @@ import { InferencePlugin } from "@janhq/core/lib/plugins";
import { requestInference } from "./helpers/sse"; import { requestInference } from "./helpers/sse";
import { ulid } from "ulid"; import { ulid } from "ulid";
import { join } from "path"; import { join } from "path";
import { appDataPath } from "@janhq/core"; import { fs } from "@janhq/core";
/** /**
* A class that implements the InferencePlugin interface from the @janhq/core package. * A class that implements the InferencePlugin interface from the @janhq/core package.
@ -54,8 +54,10 @@ export default class JanInferencePlugin implements InferencePlugin {
* @returns {Promise<void>} A promise that resolves when the model is initialized. * @returns {Promise<void>} A promise that resolves when the model is initialized.
*/ */
async initModel(modelFileName: string): Promise<void> { async initModel(modelFileName: string): Promise<void> {
const appPath = await appDataPath(); const userSpacePath = await fs.getUserSpace();
return executeOnMain(MODULE, "initModel", join(appPath, modelFileName)); const modelFullPath = join(userSpacePath, modelFileName);
return executeOnMain(MODULE, "initModel", modelFullPath);
} }
/** /**
@ -84,7 +86,7 @@ export default class JanInferencePlugin implements InferencePlugin {
content: data.message, content: data.message,
}, },
]; ];
const recentMessages = await (data.history ?? prompts); const recentMessages = data.history ?? prompts;
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
requestInference([ requestInference([
@ -121,7 +123,7 @@ export default class JanInferencePlugin implements InferencePlugin {
message: "", message: "",
user: "assistant", user: "assistant",
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
_id: ulid(), id: ulid(),
}; };
events.emit(EventName.OnNewMessageResponse, message); events.emit(EventName.OnNewMessageResponse, message);

View File

@ -124,7 +124,7 @@ function killSubprocess(): Promise<void> {
if (subprocess) { if (subprocess) {
subprocess.kill(); subprocess.kill();
subprocess = null; subprocess = null;
console.log("Subprocess terminated."); console.debug("Subprocess terminated.");
} else { } else {
return kill(PORT, "tcp").then(console.log).catch(console.log); return kill(PORT, "tcp").then(console.log).catch(console.log);
} }

View File

@ -0,0 +1,8 @@
{
"semi": false,
"singleQuote": true,
"quoteProps": "consistent",
"trailingComma": "es5",
"endOfLine": "auto",
"plugins": ["prettier-plugin-tailwindcss"]
}

View File

@ -29,6 +29,7 @@
], ],
"dependencies": { "dependencies": {
"@janhq/core": "file:../../core", "@janhq/core": "file:../../core",
"path-browserify": "^1.0.1",
"ts-loader": "^9.5.0" "ts-loader": "^9.5.0"
} }
} }

View File

@ -1,48 +0,0 @@
import { EventName, events } from "@janhq/core";
export async function pollDownloadProgress(fileName: string) {
if (
typeof window !== "undefined" &&
typeof (window as any).electronAPI === "undefined"
) {
const intervalId = setInterval(() => {
notifyProgress(fileName, intervalId);
}, 3000);
}
}
export async function notifyProgress(
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;
}

View File

@ -1,8 +1,8 @@
export const parseToModel = (model) => { export const parseToModel = (model) => {
const modelVersions = []; const modelVersions = []
model.versions.forEach((v) => { model.versions.forEach((v) => {
const version = { const version = {
_id: `${model.author}-${v.name}`, id: `${model.author}-${v.name}`,
name: v.name, name: v.name,
quantMethod: v.quantMethod, quantMethod: v.quantMethod,
bits: v.bits, bits: v.bits,
@ -11,12 +11,12 @@ export const parseToModel = (model) => {
usecase: v.usecase, usecase: v.usecase,
downloadLink: v.downloadLink, downloadLink: v.downloadLink,
productId: model.id, productId: model.id,
}; }
modelVersions.push(version); modelVersions.push(version)
}); })
const product = { const product = {
_id: model.id, id: model.id,
name: model.name, name: model.name,
shortDescription: model.shortDescription, shortDescription: model.shortDescription,
avatarUrl: model.avatarUrl, avatarUrl: model.avatarUrl,
@ -29,9 +29,9 @@ export const parseToModel = (model) => {
type: model.type, type: model.type,
createdAt: model.createdAt, createdAt: model.createdAt,
longDescription: model.longDescription, longDescription: model.longDescription,
status: "Downloadable", status: 'Downloadable',
releaseDate: 0, releaseDate: 0,
availableVersions: modelVersions, availableVersions: modelVersions,
}; }
return product; return product
}; }

View File

@ -1,20 +1,21 @@
import { PluginType, fs, downloadFile } from "@janhq/core"; import { PluginType, fs, downloadFile } from '@janhq/core'
import { ModelPlugin } from "@janhq/core/lib/plugins"; import { ModelPlugin } from '@janhq/core/lib/plugins'
import { Model, ModelCatalog } from "@janhq/core/lib/types"; import { Model, ModelCatalog } from '@janhq/core/lib/types'
import { pollDownloadProgress } from "./helpers/cloudNative"; import { parseToModel } from './helpers/modelParser'
import { parseToModel } from "./helpers/modelParser"; import { join } from 'path'
/** /**
* A plugin for managing machine learning models. * A plugin for managing machine learning models.
*/ */
export default class JanModelPlugin implements ModelPlugin { export default class JanModelPlugin implements ModelPlugin {
private static readonly _homeDir = 'models'
/** /**
* Implements type from JanPlugin. * Implements type from JanPlugin.
* @override * @override
* @returns The type of the plugin. * @returns The type of the plugin.
*/ */
type(): PluginType { type(): PluginType {
return PluginType.Model; return PluginType.Model
} }
/** /**
@ -25,6 +26,7 @@ export default class JanModelPlugin implements ModelPlugin {
/** Cloud Native /** Cloud Native
* TODO: Fetch all downloading progresses? * TODO: Fetch all downloading progresses?
**/ **/
fs.mkdir(JanModelPlugin._homeDir)
} }
/** /**
@ -39,12 +41,13 @@ export default class JanModelPlugin implements ModelPlugin {
* @returns A Promise that resolves when the model is downloaded. * @returns A Promise that resolves when the model is downloaded.
*/ */
async downloadModel(model: Model): Promise<void> { async downloadModel(model: Model): Promise<void> {
await fs.mkdir("models"); // create corresponding directory
downloadFile(model.downloadLink, `models/${model._id}`); const directoryPath = join(JanModelPlugin._homeDir, model.productName)
/** Cloud Native await fs.mkdir(directoryPath)
* MARK: Poll Downloading Progress
**/ // path to model binary
pollDownloadProgress(model._id); const path = join(directoryPath, model.id)
downloadFile(model.downloadLink, path)
} }
/** /**
@ -52,10 +55,15 @@ export default class JanModelPlugin implements ModelPlugin {
* @param filePath - The path to the model file to delete. * @param filePath - The path to the model file to delete.
* @returns A Promise that resolves when the model is deleted. * @returns A Promise that resolves when the model is deleted.
*/ */
deleteModel(filePath: string): Promise<void> { async deleteModel(filePath: string): Promise<void> {
return fs try {
.deleteFile(`models/${filePath}`) await Promise.allSettled([
.then(() => fs.deleteFile(`models/m-${filePath}.json`)); fs.deleteFile(filePath),
fs.deleteFile(`${filePath}.json`),
])
} catch (err) {
console.error(err)
}
} }
/** /**
@ -64,30 +72,46 @@ export default class JanModelPlugin implements ModelPlugin {
* @returns A Promise that resolves when the model is saved. * @returns A Promise that resolves when the model is saved.
*/ */
async saveModel(model: Model): Promise<void> { async saveModel(model: Model): Promise<void> {
await fs.writeFile(`models/m-${model._id}.json`, JSON.stringify(model)); const directoryPath = join(JanModelPlugin._homeDir, model.productName)
const jsonFilePath = join(directoryPath, `${model.id}.json`)
try {
await fs.writeFile(jsonFilePath, JSON.stringify(model))
} catch (err) {
console.error(err)
}
} }
/** /**
* Gets all downloaded models. * Gets all downloaded models.
* @returns A Promise that resolves with an array of all models. * @returns A Promise that resolves with an array of all models.
*/ */
getDownloadedModels(): Promise<Model[]> { async getDownloadedModels(): Promise<Model[]> {
return fs const results: Model[] = []
.listFiles("models") const allDirs: string[] = await fs.listFiles(JanModelPlugin._homeDir)
.then((files: string[]) => { for (const dir of allDirs) {
return Promise.all( const modelDirPath = join(JanModelPlugin._homeDir, dir)
files const isModelDir = await fs.isDirectory(modelDirPath)
.filter((file) => /^m-.*\.json$/.test(file)) if (!isModelDir) {
.map(async (file) => { // if not a directory, ignore
const model: Model = JSON.parse( continue
await fs.readFile(`models/${file}`) }
);
return model; const jsonFiles: string[] = (await fs.listFiles(modelDirPath)).filter(
}) (file: string) => file.endsWith('.json')
); )
})
.catch((e) => fs.mkdir("models").then(() => [])); for (const json of jsonFiles) {
const model: Model = JSON.parse(
await fs.readFile(join(modelDirPath, json))
)
results.push(model)
}
}
return results
} }
/** /**
* Gets all available models. * Gets all available models.
* @returns A Promise that resolves with an array of all models. * @returns A Promise that resolves with an array of all models.
@ -96,10 +120,6 @@ export default class JanModelPlugin implements ModelPlugin {
// Add a timestamp to the URL to prevent caching // Add a timestamp to the URL to prevent caching
return import( return import(
/* webpackIgnore: true */ MODEL_CATALOG_URL + `?t=${Date.now()}` /* webpackIgnore: true */ MODEL_CATALOG_URL + `?t=${Date.now()}`
).then((module) => ).then((module) => module.default.map((e) => parseToModel(e)))
module.default.map((e) => {
return parseToModel(e);
})
);
} }
} }

View File

@ -1,16 +1,16 @@
const path = require("path"); const path = require('path')
const webpack = require("webpack"); const webpack = require('webpack')
const packageJson = require("./package.json"); const packageJson = require('./package.json')
module.exports = { module.exports = {
experiments: { outputModule: true }, experiments: { outputModule: true },
entry: "./src/index.ts", // Adjust the entry point to match your project's main file entry: './src/index.ts', // Adjust the entry point to match your project's main file
mode: "production", mode: 'production',
module: { module: {
rules: [ rules: [
{ {
test: /\.tsx?$/, test: /\.tsx?$/,
use: "ts-loader", use: 'ts-loader',
exclude: /node_modules/, exclude: /node_modules/,
}, },
], ],
@ -20,20 +20,23 @@ module.exports = {
PLUGIN_NAME: JSON.stringify(packageJson.name), PLUGIN_NAME: JSON.stringify(packageJson.name),
MODULE_PATH: JSON.stringify(`${packageJson.name}/${packageJson.module}`), MODULE_PATH: JSON.stringify(`${packageJson.name}/${packageJson.module}`),
MODEL_CATALOG_URL: JSON.stringify( MODEL_CATALOG_URL: JSON.stringify(
"https://cdn.jsdelivr.net/npm/@janhq/models@latest/dist/index.js" 'https://cdn.jsdelivr.net/npm/@janhq/models@latest/dist/index.js'
), ),
}), }),
], ],
output: { output: {
filename: "index.js", // Adjust the output file name as needed filename: 'index.js', // Adjust the output file name as needed
path: path.resolve(__dirname, "dist"), path: path.resolve(__dirname, 'dist'),
library: { type: "module" }, // Specify ESM output format library: { type: 'module' }, // Specify ESM output format
}, },
resolve: { resolve: {
extensions: [".ts", ".js"], extensions: ['.ts', '.js'],
fallback: {
path: require.resolve('path-browserify'),
},
}, },
optimization: { optimization: {
minimize: false, minimize: false,
}, },
// Add loaders and other configuration as needed for your project // Add loaders and other configuration as needed for your project
}; }

View File

@ -46,7 +46,7 @@ const BottomBar = () => {
<SystemItem <SystemItem
name="Active model:" name="Active model:"
value={ value={
activeModel?._id || ( activeModel?.id || (
<Badge themes="secondary">e to show your model</Badge> <Badge themes="secondary">e to show your model</Badge>
) )
} }

View File

@ -24,7 +24,7 @@ export default function CommandListDownloadedModel() {
const { activeModel, startModel, stopModel } = useActiveModel() const { activeModel, startModel, stopModel } = useActiveModel()
const onModelActionClick = (modelId: string) => { const onModelActionClick = (modelId: string) => {
if (activeModel && activeModel._id === modelId) { if (activeModel && activeModel.id === modelId) {
stopModel(modelId) stopModel(modelId)
} else { } else {
startModel(modelId) startModel(modelId)
@ -62,7 +62,7 @@ export default function CommandListDownloadedModel() {
<CommandItem <CommandItem
key={i} key={i}
onSelect={() => { onSelect={() => {
onModelActionClick(model._id) onModelActionClick(model.id)
setOpen(false) setOpen(false)
}} }}
> >
@ -72,7 +72,7 @@ export default function CommandListDownloadedModel() {
/> />
<div className="flex w-full items-center justify-between"> <div className="flex w-full items-center justify-between">
<span>{model.name}</span> <span>{model.name}</span>
{activeModel && activeModel._id === model._id && ( {activeModel && activeModel.id === model.id && (
<Badge themes="secondary">Active</Badge> <Badge themes="secondary">Active</Badge>
)} )}
</div> </div>

View File

@ -32,9 +32,9 @@ export default function ModalCancelDownload({
const { modelDownloadStateAtom } = useDownloadState() const { modelDownloadStateAtom } = useDownloadState()
useGetPerformanceTag() useGetPerformanceTag()
const downloadAtom = useMemo( const downloadAtom = useMemo(
() => atom((get) => get(modelDownloadStateAtom)[suitableModel._id]), () => atom((get) => get(modelDownloadStateAtom)[suitableModel.id]),
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
[suitableModel._id] [suitableModel.id]
) )
const downloadState = useAtomValue(downloadAtom) const downloadState = useAtomValue(downloadAtom)

View File

@ -1,15 +1,22 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { ReactNode, useEffect, useRef } from 'react' import { ReactNode, useEffect, useRef } from 'react'
import { events, EventName, NewMessageResponse, PluginType } from '@janhq/core' import {
events,
EventName,
NewMessageResponse,
PluginType,
ChatMessage,
} from '@janhq/core'
import { Conversation, Message, MessageStatus } from '@janhq/core'
import { ConversationalPlugin, ModelPlugin } from '@janhq/core/lib/plugins' import { ConversationalPlugin, ModelPlugin } from '@janhq/core/lib/plugins'
import { Message } from '@janhq/core/lib/types'
import { useAtomValue, useSetAtom } from 'jotai' import { useAtomValue, useSetAtom } from 'jotai'
import { useDownloadState } from '@/hooks/useDownloadState' import { useDownloadState } from '@/hooks/useDownloadState'
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels' import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
import { toChatMessage } from '@/utils/message'
import { import {
addNewMessageAtom, addNewMessageAtom,
chatMessages, chatMessages,
@ -20,11 +27,8 @@ import {
updateConversationWaitingForResponseAtom, updateConversationWaitingForResponseAtom,
userConversationsAtom, userConversationsAtom,
} from '@/helpers/atoms/Conversation.atom' } from '@/helpers/atoms/Conversation.atom'
import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom' import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
import { MessageStatus, toChatMessage } from '@/models/ChatMessage'
import { pluginManager } from '@/plugin' import { pluginManager } from '@/plugin'
import { ChatMessage, Conversation } from '@/types/chatMessage'
let currentConversation: Conversation | undefined = undefined let currentConversation: Conversation | undefined = undefined
@ -50,9 +54,7 @@ export default function EventHandler({ children }: { children: ReactNode }) {
async function handleNewMessageResponse(message: NewMessageResponse) { async function handleNewMessageResponse(message: NewMessageResponse) {
if (message.conversationId) { if (message.conversationId) {
const convo = convoRef.current.find( const convo = convoRef.current.find((e) => e.id == message.conversationId)
(e) => e._id == message.conversationId
)
if (!convo) return if (!convo) return
const newResponse = toChatMessage(message) const newResponse = toChatMessage(message)
addNewMessage(newResponse) addNewMessage(newResponse)
@ -63,11 +65,11 @@ export default function EventHandler({ children }: { children: ReactNode }) {
) { ) {
if ( if (
messageResponse.conversationId && messageResponse.conversationId &&
messageResponse._id && messageResponse.id &&
messageResponse.message messageResponse.message
) { ) {
updateMessage( updateMessage(
messageResponse._id, messageResponse.id,
messageResponse.conversationId, messageResponse.conversationId,
messageResponse.message, messageResponse.message,
MessageStatus.Pending MessageStatus.Pending
@ -77,11 +79,11 @@ export default function EventHandler({ children }: { children: ReactNode }) {
if (messageResponse.conversationId) { if (messageResponse.conversationId) {
if ( if (
!currentConversation || !currentConversation ||
currentConversation._id !== messageResponse.conversationId currentConversation.id !== messageResponse.conversationId
) { ) {
if (convoRef.current && messageResponse.conversationId) if (convoRef.current && messageResponse.conversationId)
currentConversation = convoRef.current.find( currentConversation = convoRef.current.find(
(e) => e._id == messageResponse.conversationId (e) => e.id == messageResponse.conversationId
) )
} }
@ -104,11 +106,11 @@ export default function EventHandler({ children }: { children: ReactNode }) {
if ( if (
messageResponse.conversationId && messageResponse.conversationId &&
messageResponse._id && messageResponse.id &&
messageResponse.message messageResponse.message
) { ) {
updateMessage( updateMessage(
messageResponse._id, messageResponse.id,
messageResponse.conversationId, messageResponse.conversationId,
messageResponse.message, messageResponse.message,
MessageStatus.Ready MessageStatus.Ready
@ -116,27 +118,23 @@ export default function EventHandler({ children }: { children: ReactNode }) {
} }
const convo = convoRef.current.find( const convo = convoRef.current.find(
(e) => e._id == messageResponse.conversationId (e) => e.id == messageResponse.conversationId
) )
if (convo) { if (convo) {
const messagesData = (messagesRef.current ?? [])[convo._id].map<Message>( const messagesData = (messagesRef.current ?? [])[convo.id].map<Message>(
(e: ChatMessage) => { (e: ChatMessage) => ({
return { id: e.id,
// eslint-disable-next-line @typescript-eslint/naming-convention message: e.text,
_id: e.id, user: e.senderUid,
message: e.text, updatedAt: new Date(e.createdAt).toISOString(),
user: e.senderUid, createdAt: new Date(e.createdAt).toISOString(),
updatedAt: new Date(e.createdAt).toISOString(), })
createdAt: new Date(e.createdAt).toISOString(),
}
}
) )
pluginManager pluginManager
.get<ConversationalPlugin>(PluginType.Conversational) .get<ConversationalPlugin>(PluginType.Conversational)
?.saveConversation({ ?.saveConversation({
...convo, ...convo,
// eslint-disable-next-line @typescript-eslint/naming-convention id: convo.id ?? '',
_id: convo._id ?? '',
name: convo.name ?? '', name: convo.name ?? '',
message: convo.lastMessage ?? '', message: convo.lastMessage ?? '',
messages: messagesData, messages: messagesData,
@ -153,7 +151,7 @@ export default function EventHandler({ children }: { children: ReactNode }) {
if (state && state.fileName && state.success === true) { if (state && state.fileName && state.success === true) {
state.fileName = state.fileName.replace('models/', '') state.fileName = state.fileName.replace('models/', '')
setDownloadStateSuccess(state.fileName) setDownloadStateSuccess(state.fileName)
const model = models.find((e) => e._id === state.fileName) const model = models.find((e) => e.id === state.fileName)
if (model) if (model)
pluginManager pluginManager
.get<ModelPlugin>(PluginType.Model) .get<ModelPlugin>(PluginType.Model)

View File

@ -33,14 +33,17 @@ export default function EventListenerWrapper({ children }: PropsWithChildren) {
window.electronAPI.onFileDownloadUpdate( window.electronAPI.onFileDownloadUpdate(
(_event: string, state: DownloadState | undefined) => { (_event: string, state: DownloadState | undefined) => {
if (!state) return if (!state) return
setDownloadState(state) setDownloadState({
...state,
fileName: state.fileName.split('/').pop() ?? '',
})
} }
) )
window.electronAPI.onFileDownloadError( window.electronAPI.onFileDownloadError(
(_event: string, callback: any) => { (_event: string, callback: any) => {
console.log('Download error', callback) console.error('Download error', callback)
const fileName = callback.fileName.replace('models/', '') const fileName = callback.fileName.split('/').pop() ?? ''
setDownloadStateFailed(fileName) setDownloadStateFailed(fileName)
} }
) )
@ -48,10 +51,10 @@ export default function EventListenerWrapper({ children }: PropsWithChildren) {
window.electronAPI.onFileDownloadSuccess( window.electronAPI.onFileDownloadSuccess(
(_event: string, callback: any) => { (_event: string, callback: any) => {
if (callback && callback.fileName) { if (callback && callback.fileName) {
const fileName = callback.fileName.replace('models/', '') const fileName = callback.fileName.split('/').pop() ?? ''
setDownloadStateSuccess(fileName) setDownloadStateSuccess(fileName)
const model = modelsRef.current.find((e) => e._id === fileName) const model = modelsRef.current.find((e) => e.id === fileName)
if (model) if (model)
pluginManager pluginManager
.get<ModelPlugin>(PluginType.Model) .get<ModelPlugin>(PluginType.Model)
@ -66,13 +69,13 @@ export default function EventListenerWrapper({ children }: PropsWithChildren) {
window.electronAPI.onAppUpdateDownloadUpdate( window.electronAPI.onAppUpdateDownloadUpdate(
(_event: string, progress: any) => { (_event: string, progress: any) => {
setProgress(progress.percent) setProgress(progress.percent)
console.log('app update progress:', progress.percent) console.debug('app update progress:', progress.percent)
} }
) )
window.electronAPI.onAppUpdateDownloadError( window.electronAPI.onAppUpdateDownloadError(
(_event: string, callback: any) => { (_event: string, callback: any) => {
console.log('Download error', callback) console.error('Download error', callback)
setProgress(-1) setProgress(-1)
} }
) )

View File

@ -1,9 +1,8 @@
import { ChatMessage, MessageStatus } from '@janhq/core'
import { atom } from 'jotai' import { atom } from 'jotai'
import { getActiveConvoIdAtom } from './Conversation.atom' import { getActiveConvoIdAtom } from './Conversation.atom'
import { ChatMessage, MessageStatus } from '@/models/ChatMessage'
/** /**
* Stores all chat messages for all conversations * Stores all chat messages for all conversations
*/ */

View File

@ -1,8 +1,6 @@
import { Conversation, ConversationState } from '@/types/chatMessage' import { Conversation, ConversationState } from '@janhq/core'
import { atom } from 'jotai' import { atom } from 'jotai'
// import { MainViewState, setMainViewStateAtom } from './MainView.atom'
/** /**
* Stores the current active conversation id. * Stores the current active conversation id.
*/ */
@ -78,13 +76,13 @@ export const updateConversationHasMoreAtom = atom(
export const updateConversationAtom = atom( export const updateConversationAtom = atom(
null, null,
(get, set, conversation: Conversation) => { (get, set, conversation: Conversation) => {
const id = conversation._id const id = conversation.id
if (!id) return if (!id) return
const convo = get(userConversationsAtom).find((c) => c._id === id) const convo = get(userConversationsAtom).find((c) => c.id === id)
if (!convo) return if (!convo) return
const newConversations: Conversation[] = get(userConversationsAtom).map( const newConversations: Conversation[] = get(userConversationsAtom).map(
(c) => (c._id === id ? conversation : c) (c) => (c.id === id ? conversation : c)
) )
// sort new conversations based on updated at // sort new conversations based on updated at
@ -103,5 +101,5 @@ export const updateConversationAtom = atom(
*/ */
export const userConversationsAtom = atom<Conversation[]>([]) export const userConversationsAtom = atom<Conversation[]>([])
export const currentConversationAtom = atom<Conversation | undefined>((get) => export const currentConversationAtom = atom<Conversation | undefined>((get) =>
get(userConversationsAtom).find((c) => c._id === get(getActiveConvoIdAtom)) get(userConversationsAtom).find((c) => c.id === get(getActiveConvoIdAtom))
) )

View File

@ -10,6 +10,7 @@ import { toaster } from '@/containers/Toast'
import { useGetDownloadedModels } from './useGetDownloadedModels' import { useGetDownloadedModels } from './useGetDownloadedModels'
import { pluginManager } from '@/plugin' import { pluginManager } from '@/plugin'
import { join } from 'path'
const activeAssistantModelAtom = atom<Model | undefined>(undefined) const activeAssistantModelAtom = atom<Model | undefined>(undefined)
@ -21,16 +22,16 @@ export function useActiveModel() {
const { downloadedModels } = useGetDownloadedModels() const { downloadedModels } = useGetDownloadedModels()
const startModel = async (modelId: string) => { const startModel = async (modelId: string) => {
if (activeModel && activeModel._id === modelId) { if (activeModel && activeModel.id === modelId) {
console.debug(`Model ${modelId} is already init. Ignore..`) console.debug(`Model ${modelId} is already init. Ignore..`)
return return
} }
setStateModel({ state: 'start', loading: true, model: modelId }) setStateModel({ state: 'start', loading: true, model: modelId })
const model = await downloadedModels.find((e) => e._id === modelId) const model = downloadedModels.find((e) => e.id === modelId)
if (!modelId) { if (!model) {
alert(`Model ${modelId} not found! Please re-download the model first.`) alert(`Model ${modelId} not found! Please re-download the model first.`)
setStateModel(() => ({ setStateModel(() => ({
state: 'start', state: 'start',
@ -42,8 +43,8 @@ export function useActiveModel() {
const currentTime = Date.now() const currentTime = Date.now()
console.debug('Init model: ', modelId) console.debug('Init model: ', modelId)
const path = join('models', model.productName, modelId)
const res = await initModel(`models/${modelId}`) const res = await initModel(path)
if (res?.error) { if (res?.error) {
const errorMessage = `${res.error}` const errorMessage = `${res.error}`
alert(errorMessage) alert(errorMessage)

View File

@ -1,7 +1,6 @@
import { PluginType } from '@janhq/core' import { PluginType } from '@janhq/core'
import { Conversation, Model } from '@janhq/core'
import { ConversationalPlugin } from '@janhq/core/lib/plugins' import { ConversationalPlugin } from '@janhq/core/lib/plugins'
import { Model } from '@janhq/core/lib/types'
import { useAtom, useSetAtom } from 'jotai' import { useAtom, useSetAtom } from 'jotai'
import { generateConversationId } from '@/utils/conversation' import { generateConversationId } from '@/utils/conversation'
@ -12,7 +11,6 @@ import {
addNewConversationStateAtom, addNewConversationStateAtom,
} from '@/helpers/atoms/Conversation.atom' } from '@/helpers/atoms/Conversation.atom'
import { pluginManager } from '@/plugin' import { pluginManager } from '@/plugin'
import { Conversation } from '@/types/chatMessage'
export const useCreateConversation = () => { export const useCreateConversation = () => {
const [userConversations, setUserConversations] = useAtom( const [userConversations, setUserConversations] = useAtom(
@ -24,15 +22,15 @@ export const useCreateConversation = () => {
const requestCreateConvo = async (model: Model) => { const requestCreateConvo = async (model: Model) => {
const conversationName = model.name const conversationName = model.name
const mappedConvo: Conversation = { const mappedConvo: Conversation = {
// eslint-disable-next-line @typescript-eslint/naming-convention id: generateConversationId(),
_id: generateConversationId(), modelId: model.id,
modelId: model._id,
name: conversationName, name: conversationName,
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
messages: [],
} }
addNewConvoState(mappedConvo._id, { addNewConvoState(mappedConvo.id, {
hasMore: true, hasMore: true,
waitingForResponse: false, waitingForResponse: false,
}) })
@ -45,7 +43,7 @@ export const useCreateConversation = () => {
messages: [], messages: [],
}) })
setUserConversations([mappedConvo, ...userConversations]) setUserConversations([mappedConvo, ...userConversations])
setActiveConvoId(mappedConvo._id) setActiveConvoId(mappedConvo.id)
} }
return { return {

View File

@ -41,7 +41,7 @@ export default function useDeleteConversation() {
.get<ConversationalPlugin>(PluginType.Conversational) .get<ConversationalPlugin>(PluginType.Conversational)
?.deleteConversation(activeConvoId) ?.deleteConversation(activeConvoId)
const currentConversations = userConversations.filter( const currentConversations = userConversations.filter(
(c) => c._id !== activeConvoId (c) => c.id !== activeConvoId
) )
setUserConversations(currentConversations) setUserConversations(currentConversations)
deleteMessages(activeConvoId) deleteMessages(activeConvoId)
@ -50,7 +50,7 @@ export default function useDeleteConversation() {
description: `Delete chat with ${activeModel?.name} has been completed`, description: `Delete chat with ${activeModel?.name} has been completed`,
}) })
if (currentConversations.length > 0) { if (currentConversations.length > 0) {
setActiveConvoId(currentConversations[0]._id) setActiveConvoId(currentConversations[0].id)
} else { } else {
setActiveConvoId(undefined) setActiveConvoId(undefined)
} }

View File

@ -7,20 +7,20 @@ import { toaster } from '@/containers/Toast'
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels' import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
import { pluginManager } from '@/plugin/PluginManager' import { pluginManager } from '@/plugin/PluginManager'
import { join } from 'path'
export default function useDeleteModel() { export default function useDeleteModel() {
const { setDownloadedModels, downloadedModels } = useGetDownloadedModels() const { setDownloadedModels, downloadedModels } = useGetDownloadedModels()
const deleteModel = async (model: Model) => { const deleteModel = async (model: Model) => {
await pluginManager const path = join('models', model.productName, model.id)
.get<ModelPlugin>(PluginType.Model) await pluginManager.get<ModelPlugin>(PluginType.Model)?.deleteModel(path)
?.deleteModel(model._id)
// reload models // reload models
setDownloadedModels(downloadedModels.filter((e) => e._id !== model._id)) setDownloadedModels(downloadedModels.filter((e) => e.id !== model.id))
toaster({ toaster({
title: 'Delete a Model', title: 'Delete a Model',
description: `Model ${model._id} has been deleted.`, description: `Model ${model.id} has been deleted.`,
}) })
} }

View File

@ -21,7 +21,7 @@ export default function useDownloadModel() {
): Model => { ): Model => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
_id: modelVersion._id, id: modelVersion.id,
name: modelVersion.name, name: modelVersion.name,
quantMethod: modelVersion.quantMethod, quantMethod: modelVersion.quantMethod,
bits: modelVersion.bits, bits: modelVersion.bits,
@ -31,7 +31,7 @@ export default function useDownloadModel() {
downloadLink: modelVersion.downloadLink, downloadLink: modelVersion.downloadLink,
startDownloadAt: modelVersion.startDownloadAt, startDownloadAt: modelVersion.startDownloadAt,
finishDownloadAt: modelVersion.finishDownloadAt, finishDownloadAt: modelVersion.finishDownloadAt,
productId: model._id, productId: model.id,
productName: model.name, productName: model.name,
shortDescription: model.shortDescription, shortDescription: model.shortDescription,
longDescription: model.longDescription, longDescription: model.longDescription,
@ -53,7 +53,7 @@ export default function useDownloadModel() {
) => { ) => {
// set an initial download state // set an initial download state
setDownloadState({ setDownloadState({
modelId: modelVersion._id, modelId: modelVersion.id,
time: { time: {
elapsed: 0, elapsed: 0,
remaining: 0, remaining: 0,
@ -64,7 +64,7 @@ export default function useDownloadModel() {
total: 0, total: 0,
transferred: 0, transferred: 0,
}, },
fileName: modelVersion._id, fileName: modelVersion.id,
}) })
modelVersion.startDownloadAt = Date.now() modelVersion.startDownloadAt = Date.now()

View File

@ -1,13 +1,9 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { Model, Conversation } from '@janhq/core'
import { Model } from '@janhq/core/lib/types'
import { useAtomValue } from 'jotai' import { useAtomValue } from 'jotai'
import { useActiveModel } from './useActiveModel' import { useActiveModel } from './useActiveModel'
import { useGetDownloadedModels } from './useGetDownloadedModels' import { useGetDownloadedModels } from './useGetDownloadedModels'
import { currentConversationAtom } from '@/helpers/atoms/Conversation.atom' import { currentConversationAtom } from '@/helpers/atoms/Conversation.atom'
import { Conversation } from '@/types/chatMessage'
export default function useGetInputState() { export default function useGetInputState() {
const [inputState, setInputState] = useState<InputType>('loading') const [inputState, setInputState] = useState<InputType>('loading')
@ -27,7 +23,7 @@ export default function useGetInputState() {
// check if convo model id is in downloaded models // check if convo model id is in downloaded models
const isModelAvailable = downloadedModels.some( const isModelAvailable = downloadedModels.some(
(model) => model._id === convo.modelId (model) => model.id === convo.modelId
) )
if (!isModelAvailable) { if (!isModelAvailable) {
@ -36,7 +32,7 @@ export default function useGetInputState() {
return return
} }
if (convo.modelId !== currentModel._id) { if (convo.modelId !== currentModel.id) {
// in case convo model and active model is different, // in case convo model and active model is different,
// ask user to init the required model // ask user to init the required model
setInputState('model-mismatch') setInputState('model-mismatch')

View File

@ -1,16 +1,16 @@
import { PluginType } from '@janhq/core' import { PluginType, ChatMessage, ConversationState } from '@janhq/core'
import { ConversationalPlugin } from '@janhq/core/lib/plugins' import { ConversationalPlugin } from '@janhq/core/lib/plugins'
import { Conversation } from '@janhq/core/lib/types' import { Conversation } from '@janhq/core/lib/types'
import { useSetAtom } from 'jotai' import { useSetAtom } from 'jotai'
import { toChatMessage } from '@/utils/message'
import { setConvoMessagesAtom } from '@/helpers/atoms/ChatMessage.atom' import { setConvoMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
import { import {
conversationStatesAtom, conversationStatesAtom,
userConversationsAtom, userConversationsAtom,
} from '@/helpers/atoms/Conversation.atom' } from '@/helpers/atoms/Conversation.atom'
import { toChatMessage } from '@/models/ChatMessage'
import { pluginManager } from '@/plugin/PluginManager' import { pluginManager } from '@/plugin/PluginManager'
import { ChatMessage, ConversationState } from '@/types/chatMessage'
const useGetUserConversations = () => { const useGetUserConversations = () => {
const setConversationStates = useSetAtom(conversationStatesAtom) const setConversationStates = useSetAtom(conversationStatesAtom)
@ -24,19 +24,19 @@ const useGetUserConversations = () => {
?.getConversations() ?.getConversations()
const convoStates: Record<string, ConversationState> = {} const convoStates: Record<string, ConversationState> = {}
convos?.forEach((convo) => { convos?.forEach((convo) => {
convoStates[convo._id ?? ''] = { convoStates[convo.id ?? ''] = {
hasMore: true, hasMore: true,
waitingForResponse: false, waitingForResponse: false,
} }
setConvoMessages( setConvoMessages(
convo.messages.map<ChatMessage>((msg) => toChatMessage(msg)), convo.messages.map<ChatMessage>((msg) => toChatMessage(msg)),
convo._id ?? '' convo.id ?? ''
) )
}) })
setConversationStates(convoStates) setConversationStates(convoStates)
setConversations(convos ?? []) setConversations(convos ?? [])
} catch (error) { } catch (error) {
console.log(error) console.error(error)
} }
} }

View File

@ -4,13 +4,13 @@ import {
NewMessageRequest, NewMessageRequest,
PluginType, PluginType,
events, events,
ChatMessage,
Message,
Conversation,
MessageSenderType,
} from '@janhq/core' } from '@janhq/core'
import { ConversationalPlugin, InferencePlugin } from '@janhq/core/lib/plugins' import { ConversationalPlugin, InferencePlugin } from '@janhq/core/lib/plugins'
import { Message } from '@janhq/core/lib/types'
import { useAtom, useAtomValue, useSetAtom } from 'jotai' import { useAtom, useAtomValue, useSetAtom } from 'jotai'
import { currentPromptAtom } from '@/containers/Providers/Jotai' import { currentPromptAtom } from '@/containers/Providers/Jotai'
import { ulid } from 'ulid' import { ulid } from 'ulid'
import { import {
@ -22,10 +22,8 @@ import {
updateConversationAtom, updateConversationAtom,
updateConversationWaitingForResponseAtom, updateConversationWaitingForResponseAtom,
} from '@/helpers/atoms/Conversation.atom' } from '@/helpers/atoms/Conversation.atom'
import { MessageSenderType, toChatMessage } from '@/models/ChatMessage'
import { pluginManager } from '@/plugin/PluginManager' import { pluginManager } from '@/plugin/PluginManager'
import { ChatMessage, Conversation } from '@/types/chatMessage' import { toChatMessage } from '@/utils/message'
export default function useSendChatMessage() { export default function useSendChatMessage() {
const currentConvo = useAtomValue(currentConversationAtom) const currentConvo = useAtomValue(currentConversationAtom)
@ -59,7 +57,7 @@ export default function useSendChatMessage() {
if ( if (
result?.message && result?.message &&
result.message.split(' ').length <= 10 && result.message.split(' ').length <= 10 &&
conv?._id conv?.id
) { ) {
const updatedConv = { const updatedConv = {
...conv, ...conv,
@ -73,7 +71,7 @@ export default function useSendChatMessage() {
name: updatedConv.name ?? '', name: updatedConv.name ?? '',
message: updatedConv.lastMessage ?? '', message: updatedConv.lastMessage ?? '',
messages: currentMessages.map<Message>((e: ChatMessage) => ({ messages: currentMessages.map<Message>((e: ChatMessage) => ({
_id: e.id, id: e.id,
message: e.text, message: e.text,
user: e.senderUid, user: e.senderUid,
updatedAt: new Date(e.createdAt).toISOString(), updatedAt: new Date(e.createdAt).toISOString(),
@ -87,7 +85,11 @@ export default function useSendChatMessage() {
} }
const sendChatMessage = async () => { const sendChatMessage = async () => {
const convoId = currentConvo?._id as string const convoId = currentConvo?.id
if (!convoId) {
console.error('No conversation id')
return
}
setCurrentPrompt('') setCurrentPrompt('')
updateConvWaiting(convoId, true) updateConvWaiting(convoId, true)
@ -106,8 +108,7 @@ export default function useSendChatMessage() {
} as MessageHistory, } as MessageHistory,
]) ])
const newMessage: NewMessageRequest = { const newMessage: NewMessageRequest = {
// eslint-disable-next-line @typescript-eslint/naming-convention id: ulid(),
_id: ulid(),
conversationId: convoId, conversationId: convoId,
message: prompt, message: prompt,
user: MessageSenderType.User, user: MessageSenderType.User,

View File

@ -1,84 +0,0 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { NewMessageResponse } from '@janhq/core'
import { Message } from '@janhq/core/lib/types'
export enum MessageType {
Text = 'Text',
Image = 'Image',
ImageWithText = 'ImageWithText',
Error = 'Error',
}
export enum MessageSenderType {
Ai = 'assistant',
User = 'user',
}
export enum MessageStatus {
Ready = 'ready',
Pending = 'pending',
}
export interface ChatMessage {
id: string
conversationId: string
messageType: MessageType
messageSenderType: MessageSenderType
senderUid: string
senderName: string
senderAvatarUrl: string
text: string | undefined
imageUrls?: string[] | undefined
createdAt: number
status: MessageStatus
}
export interface RawMessage {
_id?: string
conversationId?: string
user?: string
avatar?: string
message?: string
createdAt?: string
updatedAt?: string
}
export const toChatMessage = (
m: RawMessage | Message | NewMessageResponse,
conversationId?: string
): ChatMessage => {
const createdAt = new Date(m.createdAt ?? '').getTime()
const imageUrls: string[] = []
const imageUrl = undefined
if (imageUrl) {
imageUrls.push(imageUrl)
}
const messageType = MessageType.Text
const messageSenderType =
m.user === 'user' ? MessageSenderType.User : MessageSenderType.Ai
const content = m.message ?? ''
const senderName = m.user === 'user' ? 'You' : 'Assistant'
return {
id: (m._id ?? 0).toString(),
conversationId: (
(m as RawMessage | NewMessageResponse)?.conversationId ??
conversationId ??
0
).toString(),
messageType: messageType,
messageSenderType: messageSenderType,
senderUid: m.user?.toString() || '0',
senderName: senderName,
senderAvatarUrl:
m.user === 'user' ? 'icons/avatar.svg' : 'icons/app_icon.svg',
text: content,
imageUrls: imageUrls,
createdAt: createdAt,
status: MessageStatus.Ready,
}
}

View File

@ -1,7 +1,6 @@
import React, { forwardRef } from 'react' import React, { forwardRef } from 'react'
import { ChatMessage } from '@janhq/core'
import SimpleTextMessage from '../SimpleTextMessage' import SimpleTextMessage from '../SimpleTextMessage'
import { ChatMessage } from '@/types/chatMessage'
type Props = { type Props = {
message: ChatMessage message: ChatMessage

View File

@ -46,16 +46,16 @@ export default function HistoryList() {
console.debug('modelId is undefined') console.debug('modelId is undefined')
return return
} }
const model = downloadedModels.find((e) => e._id === convo.modelId) const model = downloadedModels.find((e) => e.id === convo.modelId)
if (convo == null) { if (convo == null) {
console.debug('modelId is undefined') console.debug('modelId is undefined')
return return
} }
if (model != null) { if (model != null) {
startModel(model._id) startModel(model.id)
} }
if (activeConvoId !== convo._id) { if (activeConvoId !== convo.id) {
setActiveConvoId(convo._id) setActiveConvoId(convo.id)
} }
} }
@ -88,7 +88,7 @@ export default function HistoryList() {
key={i} key={i}
className={twMerge( className={twMerge(
'relative flex cursor-pointer flex-col border-b border-border px-4 py-2 hover:bg-secondary/20', 'relative flex cursor-pointer flex-col border-b border-border px-4 py-2 hover:bg-secondary/20',
activeConvoId === convo._id && 'bg-secondary-10' activeConvoId === convo.id && 'bg-secondary-10'
)} )}
onClick={() => handleActiveModel(convo as Conversation)} onClick={() => handleActiveModel(convo as Conversation)}
> >
@ -100,7 +100,7 @@ export default function HistoryList() {
<p className="mt-1 line-clamp-2 text-xs"> <p className="mt-1 line-clamp-2 text-xs">
{convo?.lastMessage ?? 'No new message'} {convo?.lastMessage ?? 'No new message'}
</p> </p>
{activeModel && activeConvoId === convo._id && ( {activeModel && activeConvoId === convo.id && (
<m.div <m.div
className="absolute right-0 top-0 h-full w-1 bg-primary/50" className="absolute right-0 top-0 h-full w-1 bg-primary/50"
layoutId="active-convo" layoutId="active-convo"

View File

@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
import React, { useState } from 'react' import React, { useState } from 'react'
import { MessageSenderType, MessageStatus } from '@janhq/core'
import hljs from 'highlight.js' import hljs from 'highlight.js'
import { Marked } from 'marked' import { Marked } from 'marked'
@ -15,8 +16,6 @@ import BubbleLoader from '@/containers/Loader/Bubble'
import { displayDate } from '@/utils/datetime' import { displayDate } from '@/utils/datetime'
import { MessageSenderType, MessageStatus } from '@/models/ChatMessage'
type Props = { type Props = {
avatarUrl: string avatarUrl: string
senderName: string senderName: string

View File

@ -56,7 +56,7 @@ const ChatScreen = () => {
const conversations = useAtomValue(userConversationsAtom) const conversations = useAtomValue(userConversationsAtom)
const isEnableChat = (currentConvo && activeModel) || conversations.length > 0 const isEnableChat = (currentConvo && activeModel) || conversations.length > 0
const [isModelAvailable, setIsModelAvailable] = useState( const [isModelAvailable, setIsModelAvailable] = useState(
downloadedModels.some((x) => x._id === currentConvo?.modelId) downloadedModels.some((x) => x.id === currentConvo?.modelId)
) )
const textareaRef = useRef<HTMLTextAreaElement>(null) const textareaRef = useRef<HTMLTextAreaElement>(null)
@ -72,7 +72,7 @@ const ChatScreen = () => {
useEffect(() => { useEffect(() => {
setIsModelAvailable( setIsModelAvailable(
downloadedModels.some((x) => x._id === currentConvo?.modelId) downloadedModels.some((x) => x.id === currentConvo?.modelId)
) )
}, [currentConvo, downloadedModels]) }, [currentConvo, downloadedModels])
@ -196,7 +196,7 @@ const ChatScreen = () => {
disabled={ disabled={
!activeModel || !activeModel ||
stateModel.loading || stateModel.loading ||
activeModel._id !== currentConvo?.modelId activeModel.id !== currentConvo?.modelId
} }
value={currentPrompt} value={currentPrompt}
onChange={(e) => { onChange={(e) => {

View File

@ -105,7 +105,7 @@ const ExploreModelItem = forwardRef<HTMLDivElement, Props>(({ model }, ref) => {
<ModelVersionList <ModelVersionList
model={model} model={model}
versions={model.availableVersions} versions={model.availableVersions}
recommendedVersion={suitableModel?._id ?? ''} recommendedVersion={suitableModel?.id ?? ''}
/> />
)} )}
</div> </div>

View File

@ -35,8 +35,8 @@ const ExploreModelItemHeader: React.FC<Props> = ({
const { performanceTag, title, getPerformanceForModel } = const { performanceTag, title, getPerformanceForModel } =
useGetPerformanceTag() useGetPerformanceTag()
const downloadAtom = useMemo( const downloadAtom = useMemo(
() => atom((get) => get(modelDownloadStateAtom)[suitableModel._id]), () => atom((get) => get(modelDownloadStateAtom)[suitableModel.id]),
[suitableModel._id] [suitableModel.id]
) )
const downloadState = useAtomValue(downloadAtom) const downloadState = useAtomValue(downloadAtom)
const { setMainViewState } = useMainViewState() const { setMainViewState } = useMainViewState()
@ -52,7 +52,7 @@ const ExploreModelItemHeader: React.FC<Props> = ({
}, [exploreModel, suitableModel]) }, [exploreModel, suitableModel])
const isDownloaded = const isDownloaded =
downloadedModels.find((model) => model._id === suitableModel._id) != null downloadedModels.find((model) => model.id === suitableModel.id) != null
let downloadButton = ( let downloadButton = (
<Button onClick={() => onDownloadClick()}> <Button onClick={() => onDownloadClick()}>

View File

@ -1,18 +1,14 @@
import { ModelCatalog } from '@janhq/core/lib/types' import { ModelCatalog } from '@janhq/core/lib/types'
import ExploreModelItem from '@/screens/ExploreModels/ExploreModelItem' import ExploreModelItem from '@/screens/ExploreModels/ExploreModelItem'
type Props = { type Props = {
models: ModelCatalog[] models: ModelCatalog[]
} }
export default function ExploreModelList(props: Props) { const ExploreModelList: React.FC<Props> = ({ models }) => (
const { models } = props <div className="relative h-full w-full flex-shrink-0">
return ( {models?.map((item, i) => <ExploreModelItem key={item.id} model={item} />)}
<div className="relative h-full w-full flex-shrink-0"> </div>
{models?.map((item, i) => ( )
<ExploreModelItem key={item._id} model={item} />
))} export default ExploreModelList
</div>
)
}

View File

@ -1,22 +1,15 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
import { ModelCatalog, ModelVersion } from '@janhq/core/lib/types' import { ModelCatalog, ModelVersion } from '@janhq/core/lib/types'
import { Button } from '@janhq/uikit' import { Button } from '@janhq/uikit'
import { Badge } from '@janhq/uikit' import { Badge } from '@janhq/uikit'
import { atom, useAtomValue } from 'jotai' import { atom, useAtomValue } from 'jotai'
import ModalCancelDownload from '@/containers/ModalCancelDownload' import ModalCancelDownload from '@/containers/ModalCancelDownload'
import { MainViewState } from '@/constants/screens' import { MainViewState } from '@/constants/screens'
import useDownloadModel from '@/hooks/useDownloadModel' import useDownloadModel from '@/hooks/useDownloadModel'
import { useDownloadState } from '@/hooks/useDownloadState' import { useDownloadState } from '@/hooks/useDownloadState'
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels' import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
import { useMainViewState } from '@/hooks/useMainViewState' import { useMainViewState } from '@/hooks/useMainViewState'
import { toGigabytes } from '@/utils/converter' import { toGigabytes } from '@/utils/converter'
type Props = { type Props = {
@ -30,13 +23,13 @@ const ModelVersionItem: React.FC<Props> = ({ model, modelVersion }) => {
const { downloadedModels } = useGetDownloadedModels() const { downloadedModels } = useGetDownloadedModels()
const { setMainViewState } = useMainViewState() const { setMainViewState } = useMainViewState()
const isDownloaded = const isDownloaded =
downloadedModels.find((model) => model._id === modelVersion._id) != null downloadedModels.find((model) => model.id === modelVersion.id) != null
const { modelDownloadStateAtom, downloadStates } = useDownloadState() const { modelDownloadStateAtom, downloadStates } = useDownloadState()
const downloadAtom = useMemo( const downloadAtom = useMemo(
() => atom((get) => get(modelDownloadStateAtom)[modelVersion._id ?? '']), () => atom((get) => get(modelDownloadStateAtom)[modelVersion.id ?? '']),
[modelVersion._id] [modelVersion.id]
) )
const downloadState = useAtomValue(downloadAtom) const downloadState = useAtomValue(downloadAtom)

View File

@ -17,10 +17,10 @@ export default function ModelVersionList({
<div className="pt-4"> <div className="pt-4">
{versions.map((item) => ( {versions.map((item) => (
<ModelVersionItem <ModelVersionItem
key={item._id} key={item.id}
model={model} model={model}
modelVersion={item} modelVersion={item}
isRecommended={item._id === recommendedVersion} isRecommended={item.id === recommendedVersion}
/> />
))} ))}
</div> </div>

View File

@ -40,7 +40,7 @@ const MyModelsScreen = () => {
if (downloadedModels.length === 0) return <BlankStateMyModel /> if (downloadedModels.length === 0) return <BlankStateMyModel />
const onModelActionClick = (modelId: string) => { const onModelActionClick = (modelId: string) => {
if (activeModel && activeModel._id === modelId) { if (activeModel && activeModel.id === modelId) {
stopModel(modelId) stopModel(modelId)
} else { } else {
startModel(modelId) startModel(modelId)
@ -53,7 +53,7 @@ const MyModelsScreen = () => {
<div className="p-4" data-test-id="testid-my-models"> <div className="p-4" data-test-id="testid-my-models">
<div className="grid grid-cols-2 gap-4 lg:grid-cols-3"> <div className="grid grid-cols-2 gap-4 lg:grid-cols-3">
{downloadedModels.map((model, i) => { {downloadedModels.map((model, i) => {
const isActiveModel = stateModel.model === model._id const isActiveModel = stateModel.model === model.id
return ( return (
<div <div
key={i} key={i}
@ -114,7 +114,7 @@ const MyModelsScreen = () => {
themes="danger" themes="danger"
onClick={() => onClick={() =>
setTimeout(async () => { setTimeout(async () => {
await stopModel(model._id) await stopModel(model.id)
deleteModel(model) deleteModel(model)
}, 500) }, 500)
} }
@ -135,7 +135,7 @@ const MyModelsScreen = () => {
} }
className="capitalize" className="capitalize"
loading={isActiveModel ? stateModel.loading : false} loading={isActiveModel ? stateModel.loading : false}
onClick={() => onModelActionClick(model._id)} onClick={() => onModelActionClick(model.id)}
> >
{isActiveModel ? stateModel.state : 'Start'} {isActiveModel ? stateModel.state : 'Start'}
&nbsp;Model &nbsp;Model

View File

@ -2,10 +2,10 @@ import * as cn from './cloudNativeService'
import { EventEmitter } from './eventsService' import { EventEmitter } from './eventsService'
export const setupCoreServices = () => { export const setupCoreServices = () => {
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
console.log('undefine', window) console.debug('undefine', window)
return return
} else { } else {
console.log('Setting up core services') console.debug('Setting up core services')
} }
if (!window.corePlugin) { if (!window.corePlugin) {
window.corePlugin = { window.corePlugin = {

View File

@ -1,63 +0,0 @@
/* eslint-disable @typescript-eslint/naming-convention */
enum MessageType {
Text = 'Text',
Image = 'Image',
ImageWithText = 'ImageWithText',
Error = 'Error',
}
export enum MessageSenderType {
Ai = 'assistant',
User = 'user',
}
enum MessageStatus {
Ready = 'ready',
Pending = 'pending',
}
interface ChatMessage {
id: string
conversationId: string
messageType: MessageType
messageSenderType: MessageSenderType
senderUid: string
senderName: string
senderAvatarUrl: string
text: string | undefined
imageUrls?: string[] | undefined
createdAt: number
status: MessageStatus
}
interface RawMessage {
_id?: string
conversationId?: string
user?: string
avatar?: string
message?: string
createdAt?: string
updatedAt?: string
}
interface Conversation {
_id: string
modelId?: string
name?: string
image?: string
message?: string
lastMessage?: string
summary?: string
createdAt?: string
updatedAt?: string
botId?: string
}
/**
* Store the state of conversation like fetching, waiting for response, etc.
*/
type ConversationState = {
hasMore: boolean
waitingForResponse: boolean
error?: Error
}

View File

@ -1,8 +1,8 @@
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
import { ModelCatalog, ModelVersion } from '@janhq/core/lib/types' import { ModelCatalog, ModelVersion } from '@janhq/core'
export const dummyModel: ModelCatalog = { export const dummyModel: ModelCatalog = {
_id: 'aladar/TinyLLama-v0-GGUF', id: 'aladar/TinyLLama-v0-GGUF',
name: 'TinyLLama-v0-GGUF', name: 'TinyLLama-v0-GGUF',
shortDescription: 'TinyLlama-1.1B-Chat-v0.3-GGUF', shortDescription: 'TinyLlama-1.1B-Chat-v0.3-GGUF',
longDescription: 'https://huggingface.co/aladar/TinyLLama-v0-GGUF/tree/main', longDescription: 'https://huggingface.co/aladar/TinyLLama-v0-GGUF/tree/main',
@ -16,7 +16,7 @@ export const dummyModel: ModelCatalog = {
createdAt: 0, createdAt: 0,
availableVersions: [ availableVersions: [
{ {
_id: 'tinyllama-1.1b-chat-v0.3.Q2_K.gguf', id: 'tinyllama-1.1b-chat-v0.3.Q2_K.gguf',
name: 'tinyllama-1.1b-chat-v0.3.Q2_K.gguf', name: 'tinyllama-1.1b-chat-v0.3.Q2_K.gguf',
quantMethod: '', quantMethod: '',
bits: 2, bits: 2,

View File

@ -1,27 +1,48 @@
import { ChatMessage } from '@/models/ChatMessage' import {
ChatMessage,
Message,
MessageSenderType,
MessageStatus,
MessageType,
NewMessageResponse,
RawMessage,
} from '@janhq/core'
/** export const toChatMessage = (
* Util function to merge two array of messages and remove duplicates. m: RawMessage | Message | NewMessageResponse,
* Also preserve the order conversationId?: string
* ): ChatMessage => {
* @param arr1 Message array 1 const createdAt = new Date(m.createdAt ?? '').getTime()
* @param arr2 Message array 2 const imageUrls: string[] = []
* @returns Merged array of messages const imageUrl = undefined
*/ if (imageUrl) {
export function mergeAndRemoveDuplicates( imageUrls.push(imageUrl)
arr1: ChatMessage[],
arr2: ChatMessage[]
): ChatMessage[] {
const mergedArray = arr1.concat(arr2)
const uniqueIdMap = new Map<string, boolean>()
const result: ChatMessage[] = []
for (const message of mergedArray) {
if (!uniqueIdMap.has(message.id)) {
uniqueIdMap.set(message.id, true)
result.push(message)
}
} }
return result.reverse() const messageType = MessageType.Text
const messageSenderType =
m.user === 'user' ? MessageSenderType.User : MessageSenderType.Ai
const content = m.message ?? ''
const senderName = m.user === 'user' ? 'You' : 'Assistant'
return {
id: (m.id ?? 0).toString(),
conversationId: (
(m as RawMessage | NewMessageResponse)?.conversationId ??
conversationId ??
0
).toString(),
messageType: messageType,
messageSenderType: messageSenderType,
senderUid: m.user?.toString() || '0',
senderName: senderName,
senderAvatarUrl:
m.user === 'user' ? 'icons/avatar.svg' : 'icons/app_icon.svg',
text: content,
imageUrls: imageUrls,
createdAt: createdAt,
status: MessageStatus.Ready,
}
} }