feat: knowledge ocr settings

This commit is contained in:
eeee0717
2025-03-21 11:12:28 +08:00
parent 60d5f7035c
commit 024aac9c4a
11 changed files with 278 additions and 9 deletions
@@ -0,0 +1 @@
<svg width='42' height='42' viewBox='0 0 42 42' fill='none' xmlns='http://www.w3.org/2000/svg'><rect x='28.6606' y='8.44495' width='6.92163' height='12.0376' rx='3.46082' transform='rotate(45 28.6606 8.44495)' fill='#7748F9'/><rect x='16.957' y='20.1488' width='6.92163' height='12.0376' rx='3.46082' transform='rotate(45 16.957 20.1488)' fill='#7748F9'/><rect x='20.1489' y='25.0432' width='6.92163' height='12.0376' rx='3.46082' transform='rotate(-45 20.1489 25.0432)' fill='#BFABFB'/><rect x='8.44482' y='13.3394' width='6.92163' height='12.0376' rx='3.46082' transform='rotate(-45 8.44482 13.3394)' fill='#BFABFB'/></svg>

After

Width:  |  Height:  |  Size: 625 B

+18
View File
@@ -0,0 +1,18 @@
import Doc2xLogo from '@renderer/assets/images/ocr/doc2x.svg'
export function getOcrProviderLogo(providerId: string) {
switch (providerId) {
case 'doc2x':
return Doc2xLogo
default:
return undefined
}
}
export const OCR_PROVIDER_CONFIG = {
doc2x: {
websites: {
official: 'https://doc2x.noedgeai.com',
apiKey: 'https://open.noedgeai.com/apiKeys'
}
}
}
+32 -2
View File
@@ -17,9 +17,11 @@ import {
updateBases,
updateItem as updateItemAction,
updateItemProcessingStatus,
updateNotes
updateNotes,
updateOcrProvider as _updateOcrProvider,
updateOcrProviders as _updateOcrProviders
} from '@renderer/store/knowledge'
import { FileType, KnowledgeBase, KnowledgeItem, ProcessingStatus } from '@renderer/types'
import { FileType, KnowledgeBase, KnowledgeItem, OcrProvider, ProcessingStatus } from '@renderer/types'
import { runAsyncFunction } from '@renderer/utils'
import { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
@@ -369,3 +371,31 @@ export const useKnowledgeBases = () => {
updateKnowledgeBases
}
}
export const useOcrProviders = () => {
const dispatch = useDispatch()
const ocrProviders = useSelector((state: RootState) => state.knowledge.ocrProviders)
const updateOcrProviders = (ocrProviders: OcrProvider[]) => {
dispatch(_updateOcrProviders(ocrProviders))
}
return {
ocrProviders,
updateOcrProviders
}
}
export const useOcrProvider = (id: string) => {
const dispatch = useDispatch()
const ocrProviders = useSelector((state: RootState) => state.knowledge.ocrProviders)
const ocrProvider = ocrProviders.find((provider) => provider.id === id)
if (!ocrProvider) {
throw new Error(`ocr provider with id ${id} not found`)
}
const updateOcrProvider = (ocrProvider: OcrProvider) => {
dispatch(_updateOcrProvider(ocrProvider))
}
return {
ocrProvider,
updateOcrProvider
}
}
+9 -2
View File
@@ -371,7 +371,7 @@
"url_added": "网址已添加",
"url_placeholder": "请输入网址, 多个网址用回车分隔",
"urls": "网址",
"settings":{
"settings": {
"title": "知识库设置",
"preprocessing": "预处理",
"preprocessing_tooltip": "对上传的文件进行ocr预处理",
@@ -380,7 +380,7 @@
"advanced": "高级设置",
"doc2x_api_key": "Doc2X API Key",
"doc2x_api_key_placeholder": "请输入 Doc2X API Key",
"doc2x_api_key_tooltip":"使用doc2x进行文档预处理"
"doc2x_api_key_tooltip": "使用doc2x进行文档预处理"
}
},
"languages": {
@@ -869,6 +869,13 @@
"input.target_language.english": "英文",
"input.target_language.japanese": "日文",
"input.target_language.russian": "俄文",
"knowledge": {
"title": "知识库",
"ocr": {
"title": "OCR设置",
"provider": "OCR服务商"
}
},
"mcp": {
"actions": "操作",
"active": "启用",
@@ -0,0 +1,126 @@
import { CheckOutlined, ExportOutlined, LoadingOutlined } from '@ant-design/icons'
import { getOcrProviderLogo, OCR_PROVIDER_CONFIG } from '@renderer/config/ocrProviders'
import { useOcrProvider } from '@renderer/hooks/useKnowledge'
import { formatApiKeys } from '@renderer/services/ApiService'
import { OcrProvider } from '@renderer/types'
import { hasObjectKey } from '@renderer/utils'
import { Avatar, Button, Divider, Flex, Input } from 'antd'
import Link from 'antd/es/typography/Link'
import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { SettingHelpLink, SettingHelpText, SettingHelpTextRow, SettingSubtitle, SettingTitle } from '..'
interface Props {
provider: OcrProvider
}
const OcrProviderSetting: FC<Props> = ({ provider: _provider }) => {
const { ocrProvider, updateOcrProvider } = useOcrProvider(_provider.id)
const { t } = useTranslation()
const [apiKey, setApiKey] = useState(ocrProvider.apiKey || '')
const [apiHost, setApiHost] = useState(ocrProvider.apiHost || '')
const [apiChecking, setApiChecking] = useState(false)
const [apiValid, setApiValid] = useState(false)
const ocrProviderConfig = OCR_PROVIDER_CONFIG[ocrProvider.id]
const apiKeyWebsite = ocrProviderConfig?.websites?.apiKey
const officialWebsite = ocrProviderConfig?.websites?.official
useEffect(() => {
setApiKey(ocrProvider.apiKey ?? '')
setApiHost(ocrProvider.apiHost ?? '')
}, [ocrProvider.apiKey, ocrProvider.apiHost])
const onUpdateApiKey = () => {
if (apiKey !== ocrProvider.apiKey) {
updateOcrProvider({ ...ocrProvider, apiKey })
}
}
const onUpdateApiHost = () => {
let trimmedHost = apiHost?.trim() || ''
if (trimmedHost.endsWith('/')) {
trimmedHost = trimmedHost.slice(0, -1)
}
if (trimmedHost !== ocrProvider.apiHost) {
updateOcrProvider({ ...ocrProvider, apiHost: trimmedHost })
} else {
setApiHost(ocrProvider.apiHost || '')
}
}
return (
<>
<SettingTitle>
<Flex align="center" gap={8}>
<ProviderLogo shape="square" src={getOcrProviderLogo(ocrProvider.id)} size={16} />
<ProviderName> {ocrProvider.name}</ProviderName>
{officialWebsite && ocrProviderConfig?.websites && (
<Link target="_blank" href={ocrProviderConfig.websites.official}>
<ExportOutlined style={{ color: 'var(--color-text)', fontSize: '12px' }} />
</Link>
)}
</Flex>
</SettingTitle>
<Divider style={{ width: '100%', margin: '10px 0' }} />
{hasObjectKey(ocrProvider, 'apiKey') && (
<>
<SettingSubtitle style={{ marginTop: 5, marginBottom: 10 }}>{t('settings.provider.api_key')}</SettingSubtitle>
<Flex gap={8}>
<Input.Password
value={apiKey}
placeholder={t('settings.provider.api_key')}
onChange={(e) => setApiKey(formatApiKeys(e.target.value))}
onBlur={onUpdateApiKey}
spellCheck={false}
type="password"
autoFocus={apiKey === ''}
/>
<Button
ghost={apiValid}
type={apiValid ? 'primary' : 'default'}
// onClick={checkSearch}
disabled={apiChecking}>
{apiChecking ? <LoadingOutlined spin /> : apiValid ? <CheckOutlined /> : t('settings.websearch.check')}
</Button>
</Flex>
<SettingHelpTextRow style={{ justifyContent: 'space-between', marginTop: 5 }}>
<SettingHelpLink target="_blank" href={apiKeyWebsite}>
{t('settings.websearch.get_api_key')}
</SettingHelpLink>
<SettingHelpText>{t('settings.provider.api_key.tip')}</SettingHelpText>
</SettingHelpTextRow>
</>
)}
{hasObjectKey(ocrProvider, 'apiHost') && (
<>
<SettingSubtitle style={{ marginTop: 5, marginBottom: 10 }}>
{t('settings.provider.api_host')}
</SettingSubtitle>
<Flex>
<Input
value={apiHost}
placeholder={t('settings.provider.api_host')}
onChange={(e) => setApiHost(e.target.value)}
onBlur={onUpdateApiHost}
/>
</Flex>
</>
)}
</>
)
}
const ProviderName = styled.span`
font-size: 14px;
font-weight: 500;
`
const ProviderLogo = styled(Avatar)`
border: 0.5px solid var(--color-border);
`
export default OcrProviderSetting
@@ -0,0 +1,45 @@
import { useTheme } from '@renderer/context/ThemeProvider'
import { useOcrProviders } from '@renderer/hooks/useKnowledge'
import { OcrProvider } from '@renderer/types'
import { Select } from 'antd'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
import OcrProviderSettings from './OcrProviderSettings'
const KnowledgeSettings: FC = () => {
const { ocrProviders } = useOcrProviders()
const { t } = useTranslation()
const [selectedProvider, setSelectedProvider] = useState<OcrProvider>(ocrProviders[0])
const { theme: themeMode } = useTheme()
return (
<SettingContainer theme={themeMode}>
<SettingGroup theme={themeMode}>
<SettingTitle>{t('settings.knowledge.ocr.title')}</SettingTitle>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.knowledge.ocr.provider')}</SettingRowTitle>
<div style={{ display: 'flex', gap: '8px' }}>
<Select
value={selectedProvider?.id}
style={{ width: '200px' }}
onChange={(value: string) => {
const provider = ocrProviders.find((p) => p.id === value)
if (!provider) return
setSelectedProvider(provider)
}}
placeholder={t('settings.websearch.search_provider_placeholder')}
options={ocrProviders.map((p) => ({ value: p.id, label: p.name }))}
/>
</div>
</SettingRow>
</SettingGroup>
<SettingGroup theme={themeMode}>
{selectedProvider && <OcrProviderSettings provider={selectedProvider} />}
</SettingGroup>
</SettingContainer>
)
}
export default KnowledgeSettings
@@ -1,6 +1,7 @@
import {
CloudOutlined,
CodeOutlined,
FileSearchOutlined,
GlobalOutlined,
InfoCircleOutlined,
LayoutOutlined,
@@ -21,6 +22,7 @@ import AboutSettings from './AboutSettings'
import DataSettings from './DataSettings/DataSettings'
import DisplaySettings from './DisplaySettings/DisplaySettings'
import GeneralSettings from './GeneralSettings'
import KnowledgeSettings from './KnowledgeSettings'
import MCPSettings from './MCPSettings'
import ProvidersList from './ProviderSettings'
import QuickAssistantSettings from './QuickAssistantSettings'
@@ -68,6 +70,13 @@ const SettingsPage: FC = () => {
{t('settings.mcp.title')}
</MenuItem>
</MenuItemLink>
<MenuItemLink to="/settings/knowledge">
<MenuItem className={isRoute('/settings/knowledge')}>
<FileSearchOutlined />
{t('settings.knowledge.title')}
</MenuItem>
</MenuItemLink>
<MenuItemLink to="/settings/general">
<MenuItem className={isRoute('/settings/general')}>
<SettingOutlined />
@@ -111,6 +120,7 @@ const SettingsPage: FC = () => {
<Route path="model" element={<ModelSettings />} />
<Route path="web-search" element={<WebSearchSettings />} />
<Route path="mcp" element={<MCPSettings />} />
<Route path="knowledge" element={<KnowledgeSettings />} />
<Route path="general/*" element={<GeneralSettings />} />
<Route path="display" element={<DisplaySettings />} />
<Route path="data/*" element={<DataSettings />} />
+1 -1
View File
@@ -38,7 +38,7 @@ const persistedReducer = persistReducer(
{
key: 'cherry-studio',
storage,
version: 81,
version: 82,
blacklist: ['runtime', 'messages'],
migrate
},
+18 -3
View File
@@ -1,13 +1,15 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import FileManager from '@renderer/services/FileManager'
import { FileType, KnowledgeBase, KnowledgeItem, ProcessingStatus } from '@renderer/types'
import { FileType, KnowledgeBase, KnowledgeItem, OcrProvider, ProcessingStatus } from '@renderer/types'
export interface KnowledgeState {
bases: KnowledgeBase[]
ocrProviders: OcrProvider[]
}
const initialState: KnowledgeState = {
bases: []
bases: [],
ocrProviders: []
}
const knowledgeSlice = createSlice({
@@ -183,6 +185,17 @@ const knowledgeSlice = createSlice({
item.uniqueIds = action.payload.uniqueIds
}
}
},
updateOcrProviders(state, action: PayloadAction<OcrProvider[]>) {
state.ocrProviders = action.payload
},
updateOcrProvider(state, action: PayloadAction<OcrProvider>) {
const index = state.ocrProviders.findIndex((provider) => provider.id === action.payload.id)
if (index !== -1) {
state.ocrProviders[index] = action.payload
}
}
}
})
@@ -201,7 +214,9 @@ export const {
updateItemProcessingStatus,
clearCompletedProcessing,
clearAllProcessing,
updateBaseItemUniqueId
updateBaseItemUniqueId,
updateOcrProviders,
updateOcrProvider
} = knowledgeSlice.actions
export default knowledgeSlice.reducer
+12
View File
@@ -772,6 +772,18 @@ const migrateConfig = {
'81': (state: RootState) => {
addProvider(state, 'copilot')
return state
},
'82': (state: RootState) => {
if (!state.knowledge.ocrProviders) {
state.knowledge.ocrProviders = []
}
state.knowledge.ocrProviders.push({
id: 'doc2x',
name: 'Doc2x',
apiKey: '',
apiHost: 'https://v2.doc2x.noedgeai.com'
})
return state
}
}
+6 -1
View File
@@ -260,7 +260,6 @@ export interface KnowledgeBase {
rerankModel?: Model
topN?: number
preprocessing?: boolean
doc2xApiKey?: string
}
export type KnowledgeBaseParams = {
@@ -279,6 +278,12 @@ export type KnowledgeBaseParams = {
topN?: number
}
export interface OcrProvider {
id: string
name: string
apiKey?: string
apiHost?: string
}
export type GenerateImageParams = {
model: string
prompt: string