Add Comment Moderation experiment#155
Add Comment Moderation experiment#155Jameswlepage wants to merge 6 commits intoWordPress:developfrom
Conversation
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
Adds AI-powered comment moderation with: - Toxicity scoring and sentiment analysis badges in Comments list - Lazy analysis that processes pending comments on page load - Bulk action to queue multiple comments for analysis - AI Reply suggestions modal with tone selection - Comment meta storage for analysis results Includes shared run-ability.ts utility for Abilities API fallback and get_model_preferences() method for model selection.
8316832 to
0a7405d
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## develop #155 +/- ##
=============================================
- Coverage 50.80% 41.67% -9.14%
- Complexity 375 447 +72
=============================================
Files 27 32 +5
Lines 1978 2426 +448
=============================================
+ Hits 1005 1011 +6
- Misses 973 1415 +442
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR adds a comprehensive AI-powered comment moderation experiment to WordPress, enabling automated toxicity detection, sentiment analysis, and AI-generated reply suggestions directly within the Comments admin screen.
Key Changes
- Introduces two new WordPress Abilities:
ai/comment-analysisfor toxicity/sentiment scoring andai/reply-suggestionfor generating contextual reply drafts - Implements React-based UI with lazy analysis of pending comments and an interactive reply modal with tone selection
- Adds a shared
run-ability.tsutility that provides Abilities API client support with REST API fallback
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 17 comments.
Show a summary per file
| File | Description |
|---|---|
webpack.config.js |
Adds webpack entry point for comment-moderation experiment bundle |
src/utils/run-ability.ts |
New shared utility for executing abilities with client/REST fallback |
src/experiments/comment-moderation/index.tsx |
React entry point mounting lazy analysis and reply modal controllers |
src/experiments/comment-moderation/components/ReplyModalController.tsx |
Manages reply modal state and WordPress inline reply form integration |
src/experiments/comment-moderation/components/ReplyModal.tsx |
UI for displaying and selecting AI-generated reply suggestions |
src/experiments/comment-moderation/components/LazyAnalysisController.tsx |
Detects and processes pending comment analysis on page load |
includes/Experiments/Comment_Moderation/Comment_Moderation.php |
Main experiment class with column rendering, bulk actions, and asset enqueuing |
includes/Experiment_Loader.php |
Registers Comment_Moderation experiment in loader |
includes/Abstracts/Abstract_Ability.php |
Adds get_model_preferences() helper method to base ability class |
includes/Abilities/Comment_Moderation/Reply_Suggestion.php |
Ability implementation for generating reply suggestions with tone control |
includes/Abilities/Comment_Moderation/Comment_Analysis.php |
Ability implementation for analyzing comment toxicity and sentiment |
includes/Abilities/Comment_Moderation/system-instruction.php |
AI system prompt for comment analysis with JSON output format |
includes/Abilities/Comment_Moderation/reply-system-instruction.php |
AI system prompt for generating contextual comment replies |
docs/experiments/comment-moderation.md |
Documentation covering hooks, data flow, and testing procedures |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| - 0.0-0.3: Low toxicity (constructive, polite, or neutral) | ||
| - 0.4-0.6: Medium toxicity (mildly rude, dismissive, or heated) |
There was a problem hiding this comment.
The toxicity score thresholds in the system instruction (0.0-0.3, 0.4-0.6, 0.7-1.0) don't align perfectly with the code's thresholds (0.4 and 0.7). The instruction has a gap at 0.3-0.4 range. The instructions should use consistent ranges like 0.0-0.39 for low, 0.4-0.69 for medium, and 0.7-1.0 for high to match the code logic.
| - 0.0-0.3: Low toxicity (constructive, polite, or neutral) | |
| - 0.4-0.6: Medium toxicity (mildly rude, dismissive, or heated) | |
| - 0.0-0.39: Low toxicity (constructive, polite, or neutral) | |
| - 0.4-0.69: Medium toxicity (mildly rude, dismissive, or heated) |
| /** | ||
| * External dependencies | ||
| */ | ||
| import React from 'react'; |
There was a problem hiding this comment.
The comment says 'WordPress dependencies' but the code imports React from 'react' which is an external dependency, not a WordPress dependency. The React import should be under the 'External dependencies' section, not 'WordPress dependencies'.
| /** | ||
| * External dependencies | ||
| */ | ||
| import React from 'react'; |
There was a problem hiding this comment.
The comment says 'WordPress dependencies' but the code imports React from 'react' which is an external dependency, not a WordPress dependency. The React import should be under the 'External dependencies' section, not 'WordPress dependencies'.
| // Find the reply button for this comment and trigger WordPress's inline reply. | ||
| const replyButton = document.querySelector< HTMLButtonElement >( | ||
| `#comment-${ commentId } .reply button` | ||
| ); | ||
|
|
||
| if ( replyButton ) { | ||
| // Click the reply button to open the inline reply form. | ||
| replyButton.click(); |
There was a problem hiding this comment.
The reply button selector '#comment-${ commentId } .reply button' is fragile and depends on WordPress's internal DOM structure. If WordPress changes its comment row HTML structure, this will break. Consider adding a data attribute to the reply button in PHP or using a more robust selector strategy.
| // Find the reply button for this comment and trigger WordPress's inline reply. | |
| const replyButton = document.querySelector< HTMLButtonElement >( | |
| `#comment-${ commentId } .reply button` | |
| ); | |
| if ( replyButton ) { | |
| // Click the reply button to open the inline reply form. | |
| replyButton.click(); | |
| // Find the reply control for this comment and trigger WordPress's inline reply. | |
| const commentRow = document.getElementById( `comment-${ commentId }` ); | |
| const replyControl = commentRow | |
| ? commentRow.querySelector< HTMLElement >( | |
| '.reply button, .reply a, [data-action="reply"], [aria-label="Reply"], [aria-label="Reply to this comment"]' | |
| ) | |
| : null; | |
| if ( replyControl ) { | |
| // Click the reply control to open the inline reply form. | |
| replyControl.click(); |
| if ( ! isset( $_GET['ai_analysis_queued'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended | ||
| return; | ||
| } | ||
|
|
||
| $count = absint( $_GET['ai_analysis_queued'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended |
There was a problem hiding this comment.
The query parameter is accessed without nonce verification, which is flagged by the phpcs comment. While displaying a success notice based on a query parameter is generally low risk, consider adding nonce verification for the bulk action redirect to ensure the query parameter wasn't tampered with. The handle_bulk_action method could add a nonce to the redirect URL that is verified here.
| nonce: string; | ||
| }; |
There was a problem hiding this comment.
The nonce is passed in the localized data but never used. If the nonce is intended for future use, this is acceptable, but if it's meant to be used for the current API calls, it should be included in the request headers or removed to avoid confusion.
| pendingPopulateTimeout.current = window.setTimeout( () => { | ||
| populateReplyTextarea( reply ); | ||
| pendingPopulateTimeout.current = null; | ||
| }, 100 ); |
There was a problem hiding this comment.
The magic number 100 (milliseconds) for the setTimeout delay is used without explanation. Consider extracting this to a named constant like REPLY_FORM_OPEN_DELAY with a comment explaining why this delay is needed (e.g., "Wait for WordPress inline reply form to render").
| useEffect( () => { | ||
| // Small delay to ensure DOM is fully rendered. | ||
| const timeoutId = setTimeout( () => { | ||
| processPendingComments(); | ||
| }, 500 ); | ||
|
|
||
| return () => clearTimeout( timeoutId ); | ||
| }, [ processPendingComments ] ); |
There was a problem hiding this comment.
The processPendingComments function is included in the useEffect dependency array, which causes the effect to recreate on every render since processPendingComments is recreated each render (it depends on isAnalyzing). This creates an infinite loop potential. Consider using useRef for processPendingComments or restructuring the dependencies to avoid this pattern.
| if ( score >= 0.7 ) { | ||
| return { | ||
| label: 'High', | ||
| className: 'ai-badge--high-toxicity', | ||
| icon: '⚠️', | ||
| }; | ||
| } | ||
| if ( score >= 0.4 ) { | ||
| return { | ||
| label: 'Medium', | ||
| className: 'ai-badge--medium-toxicity', | ||
| icon: '⚡', | ||
| }; |
There was a problem hiding this comment.
The toxicity score thresholds (0.7, 0.4) are duplicated between TypeScript and PHP code. If these thresholds need to change, they'll need to be updated in multiple places. Consider defining these as shared constants or documenting that they must be kept in sync.
| const replyTextarea = document.querySelector< HTMLTextAreaElement >( | ||
| '#replycontainer #replycontent' | ||
| ); | ||
|
|
||
| if ( replyTextarea ) { | ||
| replyTextarea.value = reply; | ||
| replyTextarea.focus(); | ||
|
|
||
| // Trigger input event for any listeners. | ||
| replyTextarea.dispatchEvent( | ||
| new Event( 'input', { bubbles: true } ) | ||
| ); | ||
| } | ||
| }, [] ); | ||
|
|
||
| /** | ||
| * Checks if the inline reply form is currently open for a specific comment. | ||
| */ | ||
| const isReplyFormOpenForComment = useCallback( | ||
| ( commentId: number ): boolean => { | ||
| const replyRow = | ||
| document.querySelector< HTMLElement >( '#replyrow' ); | ||
| const commentIdInput = document.querySelector< HTMLInputElement >( | ||
| '#replyrow #comment_ID' | ||
| ); |
There was a problem hiding this comment.
Multiple DOM selectors like '#replycontainer #replycontent', '#replyrow', '#replyrow #comment_ID' are hardcoded and depend on WordPress's internal comment form structure. These selectors are fragile and will break if WordPress changes its markup. Consider documenting this dependency or using more defensive selector strategies with fallbacks.
Added comment moderation feature details to the readme.
|
Note we'll want to update the screenshot numbers when this gets merged in, I left it with a hash instead of a number depending on when this versus other experiments get merged in. |
|
@Jameswlepage can you add tests here like was done in https://github.com/WordPress/ai/pull/147/changes#diff-0cf348eda48cae411ad9d44a2e5b449603a971f2313b0a849893ff4cf8280c46? |
|
This actually had us discussing the possibility of bringing back comments this morning. In an era where traffic is either declining or stagnant, keeping our audience engaged on-site is more important than ever. I apologize for not having had much time to test these features alongside our app or dive into the code too much. However, if this could eventually be included as part of the core efforts or if this could be made extensible enough to add—what we might call "comment value"—it would be very beneficial. We all have less staff (look at WaPo this week 😥), so it's not just replying to comments that saves labor but also quickly figuring out if its even worth replying or displaying that comment for that matter. An important metric to consider is whether a comment contributes to the discussion or reflects the "zeitgeist" of the post. For example, comments like "Yeah, I agree" are not particularly valuable. Ideally, we could use AI to filter out low-value comments. |
@sethrubenstein perhaps a
Perhaps another column for If you have more specific approaches in mind please share, as otherwise the functionality here will likely be stabilized and into a release without much variance from this experience. |
|
@jeffpaul good point, we don't want to effectively just recreate anti-spam plugins, so maybe both value and relevance would be too much. But just a value column perhaps analyzed against these criteria would work out well: To add to system instructions Thoughts? I'm happy to pull this down and give it a try myself. |
|
On thinking about this a bit further, the more challenging aspect is analyzing this against existing comments for similarities in nature and sentiment. This level of filtering likely yields a more advanced anti-spam and comment solution and not what you want this feature to be. |
|
Some thoughts from testing...
|
|
@sethrubenstein back to our conversation here, it occurs to be that what might be most helpful in addition to the sentiment+toxicity bits is a recommendation to editors on WHAT to do with a specific comment. Should they reply? Should they mark as spam or trash it? Should they just ignore it? Perhaps scope of actions to take are: Reply | Approve | Ignore | Review | Spam | Trash? Maybe also consider Feature (as in "mark as top comment") or Escalate (as in "send for legal or moderator review") thought these feel fine for future extension/iteration options. Maybe worth exposing is a confidence percent for how strong the recommendation is? Depending on what the recommendation is, that specific cell in the Comments table list view could actually be a clickable button/link to take action on the recommendation? Perhaps the column is named "Recommendation"? With all this in mind, perhaps we rename the experiment from Comment Moderation to Comment Intelligence or Comment Assistance? |



Summary
Adds AI-powered comment moderation to the WordPress Comments admin screen:
Implementation Details
ai/comment-analysisandai/reply-suggestion_ai_toxicity_score,_ai_sentiment,_ai_analysis_status)run-ability.tsutility for Abilities API with REST fallbackget_model_preferences()method toAbstract_Abilityfor consistent model selectionTest plan