diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index 4c6988cf6e..a3988d1c42 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -3,6 +3,8 @@ export enum IpcChannel { App_ClearCache = 'app:clear-cache', App_SetLaunchOnBoot = 'app:set-launch-on-boot', App_SetLanguage = 'app:set-language', + App_SetEnableSpellCheck = 'app:set-enable-spell-check', + App_SetSpellCheckLanguages = 'app:set-spell-check-languages', App_ShowUpdateDialog = 'app:show-update-dialog', App_CheckForUpdate = 'app:check-for-update', App_Reload = 'app:reload', diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 5f54d64e07..88d66d4a39 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -87,6 +87,26 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { configManager.setLanguage(language) }) + // spell check + ipcMain.handle(IpcChannel.App_SetEnableSpellCheck, (_, isEnable: boolean) => { + const windows = BrowserWindow.getAllWindows() + windows.forEach((window) => { + window.webContents.session.setSpellCheckerEnabled(isEnable) + }) + }) + + // spell check languages + ipcMain.handle(IpcChannel.App_SetSpellCheckLanguages, (_, languages: string[]) => { + if (languages.length === 0) { + return + } + const windows = BrowserWindow.getAllWindows() + windows.forEach((window) => { + window.webContents.session.setSpellCheckerLanguages(languages) + }) + configManager.set('spellCheckLanguages', languages) + }) + // launch on boot ipcMain.handle(IpcChannel.App_SetLaunchOnBoot, (_, openAtLogin: boolean) => { // Set login item settings for windows and mac diff --git a/src/main/services/ContextMenu.ts b/src/main/services/ContextMenu.ts index 2f4f5aa20f..34ec4b911a 100644 --- a/src/main/services/ContextMenu.ts +++ b/src/main/services/ContextMenu.ts @@ -9,7 +9,18 @@ class ContextMenu { const template: MenuItemConstructorOptions[] = this.createEditMenuItems(properties) const filtered = template.filter((item) => item.visible !== false) if (filtered.length > 0) { - const menu = Menu.buildFromTemplate([...filtered, ...this.createInspectMenuItems(w)]) + let template = [...filtered, ...this.createInspectMenuItems(w)] + const dictionarySuggestions = this.createDictionarySuggestions(properties, w) + if (dictionarySuggestions.length > 0) { + template = [ + ...dictionarySuggestions, + { type: 'separator' }, + this.createSpellCheckMenuItem(properties, w), + { type: 'separator' }, + ...template + ] + } + const menu = Menu.buildFromTemplate(template) menu.popup() } }) @@ -72,6 +83,53 @@ class ContextMenu { return template } + + private createSpellCheckMenuItem( + properties: Electron.ContextMenuParams, + mainWindow: Electron.BrowserWindow + ): MenuItemConstructorOptions { + const hasText = properties.selectionText.length > 0 + + return { + id: 'learnSpelling', + label: '&Learn Spelling', + visible: Boolean(properties.isEditable && hasText && properties.misspelledWord), + click: () => { + mainWindow.webContents.session.addWordToSpellCheckerDictionary(properties.misspelledWord) + } + } + } + + private createDictionarySuggestions( + properties: Electron.ContextMenuParams, + mainWindow: Electron.BrowserWindow + ): MenuItemConstructorOptions[] { + const hasText = properties.selectionText.length > 0 + + if (!hasText || !properties.misspelledWord) { + return [] + } + + if (properties.dictionarySuggestions.length === 0) { + return [ + { + id: 'dictionarySuggestions', + label: 'No Guesses Found', + visible: true, + enabled: false + } + ] + } + + return properties.dictionarySuggestions.map((suggestion) => ({ + id: 'dictionarySuggestions', + label: suggestion, + visible: Boolean(properties.isEditable && hasText && properties.misspelledWord), + click: (menuItem: Electron.MenuItem) => { + mainWindow.webContents.replaceMisspelling(menuItem.label) + } + })) + } } export const contextMenu = new ContextMenu() diff --git a/src/main/services/WindowService.ts b/src/main/services/WindowService.ts index f6322e8939..78784120b0 100644 --- a/src/main/services/WindowService.ts +++ b/src/main/services/WindowService.ts @@ -95,6 +95,7 @@ export class WindowService { this.setupMaximize(mainWindow, mainWindowState.isMaximized) this.setupContextMenu(mainWindow) + this.setupSpellCheck(mainWindow) this.setupWindowEvents(mainWindow) this.setupWebContentsHandlers(mainWindow) this.setupWindowLifecycleEvents(mainWindow) @@ -102,6 +103,18 @@ export class WindowService { this.loadMainWindowContent(mainWindow) } + private setupSpellCheck(mainWindow: BrowserWindow) { + const enableSpellCheck = configManager.get('enableSpellCheck', false) + if (enableSpellCheck) { + try { + const spellCheckLanguages = configManager.get('spellCheckLanguages', []) as string[] + spellCheckLanguages.length > 0 && mainWindow.webContents.session.setSpellCheckerLanguages(spellCheckLanguages) + } catch (error) { + Logger.error('Failed to set spell check languages:', error as Error) + } + } + } + private setupMainWindowMonitor(mainWindow: BrowserWindow) { mainWindow.webContents.on('render-process-gone', (_, details) => { Logger.error(`Renderer process crashed with: ${JSON.stringify(details)}`) diff --git a/src/preload/index.ts b/src/preload/index.ts index 5138d4e4de..114ad13ef6 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -17,6 +17,8 @@ const api = { checkForUpdate: () => ipcRenderer.invoke(IpcChannel.App_CheckForUpdate), showUpdateDialog: () => ipcRenderer.invoke(IpcChannel.App_ShowUpdateDialog), setLanguage: (lang: string) => ipcRenderer.invoke(IpcChannel.App_SetLanguage, lang), + setEnableSpellCheck: (isEnable: boolean) => ipcRenderer.invoke(IpcChannel.App_SetEnableSpellCheck, isEnable), + setSpellCheckLanguages: (languages: string[]) => ipcRenderer.invoke(IpcChannel.App_SetSpellCheckLanguages, languages), setLaunchOnBoot: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetLaunchOnBoot, isActive), setLaunchToTray: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetLaunchToTray, isActive), setTray: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetTray, isActive), diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index fefebd6394..fa1f493d80 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -864,7 +864,7 @@ "paint_course": "tutorial", "prompt_placeholder_edit": "Enter your image description, text drawing uses \"double quotes\" to wrap", "prompt_placeholder_en": "Enter your image description, currently Imagen only supports English prompts", - "proxy_required": "Open the proxy and enable “TUN mode” to view generated images or copy them to the browser for opening. In the future, domestic direct connection will be supported", + "proxy_required": "Open the proxy and enable \"TUN mode\" to view generated images or copy them to the browser for opening. In the future, domestic direct connection will be supported", "image_file_required": "Please upload an image first", "image_file_retry": "Please re-upload an image first", "image_placeholder": "No image available", @@ -1392,6 +1392,8 @@ "general.user_name": "User Name", "general.user_name.placeholder": "Enter your name", "general.view_webdav_settings": "View WebDAV settings", + "general.spell_check": "Spell Check", + "general.spell_check.languages": "Use spell check for", "input.auto_translate_with_space": "Quickly translate with 3 spaces", "input.show_translate_confirm": "Show translation confirmation dialog", "input.target_language": "Target language", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 612df65d71..ffa579d563 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -1387,6 +1387,8 @@ "general.user_name": "ユーザー名", "general.user_name.placeholder": "ユーザー名を入力", "general.view_webdav_settings": "WebDAV設定を表示", + "general.spell_check": "スペルチェック", + "general.spell_check.languages": "スペルチェック言語", "input.auto_translate_with_space": "スペースを3回押して翻訳", "input.target_language": "目標言語", "input.target_language.chinese": "簡体字中国語", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index ec2ad7785f..a713da42ee 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -1387,6 +1387,8 @@ "general.user_name": "Имя пользователя", "general.user_name.placeholder": "Введите ваше имя", "general.view_webdav_settings": "Просмотр настроек WebDAV", + "general.spell_check": "Проверка орфографии", + "general.spell_check.languages": "Языки проверки орфографии", "input.auto_translate_with_space": "Быстрый перевод с помощью 3-х пробелов", "input.target_language": "Целевой язык", "input.target_language.chinese": "Китайский упрощенный", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 8d7c30f323..c16089db5e 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -863,8 +863,8 @@ "learn_more": "了解更多", "paint_course": "教程", "prompt_placeholder_edit": "输入你的图片描述,文本绘制用 \"双引号\" 包裹", - "prompt_placeholder_en": "输入”英文“图片描述,目前 Imagen 仅支持英文提示词", - "proxy_required": "打开代理并开启”TUN模式“查看生成图片或复制到浏览器打开,后续会支持国内直连", + "prompt_placeholder_en": "输入\"英文\"图片描述,目前 Imagen 仅支持英文提示词", + "proxy_required": "打开代理并开启\"TUN模式\"查看生成图片或复制到浏览器打开,后续会支持国内直连", "image_file_required": "请先上传图片", "image_file_retry": "请重新上传图片", "image_placeholder": "暂无图片", @@ -960,7 +960,7 @@ "magic_prompt_option_tip": "智能优化放大提示词" }, "text_desc_required": "请先输入图片描述", - "req_error_text": "运行失败,请重试。提示词避免“版权词”和”敏感词”哦。", + "req_error_text": "运行失败,请重试。提示词避免\"版权词\"和\"敏感词\"哦。", "req_error_token": "请检查令牌有效性", "req_error_no_balance": "请检查令牌有效性", "image_handle_required": "请先上传图片", @@ -1390,9 +1390,11 @@ "general.restore.button": "恢复", "general.title": "常规设置", "general.user_name": "用户名", - "general.user_name.placeholder": "请输入用户名", + "general.user_name.placeholder": "输入您的姓名", "general.view_webdav_settings": "查看 WebDAV 设置", - "input.auto_translate_with_space": "快速敲击3次空格翻译", + "general.spell_check": "拼写检查", + "general.spell_check.languages": "拼写检查语言", + "input.auto_translate_with_space": "3个空格快速翻译", "input.show_translate_confirm": "显示翻译确认对话框", "input.target_language": "目标语言", "input.target_language.chinese": "简体中文", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 1d23fb540a..72be4f02e6 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -1389,6 +1389,8 @@ "general.user_name": "使用者名稱", "general.user_name.placeholder": "輸入您的名稱", "general.view_webdav_settings": "檢視 WebDAV 設定", + "general.spell_check": "拼寫檢查", + "general.spell_check.languages": "拼寫檢查語言", "input.auto_translate_with_space": "快速敲擊 3 次空格翻譯", "input.show_translate_confirm": "顯示翻譯確認對話框", "input.target_language": "目標語言", diff --git a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx index 958b779030..360a76d8a8 100644 --- a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx +++ b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx @@ -77,7 +77,8 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = showInputEstimatedTokens, autoTranslateWithSpace, enableQuickPanelTriggers, - enableBackspaceDeleteModel + enableBackspaceDeleteModel, + enableSpellCheck } = useSettings() const [expended, setExpend] = useState(false) const [estimateTokenCount, setEstimateTokenCount] = useState(0) @@ -780,9 +781,8 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = : t('chat.input.placeholder', { key: getSendMessageShortcutLabel(sendMessageShortcut) }) } autoFocus - contextMenu="true" variant="borderless" - spellCheck={false} + spellCheck={enableSpellCheck} rows={2} ref={textareaRef} style={{ diff --git a/src/renderer/src/pages/home/Messages/MessageEditor.tsx b/src/renderer/src/pages/home/Messages/MessageEditor.tsx index 895eb787d2..62636ccd68 100644 --- a/src/renderer/src/pages/home/Messages/MessageEditor.tsx +++ b/src/renderer/src/pages/home/Messages/MessageEditor.tsx @@ -40,7 +40,7 @@ const MessageBlockEditor: FC = ({ message, onSave, onResend, onCancel }) const model = assistant.model || assistant.defaultModel const isVision = useMemo(() => isVisionModel(model), [model]) const supportExts = useMemo(() => [...textExts, ...documentExts, ...(isVision ? imageExts : [])], [isVision]) - const { pasteLongTextAsFile, pasteLongTextThreshold, fontSize, sendMessageShortcut } = useSettings() + const { pasteLongTextAsFile, pasteLongTextThreshold, fontSize, sendMessageShortcut, enableSpellCheck } = useSettings() const { t } = useTranslation() const textareaRef = useRef(null) const attachmentButtonRef = useRef(null) @@ -222,13 +222,16 @@ const MessageBlockEditor: FC = ({ message, onSave, onResend, onCancel }) }} onKeyDown={(e) => handleKeyDown(e, block.id)} autoFocus - contextMenu="true" - spellCheck={false} + spellCheck={enableSpellCheck} onPaste={(e) => onPaste(e.nativeEvent)} onFocus={() => { // 记录当前聚焦的组件 PasteService.setLastFocusedComponent('messageEditor') }} + onContextMenu={(e) => { + // 阻止事件冒泡,避免触发全局的 Electron contextMenu + e.stopPropagation() + }} style={{ fontSize, padding: '0px 15px 8px 15px' diff --git a/src/renderer/src/pages/settings/GeneralSettings.tsx b/src/renderer/src/pages/settings/GeneralSettings.tsx index 7665099f23..ba0de7fdf9 100644 --- a/src/renderer/src/pages/settings/GeneralSettings.tsx +++ b/src/renderer/src/pages/settings/GeneralSettings.tsx @@ -2,8 +2,15 @@ import { useTheme } from '@renderer/context/ThemeProvider' import { useSettings } from '@renderer/hooks/useSettings' import i18n from '@renderer/i18n' import { RootState, useAppDispatch } from '@renderer/store' -import { setEnableDataCollection, setLanguage, setNotificationSettings } from '@renderer/store/settings' -import { setProxyMode, setProxyUrl as _setProxyUrl } from '@renderer/store/settings' +import { + setEnableDataCollection, + setEnableSpellCheck, + setLanguage, + setNotificationSettings, + setProxyMode, + setProxyUrl as _setProxyUrl, + setSpellCheckLanguages +} from '@renderer/store/settings' import { LanguageVarious } from '@renderer/types' import { NotificationSource } from '@renderer/types/notification' import { isValidProxyUrl } from '@renderer/utils' @@ -26,7 +33,8 @@ const GeneralSettings: FC = () => { trayOnClose, tray, proxyMode: storeProxyMode, - enableDataCollection + enableDataCollection, + enableSpellCheck } = useSettings() const [proxyUrl, setProxyUrl] = useState(storeProxyUrl) const { theme } = useTheme() @@ -69,6 +77,11 @@ const GeneralSettings: FC = () => { i18n.changeLanguage(value) } + const handleSpellCheckChange = (checked: boolean) => { + dispatch(setEnableSpellCheck(checked)) + window.api.setEnableSpellCheck(checked) + } + const onSetProxyUrl = () => { if (proxyUrl && !isValidProxyUrl(proxyUrl)) { window.message.error({ content: t('message.error.invalid.proxy.url'), key: 'proxy-error' }) @@ -109,11 +122,30 @@ const GeneralSettings: FC = () => { ] const notificationSettings = useSelector((state: RootState) => state.settings.notification) + const spellCheckLanguages = useSelector((state: RootState) => state.settings.spellCheckLanguages) const handleNotificationChange = (type: NotificationSource, value: boolean) => { dispatch(setNotificationSettings({ ...notificationSettings, [type]: value })) } + // Define available spell check languages with display names (only commonly supported languages) + const spellCheckLanguageOptions = [ + { value: 'en-US', label: 'English (US)', flag: '🇺🇸' }, + { value: 'es', label: 'Español', flag: '🇪🇸' }, + { value: 'fr', label: 'Français', flag: '🇫🇷' }, + { value: 'de', label: 'Deutsch', flag: '🇩🇪' }, + { value: 'it', label: 'Italiano', flag: '🇮🇹' }, + { value: 'pt', label: 'Português', flag: '🇵🇹' }, + { value: 'ru', label: 'Русский', flag: '🇷🇺' }, + { value: 'nl', label: 'Nederlands', flag: '🇳🇱' }, + { value: 'pl', label: 'Polski', flag: '🇵🇱' } + ] + + const handleSpellCheckLanguagesChange = (selectedLanguages: string[]) => { + dispatch(setSpellCheckLanguages(selectedLanguages)) + window.api.setSpellCheckLanguages(selectedLanguages) + } + return ( @@ -135,6 +167,37 @@ const GeneralSettings: FC = () => { + + {t('settings.general.spell_check')} + + + {enableSpellCheck && ( + <> + + + {t('settings.general.spell_check.languages')} + ) => { state.enableDataCollection = action.payload }, + setEnableSpellCheck: (state, action: PayloadAction) => { + state.enableSpellCheck = action.payload + }, + setSpellCheckLanguages: (state, action: PayloadAction) => { + state.spellCheckLanguages = action.payload + }, setExportMenuOptions: (state, action: PayloadAction) => { state.exportMenuOptions = action.payload }, @@ -776,8 +786,10 @@ export const { setShowOpenedMinappsInSidebar, setMinappsOpenLinkExternal, setEnableDataCollection, - setEnableQuickPanelTriggers, + setEnableSpellCheck, + setSpellCheckLanguages, setExportMenuOptions, + setEnableQuickPanelTriggers, setEnableBackspaceDeleteModel, setOpenAISummaryText, setOpenAIServiceTier,