PluginProbe
AI / trunk
AI vtrunk
1.3.0 1.2.0 1.1.0 1.0.2 1.0.1 1.0.0 0.9.0 trunk 0.1.1 0.2.0 0.2.1 0.3.0 0.3.1 0.4.0 0.4.1 0.5.0 0.6.0 0.7.0 0.8.0
ai / includes / Experiments / Summarization / Summarization.php

Summarization.php in AI trunk, at includes/Experiments/Summarization/Summarization.php

340 lines 10.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Content summarization experiment implementation.
4 *
5 * @package WordPress\AI
6 */
7
8 declare( strict_types=1 );
9
10 namespace WordPress\AI\Experiments\Summarization;
11
12 use WordPress\AI\Abilities\Summarization\Summarization as Summarization_Ability;
13 use WordPress\AI\Abstracts\Abstract_Feature;
14 use WordPress\AI\Asset_Loader;
15 use WordPress\AI\Experiments\Experiment_Category;
16
17 use function WordPress\AI\get_bulk_action_max_items;
18 use function WordPress\AI\get_min_content_length;
19 use function WordPress\AI\post_type_supports_bulk_action;
20
21 // Exit if accessed directly.
22 if ( ! defined( 'ABSPATH' ) ) {
23 exit;
24 }
25
26 /**
27 * Content summarization experiment.
28 *
29 * @since 0.2.0
30 */
31 class Summarization extends Abstract_Feature {
32
33 /**
34 * One-shot query args the bulk action redirect uses to trigger generation.
35 *
36 * @since 1.3.0
37 *
38 * @var list<string>
39 */
40 private const BULK_QUERY_ARGS = array( 'wpai_bulk_summary', 'wpai_post_ids', '_wpai_bulk_nonce' ); // phpcs:ignore SlevomatCodingStandard.Classes.DisallowMultiConstantDefinition -- This is used as an array const.
41
42 /**
43 * Nonce action signing the bulk action redirect.
44 *
45 * @since x.x.x
46 *
47 * @var string
48 */
49 private const BULK_NONCE_ACTION = 'wpai_bulk_summary';
50
51 /**
52 * {@inheritDoc}
53 */
54 public static function get_id(): string {
55 return 'summarization';
56 }
57
58 /**
59 * {@inheritDoc}
60 */
61 protected function load_metadata(): array {
62 return array(
63 'label' => __( 'Content Summarization', 'ai' ),
64 'description' => __( 'Summarizes long-form content into digestible overviews. Requires an AI connector that includes support for text generation models.', 'ai' ),
65 'category' => Experiment_Category::EDITOR,
66 );
67 }
68
69 /**
70 * {@inheritDoc}
71 */
72 public function register(): void {
73 $this->register_post_meta();
74 add_action( 'wp_abilities_api_init', array( $this, 'register_abilities' ) );
75 add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_assets' ), 5 );
76 add_action( 'enqueue_block_assets', array( $this, 'enqueue_block_assets' ) );
77
78 add_action( 'load-edit.php', array( $this, 'register_bulk_action_hooks_for_screen' ) );
79 add_action( 'admin_enqueue_scripts', array( $this, 'maybe_enqueue_bulk_assets' ) );
80 add_filter( 'removable_query_args', array( $this, 'register_removable_query_args' ) );
81 }
82
83 /**
84 * Registers the bulk summary trigger params as removable query args.
85 *
86 * The bulk action redirect carries `wpai_bulk_summary` and `wpai_post_ids`
87 * in the URL, and the bulk script runs whenever they are present. Listing
88 * them here lets core clean them out of the address bar on the first paint,
89 * via the canonical URL it prints in `admin_head`, so reloading the results
90 * page does not re-trigger the whole generation. The sort and pagination
91 * links are handled by the request URI scrub in
92 * {@see Summarization::maybe_enqueue_bulk_assets()}.
93 *
94 * @since 1.3.0
95 *
96 * @param list<string> $args Query args removed from admin URLs.
97 * @return list<string> Args including the bulk summary trigger params.
98 */
99 public function register_removable_query_args( array $args ): array {
100 return array_merge( $args, self::BULK_QUERY_ARGS );
101 }
102
103 /**
104 * Registers the bulk action hooks for the current post list table screen.
105 *
106 * Hooked to load-edit.php so it only fires on post list tables. Reads the
107 * requested post type from the query string and restricts bulk summarization
108 * to post types exposed via the REST API.
109 *
110 * @since 1.2.0
111 */
112 public function register_bulk_action_hooks_for_screen(): void {
113 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
114 $post_type = isset( $_GET['post_type'] ) ? sanitize_key( $_GET['post_type'] ) : 'post';
115
116 if ( ! post_type_supports_bulk_action( $post_type, $this->get_id() ) ) {
117 return;
118 }
119
120 add_filter( "bulk_actions-edit-{$post_type}", array( $this, 'register_bulk_action' ) );
121 add_filter( "handle_bulk_actions-edit-{$post_type}", array( $this, 'handle_bulk_action' ), 10, 3 );
122 }
123
124 /**
125 * Register any needed post meta.
126 *
127 * @since 0.3.0
128 */
129 public function register_post_meta(): void {
130 register_meta(
131 'post',
132 'wpai_generated_summary',
133 array(
134 'type' => 'string',
135 'single' => true,
136 'show_in_rest' => true,
137 )
138 );
139 }
140
141 /**
142 * Registers any needed abilities.
143 *
144 * @since 0.2.0
145 */
146 public function register_abilities(): void {
147 wp_register_ability(
148 'ai/' . $this->get_id(),
149 array(
150 'label' => $this->get_label(),
151 'description' => $this->get_description(),
152 'ability_class' => Summarization_Ability::class,
153 ),
154 );
155 }
156
157 /**
158 * Enqueues and localizes the block editor script.
159 *
160 * @since 0.3.0
161 */
162 public function enqueue_assets(): void {
163 $screen = get_current_screen();
164 if ( ! $screen || 'post' !== $screen->base ) {
165 return;
166 }
167
168 Asset_Loader::enqueue_script( 'summarization', 'experiments/summarization', array( 'include_core_abilities' => true ) );
169
170 Asset_Loader::localize_script(
171 'summarization',
172 'SummarizationData',
173 array(
174 'enabled' => $this->is_enabled(),
175 'minContentLength' => $this->get_min_content_length(),
176 )
177 );
178 }
179
180 /**
181 * Gets the minimum content length required to enable summarization.
182 *
183 * @since 1.2.0
184 *
185 * @return int The minimum number of characters required.
186 */
187 protected function get_min_content_length(): int {
188 /**
189 * Filters the minimum content length required to enable summarization.
190 *
191 * @since 1.0.0
192 * @deprecated 1.1.0 Use {@see 'wpai_min_content_length'} instead.
193 *
194 * @param int $min_content_length The minimum number of characters required. Default 250.
195 */
196 return (int) apply_filters_deprecated(
197 'wpai_summarization_min_content_length',
198 array( get_min_content_length( 'summarization', 250 ) ),
199 '1.1.0',
200 'wpai_min_content_length'
201 );
202 }
203
204 /**
205 * Adds the "Generate Summary" option to the posts list bulk actions menu.
206 *
207 * @since 1.2.0
208 *
209 * @param array<string, string> $actions The existing bulk actions.
210 * @return array<string, string> The modified bulk actions.
211 */
212 public function register_bulk_action( array $actions ): array {
213 if ( ! $this->is_enabled() ) {
214 return $actions;
215 }
216
217 $actions['wpai_generate_summary'] = __( 'Generate Summary', 'ai' );
218
219 return $actions;
220 }
221
222 /**
223 * Handles the "Generate Summary" bulk action by redirecting with selected post IDs.
224 *
225 * The actual generation is performed client-side after the redirect so that slow
226 * AI API calls do not risk hitting PHP's execution time limit.
227 *
228 * @since 1.2.0
229 *
230 * @param string $redirect_url The current redirect URL.
231 * @param string $doaction The bulk action being performed.
232 * @param list<int> $post_ids The list of post IDs to process.
233 * @return string The redirect URL, possibly with bulk summary query args appended.
234 */
235 public function handle_bulk_action( string $redirect_url, string $doaction, array $post_ids ): string {
236 if ( 'wpai_generate_summary' !== $doaction || ! current_user_can( 'edit_posts' ) ) {
237 return $redirect_url;
238 }
239
240 // Only keep posts the current user is allowed to edit.
241 $editable_ids = array_values(
242 array_filter(
243 $post_ids,
244 static function ( $id ) {
245 return current_user_can( 'edit_post', (int) $id );
246 }
247 )
248 );
249
250 if ( empty( $editable_ids ) ) {
251 return $redirect_url;
252 }
253
254 return add_query_arg(
255 array(
256 'wpai_bulk_summary' => 1,
257 'wpai_post_ids' => implode( ',', array_map( 'absint', $editable_ids ) ),
258 '_wpai_bulk_nonce' => wp_create_nonce( self::BULK_NONCE_ACTION ),
259 ),
260 $redirect_url
261 );
262 }
263
264 /**
265 * Enqueues the bulk summarization script when a bulk action redirect is detected.
266 *
267 * @since 1.2.0
268 *
269 * @param string $hook_suffix Current admin page hook suffix.
270 */
271 public function maybe_enqueue_bulk_assets( string $hook_suffix ): void {
272 if ( 'edit.php' !== $hook_suffix || ! isset( $_GET['wpai_bulk_summary'] ) || ! current_user_can( 'edit_posts' ) ) {
273 return;
274 }
275
276 $nonce = isset( $_GET['_wpai_bulk_nonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpai_bulk_nonce'] ) ) : '';
277
278 if ( ! wp_verify_nonce( $nonce, self::BULK_NONCE_ACTION ) ) {
279 return;
280 }
281
282 $raw_ids = isset( $_GET['wpai_post_ids'] ) ? sanitize_text_field( wp_unslash( $_GET['wpai_post_ids'] ) ) : '';
283 $ids = array_values( array_unique( array_filter( array_map( 'absint', explode( ',', $raw_ids ) ) ) ) );
284
285 if ( empty( $ids ) ) {
286 return;
287 }
288
289 // One billed model call per post, so bound the batch.
290 $max_items = get_bulk_action_max_items( $this->get_id() );
291 $truncated_count = max( 0, count( $ids ) - $max_items );
292 $ids = array_slice( $ids, 0, $max_items );
293
294 /*
295 * The trigger params have been read; scrub them from the request URI so
296 * the sort header links the list table builds from it do not carry them.
297 * Sorting links only strip `paged`, not removable query args, so this
298 * mirrors what core does for its own one-shot params in wp-admin/edit.php.
299 * The script receives the post IDs through wp_localize_script() below and
300 * does not need them to stay in the URL. The value is only rewritten, not
301 * output, so no sanitization applies.
302 */
303 if ( isset( $_SERVER['REQUEST_URI'] ) ) {
304 $_SERVER['REQUEST_URI'] = remove_query_arg( self::BULK_QUERY_ARGS, (string) $_SERVER['REQUEST_URI'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
305 }
306
307 // Resolve the REST base once all posts in a list table share the same post type.
308 $post_type = isset( $_GET['post_type'] ) ? sanitize_key( $_GET['post_type'] ) : 'post';
309
310 // Mirror the post type restriction the bulk action itself is registered under.
311 if ( ! post_type_supports_bulk_action( $post_type, $this->get_id() ) ) {
312 return;
313 }
314
315 $post_type_obj = get_post_type_object( $post_type );
316 $rest_base = $post_type_obj && $post_type_obj->rest_base ? (string) $post_type_obj->rest_base : 'posts';
317
318 Asset_Loader::enqueue_script( 'summarization_bulk', 'experiments/summarization-bulk', array( 'include_core_abilities' => true ) );
319 Asset_Loader::localize_script(
320 'summarization_bulk',
321 'SummarizationBulkData',
322 array(
323 'postIds' => $ids,
324 'restBase' => $rest_base,
325 'minContentLength' => $this->get_min_content_length(),
326 'truncatedCount' => $truncated_count,
327 )
328 );
329 }
330
331 /**
332 * Enqueues the block stylesheet for the editor iframe and the front end.
333 *
334 * @since 0.9.0
335 */
336 public function enqueue_block_assets(): void {
337 Asset_Loader::enqueue_style( 'summarization', 'experiments/summarization' );
338 }
339 }
340