feat(ContextMenu): add spell check and dictionary suggestions to context menu (#7067)

* feat(ContextMenu): add spell check and dictionary suggestions to context menu

- Implemented spell check functionality in the context menu with options to learn spelling and view dictionary suggestions.
- Updated WindowService to enable spellcheck in the webview.
- Enabled spell check in Inputbar and MessageEditor components.

* feat(SpellCheck): implement spell check language settings and initialization

- Added support for configuring spell check languages based on user-selected language.
- Introduced IPC channel for setting spell check languages.
- Updated settings to manage spell check enablement and languages.
- Enhanced UI to allow users to toggle spell check functionality and select languages.
- Default spell check languages are set based on the current UI language if none are specified.

* refactor(SpellCheck): enhance spell check language mapping and UI settings

- Updated spell check language mapping to default to English for unsupported languages.
- Improved UI logic to only update spell check languages when enabled and no manual selections are made.
- Added a new selection component for users to choose from commonly supported spell check languages.

* feat(SpellCheck): integrate spell check functionality into Inputbar and MessageEditor

- Added enableSpellCheck setting to control spell check functionality in both Inputbar and MessageEditor components.
- Updated spellCheck prop to utilize the new setting, enhancing user experience by allowing customization of spell check behavior.

* refactor(SpellCheck): move spell check initialization to WindowService

- Removed spell check language initialization from index.ts and integrated it into WindowService.
- Added setupSpellCheck method to configure spell check languages based on user settings.
- Enhanced error handling for spell check language setup.

* feat(SpellCheck): add enable spell check functionality and IPC channel

- Introduced a new IPC channel for enabling/disabling spell check functionality.
- Updated the preload API to include a method for setting spell check enablement.
- Modified the main IPC handler to manage spell check settings based on user input.
- Simplified spell check language handling in the settings component by directly invoking the new API method.

* refactor(SpellCheck): remove spellcheck option from WindowService configuration

- Removed the spellcheck property from the WindowService configuration object.
- This change streamlines the configuration setup as spell check functionality is now managed through IPC channels.

* feat(i18n): add spell check translations for Japanese, Russian, and Traditional Chinese

- Added new translations for spell check functionality in ja-jp, ru-ru, and zh-tw locale files.
- Included descriptions and language selection options for spell check settings to enhance user experience.

* feat(migrate): add spell check configuration migration

- Implemented migration for spell check settings, disabling spell check and clearing selected languages in the new configuration.
- Enhanced error handling to ensure state consistency during migration process.

* fix(migrate): ensure spell check settings are updated safely

- Added a check to ensure state.settings exists before modifying spell check settings during migration.
- Removed redundant error handling that returned the state unmodified in case of an error.

* fix(WindowService): set default values for spell check configuration and update related UI texts

* refactor(Inputbar, MessageEditor): remove contextMenu attribute and add context menu handling in MessageEditor

---------

Co-authored-by: beyondkmp <beyondkmkp@gmail.com>
This commit is contained in:
beyondkmp 2025-06-23 21:19:21 +08:00 committed by GitHub
parent be15206234
commit bbe380cc9e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 204 additions and 17 deletions

View File

@ -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',

View File

@ -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

View File

@ -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()

View File

@ -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)}`)

View File

@ -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),

View File

@ -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",

View File

@ -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": "簡体字中国語",

View File

@ -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": "Китайский упрощенный",

View File

@ -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": "简体中文",

View File

@ -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": "目標語言",

View File

@ -77,7 +77,8 @@ const Inputbar: FC<Props> = ({ 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<Props> = ({ 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={{

View File

@ -40,7 +40,7 @@ const MessageBlockEditor: FC<Props> = ({ 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<TextAreaRef>(null)
const attachmentButtonRef = useRef<AttachmentButtonRef>(null)
@ -222,13 +222,16 @@ const MessageBlockEditor: FC<Props> = ({ 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'

View File

@ -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<string | undefined>(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 (
<SettingContainer theme={theme}>
<SettingGroup theme={theme}>
@ -135,6 +167,37 @@ const GeneralSettings: FC = () => {
</Select>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.general.spell_check')}</SettingRowTitle>
<Switch checked={enableSpellCheck} onChange={handleSpellCheckChange} />
</SettingRow>
{enableSpellCheck && (
<>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.general.spell_check.languages')}</SettingRowTitle>
<Select
mode="multiple"
value={spellCheckLanguages}
style={{ width: 280 }}
placeholder={t('settings.general.spell_check.languages')}
onChange={handleSpellCheckLanguagesChange}
options={spellCheckLanguageOptions.map((lang) => ({
value: lang.value,
label: (
<Space.Compact direction="horizontal" block>
<Space.Compact block>{lang.label}</Space.Compact>
<span role="img" aria-label={lang.flag}>
{lang.flag}
</span>
</Space.Compact>
)
}))}
/>
</SettingRow>
</>
)}
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.proxy.mode.title')}</SettingRowTitle>
<Select

View File

@ -1603,6 +1603,10 @@ const migrateConfig = {
state.settings.exportMenuOptions.plain_text = true
}
}
if (state.settings) {
state.settings.enableSpellCheck = false
state.settings.spellCheckLanguages = []
}
return state
} catch (error) {
return state

View File

@ -155,6 +155,8 @@ export interface SettingsState {
minappsOpenLinkExternal: boolean
// 隐私设置
enableDataCollection: boolean
enableSpellCheck: boolean
spellCheckLanguages: string[]
enableQuickPanelTriggers: boolean
enableBackspaceDeleteModel: boolean
exportMenuOptions: {
@ -298,6 +300,8 @@ export const initialState: SettingsState = {
showOpenedMinappsInSidebar: true,
minappsOpenLinkExternal: false,
enableDataCollection: false,
enableSpellCheck: false,
spellCheckLanguages: [],
enableQuickPanelTriggers: false,
enableBackspaceDeleteModel: true,
exportMenuOptions: {
@ -657,6 +661,12 @@ const settingsSlice = createSlice({
setEnableDataCollection: (state, action: PayloadAction<boolean>) => {
state.enableDataCollection = action.payload
},
setEnableSpellCheck: (state, action: PayloadAction<boolean>) => {
state.enableSpellCheck = action.payload
},
setSpellCheckLanguages: (state, action: PayloadAction<string[]>) => {
state.spellCheckLanguages = action.payload
},
setExportMenuOptions: (state, action: PayloadAction<typeof initialState.exportMenuOptions>) => {
state.exportMenuOptions = action.payload
},
@ -776,8 +786,10 @@ export const {
setShowOpenedMinappsInSidebar,
setMinappsOpenLinkExternal,
setEnableDataCollection,
setEnableQuickPanelTriggers,
setEnableSpellCheck,
setSpellCheckLanguages,
setExportMenuOptions,
setEnableQuickPanelTriggers,
setEnableBackspaceDeleteModel,
setOpenAISummaryText,
setOpenAIServiceTier,