feat(UI/files): integrate Mistral file handling and update OCR provider to use optional dependency
This commit is contained in:
@@ -22,6 +22,7 @@ export default defineConfig({
|
||||
'@cherrystudio/embedjs-loader-sitemap',
|
||||
'@cherrystudio/embedjs-libsql',
|
||||
'@cherrystudio/embedjs-loader-image',
|
||||
'@cherrystudio/mac-system-ocr',
|
||||
'p-queue',
|
||||
'webdav'
|
||||
]
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import MacOCR from '@cherrystudio/mac-system-ocr'
|
||||
import { FileSource, isLocalFile, LocalFileSource, OcrProvider } from '@types'
|
||||
import Logger from 'electron-log'
|
||||
import * as fs from 'fs'
|
||||
@@ -12,9 +11,23 @@ export default class MacSysOcrProvider extends BaseOcrProvider {
|
||||
private readonly BATCH_SIZE = 4
|
||||
private readonly CONCURRENCY = 2
|
||||
private readonly MIN_TEXT_LENGTH = 1000
|
||||
private MacOCR: any
|
||||
|
||||
private async initMacOCR() {
|
||||
if (!this.MacOCR) {
|
||||
try {
|
||||
const module = await import('@cherrystudio/mac-system-ocr')
|
||||
this.MacOCR = module.default
|
||||
} catch (error) {
|
||||
Logger.error('[OCR] Failed to load mac-system-ocr:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
return this.MacOCR
|
||||
}
|
||||
|
||||
private getRecognitionLevel(level?: number) {
|
||||
return level === 0 ? MacOCR.RECOGNITION_LEVEL_FAST : MacOCR.RECOGNITION_LEVEL_ACCURATE
|
||||
return level === 0 ? this.MacOCR.RECOGNITION_LEVEL_FAST : this.MacOCR.RECOGNITION_LEVEL_ACCURATE
|
||||
}
|
||||
|
||||
constructor(provider: OcrProvider) {
|
||||
@@ -29,6 +42,7 @@ export default class MacSysOcrProvider extends BaseOcrProvider {
|
||||
): Promise<void> {
|
||||
const queue = new PQueue({ concurrency: this.CONCURRENCY })
|
||||
const batches: Promise<void>[] = []
|
||||
await this.initMacOCR()
|
||||
|
||||
// Create ordered batches
|
||||
for (let startPage = 0; startPage < totalPages; startPage += this.BATCH_SIZE) {
|
||||
@@ -44,7 +58,7 @@ export default class MacSysOcrProvider extends BaseOcrProvider {
|
||||
}
|
||||
|
||||
// Process batch
|
||||
const ocrResults = await MacOCR.recognizeBatchFromBuffer(pageBuffers, {
|
||||
const ocrResults = await this.MacOCR.recognizeBatchFromBuffer(pageBuffers, {
|
||||
maxThreads: 4,
|
||||
ocrOptions: {
|
||||
recognitionLevel: this.getRecognitionLevel(this.provider.options?.recognitionLevel),
|
||||
|
||||
@@ -39,7 +39,7 @@ export class GeminiService extends BaseFileService {
|
||||
}
|
||||
|
||||
const response: FileUploadResponse = {
|
||||
fileId: uploadResult.file.name,
|
||||
fileId: uploadResult.file.name || '',
|
||||
displayName: file.origin_name,
|
||||
status,
|
||||
originalFile: uploadResult
|
||||
@@ -89,7 +89,7 @@ export class GeminiService extends BaseFileService {
|
||||
}
|
||||
const response = await this.fileManager.listFiles()
|
||||
const fileList: FileListResponse = {
|
||||
files: response.files
|
||||
files: (response.files || [])
|
||||
.filter((file) => file.state === FileState.ACTIVE)
|
||||
.map((file) => {
|
||||
// 更新单个文件的缓存
|
||||
|
||||
@@ -22,7 +22,7 @@ export class MistralService extends BaseFileService {
|
||||
const fileBuffer = await fs.readFile(file.path)
|
||||
const response = await this.client.files.upload({
|
||||
file: {
|
||||
fileName: file.path,
|
||||
fileName: file.origin_name,
|
||||
content: new Uint8Array(fileBuffer)
|
||||
},
|
||||
purpose: 'ocr'
|
||||
@@ -50,12 +50,13 @@ export class MistralService extends BaseFileService {
|
||||
files: response.data.map((file) => ({
|
||||
id: file.id,
|
||||
displayName: file.filename || '',
|
||||
size: 0, // Size information not available in SDK response
|
||||
status: 'success' // All listed files are processed
|
||||
size: file.sizeBytes,
|
||||
status: 'success', // All listed files are processed,
|
||||
originalFile: file
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error listing files:', error)
|
||||
Logger.error('Error listing files:', error)
|
||||
return { files: [] }
|
||||
}
|
||||
}
|
||||
@@ -65,8 +66,9 @@ export class MistralService extends BaseFileService {
|
||||
await this.client.files.delete({
|
||||
fileId
|
||||
})
|
||||
Logger.info(`File ${fileId} deleted`)
|
||||
} catch (error) {
|
||||
console.error('Error deleting file:', error)
|
||||
Logger.error('Error deleting file:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -83,7 +85,7 @@ export class MistralService extends BaseFileService {
|
||||
status: 'success' // Retrieved files are always processed
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error retrieving file:', error)
|
||||
Logger.error('Error retrieving file:', error)
|
||||
return {
|
||||
fileId: fileId,
|
||||
displayName: '',
|
||||
|
||||
@@ -6,7 +6,7 @@ import React, { memo } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import GeminiFiles from './GeminiFiles'
|
||||
|
||||
import MistralFiles from './MistralFiles'
|
||||
interface ContentViewProps {
|
||||
id: FileTypes | 'all' | string
|
||||
files?: FileType[]
|
||||
@@ -49,6 +49,10 @@ const ContentView: React.FC<ContentViewProps> = ({ id, files, dataSource, column
|
||||
return <GeminiFiles id={id.replace('gemini_', '') as string} />
|
||||
}
|
||||
|
||||
if (id.startsWith('mistral_')) {
|
||||
return <MistralFiles id={id.replace('mistral_', '') as string} />
|
||||
}
|
||||
|
||||
return (
|
||||
<Table
|
||||
dataSource={dataSource}
|
||||
|
||||
@@ -31,7 +31,7 @@ const FilesPage: FC = () => {
|
||||
const { providers } = useProviders()
|
||||
|
||||
const geminiProviders = providers.filter((provider) => provider.type === 'gemini')
|
||||
|
||||
const mistralProviders = providers.filter((provider) => provider.type === 'mistral')
|
||||
const tempFilesSort = (files: FileType[]) => {
|
||||
return files.sort((a, b) => {
|
||||
const aIsTemp = a.origin_name.startsWith('temp_file')
|
||||
@@ -187,6 +187,11 @@ const FilesPage: FC = () => {
|
||||
label: provider.name,
|
||||
icon: <FilePdfOutlined />
|
||||
})),
|
||||
...mistralProviders.map((provider) => ({
|
||||
key: 'mistral_' + provider.id,
|
||||
label: provider.name,
|
||||
icon: <FilePdfOutlined />
|
||||
})),
|
||||
{ key: 'all', label: t('files.all'), icon: <FileTextOutlined /> }
|
||||
].filter(Boolean) as MenuProps['items']
|
||||
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import { DeleteOutlined } from '@ant-design/icons'
|
||||
import { type FileSchema } from '@mistralai/mistralai/src/models/components'
|
||||
import { useProvider } from '@renderer/hooks/useProvider'
|
||||
import { runAsyncFunction } from '@renderer/utils'
|
||||
import { Table } from 'antd'
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import { FC, useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface MistralFilesProps {
|
||||
id: string
|
||||
}
|
||||
|
||||
const MistralFiles: FC<MistralFilesProps> = ({ id }) => {
|
||||
const { provider } = useProvider(id)
|
||||
const { t } = useTranslation()
|
||||
const [files, setFiles] = useState<FileSchema[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const fetchFiles = useCallback(async () => {
|
||||
const response = await window.api.fileService.list(provider.type, provider.apiKey)
|
||||
const files = response.files.map((file) => file.originalFile as FileSchema)
|
||||
setFiles(files)
|
||||
}, [provider])
|
||||
|
||||
const columns: ColumnsType<FileSchema> = [
|
||||
{
|
||||
title: t('files.name'),
|
||||
dataIndex: 'filename',
|
||||
key: 'filename'
|
||||
},
|
||||
{
|
||||
title: t('files.type'),
|
||||
dataIndex: 'object',
|
||||
key: 'object'
|
||||
},
|
||||
{
|
||||
title: t('files.size'),
|
||||
dataIndex: 'sizeBytes',
|
||||
key: 'sizeBytes',
|
||||
render: (size: string) => `${(parseInt(size) / 1024 / 1024).toFixed(2)} MB`
|
||||
},
|
||||
{
|
||||
title: t('files.created_at'),
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
render: (time: number) => new Date(time * 1000).toLocaleString()
|
||||
},
|
||||
{
|
||||
title: t('files.actions'),
|
||||
dataIndex: 'actions',
|
||||
key: 'actions',
|
||||
align: 'center',
|
||||
render: (_, record) => {
|
||||
return (
|
||||
<DeleteOutlined
|
||||
style={{ cursor: 'pointer', color: 'var(--color-error)' }}
|
||||
onClick={() => {
|
||||
setFiles(files.filter((file) => file.id !== record.id))
|
||||
window.api.fileService.delete(provider.type, provider.apiKey, record.id).catch((error) => {
|
||||
console.error('Failed to delete file:', error)
|
||||
setFiles((prev) => [...prev, record])
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
useEffect(() => {
|
||||
runAsyncFunction(async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
await fetchFiles()
|
||||
setLoading(false)
|
||||
} catch (error: any) {
|
||||
console.error('Failed to fetch files:', error)
|
||||
window.message.error(error.message)
|
||||
setLoading(false)
|
||||
}
|
||||
})
|
||||
}, [fetchFiles])
|
||||
|
||||
useEffect(() => {
|
||||
setFiles([])
|
||||
}, [id])
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Table columns={columns} dataSource={files} rowKey="name" loading={loading} />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div``
|
||||
|
||||
export default MistralFiles
|
||||
|
||||
@@ -849,6 +849,12 @@ const migrateConfig = {
|
||||
name: 'System(Mac Only)'
|
||||
})
|
||||
}
|
||||
|
||||
state.llm.providers.forEach((provider) => {
|
||||
if (provider.id === 'mistral') {
|
||||
provider.type = 'mistral'
|
||||
}
|
||||
})
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user