diff --git a/projects/plugins/jetpack/changelog/update-jetpack-ai-breve-recompute-highlights b/projects/plugins/jetpack/changelog/update-jetpack-ai-breve-recompute-highlights new file mode 100644 index 0000000000000..c99d0ff14710f --- /dev/null +++ b/projects/plugins/jetpack/changelog/update-jetpack-ai-breve-recompute-highlights @@ -0,0 +1,4 @@ +Significance: patch +Type: other + +AI Assistant: Recompute Breve highlights when dictionary is loaded 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 index d24202ff1662d..1cc137adeb06f 100644 --- 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 @@ -1,6 +1,7 @@ /** * External dependencies */ +import { dispatch } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; import debugFactory from 'debug'; import nspell from 'nspell'; @@ -16,6 +17,7 @@ import type { SpellingDictionaryContext, HighlightedText, SpellChecker, + BreveDispatch, } from '../../types'; const debug = debugFactory( 'jetpack-ai-breve:spelling-mistakes' ); @@ -36,6 +38,10 @@ const contextRequests: { const fetchContext = async ( language: string ) => { debug( 'Fetching spelling context from the server' ); + const { setDictionaryLoading } = dispatch( 'jetpack/ai-breve' ) as BreveDispatch; + + setDictionaryLoading( SPELLING_MISTAKES.name, true ); + try { contextRequests[ language ] = { loading: true, loaded: false, failed: false }; const data = await getDictionary( SPELLING_MISTAKES.name, language ); @@ -51,6 +57,8 @@ const fetchContext = async ( language: string ) => { debug( 'Failed to fetch spelling context', error ); contextRequests[ language ] = { loading: false, loaded: false, failed: true }; // TODO: Handle retries + } finally { + setDictionaryLoading( SPELLING_MISTAKES.name, false ); } }; @@ -90,7 +98,7 @@ const getSpellchecker = ( { language = 'en' }: { language?: string } = {} ) => { return spellcheckers[ language ]; }; -export default function longSentences( text: string ): Array< HighlightedText > { +export default function spellingMistakes( 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 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 1380d10b5e28f..07bcd0178233f 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 @@ -4,19 +4,11 @@ import { fixes } from '@automattic/jetpack-ai-client'; import { useAnalytics } from '@automattic/jetpack-shared-extension-utils'; import { rawHandler } from '@wordpress/blocks'; -import { getBlockContent } from '@wordpress/blocks'; import { Button, Popover, Spinner } from '@wordpress/components'; -import { - dispatch as globalDispatch, - select as globalSelect, - useDispatch, - useSelect, -} from '@wordpress/data'; +import { useDispatch, useSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; import { reusableBlock as retry } from '@wordpress/icons'; -import { registerFormatType, removeFormat, RichTextValue } from '@wordpress/rich-text'; import clsx from 'clsx'; -import md5 from 'crypto-js/md5'; import React from 'react'; /** * Internal dependencies @@ -24,22 +16,17 @@ import React from 'react'; import { AiSVG } from '../../ai-icon'; 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'; import { numberToOrdinal } from '../utils/number-to-ordinal'; -import highlight from './highlight'; import './style.scss'; /** * Types */ import type { BreveDispatch, BreveSelect } from '../types'; import type { Block } from '@automattic/jetpack-ai-client'; -import type { WPFormat } from '@wordpress/rich-text/build-types/register-format-type'; -import type { RichTextFormatList } from '@wordpress/rich-text/build-types/types'; type CoreBlockEditorSelect = { getBlock: ( clientId: string ) => Block; @@ -295,89 +282,3 @@ export default function Highlight() { ); } - -export function registerBreveHighlights() { - features.forEach( feature => { - const { highlight: featureHighlight, config } = feature; - const { name, ...configSettings } = config; - const formatName = `jetpack/ai-proofread-${ name }`; - - const settings = { - name: formatName, - interactive: false, - edit: () => {}, - ...configSettings, - __experimentalGetPropsForEditableTreePreparation( _select, { blockClientId } ) { - const { getIgnoredSuggestions, isFeatureEnabled, isProofreadEnabled } = globalSelect( - 'jetpack/ai-breve' - ) as BreveSelect; - const { getAiAssistantFeature } = globalSelect( 'wordpress-com/plans' ); - const isFreePlan = getAiAssistantFeature().currentTier?.value === 0; - - return { - isProofreadEnabled: isProofreadEnabled() && getBreveAvailability( isFreePlan ), - isFeatureEnabled: isFeatureEnabled( config.name ), - ignored: getIgnoredSuggestions( { blockId: blockClientId } ), - }; - }, - __experimentalCreatePrepareEditableTree( - { isProofreadEnabled, isFeatureEnabled, ignored }, - { blockClientId, richTextIdentifier } - ) { - return ( formats: Array< RichTextFormatList >, text: string ) => { - const { getBlock } = globalSelect( 'core/block-editor' ) as CoreBlockEditorSelect; - const { getBlockMd5 } = globalSelect( 'jetpack/ai-breve' ) as BreveSelect; - const { invalidateSuggestions, setBlockMd5 } = globalDispatch( - 'jetpack/ai-breve' - ) as BreveDispatch; - - const record = { formats, text } as RichTextValue; - const type = formatName; - - // Ignored suggestions - let ignoredList = ignored; - - // Has to be defined here, as adding it to __experimentalGetPropsForEditableTreePreparation - // causes an issue with the block inserter. ref p1721746774569699-slack-C054LN8RNVA - const currentMd5 = getBlockMd5( blockClientId ); - - if ( text && isProofreadEnabled && isFeatureEnabled ) { - const block = getBlock( blockClientId ); - // Only use block content for complex blocks like tables - const blockContent = richTextIdentifier === 'content' ? text : getBlockContent( block ); - const textMd5 = md5( blockContent ).toString(); - - if ( currentMd5 !== textMd5 ) { - ignoredList = []; - invalidateSuggestions( blockClientId ); - setBlockMd5( blockClientId, textMd5 ); - } - - const highlights = featureHighlight( text ); - const applied = highlight( { - ignored: ignoredList, - content: record, - type, - indexes: highlights, - attributes: { - 'data-breve-type': config.name, - 'data-identifier': richTextIdentifier ?? 'none', - 'data-block': blockClientId, - }, - } ); - - setTimeout( () => { - registerEvents( blockClientId ); - }, 100 ); - - return applied.formats; - } - - return removeFormat( record, type, 0, record.text.length ).formats; - }; - }, - } as WPFormat; - - registerFormatType( formatName, settings ); - } ); -} diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/index.ts b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/index.ts index d5e594cd72e14..a7d2ace51948d 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/index.ts +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/index.ts @@ -11,5 +11,6 @@ import { BreveControls } from './types'; const Breve = Controls as BreveControls; export { Breve }; -export { default as Highlight, registerBreveHighlights } from './highlight'; +export { default as Highlight } from './highlight'; +export { registerBreveHighlights } from './utils/register-format'; export { store }; diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/store/actions.ts b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/store/actions.ts index 97ad5c5fb81ac..4e545cc946df3 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/store/actions.ts +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/store/actions.ts @@ -50,6 +50,14 @@ export function toggleFeature( feature: string, force?: boolean ) { }; } +export function setDictionaryLoading( feature: string, loading: boolean ) { + return { + type: 'SET_DICTIONARY_LOADING', + feature, + loading, + }; +} + export function setBlockMd5( blockId: string, md5: string ) { return { type: 'SET_BLOCK_MD5', diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/store/reducer.ts b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/store/reducer.ts index 31159e707c07a..1c97518ccdf51 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/store/reducer.ts +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/store/reducer.ts @@ -27,7 +27,7 @@ const initialConfiguration = { export function configuration( state: BreveState[ 'configuration' ] = initialConfiguration, - action: { type: string; enabled?: boolean; feature?: string } + action: { type: string; enabled?: boolean; feature?: string; loading?: boolean } ) { switch ( action.type ) { case 'SET_PROOFREAD_ENABLED': { @@ -65,6 +65,17 @@ export function configuration( disabled, }; } + + case 'SET_DICTIONARY_LOADING': { + const loading = action.loading + ? [ ...( state.loading ?? [] ), action.feature ] + : [ ...( state.loading ?? [] ) ].filter( feature => feature !== action.feature ); + + return { + ...state, + loading, + }; + } } return state; diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/store/selectors.ts b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/store/selectors.ts index b3aa92fb4b2c3..fcd481d8a3b4d 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/store/selectors.ts +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/store/selectors.ts @@ -3,7 +3,7 @@ */ import type { Anchor, BreveState } from '../types'; -// POPOVER +// Popover export function isHighlightHover( state: BreveState ) { return state.popover?.isHighlightHover; @@ -21,7 +21,7 @@ export function getPopoverLevel( state: BreveState ) { return state.popover?.level; } -// CONFIGURATION +// Configuration export function isProofreadEnabled( state: BreveState ) { return state.configuration?.enabled; @@ -31,6 +31,10 @@ export function isFeatureEnabled( state: BreveState, feature: string ) { return ! state.configuration?.disabled?.includes( feature ); } +export function isFeatureDictionaryLoading( state: BreveState, feature: string ) { + return state.configuration?.loading?.includes( feature ); +} + export function getDisabledFeatures( state: BreveState ) { return state.configuration?.disabled; } 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 836ac569ef990..f9e632a63389e 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 @@ -18,6 +18,7 @@ export type BreveState = { configuration?: { enabled?: boolean; disabled?: Array< string >; + loading?: Array< string >; }; suggestions?: { [ key: string ]: { @@ -41,6 +42,7 @@ export type BreveSelect = { getPopoverLevel: () => number; isProofreadEnabled: () => boolean; isFeatureEnabled: ( feature: string ) => boolean; + isFeatureDictionaryLoading: ( feature: string ) => boolean; getDisabledFeatures: () => Array< string >; getBlockMd5: ( blockId: string ) => string; getSuggestionsLoading: ( { @@ -73,6 +75,7 @@ export type BreveDispatch = { setPopoverAnchor: ( anchor: Anchor ) => void; toggleProofread: ( force?: boolean ) => void; toggleFeature: ( feature: string, force?: boolean ) => void; + setDictionaryLoading( feature: string, loading: boolean ): void; invalidateSuggestions: ( blockId: string ) => void; invalidateSingleSuggestion: ( feature: string, blockId: string, id: string ) => void; ignoreSuggestion: ( blockId: string, id: string ) => void; diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/utils/register-format.ts b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/utils/register-format.ts new file mode 100644 index 0000000000000..a36cf92a90ec4 --- /dev/null +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/utils/register-format.ts @@ -0,0 +1,131 @@ +/** + * External dependencies + */ +import { getBlockContent } from '@wordpress/blocks'; +import { dispatch, select } from '@wordpress/data'; +import { registerFormatType, removeFormat, RichTextValue } from '@wordpress/rich-text'; +import md5 from 'crypto-js/md5'; +/** + * Internal dependencies + */ +import features from '../features'; +import registerEvents from '../features/events'; +import highlight from '../highlight/highlight'; +import getBreveAvailability from '../utils/get-availability'; +/** + * Types + */ +import type { BreveDispatch, BreveFeature, BreveSelect } from '../types'; +import type { Block } from '@automattic/jetpack-ai-client'; +import type { WPFormat } from '@wordpress/rich-text/build-types/register-format-type'; +import type { RichTextFormatList } from '@wordpress/rich-text/build-types/types'; + +type CoreBlockEditorSelect = { + getBlock: ( clientId: string ) => Block; +}; + +export function getFormatName( featureName: string ) { + return `jetpack/ai-proofread-${ featureName }`; +} + +export function registerBreveHighlight( feature: BreveFeature ) { + if ( ! feature ) { + return; + } + + const { highlight: featureHighlight, config } = feature; + const { name, ...configSettings } = config; + const formatName = getFormatName( name ); + + const settings = { + name: formatName, + interactive: false, + + edit: () => {}, + ...configSettings, + + __experimentalGetPropsForEditableTreePreparation( _select, { blockClientId } ) { + const { + getIgnoredSuggestions, + isFeatureEnabled, + isProofreadEnabled, + isFeatureDictionaryLoading, + } = select( 'jetpack/ai-breve' ) as BreveSelect; + + const { getAiAssistantFeature } = select( 'wordpress-com/plans' ); + const isFreePlan = getAiAssistantFeature().currentTier?.value === 0; + + return { + isProofreadEnabled: isProofreadEnabled() && getBreveAvailability( isFreePlan ), + isFeatureEnabled: isFeatureEnabled( config.name ), + ignored: getIgnoredSuggestions( { blockId: blockClientId } ), + isFeatureDictionaryLoading: isFeatureDictionaryLoading( config.name ), + }; + }, + + __experimentalCreatePrepareEditableTree( + { isProofreadEnabled, isFeatureEnabled, ignored, isFeatureDictionaryLoading }, + { blockClientId, richTextIdentifier } + ) { + return ( formats: Array< RichTextFormatList >, text: string ) => { + const { getBlock } = select( 'core/block-editor' ) as CoreBlockEditorSelect; + const { getBlockMd5 } = select( 'jetpack/ai-breve' ) as BreveSelect; + const { invalidateSuggestions, setBlockMd5 } = dispatch( + 'jetpack/ai-breve' + ) as BreveDispatch; + + const record = { formats, text } as RichTextValue; + const type = formatName; + + // Ignored suggestions + let ignoredList = ignored; + + // Has to be defined here, as adding it to __experimentalGetPropsForEditableTreePreparation + // causes an issue with the block inserter. ref p1721746774569699-slack-C054LN8RNVA + const currentMd5 = getBlockMd5( blockClientId ); + + if ( text && isProofreadEnabled && isFeatureEnabled && ! isFeatureDictionaryLoading ) { + const block = getBlock( blockClientId ); + // Only use block content for complex blocks like tables + const blockContent = richTextIdentifier === 'content' ? text : getBlockContent( block ); + const textMd5 = md5( blockContent ).toString(); + + if ( currentMd5 !== textMd5 ) { + ignoredList = []; + invalidateSuggestions( blockClientId ); + setBlockMd5( blockClientId, textMd5 ); + } + + const highlights = featureHighlight( text ); + const applied = highlight( { + ignored: ignoredList, + content: record, + type, + indexes: highlights, + attributes: { + 'data-breve-type': config.name, + 'data-identifier': richTextIdentifier ?? 'none', + 'data-block': blockClientId, + }, + } ); + + setTimeout( () => { + registerEvents( blockClientId ); + }, 100 ); + + return applied.formats; + } + + return removeFormat( record, type, 0, record.text.length ).formats; + }; + }, + } as WPFormat; + + registerFormatType( formatName, settings ); +} + +export function registerBreveHighlights() { + features.forEach( feature => { + registerBreveHighlight( feature ); + } ); +}