fix: file explore on windows show empty when importing model (#2484)

Signed-off-by: James <james@jan.ai>
Co-authored-by: James <james@jan.ai>
This commit is contained in:
NamH 2024-03-29 01:24:53 +07:00 committed by GitHub
parent a03e37f5a6
commit 3ecdb81881
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 237 additions and 100 deletions

View File

@ -7,7 +7,7 @@ export enum NativeRoute {
openAppDirectory = 'openAppDirectory',
openFileExplore = 'openFileExplorer',
selectDirectory = 'selectDirectory',
selectModelFiles = 'selectModelFiles',
selectFiles = 'selectFiles',
relaunch = 'relaunch',
hideQuickAskWindow = 'hideQuickAskWindow',

View File

@ -3,3 +3,4 @@ export * from './promptTemplate'
export * from './appUpdate'
export * from './fileDownloadRequest'
export * from './networkConfig'
export * from './selectFiles'

View File

@ -0,0 +1,30 @@
export type SelectFileOption = {
/**
* The title of the dialog.
*/
title?: string
/**
* Whether the dialog allows multiple selection.
*/
allowMultiple?: boolean
buttonLabel?: string
selectDirectory?: boolean
props?: SelectFileProp[]
}
export const SelectFilePropTuple = [
'openFile',
'openDirectory',
'multiSelections',
'showHiddenFiles',
'createDirectory',
'promptToCreate',
'noResolveAliases',
'treatPackageAsDirectory',
'dontAddToRecent',
] as const
export type SelectFileProp = (typeof SelectFilePropTuple)[number]

View File

@ -6,8 +6,11 @@ import {
getJanDataFolderPath,
getJanExtensionsPath,
init,
AppEvent, NativeRoute,
AppEvent,
NativeRoute,
SelectFileProp,
} from '@janhq/core/node'
import { SelectFileOption } from '@janhq/core/.'
export function handleAppIPCs() {
/**
@ -84,23 +87,38 @@ export function handleAppIPCs() {
}
})
ipcMain.handle(NativeRoute.selectModelFiles, async () => {
const mainWindow = windowManager.mainWindow
if (!mainWindow) {
console.error('No main window found')
return
}
const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow, {
title: 'Select model files',
buttonLabel: 'Select',
properties: ['openFile', 'openDirectory', 'multiSelections'],
})
if (canceled) {
return
}
ipcMain.handle(
NativeRoute.selectFiles,
async (_event, option?: SelectFileOption) => {
const mainWindow = windowManager.mainWindow
if (!mainWindow) {
console.error('No main window found')
return
}
return filePaths
})
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,
})
if (canceled) return
return filePaths
}
)
ipcMain.handle(
NativeRoute.hideQuickAskWindow,

View File

@ -17,6 +17,7 @@ import { getImportModelStageAtom } from '@/hooks/useImportModel'
import { SUCCESS_SET_NEW_DESTINATION } from '@/screens/Settings/Advanced/DataFolder'
import CancelModelImportModal from '@/screens/Settings/CancelModelImportModal'
import ChooseWhatToImportModal from '@/screens/Settings/ChooseWhatToImportModal'
import EditModelInfoModal from '@/screens/Settings/EditModelInfoModal'
import ImportModelOptionModal from '@/screens/Settings/ImportModelOptionModal'
import ImportingModelModal from '@/screens/Settings/ImportingModelModal'
@ -70,6 +71,7 @@ const BaseLayout = (props: PropsWithChildren) => {
{importModelStage === 'IMPORTING_MODEL' && <ImportingModelModal />}
{importModelStage === 'EDIT_MODEL_INFO' && <EditModelInfoModal />}
{importModelStage === 'CONFIRM_CANCEL' && <CancelModelImportModal />}
<ChooseWhatToImportModal />
<InstallingExtensionModal />
</div>
)

View File

@ -6,15 +6,26 @@ import {
Model,
ModelExtension,
OptionType,
baseName,
fs,
joinPath,
} from '@janhq/core'
import { atom } from 'jotai'
import { atom, useSetAtom } from 'jotai'
import { v4 as uuidv4 } from 'uuid'
import { snackbar } from '@/containers/Toast'
import { FilePathWithSize } from '@/utils/file'
import { extensionManager } from '@/extension'
import { importingModelsAtom } from '@/helpers/atoms/Model.atom'
export type ImportModelStage =
| 'NONE'
| 'SELECTING_MODEL'
| 'CHOOSE_WHAT_TO_IMPORT'
| 'MODEL_SELECTED'
| 'IMPORTING_MODEL'
| 'EDIT_MODEL_INFO'
@ -38,6 +49,9 @@ export type ModelUpdate = {
}
const useImportModel = () => {
const setImportModelStage = useSetAtom(setImportModelStageAtom)
const setImportingModels = useSetAtom(importingModelsAtom)
const importModels = useCallback(
(models: ImportingModel[], optionType: OptionType) =>
localImportModels(models, optionType),
@ -49,7 +63,75 @@ const useImportModel = () => {
[]
)
return { importModels, updateModelInfo }
const sanitizeFilePaths = useCallback(
async (filePaths: string[]) => {
if (!filePaths || filePaths.length === 0) return
const sanitizedFilePaths: FilePathWithSize[] = []
for (const filePath of filePaths) {
const fileStats = await fs.fileStat(filePath, true)
if (!fileStats) continue
if (!fileStats.isDirectory) {
const fileName = await baseName(filePath)
sanitizedFilePaths.push({
path: filePath,
name: fileName,
size: fileStats.size,
})
} else {
// allowing only one level of directory
const files = await fs.readdirSync(filePath)
for (const file of files) {
const fullPath = await joinPath([filePath, file])
const fileStats = await fs.fileStat(fullPath, true)
if (!fileStats || fileStats.isDirectory) continue
sanitizedFilePaths.push({
path: fullPath,
name: file,
size: fileStats.size,
})
}
}
}
const unsupportedFiles = sanitizedFilePaths.filter(
(file) => !file.path.endsWith('.gguf')
)
const supportedFiles = sanitizedFilePaths.filter((file) =>
file.path.endsWith('.gguf')
)
const importingModels: ImportingModel[] = supportedFiles.map(
({ path, name, size }: FilePathWithSize) => ({
importId: uuidv4(),
modelId: undefined,
name: name.replace('.gguf', ''),
description: '',
path: path,
tags: [],
size: size,
status: 'PREPARING',
format: 'gguf',
})
)
if (unsupportedFiles.length > 0) {
snackbar({
description: `Only files with .gguf extension can be imported.`,
type: 'error',
})
}
if (importingModels.length === 0) return
setImportingModels(importingModels)
setImportModelStage('MODEL_SELECTED')
},
[setImportModelStage, setImportingModels]
)
return { importModels, updateModelInfo, sanitizeFilePaths }
}
const localImportModels = async (

View File

@ -0,0 +1,65 @@
import { useCallback } from 'react'
import { SelectFileOption } from '@janhq/core'
import {
Button,
Modal,
ModalContent,
ModalHeader,
ModalTitle,
} from '@janhq/uikit'
import { useSetAtom, useAtomValue } from 'jotai'
import useImportModel, {
setImportModelStageAtom,
getImportModelStageAtom,
} from '@/hooks/useImportModel'
const ChooseWhatToImportModal: React.FC = () => {
const setImportModelStage = useSetAtom(setImportModelStageAtom)
const importModelStage = useAtomValue(getImportModelStageAtom)
const { sanitizeFilePaths } = useImportModel()
const onImportFileClick = useCallback(async () => {
const options: SelectFileOption = {
title: 'Select model files',
buttonLabel: 'Select',
allowMultiple: true,
}
const filePaths = await window.core?.api?.selectFiles(options)
if (!filePaths || filePaths.length === 0) return
sanitizeFilePaths(filePaths)
}, [sanitizeFilePaths])
const onImportFolderClick = useCallback(async () => {
const options: SelectFileOption = {
title: 'Select model folders',
buttonLabel: 'Select',
allowMultiple: true,
selectDirectory: true,
}
const filePaths = await window.core?.api?.selectFiles(options)
if (!filePaths || filePaths.length === 0) return
sanitizeFilePaths(filePaths)
}, [sanitizeFilePaths])
return (
<Modal
open={importModelStage === 'CHOOSE_WHAT_TO_IMPORT'}
onOpenChange={() => setImportModelStage('SELECTING_MODEL')}
>
<ModalContent>
<ModalHeader>
<ModalTitle>Choose what to import</ModalTitle>
</ModalHeader>
<div className="mt-2 flex flex-col space-y-3">
<Button onClick={onImportFileClick}>Import file (GGUF)</Button>
<Button onClick={onImportFolderClick}>Import Folder</Button>
</div>
</ModalContent>
</Modal>
)
}
export default ChooseWhatToImportModal

View File

@ -52,9 +52,7 @@ const ImportingModelModal: React.FC = () => {
return (
<Modal
open={importModelStage === 'IMPORTING_MODEL'}
onOpenChange={() => {
setImportModelStage('NONE')
}}
onOpenChange={() => setImportModelStage('NONE')}
>
<ModalContent>
<ModalHeader>

View File

@ -1,99 +1,40 @@
import { useCallback } from 'react'
import { useDropzone } from 'react-dropzone'
import { ImportingModel, baseName, fs, joinPath } from '@janhq/core'
import { SelectFileOption, systemInformation } from '@janhq/core'
import { Modal, ModalContent, ModalHeader, ModalTitle } from '@janhq/uikit'
import { useAtomValue, useSetAtom } from 'jotai'
import { UploadCloudIcon } from 'lucide-react'
import { v4 as uuidv4 } from 'uuid'
import { snackbar } from '@/containers/Toast'
import useDropModelBinaries from '@/hooks/useDropModelBinaries'
import {
import useImportModel, {
getImportModelStageAtom,
setImportModelStageAtom,
} from '@/hooks/useImportModel'
import { FilePathWithSize } from '@/utils/file'
import { importingModelsAtom } from '@/helpers/atoms/Model.atom'
const SelectingModelModal: React.FC = () => {
const setImportModelStage = useSetAtom(setImportModelStageAtom)
const importModelStage = useAtomValue(getImportModelStageAtom)
const setImportingModels = useSetAtom(importingModelsAtom)
const { onDropModels } = useDropModelBinaries()
const { sanitizeFilePaths } = useImportModel()
const onSelectFileClick = useCallback(async () => {
const filePaths = await window.core?.api?.selectModelFiles()
const platform = (await systemInformation()).osInfo?.platform
if (platform === 'win32') {
setImportModelStage('CHOOSE_WHAT_TO_IMPORT')
return
}
const options: SelectFileOption = {
title: 'Select model folders',
buttonLabel: 'Select',
allowMultiple: true,
selectDirectory: true,
}
const filePaths = await window.core?.api?.selectFiles(options)
if (!filePaths || filePaths.length === 0) return
const sanitizedFilePaths: FilePathWithSize[] = []
for (const filePath of filePaths) {
const fileStats = await fs.fileStat(filePath, true)
if (!fileStats) continue
if (!fileStats.isDirectory) {
const fileName = await baseName(filePath)
sanitizedFilePaths.push({
path: filePath,
name: fileName,
size: fileStats.size,
})
} else {
// allowing only one level of directory
const files = await fs.readdirSync(filePath)
for (const file of files) {
const fullPath = await joinPath([filePath, file])
const fileStats = await fs.fileStat(fullPath, true)
if (!fileStats || fileStats.isDirectory) continue
sanitizedFilePaths.push({
path: fullPath,
name: file,
size: fileStats.size,
})
}
}
}
const unsupportedFiles = sanitizedFilePaths.filter(
(file) => !file.path.endsWith('.gguf')
)
const supportedFiles = sanitizedFilePaths.filter((file) =>
file.path.endsWith('.gguf')
)
const importingModels: ImportingModel[] = supportedFiles.map(
({ path, name, size }: FilePathWithSize) => {
return {
importId: uuidv4(),
modelId: undefined,
name: name.replace('.gguf', ''),
description: '',
path: path,
tags: [],
size: size,
status: 'PREPARING',
format: 'gguf',
}
}
)
if (unsupportedFiles.length > 0) {
snackbar({
description: `Only files with .gguf extension can be imported.`,
type: 'error',
})
}
if (importingModels.length === 0) return
setImportingModels(importingModels)
setImportModelStage('MODEL_SELECTED')
}, [setImportingModels, setImportModelStage])
sanitizeFilePaths(filePaths)
}, [sanitizeFilePaths, setImportModelStage])
const { isDragActive, getRootProps } = useDropzone({
noClick: true,