diff --git a/src/renderer/src/assets/images/ocr/doc2x.svg b/src/renderer/src/assets/images/ocr/doc2x.svg new file mode 100644 index 000000000..ff3d7740c --- /dev/null +++ b/src/renderer/src/assets/images/ocr/doc2x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/renderer/src/config/ocrProviders.ts b/src/renderer/src/config/ocrProviders.ts new file mode 100644 index 000000000..19c538f20 --- /dev/null +++ b/src/renderer/src/config/ocrProviders.ts @@ -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' + } + } +} diff --git a/src/renderer/src/hooks/useKnowledge.ts b/src/renderer/src/hooks/useKnowledge.ts index 628a51ecc..01a396772 100644 --- a/src/renderer/src/hooks/useKnowledge.ts +++ b/src/renderer/src/hooks/useKnowledge.ts @@ -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 + } +} diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 9bff317ae..78ef03f6c 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -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": "启用", diff --git a/src/renderer/src/pages/settings/KnowledgeSettings/OcrProviderSettings.tsx b/src/renderer/src/pages/settings/KnowledgeSettings/OcrProviderSettings.tsx new file mode 100644 index 000000000..6c6520e5b --- /dev/null +++ b/src/renderer/src/pages/settings/KnowledgeSettings/OcrProviderSettings.tsx @@ -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 = ({ 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 ( + <> + + + + + {ocrProvider.name} + {officialWebsite && ocrProviderConfig?.websites && ( + + + + )} + + + + {hasObjectKey(ocrProvider, 'apiKey') && ( + <> + {t('settings.provider.api_key')} + + setApiKey(formatApiKeys(e.target.value))} + onBlur={onUpdateApiKey} + spellCheck={false} + type="password" + autoFocus={apiKey === ''} + /> + + + + + {t('settings.websearch.get_api_key')} + + {t('settings.provider.api_key.tip')} + + + )} + + {hasObjectKey(ocrProvider, 'apiHost') && ( + <> + + {t('settings.provider.api_host')} + + + setApiHost(e.target.value)} + onBlur={onUpdateApiHost} + /> + + + )} + + ) +} + +const ProviderName = styled.span` + font-size: 14px; + font-weight: 500; +` +const ProviderLogo = styled(Avatar)` + border: 0.5px solid var(--color-border); +` + +export default OcrProviderSetting diff --git a/src/renderer/src/pages/settings/KnowledgeSettings/index.tsx b/src/renderer/src/pages/settings/KnowledgeSettings/index.tsx new file mode 100644 index 000000000..815de9594 --- /dev/null +++ b/src/renderer/src/pages/settings/KnowledgeSettings/index.tsx @@ -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(ocrProviders[0]) + const { theme: themeMode } = useTheme() + + return ( + + + {t('settings.knowledge.ocr.title')} + + + {t('settings.knowledge.ocr.provider')} +
+