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:
parent
95946ab9f2
commit
d7070d8c4a
@ -49,7 +49,7 @@ export enum DownloadEvent {
|
|||||||
|
|
||||||
export enum LocalImportModelEvent {
|
export enum LocalImportModelEvent {
|
||||||
onLocalImportModelUpdate = 'onLocalImportModelUpdate',
|
onLocalImportModelUpdate = 'onLocalImportModelUpdate',
|
||||||
onLocalImportModelError = 'onLocalImportModelError',
|
onLocalImportModelFailed = 'onLocalImportModelFailed',
|
||||||
onLocalImportModelSuccess = 'onLocalImportModelSuccess',
|
onLocalImportModelSuccess = 'onLocalImportModelSuccess',
|
||||||
onLocalImportModelFinished = 'onLocalImportModelFinished',
|
onLocalImportModelFinished = 'onLocalImportModelFinished',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -65,7 +65,7 @@ const joinPath: (paths: string[]) => Promise<string> = (paths) => global.core.ap
|
|||||||
* @param path - The path to retrieve.
|
* @param path - The path to retrieve.
|
||||||
* @returns {Promise<string>} A promise that resolves with the basename.
|
* @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.
|
* Opens an external URL in the default web browser.
|
||||||
|
|||||||
@ -19,4 +19,5 @@ export type ImportingModel = {
|
|||||||
status: ImportingModelStatus
|
status: ImportingModelStatus
|
||||||
format: string
|
format: string
|
||||||
percentage?: number
|
percentage?: number
|
||||||
|
error?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import {
|
|||||||
OptionType,
|
OptionType,
|
||||||
ImportingModel,
|
ImportingModel,
|
||||||
LocalImportModelEvent,
|
LocalImportModelEvent,
|
||||||
|
baseName,
|
||||||
} from '@janhq/core'
|
} from '@janhq/core'
|
||||||
|
|
||||||
import { extractFileName } from './helpers/path'
|
import { extractFileName } from './helpers/path'
|
||||||
@ -488,7 +489,7 @@ export default class JanModelExtension extends ModelExtension {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const binaryFileName = extractFileName(modelBinaryPath, '')
|
const binaryFileName = await baseName(modelBinaryPath)
|
||||||
|
|
||||||
const model: Model = {
|
const model: Model = {
|
||||||
...defaultModel,
|
...defaultModel,
|
||||||
@ -555,7 +556,7 @@ export default class JanModelExtension extends ModelExtension {
|
|||||||
model: ImportingModel,
|
model: ImportingModel,
|
||||||
optionType: OptionType
|
optionType: OptionType
|
||||||
): Promise<Model> {
|
): Promise<Model> {
|
||||||
const binaryName = extractFileName(model.path, '').replace(/\s/g, '')
|
const binaryName = (await baseName(model.path)).replace(/\s/g, '')
|
||||||
|
|
||||||
let modelFolderName = binaryName
|
let modelFolderName = binaryName
|
||||||
if (binaryName.endsWith(JanModelExtension._supportedModelFormat)) {
|
if (binaryName.endsWith(JanModelExtension._supportedModelFormat)) {
|
||||||
@ -568,7 +569,7 @@ export default class JanModelExtension extends ModelExtension {
|
|||||||
const modelFolderPath = await this.getModelFolderName(modelFolderName)
|
const modelFolderPath = await this.getModelFolderName(modelFolderName)
|
||||||
await fs.mkdirSync(modelFolderPath)
|
await fs.mkdirSync(modelFolderPath)
|
||||||
|
|
||||||
const uniqueFolderName = modelFolderPath.split('/').pop()
|
const uniqueFolderName = await baseName(modelFolderPath)
|
||||||
const modelBinaryFile = binaryName.endsWith(
|
const modelBinaryFile = binaryName.endsWith(
|
||||||
JanModelExtension._supportedModelFormat
|
JanModelExtension._supportedModelFormat
|
||||||
)
|
)
|
||||||
@ -637,14 +638,21 @@ export default class JanModelExtension extends ModelExtension {
|
|||||||
|
|
||||||
for (const model of models) {
|
for (const model of models) {
|
||||||
events.emit(LocalImportModelEvent.onLocalImportModelUpdate, model)
|
events.emit(LocalImportModelEvent.onLocalImportModelUpdate, model)
|
||||||
|
try {
|
||||||
const importedModel = await this.importModel(model, optionType)
|
const importedModel = await this.importModel(model, optionType)
|
||||||
|
|
||||||
events.emit(LocalImportModelEvent.onLocalImportModelSuccess, {
|
events.emit(LocalImportModelEvent.onLocalImportModelSuccess, {
|
||||||
...model,
|
...model,
|
||||||
modelId: importedModel.id,
|
modelId: importedModel.id,
|
||||||
})
|
})
|
||||||
importedModels.push(importedModel)
|
importedModels.push(importedModel)
|
||||||
|
} catch (err) {
|
||||||
|
events.emit(LocalImportModelEvent.onLocalImportModelFailed, {
|
||||||
|
...model,
|
||||||
|
error: err,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
events.emit(
|
events.emit(
|
||||||
LocalImportModelEvent.onLocalImportModelFinished,
|
LocalImportModelEvent.onLocalImportModelFinished,
|
||||||
importedModels
|
importedModels
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import { useSetAtom } from 'jotai'
|
|||||||
import { snackbar } from '../Toast'
|
import { snackbar } from '../Toast'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
setImportingModelErrorAtom,
|
||||||
setImportingModelSuccessAtom,
|
setImportingModelSuccessAtom,
|
||||||
updateImportingModelProgressAtom,
|
updateImportingModelProgressAtom,
|
||||||
} from '@/helpers/atoms/Model.atom'
|
} from '@/helpers/atoms/Model.atom'
|
||||||
@ -21,6 +22,7 @@ const ModelImportListener = ({ children }: PropsWithChildren) => {
|
|||||||
updateImportingModelProgressAtom
|
updateImportingModelProgressAtom
|
||||||
)
|
)
|
||||||
const setImportingModelSuccess = useSetAtom(setImportingModelSuccessAtom)
|
const setImportingModelSuccess = useSetAtom(setImportingModelSuccessAtom)
|
||||||
|
const setImportingModelFailed = useSetAtom(setImportingModelErrorAtom)
|
||||||
|
|
||||||
const onImportModelUpdate = useCallback(
|
const onImportModelUpdate = useCallback(
|
||||||
async (state: ImportingModel) => {
|
async (state: ImportingModel) => {
|
||||||
@ -30,6 +32,14 @@ const ModelImportListener = ({ children }: PropsWithChildren) => {
|
|||||||
[updateImportingModelProgress]
|
[updateImportingModelProgress]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const onImportModelFailed = useCallback(
|
||||||
|
async (state: ImportingModel) => {
|
||||||
|
if (!state.importId) return
|
||||||
|
setImportingModelFailed(state.importId, state.error ?? '')
|
||||||
|
},
|
||||||
|
[setImportingModelFailed]
|
||||||
|
)
|
||||||
|
|
||||||
const onImportModelSuccess = useCallback(
|
const onImportModelSuccess = useCallback(
|
||||||
(state: ImportingModel) => {
|
(state: ImportingModel) => {
|
||||||
if (!state.modelId) return
|
if (!state.modelId) return
|
||||||
@ -62,6 +72,10 @@ const ModelImportListener = ({ children }: PropsWithChildren) => {
|
|||||||
LocalImportModelEvent.onLocalImportModelFinished,
|
LocalImportModelEvent.onLocalImportModelFinished,
|
||||||
onImportModelFinished
|
onImportModelFinished
|
||||||
)
|
)
|
||||||
|
events.on(
|
||||||
|
LocalImportModelEvent.onLocalImportModelFailed,
|
||||||
|
onImportModelFailed
|
||||||
|
)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
console.debug('ModelImportListener: unregistering event listeners...')
|
console.debug('ModelImportListener: unregistering event listeners...')
|
||||||
@ -77,8 +91,17 @@ const ModelImportListener = ({ children }: PropsWithChildren) => {
|
|||||||
LocalImportModelEvent.onLocalImportModelFinished,
|
LocalImportModelEvent.onLocalImportModelFinished,
|
||||||
onImportModelFinished
|
onImportModelFinished
|
||||||
)
|
)
|
||||||
|
events.off(
|
||||||
|
LocalImportModelEvent.onLocalImportModelFailed,
|
||||||
|
onImportModelFailed
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}, [onImportModelUpdate, onImportModelSuccess, onImportModelFinished])
|
}, [
|
||||||
|
onImportModelUpdate,
|
||||||
|
onImportModelSuccess,
|
||||||
|
onImportModelFinished,
|
||||||
|
onImportModelFailed,
|
||||||
|
])
|
||||||
|
|
||||||
return <Fragment>{children}</Fragment>
|
return <Fragment>{children}</Fragment>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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(
|
export const setImportingModelSuccessAtom = atom(
|
||||||
null,
|
null,
|
||||||
(get, set, importId: string, modelId: string) => {
|
(get, set, importId: string, modelId: string) => {
|
||||||
|
|||||||
55
web/hooks/useDropModelBinaries.ts
Normal file
55
web/hooks/useDropModelBinaries.ts
Normal 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 }
|
||||||
|
}
|
||||||
@ -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 {
|
import {
|
||||||
Modal,
|
Modal,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
@ -47,6 +53,7 @@ const EditModelInfoModal: React.FC = () => {
|
|||||||
const janDataFolder = useAtomValue(janDataFolderPathAtom)
|
const janDataFolder = useAtomValue(janDataFolderPathAtom)
|
||||||
const updateImportingModel = useSetAtom(updateImportingModelAtom)
|
const updateImportingModel = useSetAtom(updateImportingModelAtom)
|
||||||
const { updateModelInfo } = useImportModel()
|
const { updateModelInfo } = useImportModel()
|
||||||
|
const [modelPath, setModelPath] = useState<string>('')
|
||||||
|
|
||||||
const editingModel = importingModels.find(
|
const editingModel = importingModels.find(
|
||||||
(model) => model.importId === editingModelId
|
(model) => model.importId === editingModelId
|
||||||
@ -88,13 +95,19 @@ const EditModelInfoModal: React.FC = () => {
|
|||||||
setEditingModelId(undefined)
|
setEditingModelId(undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
const modelFolderPath = useMemo(() => {
|
useEffect(() => {
|
||||||
return `${janDataFolder}/models/${editingModel?.modelId}`
|
const getModelPath = async () => {
|
||||||
|
const modelId = editingModel?.modelId
|
||||||
|
if (!modelId) return ''
|
||||||
|
const path = await joinPath([janDataFolder, 'models', modelId])
|
||||||
|
setModelPath(path)
|
||||||
|
}
|
||||||
|
getModelPath()
|
||||||
}, [janDataFolder, editingModel])
|
}, [janDataFolder, editingModel])
|
||||||
|
|
||||||
const onShowInFinderClick = useCallback(() => {
|
const onShowInFinderClick = useCallback(() => {
|
||||||
openFileExplorer(modelFolderPath)
|
openFileExplorer(modelPath)
|
||||||
}, [modelFolderPath])
|
}, [modelPath])
|
||||||
|
|
||||||
if (!editingModel) {
|
if (!editingModel) {
|
||||||
setImportModelStage('IMPORTING_MODEL')
|
setImportModelStage('IMPORTING_MODEL')
|
||||||
@ -104,7 +117,10 @@ const EditModelInfoModal: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal open={importModelStage === 'EDIT_MODEL_INFO'}>
|
<Modal
|
||||||
|
open={importModelStage === 'EDIT_MODEL_INFO'}
|
||||||
|
onOpenChange={onCancelClick}
|
||||||
|
>
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
<ModalTitle>Edit Model Information</ModalTitle>
|
<ModalTitle>Edit Model Information</ModalTitle>
|
||||||
@ -130,7 +146,7 @@ const EditModelInfoModal: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="mt-1 flex flex-row items-center space-x-2">
|
<div className="mt-1 flex flex-row items-center space-x-2">
|
||||||
<span className="line-clamp-1 text-xs font-normal text-[#71717A]">
|
<span className="line-clamp-1 text-xs font-normal text-[#71717A]">
|
||||||
{modelFolderPath}
|
{modelPath}
|
||||||
</span>
|
</span>
|
||||||
<Button themes="ghost" onClick={onShowInFinderClick}>
|
<Button themes="ghost" onClick={onShowInFinderClick}>
|
||||||
{openFileTitle()}
|
{openFileTitle()}
|
||||||
|
|||||||
@ -15,7 +15,8 @@ const ImportInProgressIcon: React.FC<Props> = ({
|
|||||||
const [isHovered, setIsHovered] = useState(false)
|
const [isHovered, setIsHovered] = useState(false)
|
||||||
|
|
||||||
const onMouseOver = () => {
|
const onMouseOver = () => {
|
||||||
setIsHovered(true)
|
// for now we don't allow user to cancel importing
|
||||||
|
setIsHovered(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onMouseOut = () => {
|
const onMouseOut = () => {
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
|
import { useCallback, useMemo } from 'react'
|
||||||
|
|
||||||
import { ImportingModel } from '@janhq/core/.'
|
import { ImportingModel } from '@janhq/core/.'
|
||||||
import { useSetAtom } from 'jotai'
|
import { useSetAtom } from 'jotai'
|
||||||
|
|
||||||
|
import { AlertCircle } from 'lucide-react'
|
||||||
|
|
||||||
import { setImportModelStageAtom } from '@/hooks/useImportModel'
|
import { setImportModelStageAtom } from '@/hooks/useImportModel'
|
||||||
|
|
||||||
import { toGibibytes } from '@/utils/converter'
|
import { toGibibytes } from '@/utils/converter'
|
||||||
@ -16,28 +20,39 @@ type Props = {
|
|||||||
const ImportingModelItem: React.FC<Props> = ({ model }) => {
|
const ImportingModelItem: React.FC<Props> = ({ model }) => {
|
||||||
const setImportModelStage = useSetAtom(setImportModelStageAtom)
|
const setImportModelStage = useSetAtom(setImportModelStageAtom)
|
||||||
const setEditingModelId = useSetAtom(editingModelIdAtom)
|
const setEditingModelId = useSetAtom(editingModelIdAtom)
|
||||||
const sizeInGb = toGibibytes(model.size)
|
|
||||||
|
|
||||||
const onEditModelInfoClick = () => {
|
const onEditModelInfoClick = useCallback(() => {
|
||||||
setEditingModelId(model.importId)
|
setEditingModelId(model.importId)
|
||||||
setImportModelStage('EDIT_MODEL_INFO')
|
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 (
|
return (
|
||||||
<div className="flex w-full flex-row items-center space-x-3 rounded-lg border px-4 py-3">
|
<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 className="line-clamp-1 flex-1 font-semibold text-[#09090B]">
|
||||||
<p>{sizeInGb}</p>
|
{model.name}
|
||||||
|
</p>
|
||||||
|
<p className="text-[#71717A]">{displayStatus}</p>
|
||||||
|
|
||||||
{model.status === 'IMPORTED' || model.status === 'FAILED' ? (
|
{model.status === 'IMPORTED' && (
|
||||||
<ImportSuccessIcon onEditModelClick={onEditModelInfoClick} />
|
<ImportSuccessIcon onEditModelClick={onEditModelInfoClick} />
|
||||||
) : (
|
)}
|
||||||
|
{(model.status === 'IMPORTING' || model.status === 'PREPARING') && (
|
||||||
<ImportInProgressIcon
|
<ImportInProgressIcon
|
||||||
percentage={model.percentage ?? 0}
|
percentage={model.percentage ?? 0}
|
||||||
onDeleteModelClick={onDeleteModelClick}
|
onDeleteModelClick={onDeleteModelClick}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{model.status === 'FAILED' && <AlertCircle size={24} color="#F00" />}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Modal,
|
Modal,
|
||||||
@ -31,7 +31,15 @@ const ImportingModelModal: React.FC = () => {
|
|||||||
const setImportModelStage = useSetAtom(setImportModelStageAtom)
|
const setImportModelStage = useSetAtom(setImportModelStageAtom)
|
||||||
const janDataFolder = useAtomValue(janDataFolderPathAtom)
|
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(
|
const finishedImportModel = importingModels.filter(
|
||||||
(model) => model.status === 'IMPORTED'
|
(model) => model.status === 'IMPORTED'
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import { useCallback, useState } from 'react'
|
|||||||
|
|
||||||
import { useDropzone } from 'react-dropzone'
|
import { useDropzone } from 'react-dropzone'
|
||||||
|
|
||||||
import { ImportingModel } from '@janhq/core'
|
|
||||||
import { Button, Input, ScrollArea } from '@janhq/uikit'
|
import { Button, Input, ScrollArea } from '@janhq/uikit'
|
||||||
|
|
||||||
import { useAtomValue, useSetAtom } from 'jotai'
|
import { useAtomValue, useSetAtom } from 'jotai'
|
||||||
@ -10,60 +9,29 @@ import { Plus, SearchIcon, UploadCloudIcon } from 'lucide-react'
|
|||||||
|
|
||||||
import { twMerge } from 'tailwind-merge'
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import useDropModelBinaries from '@/hooks/useDropModelBinaries'
|
||||||
|
|
||||||
import { setImportModelStageAtom } from '@/hooks/useImportModel'
|
import { setImportModelStageAtom } from '@/hooks/useImportModel'
|
||||||
|
|
||||||
import { getFileInfoFromFile } from '@/utils/file'
|
|
||||||
|
|
||||||
import RowModel from './Row'
|
import RowModel from './Row'
|
||||||
|
|
||||||
import {
|
import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
|
||||||
downloadedModelsAtom,
|
|
||||||
importingModelsAtom,
|
|
||||||
} from '@/helpers/atoms/Model.atom'
|
|
||||||
|
|
||||||
const Column = ['Name', 'Model ID', 'Size', 'Version', 'Status', '']
|
const Column = ['Name', 'Model ID', 'Size', 'Version', 'Status', '']
|
||||||
|
|
||||||
const Models: React.FC = () => {
|
const Models: React.FC = () => {
|
||||||
const downloadedModels = useAtomValue(downloadedModelsAtom)
|
const downloadedModels = useAtomValue(downloadedModelsAtom)
|
||||||
const setImportModelStage = useSetAtom(setImportModelStageAtom)
|
const setImportModelStage = useSetAtom(setImportModelStageAtom)
|
||||||
const setImportingModels = useSetAtom(importingModelsAtom)
|
|
||||||
const [searchValue, setsearchValue] = useState('')
|
const [searchValue, setsearchValue] = useState('')
|
||||||
|
const { onDropModels } = useDropModelBinaries()
|
||||||
|
|
||||||
const filteredDownloadedModels = downloadedModels
|
const filteredDownloadedModels = downloadedModels
|
||||||
.filter((x) => x.name?.toLowerCase().includes(searchValue.toLowerCase()))
|
.filter((x) => x.name?.toLowerCase().includes(searchValue.toLowerCase()))
|
||||||
.sort((a, b) => a.name.localeCompare(b.name))
|
.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({
|
const { getRootProps, isDragActive } = useDropzone({
|
||||||
noClick: true,
|
noClick: true,
|
||||||
multiple: true,
|
multiple: true,
|
||||||
onDrop,
|
onDrop: onDropModels,
|
||||||
})
|
})
|
||||||
|
|
||||||
const onImportModelClick = useCallback(() => {
|
const onImportModelClick = useCallback(() => {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { useDropzone } from 'react-dropzone'
|
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 { Modal, ModalContent, ModalHeader, ModalTitle } from '@janhq/uikit'
|
||||||
import { useAtomValue, useSetAtom } from 'jotai'
|
import { useAtomValue, useSetAtom } from 'jotai'
|
||||||
|
|
||||||
@ -9,16 +9,15 @@ import { UploadCloudIcon } from 'lucide-react'
|
|||||||
|
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
|
import { snackbar } from '@/containers/Toast'
|
||||||
|
|
||||||
|
import useDropModelBinaries from '@/hooks/useDropModelBinaries'
|
||||||
import {
|
import {
|
||||||
getImportModelStageAtom,
|
getImportModelStageAtom,
|
||||||
setImportModelStageAtom,
|
setImportModelStageAtom,
|
||||||
} from '@/hooks/useImportModel'
|
} from '@/hooks/useImportModel'
|
||||||
|
|
||||||
import {
|
import { FilePathWithSize } from '@/utils/file'
|
||||||
FilePathWithSize,
|
|
||||||
getFileInfoFromFile,
|
|
||||||
getFileNameFromPath,
|
|
||||||
} from '@/utils/file'
|
|
||||||
|
|
||||||
import { importingModelsAtom } from '@/helpers/atoms/Model.atom'
|
import { importingModelsAtom } from '@/helpers/atoms/Model.atom'
|
||||||
|
|
||||||
@ -26,6 +25,7 @@ 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 setImportingModels = useSetAtom(importingModelsAtom)
|
||||||
|
const { onDropModels } = useDropModelBinaries()
|
||||||
|
|
||||||
const onSelectFileClick = useCallback(async () => {
|
const onSelectFileClick = useCallback(async () => {
|
||||||
const filePaths = await window.core?.api?.selectModelFiles()
|
const filePaths = await window.core?.api?.selectModelFiles()
|
||||||
@ -36,7 +36,7 @@ const SelectingModelModal: React.FC = () => {
|
|||||||
const fileStats = await fs.fileStat(filePath, true)
|
const fileStats = await fs.fileStat(filePath, true)
|
||||||
if (!fileStats || fileStats.isDirectory) continue
|
if (!fileStats || fileStats.isDirectory) continue
|
||||||
|
|
||||||
const fileName = getFileNameFromPath(filePath)
|
const fileName = await baseName(filePath)
|
||||||
sanitizedFilePaths.push({
|
sanitizedFilePaths.push({
|
||||||
path: filePath,
|
path: filePath,
|
||||||
name: fileName,
|
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) => {
|
({ path, name, size }: FilePathWithSize) => {
|
||||||
return {
|
return {
|
||||||
importId: uuidv4(),
|
importId: uuidv4(),
|
||||||
modelId: undefined,
|
modelId: undefined,
|
||||||
name: name,
|
name: name.replace('.gguf', ''),
|
||||||
description: '',
|
description: '',
|
||||||
path: path,
|
path: path,
|
||||||
tags: [],
|
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
|
if (importingModels.length === 0) return
|
||||||
|
|
||||||
setImportingModels(importingModels)
|
setImportingModels(importingModels)
|
||||||
setImportModelStage('MODEL_SELECTED')
|
setImportModelStage('MODEL_SELECTED')
|
||||||
}, [setImportingModels, setImportModelStage])
|
}, [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({
|
const { isDragActive, getRootProps } = useDropzone({
|
||||||
noClick: true,
|
noClick: true,
|
||||||
multiple: true,
|
multiple: true,
|
||||||
onDrop,
|
onDrop: onDropModels,
|
||||||
})
|
})
|
||||||
|
|
||||||
const borderColor = isDragActive ? 'border-primary' : 'border-[#F4F4F5]'
|
const borderColor = isDragActive ? 'border-primary' : 'border-[#F4F4F5]'
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { baseName } from '@janhq/core'
|
||||||
|
|
||||||
export type FilePathWithSize = {
|
export type FilePathWithSize = {
|
||||||
path: string
|
path: string
|
||||||
name: string
|
name: string
|
||||||
@ -8,24 +10,17 @@ export interface FileWithPath extends File {
|
|||||||
path?: string
|
path?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getFileNameFromPath = (filePath: string): string => {
|
export const getFileInfoFromFile = async (
|
||||||
let fileName = filePath.split('/').pop() ?? ''
|
|
||||||
if (fileName.split('.').length > 1) {
|
|
||||||
fileName = fileName.split('.').slice(0, -1).join('.')
|
|
||||||
}
|
|
||||||
|
|
||||||
return fileName
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getFileInfoFromFile = (
|
|
||||||
files: FileWithPath[]
|
files: FileWithPath[]
|
||||||
): FilePathWithSize[] => {
|
): Promise<FilePathWithSize[]> => {
|
||||||
const result: FilePathWithSize[] = []
|
const result: FilePathWithSize[] = []
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
if (file.path && file.path.length > 0) {
|
if (file.path && file.path.length > 0) {
|
||||||
|
const fileName = await baseName(file.path)
|
||||||
|
|
||||||
result.push({
|
result.push({
|
||||||
path: file.path,
|
path: file.path,
|
||||||
name: getFileNameFromPath(file.path),
|
name: fileName,
|
||||||
size: file.size,
|
size: file.size,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user