From b9917b54f5ac3e8bb076eacd2ab67769e27cd261 Mon Sep 17 00:00:00 2001 From: Darin Kotter Date: Thu, 4 Dec 2025 14:14:37 -0700 Subject: [PATCH 01/29] Add the base Summarization Experiment code --- includes/Experiment_Loader.php | 1 + .../Summarization/Summarization.php | 61 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 includes/Experiments/Summarization/Summarization.php diff --git a/includes/Experiment_Loader.php b/includes/Experiment_Loader.php index 37f19bd7..b7c20cf5 100644 --- a/includes/Experiment_Loader.php +++ b/includes/Experiment_Loader.php @@ -104,6 +104,7 @@ public function register_default_experiments(): void { */ private function get_default_experiments(): array { $experiment_classes = array( + \WordPress\AI\Experiments\Summarization\Summarization::class, \WordPress\AI\Experiments\Title_Generation\Title_Generation::class, ); diff --git a/includes/Experiments/Summarization/Summarization.php b/includes/Experiments/Summarization/Summarization.php new file mode 100644 index 00000000..933aa09e --- /dev/null +++ b/includes/Experiments/Summarization/Summarization.php @@ -0,0 +1,61 @@ + 'summarization', + 'label' => __( 'Content Summarization', 'ai' ), + 'description' => __( 'Summarizes long-form content into digestible overviews', 'ai' ), + ); + } + + /** + * {@inheritDoc} + * + * @since x.x.x + */ + public function register(): void { + add_action( 'wp_abilities_api_init', array( $this, 'register_abilities' ) ); + } + + /** + * Registers any needed abilities. + * + * @since x.x.x + */ + public function register_abilities(): void { + wp_register_ability( + 'ai/' . $this->get_id(), + array( + 'label' => $this->get_label(), + 'description' => $this->get_description(), + 'ability_class' => Summarization_Ability::class, + ), + ); + } +} From 82bdbcff239717f297a5d84f3a7a4fb7cf81e2a6 Mon Sep 17 00:00:00 2001 From: Darin Kotter Date: Thu, 4 Dec 2025 14:47:35 -0700 Subject: [PATCH 02/29] Add the base Summarization Ability code --- .../Abilities/Summarization/Summarization.php | 226 ++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 includes/Abilities/Summarization/Summarization.php diff --git a/includes/Abilities/Summarization/Summarization.php b/includes/Abilities/Summarization/Summarization.php new file mode 100644 index 00000000..1f1c3967 --- /dev/null +++ b/includes/Abilities/Summarization/Summarization.php @@ -0,0 +1,226 @@ + 'object', + 'properties' => array( + 'content' => array( + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'description' => esc_html__( 'Content to summarize.', 'ai' ), + ), + 'post_id' => array( + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'description' => esc_html__( 'Content from this post will be summarized. This overrides the content parameter if both are provided.', 'ai' ), + ), + ), + ); + } + + /** + * {@inheritDoc} + * + * @since x.x.x + */ + protected function output_schema(): array { + return array( + 'type' => 'string', + 'description' => esc_html__( 'The summary of the content.', 'ai' ), + ); + } + + /** + * {@inheritDoc} + * + * @since x.x.x + */ + protected function execute_callback( $input ) { + // Default arguments. + $args = wp_parse_args( + $input, + array( + 'content' => null, + 'post_id' => null, + ), + ); + + // If a post ID is provided, ensure the post exists before using its' content. + if ( $args['post_id'] ) { + $post = get_post( $args['post_id'] ); + + if ( ! $post ) { + return new WP_Error( + 'post_not_found', + /* translators: %d: Post ID. */ + sprintf( esc_html__( 'Post with ID %d not found.', 'ai' ), absint( $args['post_id'] ) ) + ); + } + + // Get the post context. + $context = get_post_context( $args['post_id'] ); + + // Default to the passed in content if it exists. + if ( $args['content'] ) { + $context['content'] = normalize_content( $args['content'] ); + } + } else { + $context = array( + 'content' => normalize_content( $args['content'] ?? '' ), + ); + } + + // If we have no content, return an error. + if ( empty( $context['content'] ) ) { + return new WP_Error( + 'content_not_provided', + esc_html__( 'Content is required to generate a summary.', 'ai' ) + ); + } + + // Generate the summary. + $result = $this->generate_summary( $context ); + + // If we have an error, return it. + if ( is_wp_error( $result ) ) { + return $result; + } + + // If we have no results, return an error. + if ( empty( $result ) ) { + return new WP_Error( + 'no_results', + esc_html__( 'No summary was generated.', 'ai' ) + ); + } + + // Return the summary in the format the Ability expects. + return sanitize_text_field( trim( $result ) ); + } + + /** + * {@inheritDoc} + * + * @since x.x.x + */ + protected function permission_callback( $args ) { + $post_id = isset( $args['post_id'] ) ? absint( $args['post_id'] ) : null; + + if ( $post_id ) { + $post = get_post( $args['post_id'] ); + + // Ensure the post exists. + if ( ! $post ) { + return new WP_Error( + 'post_not_found', + /* translators: %d: Post ID. */ + sprintf( esc_html__( 'Post with ID %d not found.', 'ai' ), absint( $args['post_id'] ) ) + ); + } + + // Ensure the user has permission to read this particular post. + if ( ! current_user_can( 'read_post', $post_id ) ) { + return new WP_Error( + 'insufficient_capabilities', + esc_html__( 'You do not have permission to summarize this post.', 'ai' ) + ); + } + + // Ensure the post type is allowed in REST endpoints. + $post_type = get_post_type( $post_id ); + + if ( ! $post_type ) { + return false; + } + + $post_type_obj = get_post_type_object( $post_type ); + + if ( ! $post_type_obj || empty( $post_type_obj->show_in_rest ) ) { + return false; + } + } elseif ( ! current_user_can( 'edit_posts' ) ) { + // Ensure the user has permission to edit posts in general. + return new WP_Error( + 'insufficient_capabilities', + esc_html__( 'You do not have permission to summarize content.', 'ai' ) + ); + } + + return true; + } + + /** + * {@inheritDoc} + * + * @since x.x.x + */ + protected function meta(): array { + return array( + 'show_in_rest' => true, + ); + } + + /** + * Generates a summary from the given content. + * + * @since x.x.x + * + * @param string|array $context The context to generate a title from. + * @return string|\WP_Error The generated summary, or a WP_Error if there was an error. + */ + protected function generate_summary( $context ) { + // Convert the context to a string if it's an array. + if ( is_array( $context ) ) { + $context = implode( + "\n", + array_map( + static function ( $key, $value ) { + return sprintf( + '%s: %s', + ucwords( str_replace( '_', ' ', $key ) ), + $value + ); + }, + array_keys( $context ), + $context + ) + ); + } + + // Generate the summary using the AI client. + return AI_Client::prompt_with_wp_error( '"""' . $context . '"""' ) + ->using_system_instruction( $this->get_system_instruction() ) + ->using_temperature( 0.9 ) + ->using_model_preference( ...get_preferred_models() ) + ->generate_text(); + } +} From eb8e4e69ce1a358fc955652734b375b301ead21f Mon Sep 17 00:00:00 2001 From: Darin Kotter Date: Thu, 4 Dec 2025 14:47:50 -0700 Subject: [PATCH 03/29] Add the system instructions for the Summarization Ability --- .../Summarization/system-instruction.php | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 includes/Abilities/Summarization/system-instruction.php diff --git a/includes/Abilities/Summarization/system-instruction.php b/includes/Abilities/Summarization/system-instruction.php new file mode 100644 index 00000000..0677ae5a --- /dev/null +++ b/includes/Abilities/Summarization/system-instruction.php @@ -0,0 +1,23 @@ + Date: Thu, 4 Dec 2025 14:48:42 -0700 Subject: [PATCH 04/29] Modify a bit our post context Ability and helper function to return 'not set' instead of an empty string if something isn't set, like the title or slug. Seems to give better results that way --- includes/Abilities/Utilities/Posts.php | 8 ++++---- includes/helpers.php | 5 ----- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/includes/Abilities/Utilities/Posts.php b/includes/Abilities/Utilities/Posts.php index b272f819..784e8701 100644 --- a/includes/Abilities/Utilities/Posts.php +++ b/includes/Abilities/Utilities/Posts.php @@ -124,11 +124,11 @@ private function register_get_post_details_ability(): void { } if ( in_array( 'title', $fields, true ) ) { - $details['title'] = $post->post_title; + $details['title'] = ! empty( $post->post_title ) ? $post->post_title : 'not set'; } if ( in_array( 'slug', $fields, true ) ) { - $details['slug'] = $post->post_name; + $details['slug'] = ! empty( $post->post_name ) ? $post->post_name : 'not set'; } if ( in_array( 'author', $fields, true ) ) { @@ -137,7 +137,7 @@ private function register_get_post_details_ability(): void { if ( $author ) { $details['author'] = $author->display_name; } else { - $details['author'] = ''; + $details['author'] = 'not set'; } } @@ -146,7 +146,7 @@ private function register_get_post_details_ability(): void { } if ( in_array( 'excerpt', $fields, true ) ) { - $details['excerpt'] = $post->post_excerpt; + $details['excerpt'] = ! empty( $post->post_excerpt ) ? $post->post_excerpt : 'not set'; } // Return the post details. diff --git a/includes/helpers.php b/includes/helpers.php index 2e2ab6df..a4b57743 100644 --- a/includes/helpers.php +++ b/includes/helpers.php @@ -81,11 +81,6 @@ function get_post_context( int $post_id ): array { $context['content'] = normalize_content( (string) apply_filters( 'the_content', $context['content'] ) ); } - if ( isset( $context['title'] ) ) { - $context['current_title'] = $context['title']; - unset( $context['title'] ); - } - if ( isset( $context['type'] ) ) { $context['content_type'] = $context['type']; unset( $context['type'] ); From 47df6e4b0a7aa3301f18e878135114b8b7625584 Mon Sep 17 00:00:00 2001 From: Darin Kotter Date: Thu, 4 Dec 2025 14:56:35 -0700 Subject: [PATCH 05/29] Add tests --- .../Abilities/Summarization/Summarization.php | 2 +- .../Includes/Abilities/SummarizationTest.php | 462 ++++++++++++++++++ .../Includes/Experiment_LoaderTest.php | 8 + .../Summarization/SummarizationTest.php | 75 +++ tests/Integration/Includes/HelpersTest.php | 4 +- 5 files changed, 548 insertions(+), 3 deletions(-) create mode 100644 tests/Integration/Includes/Abilities/SummarizationTest.php create mode 100644 tests/Integration/Includes/Experiments/Summarization/SummarizationTest.php diff --git a/includes/Abilities/Summarization/Summarization.php b/includes/Abilities/Summarization/Summarization.php index 1f1c3967..ad943611 100644 --- a/includes/Abilities/Summarization/Summarization.php +++ b/includes/Abilities/Summarization/Summarization.php @@ -20,7 +20,7 @@ /** * Content summarization WordPress Ability. * - * @since 0.1.0 + * @since x.x.x */ class Summarization extends Abstract_Ability { diff --git a/tests/Integration/Includes/Abilities/SummarizationTest.php b/tests/Integration/Includes/Abilities/SummarizationTest.php new file mode 100644 index 00000000..9cf24ab2 --- /dev/null +++ b/tests/Integration/Includes/Abilities/SummarizationTest.php @@ -0,0 +1,462 @@ + 'summarization', + 'label' => 'Content Summarization', + 'description' => 'Summarizes long-form content into digestible overviews', + ); + } + + /** + * Registers the experiment. + * + * @since x.x.x + */ + public function register(): void { + // No-op for testing. + } + +} + +/** + * Summarization Ability test case. + * + * @since x.x.x + */ +class SummarizationTest extends WP_UnitTestCase { + + /** + * Summarization ability instance. + * + * @var Summarization + */ + private $ability; + + /** + * Test experiment instance. + * + * @var Test_Summarization_Experiment + */ + private $experiment; + + /** + * Set up test case. + * + * @since x.x.x + */ + public function setUp(): void { + parent::setUp(); + + $this->experiment = new Test_Summarization_Experiment(); + $this->ability = new Summarization( + 'ai/summarization', + array( + 'label' => $this->experiment->get_label(), + 'description' => $this->experiment->get_description(), + ) + ); + } + + /** + * Tear down test case. + * + * @since x.x.x + */ + public function tearDown(): void { + wp_set_current_user( 0 ); + parent::tearDown(); + } + + /** + * Test that category() returns the correct category. + * + * @since x.x.x + */ + public function test_category_returns_correct_category() { + $reflection = new \ReflectionClass( $this->ability ); + $method = $reflection->getMethod( 'category' ); + $method->setAccessible( true ); + + $result = $method->invoke( $this->ability ); + + $this->assertEquals( 'ai-experiments', $result, 'Category should be ai-experiments' ); + } + + /** + * Test that input_schema() returns the expected schema structure. + * + * @since x.x.x + */ + public function test_input_schema_returns_expected_structure() { + $reflection = new \ReflectionClass( $this->ability ); + $method = $reflection->getMethod( 'input_schema' ); + $method->setAccessible( true ); + + $schema = $method->invoke( $this->ability ); + + $this->assertIsArray( $schema, 'Input schema should be an array' ); + $this->assertEquals( 'object', $schema['type'], 'Schema type should be object' ); + $this->assertArrayHasKey( 'properties', $schema, 'Schema should have properties' ); + $this->assertArrayHasKey( 'content', $schema['properties'], 'Schema should have content property' ); + $this->assertArrayHasKey( 'post_id', $schema['properties'], 'Schema should have post_id property' ); + + // Verify content property. + $this->assertEquals( 'string', $schema['properties']['content']['type'], 'Content should be string type' ); + $this->assertEquals( 'sanitize_text_field', $schema['properties']['content']['sanitize_callback'], 'Content should use sanitize_text_field' ); + + // Verify post_id property. + $this->assertEquals( 'integer', $schema['properties']['post_id']['type'], 'Post ID should be integer type' ); + $this->assertEquals( 'absint', $schema['properties']['post_id']['sanitize_callback'], 'Post ID should use absint' ); + } + + /** + * Test that output_schema() returns the expected schema structure. + * + * @since x.x.x + */ + public function test_output_schema_returns_expected_structure() { + $reflection = new \ReflectionClass( $this->ability ); + $method = $reflection->getMethod( 'output_schema' ); + $method->setAccessible( true ); + + $schema = $method->invoke( $this->ability ); + + $this->assertIsArray( $schema, 'Output schema should be an array' ); + $this->assertEquals( 'string', $schema['type'], 'Schema type should be string' ); + $this->assertArrayHasKey( 'description', $schema, 'Schema should have description' ); + } + + /** + * Test that get_system_instruction() returns the system instruction. + * + * @since x.x.x + */ + public function test_get_system_instruction_returns_system_instruction() { + $system_instruction = $this->ability->get_system_instruction(); + + // System instruction may be empty if file doesn't exist, or contain content if it does. + // We just verify it returns a string. + $this->assertIsString( $system_instruction, 'System instruction should be a string' ); + } + + /** + * Test that execute_callback() handles content parameter correctly. + * + * @since x.x.x + */ + public function test_execute_callback_with_content() { + $reflection = new \ReflectionClass( $this->ability ); + $method = $reflection->getMethod( 'execute_callback' ); + $method->setAccessible( true ); + + $input = array( + 'content' => 'This is some test content that needs to be summarized. It contains multiple sentences to provide enough context for a meaningful summary.', + ); + + try { + $result = $method->invoke( $this->ability, $input ); + } catch ( \Throwable $e ) { + $this->markTestSkipped( 'AI client not available in test environment: ' . $e->getMessage() ); + return; + } + + // Result may be string (success) or WP_Error (if AI client unavailable). + if ( is_wp_error( $result ) ) { + $this->markTestSkipped( 'AI client not available in test environment: ' . $result->get_error_message() ); + return; + } + + $this->assertIsString( $result, 'Result should be a string' ); + $this->assertNotEmpty( $result, 'Result should not be empty' ); + } + + /** + * Test that execute_callback() handles post_id parameter correctly. + * + * @since x.x.x + */ + public function test_execute_callback_with_post_id() { + $reflection = new \ReflectionClass( $this->ability ); + $method = $reflection->getMethod( 'execute_callback' ); + $method->setAccessible( true ); + + // Create a test post. + $post_id = $this->factory->post->create( + array( + 'post_content' => 'This is post content that needs to be summarized. It contains multiple sentences to provide enough context for a meaningful summary.', + 'post_title' => 'Test Post', + ) + ); + + $input = array( + 'post_id' => $post_id, + ); + + try { + $result = $method->invoke( $this->ability, $input ); + } catch ( \Throwable $e ) { + $this->markTestSkipped( 'AI client not available in test environment: ' . $e->getMessage() ); + return; + } + + // Result may be string (success) or WP_Error (if AI client unavailable). + if ( is_wp_error( $result ) ) { + $this->markTestSkipped( 'AI client not available in test environment: ' . $result->get_error_message() ); + return; + } + + $this->assertIsString( $result, 'Result should be a string' ); + $this->assertNotEmpty( $result, 'Result should not be empty' ); + } + + /** + * Test that execute_callback() returns error when post_id points to non-existent post. + * + * @since x.x.x + */ + public function test_execute_callback_with_invalid_post_id() { + $reflection = new \ReflectionClass( $this->ability ); + $method = $reflection->getMethod( 'execute_callback' ); + $method->setAccessible( true ); + + $input = array( + 'post_id' => 99999, // Non-existent post ID. + ); + $result = $method->invoke( $this->ability, $input ); + + $this->assertInstanceOf( WP_Error::class, $result, 'Result should be WP_Error' ); + $this->assertEquals( 'post_not_found', $result->get_error_code(), 'Error code should be post_not_found' ); + } + + /** + * Test that execute_callback() returns error when content is missing. + * + * @since x.x.x + */ + public function test_execute_callback_without_content() { + $reflection = new \ReflectionClass( $this->ability ); + $method = $reflection->getMethod( 'execute_callback' ); + $method->setAccessible( true ); + + $input = array(); + $result = $method->invoke( $this->ability, $input ); + + $this->assertInstanceOf( WP_Error::class, $result, 'Result should be WP_Error' ); + $this->assertEquals( 'content_not_provided', $result->get_error_code(), 'Error code should be content_not_provided' ); + } + + /** + * Test that permission_callback() returns true for user with edit_posts capability. + * + * @since x.x.x + */ + public function test_permission_callback_with_edit_posts_capability() { + $reflection = new \ReflectionClass( $this->ability ); + $method = $reflection->getMethod( 'permission_callback' ); + $method->setAccessible( true ); + + // Create a user with edit_posts capability. + $user_id = $this->factory->user->create( array( 'role' => 'editor' ) ); + wp_set_current_user( $user_id ); + + $result = $method->invoke( $this->ability, array() ); + + $this->assertTrue( $result, 'Permission should be granted for user with edit_posts capability' ); + } + + /** + * Test that permission_callback() returns error for user without edit_posts capability. + * + * @since x.x.x + */ + public function test_permission_callback_without_edit_posts_capability() { + $reflection = new \ReflectionClass( $this->ability ); + $method = $reflection->getMethod( 'permission_callback' ); + $method->setAccessible( true ); + + // Create a user without edit_posts capability. + $user_id = $this->factory->user->create( array( 'role' => 'subscriber' ) ); + wp_set_current_user( $user_id ); + + $result = $method->invoke( $this->ability, array() ); + + $this->assertInstanceOf( WP_Error::class, $result, 'Result should be WP_Error' ); + $this->assertEquals( 'insufficient_capabilities', $result->get_error_code(), 'Error code should be insufficient_capabilities' ); + } + + /** + * Test that permission_callback() returns error for logged out user. + * + * @since x.x.x + */ + public function test_permission_callback_for_logged_out_user() { + $reflection = new \ReflectionClass( $this->ability ); + $method = $reflection->getMethod( 'permission_callback' ); + $method->setAccessible( true ); + + // Ensure no user is logged in. + wp_set_current_user( 0 ); + + $result = $method->invoke( $this->ability, array() ); + + $this->assertInstanceOf( WP_Error::class, $result, 'Result should be WP_Error' ); + $this->assertEquals( 'insufficient_capabilities', $result->get_error_code(), 'Error code should be insufficient_capabilities' ); + } + + /** + * Test that permission_callback() returns true for user with read_post capability. + * + * @since x.x.x + */ + public function test_permission_callback_with_post_id_and_read_capability() { + $reflection = new \ReflectionClass( $this->ability ); + $method = $reflection->getMethod( 'permission_callback' ); + $method->setAccessible( true ); + + // Create a test post. + $post_id = $this->factory->post->create( + array( + 'post_content' => 'Test content', + 'post_status' => 'publish', + ) + ); + + // Create a user with read capability. + $user_id = $this->factory->user->create( array( 'role' => 'subscriber' ) ); + wp_set_current_user( $user_id ); + + $result = $method->invoke( $this->ability, array( 'post_id' => $post_id ) ); + + $this->assertTrue( $result, 'Permission should be granted for user with read_post capability' ); + } + + /** + * Test that permission_callback() returns error for user without read_post capability. + * + * @since x.x.x + */ + public function test_permission_callback_with_post_id_without_read_capability() { + $reflection = new \ReflectionClass( $this->ability ); + $method = $reflection->getMethod( 'permission_callback' ); + $method->setAccessible( true ); + + // Create a private test post. + $post_id = $this->factory->post->create( + array( + 'post_content' => 'Test content', + 'post_status' => 'private', + ) + ); + + // Create a user without read capability for this post. + $user_id = $this->factory->user->create( array( 'role' => 'subscriber' ) ); + wp_set_current_user( $user_id ); + + $result = $method->invoke( $this->ability, array( 'post_id' => $post_id ) ); + + $this->assertInstanceOf( WP_Error::class, $result, 'Result should be WP_Error' ); + $this->assertEquals( 'insufficient_capabilities', $result->get_error_code(), 'Error code should be insufficient_capabilities' ); + } + + /** + * Test that permission_callback() returns error for non-existent post. + * + * @since x.x.x + */ + public function test_permission_callback_with_nonexistent_post_id() { + $reflection = new \ReflectionClass( $this->ability ); + $method = $reflection->getMethod( 'permission_callback' ); + $method->setAccessible( true ); + + $user_id = $this->factory->user->create( array( 'role' => 'editor' ) ); + wp_set_current_user( $user_id ); + + $result = $method->invoke( $this->ability, array( 'post_id' => 99999 ) ); + + $this->assertInstanceOf( WP_Error::class, $result, 'Result should be WP_Error' ); + $this->assertEquals( 'post_not_found', $result->get_error_code(), 'Error code should be post_not_found' ); + } + + /** + * Test that permission_callback() returns false for post type without show_in_rest. + * + * @since x.x.x + */ + public function test_permission_callback_with_post_type_without_show_in_rest() { + $reflection = new \ReflectionClass( $this->ability ); + $method = $reflection->getMethod( 'permission_callback' ); + $method->setAccessible( true ); + + // Register a custom post type without show_in_rest. + register_post_type( + 'test_no_rest', + array( + 'public' => true, + 'show_in_rest' => false, + ) + ); + + // Create a test post with this post type. + $post_id = $this->factory->post->create( + array( + 'post_content' => 'Test content', + 'post_type' => 'test_no_rest', + 'post_status' => 'publish', + ) + ); + + $user_id = $this->factory->user->create( array( 'role' => 'editor' ) ); + wp_set_current_user( $user_id ); + + $result = $method->invoke( $this->ability, array( 'post_id' => $post_id ) ); + + $this->assertFalse( $result, 'Permission should be denied for post type without show_in_rest' ); + + // Clean up. + unregister_post_type( 'test_no_rest' ); + } + + /** + * Test that meta() returns the expected meta structure. + * + * @since x.x.x + */ + public function test_meta_returns_expected_structure() { + $reflection = new \ReflectionClass( $this->ability ); + $method = $reflection->getMethod( 'meta' ); + $method->setAccessible( true ); + + $meta = $method->invoke( $this->ability ); + + $this->assertIsArray( $meta, 'Meta should be an array' ); + $this->assertArrayHasKey( 'show_in_rest', $meta, 'Meta should have show_in_rest' ); + $this->assertTrue( $meta['show_in_rest'], 'show_in_rest should be true' ); + } +} + diff --git a/tests/Integration/Includes/Experiment_LoaderTest.php b/tests/Integration/Includes/Experiment_LoaderTest.php index 72c4ece1..d1a15d66 100644 --- a/tests/Integration/Includes/Experiment_LoaderTest.php +++ b/tests/Integration/Includes/Experiment_LoaderTest.php @@ -107,11 +107,19 @@ public function tearDown(): void { public function test_register_default_experiments() { $this->loader->register_default_experiments(); + $this->assertTrue( + $this->registry->has_experiment( 'summarization' ), + 'Summarization experiment should be registered' + ); $this->assertTrue( $this->registry->has_experiment( 'title-generation' ), 'Title generation experiment should be registered' ); + $experiment = $this->registry->get_experiment( 'summarization' ); + $this->assertNotNull( $experiment, 'Summarization experiment should exist' ); + $this->assertEquals( 'summarization', $experiment->get_id() ); + $experiment = $this->registry->get_experiment( 'title-generation' ); $this->assertNotNull( $experiment, 'Title generation experiment should exist' ); $this->assertEquals( 'title-generation', $experiment->get_id() ); diff --git a/tests/Integration/Includes/Experiments/Summarization/SummarizationTest.php b/tests/Integration/Includes/Experiments/Summarization/SummarizationTest.php new file mode 100644 index 00000000..c0f92d16 --- /dev/null +++ b/tests/Integration/Includes/Experiments/Summarization/SummarizationTest.php @@ -0,0 +1,75 @@ + 'test-api-key' ) ); + + // Mock has_valid_ai_credentials to return true for tests. + add_filter( 'ai_experiments_pre_has_valid_credentials_check', '__return_true' ); + + // Enable experiments globally and individually. + update_option( 'ai_experiments_enabled', true ); + update_option( 'ai_experiment_summarization_enabled', true ); + + $registry = new Experiment_Registry(); + $loader = new Experiment_Loader( $registry ); + $loader->register_default_experiments(); + $loader->initialize_experiments(); + + $experiment = $registry->get_experiment( 'summarization' ); + $this->assertInstanceOf( Summarization::class, $experiment, 'Summarization experiment should be registered in the registry.' ); + } + + /** + * Tear down test case. + * + * @since x.x.x + */ + public function tearDown(): void { + wp_set_current_user( 0 ); + delete_option( 'ai_experiments_enabled' ); + delete_option( 'ai_experiment_summarization_enabled' ); + delete_option( 'wp_ai_client_provider_credentials' ); + remove_filter( 'ai_experiments_pre_has_valid_credentials_check', '__return_true' ); + parent::tearDown(); + } + + /** + * Test that the experiment is registered correctly. + * + * @since x.x.x + */ + public function test_experiment_registration() { + $experiment = new Summarization(); + + $this->assertEquals( 'summarization', $experiment->get_id() ); + $this->assertEquals( 'Content Summarization', $experiment->get_label() ); + $this->assertTrue( $experiment->is_enabled() ); + } +} + diff --git a/tests/Integration/Includes/HelpersTest.php b/tests/Integration/Includes/HelpersTest.php index 713205e2..0d9ce48c 100644 --- a/tests/Integration/Includes/HelpersTest.php +++ b/tests/Integration/Includes/HelpersTest.php @@ -191,8 +191,8 @@ public function test_get_post_context_returns_post_metadata() { $context = \WordPress\AI\get_post_context( $post_id ); - $this->assertArrayHasKey( 'current_title', $context, 'Should have current_title' ); - $this->assertEquals( 'Test Post Title', $context['current_title'], 'Title should match' ); + $this->assertArrayHasKey( 'title', $context, 'Should have title' ); + $this->assertEquals( 'Test Post Title', $context['title'], 'Title should match' ); $this->assertArrayHasKey( 'slug', $context, 'Should have slug' ); $this->assertEquals( 'test-post-slug', $context['slug'], 'Slug should match' ); $this->assertArrayHasKey( 'author', $context, 'Should have author' ); From 72a97e020a7a94c1dd3ea796be78cc32eb603718 Mon Sep 17 00:00:00 2001 From: Darin Kotter Date: Thu, 4 Dec 2025 15:12:20 -0700 Subject: [PATCH 06/29] Revert changes to the get-post-details ability and instead remove empty values in our helper function --- includes/Abilities/Summarization/Summarization.php | 4 ++-- includes/Abilities/Utilities/Posts.php | 8 ++++---- includes/helpers.php | 3 +++ 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/includes/Abilities/Summarization/Summarization.php b/includes/Abilities/Summarization/Summarization.php index ad943611..afbe788b 100644 --- a/includes/Abilities/Summarization/Summarization.php +++ b/includes/Abilities/Summarization/Summarization.php @@ -33,12 +33,12 @@ protected function input_schema(): array { return array( 'type' => 'object', 'properties' => array( - 'content' => array( + 'content' => array( 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'description' => esc_html__( 'Content to summarize.', 'ai' ), ), - 'post_id' => array( + 'post_id' => array( 'type' => 'integer', 'sanitize_callback' => 'absint', 'description' => esc_html__( 'Content from this post will be summarized. This overrides the content parameter if both are provided.', 'ai' ), diff --git a/includes/Abilities/Utilities/Posts.php b/includes/Abilities/Utilities/Posts.php index 784e8701..b272f819 100644 --- a/includes/Abilities/Utilities/Posts.php +++ b/includes/Abilities/Utilities/Posts.php @@ -124,11 +124,11 @@ private function register_get_post_details_ability(): void { } if ( in_array( 'title', $fields, true ) ) { - $details['title'] = ! empty( $post->post_title ) ? $post->post_title : 'not set'; + $details['title'] = $post->post_title; } if ( in_array( 'slug', $fields, true ) ) { - $details['slug'] = ! empty( $post->post_name ) ? $post->post_name : 'not set'; + $details['slug'] = $post->post_name; } if ( in_array( 'author', $fields, true ) ) { @@ -137,7 +137,7 @@ private function register_get_post_details_ability(): void { if ( $author ) { $details['author'] = $author->display_name; } else { - $details['author'] = 'not set'; + $details['author'] = ''; } } @@ -146,7 +146,7 @@ private function register_get_post_details_ability(): void { } if ( in_array( 'excerpt', $fields, true ) ) { - $details['excerpt'] = ! empty( $post->post_excerpt ) ? $post->post_excerpt : 'not set'; + $details['excerpt'] = $post->post_excerpt; } // Return the post details. diff --git a/includes/helpers.php b/includes/helpers.php index a4b57743..9dd1dab0 100644 --- a/includes/helpers.php +++ b/includes/helpers.php @@ -85,6 +85,9 @@ function get_post_context( int $post_id ): array { $context['content_type'] = $context['type']; unset( $context['type'] ); } + + // Remove any empty context values. + $context = array_filter( $context ); } } From 8bd5d6a20d553bb9475899132e549cf8e2ff2853 Mon Sep 17 00:00:00 2001 From: Darin Kotter Date: Wed, 10 Dec 2025 13:38:06 -0700 Subject: [PATCH 07/29] Allow passing in extra data when our system instructions are loaded --- includes/Abstracts/Abstract_Ability.php | 27 ++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/includes/Abstracts/Abstract_Ability.php b/includes/Abstracts/Abstract_Ability.php index 91240049..0733ca1d 100644 --- a/includes/Abstracts/Abstract_Ability.php +++ b/includes/Abstracts/Abstract_Ability.php @@ -118,12 +118,14 @@ abstract protected function meta(): array; * * @since 0.1.0 * - * @param string|null $filename Optional. Explicit filename to load. If not provided, - * attempts to load `system-instruction.php` or `prompt.php`. + * @param string|null $filename Optional. Explicit filename to load. If not provided, + * attempts to load `system-instruction.php` or `prompt.php`. + * @param array $data Optional. Data to expose to the system instruction file. + * This data will be extracted as variables available in the file scope. * @return string The system instruction for the feature. */ - public function get_system_instruction( ?string $filename = null ): string { - return $this->load_system_instruction_from_file( $filename ); + public function get_system_instruction( ?string $filename = null, array $data = array() ): string { + return $this->load_system_instruction_from_file( $filename, $data ); } /** @@ -135,13 +137,19 @@ public function get_system_instruction( ?string $filename = null ): string { * return 'Your system instruction text here...'; * ``` * + * If data is provided, it will be extracted as variables available in the file scope. + * For example, if you pass `array( 'length' => 'short' )`, the variable `$length` + * will be available in the system instruction file. + * * @since 0.1.0 * - * @param string|null $filename Optional. Explicit filename to load. If not provided, - * attempts to load `system-instruction.php`. + * @param string|null $filename Optional. Explicit filename to load. If not provided, + * attempts to load `system-instruction.php`. + * @param array $data Optional. Data to expose to the system instruction file. + * This data will be extracted as variables available in the file scope. * @return string The contents of the file, or empty string if file not found. */ - protected function load_system_instruction_from_file( ?string $filename = null ): string { + protected function load_system_instruction_from_file( ?string $filename = null, array $data = array() ): string { // Get the feature's directory using reflection. $reflection = new ReflectionClass( $this ); $file_name = $reflection->getFileName(); @@ -152,6 +160,11 @@ protected function load_system_instruction_from_file( ?string $filename = null ) $feature_dir = dirname( $file_name ); + // Extract data into variables for use in the included file. + if ( ! empty( $data ) ) { + extract( $data, EXTR_SKIP ); // phpcs:ignore WordPress.PHP.DontExtract.extract_extract + } + // If explicit filename provided, use it. if ( null !== $filename ) { $file_path = trailingslashit( $feature_dir ) . $filename; From 6824c0402ff5b033736e6819d453f26f2cd69627 Mon Sep 17 00:00:00 2001 From: Darin Kotter Date: Wed, 10 Dec 2025 13:38:41 -0700 Subject: [PATCH 08/29] Allow choosing the summary length, from short, medium or long and pass that to our system instructions --- .../Abilities/Summarization/Summarization.php | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/includes/Abilities/Summarization/Summarization.php b/includes/Abilities/Summarization/Summarization.php index afbe788b..d64d1ed4 100644 --- a/includes/Abilities/Summarization/Summarization.php +++ b/includes/Abilities/Summarization/Summarization.php @@ -24,6 +24,15 @@ */ class Summarization extends Abstract_Ability { + /** + * The default length of the summary. + * + * @since x.x.x + * + * @var string + */ + protected const LENGTH_DEFAULT = 'medium'; + /** * {@inheritDoc} * @@ -43,6 +52,12 @@ protected function input_schema(): array { 'sanitize_callback' => 'absint', 'description' => esc_html__( 'Content from this post will be summarized. This overrides the content parameter if both are provided.', 'ai' ), ), + 'length' => array( + 'type' => 'enum', + 'enum' => array( 'short', 'medium', 'long' ), + 'default' => self::LENGTH_DEFAULT, + 'description' => esc_html__( 'The length of the summary.', 'ai' ), + ), ), ); } @@ -71,6 +86,7 @@ protected function execute_callback( $input ) { array( 'content' => null, 'post_id' => null, + 'length' => self::LENGTH_DEFAULT, ), ); @@ -108,7 +124,7 @@ protected function execute_callback( $input ) { } // Generate the summary. - $result = $this->generate_summary( $context ); + $result = $this->generate_summary( $context, $args['length'] ); // If we have an error, return it. if ( is_wp_error( $result ) ) { @@ -195,9 +211,10 @@ protected function meta(): array { * @since x.x.x * * @param string|array $context The context to generate a title from. + * @param string $length The desired length of the summary. * @return string|\WP_Error The generated summary, or a WP_Error if there was an error. */ - protected function generate_summary( $context ) { + protected function generate_summary( $context, $length ) { // Convert the context to a string if it's an array. if ( is_array( $context ) ) { $context = implode( @@ -218,7 +235,7 @@ static function ( $key, $value ) { // Generate the summary using the AI client. return AI_Client::prompt_with_wp_error( '"""' . $context . '"""' ) - ->using_system_instruction( $this->get_system_instruction() ) + ->using_system_instruction( $this->get_system_instruction( 'system-instruction.php', array( 'length' => $length ) ) ) ->using_temperature( 0.9 ) ->using_model_preference( ...get_preferred_models() ) ->generate_text(); From 009f2c0dccea19b8c9bdebea30e79431ebe7fe1b Mon Sep 17 00:00:00 2001 From: Darin Kotter Date: Wed, 10 Dec 2025 13:39:11 -0700 Subject: [PATCH 09/29] Dynamically set the expected return length depending on the passed in length value --- .../Abilities/Summarization/system-instruction.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/includes/Abilities/Summarization/system-instruction.php b/includes/Abilities/Summarization/system-instruction.php index 0677ae5a..90342b2d 100644 --- a/includes/Abilities/Summarization/system-instruction.php +++ b/includes/Abilities/Summarization/system-instruction.php @@ -5,15 +5,25 @@ * @package WordPress\AI\Abilities\Summarization */ +// Determine the length from the passed in global. +$length_desc = '2-3 sentences; 25-80 words'; +if ( isset( $length ) ) { + if ( 'short' === $length ) { + $length_desc = '1 sentence; <= 25 words'; + } elseif ( 'long' === $length ) { + $length_desc = '4-6 sentences; 80-160 words'; + } +} + // phpcs:ignore Squiz.PHP.Heredoc.NotAllowed -return <<<'INSTRUCTION' +return << Date: Wed, 10 Dec 2025 13:41:58 -0700 Subject: [PATCH 10/29] Update tests --- .../Abstracts/Abstract_AbilityTest.php | 156 ++++++++++++++++++ 1 file changed, 156 insertions(+) diff --git a/tests/Integration/Includes/Abstracts/Abstract_AbilityTest.php b/tests/Integration/Includes/Abstracts/Abstract_AbilityTest.php index 8b257f2f..d04e90a4 100644 --- a/tests/Integration/Includes/Abstracts/Abstract_AbilityTest.php +++ b/tests/Integration/Includes/Abstracts/Abstract_AbilityTest.php @@ -288,4 +288,160 @@ public function test_get_system_instruction_returns_empty_when_no_file() { $this->assertIsString( $system_instruction, 'System instruction should be a string' ); $this->assertEquals( '', $system_instruction, 'System instruction should be empty when no file exists' ); } + + /** + * Test that get_system_instruction() accepts optional data parameter and exposes it to the file. + * + * @since 0.1.0 + */ + public function test_get_system_instruction_with_data_parameter() { + $experiment = new Test_Ability_Experiment(); + $ability = new Test_Ability( + 'test-ability', + array( + 'label' => $experiment->get_label(), + 'description' => $experiment->get_description(), + ) + ); + + // Get the test ability's directory using reflection. + $reflection = new \ReflectionClass( $ability ); + $file_name = $reflection->getFileName(); + $feature_dir = dirname( $file_name ); + + // Create a temporary system instruction file that uses data variables. + $test_file = trailingslashit( $feature_dir ) . 'test-system-instruction.php'; + $test_content = <<<'PHP' + 'short', + 'tone' => 'professional', + 'max_words' => 100, + ); + + $system_instruction = $ability->get_system_instruction( 'test-system-instruction.php', $data ); + + // Verify the data was interpolated into the system instruction. + $this->assertIsString( $system_instruction, 'System instruction should be a string' ); + $this->assertStringContainsString( 'short length', $system_instruction, 'System instruction should contain processed length value' ); + $this->assertStringContainsString( 'professional', $system_instruction, 'System instruction should contain tone value' ); + $this->assertStringContainsString( '100', $system_instruction, 'System instruction should contain max_words value' ); + } finally { + // Clean up the test file. + if ( file_exists( $test_file ) ) { + unlink( $test_file ); + } + } + } + + /** + * Test that get_system_instruction() works without data parameter (backward compatibility). + * + * @since 0.1.0 + */ + public function test_get_system_instruction_without_data_parameter() { + $experiment = new Test_Ability_Experiment(); + $ability = new Test_Ability( + 'test-ability', + array( + 'label' => $experiment->get_label(), + 'description' => $experiment->get_description(), + ) + ); + + // Get the test ability's directory using reflection. + $reflection = new \ReflectionClass( $ability ); + $file_name = $reflection->getFileName(); + $feature_dir = dirname( $file_name ); + + // Create a temporary system instruction file without using data variables. + $test_file = trailingslashit( $feature_dir ) . 'test-system-instruction-simple.php'; + $test_content = <<<'PHP' +get_system_instruction( 'test-system-instruction-simple.php' ); + + // Verify it returns the content. + $this->assertIsString( $system_instruction, 'System instruction should be a string' ); + $this->assertStringContainsString( 'test assistant', $system_instruction, 'System instruction should contain expected content' ); + } finally { + // Clean up the test file. + if ( file_exists( $test_file ) ) { + unlink( $test_file ); + } + } + } + + /** + * Test that get_system_instruction() with empty data array works correctly. + * + * @since 0.1.0 + */ + public function test_get_system_instruction_with_empty_data_array() { + $experiment = new Test_Ability_Experiment(); + $ability = new Test_Ability( + 'test-ability', + array( + 'label' => $experiment->get_label(), + 'description' => $experiment->get_description(), + ) + ); + + // Get the test ability's directory using reflection. + $reflection = new \ReflectionClass( $ability ); + $file_name = $reflection->getFileName(); + $feature_dir = dirname( $file_name ); + + // Create a temporary system instruction file. + $test_file = trailingslashit( $feature_dir ) . 'test-system-instruction-empty.php'; + $test_content = <<<'PHP' +get_system_instruction( 'test-system-instruction-empty.php', array() ); + + // Verify it returns the content. + $this->assertIsString( $system_instruction, 'System instruction should be a string' ); + $this->assertStringContainsString( 'test assistant', $system_instruction, 'System instruction should contain expected content' ); + } finally { + // Clean up the test file. + if ( file_exists( $test_file ) ) { + unlink( $test_file ); + } + } + } } From d1866579d9d8838dc0bc44302bcc022cca43a6b9 Mon Sep 17 00:00:00 2001 From: Darin Kotter Date: Wed, 10 Dec 2025 13:49:16 -0700 Subject: [PATCH 11/29] Pull the content out of the context so we can separate those a bit more in our prompt, making it clearer to the LLM what it should be summarizing --- includes/Abilities/Summarization/Summarization.php | 14 ++++++++++---- .../Abilities/Summarization/system-instruction.php | 4 ++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/includes/Abilities/Summarization/Summarization.php b/includes/Abilities/Summarization/Summarization.php index d64d1ed4..4ef66efd 100644 --- a/includes/Abilities/Summarization/Summarization.php +++ b/includes/Abilities/Summarization/Summarization.php @@ -123,8 +123,11 @@ protected function execute_callback( $input ) { ); } + $content = $context['content']; + unset( $context['content'] ); + // Generate the summary. - $result = $this->generate_summary( $context, $args['length'] ); + $result = $this->generate_summary( $content, $context, $args['length'] ); // If we have an error, return it. if ( is_wp_error( $result ) ) { @@ -210,11 +213,12 @@ protected function meta(): array { * * @since x.x.x * - * @param string|array $context The context to generate a title from. + * @param string $content The content to summarize. + * @param string|array $context Additional context to use. * @param string $length The desired length of the summary. * @return string|\WP_Error The generated summary, or a WP_Error if there was an error. */ - protected function generate_summary( $context, $length ) { + protected function generate_summary( string $content, $context, string $length ) { // Convert the context to a string if it's an array. if ( is_array( $context ) ) { $context = implode( @@ -233,8 +237,10 @@ static function ( $key, $value ) { ); } + $content = "## Content\n\n" . $content . "\n\n## Additional Context\n\n" . $context; + // Generate the summary using the AI client. - return AI_Client::prompt_with_wp_error( '"""' . $context . '"""' ) + return AI_Client::prompt_with_wp_error( '"""' . $content . '"""' ) ->using_system_instruction( $this->get_system_instruction( 'system-instruction.php', array( 'length' => $length ) ) ) ->using_temperature( 0.9 ) ->using_model_preference( ...get_preferred_models() ) diff --git a/includes/Abilities/Summarization/system-instruction.php b/includes/Abilities/Summarization/system-instruction.php index 90342b2d..7bb0dc76 100644 --- a/includes/Abilities/Summarization/system-instruction.php +++ b/includes/Abilities/Summarization/system-instruction.php @@ -19,7 +19,7 @@ return << Date: Wed, 10 Dec 2025 14:35:38 -0700 Subject: [PATCH 12/29] Change from a post_id field to a context field. Only add context to our request if it isn't empty. Change slightly how we separate content from context --- .../Abilities/Summarization/Summarization.php | 45 ++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/includes/Abilities/Summarization/Summarization.php b/includes/Abilities/Summarization/Summarization.php index 4ef66efd..441fd69f 100644 --- a/includes/Abilities/Summarization/Summarization.php +++ b/includes/Abilities/Summarization/Summarization.php @@ -47,10 +47,10 @@ protected function input_schema(): array { 'sanitize_callback' => 'sanitize_text_field', 'description' => esc_html__( 'Content to summarize.', 'ai' ), ), - 'post_id' => array( - 'type' => 'integer', - 'sanitize_callback' => 'absint', - 'description' => esc_html__( 'Content from this post will be summarized. This overrides the content parameter if both are provided.', 'ai' ), + 'context' => array( + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'description' => esc_html__( 'Additional context to use when summarizing the content. This can either be a string of additional context or can be a post ID that will then be used to get context from that post (if it exists). If no content is provided but a valid post ID is used here, the content from that post will be used.', 'ai' ), ), 'length' => array( 'type' => 'enum', @@ -85,47 +85,45 @@ protected function execute_callback( $input ) { $input, array( 'content' => null, - 'post_id' => null, + 'context' => null, 'length' => self::LENGTH_DEFAULT, ), ); // If a post ID is provided, ensure the post exists before using its' content. - if ( $args['post_id'] ) { - $post = get_post( $args['post_id'] ); + if ( is_numeric( $args['context'] ) ) { + $post = get_post( (int) $args['context'] ); if ( ! $post ) { return new WP_Error( 'post_not_found', /* translators: %d: Post ID. */ - sprintf( esc_html__( 'Post with ID %d not found.', 'ai' ), absint( $args['post_id'] ) ) + sprintf( esc_html__( 'Post with ID %d not found.', 'ai' ), absint( $args['context'] ) ) ); } // Get the post context. - $context = get_post_context( $args['post_id'] ); + $context = get_post_context( $post->ID ); + $content = $context['content'] ?? ''; + unset( $context['content'] ); // Default to the passed in content if it exists. if ( $args['content'] ) { - $context['content'] = normalize_content( $args['content'] ); + $content = normalize_content( $args['content'] ); } } else { - $context = array( - 'content' => normalize_content( $args['content'] ?? '' ), - ); + $content = normalize_content( $args['content'] ?? '' ); + $context = $args['context'] ?? ''; } // If we have no content, return an error. - if ( empty( $context['content'] ) ) { + if ( empty( $content ) ) { return new WP_Error( 'content_not_provided', esc_html__( 'Content is required to generate a summary.', 'ai' ) ); } - $content = $context['content']; - unset( $context['content'] ); - // Generate the summary. $result = $this->generate_summary( $content, $context, $args['length'] ); @@ -152,17 +150,17 @@ protected function execute_callback( $input ) { * @since x.x.x */ protected function permission_callback( $args ) { - $post_id = isset( $args['post_id'] ) ? absint( $args['post_id'] ) : null; + $post_id = isset( $args['context'] ) && is_numeric( $args['context'] ) ? absint( $args['context'] ) : null; if ( $post_id ) { - $post = get_post( $args['post_id'] ); + $post = get_post( $post_id ); // Ensure the post exists. if ( ! $post ) { return new WP_Error( 'post_not_found', /* translators: %d: Post ID. */ - sprintf( esc_html__( 'Post with ID %d not found.', 'ai' ), absint( $args['post_id'] ) ) + sprintf( esc_html__( 'Post with ID %d not found.', 'ai' ), absint( $post_id ) ) ); } @@ -237,7 +235,12 @@ static function ( $key, $value ) { ); } - $content = "## Content\n\n" . $content . "\n\n## Additional Context\n\n" . $context; + $content = "## Content\n\n" . $content; + + // If we have additional context, add it to the content. + if ( $context ) { + $content .= "\n\n## Additional Context\n\n" . $context; + } // Generate the summary using the AI client. return AI_Client::prompt_with_wp_error( '"""' . $content . '"""' ) From fd1a81c4b4c7f2eed6e0d7c89a21f2f4ac4bdcc1 Mon Sep 17 00:00:00 2001 From: Darin Kotter Date: Wed, 10 Dec 2025 14:39:01 -0700 Subject: [PATCH 13/29] Update tests --- .../Includes/Abilities/SummarizationTest.php | 81 ++++++++++++++----- 1 file changed, 59 insertions(+), 22 deletions(-) diff --git a/tests/Integration/Includes/Abilities/SummarizationTest.php b/tests/Integration/Includes/Abilities/SummarizationTest.php index 9cf24ab2..7fec3df8 100644 --- a/tests/Integration/Includes/Abilities/SummarizationTest.php +++ b/tests/Integration/Includes/Abilities/SummarizationTest.php @@ -124,15 +124,20 @@ public function test_input_schema_returns_expected_structure() { $this->assertEquals( 'object', $schema['type'], 'Schema type should be object' ); $this->assertArrayHasKey( 'properties', $schema, 'Schema should have properties' ); $this->assertArrayHasKey( 'content', $schema['properties'], 'Schema should have content property' ); - $this->assertArrayHasKey( 'post_id', $schema['properties'], 'Schema should have post_id property' ); + $this->assertArrayHasKey( 'context', $schema['properties'], 'Schema should have context property' ); + $this->assertArrayHasKey( 'length', $schema['properties'], 'Schema should have length property' ); // Verify content property. $this->assertEquals( 'string', $schema['properties']['content']['type'], 'Content should be string type' ); $this->assertEquals( 'sanitize_text_field', $schema['properties']['content']['sanitize_callback'], 'Content should use sanitize_text_field' ); - // Verify post_id property. - $this->assertEquals( 'integer', $schema['properties']['post_id']['type'], 'Post ID should be integer type' ); - $this->assertEquals( 'absint', $schema['properties']['post_id']['sanitize_callback'], 'Post ID should use absint' ); + // Verify context property. + $this->assertEquals( 'string', $schema['properties']['context']['type'], 'Context should be string type' ); + $this->assertEquals( 'sanitize_text_field', $schema['properties']['context']['sanitize_callback'], 'Context should use sanitize_text_field' ); + + // Verify length property. + $this->assertEquals( 'enum', $schema['properties']['length']['type'], 'Length should be enum type' ); + $this->assertEquals( array( 'short', 'medium', 'long' ), $schema['properties']['length']['enum'], 'Length should be enum with values short, medium, long' ); } /** @@ -197,11 +202,11 @@ public function test_execute_callback_with_content() { } /** - * Test that execute_callback() handles post_id parameter correctly. + * Test that execute_callback() handles context parameter with post ID correctly. * * @since x.x.x */ - public function test_execute_callback_with_post_id() { + public function test_execute_callback_with_context_as_post_id() { $reflection = new \ReflectionClass( $this->ability ); $method = $reflection->getMethod( 'execute_callback' ); $method->setAccessible( true ); @@ -215,7 +220,39 @@ public function test_execute_callback_with_post_id() { ); $input = array( - 'post_id' => $post_id, + 'context' => (string) $post_id, + ); + + try { + $result = $method->invoke( $this->ability, $input ); + } catch ( \Throwable $e ) { + $this->markTestSkipped( 'AI client not available in test environment: ' . $e->getMessage() ); + return; + } + + // Result may be string (success) or WP_Error (if AI client unavailable). + if ( is_wp_error( $result ) ) { + $this->markTestSkipped( 'AI client not available in test environment: ' . $result->get_error_message() ); + return; + } + + $this->assertIsString( $result, 'Result should be a string' ); + $this->assertNotEmpty( $result, 'Result should not be empty' ); + } + + /** + * Test that execute_callback() handles context parameter as string correctly. + * + * @since x.x.x + */ + public function test_execute_callback_with_context_as_string() { + $reflection = new \ReflectionClass( $this->ability ); + $method = $reflection->getMethod( 'execute_callback' ); + $method->setAccessible( true ); + + $input = array( + 'content' => 'This is some test content that needs to be summarized.', + 'context' => 'This is additional context that should be included.', ); try { @@ -236,17 +273,17 @@ public function test_execute_callback_with_post_id() { } /** - * Test that execute_callback() returns error when post_id points to non-existent post. + * Test that execute_callback() returns error when context points to non-existent post. * * @since x.x.x */ - public function test_execute_callback_with_invalid_post_id() { + public function test_execute_callback_with_invalid_post_id_in_context() { $reflection = new \ReflectionClass( $this->ability ); $method = $reflection->getMethod( 'execute_callback' ); $method->setAccessible( true ); $input = array( - 'post_id' => 99999, // Non-existent post ID. + 'context' => '99999', // Non-existent post ID as string. ); $result = $method->invoke( $this->ability, $input ); @@ -330,11 +367,11 @@ public function test_permission_callback_for_logged_out_user() { } /** - * Test that permission_callback() returns true for user with read_post capability. + * Test that permission_callback() returns true for user with read_post capability when context is post ID. * * @since x.x.x */ - public function test_permission_callback_with_post_id_and_read_capability() { + public function test_permission_callback_with_context_as_post_id_and_read_capability() { $reflection = new \ReflectionClass( $this->ability ); $method = $reflection->getMethod( 'permission_callback' ); $method->setAccessible( true ); @@ -351,17 +388,17 @@ public function test_permission_callback_with_post_id_and_read_capability() { $user_id = $this->factory->user->create( array( 'role' => 'subscriber' ) ); wp_set_current_user( $user_id ); - $result = $method->invoke( $this->ability, array( 'post_id' => $post_id ) ); + $result = $method->invoke( $this->ability, array( 'context' => (string) $post_id ) ); $this->assertTrue( $result, 'Permission should be granted for user with read_post capability' ); } /** - * Test that permission_callback() returns error for user without read_post capability. + * Test that permission_callback() returns error for user without read_post capability when context is post ID. * * @since x.x.x */ - public function test_permission_callback_with_post_id_without_read_capability() { + public function test_permission_callback_with_context_as_post_id_without_read_capability() { $reflection = new \ReflectionClass( $this->ability ); $method = $reflection->getMethod( 'permission_callback' ); $method->setAccessible( true ); @@ -378,18 +415,18 @@ public function test_permission_callback_with_post_id_without_read_capability() $user_id = $this->factory->user->create( array( 'role' => 'subscriber' ) ); wp_set_current_user( $user_id ); - $result = $method->invoke( $this->ability, array( 'post_id' => $post_id ) ); + $result = $method->invoke( $this->ability, array( 'context' => (string) $post_id ) ); $this->assertInstanceOf( WP_Error::class, $result, 'Result should be WP_Error' ); $this->assertEquals( 'insufficient_capabilities', $result->get_error_code(), 'Error code should be insufficient_capabilities' ); } /** - * Test that permission_callback() returns error for non-existent post. + * Test that permission_callback() returns error for non-existent post when context is post ID. * * @since x.x.x */ - public function test_permission_callback_with_nonexistent_post_id() { + public function test_permission_callback_with_nonexistent_post_id_in_context() { $reflection = new \ReflectionClass( $this->ability ); $method = $reflection->getMethod( 'permission_callback' ); $method->setAccessible( true ); @@ -397,18 +434,18 @@ public function test_permission_callback_with_nonexistent_post_id() { $user_id = $this->factory->user->create( array( 'role' => 'editor' ) ); wp_set_current_user( $user_id ); - $result = $method->invoke( $this->ability, array( 'post_id' => 99999 ) ); + $result = $method->invoke( $this->ability, array( 'context' => '99999' ) ); $this->assertInstanceOf( WP_Error::class, $result, 'Result should be WP_Error' ); $this->assertEquals( 'post_not_found', $result->get_error_code(), 'Error code should be post_not_found' ); } /** - * Test that permission_callback() returns false for post type without show_in_rest. + * Test that permission_callback() returns false for post type without show_in_rest when context is post ID. * * @since x.x.x */ - public function test_permission_callback_with_post_type_without_show_in_rest() { + public function test_permission_callback_with_post_type_without_show_in_rest_when_context_is_post_id() { $reflection = new \ReflectionClass( $this->ability ); $method = $reflection->getMethod( 'permission_callback' ); $method->setAccessible( true ); @@ -434,7 +471,7 @@ public function test_permission_callback_with_post_type_without_show_in_rest() { $user_id = $this->factory->user->create( array( 'role' => 'editor' ) ); wp_set_current_user( $user_id ); - $result = $method->invoke( $this->ability, array( 'post_id' => $post_id ) ); + $result = $method->invoke( $this->ability, array( 'context' => (string) $post_id ) ); $this->assertFalse( $result, 'Permission should be denied for post type without show_in_rest' ); From e7b5109f230c0a643a7522c858e651264389e655 Mon Sep 17 00:00:00 2001 From: Darin Kotter Date: Wed, 17 Dec 2025 14:24:06 -0700 Subject: [PATCH 14/29] Load the assets on the post edit screen --- .../Summarization/Summarization.php | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/includes/Experiments/Summarization/Summarization.php b/includes/Experiments/Summarization/Summarization.php index 933aa09e..d34fd3fd 100644 --- a/includes/Experiments/Summarization/Summarization.php +++ b/includes/Experiments/Summarization/Summarization.php @@ -11,6 +11,7 @@ use WordPress\AI\Abilities\Summarization\Summarization as Summarization_Ability; use WordPress\AI\Abstracts\Abstract_Experiment; +use WordPress\AI\Asset_Loader; /** * Content summarization experiment. @@ -41,6 +42,7 @@ protected function load_experiment_metadata(): array { */ public function register(): void { add_action( 'wp_abilities_api_init', array( $this, 'register_abilities' ) ); + add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) ); } /** @@ -58,4 +60,28 @@ public function register_abilities(): void { ), ); } + + /** + * Enqueues and localizes the admin script. + * + * @since x.x.x + * + * @param string $hook_suffix The current admin page hook suffix. + */ + public function enqueue_assets( string $hook_suffix ): void { + // Load asset in new post and edit post screens only. + if ( 'post.php' !== $hook_suffix && 'post-new.php' !== $hook_suffix ) { + return; + } + + Asset_Loader::enqueue_script( 'summarization', 'experiments/summarization' ); + Asset_Loader::localize_script( + 'summarization', + 'SummarizationData', + array( + 'enabled' => $this->is_enabled(), + 'path' => Summarization_Ability::path( $this->get_id() ), + ) + ); + } } From 5f8e927a3497d0dd5bf96467cbfacbdb1548a85d Mon Sep 17 00:00:00 2001 From: Darin Kotter Date: Wed, 17 Dec 2025 14:25:56 -0700 Subject: [PATCH 15/29] Build our assets --- src/experiments/summarization/index.tsx | 0 webpack.config.js | 5 +++++ 2 files changed, 5 insertions(+) create mode 100644 src/experiments/summarization/index.tsx diff --git a/src/experiments/summarization/index.tsx b/src/experiments/summarization/index.tsx new file mode 100644 index 00000000..e69de29b diff --git a/webpack.config.js b/webpack.config.js index 49a296e9..fea2d44a 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -24,6 +24,11 @@ module.exports = { 'src/experiments/example-experiment', 'index.tsx' ), + 'experiments/summarization': path.resolve( + process.cwd(), + 'src/experiments/summarization', + 'index.tsx' + ), 'experiments/title-generation': path.resolve( process.cwd(), 'src/experiments/title-generation', From 12d8d47003fa42f76a6d3a90b4acc208ab2ee327 Mon Sep 17 00:00:00 2001 From: Darin Kotter Date: Wed, 17 Dec 2025 14:52:36 -0700 Subject: [PATCH 16/29] Add the base plugin registration and render component --- .../components/SummarizationPlugin.tsx | 53 +++++++++++++++++++ src/experiments/summarization/index.tsx | 17 ++++++ 2 files changed, 70 insertions(+) create mode 100644 src/experiments/summarization/components/SummarizationPlugin.tsx diff --git a/src/experiments/summarization/components/SummarizationPlugin.tsx b/src/experiments/summarization/components/SummarizationPlugin.tsx new file mode 100644 index 00000000..66d04e55 --- /dev/null +++ b/src/experiments/summarization/components/SummarizationPlugin.tsx @@ -0,0 +1,53 @@ +/** + * Summarization plugin component. + */ + +/** + * External dependencies + */ +import React from 'react'; + +/** + * WordPress dependencies + */ +import { Button, Flex, FlexItem } from '@wordpress/components'; +import { PluginPostStatusInfo } from '@wordpress/editor'; +import { __ } from '@wordpress/i18n'; + +/** + * Summarization plugin component. + */ +export default function SummarizationPlugin() { + return ( + + + + + + + + { __( + 'This will create a block that is a summary of the content of this post, generated by AI.', + 'ai' + ) } + + + + + ); +} diff --git a/src/experiments/summarization/index.tsx b/src/experiments/summarization/index.tsx index e69de29b..30aadad0 100644 --- a/src/experiments/summarization/index.tsx +++ b/src/experiments/summarization/index.tsx @@ -0,0 +1,17 @@ +/** + * Summarization experiment plugin registration. + */ + +/** + * WordPress dependencies + */ +import { registerPlugin } from '@wordpress/plugins'; + +/** + * Internal dependencies + */ +import SummarizationPlugin from './components/SummarizationPlugin'; + +registerPlugin( 'classifai-plugin-summarization', { + render: SummarizationPlugin, +} ); From a4007867fb34498642e0f6f9f06e4a88ee58cb90 Mon Sep 17 00:00:00 2001 From: Darin Kotter Date: Wed, 17 Dec 2025 15:16:57 -0700 Subject: [PATCH 17/29] Wire up the generation of a summary when the button is clicked --- .../components/SummarizationPlugin.tsx | 65 +++++++++++++++++-- .../functions/generate-summary.ts | 39 +++++++++++ 2 files changed, 99 insertions(+), 5 deletions(-) create mode 100644 src/experiments/summarization/functions/generate-summary.ts diff --git a/src/experiments/summarization/components/SummarizationPlugin.tsx b/src/experiments/summarization/components/SummarizationPlugin.tsx index 66d04e55..94a620e9 100644 --- a/src/experiments/summarization/components/SummarizationPlugin.tsx +++ b/src/experiments/summarization/components/SummarizationPlugin.tsx @@ -11,13 +11,61 @@ import React from 'react'; * WordPress dependencies */ import { Button, Flex, FlexItem } from '@wordpress/components'; -import { PluginPostStatusInfo } from '@wordpress/editor'; +import { dispatch, select } from '@wordpress/data'; +import { store as editorStore, PluginPostStatusInfo } from '@wordpress/editor'; +import { useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; +import { update } from '@wordpress/icons'; +import { store as noticesStore } from '@wordpress/notices'; + +/** + * Internal dependencies + */ +import { generateSummary } from '../functions/generate-summary'; + +const { aiSummarizationData } = window as any; /** * Summarization plugin component. */ export default function SummarizationPlugin() { + const [ isSummarizing, setIsSummarizing ] = useState< boolean >( false ); + const [ summary, setSummary ] = useState< string >( '' ); + const buttonLabel = summary + ? __( 'Re-generate AI Summary', 'ai' ) + : __( 'Generate AI Summary', 'ai' ); + + // Ensure the experiment is enabled. + if ( ! aiSummarizationData?.enabled ) { + return null; + } + + const postId = select( editorStore ).getCurrentPostId(); + const content = select( editorStore ).getEditedPostContent(); + + /** + * Handles the summarization button click. + */ + const handleSummarize = async () => { + setIsSummarizing( true ); + ( dispatch( noticesStore ) as any ).removeNotice( + 'ai_summarization_error' + ); + + try { + const generatedSummary = await generateSummary( postId, content ); + setSummary( generatedSummary ); + } catch ( error: any ) { + ( dispatch( noticesStore ) as any ).createErrorNotice( error, { + id: 'ai_summarization_error', + isDismissible: true, + } ); + setSummary( '' ); + } finally { + setIsSummarizing( false ); + } + }; + return ( + { summary && ( + + { summary } + + ) } diff --git a/src/experiments/summarization/functions/generate-summary.ts b/src/experiments/summarization/functions/generate-summary.ts new file mode 100644 index 00000000..604519bf --- /dev/null +++ b/src/experiments/summarization/functions/generate-summary.ts @@ -0,0 +1,39 @@ +/** + * WordPress dependencies + */ +import apiFetch from '@wordpress/api-fetch'; + +const { aiSummarizationData } = window as any; + +/** + * Generates a summary for the given post ID and content. + * + * @param {number} postId The ID of the post to generate a summary for. + * @param {string} content The content of the post to generate a summary for. + * @return {Promise} A promise that resolves to the generated summary. + */ +export async function generateSummary( + postId: number, + content: string +): Promise< string > { + return apiFetch( { + path: aiSummarizationData?.path ?? '', + method: 'POST', + data: { + input: { + context: postId.toString(), + content, + }, + }, + } ) + .then( ( response ) => { + if ( response && typeof response === 'string' ) { + return response as string; + } + + throw new Error( 'Invalid response from API' ); + } ) + .catch( ( error ) => { + throw new Error( error.message ); + } ); +} From bb17301eecd8b2472fabb68b09b4f54776985c18 Mon Sep 17 00:00:00 2001 From: Darin Kotter Date: Wed, 17 Dec 2025 15:43:16 -0700 Subject: [PATCH 18/29] Insert a paragraph block with the summary after it is generated. Make the description dynamic --- package-lock.json | 280 ++++++++---------- package.json | 1 + .../components/SummarizationPlugin.tsx | 37 ++- 3 files changed, 147 insertions(+), 171 deletions(-) diff --git a/package-lock.json b/package-lock.json index b213eb8d..31593894 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@wordpress/api-fetch": "^7.36.0", "@wordpress/block-editor": "^15.7.0", + "@wordpress/blocks": "^15.9.0", "@wordpress/components": "^30.7.0", "@wordpress/compose": "^7.34.0", "@wordpress/data": "^10.34.0", @@ -111,7 +112,6 @@ "version": "7.28.5", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1866,7 +1866,6 @@ "integrity": "sha512-eohl3hKTiVyD1ilYdw9T0OiB4hnjef89e3dMYKz+mVKDzj+5IteTseASUsOB+EU9Tf6VNTCjDePcP6wkDGmLKQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@keyv/serialize": "^1.1.1" } @@ -1908,7 +1907,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -1932,7 +1930,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -2106,7 +2103,6 @@ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -3458,7 +3454,6 @@ "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "dev": true, "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=8.0.0" } @@ -3482,7 +3477,6 @@ "integrity": "sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==", "dev": true, "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=14" }, @@ -3496,7 +3490,6 @@ "integrity": "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "1.28.0" }, @@ -3979,7 +3972,6 @@ "integrity": "sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/core": "1.30.1", "@opentelemetry/semantic-conventions": "1.28.0" @@ -4007,7 +3999,6 @@ "integrity": "sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/core": "1.30.1", "@opentelemetry/resources": "1.30.1", @@ -4036,7 +4027,6 @@ "integrity": "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==", "dev": true, "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=14" } @@ -5381,7 +5371,6 @@ "version": "8.1.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -5728,7 +5717,6 @@ "version": "9.6.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -5954,7 +5942,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -5965,7 +5952,6 @@ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -6576,7 +6562,6 @@ "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.9.2" } @@ -6681,7 +6666,6 @@ "version": "6.21.0", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/types": "6.21.0", @@ -7324,13 +7308,13 @@ } }, "node_modules/@wordpress/a11y": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@wordpress/a11y/-/a11y-4.35.0.tgz", - "integrity": "sha512-37DeBnBU20lLpQjwAZx0fkPsrrXT4COMSRhZOiFifv7wCiNW1DIukfqReAc/yZ6x0wOtqXKA3n905D8zm5NF5A==", + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@wordpress/a11y/-/a11y-4.36.0.tgz", + "integrity": "sha512-E69wsv1Ye/B3xeVVDCJO4YzHgCSVWjGPTTMe25I8HBjMyJcNvjGTSJkaWJxwRmKNapFJ7pkeVUNwmk5aJ3apWA==", "license": "GPL-2.0-or-later", "dependencies": { - "@wordpress/dom-ready": "^4.35.0", - "@wordpress/i18n": "^6.8.0" + "@wordpress/dom-ready": "^4.36.0", + "@wordpress/i18n": "^6.9.0" }, "engines": { "node": ">=18.12.0", @@ -7368,9 +7352,9 @@ } }, "node_modules/@wordpress/autop": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@wordpress/autop/-/autop-4.35.0.tgz", - "integrity": "sha512-Tpfl9BOgrOl+SpPDcOA77qotuJIzO7umqlSU4YgBFxxvOQM2L/+dJhBhzngfLKUoF9jcm7j12e7M9X9X216w+Q==", + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@wordpress/autop/-/autop-4.36.0.tgz", + "integrity": "sha512-hmmknzXVOS5/zxkmjtEH1ONeXV/hWGV/MUrj1KhcOoskbyErYjE2hiEDlgpxJofyT/EcH6bawCoAHlVSnH7txw==", "license": "GPL-2.0-or-later", "engines": { "node": ">=18.12.0", @@ -7443,9 +7427,9 @@ } }, "node_modules/@wordpress/blob": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@wordpress/blob/-/blob-4.35.0.tgz", - "integrity": "sha512-8b+A6in2dH+PPhGGI84GwNPyi8mxXUgsTsBG9Ey/WCYRFWXJmqhkgTUnZIi4PvieJbmmAILI9oyfMPSfeAb0sg==", + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@wordpress/blob/-/blob-4.36.0.tgz", + "integrity": "sha512-lctZHSmDgysObyBUPeruG6HhkPcLlAms8kTkwLa76ZS2K2qwG9BbaXeFopZnt10Ti91hOVfwitu+d796lfnNkw==", "license": "GPL-2.0-or-later", "engines": { "node": ">=18.12.0", @@ -7521,9 +7505,9 @@ } }, "node_modules/@wordpress/block-serialization-default-parser": { - "version": "5.35.0", - "resolved": "https://registry.npmjs.org/@wordpress/block-serialization-default-parser/-/block-serialization-default-parser-5.35.0.tgz", - "integrity": "sha512-uLUGNu7Jj7Nk67vkDfAZeNTjLMUuD3PXlWW+75M641wTzGtMrrHSKTNHGoo7W4LyykaDmgfiTToZAu0REIgbJg==", + "version": "5.36.0", + "resolved": "https://registry.npmjs.org/@wordpress/block-serialization-default-parser/-/block-serialization-default-parser-5.36.0.tgz", + "integrity": "sha512-bRJZZUhQkPrV0BL9upamwzkhmEzUDzleqCrIl5bT+GjSO5R1C14oBkeOtJvp7gsMAF+n7QZz/qRaEyxxNRQvIg==", "license": "GPL-2.0-or-later", "engines": { "node": ">=18.12.0", @@ -7531,26 +7515,26 @@ } }, "node_modules/@wordpress/blocks": { - "version": "15.8.0", - "resolved": "https://registry.npmjs.org/@wordpress/blocks/-/blocks-15.8.0.tgz", - "integrity": "sha512-2PpklyJiRvrh7ozH+TTUQr/X0HmpemgG3z4tDi1XX+dDe10FJCvihTJa+/D9sbZQFk1zDJUzN1Kdr0elsGkRpA==", + "version": "15.9.0", + "resolved": "https://registry.npmjs.org/@wordpress/blocks/-/blocks-15.9.0.tgz", + "integrity": "sha512-3y0SzLS/AOVwDdvbHNjbS2YNQb9670ojNq3YYbm6qxfGUzzfitSjKUsREtU2seCiomYygBRLM0cTzgSHQELHpg==", "license": "GPL-2.0-or-later", "dependencies": { - "@wordpress/autop": "^4.35.0", - "@wordpress/blob": "^4.35.0", - "@wordpress/block-serialization-default-parser": "^5.35.0", - "@wordpress/data": "^10.35.0", - "@wordpress/deprecated": "^4.35.0", - "@wordpress/dom": "^4.35.0", - "@wordpress/element": "^6.35.0", - "@wordpress/hooks": "^4.35.0", - "@wordpress/html-entities": "^4.35.0", - "@wordpress/i18n": "^6.8.0", - "@wordpress/is-shallow-equal": "^5.35.0", - "@wordpress/private-apis": "^1.35.0", - "@wordpress/rich-text": "^7.35.0", - "@wordpress/shortcode": "^4.35.0", - "@wordpress/warning": "^3.35.0", + "@wordpress/autop": "^4.36.0", + "@wordpress/blob": "^4.36.0", + "@wordpress/block-serialization-default-parser": "^5.36.0", + "@wordpress/data": "^10.36.0", + "@wordpress/deprecated": "^4.36.0", + "@wordpress/dom": "^4.36.0", + "@wordpress/element": "^6.36.0", + "@wordpress/hooks": "^4.36.0", + "@wordpress/html-entities": "^4.36.0", + "@wordpress/i18n": "^6.9.0", + "@wordpress/is-shallow-equal": "^5.36.0", + "@wordpress/private-apis": "^1.36.0", + "@wordpress/rich-text": "^7.36.0", + "@wordpress/shortcode": "^4.36.0", + "@wordpress/warning": "^3.36.0", "change-case": "^4.1.2", "colord": "^2.7.0", "fast-deep-equal": "^3.1.3", @@ -7703,19 +7687,19 @@ } }, "node_modules/@wordpress/compose": { - "version": "7.35.0", - "resolved": "https://registry.npmjs.org/@wordpress/compose/-/compose-7.35.0.tgz", - "integrity": "sha512-UUQ31Rfi+KFfqT5wkbTgq0nMh+QfbLvshhm30+EGr8R3bYziHhfLKhIL2YQXf4MIX4owOFdKgbAt4GFeVI24ng==", + "version": "7.36.0", + "resolved": "https://registry.npmjs.org/@wordpress/compose/-/compose-7.36.0.tgz", + "integrity": "sha512-Bfz1PueXmGWUxZwiCZb3JvZwErc0DeOy7KyPOAhEBW6SWlRrFI9E2dmPGomj1Kv8uw/u094yuC66PQWlnBboBA==", "license": "GPL-2.0-or-later", "dependencies": { "@types/mousetrap": "^1.6.8", - "@wordpress/deprecated": "^4.35.0", - "@wordpress/dom": "^4.35.0", - "@wordpress/element": "^6.35.0", - "@wordpress/is-shallow-equal": "^5.35.0", - "@wordpress/keycodes": "^4.35.0", - "@wordpress/priority-queue": "^3.35.0", - "@wordpress/undo-manager": "^1.35.0", + "@wordpress/deprecated": "^4.36.0", + "@wordpress/dom": "^4.36.0", + "@wordpress/element": "^6.36.0", + "@wordpress/is-shallow-equal": "^5.36.0", + "@wordpress/keycodes": "^4.36.0", + "@wordpress/priority-queue": "^3.36.0", + "@wordpress/undo-manager": "^1.36.0", "change-case": "^4.1.2", "clipboard": "^2.0.11", "mousetrap": "^1.6.5", @@ -7780,18 +7764,18 @@ } }, "node_modules/@wordpress/data": { - "version": "10.35.0", - "resolved": "https://registry.npmjs.org/@wordpress/data/-/data-10.35.0.tgz", - "integrity": "sha512-rAhyjbnDI9lOPBiJITDOwHJd8+WNmkurqwMJYDjJRqCYAl/mPx9vy6JIgIkyRWQQNiGYrCAv1z6qLqS2Cypg3Q==", + "version": "10.36.0", + "resolved": "https://registry.npmjs.org/@wordpress/data/-/data-10.36.0.tgz", + "integrity": "sha512-87AFd42z7IJWhPKcE8CrSt30BTl2tB5eDTuHjuCXx4W3893VRdr+rKe5xm8lCPlx5kIV/yece/zkIDTqYw6C1Q==", "license": "GPL-2.0-or-later", "dependencies": { - "@wordpress/compose": "^7.35.0", - "@wordpress/deprecated": "^4.35.0", - "@wordpress/element": "^6.35.0", - "@wordpress/is-shallow-equal": "^5.35.0", - "@wordpress/priority-queue": "^3.35.0", - "@wordpress/private-apis": "^1.35.0", - "@wordpress/redux-routine": "^5.35.0", + "@wordpress/compose": "^7.36.0", + "@wordpress/deprecated": "^4.36.0", + "@wordpress/element": "^6.36.0", + "@wordpress/is-shallow-equal": "^5.36.0", + "@wordpress/priority-queue": "^3.36.0", + "@wordpress/private-apis": "^1.36.0", + "@wordpress/redux-routine": "^5.36.0", "deepmerge": "^4.3.0", "equivalent-key-map": "^0.2.2", "is-plain-object": "^5.0.0", @@ -7894,12 +7878,12 @@ "license": "BSD" }, "node_modules/@wordpress/deprecated": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@wordpress/deprecated/-/deprecated-4.35.0.tgz", - "integrity": "sha512-+TruZtmDxRfVw02c1de7ofGC3CrcaPCUXCCbwfP6ruy1zM3IC3rtJfyTbqKPqO1y4uI+9Kqe5VEobqRKT3HQSg==", + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@wordpress/deprecated/-/deprecated-4.36.0.tgz", + "integrity": "sha512-QsyZrQ965f9LEGT88pwUDNAoETVU9T7wJ09w35K5kIzJaDRe8wHbnXv4fuy/MYKGRJUrj3mqg/uXmLIL8SJRpA==", "license": "GPL-2.0-or-later", "dependencies": { - "@wordpress/hooks": "^4.35.0" + "@wordpress/hooks": "^4.36.0" }, "engines": { "node": ">=18.12.0", @@ -7907,12 +7891,12 @@ } }, "node_modules/@wordpress/dom": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@wordpress/dom/-/dom-4.35.0.tgz", - "integrity": "sha512-mM/8m548RaWtkBonpCYHprXgyVFjKcFJWyHfWZz17j01xIbzBq+//cR/Lff86EiEuwynedqlFoEJUcgvPkrJZw==", + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@wordpress/dom/-/dom-4.36.0.tgz", + "integrity": "sha512-dQhTKr/QMQS5TrTwXjeTw6WXgCB4JKZ7vtOEzV06P1EibHcinH1B8V7aCSF2qed1GhTAlrL8xENK/EMxOxYqLA==", "license": "GPL-2.0-or-later", "dependencies": { - "@wordpress/deprecated": "^4.35.0" + "@wordpress/deprecated": "^4.36.0" }, "engines": { "node": ">=18.12.0", @@ -7920,9 +7904,9 @@ } }, "node_modules/@wordpress/dom-ready": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@wordpress/dom-ready/-/dom-ready-4.35.0.tgz", - "integrity": "sha512-u8ifPAFsIAkBG3ehcZdnt+b3t2S0gD3dyDMKgl+GO95wq6kJFqo/NYNQqsjeYUMOFX7cL9D0F2gxiBB4D/AQIA==", + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@wordpress/dom-ready/-/dom-ready-4.36.0.tgz", + "integrity": "sha512-oFhoWcqewtUJ2I3F3YWdrV30LQIPmJgDAgJ+HVt30YSHHBK5Qsb0s0//LeCagkXvXFPXFX2PVyhrq2kq67mqcg==", "license": "GPL-2.0-or-later", "engines": { "node": ">=18.12.0", @@ -8028,14 +8012,14 @@ } }, "node_modules/@wordpress/element": { - "version": "6.35.0", - "resolved": "https://registry.npmjs.org/@wordpress/element/-/element-6.35.0.tgz", - "integrity": "sha512-RmxQsk0ANoHtxDc6anbUH/lHL7Mmbbdn016WKHYVh4+79EzN1tBeM8OML2gHpllUISmIYQvO4wIPSmEVzRh+Gw==", + "version": "6.36.0", + "resolved": "https://registry.npmjs.org/@wordpress/element/-/element-6.36.0.tgz", + "integrity": "sha512-6Ym/Ucik49skz1XJ2GRXENoMjJx7EYnY+fbfor9KtChiCd9/3H4/rI4sZgewVPIO//fCKEk7G30HoR+xB7GZMQ==", "license": "GPL-2.0-or-later", "dependencies": { "@types/react": "^18.2.79", "@types/react-dom": "^18.2.25", - "@wordpress/escape-html": "^3.35.0", + "@wordpress/escape-html": "^3.36.0", "change-case": "^4.1.2", "is-plain-object": "^5.0.0", "react": "^18.3.0", @@ -8050,7 +8034,6 @@ "version": "10.34.0", "dev": true, "license": "GPL-2.0-or-later", - "peer": true, "dependencies": { "@inquirer/prompts": "^7.2.0", "chalk": "^4.0.0", @@ -8074,9 +8057,9 @@ } }, "node_modules/@wordpress/escape-html": { - "version": "3.35.0", - "resolved": "https://registry.npmjs.org/@wordpress/escape-html/-/escape-html-3.35.0.tgz", - "integrity": "sha512-thY7RGAI6UijG3sbBm4IRzMX9lnHkEA/vR3Y0E5BGQYzNFwc+i7t1Lm4scpEBnL3UEQxdlD1wiJTH4pv5olV+Q==", + "version": "3.36.0", + "resolved": "https://registry.npmjs.org/@wordpress/escape-html/-/escape-html-3.36.0.tgz", + "integrity": "sha512-0FvvlVPv+7X8lX5ExcTh6ib/xckGIuVXdnHglR3rZC1MJI682cx4JRUR0Igk6nKyPS8UiSQCKtN3U1aSPtZaCg==", "license": "GPL-2.0-or-later", "engines": { "node": ">=18.12.0", @@ -8238,9 +8221,9 @@ } }, "node_modules/@wordpress/html-entities": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@wordpress/html-entities/-/html-entities-4.35.0.tgz", - "integrity": "sha512-43K8viPNNWUxmCKfKN2Dx/TgJitbGeGCfWBj0aloRoeqOUeEuDkWoak+XiIrJKpwYHTYhqtePl7COMWPzwkqbw==", + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@wordpress/html-entities/-/html-entities-4.36.0.tgz", + "integrity": "sha512-BUocDjjmjmWyVAZ5BQ1RgSYdSxZPXFyy24358Cx8hEyKnFTXfQ83E/UDxrV7KAY3cEOOcNbT7Ru9pSL2XW72nw==", "license": "GPL-2.0-or-later", "engines": { "node": ">=18.12.0", @@ -8325,9 +8308,9 @@ } }, "node_modules/@wordpress/is-shallow-equal": { - "version": "5.35.0", - "resolved": "https://registry.npmjs.org/@wordpress/is-shallow-equal/-/is-shallow-equal-5.35.0.tgz", - "integrity": "sha512-ooM/QFzxu8Ueqsv0D/z0+X6grbVws3GFMSxdM12eZO1B0/x6JPAtq4msZkoxqeQ4zoR0ghYHFXzv3k+Ch1tsFw==", + "version": "5.36.0", + "resolved": "https://registry.npmjs.org/@wordpress/is-shallow-equal/-/is-shallow-equal-5.36.0.tgz", + "integrity": "sha512-nKmFEerYLDgX9X88piYz9+91IoSuWy1UXFlbNWJxImh+VXChMuBkdgyTOGQbLoZEM9BHhvcDB+//J6N+nWAdOg==", "license": "GPL-2.0-or-later", "engines": { "node": ">=18.12.0", @@ -8389,12 +8372,12 @@ } }, "node_modules/@wordpress/keycodes": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@wordpress/keycodes/-/keycodes-4.35.0.tgz", - "integrity": "sha512-+cvb9I4VorJRUsPC4C/2e0QWMz8hkjQIvjz3BjTkIXyWlxsSRSk/+a/YCrfyyA5ysuR9PgwKxxI7/ZaAPvOC6w==", + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@wordpress/keycodes/-/keycodes-4.36.0.tgz", + "integrity": "sha512-qq0s7Ehr6pKWIUuWug7oKfnPCJ+lQ0S0Fl8HCDjHDj8G4kwStxIuCr+8FBESkTeFuIgJaeI2hh1qPPGSivCDRw==", "license": "GPL-2.0-or-later", "dependencies": { - "@wordpress/i18n": "^6.8.0" + "@wordpress/i18n": "^6.9.0" }, "engines": { "node": ">=18.12.0", @@ -8587,9 +8570,9 @@ } }, "node_modules/@wordpress/priority-queue": { - "version": "3.35.0", - "resolved": "https://registry.npmjs.org/@wordpress/priority-queue/-/priority-queue-3.35.0.tgz", - "integrity": "sha512-kLdPx09CnPMPUL5hI/3SUgbBVEka20iWBxM2pvwJGrn3bleY0WxGqLtoUksn6yQds9d7jjS5cKoBYxY1H8MHgg==", + "version": "3.36.0", + "resolved": "https://registry.npmjs.org/@wordpress/priority-queue/-/priority-queue-3.36.0.tgz", + "integrity": "sha512-DcEytCZ2pU7Z9rPw7dpj6Koba0Cl9hbgxqKyypVLlzxJ39DxqZ9GA0FF3f1mzpzNwqp4mWJ0hl4SDXy6yD2kNA==", "license": "GPL-2.0-or-later", "dependencies": { "requestidlecallback": "^0.3.0" @@ -8600,9 +8583,9 @@ } }, "node_modules/@wordpress/private-apis": { - "version": "1.35.0", - "resolved": "https://registry.npmjs.org/@wordpress/private-apis/-/private-apis-1.35.0.tgz", - "integrity": "sha512-gpwYCAuWSjSps7KnmPfUSdJUf5iq8P0ln1eZPcnjs3IwRDUrKW9eUrO0jID+XUsi63qi5c+lv9UkwI8M8C9c5A==", + "version": "1.36.0", + "resolved": "https://registry.npmjs.org/@wordpress/private-apis/-/private-apis-1.36.0.tgz", + "integrity": "sha512-fbkwLei2A7Var0nqiIccKbxzrwASs/dBIYoTzq713I6w3Q9dCARStTs9HHDU2Y+gTdJlGh9cDcacc5IfVJO5kQ==", "license": "GPL-2.0-or-later", "engines": { "node": ">=18.12.0", @@ -8610,9 +8593,9 @@ } }, "node_modules/@wordpress/redux-routine": { - "version": "5.35.0", - "resolved": "https://registry.npmjs.org/@wordpress/redux-routine/-/redux-routine-5.35.0.tgz", - "integrity": "sha512-qc3MLyPWzpujvrP1wVg3xCAuZF+BItGSIIOikMk6xLN63FlNRxjh88gZ65evjU47Ik5KOjYqx3bt/GFdmzJOxw==", + "version": "5.36.0", + "resolved": "https://registry.npmjs.org/@wordpress/redux-routine/-/redux-routine-5.36.0.tgz", + "integrity": "sha512-MTxKSmJavFSwFAJ7nEMWBDPEfign135kcBl2GI6B0EfCy/mHcjCMp2G3nik0yE09MXEX6HlTK6W7j8c/z5WmwA==", "license": "GPL-2.0-or-later", "dependencies": { "is-plain-object": "^5.0.0", @@ -8656,19 +8639,19 @@ } }, "node_modules/@wordpress/rich-text": { - "version": "7.35.0", - "resolved": "https://registry.npmjs.org/@wordpress/rich-text/-/rich-text-7.35.0.tgz", - "integrity": "sha512-GhR9unaSAeVd2UQ6/WgYyAfP/xSE4VD43XuxY6hV4LaU0Qobi/N38Pamkln4eMhORcY8RzNQSkpFX27Nj/fxTQ==", + "version": "7.36.0", + "resolved": "https://registry.npmjs.org/@wordpress/rich-text/-/rich-text-7.36.0.tgz", + "integrity": "sha512-ibvCuO7H1qLB4BuAsljqJYZKB3zWnX3EqPZEMtKG3J1vDfMLOHC66Cwh/77wfWDoubwA8J709eYcmvRX5A8QBA==", "license": "GPL-2.0-or-later", "dependencies": { - "@wordpress/a11y": "^4.35.0", - "@wordpress/compose": "^7.35.0", - "@wordpress/data": "^10.35.0", - "@wordpress/deprecated": "^4.35.0", - "@wordpress/element": "^6.35.0", - "@wordpress/escape-html": "^3.35.0", - "@wordpress/i18n": "^6.8.0", - "@wordpress/keycodes": "^4.35.0", + "@wordpress/a11y": "^4.36.0", + "@wordpress/compose": "^7.36.0", + "@wordpress/data": "^10.36.0", + "@wordpress/deprecated": "^4.36.0", + "@wordpress/element": "^6.36.0", + "@wordpress/escape-html": "^3.36.0", + "@wordpress/i18n": "^6.9.0", + "@wordpress/keycodes": "^4.36.0", "colord": "2.9.3", "memize": "^2.1.0" }, @@ -8842,9 +8825,9 @@ } }, "node_modules/@wordpress/shortcode": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@wordpress/shortcode/-/shortcode-4.35.0.tgz", - "integrity": "sha512-+7dcPxBeee9LLwhS88MoNuLndoQQxwyQLlXCzWX0Min6YLWFXd0b+hmsRUlOE7qzbuZbHyW3mCk8ot9p3xK3sw==", + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@wordpress/shortcode/-/shortcode-4.36.0.tgz", + "integrity": "sha512-MhSlWzQ8t9oMTB7S04C/M+mejzzo8Rv5JDYVtLpAAbkP7U0I5VpY7lk5C7BIHRGXkIPYwQMs+yos6u3qDap7rw==", "license": "GPL-2.0-or-later", "dependencies": { "memize": "^2.0.1" @@ -8920,12 +8903,12 @@ } }, "node_modules/@wordpress/undo-manager": { - "version": "1.35.0", - "resolved": "https://registry.npmjs.org/@wordpress/undo-manager/-/undo-manager-1.35.0.tgz", - "integrity": "sha512-WyJWGLhLvkB2FF/o+QmgAoPo3NTEi5HQDwYbSEpw1pUZm7W3U/QwcaW6cWW3D/9OUtnHY4kfMGxrI5del2tYyA==", + "version": "1.36.0", + "resolved": "https://registry.npmjs.org/@wordpress/undo-manager/-/undo-manager-1.36.0.tgz", + "integrity": "sha512-WwGmLqqDViDoM7JcX+HEoAhan/DprteVuaSsFfn6N4Kj9od6egkz7VTmmbc3+oxkDSt+g1fJ/wJsocLII95fKg==", "license": "GPL-2.0-or-later", "dependencies": { - "@wordpress/is-shallow-equal": "^5.35.0" + "@wordpress/is-shallow-equal": "^5.36.0" }, "engines": { "node": ">=18.12.0", @@ -9003,9 +8986,9 @@ } }, "node_modules/@wordpress/warning": { - "version": "3.35.0", - "resolved": "https://registry.npmjs.org/@wordpress/warning/-/warning-3.35.0.tgz", - "integrity": "sha512-2UGZuHenf84UHdotBxv9ZCtlsFIy5u4QTUPBnx1gH4N9zEuJs+JiCtlOzgcl0JzT3xFK5y3cXLLVqlhf8tDMBQ==", + "version": "3.36.0", + "resolved": "https://registry.npmjs.org/@wordpress/warning/-/warning-3.36.0.tgz", + "integrity": "sha512-iCh9laMVxFHg/pcp4s7FN7g4Z4lLFLPBN2vAZ7JlglPHMr2kjw34MnkUNZZiqyis7tzHVkww5E7WohUcooKO5Q==", "license": "GPL-2.0-or-later", "engines": { "node": ">=18.12.0", @@ -9061,7 +9044,6 @@ "version": "8.15.0", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -9141,7 +9123,6 @@ "version": "6.12.6", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -10055,7 +10036,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -11691,8 +11671,7 @@ "node_modules/devtools-protocol": { "version": "0.0.1507524", "dev": true, - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/diff": { "version": "4.0.2", @@ -12290,7 +12269,6 @@ "version": "8.57.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -12345,7 +12323,6 @@ "version": "8.10.2", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -12458,7 +12435,6 @@ "version": "2.32.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -15560,7 +15536,6 @@ "version": "29.7.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -16561,8 +16536,7 @@ "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1521046.tgz", "integrity": "sha512-vhE6eymDQSKWUXwwA37NtTTVEzjtGVfDr3pRbsWEQ5onH/Snp2c+2xZHWJJawG/0hCCJLRGt4xVtEVUVILol4w==", "dev": true, - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/lighthouse/node_modules/puppeteer-core/node_modules/ws": { "version": "8.18.3", @@ -17599,7 +17573,6 @@ "integrity": "sha512-cuXAJJB1Rdqz0UO6w524matlBqDBjcNt7Ru+RDIu4y6RI1gVqiWBnylrK8sPRk81gGBA0X8hJbDXolVOoTc+sA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ajv": "^6.12.6", "ajv-errors": "^1.0.1", @@ -18562,6 +18535,7 @@ "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "playwright-core": "1.56.1" }, @@ -18581,6 +18555,7 @@ "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "playwright-core": "cli.js" }, @@ -18599,6 +18574,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } @@ -18644,7 +18620,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -19316,7 +19291,6 @@ "resolved": "https://registry.npmjs.org/preact/-/preact-10.27.2.tgz", "integrity": "sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==", "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -19336,7 +19310,6 @@ "resolved": "https://registry.npmjs.org/wp-prettier/-/wp-prettier-3.0.3.tgz", "integrity": "sha512-X4UlrxDTH8oom9qXlcjnydsjAOD2BmB6yFmvS4Z2zdTzqqpRWb+fbqrH412+l+OUXmbzJlSXjlMFYPgYG12IAA==", "dev": true, - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -19681,7 +19654,6 @@ "node_modules/react": { "version": "18.3.1", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -19748,7 +19720,6 @@ "node_modules/react-dom": { "version": "18.3.1", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -19779,7 +19750,6 @@ "version": "0.14.2", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -20002,8 +19972,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", @@ -20505,7 +20474,6 @@ "version": "1.93.3", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -20600,7 +20568,6 @@ "version": "8.17.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -21964,7 +21931,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", @@ -22300,7 +22266,6 @@ "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -22821,7 +22786,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -22998,8 +22962,7 @@ }, "node_modules/tslib": { "version": "2.8.1", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tsutils": { "version": "3.21.0", @@ -23043,7 +23006,6 @@ "version": "0.20.2", "dev": true, "license": "(MIT OR CC0-1.0)", - "peer": true, "engines": { "node": ">=10" }, @@ -23149,7 +23111,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -23252,7 +23213,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "napi-postinstall": "^0.3.0" }, @@ -23623,7 +23583,6 @@ "version": "5.102.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -23724,7 +23683,6 @@ "version": "5.1.4", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", @@ -23824,7 +23782,6 @@ "version": "5.2.2", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/bonjour": "^3.5.13", "@types/connect-history-api-fallback": "^1.5.4", @@ -24485,7 +24442,6 @@ "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.27.tgz", "integrity": "sha512-OIDwaflOaq4wC6YlPBy2L6ceKeKuF7DeTxx+jPzv1FHn9tCZ0ZwSRnUBxD05E3yed46fv/FWJbvR+Ud7x0L7zw==", "license": "MIT", - "peer": true, "dependencies": { "lib0": "^0.2.99" }, diff --git a/package.json b/package.json index edc22f78..3f2e7971 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "dependencies": { "@wordpress/api-fetch": "^7.36.0", "@wordpress/block-editor": "^15.7.0", + "@wordpress/blocks": "^15.9.0", "@wordpress/components": "^30.7.0", "@wordpress/compose": "^7.34.0", "@wordpress/data": "^10.34.0", diff --git a/src/experiments/summarization/components/SummarizationPlugin.tsx b/src/experiments/summarization/components/SummarizationPlugin.tsx index 94a620e9..719b79c3 100644 --- a/src/experiments/summarization/components/SummarizationPlugin.tsx +++ b/src/experiments/summarization/components/SummarizationPlugin.tsx @@ -10,6 +10,8 @@ import React from 'react'; /** * WordPress dependencies */ +import { createBlock } from '@wordpress/blocks'; +import { store as blockEditorStore } from '@wordpress/block-editor'; import { Button, Flex, FlexItem } from '@wordpress/components'; import { dispatch, select } from '@wordpress/data'; import { store as editorStore, PluginPostStatusInfo } from '@wordpress/editor'; @@ -25,6 +27,15 @@ import { generateSummary } from '../functions/generate-summary'; const { aiSummarizationData } = window as any; +/** + * TODO: + * - Find way to persist summary between refreshes. We need to check if a summary block exists. + * - Block variation maybe? + * - Find a way to add a regenerate button to this block. + * - Replace block when a regeneration button is clicked. + * - Store the summary in the post meta. +*/ + /** * Summarization plugin component. */ @@ -34,6 +45,15 @@ export default function SummarizationPlugin() { const buttonLabel = summary ? __( 'Re-generate AI Summary', 'ai' ) : __( 'Generate AI Summary', 'ai' ); + const buttonDescription = summary + ? __( + 'This will update the AI generated summary block with a new summary of the content of this post.', + 'ai' + ) + : __( + 'This will create a block that is a summary of the content of this post, generated by AI.', + 'ai' + ); // Ensure the experiment is enabled. if ( ! aiSummarizationData?.enabled ) { @@ -55,6 +75,13 @@ export default function SummarizationPlugin() { try { const generatedSummary = await generateSummary( postId, content ); setSummary( generatedSummary ); + + // Insert a new paragraph block with the generated summary. + const summaryBlock = createBlock( 'core/paragraph', { + content: generatedSummary, + className: 'ai-summarization-summary', + } ); + dispatch( blockEditorStore ).insertBlock( summaryBlock, 0 ); } catch ( error: any ) { ( dispatch( noticesStore ) as any ).createErrorNotice( error, { id: 'ai_summarization_error', @@ -73,11 +100,6 @@ export default function SummarizationPlugin() { className="ai-summarization-plugin-container" gap={ 2 } > - { summary && ( - - { summary } - - ) }