Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
43 changes: 27 additions & 16 deletions docs/experiments/excerpt-generation.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,30 @@

## Summary

The Excerpt Generation experiment adds AI-powered excerpt generation to the WordPress post editor. It provides a "Generate excerpt" button in the excerpt panel that uses AI to create concise, engaging summaries of post content. The experiment registers a WordPress Ability (`ai/excerpt-generation`) that can be used both through the admin UI and directly via REST API requests.
The Excerpt Generation experiment adds AI-powered excerpt generation to the WordPress post editor. It provides an "Excerpt generation" sidebar panel with a modal workflow for generating, editing, and applying excerpts. The experiment registers a WordPress Ability (`ai/excerpt-generation`) that can be used both through the admin UI and directly via REST API requests.

## Overview

### For End Users

When enabled, the Excerpt Generation experiment adds a "Generate excerpt" button to the excerpt panel in the WordPress post editor. Users can click this button to automatically generate an excerpt suggestion based on the current post content. The generated excerpt is approximately 55 words, optimized for clarity, engagement, and SEO, and suitable for archive views, RSS feeds, and search results.
When enabled, the Excerpt Generation experiment adds an "Excerpt generation" panel to the document sidebar of the WordPress post editor. Users can click its "Generate excerpt" button to open a modal that generates an excerpt from the post content, review or edit the suggestion, and apply it to the post. The generated excerpt is approximately 55 words, optimized for clarity, engagement, and SEO, and suitable for archive views, RSS feeds, and search results.

**Key Features:**

- One-click excerpt generation from post content
- Automatically populates the excerpt field in the editor
- Generates an excerpt suggestion of roughly 55 words from the post content
- Editable textarea allows fine-tuning the suggestion before applying
- Copy to clipboard functionality for use outside the excerpt field
- Works with any post type that supports excerpts
- Generates excerpts optimized for SEO and readability
- Can regenerate excerpts if you want a different suggestion
- Can regenerate if you want a different suggestion

**Workflow:**

1. Open or create a post in the editor
2. Find the "Excerpt generation" panel in the sidebar
3. Click "Generate excerpt" to open the modal
4. Review the AI-generated suggestion in the textarea and edit as needed
5. Click "Apply" to set it as the post's excerpt
6. Save/update the post as usual

### For Developers

Expand All @@ -42,12 +51,12 @@ The ability can be called directly via REST API, making it useful for automation
- `enabled`: Whether the experiment is enabled

2. **React Side:**
- The React entry point (`index.tsx`) registers a WordPress plugin that hooks into the excerpt panel using `__experimentalPluginPostExcerpt`
- `ExcerptGeneration` component renders a button that calls `useExcerptGeneration()` hook
- The React entry point (`index.tsx`) registers a `PluginDocumentSettingPanel` named `ai-excerpt-generation`, hidden when the core Excerpt panel is disabled
- `ExcerptGenerationPanel` renders the generate button; clicking it opens `ExcerptGenerationModal`, which shows the suggestion in an editable textarea with Apply, Regenerate, Copy and Cancel actions
- `useExcerptGeneration` hook:
- Gets current post ID and content from the editor store
- Calls the ability via `apiFetch` when the button is clicked
- Updates the editor store and DOM textarea with the generated excerpt
- Calls the ability when the modal opens or Regenerate is clicked
- Updates the editor store with the excerpt when it is applied
- Handles loading states and error notifications

3. **Ability Execution:**
Expand Down Expand Up @@ -303,14 +312,14 @@ When a post ID is provided, the ability uses `get_post_context()` to gather post
You can extend the React components to add custom UI elements:

1. **Modify the button component:**
- Edit `src/experiments/excerpt-generation/components/ExcerptGeneration.tsx`
- Edit `src/experiments/excerpt-generation/components/ExcerptGenerationPanel.tsx` or `ExcerptGenerationModal.tsx`

2. **Add custom hooks:**
- Create new hooks in `src/experiments/excerpt-generation/components/`
- Import and use them in the main component

3. **Customize the excerpt panel:**
- The experiment uses `__experimentalPluginPostExcerpt` to inject into the excerpt panel
3. **Customize the sidebar panel:**
- The experiment uses `PluginDocumentSettingPanel` to render the panel
- You can modify `src/experiments/excerpt-generation/index.tsx` to add additional UI

## Testing
Expand All @@ -324,10 +333,12 @@ You can extend the React components to add custom UI elements:

2. **Test in the editor:**
- Create or edit a post with content
- Scroll to the excerpt panel (or enable it in Screen Options)
- Click the "Generate excerpt" button
- Verify the excerpt is generated and populated in the field
- Open the "Excerpt generation" panel in the document sidebar
- Click "Generate excerpt" to open the modal
- Verify the suggestion populates the textarea, edit it if needed, and click "Apply"
- Verify the post's excerpt field is updated
- Click "Regenerate excerpt" to test regeneration
- Test "Copy to clipboard" and verify the text is copied

