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 / Editorial_Notes / Editorial_Notes.php

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

162 lines 4.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Editorial Notes experiment implementation.
4 *
5 * @package WordPress\AI
6 */
7
8 declare( strict_types=1 );
9
10 namespace WordPress\AI\Experiments\Editorial_Notes;
11
12 use WordPress\AI\Abilities\Editorial_Notes\Editorial_Notes as Editorial_Notes_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_min_content_length;
18
19 // Exit if accessed directly.
20 if ( ! defined( 'ABSPATH' ) ) {
21 exit;
22 }
23
24 /**
25 * Editorial Notes experiment.
26 *
27 * Runs a block-by-block review pass on post content, creating WordPress Notes
28 * with actionable suggestions for Accessibility, Readability, Grammar, and SEO.
29 *
30 * @since 0.4.0
31 */
32 class Editorial_Notes extends Abstract_Feature {
33
34 /**
35 * {@inheritDoc}
36 */
37 public static function get_id(): string {
38 return 'editorial-notes';
39 }
40
41 /**
42 * {@inheritDoc}
43 */
44 protected function load_metadata(): array {
45 return array(
46 'label' => __( 'Editorial Notes', 'ai' ),
47 'description' => __( 'Adds editorial suggestions to posts block-by-block, covering Accessibility, Readability, Grammar, and SEO. Requires an AI connector that includes support for text generation models.', 'ai' ),
48 'category' => Experiment_Category::EDITOR,
49 );
50 }
51
52 /**
53 * {@inheritDoc}
54 */
55 public function register(): void {
56 add_action( 'wp_abilities_api_init', array( $this, 'register_abilities' ) );
57 add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_assets' ) );
58 add_filter( 'rest_pre_insert_comment', array( $this, 'maybe_set_ai_author' ), 10, 2 );
59
60 register_meta(
61 'comment',
62 'wpai_note',
63 array(
64 'type' => 'boolean',
65 'single' => true,
66 'show_in_rest' => true,
67 'auth_callback' => static function ( $allowed, $meta_key, $comment_id ): bool {
68 $comment = get_comment( $comment_id );
69
70 if ( ! $comment instanceof \WP_Comment ) {
71 return false;
72 }
73
74 return current_user_can( 'edit_post', (int) $comment->comment_post_ID );
75 },
76 )
77 );
78 }
79
80 /**
81 * Registers any needed abilities.
82 *
83 * @since 0.4.0
84 */
85 public function register_abilities(): void {
86 wp_register_ability(
87 'ai/' . $this->get_id(),
88 array(
89 'label' => $this->get_label(),
90 'description' => $this->get_description(),
91 'ability_class' => Editorial_Notes_Ability::class,
92 ),
93 );
94 }
95
96 /**
97 * Overrides the author fields for AI-generated Notes before they are inserted.
98 *
99 * Fires via the rest_pre_insert_comment filter. When the REST request includes
100 * meta.wpai_note = true on a Note (comment_type "note") created by a user who can
101 * edit the target post, replaces the authenticated user's identity with a generic
102 * "AI" author so Notes are not attributed to a personal account.
103 *
104 * @since 0.4.0
105 *
106 * @param array<string, mixed>|\WP_Error $prepared_comment The prepared comment data for wp_insert_comment().
107 * @param \WP_REST_Request<array<string, mixed>> $request The REST API request.
108 * @return array<string, mixed>|\WP_Error Modified comment data, original on non-AI requests, or WP_Error if unauthorized.
109 */
110 public function maybe_set_ai_author( $prepared_comment, \WP_REST_Request $request ) {
111 if ( is_wp_error( $prepared_comment ) ) {
112 return $prepared_comment;
113 }
114
115 $meta = $request->get_param( 'meta' );
116
117 if ( ! is_array( $meta ) || empty( $meta['wpai_note'] ) ) {
118 return $prepared_comment;
119 }
120
121 // Ensure the user has permission to edit the post.
122 $post_id = (int) ( $prepared_comment['comment_post_ID'] ?? $request->get_param( 'post' ) );
123
124 if ( ! $post_id || ! current_user_can( 'edit_post', $post_id ) ) {
125 return new \WP_Error(
126 'ai_note_forbidden',
127 __( 'Sorry, you are not allowed to create AI Notes.', 'ai' ),
128 array( 'status' => rest_authorization_required_code() )
129 );
130 }
131
132 // The AI author identity is only ever applied to Notes.
133 if ( 'note' !== ( $prepared_comment['comment_type'] ?? '' ) ) {
134 return $prepared_comment;
135 }
136
137 $prepared_comment['comment_author'] = __( 'WordPress AI', 'ai' );
138 $prepared_comment['comment_author_email'] = '';
139 $prepared_comment['comment_author_url'] = '';
140 $prepared_comment['user_id'] = 0;
141
142 return $prepared_comment;
143 }
144
145 /**
146 * Enqueues and localizes the block editor script.
147 *
148 * @since 0.4.0
149 */
150 public function enqueue_assets(): void {
151 Asset_Loader::enqueue_script( 'editorial_notes', 'experiments/editorial-notes', array( 'include_core_abilities' => true ) );
152 Asset_Loader::localize_script(
153 'editorial_notes',
154 'EditorialNotesData',
155 array(
156 'enabled' => $this->is_enabled(),
157 'minContentLength' => get_min_content_length( 'editorial-notes', 75 ),
158 )
159 );
160 }
161 }
162