'POST', 'callback' => array( $this, 'handle_generate_content' ), 'permission_callback' => array( $this, 'check_permission' ), 'args' => array( 'mode' => array( 'type' => 'string', 'required' => false, 'default' => 'generate', 'enum' => array( 'generate', 'modify' ), 'sanitize_callback' => 'sanitize_key', ), 'content_type' => array( 'type' => 'string', 'required' => false, 'default' => 'paragraph', 'sanitize_callback' => 'sanitize_key', ), 'topic' => array( 'type' => 'string', 'required' => false, 'default' => '', 'sanitize_callback' => 'sanitize_text_field', ), 'instructions' => array( 'type' => 'string', 'required' => false, 'default' => '', 'sanitize_callback' => 'sanitize_textarea_field', ), 'selection' => array( 'type' => 'string', 'required' => false, 'default' => '', 'sanitize_callback' => 'wp_kses_post', ), 'tone' => array( 'type' => 'string', 'required' => false, 'default' => '', 'sanitize_callback' => 'sanitize_key', ), 'length' => array( 'type' => 'string', 'required' => false, 'default' => '', 'sanitize_callback' => 'sanitize_key', ), 'language' => array( 'type' => 'string', 'required' => false, 'default' => '', 'sanitize_callback' => 'sanitize_text_field', ), ), ) ); } /** * Permission callback for the content generation endpoint. * * @param \WP_REST_Request $request Request. * * @since 2.2.0 * @return bool|\WP_Error */ public function check_permission( \WP_REST_Request $request ) { if ( ! current_user_can( 'edit_posts' ) ) { return new \WP_Error( 'rest_forbidden', __( 'You do not have permission to generate content.', 'ai-content-writer' ), array( 'status' => rest_authorization_required_code() ) ); } $mode = (string) $request->get_param( 'mode' ); $topic = trim( (string) $request->get_param( 'topic' ) ); $selection = trim( wp_strip_all_tags( (string) $request->get_param( 'selection' ) ) ); if ( 'modify' === $mode && '' === $selection ) { return new \WP_Error( 'rest_invalid_param', __( 'Selected text is required to modify content.', 'ai-content-writer' ), array( 'status' => 400 ) ); } if ( 'generate' === $mode && '' === $topic ) { return new \WP_Error( 'rest_invalid_param', __( 'A topic or set of keywords is required to generate content.', 'ai-content-writer' ), array( 'status' => 400 ) ); } return true; } /** * Handle the content generation request. * * @param \WP_REST_Request $request The REST request object. * * @since 2.2.0 * @return \WP_REST_Response|\WP_Error The REST response or error. */ public function handle_generate_content( \WP_REST_Request $request ) { $api_model = get_option( 'aicw_api_model' ); $provider = aicw_get_ai_provider( $api_model ); if ( ! $provider ) { return new \WP_Error( 'aicw_invalid_provider', __( 'No AI provider is configured. Please choose Gemini or ChatGPT on the settings page.', 'ai-content-writer' ), array( 'status' => 400 ) ); } $prompt = $this->build_prompt( (string) $request->get_param( 'mode' ), (string) $request->get_param( 'content_type' ), (string) $request->get_param( 'topic' ), (string) $request->get_param( 'instructions' ), wp_strip_all_tags( (string) $request->get_param( 'selection' ) ), (string) $request->get_param( 'tone' ), (string) $request->get_param( 'length' ), (string) $request->get_param( 'language' ) ); $raw = $this->call_provider( $provider, $prompt ); if ( is_wp_error( $raw ) ) { return $raw; } $cleaned = $this->strip_code_fences( $raw ); $converted = aicw_convert_openai_markdown_to_html( $cleaned ); return rest_ensure_response( array( 'text' => $cleaned, 'html' => isset( $converted['content'] ) ? $converted['content'] : $cleaned, ) ); } /** * Build the prompt sent to the AI provider. * * @param string $mode Action mode. * @param string $content_type Content type. * @param string $topic Topic / keywords. * @param string $instructions Extra free-form instructions. * @param string $selection Currently selected text (modify mode only). * @param string $tone Optional tone preset. * @param string $length Optional length preset. * @param string $language Optional output language. * * @since 2.2.0 * @return string */ protected function build_prompt( $mode, $content_type, $topic, $instructions, $selection, $tone, $length, $language = '' ) { $length_map = array( 'short' => '50-100 words', 'medium' => '150-300 words', 'long' => '500-800 words', ); $tone_line = '' !== $tone ? sprintf( 'Use a %s tone.', $tone ) : ''; $length_line = isset( $length_map[ $length ] ) ? sprintf( 'Aim for roughly %s.', $length_map[ $length ] ) : ''; $language_line = '' !== $language && array_key_exists( $language, aicw_get_languages() ) ? sprintf( 'Write in %s.', $language ) : ''; $extra_line = '' !== trim( $instructions ) ? sprintf( "Additional instructions: %s", $instructions ) : ''; $type_labels = array( 'paragraph' => 'a single well-written paragraph', 'article' => 'a full article with H2/H3 markdown headings, intro, body sections, and conclusion', ); $type_label = isset( $type_labels[ $content_type ] ) ? $type_labels[ $content_type ] : $type_labels['paragraph']; switch ( $mode ) { case 'modify': $base = sprintf( "Rewrite the following text based on these instructions. Preserve markdown formatting where useful and return only the rewritten text — no preface, no explanation.\n\nOriginal text:\n%s", $selection ); break; case 'generate': default: $base = sprintf( "Write %s about: %s.\nReturn only the requested content in markdown — no preface, no explanation, no surrounding code fences.", $type_label, '' !== $topic ? $topic : 'the topic provided in the instructions' ); break; } return trim( implode( "\n\n", array_filter( array( $base, $tone_line, $length_line, $language_line, $extra_line ) ) ) ); } /** * Call the given provider and return the generated text (or WP_Error). * * @param \AIContentWriter\Services\ProviderInterface $provider The AI provider. * @param string $prompt The full prompt string. * * @since 2.2.0 * @return string|\WP_Error */ protected function call_provider( $provider, $prompt ) { if ( ! $provider->is_configured() ) { return new \WP_Error( 'aicw_provider_not_configured', sprintf( /* translators: %s: Provider label. */ __( 'No API key configured for %s. Please add your API key on the API settings page.', 'ai-content-writer' ), $provider->get_label() ), array( 'status' => 400 ) ); } return $provider->generate( $prompt ); } /** * Strip ```language ... ``` code fences that some models wrap responses in. * * @param string $text Raw model output. * * @since 2.2.0 * @return string */ protected function strip_code_fences( $text ) { $text = trim( (string) $text ); if ( preg_match( '/^```[a-zA-Z0-9_-]*\s*\n(.*)\n```$/s', $text, $m ) ) { return trim( $m[1] ); } return $text; } }