From 6cc4d73e3c664b77e5a73ea0b6fcf68cfff46110 Mon Sep 17 00:00:00 2001 From: Arkenon Date: Wed, 10 Jun 2026 13:39:12 +0300 Subject: [PATCH] feat: Add Generate with AI button into the Ability Explorer - Test Ability page. --- .../Abilities_Explorer/Abilities_Explorer.php | 35 ++-- .../Abilities_Explorer/Admin_Page.php | 154 ++++++++++++++++-- .../components/GeneratePayloadModal.jsx | 102 ++++++++++++ src/experiments/abilities-explorer/index.js | 65 ++++++++ src/experiments/abilities-explorer/index.scss | 39 ++++- 5 files changed, 357 insertions(+), 38 deletions(-) create mode 100644 src/experiments/abilities-explorer/components/GeneratePayloadModal.jsx diff --git a/includes/Experiments/Abilities_Explorer/Abilities_Explorer.php b/includes/Experiments/Abilities_Explorer/Abilities_Explorer.php index 4873de334..e4948ed12 100644 --- a/includes/Experiments/Abilities_Explorer/Abilities_Explorer.php +++ b/includes/Experiments/Abilities_Explorer/Abilities_Explorer.php @@ -1,4 +1,5 @@ $this->is_enabled(), - 'ajaxUrl' => admin_url( 'admin-ajax.php' ), - 'nonce' => wp_create_nonce( 'ai_ability_explorer_invoke' ), - 'strings' => array( - 'invoking' => esc_html__( 'Invoking ability...', 'ai' ), - 'success' => esc_html__( 'Success!', 'ai' ), - 'error' => esc_html__( 'Error', 'ai' ), - 'invalidJson' => esc_html__( 'Invalid JSON input', 'ai' ), - 'confirmInvoke' => esc_html__( 'Are you sure you want to invoke this ability?', 'ai' ), - 'copySuccess' => esc_html__( 'Copied!', 'ai' ), - 'copyError' => esc_html__( 'Failed to copy', 'ai' ), + 'enabled' => $this->is_enabled(), + 'ajaxUrl' => admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'ai_ability_explorer_invoke' ), + 'generateNonce' => wp_create_nonce( 'ai_ability_explorer_generate_payload' ), + 'strings' => array( + 'invoking' => esc_html__( 'Invoking ability...', 'ai' ), + 'success' => esc_html__( 'Success!', 'ai' ), + 'error' => esc_html__( 'Error', 'ai' ), + 'invalidJson' => esc_html__( 'Invalid JSON input', 'ai' ), + 'confirmInvoke' => esc_html__( 'Are you sure you want to invoke this ability?', 'ai' ), + 'copySuccess' => esc_html__( 'Copied!', 'ai' ), + 'copyError' => esc_html__( 'Failed to copy', 'ai' ), + 'generating' => esc_html__( 'Generating...', 'ai' ), + 'generateModalTitle' => esc_html__( 'Generate Payload with AI', 'ai' ), + 'generateModalLabel' => esc_html__( 'Describe what you want to test in natural language:', 'ai' ), + 'generateModalPlaceholder' => esc_html__( 'e.g. Query only the site URL information', 'ai' ), + 'generateBtn' => esc_html__( 'Generate', 'ai' ), + 'cancelBtn' => esc_html__( 'Cancel', 'ai' ), + 'generateEmptyCommandError' => esc_html__( 'Please enter a command.', 'ai' ), + 'generateError' => esc_html__( 'An error occurred while generating the payload. Please try again.', 'ai' ), ), ) ); diff --git a/includes/Experiments/Abilities_Explorer/Admin_Page.php b/includes/Experiments/Abilities_Explorer/Admin_Page.php index c2cf83ba3..377c557bc 100644 --- a/includes/Experiments/Abilities_Explorer/Admin_Page.php +++ b/includes/Experiments/Abilities_Explorer/Admin_Page.php @@ -1,4 +1,5 @@


-

    -
  1. -
  2. -
  3. -
  4. -
+
    +
  1. +
  2. +
  3. +
  4. +

