From eb13dd7c4fcc49546a2b956da58a2e6f10c8c044 Mon Sep 17 00:00:00 2001 From: Douglas Henri Date: Fri, 16 Aug 2024 12:26:48 -0300 Subject: [PATCH] AI Assistant: Add spelling mistake detection to Breve (#38923) * add nspell to jetpack * add spelling mistake feature * hide suggestion button for typos * changelog * fix for js test --- pnpm-lock.yaml | 16 ++++ .../update-jetpack-ai-breve-typo-local | 4 + .../breve/features/_features.colors.scss | 1 + .../components/breve/features/index.ts | 10 +++ .../breve/features/spelling-mistakes/index.ts | 80 +++++++++++++++++++ .../components/breve/highlight/index.tsx | 3 +- .../components/breve/types.ts | 6 ++ projects/plugins/jetpack/package.json | 1 + 8 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 projects/plugins/jetpack/changelog/update-jetpack-ai-breve-typo-local create mode 100644 projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/features/spelling-mistakes/index.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 24dd6e0ae6c0e..96995a9007a03 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3887,6 +3887,9 @@ importers: markdown-it-footnote: specifier: 3.0.3 version: 3.0.3 + nspell: + specifier: 2.1.5 + version: 2.1.5 photon: specifier: 4.0.0 version: 4.0.0 @@ -10511,6 +10514,10 @@ packages: resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} engines: {node: '>= 0.4'} + is-buffer@2.0.5: + resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==} + engines: {node: '>=4'} + is-builtin-module@3.2.1: resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} engines: {node: '>=6'} @@ -11734,6 +11741,9 @@ packages: - which - write-file-atomic + nspell@2.1.5: + resolution: {integrity: sha512-PSStyugKMiD9mHmqI/CR5xXrSIGejUXPlo88FBRq5Og1kO5QwQ5Ilu8D8O5I/SHpoS+mibpw6uKA8rd3vXd2Sg==} + nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} @@ -22682,6 +22692,8 @@ snapshots: call-bind: 1.0.7 has-tostringtag: 1.0.2 + is-buffer@2.0.5: {} + is-builtin-module@3.2.1: dependencies: builtin-modules: 3.3.0 @@ -24293,6 +24305,10 @@ snapshots: npm@8.19.4: {} + nspell@2.1.5: + dependencies: + is-buffer: 2.0.5 + nth-check@2.1.1: dependencies: boolbase: 1.0.0 diff --git a/projects/plugins/jetpack/changelog/update-jetpack-ai-breve-typo-local b/projects/plugins/jetpack/changelog/update-jetpack-ai-breve-typo-local new file mode 100644 index 0000000000000..9f77a091a391d --- /dev/null +++ b/projects/plugins/jetpack/changelog/update-jetpack-ai-breve-typo-local @@ -0,0 +1,4 @@ +Significance: patch +Type: other + +AI Assistant: Add spelling mistake detection to Breve diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/features/_features.colors.scss b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/features/_features.colors.scss index 53e58a2f6f870..c74fdd43f5c73 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/features/_features.colors.scss +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/features/_features.colors.scss @@ -5,6 +5,7 @@ $features-colors: ( 'complex-words': rgb( 240, 184, 73 ), 'unconfident-words': rgb( 9, 181, 133 ), 'long-sentences': rgb( 122, 0, 223 ), + 'spelling-mistakes': rgb( 214, 54, 56 ), ); @mixin properties( $feature, $color, $properties, $opacity: false ) { diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/features/index.ts b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/features/index.ts index de7662258558d..a0f5c63cee23f 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/features/index.ts +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/features/index.ts @@ -5,8 +5,10 @@ import { __ } from '@wordpress/i18n'; /** * Features */ +import { getFeatureAvailability } from '../../../../../blocks/ai-assistant/lib/utils/get-feature-availability'; import complexWords, { COMPLEX_WORDS, dictionary as dicComplex } from './complex-words'; import longSentences, { LONG_SENTENCES } from './long-sentences'; +import spellingMistakes, { SPELLING_MISTAKES } from './spelling-mistakes'; import unconfidentWords, { UNCONFIDENT_WORDS } from './unconfident-words'; /** * Types @@ -33,4 +35,12 @@ const features: Array< BreveFeature > = [ }, ]; +if ( getFeatureAvailability( 'ai-breve-typo-support' ) ) { + features.unshift( { + config: SPELLING_MISTAKES, + highlight: spellingMistakes, + description: __( 'Fix spelling mistakes.', 'jetpack' ), + } ); +} + export default features; diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/features/spelling-mistakes/index.ts b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/features/spelling-mistakes/index.ts new file mode 100644 index 0000000000000..4db69f3c54ff5 --- /dev/null +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/features/spelling-mistakes/index.ts @@ -0,0 +1,80 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import nspell from 'nspell'; +/** + * Types + */ +import type { BreveFeatureConfig, HighlightedText, SpellChecker } from '../../types'; + +export const SPELLING_MISTAKES: BreveFeatureConfig = { + name: 'spelling-mistakes', + title: __( 'Spelling mistakes', 'jetpack' ), + tagName: 'span', + className: 'jetpack-ai-breve__has-proofread-highlight--spelling-mistakes', + defaultEnabled: false, +}; + +const spellcheckers: { [ key: string ]: SpellChecker } = {}; +const spellingContexts: { + [ key: string ]: { + affix: string; + dictionary: string; + }; +} = {}; + +const loadContext = ( language: string ) => { + // TODO: Load dictionaries dynamically and save on localStorage + return spellingContexts[ language ]; +}; + +const getSpellchecker = ( { language = 'en' }: { language?: string } = {} ) => { + if ( spellcheckers[ language ] ) { + return spellcheckers[ language ]; + } + + // Cannot await here as the Rich Text function needs to be synchronous. + // Load of the dictionary in the background if necessary and re-trigger the highlights later. + const spellingContext = loadContext( language ); + + if ( ! spellingContext ) { + return null; + } + + const { affix, dictionary } = spellingContext; + spellcheckers[ language ] = nspell( affix, dictionary ); + + return spellcheckers[ language ]; +}; + +export default function longSentences( text: string ): Array< HighlightedText > { + const highlightedTexts: Array< HighlightedText > = []; + // Regex to match words, including contractions and hyphenated words + // \p{L} is a Unicode property that matches any letter in any language + // \p{M} is a Unicode property that matches any character intended to be combined with another character + const wordRegex = new RegExp( /[\p{L}\p{M}'-]+/, 'gu' ); + const words = text.match( wordRegex ) || []; + const spellchecker = getSpellchecker(); + + if ( ! spellchecker ) { + return highlightedTexts; + } + + words.forEach( ( word: string, index ) => { + if ( ! spellchecker.correct( word ) ) { + const suggestions = spellchecker.suggest( word ); + + if ( suggestions.length > 0 ) { + highlightedTexts.push( { + text: word, + startIndex: text.indexOf( word, index ), + endIndex: text.indexOf( word, index ) + word.length, + suggestions, + } ); + } + } + } ); + + return highlightedTexts; +} diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/highlight/index.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/highlight/index.tsx index 6c37a8c99decb..57bba06fb418b 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/highlight/index.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/highlight/index.tsx @@ -25,6 +25,7 @@ import { BREVE_FEATURE_NAME } from '../constants'; import features from '../features'; import registerEvents from '../features/events'; import { LONG_SENTENCES } from '../features/long-sentences'; +import { SPELLING_MISTAKES } from '../features/spelling-mistakes'; import getBreveAvailability from '../utils/get-availability'; import { getNodeTextIndex } from '../utils/get-node-text-index'; import { getNonLinkAncestor } from '../utils/get-non-link-ancestor'; @@ -231,7 +232,7 @@ export default function Highlight() {
{ title }
- { ! hasSuggestions && ( + { ! hasSuggestions && feature !== SPELLING_MISTAKES.name && (
{ loading ? (
diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/types.ts b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/types.ts index 4f2ec5d580bcb..199ff5c12d625 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/types.ts +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/types.ts @@ -105,6 +105,12 @@ export type BreveFeature = { export type HighlightedText = { text: string; suggestion?: string; + suggestions?: Array< string >; startIndex: number; endIndex: number; }; + +export type SpellChecker = { + correct: ( word: string ) => boolean; + suggest: ( word: string ) => Array< string >; +}; diff --git a/projects/plugins/jetpack/package.json b/projects/plugins/jetpack/package.json index 2986d548f9687..df571d9e8f6bc 100644 --- a/projects/plugins/jetpack/package.json +++ b/projects/plugins/jetpack/package.json @@ -97,6 +97,7 @@ "mapbox-gl": "1.13.0", "markdown-it": "14.0.0", "markdown-it-footnote": "3.0.3", + "nspell": "2.1.5", "photon": "4.0.0", "postcss-custom-properties": "12.1.7", "prop-types": "15.7.2",