Skip to content

Commit

Permalink
AI Assistant: Add option to add word to Breve spelling dictionary (#3…
Browse files Browse the repository at this point in the history
…9046)

* add reloadDictionary action and update type

* add button to add to dictionary

* add jetpack_ai_breve_add_to_dictionary event call

* changelog

* change button positioning

* update UI

* fix block serialization
  • Loading branch information
dhasilva authored Aug 26, 2024
1 parent a538067 commit 64d0535
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 22 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: other

AI Assistant: Add option to add word to spelling dictionary
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const SPELLING_MISTAKES: BreveFeatureConfig = {
defaultEnabled: false,
};

const spellcheckers: { [ key: string ]: SpellChecker } = {};
const spellCheckers: { [ key: string ]: SpellChecker } = {};
const contextRequests: {
[ key: string ]: { loading: boolean; loaded: boolean; failed: boolean };
} = {};
Expand Down Expand Up @@ -79,9 +79,9 @@ const getContext = ( language: string ) => {
return context;
};

export const getSpellchecker = ( { language = 'en' }: { language?: string } = {} ) => {
if ( spellcheckers[ language ] ) {
return spellcheckers[ language ];
export const getSpellChecker = ( { language = 'en' }: { language?: string } = {} ) => {
if ( spellCheckers[ language ] ) {
return spellCheckers[ language ];
}

// Cannot await here as the Rich Text function needs to be synchronous.
Expand All @@ -93,9 +93,74 @@ export const getSpellchecker = ( { language = 'en' }: { language?: string } = {}
}

const { affix, dictionary } = spellingContext;
spellcheckers[ language ] = nspell( affix, dictionary );
const spellChecker = nspell( affix, dictionary ) as unknown as SpellChecker;

return spellcheckers[ language ];
// Get the exceptions from the local storage
const exceptions: string[] = Array.from(
new Set(
JSON.parse(
localStorage.getItem( `jetpack-ai-breve-spelling-exceptions-${ language }` ) as string
) || []
)
);
exceptions.forEach( exception => spellChecker.add( exception ) );

spellCheckers[ language ] = spellChecker;

return spellCheckers[ language ];
};

export const addTextToDictionary = (
text: string,
{ language = 'en' }: { language?: string } = {}
) => {
const spellChecker = getSpellChecker( { language } );
const { reloadDictionary } = dispatch( 'jetpack/ai-breve' ) as BreveDispatch;

if ( ! spellChecker ) {
return;
}

try {
// Save the new exception to the local storage
const current = new Set(
JSON.parse(
localStorage.getItem( `jetpack-ai-breve-spelling-exceptions-${ language }` ) as string
) || []
);

current.add( text );

localStorage.setItem(
`jetpack-ai-breve-spelling-exceptions-${ language }`,
JSON.stringify( Array.from( current ) )
);
} catch ( error ) {
debug( 'Failed to add text to the dictionary', error );
return;
}

// Recompute the spell checker on the next call
delete spellCheckers[ language ];

reloadDictionary( SPELLING_MISTAKES.name );

debug( 'Added text to the dictionary', text );
};

export const suggestSpellingFixes = (
text: string,
{ language = 'en' }: { language?: string } = {}
) => {
const spellChecker = getSpellChecker( { language } );

if ( ! spellChecker ) {
return [];
}

const suggestions = spellChecker.suggest( text );

return suggestions;
};

export default function spellingMistakes( text: string ): Array< HighlightedText > {
Expand All @@ -110,9 +175,9 @@ export default function spellingMistakes( text: string ): Array< HighlightedText
// Split hyphenated words into separate words as nspell doesn't work well with them
.map( word => word.split( '-' ) )
.flat();
const spellchecker = getSpellchecker();
const spellChecker = getSpellChecker();

if ( ! spellchecker ) {
if ( ! spellChecker ) {
return highlightedTexts;
}

Expand All @@ -122,7 +187,7 @@ export default function spellingMistakes( text: string ): Array< HighlightedText
words.forEach( ( word: string ) => {
const wordIndex = text.indexOf( word, searchStartIndex );

if ( ! spellchecker.correct( word ) ) {
if ( ! spellChecker.correct( word ) ) {
highlightedTexts.push( {
text: word,
startIndex: wordIndex,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/
import { fixes } from '@automattic/jetpack-ai-client';
import { useAnalytics } from '@automattic/jetpack-shared-extension-utils';
import { rawHandler, getBlockContent } from '@wordpress/blocks';
import { rawHandler, serialize } from '@wordpress/blocks';
import { Button, Popover, Spinner } from '@wordpress/components';
import { useDispatch, useSelect } from '@wordpress/data';
import { useState, useEffect } from '@wordpress/element';
Expand All @@ -18,7 +18,11 @@ import { AiSVG } from '../../ai-icon';
import { BREVE_FEATURE_NAME } from '../constants';
import features from '../features';
import { LONG_SENTENCES } from '../features/long-sentences';
import { SPELLING_MISTAKES, getSpellchecker } from '../features/spelling-mistakes';
import {
SPELLING_MISTAKES,
addTextToDictionary,
suggestSpellingFixes,
} from '../features/spelling-mistakes';
import getTargetText from '../utils/get-target-text';
import { numberToOrdinal } from '../utils/number-to-ordinal';
import replaceOccurrence from '../utils/replace-occurrence';
Expand Down Expand Up @@ -194,7 +198,8 @@ export default function Highlight() {

const { target, occurrence } = getTargetText( anchor as HTMLElement );

const html = getBlockContent( block );
// The serialize function returns the block's HTML with its Gutenberg comments
const html = serialize( block );
const fixedHtml = replaceOccurrence( {
text: html,
target,
Expand All @@ -221,12 +226,25 @@ export default function Highlight() {
const handleIgnoreSuggestion = () => {
ignoreSuggestion( blockId, id );
setPopoverHover( false );

tracks.recordEvent( 'jetpack_ai_breve_ignore', {
feature: BREVE_FEATURE_NAME,
type: feature,
} );
};

const handleAddToDictionary = () => {
const { target } = getTargetText( anchor as HTMLElement );
addTextToDictionary( target );

tracks.recordEvent( 'jetpack_ai_breve_add_to_dictionary', {
feature: BREVE_FEATURE_NAME,
type: feature,
word: target,
language: 'en',
} );
};

useEffect( () => {
if ( feature === SPELLING_MISTAKES.name && isPopoverOpen ) {
// Get the typo
Expand All @@ -236,11 +254,8 @@ export default function Highlight() {
return;
}

// Get the spellchecker
const spellchecker = getSpellchecker();

// Get the suggestions
setSpellingSuggestions( spellchecker?.suggest( typo ) ?? [] );
setSpellingSuggestions( suggestSpellingFixes( typo ) );
} else {
setSpellingSuggestions( [] );
}
Expand Down Expand Up @@ -307,24 +322,36 @@ export default function Highlight() {
{ suggestions?.suggestion }
</Button>
) }

{ feature === SPELLING_MISTAKES.name &&
spellingSuggestions.map( spellingSuggestion => (
<Button
variant="tertiary"
onClick={ () => handleApplySpellingFix( spellingSuggestion ) }
key={ spellingSuggestion }
className="jetpack-ai-breve__spelling-suggestion"
>
{ spellingSuggestion }
</Button>
) ) }

<div className="jetpack-ai-breve__helper">
{ hasSuggestions
? __( 'Click on the suggestion to insert it.', 'jetpack' )
: description }
<Button variant="link" onClick={ handleIgnoreSuggestion }>
{ __( 'Ignore', 'jetpack' ) }
</Button>
{ feature === SPELLING_MISTAKES.name && (
<Button variant="link" onClick={ handleAddToDictionary }>
{ __( 'Add to dictionary', 'jetpack' ) }
</Button>
) }

{ feature !== SPELLING_MISTAKES.name &&
( hasSuggestions
? __( 'Click on the suggestion to insert it.', 'jetpack' )
: description ) }

<div className="jetpack-ai-breve__helper-buttons-wrapper">
<Button variant="link" onClick={ handleIgnoreSuggestion }>
{ __( 'Ignore', 'jetpack' ) }
</Button>
</div>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,16 @@
border-top: 1px solid #dcdcde;
width: 100%;

.jetpack-ai-breve__spelling-suggestion {
& + .jetpack-ai-breve__spelling-suggestion {
border-top: 1px solid #dcdcde;
}

&.components-button.is-tertiary {
padding: 8px 12px;
}
}

.jetpack-ai-breve__helper {
padding: 4px 8px;
background-color: #f6f7f7;
Expand All @@ -104,6 +114,11 @@
align-items: center;
gap: 8px;

.jetpack-ai-breve__helper-buttons-wrapper {
display: flex;
gap: 16px;
}

.components-button {
padding: 0px;
color: #646970;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,16 @@ export function invalidateSingleSuggestion( feature: string, blockId: string, id
};
}

export function reloadDictionary( feature: string ) {
return ( { dispatch } ) => {
dispatch( setDictionaryLoading( feature, true ) );

setTimeout( () => {
dispatch( setDictionaryLoading( feature, false ) );
}, 100 );
};
}

export function setSuggestions( {
anchor,
id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export type BreveDispatch = {
setDictionaryLoading( feature: string, loading: boolean ): void;
invalidateSuggestions: ( blockId: string ) => void;
invalidateSingleSuggestion: ( feature: string, blockId: string, id: string ) => void;
reloadDictionary: ( feature: string ) => void;
ignoreSuggestion: ( blockId: string, id: string ) => void;
setBlockMd5: ( blockId: string, md5: string ) => void;
setSuggestions: ( suggestions: {
Expand Down Expand Up @@ -116,6 +117,7 @@ export type HighlightedText = {
export type SpellChecker = {
correct: ( word: string ) => boolean;
suggest: ( word: string ) => Array< string >;
add: ( word: string ) => void;
};

export type SpellingDictionaryContext = {
Expand Down

0 comments on commit 64d0535

Please sign in to comment.