@@ -323,6 +326,11 @@ private function render_test_runner(): void { + + + @@ -483,6 +491,116 @@ public function ajax_invoke_ability(): void { } } + /** + * AJAX handler for AI-assisted payload generation. + * + * @since 1.0.1 + */ + public function ajax_generate_payload(): void { + // Verify nonce. + check_ajax_referer( 'ai_ability_explorer_generate_payload', 'nonce' ); + + // Check user capabilities. + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( + array( + 'message' => __( 'Insufficient permissions.', 'ai' ), + ) + ); + } + + // Get parameters. + $ability_slug = isset( $_POST['ability'] ) ? sanitize_text_field( wp_unslash( $_POST['ability'] ) ) : ''; + $command = isset( $_POST['command'] ) ? sanitize_textarea_field( wp_unslash( $_POST['command'] ) ) : ''; + + if ( empty( $ability_slug ) ) { + wp_send_json_error( + array( + 'message' => __( 'Ability slug is required.', 'ai' ), + ) + ); + } + + if ( empty( $command ) ) { + wp_send_json_error( + array( + 'message' => __( 'A command is required.', 'ai' ), + ) + ); + } + + // Get ability to retrieve its input schema. + $ability = Ability_Handler::get_ability( $ability_slug ); + + if ( ! $ability ) { + wp_send_json_error( + array( + 'message' => __( 'Ability not found.', 'ai' ), + ) + ); + } + + if ( ! function_exists( 'wp_ai_client_prompt' ) ) { + wp_send_json_error( + array( + 'message' => __( 'AI provider is not available.', 'ai' ), + ) + ); + } + + $schema_json = wp_json_encode( $ability['input_schema'], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ); + + $system_instruction = 'You are a JSON payload generator. Given an input schema and a user command, generate a valid JSON object that satisfies the schema and fulfills the command. Return ONLY valid JSON with no explanation, no markdown, and no code fences.'; + + $user_prompt = sprintf( + "Input Schema:\n%s\n\nUser Command: %s", + $schema_json, + $command + ); + + $prompt_builder = wp_ai_client_prompt( $user_prompt ) + ->using_system_instruction( $system_instruction ); + + if ( ! $prompt_builder->is_supported_for_text_generation() ) { + wp_send_json_error( + array( + 'message' => __( 'No AI provider available. Please connect one in the plugin settings.', 'ai' ), + ) + ); + } + + $result = $prompt_builder->generate_text(); + + if ( is_wp_error( $result ) ) { + wp_send_json_error( + array( + 'message' => $result->get_error_message(), + ) + ); + } + + // Strip markdown code fences if the model includes them. + $raw = trim( (string) $result ); + $raw = preg_replace( '/^```(?:json)?\s*/i', '', $raw ) ?? $raw; + $raw = preg_replace( '/\s*```$/', '', $raw ) ?? $raw; + $raw = trim( $raw ); + + $parsed = json_decode( $raw, true ); + if ( json_last_error() !== JSON_ERROR_NONE ) { + wp_send_json_error( + array( + 'message' => __( 'The AI returned a response that could not be parsed as JSON. Please try again or rephrase your command.', 'ai' ), + ) + ); + } + + wp_send_json_success( + array( + 'payload' => wp_json_encode( $parsed, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ), + ) + ); + } + /** * Add contextual help tabs to the screen. * @@ -500,7 +618,7 @@ public function add_help_tabs(): void { 'id' => 'abilities-overview', 'title' => __( 'Overview', 'ai' ), 'content' => - '

' . esc_html__( 'Abilities are a standardized way for WordPress core, plugins, and themes to expose discrete units of functionality. Each ability has a name, optional input/output schemas, and can be invoked programmatically.', 'ai' ) . '

' . + '

' . esc_html__( 'Abilities are a standardized way for WordPress core, plugins, and themes to expose discrete units of functionality. Each ability has a name, optional input/output schemas, and can be invoked programmatically.', 'ai' ) . '

' . '

' . esc_html__( 'The Abilities Explorer lets you browse every registered ability, inspect its schemas, and test it with custom input right from the admin.', 'ai' ) . '

', ) ); @@ -512,11 +630,11 @@ public function add_help_tabs(): void { 'id' => 'abilities-providers', 'title' => esc_html__( 'Providers', 'ai' ), 'content' => - '

' . esc_html__( 'Every ability is associated with a provider that indicates where it comes from:', 'ai' ) . '

' . + '

' . esc_html__( 'Every ability is associated with a provider that indicates where it comes from:', 'ai' ) . '

' . '', ) ); @@ -526,19 +644,19 @@ public function add_help_tabs(): void { 'id' => 'abilities-testing', 'title' => esc_html__( 'Testing', 'ai' ), 'content' => - '

' . esc_html__( 'You can test any ability directly from this screen:', 'ai' ) . '

' . + '

' . esc_html__( 'You can test any ability directly from this screen:', 'ai' ) . '

' . '
    ' . - '
  1. ' . __( 'Click "Test" next to an ability in the list.', 'ai' ) . '
  2. ' . - '
  3. ' . __( 'Edit the pre-filled Input Data if the ability accepts JSON parameters.', 'ai' ) . '
  4. ' . - '
  5. ' . __( 'Use "Validate Input" to check your JSON against the schema.', 'ai' ) . '
  6. ' . - '
  7. ' . __( 'Click "Invoke Ability" to execute it and see the result.', 'ai' ) . '
  8. ' . + '
  9. ' . __( 'Click "Test" next to an ability in the list.', 'ai' ) . '
  10. ' . + '
  11. ' . __( 'Edit the pre-filled Input Data if the ability accepts JSON parameters.', 'ai' ) . '
  12. ' . + '
  13. ' . __( 'Use "Validate Input" to check your JSON against the schema.', 'ai' ) . '
  14. ' . + '
  15. ' . __( 'Click "Invoke Ability" to execute it and see the result.', 'ai' ) . '
  16. ' . '
', ) ); $screen->set_help_sidebar( '

' . esc_html__( 'For more information:', 'ai' ) . '

' . - '

' . esc_html__( 'Abilities API Documentation', 'ai' ) . '

' + '

' . esc_html__( 'Abilities API Documentation', 'ai' ) . '

' ); } } diff --git a/src/experiments/abilities-explorer/components/GeneratePayloadModal.jsx b/src/experiments/abilities-explorer/components/GeneratePayloadModal.jsx new file mode 100644 index 000000000..6c2dd84bc --- /dev/null +++ b/src/experiments/abilities-explorer/components/GeneratePayloadModal.jsx @@ -0,0 +1,102 @@ +/** + * WordPress dependencies + */ +import { useState } from '@wordpress/element'; +import { Modal, Button, TextareaControl, Notice } from '@wordpress/components'; + +/** + * @param {Object} props + * @param {Function} props.onClose Close/unmount callback. + * @param {Function} props.onSuccess Called with the generated JSON string on success. + * @param {string} props.abilitySlug Slug of the ability being tested. + * @param {Object} props.strings Localised string map from aiAbilityExplorer. + * @param {string} props.ajaxUrl WordPress admin-ajax URL. + * @param {string} props.nonce Nonce for ai_ability_explorer_generate_payload. + */ +export default function GeneratePayloadModal( { + onClose, + onSuccess, + abilitySlug, + strings, + ajaxUrl, + nonce, +} ) { + const [ command, setCommand ] = useState( '' ); + const [ isGenerating, setIsGenerating ] = useState( false ); + const [ error, setError ] = useState( '' ); + + async function handleGenerate() { + if ( ! command.trim() ) { + setError( strings.generateEmptyCommandError ); + return; + } + + setIsGenerating( true ); + setError( '' ); + + const formData = new FormData(); + formData.append( 'action', 'ai_ability_explorer_generate_payload' ); + formData.append( 'nonce', nonce ); + formData.append( 'ability', abilitySlug ); + formData.append( 'command', command ); + + try { + const response = await fetch( ajaxUrl, { + method: 'POST', + body: formData, + credentials: 'same-origin', + } ); + const data = await response.json(); + + if ( data.success && data.data?.payload ) { + onSuccess( data.data.payload ); + onClose(); + } else { + setError( data.data?.message || strings.generateError ); + } + } catch { + setError( strings.generateError ); + } finally { + setIsGenerating( false ); + } + } + + return ( + + + +
+ { error && ( + + { error } + + ) } +
+ +
+ + +
+
+ ); +} diff --git a/src/experiments/abilities-explorer/index.js b/src/experiments/abilities-explorer/index.js index aa292ca6d..d53595eb7 100644 --- a/src/experiments/abilities-explorer/index.js +++ b/src/experiments/abilities-explorer/index.js @@ -2,6 +2,9 @@ * Ability Explorer Admin JavaScript */ +/** + * WordPress dependencies + */ /** * WordPress dependencies */ @@ -12,10 +15,15 @@ import { __ } from '@wordpress/i18n'; */ const { aiAbilityExplorer, navigator } = window; +/** + * Internal dependencies + */ /** * Internal dependencies */ import './index.scss'; +import { createRoot, createElement } from '@wordpress/element'; +import GeneratePayloadModal from './components/GeneratePayloadModal'; ( function () { 'use strict'; @@ -91,6 +99,16 @@ import './index.scss'; } ); } + // Generate with AI button + const generateAiButton = document.getElementById( + 'ability-test-generate-ai' + ); + if ( generateAiButton ) { + generateAiButton.addEventListener( 'click', function () { + self.openGenerateModal(); + } ); + } + // Auto-format JSON on blur const payload = document.getElementById( 'ability-test-payload' ); if ( payload ) { @@ -573,6 +591,53 @@ import './index.scss'; }, 1500 ); }, + /** + * Open the AI payload generation modal (rendered via wp.components.Modal). + */ + openGenerateModal() { + const invokeButton = document.getElementById( + 'ability-test-invoke' + ); + const abilitySlug = invokeButton + ? invokeButton.dataset.ability + : ''; + + if ( ! abilitySlug ) { + return; + } + + const container = document.createElement( 'div' ); + document.body.appendChild( container ); + + const root = createRoot( container ); + + const close = () => { + root.unmount(); + document.body.removeChild( container ); + }; + + const handleSuccess = ( payload ) => { + const payloadTextarea = document.getElementById( + 'ability-test-payload' + ); + if ( payloadTextarea ) { + payloadTextarea.value = payload; + payloadTextarea.dispatchEvent( new Event( 'input' ) ); + } + }; + + root.render( + createElement( GeneratePayloadModal, { + onClose: close, + onSuccess: handleSuccess, + abilitySlug, + strings: aiAbilityExplorer.strings, + ajaxUrl: aiAbilityExplorer.ajaxUrl, + nonce: aiAbilityExplorer.generateNonce, + } ) + ); + }, + /** * Escape HTML * diff --git a/src/experiments/abilities-explorer/index.scss b/src/experiments/abilities-explorer/index.scss index ca80706c2..97ef7e425 100644 --- a/src/experiments/abilities-explorer/index.scss +++ b/src/experiments/abilities-explorer/index.scss @@ -252,15 +252,21 @@ } .ability-test-validation.validation-success { - background: #edfaef; /* WP green-50 */ - border-color: #00a32a; /* WP green-600 */ - color: #00761e; /* WP green-700 */ + background: #edfaef; + /* WP green-50 */ + border-color: #00a32a; + /* WP green-600 */ + color: #00761e; + /* WP green-700 */ } .ability-test-validation.validation-error { - background: #fcf0f1; /* WP red-50 */ - border-color: #d63638; /* WP red-600 */ - color: #b32d2e; /* WP red-700 */ + background: #fcf0f1; + /* WP red-50 */ + border-color: #d63638; + /* WP red-600 */ + color: #b32d2e; + /* WP red-700 */ } .ability-test-validation h4 { @@ -374,8 +380,13 @@ } @keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } } /* Responsive */ @@ -415,3 +426,15 @@ color: var(--wp--preset--color--foreground, #646970); font-size: 13px; } + +/* AI Generate Payload Modal */ +.ability-generate-modal-footer { + display: flex; + gap: 8px; + margin-top: 16px; +} + +.ability-generate-modal-error { + flex: 1; + margin-top: 16px; +} \ No newline at end of file