Back

97.22% Statements 35/36
95% Branches 19/20
100% Functions 6/6
97.22% Lines 35/36

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129                                7x   7x 7x 7x   7x 7x       7x 7x           7x   7x   3x 3x 3x   3x 3x 3x 3x 2x 2x     2x   1x       3x                     7x 1x 1x     7x 1x 1x     7x 1x                       6x 6x 6x           6x                         6x                                  
import { useAiFeature } from '@automattic/jetpack-ai-client';
import { Button } from '@wordpress/components';
import { useDispatch, useSelect } from '@wordpress/data';
import { useCallback, useId } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { lock } from '@wordpress/icons';
import { store as noticesStore } from '@wordpress/notices';
import { Tooltip, VisuallyHidden } from '@wordpress/ui';
import { useBlockHasDraft } from '../hooks/use-drafts';
import { suggestGuidelines } from '../lib/api';
import { acceptBlockSuggestion } from '../lib/dom';
import { getBlockModalTextarea } from '../lib/drafts';
import { recordGuidelinesEvent } from '../lib/tracks';
import { AI_STORE_NAME } from '../store';
 
export default function BlockSuggestionButtons( { blockName, blockModal } ) {
	const { createErrorNotice } = useDispatch( noticesStore );
	const { startSectionLoading, stopSectionLoading, setSuggestion, clearSuggestion } =
		useDispatch( AI_STORE_NAME );
	const { hasFeature } = useAiFeature();
	const descriptionId = useId();
 
	const blockLoading = useSelect(
		select => select( AI_STORE_NAME ).isSectionLoading( blockName ),
		[ blockName ]
	);
 
	const suggestion = useSelect(
		select => select( AI_STORE_NAME ).getSuggestion( blockName ),
		[ blockName ]
	);
 
	// The modal textarea opens prefilled with the saved guideline, so a
	// non-empty draft is what "a guideline exists" looks like from here.
	const hasDraft = useBlockHasDraft( blockModal );
 
	const handleGenerate = useCallback( async () => {
		// Snapshot at click time; the render-time hook value drives the label.
		const currentText = getBlockModalTextarea( blockModal )?.value || '';
		const action = currentText ? 'improve' : 'generate';
		recordGuidelinesEvent( 'generate', { type: 'block', slug: blockName, action } );
 
		startSectionLoading( blockName );
		try {
			const existingContent = currentText ? { [ blockName ]: currentText } : {};
			const response = await suggestGuidelines( [ blockName ], existingContent );
			const text = response?.suggestions?.[ blockName ];
			Iif ( ! text ) {
				throw new Error( 'No suggestion returned.' );
			}
			setSuggestion( blockName, text );
		} catch {
			createErrorNotice( __( 'Failed to generate guidelines. Please try again.', 'jetpack' ), {
				type: 'snackbar',
			} );
		} finally {
			stopSectionLoading( blockName );
		}
	}, [
		blockModal,
		blockName,
		startSectionLoading,
		stopSectionLoading,
		setSuggestion,
		createErrorNotice,
	] );
 
	const handleAccept = useCallback( () => {
		recordGuidelinesEvent( 'accept', { type: 'block', slug: blockName } );
		acceptBlockSuggestion( blockModal, blockName, suggestion, clearSuggestion );
	}, [ blockModal, blockName, suggestion, clearSuggestion ] );
 
	const handleDismiss = useCallback( () => {
		recordGuidelinesEvent( 'dismiss', { type: 'block', slug: blockName } );
		clearSuggestion( blockName );
	}, [ blockName, clearSuggestion ] );
 
	if ( suggestion ) {
		return (
			<div className="jetpack-content-guidelines-ai__suggestion-actions">
				<Button variant="primary" onClick={ handleAccept }>
					{ __( 'Accept suggestion', 'jetpack' ) }
				</Button>
				<Button variant="tertiary" onClick={ handleDismiss }>
					{ __( 'Dismiss', 'jetpack' ) }
				</Button>
			</div>
		);
	}
 
	const generateLabel = __( 'Generate guidelines', 'jetpack' );
	const improveLabel = __( 'Improve guidelines', 'jetpack' );
	const label = hasDraft ? improveLabel : generateLabel;
 
	// Locked look without an AI plan, but no click action: the upgrade notice
	// renders on the page behind this modal, so a click-to-nudge here would be
	// invisible. The lock icon and tooltip still explain the state.
	const button = (
		<Button
			variant="secondary"
			icon={ hasFeature ? undefined : lock }
			onClick={ handleGenerate }
			disabled={ blockLoading || ! hasFeature }
			accessibleWhenDisabled
			aria-describedby={ hasFeature ? undefined : descriptionId }
			className="jetpack-content-guidelines-ai__section-generate-button"
		>
			{ label }
		</Button>
	);
 
	return (
		<div className="jetpack-content-guidelines-ai__suggestion-actions">
			{ ! hasFeature ? (
				<Tooltip.Root>
					<Tooltip.Trigger render={ button } />
					{ /* The popup is visual-only, so this carries the sentence to screen readers. */ }
					<VisuallyHidden id={ descriptionId } render={ <span /> }>
						{ __( 'Upgrade to unlock the AI assistant', 'jetpack' ) }
					</VisuallyHidden>
					<Tooltip.Popup>{ __( 'Upgrade to unlock the AI assistant', 'jetpack' ) }</Tooltip.Popup>
				</Tooltip.Root>
			) : (
				button
			) }
		</div>
	);
}