Skip to content

Commit

Permalink
fix: file explore on windows show empty when importing model
Browse files Browse the repository at this point in the history
Signed-off-by: James <james@jan.ai>
  • Loading branch information
James authored and jan-hiro-v committed Mar 25, 2024
1 parent 1ad794c commit a78978b
Show file tree
Hide file tree
Showing 9 changed files with 236 additions and 99 deletions.
2 changes: 1 addition & 1 deletion core/src/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>
<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

0 comments on commit a78978b

Please sign in to comment.