diff --git a/projects/plugins/jetpack/changelog/add-proofread-ignore-button b/projects/plugins/jetpack/changelog/add-proofread-ignore-button new file mode 100644 index 0000000000000..0c6924aa76a8d --- /dev/null +++ b/projects/plugins/jetpack/changelog/add-proofread-ignore-button @@ -0,0 +1,4 @@ +Significance: minor +Type: other + +Add ignore button for AI Proofread 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 368c928eed4d4..de7662258558d 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 @@ -1,3 +1,7 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; /** * Features */ @@ -15,14 +19,17 @@ const features: Array< BreveFeature > = [ config: COMPLEX_WORDS, highlight: complexWords, dictionary: dicComplex, + description: __( 'Use simple, direct words.', 'jetpack' ), }, { config: LONG_SENTENCES, highlight: longSentences, + description: __( 'Long sentences are hard to read.', 'jetpack' ), }, { config: UNCONFIDENT_WORDS, highlight: unconfidentWords, + description: __( 'Remove weasel words.', 'jetpack' ), }, ]; diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/highlight/highlight.ts b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/highlight/highlight.ts index ce1a2060f71d7..52c032bad86ca 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/highlight/highlight.ts +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/highlight/highlight.ts @@ -14,6 +14,13 @@ export type HighlightProps = { type: string; indexes: Array< HighlightedText >; attributes?: { [ key: string ]: string }; + ignored: Array< string >; +}; + +type HighlightData = { + start: number; + end: number; + id: string; }; const applyHighlightFormat = ( { @@ -21,6 +28,7 @@ const applyHighlightFormat = ( { type, indexes, attributes = {}, + ignored = [], }: HighlightProps ): RichTextValue => { let newContent = content; @@ -28,10 +36,12 @@ const applyHighlightFormat = ( { newContent = indexes .map( highlightedText => { const { startIndex, endIndex, text } = highlightedText; - return { start: startIndex, end: endIndex, text } as RichTextValue; + const id = md5( `${ text }-${ startIndex }-${ endIndex }` ).toString(); + return { start: startIndex, end: endIndex, id } as HighlightData; } ) - .reduce( ( acc: RichTextValue, { start, end, text }: RichTextValue ) => { - const currentAttr = { ...attributes, 'data-id': md5( `${ text }-${ start }-${ end }` ) }; + .filter( data => ! ignored.includes( data?.id ) ) + .reduce( ( acc: RichTextValue, { start, end, id }: HighlightData ) => { + const currentAttr = { ...attributes, 'data-id': id }; const format = { type, @@ -45,6 +55,12 @@ const applyHighlightFormat = ( { return newContent; }; -export default function highlight( { content, type, indexes, attributes }: HighlightProps ) { - return applyHighlightFormat( { indexes, content, type, attributes } ); +export default function highlight( { + content, + type, + indexes, + attributes, + ignored, +}: HighlightProps ) { + return applyHighlightFormat( { indexes, content, type, attributes, ignored } ); } 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 923c174224142..9cf54fc0339b1 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 @@ -43,7 +43,7 @@ type CoreBlockEditorSelect = { // Setup the Breve highlights export default function Highlight() { - const { setPopoverHover, setSuggestions, invalidateSuggestions } = useDispatch( + const { setPopoverHover, setSuggestions, invalidateSuggestions, ignoreSuggestion } = useDispatch( 'jetpack/ai-breve' ) as BreveDispatch; @@ -55,54 +55,64 @@ export default function Highlight() { return { getBlock: selector.getBlock }; }, [] ); - const { anchor, virtual, popoverOpen, id, feature, blockId, title, loading, suggestions } = - useSelect( select => { - const breveSelect = select( 'jetpack/ai-breve' ) as BreveSelect; - - // Popover - const isPopoverHover = breveSelect.isPopoverHover(); - const isHighlightHover = breveSelect.isHighlightHover(); - - // Anchor data - const { target: anchorEl, virtual: virtualEl } = breveSelect.getPopoverAnchor() ?? { - target: null, - virtual: null, - }; - const anchorFeature = anchorEl?.getAttribute?.( 'data-type' ) as string; - const anchorId = anchorEl?.getAttribute?.( 'data-id' ) as string; - const anchorBlockId = anchorEl?.getAttribute?.( 'data-block' ) as string; - - const config = features?.find?.( ftr => ftr.config.name === anchorFeature )?.config ?? { - name: '', - title: '', - }; - - // Suggestions - const loadingSuggestions = breveSelect.getSuggestionsLoading( { - feature: anchorFeature, - id: anchorId, - blockId: anchorBlockId, - } ); + const { + anchor, + virtual, + popoverOpen, + id, + feature, + blockId, + title, + loading, + suggestions, + description, + } = useSelect( select => { + const breveSelect = select( 'jetpack/ai-breve' ) as BreveSelect; + + // Popover + const isPopoverHover = breveSelect.isPopoverHover(); + const isHighlightHover = breveSelect.isHighlightHover(); + + // Anchor data + const defaultAnchor = { target: null, virtual: null }; + const { target: anchorEl, virtual: virtualEl } = + breveSelect.getPopoverAnchor() ?? defaultAnchor; + const anchorFeature = anchorEl?.getAttribute?.( 'data-type' ) as string; + const anchorId = anchorEl?.getAttribute?.( 'data-id' ) as string; + const anchorBlockId = anchorEl?.getAttribute?.( 'data-block' ) as string; + + // Feature data + const featureData = features?.find?.( ftr => ftr.config.name === anchorFeature ); + const featureConfig = featureData?.config ?? { name: '', title: '' }; + const featureDescription = featureData?.description ?? ''; + const featureTitle = featureConfig?.title ?? ''; + + // Suggestions + const loadingSuggestions = breveSelect.getSuggestionsLoading( { + feature: anchorFeature, + id: anchorId, + blockId: anchorBlockId, + } ); - const suggestionsData = breveSelect.getSuggestions( { - feature: anchorFeature, - id: anchorId, - blockId: anchorBlockId, - } ); + const suggestionsData = breveSelect.getSuggestions( { + feature: anchorFeature, + id: anchorId, + blockId: anchorBlockId, + } ); - return { - config, - anchor: anchorEl, - virtual: virtualEl, - title: config?.title, - feature: anchorFeature, - id: anchorId, - blockId: anchorBlockId, - popoverOpen: isHighlightHover || isPopoverHover, - loading: loadingSuggestions, - suggestions: suggestionsData, - }; - }, [] ); + return { + title: featureTitle, + description: featureDescription, + anchor: anchorEl, + virtual: virtualEl, + feature: anchorFeature, + id: anchorId, + blockId: anchorBlockId, + popoverOpen: isHighlightHover || isPopoverHover, + loading: loadingSuggestions, + suggestions: suggestionsData, + }; + }, [] ); const isPopoverOpen = popoverOpen && virtual; const hasSuggestions = Boolean( suggestions?.suggestion ); @@ -187,6 +197,15 @@ export default function Highlight() { } ); }; + const handleIgnoreSuggestion = () => { + ignoreSuggestion( feature, blockId, id ); + setPopoverHover( false ); + tracks.recordEvent( 'jetpack_ai_breve_ignore', { + feature: BREVE_FEATURE_NAME, + type: feature, + } ); + }; + return ( <> { isPopoverOpen && ( @@ -205,32 +224,44 @@ export default function Highlight() { 'has-suggestions': hasSuggestions, } ) } > -
-
-
{ title }
+
+
+
+
{ title }
+
+ { ! hasSuggestions && ( +
+ { loading ? ( +
+ +
+ ) : ( + + ) } +
+ ) }
- { hasSuggestions ? ( -
+
+ { hasSuggestions && ( -
- { __( 'Click on the suggestion to insert it.', 'jetpack' ) } -
-
- ) : ( -
- { loading ? ( -
- -
+ ) } +
+ { hasSuggestions ? ( + __( 'Click on the suggestion to insert it.', 'jetpack' ) ) : ( - + <> + { description } + + ) }
- ) } +
) } @@ -249,51 +280,53 @@ export function registerBreveHighlights() { interactive: false, edit: () => {}, ...configSettings, - __experimentalGetPropsForEditableTreePreparation() { + __experimentalGetPropsForEditableTreePreparation( _select, { blockClientId } ) { + const { getIgnoredSuggestions, isFeatureEnabled, isProofreadEnabled } = globalSelect( + 'jetpack/ai-breve' + ) as BreveSelect; + return { - isProofreadEnabled: ( - globalSelect( 'jetpack/ai-breve' ) as BreveSelect - ).isProofreadEnabled(), - isFeatureEnabled: ( globalSelect( 'jetpack/ai-breve' ) as BreveSelect ).isFeatureEnabled( - config.name - ), + isProofreadEnabled: isProofreadEnabled(), + isFeatureEnabled: isFeatureEnabled( config.name ), + ignored: getIgnoredSuggestions( { feature: config.name, blockId: blockClientId } ), }; }, __experimentalCreatePrepareEditableTree( - { isProofreadEnabled, isFeatureEnabled }, + { 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 = ( globalSelect( 'jetpack/ai-breve' ) as BreveSelect ).getBlockMd5( - formatName, - blockClientId - ); + const currentMd5 = getBlockMd5( formatName, blockClientId ); if ( text && isProofreadEnabled && isFeatureEnabled ) { - const block = globalSelect( 'core/block-editor' ).getBlock( blockClientId ); - const blockContent = getBlockContent( block ); + 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 ) { - ( globalDispatch( 'jetpack/ai-breve' ) as BreveDispatch ).invalidateSuggestions( - type, - blockClientId - ); - - ( globalDispatch( 'jetpack/ai-breve' ) as BreveDispatch ).setBlockMd5( - type, - blockClientId, - textMd5 - ); + ignoredList = []; + invalidateSuggestions( type, blockClientId ); + setBlockMd5( type, blockClientId, textMd5 ); } const highlights = featureHighlight( text ); const applied = highlight( { + ignored: ignoredList, content: record, type, indexes: highlights, diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/highlight/style.scss b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/highlight/style.scss index 39b84c95806df..ec39a3bb92795 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/highlight/style.scss +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/breve/highlight/style.scss @@ -20,70 +20,86 @@ .highlight-content { align-items: center; display: flex; - justify-content: space-between; - gap: 32px; + flex-direction: column; &.has-suggestions { align-items: flex-start; - flex-direction: column; padding-top: 8px; gap: 8px; } - .title, - .action { - padding: 8px 12px; - } - - .title { + .header-container { display: flex; - align-items: center; - justify-content: flex-start; - gap: 8px; - font-size: 14px; - font-weight: 500; - white-space: nowrap; + width: 100%; + justify-content: space-between; + gap: 32px; - .color { - width: 10px; - height: 10px; - border-radius: 50%; - @include features-colors( ( 'background-color' ) ); + .title, + .action { + padding: 8px 12px; } - } - .action { - font-size: 13px; - display: flex; - align-items: center; - justify-content: flex-end; - gap: 4px; + .title { + display: flex; + align-items: center; + justify-content: flex-start; + gap: 8px; + font-size: 14px; + font-weight: 500; + white-space: nowrap; - .components-button { - padding-top: 0px; - padding-bottom: 0px; - padding-right: 0px; - height: unset; + .color { + width: 10px; + height: 10px; + border-radius: 50%; + @include features-colors( ( 'background-color' ) ); + } } - & > svg, - path { - fill: #0277a8; + .action { + font-size: 13px; + display: flex; + align-items: center; + justify-content: flex-end; + gap: 4px; + + .suggest.components-button { + padding-top: 0px; + padding-bottom: 0px; + padding-right: 0px; + height: unset; + + & > svg, + path { + fill: #0277a8; + } + } } } - .suggestion-container { + .bottom-container { display: flex; flex-direction: column; border-top: 1px solid #dcdcde; + width: 100%; .helper { - padding: 8px; + padding: 4px 8px; background-color: #f6f7f7; white-space: nowrap; color: #646970; - margin: 0 4px 4px 4px; + margin: 4px; border-radius: 4px; + font-size: 12px; + display: flex; + justify-content: space-between; + align-items: center; + gap: 8px; + + .components-button { + padding: 0px; + color: #646970; + } } .components-button.is-tertiary { 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 4f6de14523e49..bbe40db68d434 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 @@ -66,6 +66,15 @@ export function invalidateSuggestions( feature: string, blockId: string ) { }; } +export function ignoreSuggestion( feature: string, blockId: string, id: string ) { + return { + type: 'IGNORE_SUGGESTION', + feature, + blockId, + id, + }; +} + export function setSuggestions( { anchor, id, 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 1c96c035e18e1..922b91e9a9ae7 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 @@ -178,6 +178,19 @@ export function suggestions( }, }; } + + case 'IGNORE_SUGGESTION': { + return { + ...current, + [ feature ]: { + ...( current[ feature ] ?? {} ), + [ blockId ]: { + ...currentBlock, + ignored: [ ...( currentBlock.ignored ?? [] ), id ], + }, + }, + }; + } } 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 ade9a7fe14c37..3753f1cd89733 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 @@ -54,3 +54,10 @@ export function getSuggestions( ) { return state.suggestions?.[ feature ]?.[ blockId ]?.[ id ]?.suggestions; } + +export function getIgnoredSuggestions( + state: BreveState, + { feature, blockId }: { feature: string; blockId: string } +) { + return state.suggestions?.[ feature ]?.[ blockId ]?.ignored; +} 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 6e25ceeec4b9e..922b1d4807a80 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 @@ -64,6 +64,13 @@ export type BreveSelect = { html: string; suggestion: string; }; + getIgnoredSuggestions: ( { + feature, + blockId, + }: { + feature: string; + blockId: string; + } ) => Array< string >; }; export type BreveDispatch = { @@ -73,6 +80,7 @@ export type BreveDispatch = { toggleProofread: ( force?: boolean ) => void; toggleFeature: ( feature: string, force?: boolean ) => void; invalidateSuggestions: ( feature: string, blockId: string ) => void; + ignoreSuggestion: ( feature: string, blockId: string, id: string ) => void; setBlockMd5: ( feature: string, blockId: string, md5: string ) => void; setSuggestions: ( suggestions: { anchor: Anchor[ 'target' ]; @@ -97,6 +105,7 @@ export type BreveFeature = { config: BreveFeatureConfig; highlight: ( text: string ) => Array< HighlightedText >; dictionary?: { [ key: string ]: string }; + description: string; }; export type HighlightedText = {