Skip to content

Commit

Permalink
AI Proofread: Introduce ignore button and description (#38517)
Browse files Browse the repository at this point in the history
* AI Proofread: Add ignore button

* AI Proofread: Mark suggestion ignored on store

* AI Proofread: Disable highlight if suggestion was ignored

* changelog

* AI Proofread: Update interface for ignore button

* AI Proofread: Add gap in helper container
  • Loading branch information
renatoagds authored Jul 30, 2024
1 parent e0b81a3 commit ffd27b8
Show file tree
Hide file tree
Showing 9 changed files with 247 additions and 133 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: other

Add ignore button for AI Proofread
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
/**
* Features
*/
Expand All @@ -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' ),
},
];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,34 @@ 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 = ( {
content,
type,
indexes,
attributes = {},
ignored = [],
}: HighlightProps ): RichTextValue => {
let newContent = content;

if ( indexes.length > 0 ) {
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,
Expand All @@ -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 } );
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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 );
Expand Down Expand Up @@ -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 && (
Expand All @@ -205,32 +224,44 @@ export default function Highlight() {
'has-suggestions': hasSuggestions,
} ) }
>
<div className="title">
<div className="color" data-type={ feature } />
<div>{ title }</div>
<div className="header-container">
<div className="title">
<div className="color" data-type={ feature } />
<div>{ title }</div>
</div>
{ ! hasSuggestions && (
<div className="action">
{ loading ? (
<div className="loading">
<Spinner />
</div>
) : (
<Button className="suggest" icon={ AiSVG } onClick={ handleSuggestions }>
{ __( 'Suggest', 'jetpack' ) }
</Button>
) }
</div>
) }
</div>
{ hasSuggestions ? (
<div className="suggestion-container">
<div className="bottom-container">
{ hasSuggestions && (
<Button variant="tertiary" onClick={ handleApplySuggestion }>
{ suggestions?.suggestion }
</Button>
<div className="helper">
{ __( 'Click on the suggestion to insert it.', 'jetpack' ) }
</div>
</div>
) : (
<div className="action">
{ loading ? (
<div className="loading">
<Spinner />
</div>
) }
<div className="helper">
{ hasSuggestions ? (
__( 'Click on the suggestion to insert it.', 'jetpack' )
) : (
<Button icon={ AiSVG } onClick={ handleSuggestions }>
{ __( 'Suggest', 'jetpack' ) }
</Button>
<>
{ description }
<Button variant="link" onClick={ handleIgnoreSuggestion }>
{ __( 'Dismiss', 'jetpack' ) }
</Button>
</>
) }
</div>
) }
</div>
</div>
</Popover>
) }
Expand All @@ -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,
Expand Down
Loading

0 comments on commit ffd27b8

Please sign in to comment.