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

View File

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

View File

@ -57,6 +57,8 @@ export interface UseRichEditorOptions {
editable?: boolean
/** Whether to enable table of contents functionality */
enableTableOfContents?: boolean
/** Whether to enable spell check */
enableSpellCheck?: boolean
/** Show table action menu (row/column) with concrete actions and position */
onShowTableActionMenu?: (payload: {
type: 'row' | 'column'
@ -126,6 +128,7 @@ export const useRichEditor = (options: UseRichEditorOptions = {}): UseRichEditor
previewLength = 50,
placeholder = '',
editable = true,
enableSpellCheck = false,
onShowTableActionMenu,
scrollParent
} = options
@ -410,7 +413,9 @@ export const useRichEditor = (options: UseRichEditorOptions = {}): UseRichEditor
// Allow text selection even when not 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 }) => {

View File

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

View File

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

View File

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

View File

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

View File

@ -334,6 +334,7 @@
"new_topic": "Nuevo tema {{Command}}",
"pause": "Pausar",
"placeholder": "Escribe aquí tu mensaje...",
"placeholder_without_triggers": "Escriba un mensaje aquí y presione {{key}} para enviar",
"send": "Enviar",
"settings": "Configuración",
"thinking": {
@ -1696,6 +1697,12 @@
"provider_settings": "Ir a la configuración del proveedor"
},
"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",
"collapse": "ocultar",
"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_desc": "Fecha de actualización (de más nuevo a más antiguo)",
"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",
"starred_notes": "notas guardadas",
"title": "notas",

View File

@ -334,6 +334,7 @@
"new_topic": "Nouveau sujet {{Command}}",
"pause": "Pause",
"placeholder": "Entrez votre message ici...",
"placeholder_without_triggers": "Entrez votre message ici, appuyez sur {{key}} pour envoyer",
"send": "Envoyer",
"settings": "Paramètres",
"thinking": {
@ -1696,6 +1697,12 @@
"provider_settings": "Aller aux paramètres du fournisseur"
},
"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",
"collapse": "réduire",
"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_desc": "Date de mise à jour (du plus récent au plus ancien)",
"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",
"starred_notes": "notes de collection",
"title": "notes",

View File

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

View File

@ -334,6 +334,7 @@
"new_topic": "Novo tópico {{Command}}",
"pause": "Pausar",
"placeholder": "Digite sua mensagem aqui...",
"placeholder_without_triggers": "Digite a mensagem aqui, pressione {{key}} para enviar",
"send": "Enviar",
"settings": "Configurações",
"thinking": {
@ -1696,6 +1697,12 @@
"provider_settings": "Ir para as configurações do provedor"
},
"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",
"collapse": "[minimizar]",
"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_desc": "atualização de tempo (do mais novo para o mais antigo)",
"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",
"starred_notes": "notas salvas",
"title": "nota",

View File

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

View File

@ -1,11 +1,16 @@
import ActionIconButton from '@renderer/components/Buttons/ActionIconButton'
import CodeEditor from '@renderer/components/CodeEditor'
import { HSpaceBetweenStack } from '@renderer/components/Layout'
import RichEditor from '@renderer/components/RichEditor'
import { RichEditorRef } from '@renderer/components/RichEditor/types'
import Selector from '@renderer/components/Selector'
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 { Empty } from 'antd'
import { Empty, Tooltip } from 'antd'
import { SpellCheck } from 'lucide-react'
import { FC, memo, RefObject, useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@ -21,7 +26,9 @@ interface NotesEditorProps {
const NotesEditor: FC<NotesEditorProps> = memo(
({ activeNodeId, currentContent, tokenCount, onMarkdownChange, editorRef }) => {
const { t } = useTranslation()
const dispatch = useAppDispatch()
const { settings } = useNotesSettings()
const { enableSpellCheck } = useSettings()
const currentViewMode = useMemo(() => {
if (settings.defaultViewMode === 'edit') {
return settings.defaultEditMode
@ -78,6 +85,7 @@ const NotesEditor: FC<NotesEditorProps> = memo(
isFullWidth={settings.isFullWidth}
fontFamily={settings.fontFamily}
fontSize={settings.fontSize}
enableSpellCheck={enableSpellCheck}
/>
)}
</RichEditorContainer>
@ -92,8 +100,21 @@ const NotesEditor: FC<NotesEditorProps> = memo(
color: 'var(--color-text-3)',
display: 'flex',
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
value={tmpViewMode as EditorView}
onChange={(value: EditorView) => setTmpViewMode(value)}