fix: set cortex data folder path when starting jan (#3252)

* fix: set cortex data folder path when starting jan

* fix: change port to 1338

* fix: add migration in advanced setting

* update

* update new cortex

* feat: add import model error handler

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

---------

Signed-off-by: James <namnh0122@gmail.com>
This commit is contained in:
NamH 2024-08-06 07:43:32 +07:00 committed by GitHub
parent 224ca3f7cc
commit 91c77eda78
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 330 additions and 290 deletions

View File

@ -129,4 +129,5 @@ export interface DownloadStateEvent {
export enum DownloadType2 {
Model = 'model',
Miscelanous = 'miscelanous',
Engine = 'engine',
}

View File

@ -8,16 +8,15 @@ import {
} from '@janhq/core/node'
import { menu } from '../utils/menu'
import { join } from 'path'
import { getJanDataFolderPath } from './../utils/path'
import { getAppConfigurations, getJanDataFolderPath } from './../utils/path'
import {
readdirSync,
writeFileSync,
readFileSync,
existsSync,
mkdirSync
mkdirSync,
} from 'fs'
import { dump } from 'js-yaml'
import os from 'os'
const isMac = process.platform === 'darwin'
@ -178,7 +177,7 @@ export function handleAppIPCs() {
}
)
ipcMain.handle(NativeRoute.showOpenMenu, function (e, args) {
ipcMain.handle(NativeRoute.showOpenMenu, function (_e, args) {
if (!isMac && windowManager.mainWindow) {
menu.popup({
window: windowManager.mainWindow,
@ -209,10 +208,11 @@ export function handleAppIPCs() {
})
ipcMain.handle(NativeRoute.openAppLog, async (_event): Promise<void> => {
const cortexHomeDir = join(os.homedir(), 'cortex')
const configuration = getAppConfigurations()
const dataFolder = configuration.data_folder
try {
const errorMessage = await shell.openPath(join(cortexHomeDir))
const errorMessage = await shell.openPath(join(dataFolder))
if (errorMessage) {
console.error(`An error occurred: ${errorMessage}`)
} else {
@ -227,21 +227,19 @@ export function handleAppIPCs() {
const janModelFolderPath = join(getJanDataFolderPath(), 'models')
const allModelFolders = readdirSync(janModelFolderPath)
const cortexHomeDir = join(os.homedir(), 'cortex')
const cortexModelFolderPath = join(cortexHomeDir, 'models')
const configration = getAppConfigurations()
const destinationFolderPath = join(configration.data_folder, 'models')
if(!existsSync(cortexModelFolderPath))
mkdirSync(cortexModelFolderPath)
console.log('cortexModelFolderPath', cortexModelFolderPath)
if (!existsSync(destinationFolderPath)) mkdirSync(destinationFolderPath)
console.log('destinationFolderPath', destinationFolderPath)
const reflect = require('@alumna/reflect')
for (const modelName of allModelFolders) {
const modelFolderPath = join(janModelFolderPath, modelName)
try {
const filesInModelFolder = readdirSync(modelFolderPath)
const destinationPath = join(cortexModelFolderPath, modelName)
const destinationPath = join(destinationFolderPath, modelName)
const modelJsonFullPath = join(
janModelFolderPath,
@ -253,15 +251,18 @@ export function handleAppIPCs() {
const fileNames: string[] = model.sources.map((x: any) => x.filename)
let files: string[] = []
if(filesInModelFolder.length > 1) {
// prepend fileNames with cortexModelFolderPath
if (filesInModelFolder.length > 1) {
// prepend fileNames with model folder path
files = fileNames.map((x: string) =>
join(cortexModelFolderPath, model.id, x)
join(destinationFolderPath, model.id, x)
)
} else if(model.sources.length && !/^(http|https):\/\/[^/]+\/.*/.test(model.sources[0].url)) {
} else if (
model.sources.length &&
!/^(http|https):\/\/[^/]+\/.*/.test(model.sources[0].url)
) {
// Symlink case
files = [ model.sources[0].url ]
} else continue;
files = [model.sources[0].url]
} else continue
// create folder if not exist
// only for local model files
@ -269,7 +270,10 @@ export function handleAppIPCs() {
mkdirSync(destinationPath, { recursive: true })
}
const engine = (model.engine === 'nitro' || model.engine === 'cortex') ? 'cortex.llamacpp' : (model.engine ?? 'cortex.llamacpp')
const engine =
model.engine === 'nitro' || model.engine === 'cortex'
? 'cortex.llamacpp'
: (model.engine ?? 'cortex.llamacpp')
const updatedModelFormat = {
id: model.id,
@ -296,7 +300,7 @@ export function handleAppIPCs() {
max_tokens: model.parameters?.max_tokens ?? 2048,
stream: model.parameters?.stream ?? true,
}
if(filesInModelFolder.length > 1 ) {
if (filesInModelFolder.length > 1) {
const { err } = await reflect({
src: modelFolderPath,
dest: destinationPath,
@ -307,14 +311,14 @@ export function handleAppIPCs() {
errorOnExist: false,
})
if (err) {
console.error(err);
continue;
if (err) {
console.error(err)
continue
}
}
// create the model.yml file
const modelYamlData = dump(updatedModelFormat)
const modelYamlPath = join(cortexModelFolderPath, `${modelName}.yaml`)
const modelYamlPath = join(destinationFolderPath, `${modelName}.yaml`)
writeFileSync(modelYamlPath, modelYamlData)
} catch (err) {
@ -354,11 +358,11 @@ export function handleAppIPCs() {
'messages.jsonl'
)
if(!existsSync(messageFullPath)) continue;
if (!existsSync(messageFullPath)) continue
const lines = readFileSync(messageFullPath, 'utf-8')
.toString()
.split('\n')
.filter((line: any) => line !== '')
.toString()
.split('\n')
.filter((line: any) => line !== '')
for (const line of lines) {
messages.push(JSON.parse(line))
}
@ -379,8 +383,10 @@ export function handleAppIPCs() {
const janModelsFolderPath = join(getJanDataFolderPath(), 'models')
if (!existsSync(janModelsFolderPath)) {
console.debug('No local models found')
return false
}
// get children of thread folder
const allModelsFolders = readdirSync(janModelsFolderPath)
let hasLocalModels = false

View File

@ -19,7 +19,7 @@ import { handleAppIPCs } from './handlers/native'
* Utils
**/
import { setupMenu } from './utils/menu'
import { createUserSpace } from './utils/path'
import { createUserSpace, getAppConfigurations } from './utils/path'
import { migrate } from './utils/migration'
import { cleanUpAndQuit } from './utils/clean'
import { setupCore } from './utils/setup'
@ -58,12 +58,31 @@ Object.assign(console, log.functions)
let cortexService: ChildProcess | undefined = undefined
const cortexJsPort = 1338
const cortexCppPort = 3940
const host = '127.0.0.1'
app
.whenReady()
.then(() => killProcessesOnPort(3929))
.then(() => killProcessesOnPort(1337))
.then(() => killProcessesOnPort(cortexCppPort))
.then(() => killProcessesOnPort(cortexJsPort))
.then(() => {
const command = `${cortexPath} -a 127.0.0.1 -p 1337`
const appConfiguration = getAppConfigurations()
const janDataFolder = appConfiguration.data_folder
const cortexParams: Record<string, string> = {
'-n': 'jan',
'-a': host,
'-p': cortexJsPort.toString(),
'-ep': cortexCppPort.toString(),
'--dataFolder': janDataFolder,
}
// add cortex parameters to the command
const command = Object.entries(cortexParams).reduce(
(acc, [key, value]) => `${acc} ${key} ${value}`,
`${cortexPath}`
)
log.info('Starting cortex with command:', command)
// init cortex
@ -154,7 +173,7 @@ async function stopCortexService() {
async function stopApiServer() {
// this function is not meant to be success. It will throw an error.
try {
await fetch('http://localhost:1337/v1/system', {
await fetch(`http://${host}:${cortexJsPort}/v1/system`, {
method: 'DELETE',
})
} catch (error) {

View File

@ -1 +1 @@
0.5.0-27
0.5.0-29

View File

@ -40,16 +40,21 @@ const DownloadStatus: React.FC = () => {
? ((totalTransfferedSize / totalDownloadSize) * 100).toFixed(2)
: 0
const downloadTitle = `Downloading ${downloadStates
.map((state) => state.type)
.join(', ')
.trim()}`
return (
<Fragment>
{Object.values(downloadStates)?.length > 0 && (
<Modal
title="Downloading model"
title={downloadTitle}
trigger={
<div className="flex cursor-pointer items-center gap-2">
<Button size="small" theme="ghost">
<span className="font-medium">
Downloading model{' '}
{downloadTitle}{' '}
{Object.values(downloadStates).length > 1 &&
`1/${Object.values(downloadStates).length}`}
</span>

View File

@ -15,6 +15,8 @@ import TopPanel from '@/containers/Layout/TopPanel'
import { getImportModelStageAtom } from '@/hooks/useImportModel'
import useMigratingData from '@/hooks/useMigratingData'
import DownloadLocalModelModal from '@/screens/HubScreen2/components/DownloadLocalModelModal'
import InferenceErrorModal from '@/screens/HubScreen2/components/InferenceErrorModal'
import SetUpApiKeyModal from '@/screens/HubScreen2/components/SetUpApiKeyModal'
@ -33,17 +35,24 @@ import LoadingModal from '../LoadingModal'
import MainViewContainer from '../MainViewContainer'
import ModalMigrations, {
showMigrationModalAtom,
} from '../Providers/ModalMigrations'
import WaitingForCortexModal from '../WaitingCortexModal'
import InstallingExtensionModal from './BottomPanel/InstallingExtension/InstallingExtensionModal'
import { MainViewState, mainViewStateAtom } from '@/helpers/atoms/App.atom'
import { didShowMigrationWarningAtom } from '@/helpers/atoms/AppConfig.atom'
import { reduceTransparentAtom } from '@/helpers/atoms/Setting.atom'
const BaseLayout = () => {
const didShowMigrationWarning = useAtomValue(didShowMigrationWarningAtom)
const setShowMigrationModal = useSetAtom(showMigrationModalAtom)
const setMainViewState = useSetAtom(mainViewStateAtom)
const importModelStage = useAtomValue(getImportModelStageAtom)
const reduceTransparent = useAtomValue(reduceTransparentAtom)
const { getJanThreadsAndMessages, getJanLocalModels } = useMigratingData()
useEffect(() => {
if (localStorage.getItem(SUCCESS_SET_NEW_DESTINATION) === 'true') {
@ -51,6 +60,29 @@ const BaseLayout = () => {
}
}, [setMainViewState])
useEffect(() => {
if (didShowMigrationWarning) return
const isUserHaveData = async (): Promise<boolean> => {
const threadAndMessageData = await getJanThreadsAndMessages()
const isUserHaveAnyModel = await getJanLocalModels()
return threadAndMessageData.threads.length > 0 || isUserHaveAnyModel
}
isUserHaveData()
.then((isUserHaveData) => {
if (isUserHaveData === true) {
setShowMigrationModal(true)
}
})
.catch((e) => console.error('Error checking user data', e))
}, [
didShowMigrationWarning,
getJanThreadsAndMessages,
getJanLocalModels,
setShowMigrationModal,
])
return (
<div
className={twMerge(
@ -92,6 +124,7 @@ const BaseLayout = () => {
<ChooseWhatToImportModal />
<InstallingExtensionModal />
<HuggingFaceRepoDetailModal />
<ModalMigrations />
</div>
<BottomPanel />
</div>

View File

@ -1,8 +1,8 @@
import React, { Fragment, useCallback, useEffect } from 'react'
import React, { Fragment, useCallback, useMemo, useState } from 'react'
import { Button, Modal, Badge } from '@janhq/joi'
import { useAtom, useAtomValue } from 'jotai'
import { atom, useAtom, useSetAtom } from 'jotai'
import { AlertTriangleIcon } from 'lucide-react'
import { twMerge } from 'tailwind-merge'
@ -11,25 +11,24 @@ import Spinner from '@/containers/Loader/Spinner'
import useMigratingData from '@/hooks/useMigratingData'
import {
didShowMigrationWarningAtom,
modelsMigrationSuccessAtom,
threadsMessagesMigrationSuccessAtom,
skipMigrationAtom,
} from '@/helpers/atoms/AppConfig.atom'
import { didShowMigrationWarningAtom } from '@/helpers/atoms/AppConfig.atom'
export const showMigrationModalAtom = atom<boolean>(false)
const MigrationStates = ['idle', 'in_progress', 'failed', 'success'] as const
type MigrationState = (typeof MigrationStates)[number]
const ModalMigrations = () => {
const [didShowMigrationWarning, setDidShowMigrationWarning] = useAtom(
didShowMigrationWarningAtom
)
const [skipMigration, setSkipMigration] = useAtom(skipMigrationAtom)
const modelsMigrationSuccess = useAtomValue(modelsMigrationSuccessAtom)
const threadsMessagesMigrationSuccess = useAtomValue(
threadsMessagesMigrationSuccessAtom
const setDidShowMigrationModal = useSetAtom(didShowMigrationWarningAtom)
const [showMigrationModal, setShowMigrationModal] = useAtom(
showMigrationModalAtom
)
const [step, setStep] = React.useState(1)
const [loaderThreads, setLoaderThreads] = React.useState(false)
const [loaderModels, setLoaderModels] = React.useState(false)
const { migrateModels, migrateThreadsAndMessages } = useMigratingData()
const [threadAndMessageMigrationState, setThreadAndMessageMigrationState] =
useState<MigrationState>('idle')
const [modelMigrationState, setModelMigrationState] =
useState<MigrationState>('idle')
const getStepTitle = () => {
switch (step) {
@ -37,84 +36,63 @@ const ModalMigrations = () => {
return 'Important Update: Data Migration Needed'
default:
return loaderThreads || loaderModels
? 'Migration In Progress'
return threadAndMessageMigrationState === 'in_progress' ||
modelMigrationState === 'in_progress'
? 'Migrating'
: 'Migration Completed'
}
}
const handleStartMigration = async () => {
setStep(2)
await handleStartMigrationModels()
await handleStartMigrationThreads()
}
const handleStartMigrationThreads = async () => {
setLoaderThreads(true)
await migrateThreadsAndMessages()
setTimeout(() => {
setLoaderThreads(false)
}, 1200)
}
const handleStartMigrationModels = async () => {
setLoaderModels(true)
await migrateModels()
setTimeout(() => {
setLoaderModels(false)
}, 1200)
}
const {
getJanThreadsAndMessages,
migrateModels,
migrateThreadsAndMessages,
getJanLocalModels,
} = useMigratingData()
const getMigrationNotif = useCallback(async () => {
const migrationThreadsAndMessages = useCallback(async () => {
setThreadAndMessageMigrationState('in_progress')
try {
const resultThreadsAndMessages = await getJanThreadsAndMessages()
const resultLocalModels = await getJanLocalModels()
if (
resultThreadsAndMessages.threads.length > 0 ||
resultThreadsAndMessages.messages.length > 0 ||
resultLocalModels
) {
setDidShowMigrationWarning(true)
} else {
setDidShowMigrationWarning(false)
}
} catch (error) {
setDidShowMigrationWarning(false)
console.error(error)
await migrateThreadsAndMessages()
setThreadAndMessageMigrationState('success')
console.debug('Migrating threads and messages successfully!')
} catch (err) {
console.error('Migrating threads and messages error', err)
setThreadAndMessageMigrationState('failed')
}
}, [getJanLocalModels, getJanThreadsAndMessages, setDidShowMigrationWarning])
}, [setThreadAndMessageMigrationState, migrateThreadsAndMessages])
useEffect(() => {
if (
skipMigration ||
(threadsMessagesMigrationSuccess && modelsMigrationSuccess)
) {
return setDidShowMigrationWarning(false)
} else {
getMigrationNotif()
const migratingModels = useCallback(async () => {
setModelMigrationState('in_progress')
try {
await migrateModels()
setModelMigrationState('success')
console.debug('Migrating models successfully!')
} catch (err) {
console.error('Migrating models error', err)
setModelMigrationState('failed')
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
skipMigration,
setDidShowMigrationWarning,
threadsMessagesMigrationSuccess,
modelsMigrationSuccess,
])
}, [migrateModels, setModelMigrationState])
const onStartMigrationClick = useCallback(async () => {
setStep(2)
await migratingModels()
await migrationThreadsAndMessages()
}, [migratingModels, migrationThreadsAndMessages])
const onDismiss = useCallback(() => {
setStep(1)
setShowMigrationModal(false)
setDidShowMigrationModal(true)
}, [setDidShowMigrationModal, setShowMigrationModal])
const disableDismissButton = useMemo(
() =>
threadAndMessageMigrationState === 'in_progress' ||
modelMigrationState === 'in_progress',
[threadAndMessageMigrationState, modelMigrationState]
)
return (
<Modal
open={didShowMigrationWarning}
open={showMigrationModal}
hideClose
title={getStepTitle()}
content={
<>
<Fragment>
{step === 1 && (
<Fragment>
<p className="text-[hsla(var(--text-secondary))]">
@ -149,19 +127,10 @@ const ModalMigrations = () => {
</div>
<div className="flex justify-end">
<Button
className="mt-4"
theme="ghost"
onClick={() => {
setSkipMigration(true)
}}
>
<Button className="mt-4" theme="ghost" onClick={onDismiss}>
Skip
</Button>
<Button
className="ml-2 mt-4"
onClick={() => handleStartMigration()}
>
<Button className="ml-2 mt-4" onClick={onStartMigrationClick}>
Migrate Now
</Button>
</div>
@ -172,16 +141,17 @@ const ModalMigrations = () => {
<div
className={twMerge(
'mb-2 mt-4 flex justify-between rounded-lg border border-[hsla(var(--app-border))] p-3',
threadsMessagesMigrationSuccess
threadAndMessageMigrationState === 'success'
? 'bg-[hsla(var(--success-bg-soft))]'
: 'bg-[hsla(var(--destructive-bg-soft))]',
loaderThreads && 'bg-trasparent'
threadAndMessageMigrationState === 'in_progress' &&
'bg-trasparent'
)}
>
<div className="flex items-center gap-x-1.5">
{!loaderThreads && (
{threadAndMessageMigrationState !== 'in_progress' && (
<>
{threadsMessagesMigrationSuccess ? (
{threadAndMessageMigrationState === 'success' ? (
<Badge theme="success">Success</Badge>
) : (
<Badge theme="destructive">Failed</Badge>
@ -190,14 +160,14 @@ const ModalMigrations = () => {
)}
<p className="font-bold">Threads</p>
</div>
{loaderThreads ? (
{threadAndMessageMigrationState === 'in_progress' ? (
<Spinner />
) : (
!threadsMessagesMigrationSuccess && (
threadAndMessageMigrationState !== 'success' && (
<Button
size="small"
theme="ghost"
onClick={() => handleStartMigrationThreads()}
onClick={migrateThreadsAndMessages}
>
Retry
</Button>
@ -207,16 +177,16 @@ const ModalMigrations = () => {
<div
className={twMerge(
'my-2 flex justify-between rounded-lg border border-[hsla(var(--app-border))] p-3',
modelsMigrationSuccess
modelMigrationState === 'success'
? 'bg-[hsla(var(--success-bg-soft))]'
: 'bg-[hsla(var(--destructive-bg-soft))]',
loaderModels && 'bg-trasparent'
modelMigrationState === 'in_progress' && 'bg-trasparent'
)}
>
<div className="flex items-center gap-x-1.5">
{!loaderModels && (
{modelMigrationState !== 'in_progress' && (
<>
{modelsMigrationSuccess ? (
{modelMigrationState === 'success' ? (
<Badge theme="success">Success</Badge>
) : (
<Badge theme="destructive">Failed</Badge>
@ -225,14 +195,14 @@ const ModalMigrations = () => {
)}
<p className="font-bold">Models</p>
</div>
{loaderModels ? (
{modelMigrationState === 'in_progress' ? (
<Spinner />
) : (
!modelsMigrationSuccess && (
modelMigrationState === 'failed' && (
<Button
size="small"
theme="ghost"
onClick={() => handleStartMigrationModels()}
onClick={migratingModels}
>
Retry
</Button>
@ -242,17 +212,15 @@ const ModalMigrations = () => {
<div className="flex justify-end">
<Button
className="mt-2"
disabled={loaderThreads || loaderModels}
onClick={() => {
setDidShowMigrationWarning(false)
}}
disabled={disableDismissButton}
onClick={onDismiss}
>
Done
</Button>
</div>
</Fragment>
)}
</>
</Fragment>
}
/>
)

View File

@ -14,7 +14,6 @@ import ThemeWrapper from '@/containers/Providers/Theme'
import { setupCoreServices } from '@/services/coreService'
import DataLoader from './DataLoader'
import ModalMigrations from './ModalMigrations'
import Responsive from './Responsive'
@ -42,7 +41,6 @@ const Providers = ({ children }: PropsWithChildren) => {
<Toaster />
</Fragment>
)}
<ModalMigrations />
</QueryClientProvider>
</JotaiWrapper>
</ThemeWrapper>

View File

@ -8,10 +8,6 @@ const IGNORE_SSL = 'ignoreSSLFeature'
const HTTPS_PROXY_FEATURE = 'httpsProxyFeature'
const QUICK_ASK_ENABLED = 'quickAskEnabled'
const MIGRATION_WARNING = 'didShowMigrationWarning'
const THREADS_MESSAGES_MIGRATION_SUCCESS = 'threadsMessagesMigrationSuccess'
const MODELS_MIGRATION_SUCCESS = 'modelsMigrationSuccess'
const SKIP_MIGRATION = 'skipMigration'
export const janDataFolderPathAtom = atom('')
export const experimentalFeatureEnabledAtom = atomWithStorage(
@ -27,26 +23,6 @@ export const vulkanEnabledAtom = atomWithStorage(VULKAN_ENABLED, false)
export const quickAskEnabledAtom = atomWithStorage(QUICK_ASK_ENABLED, false)
export const didShowMigrationWarningAtom = atomWithStorage(
MIGRATION_WARNING,
false
)
export const threadsMessagesMigrationSuccessAtom = atomWithStorage(
THREADS_MESSAGES_MIGRATION_SUCCESS,
false,
undefined,
{
getOnInit: true,
}
)
export const modelsMigrationSuccessAtom = atomWithStorage(
MODELS_MIGRATION_SUCCESS,
false,
undefined,
{
getOnInit: true,
}
)
export const skipMigrationAtom = atomWithStorage(
SKIP_MIGRATION,
false,
undefined,
{
@ -54,4 +30,4 @@ export const skipMigrationAtom = atomWithStorage(
}
)
export const hostAtom = atom('http://localhost:1337/v1')
export const hostAtom = atom('http://127.0.0.1:1338/v1')

View File

@ -247,22 +247,24 @@ const useCortex = () => {
const downloadModel = useCallback(
async (modelId: string, fileName?: string, persistedModelId?: string) => {
try {
// return await cortex.models.download(modelId)
return await fetch(`${host}/models/${modelId}/pull`, {
method: 'POST',
headers: {
'accept': 'application/json',
// eslint-disable-next-line @typescript-eslint/naming-convention
'Content-Type': 'application/json',
},
body: JSON.stringify({
fileName: fileName,
persistedModelId: persistedModelId,
}),
})
} catch (err) {
console.error(err)
const response = await fetch(`${host}/models/${modelId}/pull`, {
method: 'POST',
headers: {
'accept': 'application/json',
// eslint-disable-next-line @typescript-eslint/naming-convention
'Content-Type': 'application/json',
},
body: JSON.stringify({
fileName: fileName,
persistedModelId: persistedModelId,
}),
})
if (!response.ok) {
const responseJson = await response.json()
const errorMessage: string =
(responseJson.error?.message as string) ??
`Failed to download model ${modelId}`
throw new Error(errorMessage)
}
},
[host]

View File

@ -1,25 +1,13 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useCallback } from 'react'
import { useAtom } from 'jotai'
import useAssistantQuery from './useAssistantQuery'
import useCortex from './useCortex'
import useMessageCreateMutation from './useMessageCreateMutation'
import useThreads from './useThreads'
import {
threadsMessagesMigrationSuccessAtom,
modelsMigrationSuccessAtom,
} from '@/helpers/atoms/AppConfig.atom'
const useMigratingData = () => {
const [threadsMessagesMigrationSuccess, setThreadsMessagesMigrationSuccess] =
useAtom(threadsMessagesMigrationSuccessAtom)
const [modelsMigrationSuccess, setModelsMigrationSuccess] = useAtom(
modelsMigrationSuccessAtom
)
const { createThread } = useThreads()
const { updateThread } = useCortex()
const createMessage = useMessageCreateMutation()
@ -32,92 +20,69 @@ const useMigratingData = () => {
return window?.electronAPI?.getAllMessagesAndThreads()
}, [])
const getJanLocalModels = useCallback(async (): Promise<{
hasLocalModels: boolean
}> => {
const getJanLocalModels = useCallback(async (): Promise<boolean> => {
// TODO: change the name of this function
return window?.electronAPI?.getAllLocalModels()
}, [])
const migrateModels = useCallback(async () => {
try {
if (!modelsMigrationSuccess) {
await window?.electronAPI?.syncModelFileToCortex()
setModelsMigrationSuccess(true)
}
} catch (err) {
console.log(err)
setModelsMigrationSuccess(false)
}
}, [modelsMigrationSuccess, setModelsMigrationSuccess])
return window?.electronAPI?.syncModelFileToCortex()
}, [])
const migrateThreadsAndMessages = useCallback(async () => {
if (!assistants || assistants.length === 0) {
console.error('No assistant found')
return
}
try {
if (threadsMessagesMigrationSuccess) return
const threadsAndMessages = await getJanThreadsAndMessages()
const janThreads = threadsAndMessages.threads
for (const thread of janThreads) {
const modelId: string | undefined = thread.assistants[0]?.model?.id
if (!modelId || modelId.trim().length === 0 || modelId === '*') {
console.error(
`Ignore thread ${thread.id} because modelId is not found`
)
continue
}
const threadTitle: string = thread.title ?? 'New Thread'
const instructions: string = thread.assistants[0]?.instructions ?? ''
// currently, we don't have api support for creating thread with messages
const cortexThread = await createThread(modelId, assistants[0])
console.log('createThread', cortexThread)
// update instruction
cortexThread.assistants[0].instructions = instructions
cortexThread.title = threadTitle
const threadsAndMessages = await getJanThreadsAndMessages()
const janThreads = threadsAndMessages.threads
// update thread name
await updateThread(cortexThread)
console.log('updateThread', cortexThread)
// we finished with thread, now continue with messages
const janMessages = threadsAndMessages.messages.filter(
(m) => m.thread_id === thread.id
)
console.log(janMessages)
for (let j = 0; j < janMessages.length; ++j) {
const janMessage = janMessages[j]
// filter out the system message if any
if (janMessage.role === 'system') continue
try {
const messageContent: string =
janMessage.content[0]?.text.value ?? ''
// can speed up here with Promise.allSettled
await createMessage.mutateAsync({
threadId: cortexThread.id,
createMessageParams: {
content: messageContent,
role: janMessage.role,
},
})
} catch (err) {
console.error(err)
}
}
setThreadsMessagesMigrationSuccess(true)
for (const thread of janThreads) {
const modelId: string | undefined = thread.assistants[0]?.model?.id
if (!modelId || modelId.trim().length === 0 || modelId === '*') {
console.error(`Ignore thread ${thread.id} because modelId is not found`)
continue
}
const threadTitle: string = thread.title ?? 'New Thread'
const instructions: string = thread.assistants[0]?.instructions ?? ''
// currently, we don't have api support for creating thread with messages
const cortexThread = await createThread(modelId, assistants[0])
console.log('createThread', cortexThread)
// update instruction
cortexThread.assistants[0].instructions = instructions
cortexThread.title = threadTitle
// update thread name
await updateThread(cortexThread)
console.log('updateThread', cortexThread)
// we finished with thread, now continue with messages
const janMessages = threadsAndMessages.messages.filter(
(m) => m.thread_id === thread.id
)
for (let j = 0; j < janMessages.length; ++j) {
const janMessage = janMessages[j]
// filter out the system message if any
if (janMessage.role === 'system') continue
const messageContent: string = janMessage.content[0]?.text.value ?? ''
// can speed up here with Promise.allSettled
await createMessage.mutateAsync({
threadId: cortexThread.id,
createMessageParams: {
content: messageContent,
role: janMessage.role,
},
})
}
} catch (err) {
console.log(err)
setThreadsMessagesMigrationSuccess(false)
}
}, [
assistants,
getJanThreadsAndMessages,
threadsMessagesMigrationSuccess,
createThread,
updateThread,
setThreadsMessagesMigrationSuccess,
createMessage,
])

View File

@ -0,0 +1,64 @@
import { useCallback } from 'react'
import { Button } from '@janhq/joi'
import { useAtomValue, useSetAtom } from 'jotai'
import { showMigrationModalAtom } from '@/containers/Providers/ModalMigrations'
import { toaster } from '@/containers/Toast'
import useThreads from '@/hooks/useThreads'
import { threadsAtom } from '@/helpers/atoms/Thread.atom'
const DataMigration: React.FC = () => {
const setShowMigrationModal = useSetAtom(showMigrationModalAtom)
const threads = useAtomValue(threadsAtom)
const { deleteThread } = useThreads()
const onStartMigrationClick = useCallback(() => {
setShowMigrationModal(true)
}, [setShowMigrationModal])
const onCleanUpDataClick = useCallback(async () => {
for (const thread of threads) {
try {
await deleteThread(thread.id)
} catch (err) {
console.error('Error deleting thread', err)
toaster({
title: 'Delete thread failed',
description: `Failed to delete thread ${thread.title}`,
type: 'error',
})
}
}
toaster({
title: 'Delete thread successfully!',
type: 'success',
})
}, [threads, deleteThread])
return (
<div className="flex w-full flex-col items-start justify-between gap-4 border-b border-[hsla(var(--app-border))] py-4 first:pt-0 last:border-none sm:flex-row">
<div className="flex-shrink-0 space-y-1">
<div className="flex gap-x-2">
<h6 className="font-semibold capitalize">Clear logs</h6>
</div>
<p className="font-medium leading-relaxed text-[hsla(var(--text-secondary))]">
Clear all logs from Jan app.
</p>
</div>
<div className="flex flex-row gap-x-2">
<Button theme="primary" onClick={onStartMigrationClick}>
Start migration
</Button>
<Button theme="destructive" onClick={onCleanUpDataClick}>
Remove threads and messages
</Button>
</div>
</div>
)
}
export default DataMigration

View File

@ -13,6 +13,8 @@ import { toaster } from '@/containers/Toast'
import useModelStop from '@/hooks/useModelStop'
import { useSettings } from '@/hooks/useSettings'
import DataMigration from './FactoryReset/components/DataMigration'
import {
experimentalFeatureEnabledAtom,
ignoreSslAtom,
@ -21,6 +23,7 @@ import {
vulkanEnabledAtom,
quickAskEnabledAtom,
} from '@/helpers/atoms/AppConfig.atom'
import { activeModelsAtom } from '@/helpers/atoms/Model.atom'
// type GPU = {
@ -459,6 +462,7 @@ const Advanced = () => {
{/* Factory Reset */}
{/* <FactoryReset /> */}
{experimentalEnabled && <DataMigration />}
</div>
</ScrollArea>
)

View File

@ -1,4 +1,4 @@
import { useEffect } from 'react'
import { Fragment, useEffect } from 'react'
import { Modal } from '@janhq/joi'
import { useAtomValue, useSetAtom } from 'jotai'
@ -13,11 +13,15 @@ import {
import ImportingModelItem from './ImportingModelItem'
import { importingModelsAtom } from '@/helpers/atoms/Model.atom'
import {
importingModelsAtom,
setImportingModelErrorAtom,
} from '@/helpers/atoms/Model.atom'
const ImportingModelModal = () => {
const { downloadModel } = useCortex()
const setImportModelStage = useSetAtom(setImportModelStageAtom)
const setImportModelError = useSetAtom(setImportingModelErrorAtom)
const importingModels = useAtomValue(importingModelsAtom)
const importModelStage = useAtomValue(getImportModelStageAtom)
@ -28,7 +32,16 @@ const ImportingModelModal = () => {
useEffect(() => {
const importModels = async () => {
for (const model of importingModels) {
await downloadModel(model.path)
try {
await downloadModel(model.path)
} catch (error) {
let errorMessage = String(error)
if (error instanceof Error) {
errorMessage = error.message
}
setImportModelError(model.path, errorMessage)
}
}
}
importModels()
@ -41,21 +54,7 @@ const ImportingModelModal = () => {
onOpenChange={() => setImportModelStage('NONE')}
title={`Importing model (${finishedImportModel}/${importingModels.length})`}
content={
<div>
<div className="flex flex-row items-center space-x-2 pb-3">
{/* <label className="text-[hsla(var(--text-secondary)] text-xs">
{modelFolder}
</label>
<Button
theme="ghost"
size="small"
variant="outline"
onClick={onOpenModelFolderClick}
>
{openFileTitle()}
</Button> */}
</div>
<Fragment>
<div className="mb-2 space-y-3">
{importingModels.map((model) => (
<ImportingModelItem key={model.importId} model={model} />
@ -75,7 +74,7 @@ const ImportingModelModal = () => {
</p>
</div>
</div>
</div>
</Fragment>
}
/>
)