Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
111 commits
Select commit Hold shift + click to select a range
3e19659
Jetpack AI: Add suggest-guidelines proxy endpoint (FORNO-186)
aagam-shah Mar 5, 2026
e9f9eb6
Add content filters support (authors, categories) to suggest-guidelin…
aagam-shah Mar 5, 2026
fb20ab7
Add "Generate with Jetpack" button to Content Guidelines admin page
aagam-shah Mar 13, 2026
676fb4f
Refine Generate with Jetpack button: availability checks and upgrade UX
aagam-shah Mar 17, 2026
e746d2e
Add @since annotation and gate admin-only include behind is_admin()
aagam-shah Mar 17, 2026
6abbb9a
Fix hook suffix to match Gutenberg's registered page slug
aagam-shah Apr 6, 2026
e62ff96
Refactor content-guidelines-ai into component structure
aagam-shah Apr 6, 2026
9cf2b26
Add 'Suggest All' button in Guidelines page header
aagam-shah Apr 6, 2026
f22ec91
Add empty state banner for guideline generation
aagam-shah Apr 6, 2026
329f1c2
Add styles for content guidelines AI components
aagam-shah Apr 6, 2026
4126425
Update changelog for content guidelines AI feature
aagam-shah Apr 6, 2026
24f67d7
Fix header button positioning
aagam-shah Apr 6, 2026
ce21431
Remove per-section generate buttons
aagam-shah Apr 6, 2026
815df29
Clean up PHP docblock
aagam-shah Apr 6, 2026
9f8130c
Add suggestion preview with Accept/Dismiss flow
aagam-shah Apr 6, 2026
f0939cc
Align with exploration-b prototype patterns
aagam-shah Apr 6, 2026
f959b13
Code review fixes and rich empty state banner
aagam-shah Apr 6, 2026
8d16b09
Fix CSS filename — build outputs .css not .min.css
aagam-shah Apr 6, 2026
12dbf80
Use inline Jetpack icon SVG with white fill
aagam-shah Apr 6, 2026
9060c02
Use shared JetpackLogo from jetpack-shared-extension-utils
aagam-shah Apr 6, 2026
16f4b4d
Polish UI: shimmer effect, badge, banner, button visibility
aagam-shah Apr 6, 2026
9c995c2
Use CSS logical properties for RTL support
aagam-shah Apr 6, 2026
db58e49
Add word-level diff view for suggestion review
aagam-shah Apr 6, 2026
7a2f879
Use static import for @wordpress/api-fetch
aagam-shah Apr 6, 2026
5ac59cd
Extract useGenerateAll hook to deduplicate batch generation
aagam-shah Apr 6, 2026
585f805
Add comment explaining DOM class manipulation in suggestion-actions
aagam-shah Apr 6, 2026
eee6def
Document why React roots don't need cleanup in inject.js
aagam-shah Apr 6, 2026
822b184
Add aria-hidden when SuggestAllButton is visually hidden
aagam-shah Apr 6, 2026
4244642
Fix re-injection after Navigator screen transitions
aagam-shah Apr 6, 2026
311841a
Improve injection: unmount old roots, debounce observer
aagam-shah Apr 6, 2026
9b20706
Use top-level diff import instead of deep path
aagam-shah Apr 6, 2026
537282f
Add screen reader context to diff view
aagam-shah Apr 6, 2026
2fee0f5
Fix stylelint errors in style.scss
aagam-shah Apr 6, 2026
b1b8bea
Fix i18n extraction: assign translated strings before ternary
aagam-shah Apr 6, 2026
00050b4
Fix stylelint rule-empty-line-before in @keyframes
aagam-shah Apr 6, 2026
69b2b94
Fix prettier formatting and import order for CI
aagam-shah Apr 6, 2026
606dadc
Fix banner dismiss: persist in localStorage, ignore per-section gener…
aagam-shah Apr 8, 2026
67617bc
Add Close button and X icon to banner
aagam-shah Apr 8, 2026
f0b22db
Improve suggestion UX: green border, click-to-accept, hide badge
aagam-shah Apr 8, 2026
dfe68df
Overlay diff on textarea instead of hiding DataForm
aagam-shah Apr 8, 2026
65af91d
Revert to hiding DataForm approach with better selectors
aagam-shah Apr 8, 2026
bb9fa9b
Match diff container to Gutenberg textarea styling
aagam-shah Apr 8, 2026
7f07ddf
Share banner dismiss state via wp.data store
aagam-shah Apr 8, 2026
0c70895
Update pnpm-lock.yaml after rebase
aagam-shah Apr 8, 2026
dc44b50
Fix banner disappearing on accept + reverse Jetpack logo
aagam-shah Apr 8, 2026
9c49fbe
Make logo triangles transparent so button color shows through
aagam-shah Apr 8, 2026
538033e
Set logo triangles to button accent color
aagam-shah Apr 8, 2026
3b5fe65
Use JetpackLogo from jetpack-components with logoColor="#fff"
aagam-shah Apr 8, 2026
827d332
Diff against current textarea draft, not persisted value
aagam-shah Apr 8, 2026
f3f8659
Fix layout shift when textarea is resized
aagam-shah Apr 8, 2026
1ab60ff
Simplify layout: remove dynamic height sync
aagam-shah Apr 8, 2026
ceb1457
Track textarea height via ResizeObserver for diff container
aagam-shah Apr 8, 2026
7e636d7
Drop ResizeObserver, add resize:vertical to diff container
aagam-shah Apr 8, 2026
2d6cb16
Capture textarea height via useState before hiding
aagam-shah Apr 8, 2026
9ea4169
Update API format to match wpcom endpoint
aagam-shah Apr 8, 2026
9144352
Replace snackbar with persistent upgrade Notice
aagam-shah Apr 8, 2026
427e1b0
Fix upgrade notice width to match accordion cards
aagam-shah Apr 8, 2026
4e65efd
Update pnpm-lock.yaml
aagam-shah Apr 8, 2026
9b3b41f
Hide header button whenever banner is not dismissed
aagam-shah Apr 9, 2026
08ad759
feat: add BlockGenerateButton component
aagam-shah Apr 9, 2026
213ab78
feat: add BlockSuggestionActions component
aagam-shah Apr 9, 2026
33315e3
feat: add block suggestion injection points
aagam-shah Apr 9, 2026
bc125c8
feat: add CSS for block modal suggestion and injected containers
aagam-shah Apr 9, 2026
edf93c4
feat: add block-level Improve guidelines with shared DiffView
aagam-shah Apr 9, 2026
2fce542
chore: update pnpm-lock.yaml
aagam-shah Apr 9, 2026
4877523
Merge remote-tracking branch 'origin/trunk' into add/content-guidelin…
aagam-shah Apr 9, 2026
4884008
fix: separate i18n calls to prevent minifier inlining
aagam-shah Apr 9, 2026
f7fdbd4
Merge remote-tracking branch 'origin/forno-186-build-site-content-agg…
aagam-shah Apr 10, 2026
ab6fe0b
fix: align proxy and JS API with wpcom categories format
aagam-shah Apr 10, 2026
390c775
fix: use text cursor on diff view
aagam-shah Apr 10, 2026
22b77d6
fix: resolve phan errors in suggest-guidelines proxy
aagam-shah Apr 10, 2026
f023527
feat: gate AI guidelines to Automatticians during internal rollout
aagam-shah Apr 13, 2026
216bb1b
fix: fallback diff height when textarea is hidden
aagam-shah Apr 13, 2026
6ed332a
feat: use useAiFeature hook for plan gating
aagam-shah Apr 13, 2026
6217069
Allow content guidelines AI access via enable_ai_generation query param
Apr 14, 2026
fd5e3b8
Allow content guidelines AI access via enable_ai_generation query param
aagam-shah Apr 14, 2026
6865aa2
fix: remove requireUpgrade hack in UpgradeNotice
aagam-shah Apr 14, 2026
a1070b2
Merge remote-tracking branch 'origin/trunk' into add/content-guidelin…
aagam-shah Apr 14, 2026
016c688
feat: add Tracks events for content guidelines AI
aagam-shah Apr 14, 2026
af3c61f
chore: remove mock API, use real suggest-guidelines endpoint
aagam-shah Apr 14, 2026
0b29879
debug: add console logs to UpgradeNotice for plan gating
aagam-shah Apr 14, 2026
9ba0e80
feat: gate AI guidelines to paid plans via hasFeature
aagam-shah Apr 14, 2026
57a0a29
fix: hide banner and header button when user lacks AI plan
aagam-shah Apr 14, 2026
0d30d07
chore: pin diff library to 8.0.4
aagam-shah Apr 14, 2026
cce69dd
fix: update upgrade notice text, style, and make dismissible
aagam-shah Apr 15, 2026
6bc6bc0
fix: guard localStorage access with try/catch
aagam-shah Apr 17, 2026
3d7a0a9
fix: record Tracks event when accepting via diff click
aagam-shah Apr 17, 2026
8a13179
docs: explain why MutationObserver watches document.body
aagam-shah Apr 17, 2026
30ad236
docs: explain intentional Simple/Atomic-only gating
aagam-shah Apr 21, 2026
07a8f7e
Merge remote-tracking branch 'origin/trunk' into add/content-guidelin…
aagam-shah Apr 21, 2026
0fbc5aa
refactor: pass blockModal element as prop instead of re-querying
aagam-shah Apr 21, 2026
bc9a0d0
fix: migrate Badge from jetpack-components to @wordpress/ui
aagam-shah Apr 21, 2026
60ead07
fix: guard startInjection against being called twice
aagam-shah Apr 21, 2026
b8bf9df
fix: unmount block modal slot roots when modal closes
aagam-shah Apr 21, 2026
42c9abf
Merge remote-tracking branch 'origin/trunk' into add/content-guidelin…
aagam-shah May 26, 2026
9437768
Merge remote-tracking branch 'origin/add/content-guidelines-plugin-ex…
aagam-shah May 26, 2026
85fcad5
fix: add deprecated annotations to pnpm-lock.yaml
aagam-shah May 26, 2026
92ff40a
Merge origin/trunk; regenerate pnpm-lock.yaml cleanly
aagam-shah Jun 2, 2026
2a52db0
Content Guidelines AI: update injection selectors for latest Gutenber…
aagam-shah Jun 3, 2026
60edab7
Content Guidelines AI: fix data store name for latest Gutenberg
aagam-shah Jun 3, 2026
7c3c46c
Content Guidelines AI: fix remaining renamed-Gutenberg references
aagam-shah Jun 3, 2026
b52bd5b
Content Guidelines AI: right-align the header Generate button
aagam-shah Jun 3, 2026
8d18381
Content Guidelines AI: optimize injection observer and use imported s…
aagam-shah Jun 3, 2026
21861cd
Content Guidelines AI: fix header button label flicker on load
aagam-shah Jun 4, 2026
e66278f
Merge remote-tracking branch 'origin/trunk' into add/content-guidelin…
aagam-shah Jun 9, 2026
48900b9
Content Guidelines AI: address PR review (empty result + malformed JSON)
aagam-shah Jun 9, 2026
fc8d4b7
Content Guidelines AI: remove enable_ai_generation query-param bypass
aagam-shah Jun 11, 2026
5ead501
Merge remote-tracking branch 'origin/trunk' into add/content-guidelin…
aagam-shah Jun 11, 2026
4edbfb3
Content Guidelines AI: re-resolve block name when the modal combobox …
aagam-shah Jun 11, 2026
45a868d
Content Guidelines AI: wire up JS translations and the Tracks client
aagam-shah Jun 11, 2026
a56a122
Merge remote-tracking branch 'origin/trunk' into add/content-guidelin…
aagam-shah Jun 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions projects/plugins/jetpack/_inc/content-guidelines-ai.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* Content Guidelines AI — Entry point.
*
* Injects Jetpack AI-powered generate/improve buttons into the
* Content Guidelines admin page (Gutenberg experimental feature).
*/
import '@automattic/jetpack-shared-extension-utils/store/wordpress-com';
import './content-guidelines-ai/store';
import './content-guidelines-ai/style.scss';
import { startInjection } from './content-guidelines-ai/lib/inject';

