Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] feat/489: Add Rewrite Tone feature #803

Draft
wants to merge 7 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{

Check warning on line 1 in .eslintrc.json

View workflow job for this annotation

GitHub Actions / eslint

File ignored by default.
"globals": {
"wp": "readonly",
"jQuery": "readonly",
Expand All @@ -21,7 +21,8 @@
"Headers": "readonly",
"requestAnimationFrame": "readonly",
"React": "readonly",
"Block": "readonly"
"Block": "readonly",
"DOMParser": "readonly"
},
"extends": ["plugin:@wordpress/eslint-plugin/recommended"],
"ignorePatterns": ["*.json", "webpack.config.js"]
Expand Down
255 changes: 255 additions & 0 deletions includes/Classifai/Features/RewriteTone.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
<?php

namespace Classifai\Features;

use Classifai\Providers\OpenAI\ChatGPT;
use Classifai\Services\LanguageProcessing;
use WP_REST_Server;
use WP_REST_Request;
use WP_Error;

use function Classifai\get_asset_info;
use function Classifai\sanitize_prompts;

/**
* Class RewriteTone
*/
class RewriteTone extends Feature {
/**
* ID of the current feature.
*
* @var string
*/
const ID = 'feature_rewrite_tone';

/**
* Prompt for rewriting tone.
*
* @var string
*/
public $prompt = 'You are modifying the tone and lingo of the following text to Renaissance English.';

/**
* Constructor.
*/
public function __construct() {
$this->label = __( 'Rewrite Tone', 'classifai' );

// Contains all providers that are registered to the service.
$this->provider_instances = $this->get_provider_instances( LanguageProcessing::get_service_providers() );

// Contains just the providers this feature supports.
$this->supported_providers = [
ChatGPT::ID => __( 'OpenAI ChatGPT', 'classifai' ),
];
}

/**
* Set up necessary hooks.
*
* We utilize this so we can register the REST route.
*/
public function setup() {
parent::setup();
add_action( 'rest_api_init', [ $this, 'register_endpoints' ] );
}

/**
* Set up necessary hooks.
*/
public function feature_setup() {
add_action( 'enqueue_block_assets', [ $this, 'enqueue_editor_assets' ] );
}

/**
* Register any needed endpoints.
*/
public function register_endpoints() {
register_rest_route(
'classifai/v1',
'rewrite-tone',
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'rest_endpoint_callback' ],
'permission_callback' => '__return_true',
'args' => [
'id' => [
'required' => true,
'type' => 'integer',
'sanitize_callback' => 'absint',
'description' => esc_html__( 'Post ID to resize the content for.', 'classifai' ),
],
'content' => [
'type' => 'array',
'sanitize_callback' => function ( $content_array ) {
if ( is_array( $content_array ) ) {
return array_map(
function ( $item ) {
$item['clientId'] = sanitize_text_field( $item['clientId'] );
$item['content'] = wp_kses_post( $item['content'] );
return $item;
},
$content_array
);
}

return [];
},
'validate_callback' => function ( $content_array ) {
if ( is_array( $content_array ) ) {
foreach ( $content_array as $item ) {
if ( ! isset( $item['clientId'] ) || ! is_string( $item['clientId'] ) ) {
return new WP_Error( 'rewrite_tone_invalid_client_id', __( 'Each item must have a valid clientId string.', 'classifai' ), [ 'status' => 400 ] );
}

if ( ! isset( $item['content'] ) || ! is_string( $item['content'] ) ) {
return new WP_Error( 'rewrite_tone_invalid_content', __( 'Each item must have valid content as a string.', 'classifai' ), [ 'status' => 400 ] );
}
}
return true;
}
return new WP_Error( 'rewrite_tone_invalid_data_format', __( 'Content must be an array of objects.', 'classifai' ), [ 'status' => 400 ] );
},
'description' => esc_html__( 'The content to resize.', 'classifai' ),
],
],
]
);
}

/**
* Check if a given request has access to resize content.
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_Error|bool
*/
public function resize_content_permissions_check( WP_REST_Request $request ) {
$post_id = $request->get_param( 'id' );

// Ensure we have a logged in user that can edit the item.
if ( empty( $post_id ) || ! current_user_can( 'edit_post', $post_id ) ) {
return false;
}

$post_type = get_post_type( $post_id );
$post_type_obj = get_post_type_object( $post_type );

// Ensure the post type is allowed in REST endpoints.
if ( ! $post_type || empty( $post_type_obj ) || empty( $post_type_obj->show_in_rest ) ) {
return false;
}

// Ensure the feature is enabled. Also runs a user check.
if ( ! $this->is_feature_enabled() ) {
return new WP_Error( 'not_enabled', esc_html__( 'Rewrite Tone is not currently enabled.', 'classifai' ) );
}

return true;
}

/**
* Generic request handler for all our custom routes.
*
* @param WP_REST_Request $request The full request object.
* @return \WP_REST_Response
*/
public function rest_endpoint_callback( WP_REST_Request $request ) {
$route = $request->get_route();

if ( strpos( $route, '/classifai/v1/rewrite-tone' ) === 0 ) {
return rest_ensure_response(
$this->run(
$request->get_param( 'id' ),
'rewrite_tone',
[
'content' => $request->get_param( 'content' ),
]
)
);
}

return parent::rest_endpoint_callback( $request );
}

/**
* Enqueue the editor scripts.
*/
public function enqueue_editor_assets() {
global $post;

if ( empty( $post ) || ! is_admin() ) {
return;
}

wp_enqueue_script(
'classifai-plugin-rewrite-tone-js',
CLASSIFAI_PLUGIN_URL . 'dist/classifai-plugin-rewrite-tone.js',
array_merge(
get_asset_info( 'classifai-plugin-rewrite-tone', 'dependencies' ),
array( Feature::PLUGIN_AREA_SCRIPT )
),
get_asset_info( 'classifai-plugin-rewrite-tone', 'version' ),
true
);
}

