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:
parent
a03e37f5a6
commit
3ecdb81881
@ -7,7 +7,7 @@ export enum NativeRoute {
|
||||
openAppDirectory = 'openAppDirectory',
|
||||
openFileExplore = 'openFileExplorer',
|
||||
selectDirectory = 'selectDirectory',
|
||||
selectModelFiles = 'selectModelFiles',
|
||||
selectFiles = 'selectFiles',
|
||||
relaunch = 'relaunch',
|
||||
|
||||
hideQuickAskWindow = 'hideQuickAskWindow',
|
||||
|
||||
@ -3,3 +3,4 @@ export * from './promptTemplate'
|
||||
export * from './appUpdate'
|
||||
export * from './fileDownloadRequest'
|
||||
export * from './networkConfig'
|
||||
export * from './selectFiles'
|
||||
|
||||
30
core/src/types/miscellaneous/selectFiles.ts
Normal file
30
core/src/types/miscellaneous/selectFiles.ts
Normal 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]
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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 (
|
||||
|
||||
65
web/screens/Settings/ChooseWhatToImportModal/index.tsx
Normal file
65
web/screens/Settings/ChooseWhatToImportModal/index.tsx
Normal 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
|
||||
@ -52,9 +52,7 @@ const ImportingModelModal: React.FC = () => {
|
||||
return (
|
||||
<Modal
|
||||
open={importModelStage === 'IMPORTING_MODEL'}
|
||||
onOpenChange={() => {
|
||||
setImportModelStage('NONE')
|
||||
}}
|
||||
onOpenChange={() => setImportModelStage('NONE')}
|
||||
>
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user