if ( document.readyState === 'loading' ) {
document.addEventListener( 'DOMContentLoaded', startInjection );
} else {
startInjection();
}
86 changes: 86 additions & 0 deletions projects/plugins/jetpack/_inc/content-guidelines-ai.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php
/**
* Content Guidelines AI — Jetpack AI integration.
*
* Enqueues a standalone JS bundle on the Content Guidelines admin page
* that adds AI-powered guideline generation via Jetpack.
*
* @package automattic/jetpack
*/

use Automattic\Jetpack\Assets;
use Automattic\Jetpack\Status\Visitor;
use Automattic\Jetpack\Tracking;

if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}

/**
* Check if the current user is an Automattician.
*
* - Simple sites: wpcom_is_proxied_request() + is_automattician()
* - Atomic sites: Visitor::is_automattician_feature_flags_only()
*
* @return bool
*/
function jetpack_content_guidelines_ai_is_automattician() {
// Simple sites.
if ( function_exists( 'wpcom_is_proxied_request' )
&& wpcom_is_proxied_request()
&& function_exists( 'is_automattician' )
&& is_automattician()
) {
return true;
}

// Atomic sites.
return ( new Visitor() )->is_automattician_feature_flags_only();
}

/**
* Enqueue content-guidelines-ai script on the Content Guidelines admin page.
*
* @since $$next-version$$
*
* @param string $hook_suffix The current admin page hook suffix.
*/
function jetpack_content_guidelines_ai_enqueue_scripts( $hook_suffix ) {
if ( 'settings_page_guidelines-wp-admin' !== $hook_suffix ) {
return;
}

// Temporarily gate to Automatticians only during internal rollout.
if ( ! jetpack_content_guidelines_ai_is_automattician() ) {
return;
}

// Bail when build artifacts are missing rather than enqueueing a script
// with guessed (and likely wrong) dependencies.
if ( ! file_exists( JETPACK__PLUGIN_DIR . '_inc/build/content-guidelines-ai.min.asset.php' ) ) {
return;
}

// The bundle records Tracks events via @automattic/jetpack-analytics,
// which only queues into window._tkq. Enqueue the Tracks client (w.js)
// so events send without relying on whichever platform widgets happen
// to load it.
Tracking::register_tracks_functions_scripts( true );

// Handles dependencies/version from the asset file, JS translations for
// the text domain, the CSS (including the .rtl.css variant), and style
// dependencies derived from the script's.
Assets::register_script(
'jetpack-content-guidelines-ai',
'_inc/build/content-guidelines-ai.min.js',
JETPACK__PLUGIN_FILE,
array(
'in_footer' => true,
'textdomain' => 'jetpack',
'css_path' => '_inc/build/content-guidelines-ai.css',
'enqueue' => true,
)
);
}

add_action( 'admin_enqueue_scripts', 'jetpack_content_guidelines_ai_enqueue_scripts' );
Original file line number Diff line number Diff line change
@@ -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 }
/>
);
}
Original file line number Diff line number Diff line change
@@ -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>
);
}
Original file line number Diff line number Diff line change
@@ -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 }
Comment thread
aagam-shah marked this conversation as resolved.
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>
);
}
Loading
Loading