3. **Test with different post types:**
- The experiment only loads for post types that support excerpts
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ public function enqueue_assets( string $hook_suffix ): void {
}

Asset_Loader::enqueue_script( 'excerpt_generation', 'experiments/excerpt-generation', array( 'include_core_abilities' => true ) );
Asset_Loader::enqueue_style( 'excerpt_generation', 'experiments/excerpt-generation' );
Asset_Loader::localize_script(
'excerpt_generation',
'ExcerptGenerationData',
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/**
* Modal component for generating and editing an excerpt suggestion.
*/

/**
* WordPress dependencies
*/
import { useEffect } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { Modal, Button, TextareaControl, Notice } from '@wordpress/components';

/**
* Internal dependencies
*/
import { useCopyToClipboardFeedback } from '../../../hooks/use-copy-to-clipboard-feedback';

interface ExcerptGenerationModalProps {
isGenerating: boolean;
suggestion: string | null;
editableText: string;
isContentTooShort: boolean;
tooShortLabel: string;
onEditableTextChange: ( text: string ) => void;
onGenerate: () => Promise< void >;
onApply: ( text: string ) => void;
onClose: () => void;
}

function CopyButton( {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not for this PR, but some components could be reused across experiments. I think it would help with UI consistency without the risk of drifting.

text,
disabled,
}: {
text: string;
disabled: boolean;
} ): JSX.Element {
const { ref, hasCopied } = useCopyToClipboardFeedback< HTMLButtonElement >(
{
text,
announcement: __( 'Excerpt copied to clipboard.', 'ai' ),
}
);

const isCopyDisabled = disabled || hasCopied;

return (
<Button
ref={ isCopyDisabled ? undefined : ref }
variant="tertiary"
disabled={ isCopyDisabled }
accessibleWhenDisabled
__next40pxDefaultSize
>
{ hasCopied
? __( 'Copied!', 'ai' )
: __( 'Copy to clipboard', 'ai' ) }
</Button>
);
}

/**
* Modal for generating and editing an excerpt.
*
* @param props Component props.
* @param props.isGenerating Whether generation is in progress.
* @param props.suggestion The generated suggestion.
* @param props.editableText The current editable text value.
* @param props.isContentTooShort Whether the content is too short to generate a suggestion.
* @param props.tooShortLabel Label to show when content is too short.
* @param props.onEditableTextChange Callback to update the editable text.
* @param props.onGenerate Callback to trigger generation.
* @param props.onApply Callback to apply the excerpt.
* @param props.onClose Callback to close the modal.
*/
export default function ExcerptGenerationModal( {
isGenerating,
suggestion,
editableText,
isContentTooShort,
tooShortLabel,
onEditableTextChange,
onGenerate,
onApply,
onClose,
}: ExcerptGenerationModalProps ): React.JSX.Element {
// Populate the textarea when a new suggestion arrives.
useEffect( () => {
if ( suggestion ) {
onEditableTextChange( suggestion );
}
}, [ suggestion, onEditableTextChange ] );

let generateButtonLabel: string = suggestion
? __( 'Regenerate', 'ai' )
: __( 'Generate', 'ai' );

if ( isGenerating ) {
generateButtonLabel = __( 'Generating…', 'ai' );
}

return (
<Modal
title={ __( 'Excerpt generation', 'ai' ) }
onRequestClose={ onClose }
className="ai-excerpt-generation-modal"
size="medium"
focusOnMount="firstContentElement"
>
<div className="ai-excerpt-generation-modal__content">
{ isContentTooShort && (
<Notice status="warning" isDismissible={ false }>
{ tooShortLabel }
</Notice>
) }

<TextareaControl
label={ __( 'Excerpt', 'ai' ) }
hideLabelFromVision
value={ editableText }
onChange={ onEditableTextChange }
rows={ 5 }
disabled={ isGenerating }
/>

<div className="ai-excerpt-generation-modal__actions">
<Button
variant="primary"
onClick={ () => {
onApply( editableText );
onClose();
} }
accessibleWhenDisabled
disabled={
isGenerating ||
( !! editableText &&
editableText.trim().length === 0 )
}
__next40pxDefaultSize
>
{ __( 'Apply', 'ai' ) }
</Button>
<Button
variant="secondary"
label={
isContentTooShort
? tooShortLabel
: generateButtonLabel
}
showTooltip
onClick={ onGenerate }
disabled={ isGenerating || isContentTooShort }
isBusy={ isGenerating }
accessibleWhenDisabled
__next40pxDefaultSize
>
{ generateButtonLabel }
</Button>
<CopyButton
text={ editableText }
disabled={
isGenerating || editableText.trim().length === 0
}
/>
<Button
variant="tertiary"
isDestructive
onClick={ onClose }
className="ai-excerpt-generation-modal__cancel"
__next40pxDefaultSize
>
{ __( 'Cancel', 'ai' ) }
</Button>
</div>
</div>
</Modal>
);
}
Loading
Loading