diff --git a/core/src/events.ts b/core/src/events.ts index b39a38408..e962dead4 100644 --- a/core/src/events.ts +++ b/core/src/events.ts @@ -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; diff --git a/core/src/fs.ts b/core/src/fs.ts index 04046705a..73ac31636 100644 --- a/core/src/fs.ts +++ b/core/src/fs.ts @@ -8,6 +8,21 @@ const writeFile: (path: string, data: string) => Promise = (path, data) => window.coreAPI?.writeFile(path, data) ?? window.electronAPI?.writeFile(path, data); +/** + * Gets the user space path. + * @returns {Promise} A Promise that resolves with the user space path. + */ +const getUserSpace = (): Promise => + 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 => + 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 = (path) => window.coreAPI?.deleteFile(path) ?? window.electronAPI?.deleteFile(path); export const fs = { + isDirectory, + getUserSpace, writeFile, readFile, listFiles, diff --git a/core/src/index.ts b/core/src/index.ts index 39a69d702..5c741c863 100644 --- a/core/src/index.ts +++ b/core/src/index.ts @@ -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. diff --git a/core/src/types/index.ts b/core/src/types/index.ts index c2062b5d1..296bc1a7e 100644 --- a/core/src/types/index.ts +++ b/core/src/types/index.ts @@ -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; +}; diff --git a/electron/.prettierrc b/electron/.prettierrc new file mode 100644 index 000000000..46f1abcb0 --- /dev/null +++ b/electron/.prettierrc @@ -0,0 +1,8 @@ +{ + "semi": false, + "singleQuote": true, + "quoteProps": "consistent", + "trailingComma": "es5", + "endOfLine": "auto", + "plugins": ["prettier-plugin-tailwindcss"] +} diff --git a/electron/handlers/download.ts b/electron/handlers/download.ts index ab672f25c..3a1fc36d1 100644 --- a/electron/handlers/download.ts +++ b/electron/handlers/download.ts @@ -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) + }) } diff --git a/electron/handlers/fs.ts b/electron/handlers/fs.ts index af77e3002..c1e8a85e4 100644 --- a/electron/handlers/fs.ts +++ b/electron/handlers/fs.ts @@ -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 => 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 => { + 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 => { + ipcMain.handle('readFile', async (event, path: string): Promise => { 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 => { 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 => { + ipcMain.handle('mkdir', async (event, path: string): Promise => { 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 => { + ipcMain.handle('rmdir', async (event, path: string): Promise => { 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 => { 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 + }) } diff --git a/electron/handlers/plugin.ts b/electron/handlers/plugin.ts index 26eb3c583..22bf253e6 100644 --- a/electron/handlers/plugin.ts +++ b/electron/handlers/plugin.ts @@ -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 diff --git a/electron/handlers/update.ts b/electron/handlers/update.ts index 096d09bac..340db54b9 100644 --- a/electron/handlers/update.ts +++ b/electron/handlers/update.ts @@ -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", { diff --git a/electron/main.ts b/electron/main.ts index 741a75867..5f1d6b086 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -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() } diff --git a/electron/managers/plugin.ts b/electron/managers/plugin.ts index 889425ec7..227eab34e 100644 --- a/electron/managers/plugin.ts +++ b/electron/managers/plugin.ts @@ -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 { diff --git a/electron/preload.ts b/electron/preload.ts index 398913da2..dfba13bd1 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -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), +}) diff --git a/plugins/conversational-json/package.json b/plugins/conversational-json/package.json index 198756f82..520970664 100644 --- a/plugins/conversational-json/package.json +++ b/plugins/conversational-json/package.json @@ -24,6 +24,7 @@ }, "dependencies": { "@janhq/core": "file:../../core", + "path-browserify": "^1.0.1", "ts-loader": "^9.5.0" }, "engines": { diff --git a/plugins/conversational-json/src/index.ts b/plugins/conversational-json/src/index.ts index b87f52a84..0e8465fd5 100644 --- a/plugins/conversational-json/src/index.ts +++ b/plugins/conversational-json/src/index.ts @@ -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 { 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 { - 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 { - 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 { - 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-'))) + }) } } diff --git a/plugins/conversational-json/webpack.config.js b/plugins/conversational-json/webpack.config.js index d4b0db2bd..36e338295 100644 --- a/plugins/conversational-json/webpack.config.js +++ b/plugins/conversational-json/webpack.config.js @@ -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: { diff --git a/plugins/conversational-plugin/src/index.ts b/plugins/conversational-plugin/src/index.ts index b1c5bd937..01045b6c8 100644 --- a/plugins/conversational-plugin/src/index.ts +++ b/plugins/conversational-plugin/src/index.ts @@ -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 ); } diff --git a/plugins/inference-plugin/src/index.ts b/plugins/inference-plugin/src/index.ts index ebd44657f..b02c0f628 100644 --- a/plugins/inference-plugin/src/index.ts +++ b/plugins/inference-plugin/src/index.ts @@ -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} A promise that resolves when the model is initialized. */ async initModel(modelFileName: string): Promise { - 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); diff --git a/plugins/inference-plugin/src/module.ts b/plugins/inference-plugin/src/module.ts index ba6afdf90..613ad9fca 100644 --- a/plugins/inference-plugin/src/module.ts +++ b/plugins/inference-plugin/src/module.ts @@ -124,7 +124,7 @@ function killSubprocess(): Promise { 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); } diff --git a/plugins/model-plugin/.prettierrc b/plugins/model-plugin/.prettierrc new file mode 100644 index 000000000..46f1abcb0 --- /dev/null +++ b/plugins/model-plugin/.prettierrc @@ -0,0 +1,8 @@ +{ + "semi": false, + "singleQuote": true, + "quoteProps": "consistent", + "trailingComma": "es5", + "endOfLine": "auto", + "plugins": ["prettier-plugin-tailwindcss"] +} diff --git a/plugins/model-plugin/package.json b/plugins/model-plugin/package.json index 171f0a4e9..43d1ffa8e 100644 --- a/plugins/model-plugin/package.json +++ b/plugins/model-plugin/package.json @@ -29,6 +29,7 @@ ], "dependencies": { "@janhq/core": "file:../../core", + "path-browserify": "^1.0.1", "ts-loader": "^9.5.0" } } diff --git a/plugins/model-plugin/src/helpers/cloudNative.ts b/plugins/model-plugin/src/helpers/cloudNative.ts deleted file mode 100644 index 90c6d3f1e..000000000 --- a/plugins/model-plugin/src/helpers/cloudNative.ts +++ /dev/null @@ -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 { - 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; -} diff --git a/plugins/model-plugin/src/helpers/modelParser.ts b/plugins/model-plugin/src/helpers/modelParser.ts index d8b8a81f5..826a2afba 100644 --- a/plugins/model-plugin/src/helpers/modelParser.ts +++ b/plugins/model-plugin/src/helpers/modelParser.ts @@ -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 +} diff --git a/plugins/model-plugin/src/index.ts b/plugins/model-plugin/src/index.ts index ccfed6bfe..2e599c2d4 100644 --- a/plugins/model-plugin/src/index.ts +++ b/plugins/model-plugin/src/index.ts @@ -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 { - 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 { - return fs - .deleteFile(`models/${filePath}`) - .then(() => fs.deleteFile(`models/m-${filePath}.json`)); + async deleteModel(filePath: string): Promise { + 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 { - 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 { - 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 { + 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))) } } diff --git a/plugins/model-plugin/webpack.config.js b/plugins/model-plugin/webpack.config.js index 60fa1a9b0..3475516ed 100644 --- a/plugins/model-plugin/webpack.config.js +++ b/plugins/model-plugin/webpack.config.js @@ -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 -}; +} diff --git a/web/containers/Layout/BottomBar/index.tsx b/web/containers/Layout/BottomBar/index.tsx index 9752888fa..9153da2c7 100644 --- a/web/containers/Layout/BottomBar/index.tsx +++ b/web/containers/Layout/BottomBar/index.tsx @@ -46,7 +46,7 @@ const BottomBar = () => { ⌘e to show your model ) } diff --git a/web/containers/Layout/TopBar/CommandListDownloadedModel/index.tsx b/web/containers/Layout/TopBar/CommandListDownloadedModel/index.tsx index 108aa8e82..fee918f3a 100644 --- a/web/containers/Layout/TopBar/CommandListDownloadedModel/index.tsx +++ b/web/containers/Layout/TopBar/CommandListDownloadedModel/index.tsx @@ -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() { { - onModelActionClick(model._id) + onModelActionClick(model.id) setOpen(false) }} > @@ -72,7 +72,7 @@ export default function CommandListDownloadedModel() { />
{model.name} - {activeModel && activeModel._id === model._id && ( + {activeModel && activeModel.id === model.id && ( Active )}
diff --git a/web/containers/ModalCancelDownload/index.tsx b/web/containers/ModalCancelDownload/index.tsx index 2f753d705..e62dda0ca 100644 --- a/web/containers/ModalCancelDownload/index.tsx +++ b/web/containers/ModalCancelDownload/index.tsx @@ -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) diff --git a/web/containers/Providers/EventHandler.tsx b/web/containers/Providers/EventHandler.tsx index f1b943427..ff2934282 100644 --- a/web/containers/Providers/EventHandler.tsx +++ b/web/containers/Providers/EventHandler.tsx @@ -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( - (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( + (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(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(PluginType.Model) diff --git a/web/containers/Providers/EventListener.tsx b/web/containers/Providers/EventListener.tsx index 528cd254b..2e91a70cf 100644 --- a/web/containers/Providers/EventListener.tsx +++ b/web/containers/Providers/EventListener.tsx @@ -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(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) } ) diff --git a/web/helpers/atoms/ChatMessage.atom.ts b/web/helpers/atoms/ChatMessage.atom.ts index c45808288..d343d0f7c 100644 --- a/web/helpers/atoms/ChatMessage.atom.ts +++ b/web/helpers/atoms/ChatMessage.atom.ts @@ -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 */ diff --git a/web/helpers/atoms/Conversation.atom.ts b/web/helpers/atoms/Conversation.atom.ts index 3a661d385..2265c5c2e 100644 --- a/web/helpers/atoms/Conversation.atom.ts +++ b/web/helpers/atoms/Conversation.atom.ts @@ -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([]) export const currentConversationAtom = atom((get) => - get(userConversationsAtom).find((c) => c._id === get(getActiveConvoIdAtom)) + get(userConversationsAtom).find((c) => c.id === get(getActiveConvoIdAtom)) ) diff --git a/web/hooks/useActiveModel.ts b/web/hooks/useActiveModel.ts index a9d517fbf..efe05672e 100644 --- a/web/hooks/useActiveModel.ts +++ b/web/hooks/useActiveModel.ts @@ -10,6 +10,7 @@ import { toaster } from '@/containers/Toast' import { useGetDownloadedModels } from './useGetDownloadedModels' import { pluginManager } from '@/plugin' +import { join } from 'path' const activeAssistantModelAtom = atom(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) diff --git a/web/hooks/useCreateConversation.ts b/web/hooks/useCreateConversation.ts index 755876c0a..e5f2a669f 100644 --- a/web/hooks/useCreateConversation.ts +++ b/web/hooks/useCreateConversation.ts @@ -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 { diff --git a/web/hooks/useDeleteConversation.ts b/web/hooks/useDeleteConversation.ts index 8826aab5b..459b527b5 100644 --- a/web/hooks/useDeleteConversation.ts +++ b/web/hooks/useDeleteConversation.ts @@ -41,7 +41,7 @@ export default function useDeleteConversation() { .get(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) } diff --git a/web/hooks/useDeleteModel.ts b/web/hooks/useDeleteModel.ts index 1980e4514..4d527e8cb 100644 --- a/web/hooks/useDeleteModel.ts +++ b/web/hooks/useDeleteModel.ts @@ -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(PluginType.Model) - ?.deleteModel(model._id) + const path = join('models', model.productName, model.id) + await pluginManager.get(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.`, }) } diff --git a/web/hooks/useDownloadModel.ts b/web/hooks/useDownloadModel.ts index 29a41333b..3ec1bf330 100644 --- a/web/hooks/useDownloadModel.ts +++ b/web/hooks/useDownloadModel.ts @@ -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() diff --git a/web/hooks/useGetInputState.ts b/web/hooks/useGetInputState.ts index 26a1c83d7..d1b52f080 100644 --- a/web/hooks/useGetInputState.ts +++ b/web/hooks/useGetInputState.ts @@ -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('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') diff --git a/web/hooks/useGetUserConversations.ts b/web/hooks/useGetUserConversations.ts index 35e659ee2..5f0ad7435 100644 --- a/web/hooks/useGetUserConversations.ts +++ b/web/hooks/useGetUserConversations.ts @@ -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 = {} convos?.forEach((convo) => { - convoStates[convo._id ?? ''] = { + convoStates[convo.id ?? ''] = { hasMore: true, waitingForResponse: false, } setConvoMessages( convo.messages.map((msg) => toChatMessage(msg)), - convo._id ?? '' + convo.id ?? '' ) }) setConversationStates(convoStates) setConversations(convos ?? []) } catch (error) { - console.log(error) + console.error(error) } } diff --git a/web/hooks/useSendChatMessage.ts b/web/hooks/useSendChatMessage.ts index 256c701ef..5d5e1598c 100644 --- a/web/hooks/useSendChatMessage.ts +++ b/web/hooks/useSendChatMessage.ts @@ -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((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, diff --git a/web/models/ChatMessage.ts b/web/models/ChatMessage.ts deleted file mode 100644 index b7f18d932..000000000 --- a/web/models/ChatMessage.ts +++ /dev/null @@ -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, - } -} diff --git a/web/screens/Chat/ChatItem/index.tsx b/web/screens/Chat/ChatItem/index.tsx index 6b4f0b3f8..86163bbbe 100644 --- a/web/screens/Chat/ChatItem/index.tsx +++ b/web/screens/Chat/ChatItem/index.tsx @@ -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 diff --git a/web/screens/Chat/HistoryList/index.tsx b/web/screens/Chat/HistoryList/index.tsx index 628023e0c..34587e2ae 100644 --- a/web/screens/Chat/HistoryList/index.tsx +++ b/web/screens/Chat/HistoryList/index.tsx @@ -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() {

{convo?.lastMessage ?? 'No new message'}

- {activeModel && activeConvoId === convo._id && ( + {activeModel && activeConvoId === convo.id && ( { 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(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) => { diff --git a/web/screens/ExploreModels/ExploreModelItem/index.tsx b/web/screens/ExploreModels/ExploreModelItem/index.tsx index b13816d39..a201210e5 100644 --- a/web/screens/ExploreModels/ExploreModelItem/index.tsx +++ b/web/screens/ExploreModels/ExploreModelItem/index.tsx @@ -105,7 +105,7 @@ const ExploreModelItem = forwardRef(({ model }, ref) => { )} diff --git a/web/screens/ExploreModels/ExploreModelItemHeader/index.tsx b/web/screens/ExploreModels/ExploreModelItemHeader/index.tsx index fd4babfd3..bf55aec61 100644 --- a/web/screens/ExploreModels/ExploreModelItemHeader/index.tsx +++ b/web/screens/ExploreModels/ExploreModelItemHeader/index.tsx @@ -35,8 +35,8 @@ const ExploreModelItemHeader: React.FC = ({ 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 = ({ }, [exploreModel, suitableModel]) const isDownloaded = - downloadedModels.find((model) => model._id === suitableModel._id) != null + downloadedModels.find((model) => model.id === suitableModel.id) != null let downloadButton = (