diff --git a/includes/Experiments/Title_Generation/Title_Generation.php b/includes/Experiments/Title_Generation/Title_Generation.php index 7aaab758..7d549f86 100644 --- a/includes/Experiments/Title_Generation/Title_Generation.php +++ b/includes/Experiments/Title_Generation/Title_Generation.php @@ -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', diff --git a/src/experiments/title-generation/components/TitleToolbar.tsx b/src/experiments/title-generation/components/TitleToolbar.tsx index 7c1178c2..4a1559d7 100644 --- a/src/experiments/title-generation/components/TitleToolbar.tsx +++ b/src/experiments/title-generation/components/TitleToolbar.tsx @@ -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'; @@ -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. */ @@ -75,6 +84,15 @@ 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 @@ -82,7 +100,7 @@ export default function TitleToolbar( { : __( 'Generate', 'ai' ); /** - * Handles the generate/re-generate button click. + * Handles the toolbar Generate/Re-generate button click. */ const handleGenerate = async () => { if ( isGenerating ) { @@ -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' @@ -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; @@ -146,6 +198,58 @@ export default function TitleToolbar( { ) } + { isOpen && ( + +

+ { __( + 'Review the suggested title or regenerate for a new one.', + 'ai' + ) } +

+ + + + + + + + + +
+ ) } ); } diff --git a/src/experiments/title-generation/index.scss b/src/experiments/title-generation/index.scss new file mode 100644 index 00000000..f3190ce5 --- /dev/null +++ b/src/experiments/title-generation/index.scss @@ -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; + } +} diff --git a/src/experiments/title-generation/index.tsx b/src/experiments/title-generation/index.tsx index 0a9618fc..39166b7c 100644 --- a/src/experiments/title-generation/index.tsx +++ b/src/experiments/title-generation/index.tsx @@ -13,6 +13,7 @@ import { registerPlugin } from '@wordpress/plugins'; /** * Internal dependencies */ +import './index.scss'; import TitleToolbar from './components/TitleToolbar'; import { TitleToolbarWrapper } from './components/TitleToolbarWrapper'; diff --git a/tests/e2e/specs/experiments/title-generation.spec.js b/tests/e2e/specs/experiments/title-generation.spec.js index bd4ffbe5..eb207ad1 100644 --- a/tests/e2e/specs/experiments/title-generation.spec.js +++ b/tests/e2e/specs/experiments/title-generation.spec.js @@ -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( @@ -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(