Skip to content
Open
1 change: 1 addition & 0 deletions includes/Experiments/Title_Generation/Title_Generation.php
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( 'title_generation', 'experiments/title-generation' );
Asset_Loader::enqueue_style( 'title_generation', 'experiments/title-generation' );
Asset_Loader::localize_script(
'title_generation',
'TitleGenerationData',
Expand Down
120 changes: 112 additions & 8 deletions src/experiments/title-generation/components/TitleToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,15 @@
/**
* WordPress dependencies
*/
import { Button, ToolbarGroup, ToolbarButton } from '@wordpress/components';
import {
Button,
Flex,
FlexItem,
Modal,
TextareaControl,
ToolbarGroup,
ToolbarButton,
} from '@wordpress/components';
import { dispatch, select, useDispatch } from '@wordpress/data';
import { store as editorStore, PostTypeSupportCheck } from '@wordpress/editor';
import { useState } from '@wordpress/element';
Expand Down Expand Up @@ -58,7 +66,8 @@ async function generateTitle(
/**
* TitleToolbar component.
*
* Provides Generate/Re-generate button.
* Provides Generate/Re-generate button and a modal for reviewing and
* inserting the AI-generated title suggestion.
*
* @return {JSX.Element} The toolbar component.
*/
Expand All @@ -75,14 +84,23 @@ export default function TitleToolbar( {
const { editPost } = useDispatch( editorStore );

const [ isGenerating, setIsGenerating ] = useState< boolean >( false );
const [ isRegenerating, setIsRegenerating ] = useState< boolean >( false );
const [ isOpen, setOpen ] = useState< boolean >( false );
const [ generatedTitle, setGeneratedTitle ] = useState< string >( '' );

const openModal = () => setOpen( true );
const closeModal = () => {
setOpen( false );
setGeneratedTitle( '' );
};

const hasTitle = title.trim().length > 0;
const buttonLabel = hasTitle
? __( 'Re-generate', 'ai' )
: __( 'Generate', 'ai' );

/**
* Handles the generate/re-generate button click.
* Handles the toolbar Generate/Re-generate button click.
*/
const handleGenerate = async () => {
if ( isGenerating ) {
Expand All @@ -96,11 +114,9 @@ export default function TitleToolbar( {
);

try {
const generatedTitle = await generateTitle(
postId as number,
content
);
editPost( { title: generatedTitle } );
const result = await generateTitle( postId as number, content );
setGeneratedTitle( result );
openModal();
} catch ( error: any ) {
const message =
typeof error === 'string'
Expand All @@ -115,6 +131,42 @@ export default function TitleToolbar( {
}
};

/**
* Handles the Re-generate button inside the modal.
* Fetches a new suggestion without closing the modal.
*/
const handleRegenerate = async () => {
const content = select( editorStore ).getEditedPostContent();
setIsRegenerating( true );
( dispatch( noticesStore ) as any ).removeNotice(
'ai_title_generation_error'
);

try {
const result = await generateTitle( postId as number, content );
setGeneratedTitle( result );
} catch ( error: any ) {
const message =
typeof error === 'string'
? error
: error?.message ?? __( 'Failed to generate title.', 'ai' );
( dispatch( noticesStore ) as any ).createErrorNotice( message, {
id: 'ai_title_generation_error',
isDismissible: true,
} );
} finally {
setIsRegenerating( false );
}
};

/**
* Applies the generated title to the post and closes the modal.
*/
const handleInsert = () => {
editPost( { title: generatedTitle } );
closeModal();
};

// Don't render if disabled.
if ( ! aiTitleGenerationData?.enabled ) {
return null;
Expand Down Expand Up @@ -146,6 +198,58 @@ export default function TitleToolbar( {
</ToolbarButton>
</ToolbarGroup>
) }
{ isOpen && (
<Modal
title={ __( 'Title suggestion', 'ai' ) }
onRequestClose={ closeModal }
isFullScreen={ false }
size="medium"
className="ai-title-generation-modal"
>
<p className="ai-title-generation-subtitle">
{ __(
'Review the suggested title or regenerate for a new one.',
'ai'
) }
</p>
<TextareaControl
rows={ 2 }
label={ __( 'Generated title', 'ai' ) }
hideLabelFromVision
value={ generatedTitle }
onChange={ setGeneratedTitle }
disabled={ isRegenerating }
__nextHasNoMarginBottom
/>
<Flex
justify="flex-end"
gap="3"
className="ai-title-generation-actions"
>
<FlexItem>
<Button
variant="secondary"
onClick={ handleRegenerate }
disabled={ isRegenerating }
isBusy={ isRegenerating }
>
{ isRegenerating
? __( 'Regenerating…', 'ai' )
: __( 'Re-generate', 'ai' ) }
</Button>
</FlexItem>
<FlexItem>
<Button
variant="primary"
onClick={ handleInsert }
disabled={ isRegenerating || ! generatedTitle }
>
{ __( 'Insert', 'ai' ) }
</Button>
</FlexItem>
</Flex>
</Modal>
) }
</PostTypeSupportCheck>
);
}
14 changes: 14 additions & 0 deletions src/experiments/title-generation/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.ai-title-generation-modal {
.ai-title-generation-subtitle {
margin: 0 0 16px;
color: #757575;
}

.components-textarea-control__input {
resize: none;
}

.ai-title-generation-actions {
margin-top: 24px;
}
}
1 change: 1 addition & 0 deletions src/experiments/title-generation/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { registerPlugin } from '@wordpress/plugins';
/**
* Internal dependencies
*/
import './index.scss';
import TitleToolbar from './components/TitleToolbar';
import { TitleToolbarWrapper } from './components/TitleToolbarWrapper';

Expand Down
46 changes: 44 additions & 2 deletions tests/e2e/specs/experiments/title-generation.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,28 @@ test.describe( 'Title Generation Experiment', () => {
.locator( '.ai-title-toolbar-container button' )
.click();

// Ensure the title is updated directly (no modal).
// Ensure the title modal is visible.
await expect(
page.locator( '.ai-title-generation-modal' )
).toBeVisible();

// Ensure the generated title textarea is visible.
await expect(
page.locator( '.ai-title-generation-modal textarea' )
).toBeVisible();

// Click Insert to apply the generated title.
await page
.locator( '.ai-title-generation-modal' )
.getByRole( 'button', { name: 'Insert' } )
.click();

// Ensure the title modal is closed.
await expect(
page.locator( '.ai-title-generation-modal' )
).not.toBeVisible();

// Ensure the title is updated.
await expect(
editor.canvas.locator( '.editor-post-title__input' )
).toHaveText(
Expand Down Expand Up @@ -111,7 +132,28 @@ test.describe( 'Title Generation Experiment', () => {
.locator( '.ai-title-toolbar-container button' )
.click();

// Ensure the title is updated directly (no modal).
// Ensure the title modal is visible.
await expect(
page.locator( '.ai-title-generation-modal' )
).toBeVisible();

// Ensure the generated title textarea is visible.
await expect(
page.locator( '.ai-title-generation-modal textarea' )
).toBeVisible();

// Click Insert to apply the generated title.
await page
.locator( '.ai-title-generation-modal' )
.getByRole( 'button', { name: 'Insert' } )
.click();

// Ensure the title modal is closed.
await expect(
page.locator( '.ai-title-generation-modal' )
).not.toBeVisible();

// Ensure the title is updated.
await expect(
editor.canvas.locator( '.editor-post-title__input' )
).toHaveText(
Expand Down
Loading