jan/electron/handlers/native.ts
NamH 101268f6f3
feat: integrating cortex (#3001)
* feat: integrating cortex

* Temporary prevent crash

Signed-off-by: James <namnh0122@gmail.com>

* fix yarn lint

Signed-off-by: James <namnh0122@gmail.com>

* refactor: remove core node module - fs - extensions and so on (#3151)

* add migration script for threads, messages and models

Signed-off-by: James <namnh0122@gmail.com>

* remove freq_penalty and presence_penalty if model not supported

Signed-off-by: James <namnh0122@gmail.com>

* add back models in my models

Signed-off-by: James <namnh0122@gmail.com>

* fix api-url for setup API key popup

Signed-off-by: James <namnh0122@gmail.com>

* fix using model name for dropdown model

Signed-off-by: James <namnh0122@gmail.com>

* fix can't click to hotkey

Signed-off-by: James <namnh0122@gmail.com>

* fix: disable some UIs

Signed-off-by: James <namnh0122@gmail.com>

* fix build

Signed-off-by: James <namnh0122@gmail.com>

* reduce calling HF api

Signed-off-by: James <namnh0122@gmail.com>

* some ui update

Signed-off-by: James <namnh0122@gmail.com>

* feat: modal migration UI  (#3153)

* feat: handle popup migration

* chore: update loader

* chore: integrate script migration

* chore: cleanup import

* chore: moving out spinner loader

* chore: update check thread message success migrate

* chore: add handle script into retry button

* remove warning from joi

Signed-off-by: James <namnh0122@gmail.com>

* chore: fix duplicate children

* fix: path after migrating model

Signed-off-by: James <namnh0122@gmail.com>

* chore: apply mutation for config

* chore: prevent calling too many create assistant api

Signed-off-by: James <namnh0122@gmail.com>

* using cortexso

Signed-off-by: James <namnh0122@gmail.com>

* update download api

Signed-off-by: James <namnh0122@gmail.com>

* fix use on slider item

Signed-off-by: James <namnh0122@gmail.com>

* fix: ui no download model or simple onboarding (#3166)

* fix download huggingface model match with slider item

Signed-off-by: James <namnh0122@gmail.com>

* update owner_logo to logo and author

Signed-off-by: James <namnh0122@gmail.com>

* update new cortexso

Signed-off-by: James <namnh0122@gmail.com>

* Add install python step for macos

* add engine table

Signed-off-by: James <namnh0122@gmail.com>

* fix local icons

Signed-off-by: James <namnh0122@gmail.com>

* feat: add search feature for model hub

Signed-off-by: James <namnh0122@gmail.com>

* fix misalign switch

Signed-off-by: James <namnh0122@gmail.com>

* fix: delete thread not focus on other thread

Signed-off-by: James <namnh0122@gmail.com>

* add get model from hugging face

Signed-off-by: James <namnh0122@gmail.com>

* fix download from hugging face

Signed-off-by: James <namnh0122@gmail.com>

* small update

Signed-off-by: James <namnh0122@gmail.com>

* update

Signed-off-by: James <namnh0122@gmail.com>

* fix system monitor rounded only on the left

Signed-off-by: James <namnh0122@gmail.com>

* chore: update ui new hub screen (#3174)

* chore: update ui new hub screen

* chore: update layout centerpanel thread and hub screen

* chore: update detail model by group

* update cortexso 0.1.13

Signed-off-by: James <namnh0122@gmail.com>

* chore: add file size

Signed-off-by: James <namnh0122@gmail.com>

* chore: put engine to experimental feature

Signed-off-by: James <namnh0122@gmail.com>

* chore: open cortex folder

Signed-off-by: James <namnh0122@gmail.com>

* chore: add back user avatar

Signed-off-by: James <namnh0122@gmail.com>

* chore: minor UI hub (#3182)

* chore: add back right click thread list and update 3 dots are overlapping with the text

* chore: update position dropdown list my models

* chore: make on-device tab showing 6 items instead of 4

* chore: update style description modals detail model

* chore: update isGeneration loader and author name on modal

* feat: integrate cortex single executable

Signed-off-by: James <namnh0122@gmail.com>

* fix build

Signed-off-by: James <namnh0122@gmail.com>

* chore: added blank state

* chore: update ui component blank state

* bump cortex binary version

* fix: logic show modal migration (#3165)

* fix: logic show modal migration

* chore: fixed logic

* chore: read contain format gguf local models

* chore: change return hasLocalModel

* chore: intiial skipmigration state

* chore: filter embedding model

* fix: delete top thread not focus on any other thread

* chore: added UI no result component search models group (#3188)

* fix: remote model should show all when user config that engine

Signed-off-by: James <namnh0122@gmail.com>

* chore: set state thread and models migration using getOnInit (#3189)

* chore: set state thread and models migration using getOnInit

* chore: add state as dependecies hooks

* chore: system monitor panel show engine model (#3192)

* fix: remove config api, replace with engine

Signed-off-by: James <namnh0122@gmail.com>

* update

Signed-off-by: James <namnh0122@gmail.com>

* update reactquery

Signed-off-by: James <namnh0122@gmail.com>

* bump cortex 0.4.35

* feat: add waiting for cortex popup

Signed-off-by: James <namnh0122@gmail.com>

* chore: add loader detail model popup (#3195)

* chore: model start loader (#3197)

* chore: added model loader when user starting chat without model active

* chore: update copies loader

* fix: select min file size if recommended quant does not exist

Signed-off-by: James <namnh0122@gmail.com>

* chore: temporary hide gpu config

* fix: tensorrt not shown

Signed-off-by: James <namnh0122@gmail.com>

* fix lint

Signed-off-by: James <namnh0122@gmail.com>

* fix tests

Signed-off-by: James <namnh0122@gmail.com>

* fix e2e tests (wip)

Signed-off-by: James <namnh0122@gmail.com>

* update

Signed-off-by: James <namnh0122@gmail.com>

* fix: adding element and correct test to adapt new UI

* fix: temp skip unstable part

* fix: only show models which can be supported

Signed-off-by: James <namnh0122@gmail.com>

* Update version.txt

* update send message

Signed-off-by: James <namnh0122@gmail.com>

* fix: not allow user send message when is generating

Signed-off-by: James <namnh0122@gmail.com>

* chore: temp skip Playwright test due to env issue

* chore: temp skip Playwright test due to env issue

* update

Signed-off-by: James <namnh0122@gmail.com>

* chore: minor-ui-feedback (#3202)

---------

Signed-off-by: James <namnh0122@gmail.com>
Co-authored-by: Louis <louis@jan.ai>
Co-authored-by: Faisal Amir <urmauur@gmail.com>
Co-authored-by: Hien To <tominhhien97@gmail.com>
Co-authored-by: Van Pham <64197333+Van-QA@users.noreply.github.com>
Co-authored-by: Van-QA <van@jan.ai>
2024-07-26 17:52:43 +07:00

380 lines
11 KiB
TypeScript

import { app, ipcMain, dialog, shell, nativeTheme } from 'electron'
import { windowManager } from '../managers/window'
import {
AppEvent,
NativeRoute,
SelectFileProp,
SelectFileOption,
} from '@janhq/core/node'
import { menu } from '../utils/menu'
import { join } from 'path'
import { getJanDataFolderPath } from './../utils/path'
import {
readdirSync,
writeFileSync,
readFileSync,
existsSync,
mkdirSync,
} from 'fs'
import { dump } from 'js-yaml'
import os from 'os'
const isMac = process.platform === 'darwin'
export function handleAppIPCs() {
/**
* Handles the "setNativeThemeLight" IPC message by setting the native theme source to "light".
* This will change the appearance of the app to the light theme.
*/
ipcMain.handle(NativeRoute.setNativeThemeLight, () => {
nativeTheme.themeSource = 'light'
})
ipcMain.handle(NativeRoute.setCloseApp, () => {
windowManager.mainWindow?.close()
})
ipcMain.handle(NativeRoute.setMinimizeApp, () => {
windowManager.mainWindow?.minimize()
})
ipcMain.handle(NativeRoute.homePath, () => {
// Handles the 'get jan home path' IPC event. This event is triggered to get the default jan home path.
return join(
process.env[process.platform == 'win32' ? 'USERPROFILE' : 'HOME'] ?? '',
'jan'
)
})
ipcMain.handle(NativeRoute.setMaximizeApp, async (_event) => {
if (windowManager.mainWindow?.isMaximized()) {
windowManager.mainWindow.unmaximize()
} else {
windowManager.mainWindow?.maximize()
}
})
ipcMain.handle(NativeRoute.getThemes, async () => {
const folderPath = join(getJanDataFolderPath(), 'themes')
const installedThemes = readdirSync(folderPath)
const themesOptions = Promise.all(
installedThemes
.filter((x: string) => x !== '.DS_Store')
.map(async (x: string) => {
const y = join(folderPath, x, `theme.json`)
const c = JSON.parse(readFileSync(y, 'utf-8'))
return { name: c?.displayName, value: c.id }
})
)
return themesOptions
})
ipcMain.handle(NativeRoute.readTheme, async (_event, themeId: string) => {
const folderPath = join(getJanDataFolderPath(), 'themes')
const filePath = join(folderPath, themeId, `theme.json`)
return JSON.parse(readFileSync(filePath, 'utf-8'))
})
/**
* Handles the "setNativeThemeDark" IPC message by setting the native theme source to "dark".
* This will change the appearance of the app to the dark theme.
*/
ipcMain.handle(NativeRoute.setNativeThemeDark, () => {
nativeTheme.themeSource = 'dark'
})
/**
* Opens a URL in the user's default browser.
* @param _event - The IPC event object.
* @param url - The URL to open.
*/
ipcMain.handle(NativeRoute.openExternalUrl, async (_event, url) => {
shell.openExternal(url)
})
/**
* Opens a URL in the user's default browser.
* @param _event - The IPC event object.
* @param url - The URL to open.
*/
ipcMain.handle(NativeRoute.openFileExplore, async (_event, url) => {
shell.openPath(url)
})
/**
* Relaunches the app in production - reload window in development.
* @param _event - The IPC event object.
* @param url - The URL to reload.
*/
ipcMain.handle(NativeRoute.relaunch, async (_event) => {
app.relaunch()
app.exit()
})
ipcMain.handle(NativeRoute.selectDirectory, async () => {
const mainWindow = windowManager.mainWindow
if (!mainWindow) {
console.error('No main window found')
return
}
const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow, {
title: 'Select a folder',
buttonLabel: 'Select Folder',
properties: ['openDirectory', 'createDirectory'],
})
if (canceled) {
return
} else {
return filePaths[0]
}
})
ipcMain.handle(
NativeRoute.selectFiles,
async (_event, option?: SelectFileOption) => {
const mainWindow = windowManager.mainWindow
if (!mainWindow) {
console.error('No main window found')
return
}
const title = option?.title ?? 'Select files'
const buttonLabel = option?.buttonLabel ?? 'Select'
const props: SelectFileProp[] = ['openFile']
if (option?.allowMultiple) {
props.push('multiSelections')
}
if (option?.selectDirectory) {
props.push('openDirectory')
}
console.debug(`Select files with props: ${props}`)
const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow, {
title,
buttonLabel,
properties: props,
filters: option?.filters,
})
if (canceled) return
return filePaths
}
)
ipcMain.handle(
NativeRoute.hideQuickAskWindow,
async (): Promise<void> => windowManager.hideQuickAskWindow()
)
ipcMain.handle(
NativeRoute.sendQuickAskInput,
async (_event, input: string): Promise<void> => {
windowManager.mainWindow?.webContents.send(
AppEvent.onUserSubmitQuickAsk,
input
)
}
)
ipcMain.handle(NativeRoute.showOpenMenu, function (e, args) {
if (!isMac && windowManager.mainWindow) {
menu.popup({
window: windowManager.mainWindow,
x: args.x,
y: args.y,
})
}
})
ipcMain.handle(
NativeRoute.hideMainWindow,
async (): Promise<void> => windowManager.hideMainWindow()
)
ipcMain.handle(
NativeRoute.showMainWindow,
async (): Promise<void> => windowManager.showMainWindow()
)
ipcMain.handle(
NativeRoute.quickAskSizeUpdated,
async (_event, heightOffset: number): Promise<void> =>
windowManager.expandQuickAskWindow(heightOffset)
)
ipcMain.handle(NativeRoute.ackDeepLink, async (_event): Promise<void> => {
windowManager.ackDeepLink()
})
ipcMain.handle(NativeRoute.openAppLog, async (_event): Promise<void> => {
const cortexHomeDir = join(os.homedir(), 'cortex')
try {
const errorMessage = await shell.openPath(join(cortexHomeDir))
if (errorMessage) {
console.error(`An error occurred: ${errorMessage}`)
} else {
console.log('Path opened successfully')
}
} catch (error) {
console.error(`Failed to open path: ${error}`)
}
})
ipcMain.handle(NativeRoute.syncModelFileToCortex, async (_event) => {
const janModelFolderPath = join(getJanDataFolderPath(), 'models')
const allModelFolders = readdirSync(janModelFolderPath)
const cortexHomeDir = join(os.homedir(), 'cortex')
const cortexModelFolderPath = join(cortexHomeDir, 'models')
console.log('cortexModelFolderPath', cortexModelFolderPath)
const reflect = require('@alumna/reflect')
for (const modelName of allModelFolders) {
const modelFolderPath = join(janModelFolderPath, modelName)
const filesInModelFolder = readdirSync(modelFolderPath)
if (filesInModelFolder.length <= 1) {
// if only have model.json file or empty folder, we skip it
continue
}
const destinationPath = join(cortexModelFolderPath, modelName)
// create folder if not exist
if (!existsSync(destinationPath)) {
mkdirSync(destinationPath, { recursive: true })
}
try {
const modelJsonFullPath = join(
janModelFolderPath,
modelName,
'model.json'
)
const model = JSON.parse(readFileSync(modelJsonFullPath, 'utf-8'))
const fileNames: string[] = model.sources.map((x: any) => x.filename)
// prepend fileNames with cortexModelFolderPath
const files = fileNames.map((x: string) =>
join(cortexModelFolderPath, model.id, x)
)
const engine = 'cortex.llamacpp'
const updatedModelFormat = {
id: model.id,
name: model.id,
model: model.id,
version: Number(model.version),
files: files ?? [],
created: Date.now(),
object: 'model',
owned_by: model.metadata?.author ?? '',
// settings
ngl: model.settings?.ngl,
ctx_len: model.settings?.ctx_len ?? 2048,
engine: engine,
prompt_template: model.settings?.prompt_template ?? '',
// parameters
stop: model.parameters?.stop ?? [],
top_p: model.parameters?.top_p,
temperature: model.parameters?.temperature,
frequency_penalty: model.parameters?.frequency_penalty,
presence_penalty: model.parameters?.presence_penalty,
max_tokens: model.parameters?.max_tokens ?? 2048,
stream: model.parameters?.stream ?? true,
}
const { err } = await reflect({
src: modelFolderPath,
dest: destinationPath,
recursive: true,
exclude: ['model.json'],
delete: false,
overwrite: true,
errorOnExist: false,
})
if (err) console.error(err)
else {
// create the model.yml file
const modelYamlData = dump(updatedModelFormat)
const modelYamlPath = join(cortexModelFolderPath, `${modelName}.yaml`)
writeFileSync(modelYamlPath, modelYamlData)
}
} catch (err) {
console.error(err)
}
}
})
ipcMain.handle(
NativeRoute.getAllMessagesAndThreads,
async (_event): Promise<any> => {
const janThreadFolderPath = join(getJanDataFolderPath(), 'threads')
// get children of thread folder
const allThreadFolders = readdirSync(janThreadFolderPath)
const threads: any[] = []
const messages: any[] = []
for (const threadFolder of allThreadFolders) {
try {
const threadJsonFullPath = join(
janThreadFolderPath,
threadFolder,
'thread.json'
)
const thread = JSON.parse(readFileSync(threadJsonFullPath, 'utf-8'))
threads.push(thread)
const messageFullPath = join(
janThreadFolderPath,
threadFolder,
'messages.jsonl'
)
const lines = readFileSync(messageFullPath, 'utf-8')
.toString()
.split('\n')
.filter((line: any) => line !== '')
for (const line of lines) {
messages.push(JSON.parse(line))
}
} catch (err) {
console.error(err)
}
}
return {
threads,
messages,
}
}
)
ipcMain.handle(
NativeRoute.getAllLocalModels,
async (_event): Promise<boolean> => {
const janModelsFolderPath = join(getJanDataFolderPath(), 'models')
// get children of thread folder
const allModelsFolders = readdirSync(janModelsFolderPath)
let hasLocalModels = false
for (const modelFolder of allModelsFolders) {
try {
const modelsFullPath = join(janModelsFolderPath, modelFolder)
const dir = readdirSync(modelsFullPath)
const ggufFile = dir.some((file) => file.endsWith('.gguf'))
if (ggufFile) {
hasLocalModels = true
break
}
} catch (err) {
console.error(err)
}
}
return hasLocalModels
}
)
}