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 27 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
af8eb54
Merge branch 'develop' of github.com:10up/classifai into develop
Sidsector9 Nov 20, 2024
5a1f945
Merge branch 'develop' of github.com:10up/classifai into develop
Sidsector9 Jan 14, 2025
4e6dbb9
register Tone rewriting
Sidsector9 Jan 14, 2025
dbe1299
setup webpack and eslintrc
Sidsector9 Jan 14, 2025
a901a54
add the useSelectedBlocks hook
Sidsector9 Jan 14, 2025
749a2c7
register classifai-rewrite-tone-plugin
Sidsector9 Jan 14, 2025
f453c28
add the filterAndFlattenAllowedBlocks util
Sidsector9 Jan 14, 2025
c865d7a
add the useEditorCanvas hook
Sidsector9 Jan 18, 2025
7475d59
add the InjectIframeStyles component
Sidsector9 Jan 18, 2025
b2eb294
add the getClientIdToBlockContentMapping util
Sidsector9 Jan 18, 2025
217ee9a
add the stripOutermostTag util
Sidsector9 Feb 26, 2025
de829ae
implement the rewriteTone() method
Sidsector9 Feb 26, 2025
bb94e40
add replaceBlocksInSource util
Sidsector9 Feb 28, 2025
30b24b9
add InjectIframeStyles component
Sidsector9 Feb 28, 2025
1551101
:white_check_mark: implement rewrite tone with preview and replacement
Sidsector9 Feb 28, 2025
a4c7e95
Merge branch 'develop' of github.com:10up/classifai into develop
Sidsector9 Feb 28, 2025
06da5dd
Merge branch 'develop' into feat/489v2
Sidsector9 Feb 28, 2025
2876750
add tones
Sidsector9 Mar 2, 2025
2812643
move ClassifAI slot-fills to their separate files
Sidsector9 Mar 2, 2025
234b090
add tone attributes
Sidsector9 Mar 2, 2025
b9f5405
render tone descriptions
Sidsector9 Mar 2, 2025
5ffc7c7
add styling to the previewer controls
Sidsector9 Mar 2, 2025
ac412e1
handle tone attributes server-side
Sidsector9 Mar 2, 2025
465ceee
implement custom popover
Sidsector9 Mar 2, 2025
b6a0ab6
fix previewer controls alignment
Sidsector9 Mar 2, 2025
2a0f02a
fix PHPCS errors
Sidsector9 Mar 3, 2025
2bf93df
fix eslint errors
Sidsector9 Mar 3, 2025
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 @@ -23,7 +23,8 @@
"React": "readonly",
"Block": "readonly",
"classifai_term_cleanup_params": "readonly",
"classifAISettings": "readonly"
"classifAISettings": "readonly",
"DOMParser": "readonly"
},
"rules": {
"react/jsx-no-undef": "off"
Expand Down
251 changes: 251 additions & 0 deletions includes/Classifai/Features/RewriteTone.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
<?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';

/**
* 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' ),
'emotion' => $request->get_param( 'emotion' ),
'formality' => $request->get_param( 'formality' ),
'intent' => $request->get_param( 'intent' ),
'audience' => $request->get_param( 'audience' ),
]
)
);
}

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,

Check failure on line 216 in includes/Classifai/Features/RewriteTone.php

View workflow job for this annotation

GitHub Actions / PHPStan

Access to an undefined property Classifai\Features\RewriteTone::$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' ),
'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;
}
}
75 changes: 74 additions & 1 deletion includes/Classifai/Providers/OpenAI/ChatGPT.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use Classifai\Features\KeyTakeaways;
use Classifai\Providers\Provider;
use Classifai\Normalizer;
use Classifai\Features\RewriteTone;
use WP_Error;

use function Classifai\get_default_prompt;
Expand Down Expand Up @@ -200,7 +201,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 @@ -224,6 +225,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;
case 'key_takeaways':
$return = $this->generate_key_takeaways( $post_id, $args );
break;
Expand Down Expand Up @@ -870,6 +874,75 @@ public function resize_content( int $post_id, array $args = array() ) {
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() );

$emotion = sanitize_text_field( $args['emotion'] ?? 'happy' );
$formality = sanitize_text_field( $args['formality'] ?? 'formal' );
$intent = sanitize_text_field( $args['intent'] ?? 'storytelling' );
$audience = sanitize_text_field( $args['audience'] ?? 'general' );
$prompt = "Rewrite the following content with the following tone attributes: Emotion: {$emotion}, Formality: {$formality}, Intent: {$intent}, Audience: {$audience}. Ensure that the rewritten content aligns with the selected attributes. Keep it {$formality} and make it {$intent}. The tone should reflect a {$emotion} sentiment while ensuring it resonates well with a {$audience} audience.";

$body = apply_filters(
'classifai_chatgpt_resize_content_request_body',
[
'model' => $this->chatgpt_model,
'messages' => [
[
'role' => 'system',
'content' => $prompt,
],
[
'role' => 'system',
'content' => "Rewrite the above content while maintaining its original meaning but transforming it according to the specified tone attributes.",
],
[
'role' => 'system',
'content' => "Please return each modified content with its corresponding 'clientId'. This is extremely important.",
],
[
'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;
}

/**
* Generate key takeaways from 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 @@ -77,6 +77,7 @@ public function register_language_processing_features( array $features ): array
'\Classifai\Features\TitleGeneration',
'\Classifai\Features\ExcerptGeneration',
'\Classifai\Features\ContentResizing',
'\Classifai\Features\RewriteTone',
'\Classifai\Features\KeyTakeaways',
'\Classifai\Features\TextToSpeech',
'\Classifai\Features\AudioTranscriptsGeneration',
Expand Down
Loading
Loading