Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 1 commit into from
Mar 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion core/src/types/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export enum NativeRoute {
openAppDirectory = 'openAppDirectory',
openFileExplore = 'openFileExplorer',
selectDirectory = 'selectDirectory',
selectModelFiles = 'selectModelFiles',
selectFiles = 'selectFiles',
relaunch = 'relaunch',

hideQuickAskWindow = 'hideQuickAskWindow',
Expand Down
3 changes: 2 additions & 1 deletion core/src/types/miscellaneous/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export * from './systemResourceInfo'
export * from './promptTemplate'
export * from './appUpdate'
export * from './fileDownloadRequest'
export * from './networkConfig'
export * from './networkConfig'
export * from './selectFiles'
30 changes: 30 additions & 0 deletions core/src/types/miscellaneous/selectFiles.ts
Original file line number Diff line number Diff line change
@@ -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]
52 changes: 35 additions & 17 deletions electron/handlers/native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
/**
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions web/containers/Layout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -70,6 +71,7 @@ const BaseLayout = (props: PropsWithChildren) => {
{importModelStage === 'IMPORTING_MODEL' && <ImportingModelModal />}
{importModelStage === 'EDIT_MODEL_INFO' && <EditModelInfoModal />}
{importModelStage === 'CONFIRM_CANCEL' && <CancelModelImportModal />}
<ChooseWhatToImportModal />
<InstallingExtensionModal />
</div>
)
Expand Down
86 changes: 84 additions & 2 deletions web/hooks/useImportModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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),
Expand All @@ -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 (
Expand Down
65 changes: 65 additions & 0 deletions web/screens/Settings/ChooseWhatToImportModal/index.tsx
Original file line number Diff line number Diff line change
@@ -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
4 changes: 1 addition & 3 deletions web/screens/Settings/ImportingModelModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,7 @@ const ImportingModelModal: React.FC = () => {
return (
<Modal
open={importModelStage === 'IMPORTING_MODEL'}
onOpenChange={() => {
setImportModelStage('NONE')
}}
onOpenChange={() => setImportModelStage('NONE')}
>
<ModalContent>
<ModalHeader>
Expand Down
Loading
Loading