/**
* Get the description for the enable field.
*
* @return string
*/
public function get_enable_description(): string {
return esc_html__( '"Condense this text" and "Expand this text" menu items will be added to the paragraph block\'s toolbar menu.', 'classifai' );
}

/**
* Add any needed custom fields.
*/
public function add_custom_settings_fields() {
$settings = $this->get_settings();

add_settings_field(
'rewrite_tone_prompt',
esc_html__( 'Prompt', 'classifai' ),
[ $this, 'render_prompt_repeater_field' ],
$this->get_option_name(),
$this->get_option_name() . '_section',
[
'label_for' => 'rewrite_tone_prompt',
'placeholder' => $this->prompt,
'default_value' => $settings['rewrite_tone_prompt'],
'description' => esc_html__( 'Add a custom prompt, if desired.', 'classifai' ),
]
);
}

/**
* Returns the default settings for the feature.
*
* @return array
*/
public function get_feature_default_settings(): array {
return [
'rewrite_tone_prompt' => [
[
'title' => esc_html__( 'ClassifAI default', 'classifai' ),
'prompt' => $this->prompt,
'original' => 1,
],
],
'provider' => ChatGPT::ID,
];
}

/**
* Sanitizes the default feature settings.
*
* @param array $new_settings Settings being saved.
* @return array
*/
public function sanitize_default_feature_settings( array $new_settings ): array {
$new_settings['rewrite_tone_prompt'] = sanitize_prompts( 'rewrite_tone_prompt', $new_settings );

return $new_settings;
}
}
80 changes: 79 additions & 1 deletion includes/Classifai/Providers/OpenAI/ChatGPT.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

use Classifai\Features\ContentResizing;
use Classifai\Features\ExcerptGeneration;
use Classifai\Features\RewriteTone;
use Classifai\Features\TitleGeneration;
use Classifai\Providers\Provider;
use Classifai\Normalizer;
Expand Down Expand Up @@ -181,7 +182,7 @@ public function sanitize_api_key( array $new_settings ): string {
*/
public function rest_endpoint_callback( $post_id = 0, string $route_to_call = '', array $args = [] ) {
if ( ! $post_id || ! get_post( $post_id ) ) {
return new WP_Error( 'post_id_required', esc_html__( 'A valid post ID is required to generate an excerpt.', 'classifai' ) );
return new WP_Error( 'post_id_required', esc_html__( 'A valid post ID is required.', 'classifai' ) );
}

$route_to_call = strtolower( $route_to_call );
Expand All @@ -198,6 +199,9 @@ public function rest_endpoint_callback( $post_id = 0, string $route_to_call = ''
case 'resize_content':
$return = $this->resize_content( $post_id, $args );
break;
case 'rewrite_tone':
$return = $this->rewrite_tone( $post_id, $args );
break;
}

return $return;
Expand Down Expand Up @@ -416,6 +420,80 @@ public function generate_titles( int $post_id = 0, array $args = [] ) {
return $return;
}

/**
* Rewrite the tone of the content.
*
* @param int $post_id The Post Id we're processing
* @param array $args Arguments passed in.
*/
public function rewrite_tone( int $post_id, array $args = [] ) {
$feature = new RewriteTone();
$settings = $feature->get_settings();
$request = new APIRequest( $settings[ static::ID ]['api_key'] ?? '', $feature->get_option_name() );
$prompt = esc_textarea( get_default_prompt( $settings['rewrite_tone_prompt'] ) ?? $feature->prompt );

/**
* Filter the prompt we will send to ChatGPT.
*
* @since x.x.x
* @hook classifai_chatgpt_rewrite_tone_prompt
*
* @param {string} $prompt Prompt we are sending to ChatGPT. Gets added before post content.
* @param {int} $post_id ID of post we are summarizing.
* @param {array} $args Arguments passed to endpoint.
*
* @return {string} Prompt.
*/
$prompt = apply_filters( 'classifai_chatgpt_rewrite_tone_prompt', $prompt, $post_id, $args );

$body = apply_filters(
'classifai_chatgpt_resize_content_request_body',
[
'model' => $this->chatgpt_model,
'messages' => [
[
'role' => 'system',
'content' => $prompt,
],
[
'role' => 'system',
'content' => "Please return each modified content with its corresponding 'clientId'.",
],
[
'role' => 'system',
'content' => 'The inline styles and HTML attributes should be preserved in the response.',
],
[
'role' => 'system',
'content' => 'The HTML in the input should be preserved in the response.',
],
[
'role' => 'user',
'content' => wp_json_encode( $args['content'] ),
],
],
],
);

$response = $request->post(
$this->chatgpt_url,
[
'body' => wp_json_encode( $body ),
]
);

$return = [];

foreach ( $response['choices'] as $choice ) {
if ( isset( $choice['message'], $choice['message']['content'] ) ) {
// ChatGPT often adds quotes to strings, so remove those as well as extra spaces.
$return[] = trim( $choice['message']['content'], ' "\'' );
}
}

return $return;
}

/**
* Resizes content.
*
Expand Down
1 change: 1 addition & 0 deletions includes/Classifai/Services/ServicesManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ public function register_language_processing_features( array $features ): array
'\Classifai\Features\TitleGeneration',
'\Classifai\Features\ExcerptGeneration',
'\Classifai\Features\ContentResizing',
'\Classifai\Features\RewriteTone',
'\Classifai\Features\TextToSpeech',
'\Classifai\Features\AudioTranscriptsGeneration',
'\Classifai\Features\Moderation',
Expand Down
Loading
Loading