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', openAppDirectory = 'openAppDirectory',
openFileExplore = 'openFileExplorer', openFileExplore = 'openFileExplorer',
selectDirectory = 'selectDirectory', selectDirectory = 'selectDirectory',
selectModelFiles = 'selectModelFiles', selectFiles = 'selectFiles',
relaunch = 'relaunch', relaunch = 'relaunch',
hideQuickAskWindow = 'hideQuickAskWindow', hideQuickAskWindow = 'hideQuickAskWindow',

View File

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

View File

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

View File

@ -6,15 +6,26 @@ import {
Model, Model,
ModelExtension, ModelExtension,
OptionType, OptionType,
baseName,
fs,
joinPath,
} from '@janhq/core' } 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 { extensionManager } from '@/extension'
import { importingModelsAtom } from '@/helpers/atoms/Model.atom'
export type ImportModelStage = export type ImportModelStage =
| 'NONE' | 'NONE'
| 'SELECTING_MODEL' | 'SELECTING_MODEL'
| 'CHOOSE_WHAT_TO_IMPORT'
| 'MODEL_SELECTED' | 'MODEL_SELECTED'
| 'IMPORTING_MODEL' | 'IMPORTING_MODEL'
| 'EDIT_MODEL_INFO' | 'EDIT_MODEL_INFO'
@ -38,6 +49,9 @@ export type ModelUpdate = {
} }
const useImportModel = () => { const useImportModel = () => {
const setImportModelStage = useSetAtom(setImportModelStageAtom)
const setImportingModels = useSetAtom(importingModelsAtom)
const importModels = useCallback( const importModels = useCallback(
(models: ImportingModel[], optionType: OptionType) => (models: ImportingModel[], optionType: OptionType) =>
localImportModels(models, 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 ( 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 ( return (
<Modal <Modal
open={importModelStage === 'IMPORTING_MODEL'} open={importModelStage === 'IMPORTING_MODEL'}
onOpenChange={() => { onOpenChange={() => setImportModelStage('NONE')}
setImportModelStage('NONE')
}}
> >
<ModalContent> <ModalContent>
<ModalHeader> <ModalHeader>

View File

@ -1,99 +1,40 @@
import { useCallback } from 'react' import { useCallback } from 'react'
import { useDropzone } from 'react-dropzone' 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 { Modal, ModalContent, ModalHeader, ModalTitle } from '@janhq/uikit'
import { useAtomValue, useSetAtom } from 'jotai' import { useAtomValue, useSetAtom } from 'jotai'
import { UploadCloudIcon } from 'lucide-react' import { UploadCloudIcon } from 'lucide-react'
import { v4 as uuidv4 } from 'uuid'
import { snackbar } from '@/containers/Toast'
import useDropModelBinaries from '@/hooks/useDropModelBinaries' import useDropModelBinaries from '@/hooks/useDropModelBinaries'
import { import useImportModel, {
getImportModelStageAtom, getImportModelStageAtom,
setImportModelStageAtom, setImportModelStageAtom,
} from '@/hooks/useImportModel' } from '@/hooks/useImportModel'
import { FilePathWithSize } from '@/utils/file'
import { importingModelsAtom } from '@/helpers/atoms/Model.atom'
const SelectingModelModal: React.FC = () => { const SelectingModelModal: React.FC = () => {
const setImportModelStage = useSetAtom(setImportModelStageAtom) const setImportModelStage = useSetAtom(setImportModelStageAtom)
const importModelStage = useAtomValue(getImportModelStageAtom) const importModelStage = useAtomValue(getImportModelStageAtom)
const setImportingModels = useSetAtom(importingModelsAtom)
const { onDropModels } = useDropModelBinaries() const { onDropModels } = useDropModelBinaries()
const { sanitizeFilePaths } = useImportModel()
const onSelectFileClick = useCallback(async () => { 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 if (!filePaths || filePaths.length === 0) return
sanitizeFilePaths(filePaths)
const sanitizedFilePaths: FilePathWithSize[] = [] }, [sanitizeFilePaths, setImportModelStage])
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])
const { isDragActive, getRootProps } = useDropzone({ const { isDragActive, getRootProps } = useDropzone({
noClick: true, noClick: true,