fix: some bugs for import model (#2181)

* fix: some bugs for import model

Signed-off-by: James <james@jan.ai>

Signed-off-by: James <james@jan.ai>
Co-authored-by: James <james@jan.ai>
This commit is contained in:
NamH 2024-02-27 23:59:37 +07:00 committed by GitHub
parent 95946ab9f2
commit d7070d8c4a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 212 additions and 116 deletions

View File

@ -49,7 +49,7 @@ export enum DownloadEvent {
export enum LocalImportModelEvent {
onLocalImportModelUpdate = 'onLocalImportModelUpdate',
onLocalImportModelError = 'onLocalImportModelError',
onLocalImportModelFailed = 'onLocalImportModelFailed',
onLocalImportModelSuccess = 'onLocalImportModelSuccess',
onLocalImportModelFinished = 'onLocalImportModelFinished',
}

View File

@ -65,7 +65,7 @@ const joinPath: (paths: string[]) => Promise<string> = (paths) => global.core.ap
* @param path - The path to retrieve.
* @returns {Promise<string>} A promise that resolves with the basename.
*/
const baseName: (paths: string[]) => Promise<string> = (path) => global.core.api?.baseName(path)
const baseName: (paths: string) => Promise<string> = (path) => global.core.api?.baseName(path)
/**
* Opens an external URL in the default web browser.

View File

@ -19,4 +19,5 @@ export type ImportingModel = {
status: ImportingModelStatus
format: string
percentage?: number
error?: string
}

View File

@ -16,6 +16,7 @@ import {
OptionType,
ImportingModel,
LocalImportModelEvent,
baseName,
} from '@janhq/core'
import { extractFileName } from './helpers/path'
@ -488,7 +489,7 @@ export default class JanModelExtension extends ModelExtension {
return
}
const binaryFileName = extractFileName(modelBinaryPath, '')
const binaryFileName = await baseName(modelBinaryPath)
const model: Model = {
...defaultModel,
@ -555,7 +556,7 @@ export default class JanModelExtension extends ModelExtension {
model: ImportingModel,
optionType: OptionType
): Promise<Model> {
const binaryName = extractFileName(model.path, '').replace(/\s/g, '')
const binaryName = (await baseName(model.path)).replace(/\s/g, '')
let modelFolderName = binaryName
if (binaryName.endsWith(JanModelExtension._supportedModelFormat)) {
@ -568,7 +569,7 @@ export default class JanModelExtension extends ModelExtension {
const modelFolderPath = await this.getModelFolderName(modelFolderName)
await fs.mkdirSync(modelFolderPath)
const uniqueFolderName = modelFolderPath.split('/').pop()
const uniqueFolderName = await baseName(modelFolderPath)
const modelBinaryFile = binaryName.endsWith(
JanModelExtension._supportedModelFormat
)
@ -637,14 +638,21 @@ export default class JanModelExtension extends ModelExtension {
for (const model of models) {
events.emit(LocalImportModelEvent.onLocalImportModelUpdate, model)
const importedModel = await this.importModel(model, optionType)
events.emit(LocalImportModelEvent.onLocalImportModelSuccess, {
...model,
modelId: importedModel.id,
})
importedModels.push(importedModel)
try {
const importedModel = await this.importModel(model, optionType)
events.emit(LocalImportModelEvent.onLocalImportModelSuccess, {
...model,
modelId: importedModel.id,
})
importedModels.push(importedModel)
} catch (err) {
events.emit(LocalImportModelEvent.onLocalImportModelFailed, {
...model,
error: err,
})
}
}
events.emit(
LocalImportModelEvent.onLocalImportModelFinished,
importedModels

View File

@ -12,6 +12,7 @@ import { useSetAtom } from 'jotai'
import { snackbar } from '../Toast'
import {
setImportingModelErrorAtom,
setImportingModelSuccessAtom,
updateImportingModelProgressAtom,
} from '@/helpers/atoms/Model.atom'
@ -21,6 +22,7 @@ const ModelImportListener = ({ children }: PropsWithChildren) => {
updateImportingModelProgressAtom
)
const setImportingModelSuccess = useSetAtom(setImportingModelSuccessAtom)
const setImportingModelFailed = useSetAtom(setImportingModelErrorAtom)
const onImportModelUpdate = useCallback(
async (state: ImportingModel) => {
@ -30,6 +32,14 @@ const ModelImportListener = ({ children }: PropsWithChildren) => {
[updateImportingModelProgress]
)
const onImportModelFailed = useCallback(
async (state: ImportingModel) => {
if (!state.importId) return
setImportingModelFailed(state.importId, state.error ?? '')
},
[setImportingModelFailed]
)
const onImportModelSuccess = useCallback(
(state: ImportingModel) => {
if (!state.modelId) return
@ -62,6 +72,10 @@ const ModelImportListener = ({ children }: PropsWithChildren) => {
LocalImportModelEvent.onLocalImportModelFinished,
onImportModelFinished
)
events.on(
LocalImportModelEvent.onLocalImportModelFailed,
onImportModelFailed
)
return () => {
console.debug('ModelImportListener: unregistering event listeners...')
@ -77,8 +91,17 @@ const ModelImportListener = ({ children }: PropsWithChildren) => {
LocalImportModelEvent.onLocalImportModelFinished,
onImportModelFinished
)
events.off(
LocalImportModelEvent.onLocalImportModelFailed,
onImportModelFailed
)
}
}, [onImportModelUpdate, onImportModelSuccess, onImportModelFinished])
}, [
onImportModelUpdate,
onImportModelSuccess,
onImportModelFinished,
onImportModelFailed,
])
return <Fragment>{children}</Fragment>
}

View File

@ -67,6 +67,24 @@ export const updateImportingModelProgressAtom = atom(
}
)
export const setImportingModelErrorAtom = atom(
null,
(get, set, importId: string, error: string) => {
const model = get(importingModelsAtom).find((x) => x.importId === importId)
if (!model) return
const newModel: ImportingModel = {
...model,
status: 'FAILED',
}
console.error(`Importing model ${model} failed`, error)
const newList = get(importingModelsAtom).map((m) =>
m.importId === importId ? newModel : m
)
set(importingModelsAtom, newList)
}
)
export const setImportingModelSuccessAtom = atom(
null,
(get, set, importId: string, modelId: string) => {

View File

@ -0,0 +1,55 @@
import { useCallback } from 'react'
import { ImportingModel } from '@janhq/core'
import { useSetAtom } from 'jotai'
import { v4 as uuidv4 } from 'uuid'
import { snackbar } from '@/containers/Toast'
import { getFileInfoFromFile } from '@/utils/file'
import { setImportModelStageAtom } from './useImportModel'
import { importingModelsAtom } from '@/helpers/atoms/Model.atom'
export default function useDropModelBinaries() {
const setImportingModels = useSetAtom(importingModelsAtom)
const setImportModelStage = useSetAtom(setImportModelStageAtom)
const onDropModels = useCallback(
async (acceptedFiles: File[]) => {
const files = await getFileInfoFromFile(acceptedFiles)
const unsupportedFiles = files.filter(
(file) => !file.path.endsWith('.gguf')
)
const supportedFiles = files.filter((file) => file.path.endsWith('.gguf'))
const importingModels: ImportingModel[] = supportedFiles.map((file) => ({
importId: uuidv4(),
modelId: undefined,
name: file.name.replace('.gguf', ''),
description: '',
path: file.path,
tags: [],
size: file.size,
status: 'PREPARING',
format: 'gguf',
}))
if (unsupportedFiles.length > 0) {
snackbar({
description: `File has to be a .gguf file`,
type: 'error',
})
}
if (importingModels.length === 0) return
setImportingModels(importingModels)
setImportModelStage('MODEL_SELECTED')
},
[setImportModelStage, setImportingModels]
)
return { onDropModels }
}

View File

@ -1,6 +1,12 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useCallback, useEffect, useState } from 'react'
import { Model, ModelEvent, events, openFileExplorer } from '@janhq/core'
import {
Model,
ModelEvent,
events,
joinPath,
openFileExplorer,
} from '@janhq/core'
import {
Modal,
ModalContent,
@ -47,6 +53,7 @@ const EditModelInfoModal: React.FC = () => {
const janDataFolder = useAtomValue(janDataFolderPathAtom)
const updateImportingModel = useSetAtom(updateImportingModelAtom)
const { updateModelInfo } = useImportModel()
const [modelPath, setModelPath] = useState<string>('')
const editingModel = importingModels.find(
(model) => model.importId === editingModelId
@ -88,13 +95,19 @@ const EditModelInfoModal: React.FC = () => {
setEditingModelId(undefined)
}
const modelFolderPath = useMemo(() => {
return `${janDataFolder}/models/${editingModel?.modelId}`
useEffect(() => {
const getModelPath = async () => {
const modelId = editingModel?.modelId
if (!modelId) return ''
const path = await joinPath([janDataFolder, 'models', modelId])
setModelPath(path)
}
getModelPath()
}, [janDataFolder, editingModel])
const onShowInFinderClick = useCallback(() => {
openFileExplorer(modelFolderPath)
}, [modelFolderPath])
openFileExplorer(modelPath)
}, [modelPath])
if (!editingModel) {
setImportModelStage('IMPORTING_MODEL')
@ -104,7 +117,10 @@ const EditModelInfoModal: React.FC = () => {
}
return (
<Modal open={importModelStage === 'EDIT_MODEL_INFO'}>
<Modal
open={importModelStage === 'EDIT_MODEL_INFO'}
onOpenChange={onCancelClick}
>
<ModalContent>
<ModalHeader>
<ModalTitle>Edit Model Information</ModalTitle>
@ -130,7 +146,7 @@ const EditModelInfoModal: React.FC = () => {
</div>
<div className="mt-1 flex flex-row items-center space-x-2">
<span className="line-clamp-1 text-xs font-normal text-[#71717A]">
{modelFolderPath}
{modelPath}
</span>
<Button themes="ghost" onClick={onShowInFinderClick}>
{openFileTitle()}

View File

@ -15,7 +15,8 @@ const ImportInProgressIcon: React.FC<Props> = ({
const [isHovered, setIsHovered] = useState(false)
const onMouseOver = () => {
setIsHovered(true)
// for now we don't allow user to cancel importing
setIsHovered(false)
}
const onMouseOut = () => {

View File

@ -1,6 +1,10 @@
import { useCallback, useMemo } from 'react'
import { ImportingModel } from '@janhq/core/.'
import { useSetAtom } from 'jotai'
import { AlertCircle } from 'lucide-react'
import { setImportModelStageAtom } from '@/hooks/useImportModel'
import { toGibibytes } from '@/utils/converter'
@ -16,28 +20,39 @@ type Props = {
const ImportingModelItem: React.FC<Props> = ({ model }) => {
const setImportModelStage = useSetAtom(setImportModelStageAtom)
const setEditingModelId = useSetAtom(editingModelIdAtom)
const sizeInGb = toGibibytes(model.size)
const onEditModelInfoClick = () => {
const onEditModelInfoClick = useCallback(() => {
setEditingModelId(model.importId)
setImportModelStage('EDIT_MODEL_INFO')
}
}, [setImportModelStage, setEditingModelId, model.importId])
const onDeleteModelClick = () => {}
const onDeleteModelClick = useCallback(() => {}, [])
const displayStatus = useMemo(() => {
if (model.status === 'FAILED') {
return 'Failed'
} else {
return toGibibytes(model.size)
}
}, [model.status, model.size])
return (
<div className="flex w-full flex-row items-center space-x-3 rounded-lg border px-4 py-3">
<p className="line-clamp-1 flex-1">{model.name}</p>
<p>{sizeInGb}</p>
<p className="line-clamp-1 flex-1 font-semibold text-[#09090B]">
{model.name}
</p>
<p className="text-[#71717A]">{displayStatus}</p>
{model.status === 'IMPORTED' || model.status === 'FAILED' ? (
{model.status === 'IMPORTED' && (
<ImportSuccessIcon onEditModelClick={onEditModelInfoClick} />
) : (
)}
{(model.status === 'IMPORTING' || model.status === 'PREPARING') && (
<ImportInProgressIcon
percentage={model.percentage ?? 0}
onDeleteModelClick={onDeleteModelClick}
/>
)}
{model.status === 'FAILED' && <AlertCircle size={24} color="#F00" />}
</div>
)
}

View File

@ -1,6 +1,6 @@
import { useCallback, useMemo } from 'react'
import { useCallback, useEffect, useState } from 'react'
import { openFileExplorer } from '@janhq/core'
import { joinPath, openFileExplorer } from '@janhq/core'
import {
Button,
Modal,
@ -31,7 +31,15 @@ const ImportingModelModal: React.FC = () => {
const setImportModelStage = useSetAtom(setImportModelStageAtom)
const janDataFolder = useAtomValue(janDataFolderPathAtom)
const modelFolder = useMemo(() => `${janDataFolder}/models`, [janDataFolder])
const [modelFolder, setModelFolder] = useState('')
useEffect(() => {
const getModelPath = async () => {
const modelPath = await joinPath([janDataFolder, 'models'])
setModelFolder(modelPath)
}
getModelPath()
}, [janDataFolder])
const finishedImportModel = importingModels.filter(
(model) => model.status === 'IMPORTED'

View File

@ -2,7 +2,6 @@ import { useCallback, useState } from 'react'
import { useDropzone } from 'react-dropzone'
import { ImportingModel } from '@janhq/core'
import { Button, Input, ScrollArea } from '@janhq/uikit'
import { useAtomValue, useSetAtom } from 'jotai'
@ -10,60 +9,29 @@ import { Plus, SearchIcon, UploadCloudIcon } from 'lucide-react'
import { twMerge } from 'tailwind-merge'
import { v4 as uuidv4 } from 'uuid'
import useDropModelBinaries from '@/hooks/useDropModelBinaries'
import { setImportModelStageAtom } from '@/hooks/useImportModel'
import { getFileInfoFromFile } from '@/utils/file'
import RowModel from './Row'
import {
downloadedModelsAtom,
importingModelsAtom,
} from '@/helpers/atoms/Model.atom'
import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
const Column = ['Name', 'Model ID', 'Size', 'Version', 'Status', '']
const Models: React.FC = () => {
const downloadedModels = useAtomValue(downloadedModelsAtom)
const setImportModelStage = useSetAtom(setImportModelStageAtom)
const setImportingModels = useSetAtom(importingModelsAtom)
const [searchValue, setsearchValue] = useState('')
const { onDropModels } = useDropModelBinaries()
const filteredDownloadedModels = downloadedModels
.filter((x) => x.name?.toLowerCase().includes(searchValue.toLowerCase()))
.sort((a, b) => a.name.localeCompare(b.name))
const onDrop = useCallback(
(acceptedFiles: File[]) => {
const filePathWithSize = getFileInfoFromFile(acceptedFiles)
const importingModels: ImportingModel[] = filePathWithSize.map(
(file) => ({
importId: uuidv4(),
modelId: undefined,
name: file.name,
description: '',
path: file.path,
tags: [],
size: file.size,
status: 'PREPARING',
format: 'gguf',
})
)
if (importingModels.length === 0) return
setImportingModels(importingModels)
setImportModelStage('MODEL_SELECTED')
},
[setImportModelStage, setImportingModels]
)
const { getRootProps, isDragActive } = useDropzone({
noClick: true,
multiple: true,
onDrop,
onDrop: onDropModels,
})
const onImportModelClick = useCallback(() => {

View File

@ -1,7 +1,7 @@
import { useCallback } from 'react'
import { useDropzone } from 'react-dropzone'
import { ImportingModel, fs } from '@janhq/core'
import { ImportingModel, baseName, fs } from '@janhq/core'
import { Modal, ModalContent, ModalHeader, ModalTitle } from '@janhq/uikit'
import { useAtomValue, useSetAtom } from 'jotai'
@ -9,16 +9,15 @@ import { UploadCloudIcon } from 'lucide-react'
import { v4 as uuidv4 } from 'uuid'
import { snackbar } from '@/containers/Toast'
import useDropModelBinaries from '@/hooks/useDropModelBinaries'
import {
getImportModelStageAtom,
setImportModelStageAtom,
} from '@/hooks/useImportModel'
import {
FilePathWithSize,
getFileInfoFromFile,
getFileNameFromPath,
} from '@/utils/file'
import { FilePathWithSize } from '@/utils/file'
import { importingModelsAtom } from '@/helpers/atoms/Model.atom'
@ -26,6 +25,7 @@ const SelectingModelModal: React.FC = () => {
const setImportModelStage = useSetAtom(setImportModelStageAtom)
const importModelStage = useAtomValue(getImportModelStageAtom)
const setImportingModels = useSetAtom(importingModelsAtom)
const { onDropModels } = useDropModelBinaries()
const onSelectFileClick = useCallback(async () => {
const filePaths = await window.core?.api?.selectModelFiles()
@ -36,7 +36,7 @@ const SelectingModelModal: React.FC = () => {
const fileStats = await fs.fileStat(filePath, true)
if (!fileStats || fileStats.isDirectory) continue
const fileName = getFileNameFromPath(filePath)
const fileName = await baseName(filePath)
sanitizedFilePaths.push({
path: filePath,
name: fileName,
@ -44,12 +44,19 @@ const SelectingModelModal: React.FC = () => {
})
}
const importingModels: ImportingModel[] = sanitizedFilePaths.map(
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,
name: name.replace('.gguf', ''),
description: '',
path: path,
tags: [],
@ -59,41 +66,22 @@ const SelectingModelModal: React.FC = () => {
}
}
)
if (unsupportedFiles.length > 0) {
snackbar({
description: `File has to be a .gguf file`,
type: 'error',
})
}
if (importingModels.length === 0) return
setImportingModels(importingModels)
setImportModelStage('MODEL_SELECTED')
}, [setImportingModels, setImportModelStage])
const onDrop = useCallback(
(acceptedFiles: File[]) => {
const filePathWithSize = getFileInfoFromFile(acceptedFiles)
const importingModels: ImportingModel[] = filePathWithSize.map(
(file) => ({
importId: uuidv4(),
modelId: undefined,
name: file.name,
description: '',
path: file.path,
tags: [],
size: file.size,
status: 'PREPARING',
format: 'gguf',
})
)
if (importingModels.length === 0) return
setImportingModels(importingModels)
setImportModelStage('MODEL_SELECTED')
},
[setImportModelStage, setImportingModels]
)
const { isDragActive, getRootProps } = useDropzone({
noClick: true,
multiple: true,
onDrop,
onDrop: onDropModels,
})
const borderColor = isDragActive ? 'border-primary' : 'border-[#F4F4F5]'

View File

@ -1,3 +1,5 @@
import { baseName } from '@janhq/core'
export type FilePathWithSize = {
path: string
name: string
@ -8,24 +10,17 @@ export interface FileWithPath extends File {
path?: string
}
export const getFileNameFromPath = (filePath: string): string => {
let fileName = filePath.split('/').pop() ?? ''
if (fileName.split('.').length > 1) {
fileName = fileName.split('.').slice(0, -1).join('.')
}
return fileName
}
export const getFileInfoFromFile = (
export const getFileInfoFromFile = async (
files: FileWithPath[]
): FilePathWithSize[] => {
): Promise<FilePathWithSize[]> => {
const result: FilePathWithSize[] = []
for (const file of files) {
if (file.path && file.path.length > 0) {
const fileName = await baseName(file.path)
result.push({
path: file.path,
name: getFileNameFromPath(file.path),
name: fileName,
size: file.size,
})
}