feat: knowledge ocr settings
This commit is contained in:
@@ -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 |
@@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 />} />
|
||||
|
||||
@@ -38,7 +38,7 @@ const persistedReducer = persistReducer(
|
||||
{
|
||||
key: 'cherry-studio',
|
||||
storage,
|
||||
version: 81,
|
||||
version: 82,
|
||||
blacklist: ['runtime', 'messages'],
|
||||
migrate
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user