feat(notes): add spell-check control (#10507)

* feat(notes): add spell-check control

* feat(notes): add spell-check toggle to preview mode toolbar

* feat(settings): move spellcheck to global and use hook
This commit is contained in:
George·Dong 2025-10-08 17:48:26 +08:00 committed by GitHub
parent d4b3428160
commit 504531d4d5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 94 additions and 4 deletions

View File

@ -48,7 +48,8 @@ const RichEditor = ({
enableContentSearch = false, enableContentSearch = false,
isFullWidth = false, isFullWidth = false,
fontFamily = 'default', fontFamily = 'default',
fontSize = 16 fontSize = 16,
enableSpellCheck = false
// toolbarItems: _toolbarItems // TODO: Implement custom toolbar items // toolbarItems: _toolbarItems // TODO: Implement custom toolbar items
}: RichEditorProps & { ref?: React.RefObject<RichEditorRef | null> }) => { }: RichEditorProps & { ref?: React.RefObject<RichEditorRef | null> }) => {
// Use the rich editor hook for complete editor management // Use the rich editor hook for complete editor management
@ -71,6 +72,7 @@ const RichEditor = ({
onBlur, onBlur,
placeholder, placeholder,
editable, editable,
enableSpellCheck,
scrollParent: () => scrollContainerRef.current, scrollParent: () => scrollContainerRef.current,
onShowTableActionMenu: ({ position, actions }) => { onShowTableActionMenu: ({ position, actions }) => {
const iconMap: Record<string, React.ReactNode> = { const iconMap: Record<string, React.ReactNode> = {

View File

@ -50,6 +50,8 @@ export interface RichEditorProps {
fontFamily?: 'default' | 'serif' fontFamily?: 'default' | 'serif'
/** Font size in pixels */ /** Font size in pixels */
fontSize?: number fontSize?: number
/** Whether to enable spell check */
enableSpellCheck?: boolean
} }
export interface ToolbarItem { export interface ToolbarItem {

View File

@ -57,6 +57,8 @@ export interface UseRichEditorOptions {
editable?: boolean editable?: boolean
/** Whether to enable table of contents functionality */ /** Whether to enable table of contents functionality */
enableTableOfContents?: boolean enableTableOfContents?: boolean
/** Whether to enable spell check */
enableSpellCheck?: boolean
/** Show table action menu (row/column) with concrete actions and position */ /** Show table action menu (row/column) with concrete actions and position */
onShowTableActionMenu?: (payload: { onShowTableActionMenu?: (payload: {
type: 'row' | 'column' type: 'row' | 'column'
@ -126,6 +128,7 @@ export const useRichEditor = (options: UseRichEditorOptions = {}): UseRichEditor
previewLength = 50, previewLength = 50,
placeholder = '', placeholder = '',
editable = true, editable = true,
enableSpellCheck = false,
onShowTableActionMenu, onShowTableActionMenu,
scrollParent scrollParent
} = options } = options
@ -410,7 +413,9 @@ export const useRichEditor = (options: UseRichEditorOptions = {}): UseRichEditor
// Allow text selection even when not editable // Allow text selection even when not editable
style: editable style: editable
? '' ? ''
: 'user-select: text; -webkit-user-select: text; -moz-user-select: text; -ms-user-select: text;' : 'user-select: text; -webkit-user-select: text; -moz-user-select: text; -ms-user-select: text;',
// Set spellcheck attribute on the contenteditable element
spellcheck: enableSpellCheck ? 'true' : 'false'
} }
}, },
onUpdate: ({ editor }) => { onUpdate: ({ editor }) => {

View File

@ -1784,6 +1784,8 @@
"sort_updated_asc": "Update time (oldest first)", "sort_updated_asc": "Update time (oldest first)",
"sort_updated_desc": "Update time (newest first)", "sort_updated_desc": "Update time (newest first)",
"sort_z2a": "File name (Z-A)", "sort_z2a": "File name (Z-A)",
"spell_check": "Spell Check",
"spell_check_tooltip": "Enable/Disable spell check",
"star": "Favorite note", "star": "Favorite note",
"starred_notes": "Collected notes", "starred_notes": "Collected notes",
"title": "Notes", "title": "Notes",

View File

@ -1784,6 +1784,8 @@
"sort_updated_asc": "更新时间(从旧到新)", "sort_updated_asc": "更新时间(从旧到新)",
"sort_updated_desc": "更新时间(从新到旧)", "sort_updated_desc": "更新时间(从新到旧)",
"sort_z2a": "文件名Z-A", "sort_z2a": "文件名Z-A",
"spell_check": "拼写检查",
"spell_check_tooltip": "启用/禁用拼写检查",
"star": "收藏笔记", "star": "收藏笔记",
"starred_notes": "收藏的笔记", "starred_notes": "收藏的笔记",
"title": "笔记", "title": "笔记",

View File

@ -1784,6 +1784,8 @@
"sort_updated_asc": "更新時間(從舊到新)", "sort_updated_asc": "更新時間(從舊到新)",
"sort_updated_desc": "更新時間(從新到舊)", "sort_updated_desc": "更新時間(從新到舊)",
"sort_z2a": "文件名Z-A", "sort_z2a": "文件名Z-A",
"spell_check": "拼寫檢查",
"spell_check_tooltip": "啟用/禁用拼寫檢查",
"star": "收藏筆記", "star": "收藏筆記",
"starred_notes": "收藏的筆記", "starred_notes": "收藏的筆記",
"title": "筆記", "title": "筆記",

View File

@ -334,6 +334,7 @@
"new_topic": "Νέο θέμα {{Command}}", "new_topic": "Νέο θέμα {{Command}}",
"pause": "Παύση", "pause": "Παύση",
"placeholder": "Εισάγετε μήνυμα εδώ...", "placeholder": "Εισάγετε μήνυμα εδώ...",
"placeholder_without_triggers": "Εδώ εισαγάγετε το μήνυμα, πατήστε {{key}} για αποστολή",
"send": "Αποστολή", "send": "Αποστολή",
"settings": "Ρυθμίσεις", "settings": "Ρυθμίσεις",
"thinking": { "thinking": {
@ -1696,6 +1697,12 @@
"provider_settings": "Μετάβαση στις ρυθμίσεις παρόχου" "provider_settings": "Μετάβαση στις ρυθμίσεις παρόχου"
}, },
"notes": { "notes": {
"auto_rename": {
"empty_note": "Το σημείωμα είναι κενό, δεν μπορεί να δημιουργηθεί όνομα",
"failed": "Αποτυχία δημιουργίας ονόματος σημείωσης",
"label": "Δημιουργία ονόματος σημείωσης",
"success": "Η δημιουργία του ονόματος σημειώσεων ολοκληρώθηκε με επιτυχία"
},
"characters": "χαρακτήρας", "characters": "χαρακτήρας",
"collapse": "σύμπτυξη", "collapse": "σύμπτυξη",
"content_placeholder": "Παρακαλώ εισαγάγετε το περιεχόμενο των σημειώσεων...", "content_placeholder": "Παρακαλώ εισαγάγετε το περιεχόμενο των σημειώσεων...",
@ -1777,6 +1784,8 @@
"sort_updated_asc": "χρόνος ενημέρωσης (από παλιά στα νέα)", "sort_updated_asc": "χρόνος ενημέρωσης (από παλιά στα νέα)",
"sort_updated_desc": "χρόνος ενημέρωσης (από νεώτερο σε παλαιότερο)", "sort_updated_desc": "χρόνος ενημέρωσης (από νεώτερο σε παλαιότερο)",
"sort_z2a": "όνομα αρχείου (Z-A)", "sort_z2a": "όνομα αρχείου (Z-A)",
"spell_check": "Έλεγχος ορθογραφίας",
"spell_check_tooltip": "Ενεργοποίηση/Απενεργοποίηση ελέγχου ορθογραφίας",
"star": "Αγαπημένες σημειώσεις", "star": "Αγαπημένες σημειώσεις",
"starred_notes": "Σημειώσεις συλλογής", "starred_notes": "Σημειώσεις συλλογής",
"title": "σημειώσεις", "title": "σημειώσεις",

View File

@ -334,6 +334,7 @@
"new_topic": "Nuevo tema {{Command}}", "new_topic": "Nuevo tema {{Command}}",
"pause": "Pausar", "pause": "Pausar",
"placeholder": "Escribe aquí tu mensaje...", "placeholder": "Escribe aquí tu mensaje...",
"placeholder_without_triggers": "Escriba un mensaje aquí y presione {{key}} para enviar",
"send": "Enviar", "send": "Enviar",
"settings": "Configuración", "settings": "Configuración",
"thinking": { "thinking": {
@ -1696,6 +1697,12 @@
"provider_settings": "Ir a la configuración del proveedor" "provider_settings": "Ir a la configuración del proveedor"
}, },
"notes": { "notes": {
"auto_rename": {
"empty_note": "La nota está vacía, no se puede generar un nombre",
"failed": "Error al generar el nombre de la nota",
"label": "Generar nombre de nota",
"success": "Se ha generado correctamente el nombre de la nota"
},
"characters": "carácter", "characters": "carácter",
"collapse": "ocultar", "collapse": "ocultar",
"content_placeholder": "Introduzca el contenido de la nota...", "content_placeholder": "Introduzca el contenido de la nota...",
@ -1777,6 +1784,8 @@
"sort_updated_asc": "Fecha de actualización (de más antigua a más reciente)", "sort_updated_asc": "Fecha de actualización (de más antigua a más reciente)",
"sort_updated_desc": "Fecha de actualización (de más nuevo a más antiguo)", "sort_updated_desc": "Fecha de actualización (de más nuevo a más antiguo)",
"sort_z2a": "Nombre de archivo (Z-A)", "sort_z2a": "Nombre de archivo (Z-A)",
"spell_check": "comprobación ortográfica",
"spell_check_tooltip": "Habilitar/deshabilitar revisión ortográfica",
"star": "Notas guardadas", "star": "Notas guardadas",
"starred_notes": "notas guardadas", "starred_notes": "notas guardadas",
"title": "notas", "title": "notas",

View File

@ -334,6 +334,7 @@
"new_topic": "Nouveau sujet {{Command}}", "new_topic": "Nouveau sujet {{Command}}",
"pause": "Pause", "pause": "Pause",
"placeholder": "Entrez votre message ici...", "placeholder": "Entrez votre message ici...",
"placeholder_without_triggers": "Entrez votre message ici, appuyez sur {{key}} pour envoyer",
"send": "Envoyer", "send": "Envoyer",
"settings": "Paramètres", "settings": "Paramètres",
"thinking": { "thinking": {
@ -1696,6 +1697,12 @@
"provider_settings": "Aller aux paramètres du fournisseur" "provider_settings": "Aller aux paramètres du fournisseur"
}, },
"notes": { "notes": {
"auto_rename": {
"empty_note": "La note est vide, impossible de générer un nom",
"failed": "Échec de la génération du nom de note",
"label": "Générer un nom de note",
"success": "La génération du nom de note a réussi"
},
"characters": "caractère", "characters": "caractère",
"collapse": "réduire", "collapse": "réduire",
"content_placeholder": "Veuillez saisir le contenu de la note...", "content_placeholder": "Veuillez saisir le contenu de la note...",
@ -1777,6 +1784,8 @@
"sort_updated_asc": "Heure de mise à jour (du plus ancien au plus récent)", "sort_updated_asc": "Heure de mise à jour (du plus ancien au plus récent)",
"sort_updated_desc": "Date de mise à jour (du plus récent au plus ancien)", "sort_updated_desc": "Date de mise à jour (du plus récent au plus ancien)",
"sort_z2a": "Nom de fichier (Z-A)", "sort_z2a": "Nom de fichier (Z-A)",
"spell_check": "Vérification orthographique",
"spell_check_tooltip": "Activer/Désactiver la vérification orthographique",
"star": "Notes enregistrées", "star": "Notes enregistrées",
"starred_notes": "notes de collection", "starred_notes": "notes de collection",
"title": "notes", "title": "notes",

View File

@ -334,6 +334,7 @@
"new_topic": "新しいトピック {{Command}}", "new_topic": "新しいトピック {{Command}}",
"pause": "一時停止", "pause": "一時停止",
"placeholder": "ここにメッセージを入力し、{{key}} を押して送信...", "placeholder": "ここにメッセージを入力し、{{key}} を押して送信...",
"placeholder_without_triggers": "ここにメッセージを入力し、{{key}} を押して送信してください",
"send": "送信", "send": "送信",
"settings": "設定", "settings": "設定",
"thinking": { "thinking": {
@ -1696,6 +1697,12 @@
"provider_settings": "プロバイダー設定に移動" "provider_settings": "プロバイダー設定に移動"
}, },
"notes": { "notes": {
"auto_rename": {
"empty_note": "ノートが空です。名前を生成できません。",
"failed": "ノート名の生成に失敗しました",
"label": "ノート名の生成",
"success": "ノート名の生成に成功しました"
},
"characters": "文字", "characters": "文字",
"collapse": "閉じる", "collapse": "閉じる",
"content_placeholder": "メモの内容を入力してください...", "content_placeholder": "メモの内容を入力してください...",
@ -1777,6 +1784,8 @@
"sort_updated_asc": "更新日時(古い順)", "sort_updated_asc": "更新日時(古い順)",
"sort_updated_desc": "更新日時(新しい順)", "sort_updated_desc": "更新日時(新しい順)",
"sort_z2a": "ファイル名Z-A", "sort_z2a": "ファイル名Z-A",
"spell_check": "スペルチェック",
"spell_check_tooltip": "スペルチェックの有効/無効",
"star": "お気に入りのノート", "star": "お気に入りのノート",
"starred_notes": "収集したノート", "starred_notes": "収集したノート",
"title": "ノート", "title": "ノート",

View File

@ -334,6 +334,7 @@
"new_topic": "Novo tópico {{Command}}", "new_topic": "Novo tópico {{Command}}",
"pause": "Pausar", "pause": "Pausar",
"placeholder": "Digite sua mensagem aqui...", "placeholder": "Digite sua mensagem aqui...",
"placeholder_without_triggers": "Digite a mensagem aqui, pressione {{key}} para enviar",
"send": "Enviar", "send": "Enviar",
"settings": "Configurações", "settings": "Configurações",
"thinking": { "thinking": {
@ -1696,6 +1697,12 @@
"provider_settings": "Ir para as configurações do provedor" "provider_settings": "Ir para as configurações do provedor"
}, },
"notes": { "notes": {
"auto_rename": {
"empty_note": "A nota está vazia, não é possível gerar um nome",
"failed": "Falha ao gerar o nome da nota",
"label": "Gerar nome da nota",
"success": "Nome da nota gerado com sucesso"
},
"characters": "caractere", "characters": "caractere",
"collapse": "[minimizar]", "collapse": "[minimizar]",
"content_placeholder": "Introduza o conteúdo da nota...", "content_placeholder": "Introduza o conteúdo da nota...",
@ -1777,6 +1784,8 @@
"sort_updated_asc": "Tempo de atualização (do mais antigo para o mais recente)", "sort_updated_asc": "Tempo de atualização (do mais antigo para o mais recente)",
"sort_updated_desc": "atualização de tempo (do mais novo para o mais antigo)", "sort_updated_desc": "atualização de tempo (do mais novo para o mais antigo)",
"sort_z2a": "Nome do arquivo (Z-A)", "sort_z2a": "Nome do arquivo (Z-A)",
"spell_check": "verificação ortográfica",
"spell_check_tooltip": "Ativar/Desativar verificação ortográfica",
"star": "Notas favoritas", "star": "Notas favoritas",
"starred_notes": "notas salvas", "starred_notes": "notas salvas",
"title": "nota", "title": "nota",

View File

@ -334,6 +334,7 @@
"new_topic": "Новый топик {{Command}}", "new_topic": "Новый топик {{Command}}",
"pause": "Остановить", "pause": "Остановить",
"placeholder": "Введите ваше сообщение здесь, нажмите {{key}} для отправки...", "placeholder": "Введите ваше сообщение здесь, нажмите {{key}} для отправки...",
"placeholder_without_triggers": "Введите сообщение здесь, нажмите {{key}}, чтобы отправить",
"send": "Отправить", "send": "Отправить",
"settings": "Настройки", "settings": "Настройки",
"thinking": { "thinking": {
@ -1696,6 +1697,12 @@
"provider_settings": "Перейти к настройкам поставщика" "provider_settings": "Перейти к настройкам поставщика"
}, },
"notes": { "notes": {
"auto_rename": {
"empty_note": "Заметки пусты, имя невозможно сгенерировать",
"failed": "Создание названия заметки не удалось",
"label": "Создать название заметки",
"success": "Имя заметки успешно создано"
},
"characters": "Символы", "characters": "Символы",
"collapse": "Свернуть", "collapse": "Свернуть",
"content_placeholder": "Введите содержимое заметки...", "content_placeholder": "Введите содержимое заметки...",
@ -1777,6 +1784,8 @@
"sort_updated_asc": "Время обновления (от старого к новому)", "sort_updated_asc": "Время обновления (от старого к новому)",
"sort_updated_desc": "Время обновления (от нового к старому)", "sort_updated_desc": "Время обновления (от нового к старому)",
"sort_z2a": "Имя файла (Я-А)", "sort_z2a": "Имя файла (Я-А)",
"spell_check": "Проверка орфографии",
"spell_check_tooltip": "Включить/отключить проверку орфографии",
"star": "Избранные заметки", "star": "Избранные заметки",
"starred_notes": "Сохраненные заметки", "starred_notes": "Сохраненные заметки",
"title": "заметки", "title": "заметки",

View File

@ -1,11 +1,16 @@
import ActionIconButton from '@renderer/components/Buttons/ActionIconButton'
import CodeEditor from '@renderer/components/CodeEditor' import CodeEditor from '@renderer/components/CodeEditor'
import { HSpaceBetweenStack } from '@renderer/components/Layout' import { HSpaceBetweenStack } from '@renderer/components/Layout'
import RichEditor from '@renderer/components/RichEditor' import RichEditor from '@renderer/components/RichEditor'
import { RichEditorRef } from '@renderer/components/RichEditor/types' import { RichEditorRef } from '@renderer/components/RichEditor/types'
import Selector from '@renderer/components/Selector' import Selector from '@renderer/components/Selector'
import { useNotesSettings } from '@renderer/hooks/useNotesSettings' import { useNotesSettings } from '@renderer/hooks/useNotesSettings'
import { useSettings } from '@renderer/hooks/useSettings'
import { useAppDispatch } from '@renderer/store'
import { setEnableSpellCheck } from '@renderer/store/settings'
import { EditorView } from '@renderer/types' import { EditorView } from '@renderer/types'
import { Empty } from 'antd' import { Empty, Tooltip } from 'antd'
import { SpellCheck } from 'lucide-react'
import { FC, memo, RefObject, useCallback, useMemo, useState } from 'react' import { FC, memo, RefObject, useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
@ -21,7 +26,9 @@ interface NotesEditorProps {
const NotesEditor: FC<NotesEditorProps> = memo( const NotesEditor: FC<NotesEditorProps> = memo(
({ activeNodeId, currentContent, tokenCount, onMarkdownChange, editorRef }) => { ({ activeNodeId, currentContent, tokenCount, onMarkdownChange, editorRef }) => {
const { t } = useTranslation() const { t } = useTranslation()
const dispatch = useAppDispatch()
const { settings } = useNotesSettings() const { settings } = useNotesSettings()
const { enableSpellCheck } = useSettings()
const currentViewMode = useMemo(() => { const currentViewMode = useMemo(() => {
if (settings.defaultViewMode === 'edit') { if (settings.defaultViewMode === 'edit') {
return settings.defaultEditMode return settings.defaultEditMode
@ -78,6 +85,7 @@ const NotesEditor: FC<NotesEditorProps> = memo(
isFullWidth={settings.isFullWidth} isFullWidth={settings.isFullWidth}
fontFamily={settings.fontFamily} fontFamily={settings.fontFamily}
fontSize={settings.fontSize} fontSize={settings.fontSize}
enableSpellCheck={enableSpellCheck}
/> />
)} )}
</RichEditorContainer> </RichEditorContainer>
@ -92,8 +100,21 @@ const NotesEditor: FC<NotesEditorProps> = memo(
color: 'var(--color-text-3)', color: 'var(--color-text-3)',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: 8 gap: 12
}}> }}>
{tmpViewMode === 'preview' && (
<Tooltip placement="top" title={t('notes.spell_check_tooltip')} mouseLeaveDelay={0} arrow>
<ActionIconButton
active={enableSpellCheck}
onClick={() => {
const newValue = !enableSpellCheck
dispatch(setEnableSpellCheck(newValue))
window.api.setEnableSpellCheck(newValue)
}}>
<SpellCheck size={18} />
</ActionIconButton>
</Tooltip>
)}
<Selector <Selector
value={tmpViewMode as EditorView} value={tmpViewMode as EditorView}
onChange={(value: EditorView) => setTmpViewMode(value)} onChange={(value: EditorView) => setTmpViewMode(value)}