initial
This commit is contained in:
+73
@@ -0,0 +1,73 @@
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { useCallback, useEffect, useState } from '@wordpress/element';
|
||||
import { acceptBlockSuggestion } from '../lib/dom';
|
||||
import { recordGuidelinesEvent } from '../lib/tracks';
|
||||
import { AI_STORE_NAME } from '../store';
|
||||
import DiffView from './diff-view';
|
||||
|
||||
// Renders only the diff view. Accept/Dismiss and Improve buttons live in
|
||||
// BlockSuggestionButtons, injected as a separate row above the modal action bar.
|
||||
export default function BlockSuggestionActions( { blockName, blockModal } ) {
|
||||
const suggestion = useSelect(
|
||||
select => select( AI_STORE_NAME ).getSuggestion( blockName ),
|
||||
[ blockName ]
|
||||
);
|
||||
const blockLoading = useSelect(
|
||||
select => select( AI_STORE_NAME ).isSectionLoading( blockName ),
|
||||
[ blockName ]
|
||||
);
|
||||
const { clearSuggestion } = useDispatch( AI_STORE_NAME );
|
||||
|
||||
const [ original, setOriginal ] = useState( '' );
|
||||
const [ textareaHeight, setTextareaHeight ] = useState( null );
|
||||
|
||||
// Clear stale suggestion when the modal closes (component unmounts).
|
||||
useEffect( () => {
|
||||
return () => clearSuggestion( blockName );
|
||||
}, [ blockName, clearSuggestion ] );
|
||||
|
||||
// Toggle shimmer and suggestion classes on the modal.
|
||||
useEffect( () => {
|
||||
if ( ! blockModal ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Capture textarea content and height before hiding it.
|
||||
if ( suggestion && ! blockModal.classList.contains( 'has-jetpack-suggestion' ) ) {
|
||||
const textarea = blockModal.querySelector( '.components-textarea-control__input' );
|
||||
if ( textarea ) {
|
||||
setOriginal( textarea.value || '' );
|
||||
if ( textarea.offsetHeight > 0 ) {
|
||||
setTextareaHeight( textarea.offsetHeight );
|
||||
} else {
|
||||
const rows = parseInt( textarea.getAttribute( 'rows' ), 10 ) || 6;
|
||||
setTextareaHeight( rows * 20 + 20 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
blockModal.classList.toggle( 'has-jetpack-suggestion', !! suggestion );
|
||||
blockModal.classList.toggle( 'is-jetpack-loading', blockLoading && ! suggestion );
|
||||
return () => {
|
||||
blockModal.classList.remove( 'has-jetpack-suggestion', 'is-jetpack-loading' );
|
||||
};
|
||||
}, [ blockModal, suggestion, blockLoading ] );
|
||||
|
||||
const handleAccept = useCallback( () => {
|
||||
recordGuidelinesEvent( 'accept', { type: 'block', slug: blockName } );
|
||||
acceptBlockSuggestion( blockModal, blockName, suggestion, clearSuggestion );
|
||||
}, [ blockModal, blockName, suggestion, clearSuggestion ] );
|
||||
|
||||
if ( ! suggestion ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DiffView
|
||||
original={ original }
|
||||
suggestion={ suggestion }
|
||||
onAccept={ handleAccept }
|
||||
height={ textareaHeight }
|
||||
/>
|
||||
);
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
import { useAiFeature } from '@automattic/jetpack-ai-client';
|
||||
import { Button } from '@wordpress/components';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { useCallback } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { store as noticesStore } from '@wordpress/notices';
|
||||
import { STORE_NAME } from '../constants';
|
||||
import { suggestGuidelines } from '../lib/api';
|
||||
import { acceptBlockSuggestion } from '../lib/dom';
|
||||
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 blockLoading = useSelect(
|
||||
select => select( AI_STORE_NAME ).isSectionLoading( blockName ),
|
||||
[ blockName ]
|
||||
);
|
||||
|
||||
const suggestion = useSelect(
|
||||
select => select( AI_STORE_NAME ).getSuggestion( blockName ),
|
||||
[ blockName ]
|
||||
);
|
||||
|
||||
const saved = useSelect(
|
||||
select => select( STORE_NAME ).getBlockGuideline( blockName ),
|
||||
[ blockName ]
|
||||
);
|
||||
|
||||
const handleGenerate = useCallback( async () => {
|
||||
const action = saved ? 'improve' : 'generate';
|
||||
recordGuidelinesEvent( 'generate', { type: 'block', slug: blockName, action } );
|
||||
|
||||
const textarea = blockModal?.querySelector( '.components-textarea-control__input' );
|
||||
const currentText = textarea?.value || '';
|
||||
|
||||
startSectionLoading( blockName );
|
||||
try {
|
||||
const existingContent = currentText ? { [ blockName ]: currentText } : {};
|
||||
const response = await suggestGuidelines( [ blockName ], existingContent );
|
||||
const text = response?.suggestions?.[ blockName ];
|
||||
if ( ! 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,
|
||||
saved,
|
||||
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 = saved ? improveLabel : generateLabel;
|
||||
|
||||
return (
|
||||
<div className="jetpack-content-guidelines-ai__suggestion-actions">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={ handleGenerate }
|
||||
disabled={ blockLoading || ! hasFeature }
|
||||
accessibleWhenDisabled
|
||||
className="jetpack-content-guidelines-ai__section-generate-button"
|
||||
>
|
||||
{ label }
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { useCallback, useMemo } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { diffWords } from 'diff';
|
||||
|
||||
export default function DiffView( { original, suggestion, onAccept, height } ) {
|
||||
const diff = useMemo( () => {
|
||||
if ( ! suggestion ) {
|
||||
return [];
|
||||
}
|
||||
return diffWords( original, suggestion );
|
||||
}, [ original, suggestion ] );
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
e => {
|
||||
if ( e.key === 'Enter' || e.key === ' ' ) {
|
||||
e.preventDefault();
|
||||
onAccept();
|
||||
}
|
||||
},
|
||||
[ onAccept ]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="jetpack-content-guidelines-ai__diff"
|
||||
style={ height ? { height } : undefined }
|
||||
role="button"
|
||||
tabIndex={ 0 }
|
||||
aria-label={ __( 'Click to accept suggested changes', 'jetpack' ) }
|
||||
onClick={ onAccept }
|
||||
onKeyDown={ handleKeyDown }
|
||||
>
|
||||
<span className="screen-reader-text">
|
||||
{ __( 'Changes from current to suggested guidelines:', 'jetpack' ) }
|
||||
</span>
|
||||
{ diff.map( ( part, i ) => {
|
||||
if ( part.added ) {
|
||||
return (
|
||||
<ins key={ i } className="jetpack-content-guidelines-ai__diff-added">
|
||||
{ part.value }
|
||||
</ins>
|
||||
);
|
||||
}
|
||||
if ( part.removed ) {
|
||||
return (
|
||||
<del key={ i } className="jetpack-content-guidelines-ai__diff-removed">
|
||||
{ part.value }
|
||||
</del>
|
||||
);
|
||||
}
|
||||
return <span key={ i }>{ part.value }</span>;
|
||||
} ) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
import { useAiFeature } from '@automattic/jetpack-ai-client';
|
||||
import { Button } from '@wordpress/components';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { useCallback } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { closeSmall } from '@wordpress/icons';
|
||||
import useGenerateAll from '../hooks/use-generate-all';
|
||||
import { AI_STORE_NAME } from '../store';
|
||||
|
||||
export default function EmptyStateBanner() {
|
||||
const { generate } = useGenerateAll();
|
||||
const { hasFeature } = useAiFeature();
|
||||
const { dismissBanner } = useDispatch( AI_STORE_NAME );
|
||||
|
||||
const dismissed = useSelect( select => select( AI_STORE_NAME ).isBannerDismissed(), [] );
|
||||
|
||||
const handleDismiss = useCallback( () => {
|
||||
dismissBanner();
|
||||
}, [ dismissBanner ] );
|
||||
|
||||
const handleGetStarted = useCallback( () => {
|
||||
dismissBanner();
|
||||
generate();
|
||||
}, [ dismissBanner, generate ] );
|
||||
|
||||
if ( dismissed || ! hasFeature ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="jetpack-content-guidelines-ai__banner">
|
||||
<div className="jetpack-content-guidelines-ai__banner-content">
|
||||
<h2>{ __( 'Generate your guidelines in seconds', 'jetpack' ) }</h2>
|
||||
<p>
|
||||
{ __(
|
||||
'Use Jetpack to analyze your site and create draft guidelines based on your actual content.',
|
||||
'jetpack'
|
||||
) }
|
||||
</p>
|
||||
<div className="jetpack-content-guidelines-ai__banner-actions">
|
||||
<Button
|
||||
className="jetpack-content-guidelines-ai__banner-cta"
|
||||
variant="primary"
|
||||
onClick={ handleGetStarted }
|
||||
>
|
||||
{ __( 'Get started', 'jetpack' ) }
|
||||
</Button>
|
||||
<Button
|
||||
className="jetpack-content-guidelines-ai__banner-close-text"
|
||||
variant="tertiary"
|
||||
onClick={ handleDismiss }
|
||||
>
|
||||
{ __( 'Close', 'jetpack' ) }
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
className="jetpack-content-guidelines-ai__banner-close"
|
||||
icon={ closeSmall }
|
||||
label={ __( 'Dismiss banner', 'jetpack' ) }
|
||||
size="small"
|
||||
onClick={ handleDismiss }
|
||||
/>
|
||||
<div className="jetpack-content-guidelines-ai__banner-orb jetpack-content-guidelines-ai__banner-orb--top" />
|
||||
<div className="jetpack-content-guidelines-ai__banner-orb jetpack-content-guidelines-ai__banner-orb--bottom" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
import { useAiFeature } from '@automattic/jetpack-ai-client';
|
||||
import { Button } from '@wordpress/components';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { useCallback } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { store as noticesStore } from '@wordpress/notices';
|
||||
import { STORE_NAME } from '../constants';
|
||||
import { suggestGuidelines } from '../lib/api';
|
||||
import { recordGuidelinesEvent } from '../lib/tracks';
|
||||
import { AI_STORE_NAME } from '../store';
|
||||
|
||||
export default function SectionGenerateButton( { slug } ) {
|
||||
const { createErrorNotice } = useDispatch( noticesStore );
|
||||
const { startSectionLoading, stopSectionLoading, setSuggestion } = useDispatch( AI_STORE_NAME );
|
||||
const { hasFeature } = useAiFeature();
|
||||
|
||||
const sectionLoading = useSelect(
|
||||
select => select( AI_STORE_NAME ).isSectionLoading( slug ),
|
||||
[ slug ]
|
||||
);
|
||||
const draft = useSelect( select => select( STORE_NAME ).getGuideline( slug ), [ slug ] );
|
||||
|
||||
const isEmpty = ! draft;
|
||||
const generateLabel = __( 'Generate guidelines', 'jetpack' );
|
||||
const improveLabel = __( 'Improve guidelines', 'jetpack' );
|
||||
const label = isEmpty ? generateLabel : improveLabel;
|
||||
|
||||
const handleClick = useCallback( async () => {
|
||||
const action = isEmpty ? 'generate' : 'improve';
|
||||
recordGuidelinesEvent( 'generate', { type: 'section', slug, action } );
|
||||
|
||||
startSectionLoading( slug );
|
||||
try {
|
||||
const existingContent = draft ? { [ slug ]: draft } : {};
|
||||
const response = await suggestGuidelines( [ slug ], existingContent );
|
||||
const suggestion = response?.suggestions?.[ slug ];
|
||||
if ( ! suggestion ) {
|
||||
throw new Error( 'No suggestion returned.' );
|
||||
}
|
||||
setSuggestion( slug, suggestion );
|
||||
} catch {
|
||||
createErrorNotice( __( 'Failed to generate guidelines. Please try again.', 'jetpack' ), {
|
||||
type: 'snackbar',
|
||||
} );
|
||||
} finally {
|
||||
stopSectionLoading( slug );
|
||||
}
|
||||
}, [
|
||||
slug,
|
||||
draft,
|
||||
isEmpty,
|
||||
startSectionLoading,
|
||||
stopSectionLoading,
|
||||
setSuggestion,
|
||||
createErrorNotice,
|
||||
] );
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="tertiary"
|
||||
onClick={ handleClick }
|
||||
disabled={ sectionLoading || ! hasFeature }
|
||||
accessibleWhenDisabled
|
||||
className="jetpack-content-guidelines-ai__section-generate-button"
|
||||
>
|
||||
{ label }
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import { JetpackLogo } from '@automattic/jetpack-components';
|
||||
import { Button } from '@wordpress/components';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { STORE_NAME, VALID_SECTIONS } from '../constants';
|
||||
import useGenerateAll from '../hooks/use-generate-all';
|
||||
import { AI_STORE_NAME } from '../store';
|
||||
|
||||
export default function SuggestAllButton() {
|
||||
const { generate, loading, hasFeature } = useGenerateAll();
|
||||
|
||||
const bannerDismissed = useSelect( select => select( AI_STORE_NAME ).isBannerDismissed(), [] );
|
||||
|
||||
const allGuidelines = useSelect( select => {
|
||||
const store = select( STORE_NAME );
|
||||
return Object.fromEntries( VALID_SECTIONS.map( slug => [ slug, store.getGuideline( slug ) ] ) );
|
||||
}, [] );
|
||||
|
||||
const allEmpty = VALID_SECTIONS.every( slug => ! allGuidelines[ slug ] );
|
||||
|
||||
const generateLabel = __( 'Generate guidelines', 'jetpack' );
|
||||
const improveLabel = __( 'Improve guidelines', 'jetpack' );
|
||||
const label = allEmpty ? generateLabel : improveLabel;
|
||||
|
||||
// Hide when the banner is visible (not yet dismissed) or user lacks AI plan.
|
||||
const hidden = ! bannerDismissed || ! hasFeature;
|
||||
const hiddenProps = hidden ? { style: { display: 'none' }, 'aria-hidden': true } : {};
|
||||
|
||||
return (
|
||||
<Button
|
||||
{ ...hiddenProps }
|
||||
variant="primary"
|
||||
icon={ <JetpackLogo showText={ false } height={ 18 } logoColor="#fff" /> }
|
||||
onClick={ generate }
|
||||
disabled={ loading || ! hasFeature }
|
||||
accessibleWhenDisabled
|
||||
isBusy={ loading }
|
||||
className="jetpack-content-guidelines-ai__suggest-all-button"
|
||||
>
|
||||
{ label }
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
import { Button } from '@wordpress/components';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { useCallback, useEffect, useState } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { STORE_NAME } from '../constants';
|
||||
import { recordGuidelinesEvent } from '../lib/tracks';
|
||||
import { AI_STORE_NAME } from '../store';
|
||||
import DiffView from './diff-view';
|
||||
|
||||
export default function SuggestionActions( { slug } ) {
|
||||
const suggestion = useSelect( select => select( AI_STORE_NAME ).getSuggestion( slug ), [ slug ] );
|
||||
const sectionLoading = useSelect(
|
||||
select => select( AI_STORE_NAME ).isSectionLoading( slug ),
|
||||
[ slug ]
|
||||
);
|
||||
const { clearSuggestion } = useDispatch( AI_STORE_NAME );
|
||||
const { setGuideline } = useDispatch( STORE_NAME );
|
||||
|
||||
const [ original, setOriginal ] = useState( '' );
|
||||
const [ textareaHeight, setTextareaHeight ] = useState( null );
|
||||
|
||||
// Direct DOM class manipulation is necessary because this component is rendered in
|
||||
// a separate React root injected into Gutenberg's page — we can't control classes
|
||||
// on Gutenberg-owned elements through React props.
|
||||
useEffect( () => {
|
||||
const item = document.querySelector( `.guidelines__list-item[data-slug="${ slug }"]` );
|
||||
const form = item?.querySelector( 'form' );
|
||||
if ( ! form ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Capture textarea draft and height before hiding it.
|
||||
if ( suggestion && ! form.classList.contains( 'has-jetpack-suggestion' ) ) {
|
||||
const textarea = form.querySelector( 'textarea' );
|
||||
if ( textarea ) {
|
||||
setOriginal( textarea.value || '' );
|
||||
if ( textarea.offsetHeight > 0 ) {
|
||||
setTextareaHeight( textarea.offsetHeight );
|
||||
} else {
|
||||
// Fallback when textarea is hidden (e.g. collapsed accordion).
|
||||
// Compute height from rows attribute to match the textarea.
|
||||
const rows = parseInt( textarea.getAttribute( 'rows' ), 10 ) || 4;
|
||||
// line-height: 20px, padding: 9px top + 9px bottom, border: 1px + 1px.
|
||||
setTextareaHeight( rows * 20 + 20 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
form.classList.toggle( 'has-jetpack-suggestion', !! suggestion );
|
||||
form.classList.toggle( 'is-jetpack-loading', sectionLoading && ! suggestion );
|
||||
return () => {
|
||||
form.classList.remove( 'has-jetpack-suggestion', 'is-jetpack-loading' );
|
||||
};
|
||||
}, [ slug, suggestion, sectionLoading ] );
|
||||
|
||||
const handleAccept = useCallback( () => {
|
||||
recordGuidelinesEvent( 'accept', { type: 'section', slug } );
|
||||
setGuideline( slug, suggestion );
|
||||
clearSuggestion( slug );
|
||||
}, [ slug, suggestion, setGuideline, clearSuggestion ] );
|
||||
|
||||
const handleDismiss = useCallback( () => {
|
||||
recordGuidelinesEvent( 'dismiss', { type: 'section', slug } );
|
||||
clearSuggestion( slug );
|
||||
}, [ slug, clearSuggestion ] );
|
||||
|
||||
if ( ! suggestion ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="jetpack-content-guidelines-ai__suggestion">
|
||||
<DiffView
|
||||
original={ original }
|
||||
suggestion={ suggestion }
|
||||
onAccept={ handleAccept }
|
||||
height={ textareaHeight }
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Spinner } from '@wordpress/components';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Badge } from '@wordpress/ui';
|
||||
import { AI_STORE_NAME } from '../store';
|
||||
|
||||
export default function SuggestionBadge( { slug } ) {
|
||||
const sectionLoading = useSelect(
|
||||
select => select( AI_STORE_NAME ).isSectionLoading( slug ),
|
||||
[ slug ]
|
||||
);
|
||||
const hasSuggestion = useSelect(
|
||||
select => select( AI_STORE_NAME ).hasSuggestion( slug ),
|
||||
[ slug ]
|
||||
);
|
||||
|
||||
if ( sectionLoading && ! hasSuggestion ) {
|
||||
return (
|
||||
<span className="jetpack-content-guidelines-ai__badge--loading">
|
||||
<Spinner />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if ( hasSuggestion ) {
|
||||
return <Badge intent="stable">{ __( 'Suggestion', 'jetpack' ) }</Badge>;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useAICheckout, useAiFeature } from '@automattic/jetpack-ai-client';
|
||||
import { Button, Notice } from '@wordpress/components';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { useCallback } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { recordAiEvent } from '../lib/tracks';
|
||||
import { AI_STORE_NAME } from '../store';
|
||||
|
||||
export default function UpgradeNotice() {
|
||||
const { hasFeature } = useAiFeature();
|
||||
const { checkoutUrl } = useAICheckout();
|
||||
const { dismissBanner } = useDispatch( AI_STORE_NAME );
|
||||
const dismissed = useSelect( select => select( AI_STORE_NAME ).isBannerDismissed(), [] );
|
||||
|
||||
const handleUpgradeClick = useCallback( () => {
|
||||
// Record the click only. Dismissal is persisted by the close button
|
||||
// ( onRemove ), so the nudge returns if checkout is opened and abandoned.
|
||||
recordAiEvent( 'jetpack_ai_upgrade_button', {
|
||||
placement: 'content-guidelines',
|
||||
} );
|
||||
}, [] );
|
||||
|
||||
if ( hasFeature || dismissed ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Notice
|
||||
status="success"
|
||||
isDismissible
|
||||
onRemove={ dismissBanner }
|
||||
className="jetpack-content-guidelines-ai__upgrade-notice"
|
||||
>
|
||||
<p>
|
||||
{ __(
|
||||
'Not sure where to start? Jetpack can read your site and suggest guidelines tailored to your content. Upgrade to get started.',
|
||||
'jetpack'
|
||||
) }
|
||||
</p>
|
||||
{ checkoutUrl && (
|
||||
<Button
|
||||
variant="primary"
|
||||
href={ checkoutUrl }
|
||||
target="_blank"
|
||||
onClick={ handleUpgradeClick }
|
||||
>
|
||||
{ __( 'Upgrade', 'jetpack' ) }
|
||||
</Button>
|
||||
) }
|
||||
</Notice>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user