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.
*/
export type NewMessageRequest = {
_id?: string;
id?: string;
conversationId?: string;
user?: string;
avatar?: string;
@ -34,7 +34,7 @@ export type NewMessageRequest = {
* The `NewMessageRequest` type defines the shape of a new message request object.
*/
export type NewMessageResponse = {
_id?: string;
id?: string;
conversationId?: string;
user?: 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.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.
* @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);
export const fs = {
isDirectory,
getUserSpace,
writeFile,
readFile,
listFiles,

View File

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

View File

@ -1,5 +1,5 @@
export interface Conversation {
_id: string;
id: string;
modelId?: string;
botId?: string;
name: string;
@ -8,11 +8,23 @@ export interface Conversation {
createdAt?: string;
updatedAt?: string;
messages: Message[];
lastMessage?: string;
}
export interface Message {
id: string;
message?: string;
user?: string;
_id: string;
createdAt?: string;
updatedAt?: string;
}
export interface RawMessage {
id?: string;
conversationId?: string;
user?: string;
avatar?: string;
message?: string;
createdAt?: string;
updatedAt?: string;
}
@ -22,7 +34,7 @@ export interface Model {
* Combination of owner and model name.
* Being used as file name. MUST be unique.
*/
_id: string;
id: string;
name: string;
quantMethod: string;
bits: number;
@ -51,7 +63,7 @@ export interface Model {
tags: string[];
}
export interface ModelCatalog {
_id: string;
id: string;
name: string;
shortDescription: string;
avatarUrl: string;
@ -74,7 +86,7 @@ export type ModelVersion = {
* Combination of owner and model name.
* Being used as file name. Should be unique.
*/
_id: string;
id: string;
name: string;
quantMethod: string;
bits: number;
@ -89,3 +101,40 @@ export type ModelVersion = {
startDownloadAt?: 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 { DownloadManager } from "../managers/download";
import { resolve, join } from "path";
import { WindowManager } from "../managers/window";
import request from "request";
import { createWriteStream, unlink } from "fs";
const progress = require("request-progress");
import { app, ipcMain } from 'electron'
import { DownloadManager } from '../managers/download'
import { resolve, join } from 'path'
import { WindowManager } from '../managers/window'
import request from 'request'
import { createWriteStream, unlink } from 'fs'
const progress = require('request-progress')
export function handleDownloaderIPCs() {
/**
@ -12,18 +12,18 @@ export function handleDownloaderIPCs() {
* @param _event - The IPC event object.
* @param fileName - The name of the file being downloaded.
*/
ipcMain.handle("pauseDownload", async (_event, fileName) => {
DownloadManager.instance.networkRequests[fileName]?.pause();
});
ipcMain.handle('pauseDownload', async (_event, fileName) => {
DownloadManager.instance.networkRequests[fileName]?.pause()
})
/**
* Handles the "resumeDownload" IPC message by resuming the download associated with the provided fileName.
* @param _event - The IPC event object.
* @param fileName - The name of the file being downloaded.
*/
ipcMain.handle("resumeDownload", async (_event, fileName) => {
DownloadManager.instance.networkRequests[fileName]?.resume();
});
ipcMain.handle('resumeDownload', async (_event, fileName) => {
DownloadManager.instance.networkRequests[fileName]?.resume()
})
/**
* 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 fileName - The name of the file being downloaded.
*/
ipcMain.handle("abortDownload", async (_event, fileName) => {
const rq = DownloadManager.instance.networkRequests[fileName];
DownloadManager.instance.networkRequests[fileName] = undefined;
const userDataPath = app.getPath("userData");
const fullPath = join(userDataPath, fileName);
rq?.abort();
let result = "NULL";
ipcMain.handle('abortDownload', async (_event, fileName) => {
const rq = DownloadManager.instance.networkRequests[fileName]
DownloadManager.instance.networkRequests[fileName] = undefined
const userDataPath = app.getPath('userData')
const fullPath = join(userDataPath, fileName)
rq?.abort()
let result = 'NULL'
unlink(fullPath, function (err) {
if (err && err.code == "ENOENT") {
result = `File not exist: ${err}`;
if (err && err.code == 'ENOENT') {
result = `File not exist: ${err}`
} else if (err) {
result = `File delete error: ${err}`;
result = `File delete error: ${err}`
} 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.
@ -56,51 +58,51 @@ export function handleDownloaderIPCs() {
* @param url - The URL to download the file from.
* @param fileName - The name to give the downloaded file.
*/
ipcMain.handle("downloadFile", async (_event, url, fileName) => {
const userDataPath = app.getPath("userData");
const destination = resolve(userDataPath, fileName);
const rq = request(url);
ipcMain.handle('downloadFile', async (_event, url, fileName) => {
const userDataPath = join(app.getPath('home'), 'jan')
const destination = resolve(userDataPath, fileName)
const rq = request(url)
progress(rq, {})
.on("progress", function (state: any) {
.on('progress', function (state: any) {
WindowManager?.instance.currentWindow?.webContents.send(
"FILE_DOWNLOAD_UPDATE",
'FILE_DOWNLOAD_UPDATE',
{
...state,
fileName,
}
);
)
})
.on("error", function (err: Error) {
.on('error', function (err: Error) {
WindowManager?.instance.currentWindow?.webContents.send(
"FILE_DOWNLOAD_ERROR",
'FILE_DOWNLOAD_ERROR',
{
fileName,
err,
}
);
)
})
.on("end", function () {
.on('end', function () {
if (DownloadManager.instance.networkRequests[fileName]) {
WindowManager?.instance.currentWindow?.webContents.send(
"FILE_DOWNLOAD_COMPLETE",
'FILE_DOWNLOAD_COMPLETE',
{
fileName,
}
);
DownloadManager.instance.setRequest(fileName, undefined);
)
DownloadManager.instance.setRequest(fileName, undefined)
} else {
WindowManager?.instance.currentWindow?.webContents.send(
"FILE_DOWNLOAD_ERROR",
'FILE_DOWNLOAD_ERROR',
{
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 * as fs from "fs";
import { join } from "path";
import { app, ipcMain } from 'electron'
import * as fs from 'fs'
import { join } from 'path'
/**
* Handles file system operations.
*/
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.
* @param event - The event object.
* @param path - The path of the file to read.
* @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) => {
fs.readFile(join(app.getPath("userData"), path), "utf8", (err, data) => {
fs.readFile(join(userSpacePath, path), 'utf8', (err, data) => {
if (err) {
reject(err);
reject(err)
} else {
resolve(data);
resolve(data)
}
});
});
});
})
})
})
/**
* 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.
*/
ipcMain.handle(
"writeFile",
'writeFile',
async (event, path: string, data: string): Promise<void> => {
return new Promise((resolve, reject) => {
fs.writeFile(
join(app.getPath("userData"), path),
data,
"utf8",
(err) => {
if (err) {
reject(err);
} else {
resolve();
}
fs.writeFile(join(userSpacePath, path), data, 'utf8', (err) => {
if (err) {
reject(err)
} else {
resolve()
}
);
});
})
})
}
);
)
/**
* Creates a directory in the user data directory.
@ -57,21 +77,17 @@ export function handleFsIPCs() {
* @param path - The path of the directory to create.
* @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) => {
fs.mkdir(
join(app.getPath("userData"), path),
{ recursive: true },
(err) => {
if (err) {
reject(err);
} else {
resolve();
}
fs.mkdir(join(userSpacePath, path), { recursive: true }, (err) => {
if (err) {
reject(err)
} else {
resolve()
}
);
});
});
})
})
})
/**
* Removes a directory in the user data directory.
@ -79,21 +95,17 @@ export function handleFsIPCs() {
* @param path - The path of the directory to remove.
* @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) => {
fs.rmdir(
join(app.getPath("userData"), path),
{ recursive: true },
(err) => {
if (err) {
reject(err);
} else {
resolve();
}
fs.rmdir(join(userSpacePath, path), { recursive: true }, (err) => {
if (err) {
reject(err)
} else {
resolve()
}
);
});
});
})
})
})
/**
* 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.
*/
ipcMain.handle(
"listFiles",
'listFiles',
async (event, path: string): Promise<string[]> => {
return new Promise((resolve, reject) => {
fs.readdir(join(app.getPath("userData"), path), (err, files) => {
fs.readdir(join(userSpacePath, path), (err, files) => {
if (err) {
reject(err);
reject(err)
} else {
resolve(files);
resolve(files)
}
});
});
})
})
}
);
)
/**
* Deletes a file from the user data folder.
@ -122,22 +134,23 @@ export function handleFsIPCs() {
* @param filePath - The path to the file to delete.
* @returns A string indicating the result of the operation.
*/
ipcMain.handle("deleteFile", async (_event, filePath) => {
const userDataPath = app.getPath("userData");
const fullPath = join(userDataPath, filePath);
ipcMain.handle('deleteFile', async (_event, filePath) => {
const fullPath = join(userSpacePath, filePath)
let result = "NULL";
let result = 'NULL'
fs.unlink(fullPath, function (err) {
if (err && err.code == "ENOENT") {
result = `File not exist: ${err}`;
if (err && err.code == 'ENOENT') {
result = `File not exist: ${err}`
} else if (err) {
result = `File delete error: ${err}`;
result = `File delete error: ${err}`
} 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") {
return module[method](...args);
} else {
console.log(module[method]);
console.debug(module[method]);
console.error(`Function "${method}" does not exist in the module.`);
}
}
@ -75,7 +75,7 @@ export function handlePluginIPCs() {
const fullPath = join(userDataPath, "plugins");
rmdir(fullPath, { recursive: true }, function (err) {
if (err) console.log(err);
if (err) console.error(err);
ModuleManager.instance.clearImportedModules();
// just relaunch if packaged, should launch manually in development mode

View File

@ -42,7 +42,7 @@ export function handleAppUpdates() {
/* App Update Progress */
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(
"APP_UPDATE_PROGRESS",
{

View File

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

View File

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

View File

@ -33,6 +33,8 @@
* @property {Function} relaunch - Relaunches the app.
* @property {Function} openAppDirectory - Opens the app directory.
* @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} 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.
@ -52,81 +54,85 @@
*/
// 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[]) =>
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) =>
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) =>
ipcRenderer.invoke("installRemotePlugin", pluginName),
ipcRenderer.invoke('installRemotePlugin', pluginName),
downloadFile: (url: string, path: string) =>
ipcRenderer.invoke("downloadFile", url, path),
ipcRenderer.invoke('downloadFile', url, path),
pauseDownload: (fileName: string) =>
ipcRenderer.invoke("pauseDownload", fileName),
ipcRenderer.invoke('pauseDownload', fileName),
resumeDownload: (fileName: string) =>
ipcRenderer.invoke("resumeDownload", fileName),
ipcRenderer.invoke('resumeDownload', fileName),
abortDownload: (fileName: string) =>
ipcRenderer.invoke("abortDownload", fileName),
ipcRenderer.invoke('abortDownload', fileName),
onFileDownloadUpdate: (callback: any) =>
ipcRenderer.on("FILE_DOWNLOAD_UPDATE", callback),
ipcRenderer.on('FILE_DOWNLOAD_UPDATE', callback),
onFileDownloadError: (callback: any) =>
ipcRenderer.on("FILE_DOWNLOAD_ERROR", callback),
ipcRenderer.on('FILE_DOWNLOAD_ERROR', callback),
onFileDownloadSuccess: (callback: any) =>
ipcRenderer.on("FILE_DOWNLOAD_COMPLETE", callback),
ipcRenderer.on('FILE_DOWNLOAD_COMPLETE', callback),
onAppUpdateDownloadUpdate: (callback: any) =>
ipcRenderer.on("APP_UPDATE_PROGRESS", callback),
ipcRenderer.on('APP_UPDATE_PROGRESS', callback),
onAppUpdateDownloadError: (callback: any) =>
ipcRenderer.on("APP_UPDATE_ERROR", callback),
ipcRenderer.on('APP_UPDATE_ERROR', callback),
onAppUpdateDownloadSuccess: (callback: any) =>
ipcRenderer.on("APP_UPDATE_COMPLETE", callback),
});
ipcRenderer.on('APP_UPDATE_COMPLETE', callback),
})

View File

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

View File

@ -1,12 +1,15 @@
import { PluginType, fs } from '@janhq/core'
import { ConversationalPlugin } from '@janhq/core/lib/plugins'
import { Conversation } from '@janhq/core/lib/types'
import { join } from 'path'
/**
* JSONConversationalPlugin is a ConversationalPlugin implementation that provides
* functionality for managing conversations.
*/
export default class JSONConversationalPlugin implements ConversationalPlugin {
private static readonly _homeDir = 'threads'
/**
* Returns the type of the plugin.
*/
@ -18,7 +21,7 @@ export default class JSONConversationalPlugin implements ConversationalPlugin {
* Called when the plugin is loaded.
*/
onLoad() {
fs.mkdir('conversations')
fs.mkdir(JSONConversationalPlugin._homeDir)
console.debug('JSONConversationalPlugin loaded')
}
@ -65,10 +68,14 @@ export default class JSONConversationalPlugin implements ConversationalPlugin {
*/
saveConversation(conversation: Conversation): Promise<void> {
return fs
.mkdir(`conversations/${conversation._id}`)
.mkdir(`${JSONConversationalPlugin._homeDir}/${conversation.id}`)
.then(() =>
fs.writeFile(
`conversations/${conversation._id}/${conversation._id}.json`,
join(
JSONConversationalPlugin._homeDir,
conversation.id,
`${conversation.id}.json`
),
JSON.stringify(conversation)
)
)
@ -79,7 +86,9 @@ export default class JSONConversationalPlugin implements ConversationalPlugin {
* @param conversationId The ID of the conversation to delete.
*/
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
*/
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 async getConversationDocs(): Promise<string[]> {
return fs.listFiles(`conversations`).then((files: string[]) => {
return Promise.all(files.filter((file) => file.startsWith('jan-')))
})
return fs
.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({})],
resolve: {
extensions: [".ts", ".js"],
fallback: {
path: require.resolve('path-browserify'),
},
},
// Do not minify the output, otherwise it breaks the class registration
optimization: {

View File

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

View File

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

View File

@ -124,7 +124,7 @@ function killSubprocess(): Promise<void> {
if (subprocess) {
subprocess.kill();
subprocess = null;
console.log("Subprocess terminated.");
console.debug("Subprocess terminated.");
} else {
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": {
"@janhq/core": "file:../../core",
"path-browserify": "^1.0.1",
"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) => {
const modelVersions = [];
const modelVersions = []
model.versions.forEach((v) => {
const version = {
_id: `${model.author}-${v.name}`,
id: `${model.author}-${v.name}`,
name: v.name,
quantMethod: v.quantMethod,
bits: v.bits,
@ -11,12 +11,12 @@ export const parseToModel = (model) => {
usecase: v.usecase,
downloadLink: v.downloadLink,
productId: model.id,
};
modelVersions.push(version);
});
}
modelVersions.push(version)
})
const product = {
_id: model.id,
id: model.id,
name: model.name,
shortDescription: model.shortDescription,
avatarUrl: model.avatarUrl,
@ -29,9 +29,9 @@ export const parseToModel = (model) => {
type: model.type,
createdAt: model.createdAt,
longDescription: model.longDescription,
status: "Downloadable",
status: 'Downloadable',
releaseDate: 0,
availableVersions: modelVersions,
};
return product;
};
}
return product
}

View File

@ -1,20 +1,21 @@
import { PluginType, fs, downloadFile } from "@janhq/core";
import { ModelPlugin } from "@janhq/core/lib/plugins";
import { Model, ModelCatalog } from "@janhq/core/lib/types";
import { pollDownloadProgress } from "./helpers/cloudNative";
import { parseToModel } from "./helpers/modelParser";
import { PluginType, fs, downloadFile } from '@janhq/core'
import { ModelPlugin } from '@janhq/core/lib/plugins'
import { Model, ModelCatalog } from '@janhq/core/lib/types'
import { parseToModel } from './helpers/modelParser'
import { join } from 'path'
/**
* A plugin for managing machine learning models.
*/
export default class JanModelPlugin implements ModelPlugin {
private static readonly _homeDir = 'models'
/**
* Implements type from JanPlugin.
* @override
* @returns The type of the plugin.
*/
type(): PluginType {
return PluginType.Model;
return PluginType.Model
}
/**
@ -25,6 +26,7 @@ export default class JanModelPlugin implements ModelPlugin {
/** Cloud Native
* 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.
*/
async downloadModel(model: Model): Promise<void> {
await fs.mkdir("models");
downloadFile(model.downloadLink, `models/${model._id}`);
/** Cloud Native
* MARK: Poll Downloading Progress
**/
pollDownloadProgress(model._id);
// create corresponding directory
const directoryPath = join(JanModelPlugin._homeDir, model.productName)
await fs.mkdir(directoryPath)
// path to model binary
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.
* @returns A Promise that resolves when the model is deleted.
*/
deleteModel(filePath: string): Promise<void> {
return fs
.deleteFile(`models/${filePath}`)
.then(() => fs.deleteFile(`models/m-${filePath}.json`));
async deleteModel(filePath: string): Promise<void> {
try {
await Promise.allSettled([
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.
*/
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.
* @returns A Promise that resolves with an array of all models.
*/
getDownloadedModels(): Promise<Model[]> {
return fs
.listFiles("models")
.then((files: string[]) => {
return Promise.all(
files
.filter((file) => /^m-.*\.json$/.test(file))
.map(async (file) => {
const model: Model = JSON.parse(
await fs.readFile(`models/${file}`)
);
return model;
})
);
})
.catch((e) => fs.mkdir("models").then(() => []));
async getDownloadedModels(): Promise<Model[]> {
const results: Model[] = []
const allDirs: string[] = await fs.listFiles(JanModelPlugin._homeDir)
for (const dir of allDirs) {
const modelDirPath = join(JanModelPlugin._homeDir, dir)
const isModelDir = await fs.isDirectory(modelDirPath)
if (!isModelDir) {
// if not a directory, ignore
continue
}
const jsonFiles: string[] = (await fs.listFiles(modelDirPath)).filter(
(file: string) => file.endsWith('.json')
)
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.
* @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
return import(
/* webpackIgnore: true */ MODEL_CATALOG_URL + `?t=${Date.now()}`
).then((module) =>
module.default.map((e) => {
return parseToModel(e);
})
);
).then((module) => module.default.map((e) => parseToModel(e)))
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,8 @@
import { ChatMessage, MessageStatus } from '@janhq/core'
import { atom } from 'jotai'
import { getActiveConvoIdAtom } from './Conversation.atom'
import { ChatMessage, MessageStatus } from '@/models/ChatMessage'
/**
* 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 { MainViewState, setMainViewStateAtom } from './MainView.atom'
/**
* Stores the current active conversation id.
*/
@ -78,13 +76,13 @@ export const updateConversationHasMoreAtom = atom(
export const updateConversationAtom = atom(
null,
(get, set, conversation: Conversation) => {
const id = conversation._id
const id = conversation.id
if (!id) return
const convo = get(userConversationsAtom).find((c) => c._id === id)
const convo = get(userConversationsAtom).find((c) => c.id === id)
if (!convo) return
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
@ -103,5 +101,5 @@ export const updateConversationAtom = atom(
*/
export const userConversationsAtom = atom<Conversation[]>([])
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 { pluginManager } from '@/plugin'
import { join } from 'path'
const activeAssistantModelAtom = atom<Model | undefined>(undefined)
@ -21,16 +22,16 @@ export function useActiveModel() {
const { downloadedModels } = useGetDownloadedModels()
const startModel = async (modelId: string) => {
if (activeModel && activeModel._id === modelId) {
if (activeModel && activeModel.id === modelId) {
console.debug(`Model ${modelId} is already init. Ignore..`)
return
}
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.`)
setStateModel(() => ({
state: 'start',
@ -42,8 +43,8 @@ export function useActiveModel() {
const currentTime = Date.now()
console.debug('Init model: ', modelId)
const res = await initModel(`models/${modelId}`)
const path = join('models', model.productName, modelId)
const res = await initModel(path)
if (res?.error) {
const errorMessage = `${res.error}`
alert(errorMessage)

View File

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

View File

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

View File

@ -7,20 +7,20 @@ import { toaster } from '@/containers/Toast'
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
import { pluginManager } from '@/plugin/PluginManager'
import { join } from 'path'
export default function useDeleteModel() {
const { setDownloadedModels, downloadedModels } = useGetDownloadedModels()
const deleteModel = async (model: Model) => {
await pluginManager
.get<ModelPlugin>(PluginType.Model)
?.deleteModel(model._id)
const path = join('models', model.productName, model.id)
await pluginManager.get<ModelPlugin>(PluginType.Model)?.deleteModel(path)
// reload models
setDownloadedModels(downloadedModels.filter((e) => e._id !== model._id))
setDownloadedModels(downloadedModels.filter((e) => e.id !== model.id))
toaster({
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 => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
_id: modelVersion._id,
id: modelVersion.id,
name: modelVersion.name,
quantMethod: modelVersion.quantMethod,
bits: modelVersion.bits,
@ -31,7 +31,7 @@ export default function useDownloadModel() {
downloadLink: modelVersion.downloadLink,
startDownloadAt: modelVersion.startDownloadAt,
finishDownloadAt: modelVersion.finishDownloadAt,
productId: model._id,
productId: model.id,
productName: model.name,
shortDescription: model.shortDescription,
longDescription: model.longDescription,
@ -53,7 +53,7 @@ export default function useDownloadModel() {
) => {
// set an initial download state
setDownloadState({
modelId: modelVersion._id,
modelId: modelVersion.id,
time: {
elapsed: 0,
remaining: 0,
@ -64,7 +64,7 @@ export default function useDownloadModel() {
total: 0,
transferred: 0,
},
fileName: modelVersion._id,
fileName: modelVersion.id,
})
modelVersion.startDownloadAt = Date.now()

View File

@ -1,13 +1,9 @@
import { useEffect, useState } from 'react'
import { Model } from '@janhq/core/lib/types'
import { Model, Conversation } from '@janhq/core'
import { useAtomValue } from 'jotai'
import { useActiveModel } from './useActiveModel'
import { useGetDownloadedModels } from './useGetDownloadedModels'
import { currentConversationAtom } from '@/helpers/atoms/Conversation.atom'
import { Conversation } from '@/types/chatMessage'
export default function useGetInputState() {
const [inputState, setInputState] = useState<InputType>('loading')
@ -27,7 +23,7 @@ export default function useGetInputState() {
// check if convo model id is in downloaded models
const isModelAvailable = downloadedModels.some(
(model) => model._id === convo.modelId
(model) => model.id === convo.modelId
)
if (!isModelAvailable) {
@ -36,7 +32,7 @@ export default function useGetInputState() {
return
}
if (convo.modelId !== currentModel._id) {
if (convo.modelId !== currentModel.id) {
// in case convo model and active model is different,
// ask user to init the required model
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 { Conversation } from '@janhq/core/lib/types'
import { useSetAtom } from 'jotai'
import { toChatMessage } from '@/utils/message'
import { setConvoMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
import {
conversationStatesAtom,
userConversationsAtom,
} from '@/helpers/atoms/Conversation.atom'
import { toChatMessage } from '@/models/ChatMessage'
import { pluginManager } from '@/plugin/PluginManager'
import { ChatMessage, ConversationState } from '@/types/chatMessage'
const useGetUserConversations = () => {
const setConversationStates = useSetAtom(conversationStatesAtom)
@ -24,19 +24,19 @@ const useGetUserConversations = () => {
?.getConversations()
const convoStates: Record<string, ConversationState> = {}
convos?.forEach((convo) => {
convoStates[convo._id ?? ''] = {
convoStates[convo.id ?? ''] = {
hasMore: true,
waitingForResponse: false,
}
setConvoMessages(
convo.messages.map<ChatMessage>((msg) => toChatMessage(msg)),
convo._id ?? ''
convo.id ?? ''
)
})
setConversationStates(convoStates)
setConversations(convos ?? [])
} catch (error) {
console.log(error)
console.error(error)
}
}

View File

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

View File

@ -46,16 +46,16 @@ export default function HistoryList() {
console.debug('modelId is undefined')
return
}
const model = downloadedModels.find((e) => e._id === convo.modelId)
const model = downloadedModels.find((e) => e.id === convo.modelId)
if (convo == null) {
console.debug('modelId is undefined')
return
}
if (model != null) {
startModel(model._id)
startModel(model.id)
}
if (activeConvoId !== convo._id) {
setActiveConvoId(convo._id)
if (activeConvoId !== convo.id) {
setActiveConvoId(convo.id)
}
}
@ -88,7 +88,7 @@ export default function HistoryList() {
key={i}
className={twMerge(
'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)}
>
@ -100,7 +100,7 @@ export default function HistoryList() {
<p className="mt-1 line-clamp-2 text-xs">
{convo?.lastMessage ?? 'No new message'}
</p>
{activeModel && activeConvoId === convo._id && (
{activeModel && activeConvoId === convo.id && (
<m.div
className="absolute right-0 top-0 h-full w-1 bg-primary/50"
layoutId="active-convo"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,10 +2,10 @@ import * as cn from './cloudNativeService'
import { EventEmitter } from './eventsService'
export const setupCoreServices = () => {
if (typeof window === 'undefined') {
console.log('undefine', window)
console.debug('undefine', window)
return
} else {
console.log('Setting up core services')
console.debug('Setting up core services')
}
if (!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 */
import { ModelCatalog, ModelVersion } from '@janhq/core/lib/types'
import { ModelCatalog, ModelVersion } from '@janhq/core'
export const dummyModel: ModelCatalog = {
_id: 'aladar/TinyLLama-v0-GGUF',
id: 'aladar/TinyLLama-v0-GGUF',
name: 'TinyLLama-v0-GGUF',
shortDescription: 'TinyLlama-1.1B-Chat-v0.3-GGUF',
longDescription: 'https://huggingface.co/aladar/TinyLLama-v0-GGUF/tree/main',
@ -16,7 +16,7 @@ export const dummyModel: ModelCatalog = {
createdAt: 0,
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',
quantMethod: '',
bits: 2,

View File

@ -1,27 +1,48 @@
import { ChatMessage } from '@/models/ChatMessage'
import {
ChatMessage,
Message,
MessageSenderType,
MessageStatus,
MessageType,
NewMessageResponse,
RawMessage,
} from '@janhq/core'
/**
* Util function to merge two array of messages and remove duplicates.
* Also preserve the order
*
* @param arr1 Message array 1
* @param arr2 Message array 2
* @returns Merged array of messages
*/
export function mergeAndRemoveDuplicates(
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)
}
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)
}
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,
}
}