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',
|
openAppDirectory = 'openAppDirectory',
|
||||||
openFileExplore = 'openFileExplorer',
|
openFileExplore = 'openFileExplorer',
|
||||||
selectDirectory = 'selectDirectory',
|
selectDirectory = 'selectDirectory',
|
||||||
selectModelFiles = 'selectModelFiles',
|
selectFiles = 'selectFiles',
|
||||||
relaunch = 'relaunch',
|
relaunch = 'relaunch',
|
||||||
|
|
||||||
hideQuickAskWindow = 'hideQuickAskWindow',
|
hideQuickAskWindow = 'hideQuickAskWindow',
|
||||||
|
|||||||
@ -3,3 +3,4 @@ export * from './promptTemplate'
|
|||||||
export * from './appUpdate'
|
export * from './appUpdate'
|
||||||
export * from './fileDownloadRequest'
|
export * from './fileDownloadRequest'
|
||||||
export * from './networkConfig'
|
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,
|
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,
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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 (
|
||||||
|
|||||||
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 (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
open={importModelStage === 'IMPORTING_MODEL'}
|
open={importModelStage === 'IMPORTING_MODEL'}
|
||||||
onOpenChange={() => {
|
onOpenChange={() => setImportModelStage('NONE')}
|
||||||
setImportModelStage('NONE')
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user