From c2a7d2ee40ba1f9ed4149f88beab5f076423cf7f Mon Sep 17 00:00:00 2001 From: Darin Kotter Date: Thu, 23 Jan 2025 10:11:53 -0700 Subject: [PATCH 01/15] Add the beginnings of the Key Takeaways block --- .../Classifai/Blocks/key-takeaways/block.json | 20 +++++++++ .../Classifai/Blocks/key-takeaways/edit.js | 43 +++++++++++++++++++ .../Classifai/Blocks/key-takeaways/index.js | 25 +++++++++++ .../Classifai/Blocks/key-takeaways/save.js | 8 ++++ 4 files changed, 96 insertions(+) create mode 100644 includes/Classifai/Blocks/key-takeaways/block.json create mode 100644 includes/Classifai/Blocks/key-takeaways/edit.js create mode 100644 includes/Classifai/Blocks/key-takeaways/index.js create mode 100644 includes/Classifai/Blocks/key-takeaways/save.js diff --git a/includes/Classifai/Blocks/key-takeaways/block.json b/includes/Classifai/Blocks/key-takeaways/block.json new file mode 100644 index 000000000..432031f12 --- /dev/null +++ b/includes/Classifai/Blocks/key-takeaways/block.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "title": "Key Takeaways", + "description": "Generate a list of key takeaways from a post", + "textdomain": "classifai", + "name": "classifai/key-takeaways", + "category": "classifai-blocks", + "attributes": { + "layout": { + "type": "string", + "default": "list" + } + }, + "supports": { + "html": false + }, + "editorScript": "key-takeaways-editor-script", + "style": "key-takeaways-style" +} diff --git a/includes/Classifai/Blocks/key-takeaways/edit.js b/includes/Classifai/Blocks/key-takeaways/edit.js new file mode 100644 index 000000000..0fadce1bd --- /dev/null +++ b/includes/Classifai/Blocks/key-takeaways/edit.js @@ -0,0 +1,43 @@ +/** + * WordPress dependencies + */ +import { useBlockProps, BlockControls } from '@wordpress/block-editor'; +import { ToolbarGroup } from '@wordpress/components'; +import { list, grid } from '@wordpress/icons'; +import { __ } from '@wordpress/i18n'; + +const BlockEdit = ( props ) => { + const { attributes, setAttributes } = props; + const { layout } = attributes; + const blockProps = useBlockProps(); + + const layoutControls = [ + { + icon: list, + title: __( 'List view', 'classifai' ), + onClick: () => setAttributes( { layout: 'list' } ), + isActive: layout === 'list', + }, + { + icon: grid, + title: __( 'Paragraph view', 'classifai' ), + onClick: () => setAttributes( { layout: 'paragraph' } ), + isActive: layout === 'paragraph', + }, + ]; + + return ( + <> + + + +
+
+ CONTENT GOES HERE +
+
+ + ); +}; + +export default BlockEdit; diff --git a/includes/Classifai/Blocks/key-takeaways/index.js b/includes/Classifai/Blocks/key-takeaways/index.js new file mode 100644 index 000000000..3568235b1 --- /dev/null +++ b/includes/Classifai/Blocks/key-takeaways/index.js @@ -0,0 +1,25 @@ +/** + * Key Takeaways block + */ + +/** + * WordPress dependencies + */ +import { registerBlockType } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import edit from './edit'; +import save from './save'; +import block from './block.json'; +import { ReactComponent as icon } from '../../../../assets/img/block-icon.svg'; + +/** + * Register block + */ +registerBlockType( block, { + edit, + save, + icon, +} ); diff --git a/includes/Classifai/Blocks/key-takeaways/save.js b/includes/Classifai/Blocks/key-takeaways/save.js new file mode 100644 index 000000000..74a7f39c8 --- /dev/null +++ b/includes/Classifai/Blocks/key-takeaways/save.js @@ -0,0 +1,8 @@ +/** + * See https://wordpress.org/gutenberg/handbook/designers-developers/developers/block-api/block-edit-save/#save + * + * @return {null} Dynamic blocks do not save the HTML. + */ +const BlockSave = () => null; + +export default BlockSave; From 5206040efcc9c346ab202c05f4800d2fe7048a1e Mon Sep 17 00:00:00 2001 From: Darin Kotter Date: Thu, 23 Jan 2025 10:51:44 -0700 Subject: [PATCH 02/15] Register a new Feature for the Key Takeaways functionality and add initial settings --- includes/Classifai/Features/KeyTakeaways.php | 266 ++++++++++++++++++ .../Classifai/Services/ServicesManager.php | 1 + .../feature-additional-settings/index.js | 4 + .../key-takeaways.js | 105 +++++++ webpack.config.js | 3 + 5 files changed, 379 insertions(+) create mode 100644 includes/Classifai/Features/KeyTakeaways.php create mode 100644 src/js/settings/components/feature-additional-settings/key-takeaways.js diff --git a/includes/Classifai/Features/KeyTakeaways.php b/includes/Classifai/Features/KeyTakeaways.php new file mode 100644 index 000000000..ef4261e4d --- /dev/null +++ b/includes/Classifai/Features/KeyTakeaways.php @@ -0,0 +1,266 @@ +label = __( 'Key Takeaways', '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' ), + GeminiAPI::ID => __( 'Google AI (Gemini API)', 'classifai' ), + OpenAI::ID => __( 'Azure OpenAI', 'classifai' ), + Grok::ID => __( 'xAI Grok', 'classifai' ), + ChromeAI::ID => __( 'Chrome AI (experimental)', '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' ] ); + add_action( + 'admin_footer', + static function () { + if ( + ( isset( $_GET['tab'], $_GET['feature'] ) ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended + && 'language_processing' === sanitize_text_field( wp_unslash( $_GET['tab'] ) ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended + && self::ID === sanitize_text_field( wp_unslash( $_GET['feature'] ) ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended + ) { + printf( + '', + esc_html__( 'Are you sure you want to delete the prompt?', 'classifai' ), + ); + } + } + ); + } + + /** + * 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', + 'key-takeaways(?:/(?P\d+))?', + [ + [ + 'methods' => WP_REST_Server::READABLE, + 'callback' => [ $this, 'rest_endpoint_callback' ], + 'args' => [ + 'id' => [ + 'required' => true, + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'description' => esc_html__( 'Post ID to generate key takeaways for.', 'classifai' ), + ], + ], + 'permission_callback' => [ $this, 'generate_key_takeaways_permissions_check' ], + ], + [ + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => [ $this, 'rest_endpoint_callback' ], + 'args' => [ + 'content' => [ + 'required' => true, + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + 'description' => esc_html__( 'Content to generate key takeaways from.', 'classifai' ), + ], + ], + 'permission_callback' => [ $this, 'generate_key_takeaways_permissions_check' ], + ], + ] + ); + } + + /** + * Check if a given request has access to generate key takeaways. + * + * This check ensures we have a proper post ID, the current user + * making the request has access to that post, that we are + * properly authenticated and that the feature is turned on. + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|bool + */ + public function generate_key_takeaways_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__( 'Key takeaways 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/key-takeaways' ) === 0 ) { + return rest_ensure_response( + $this->run( + $request->get_param( 'id' ), + 'key-takeaways', + [ + '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; + } + } + + /** + * Get the description for the enable field. + * + * @return string + */ + public function get_enable_description(): string { + return esc_html__( 'A new block will be registered that when added to an item, will generate key takeaways from the content.', 'classifai' ); + } + + /** + * Returns the default settings for the feature. + * + * @return array + */ + public function get_feature_default_settings(): array { + return [ + 'key_takeaways_prompt' => [ + [ + 'title' => esc_html__( 'ClassifAI default', 'classifai' ), + 'prompt' => $this->prompt, + 'original' => 1, + ], + ], + 'post_types' => [ + 'post' => 'post', + ], + 'render' => 'list', + 'provider' => ChatGPT::ID, + ]; + } + + /** + * Returns the settings for the feature. + * + * @param string $index The index of the setting to return. + * @return array|mixed + */ + public function get_settings( $index = false ) { + $settings = parent::get_settings( $index ); + + // Keep using the original prompt from the codebase to allow updates. + if ( $settings && ! empty( $settings['key_takeaways_prompt'] ) ) { + foreach ( $settings['key_takeaways_prompt'] as $key => $prompt ) { + if ( 1 === intval( $prompt['original'] ) ) { + $settings['key_takeaways_prompt'][ $key ]['prompt'] = $this->prompt; + break; + } + } + } + + return $settings; + } + + /** + * Sanitizes the default feature settings. + * + * @param array $new_settings Settings being saved. + * @return array + */ + public function sanitize_default_feature_settings( array $new_settings ): array { + $settings = $this->get_settings(); + + $new_settings['key_takeaways_prompt'] = sanitize_prompts( 'key_takeaways_prompt', $new_settings ); + + $new_settings['post_types'] = isset( $new_settings['post_types'] ) ? array_map( 'sanitize_text_field', $new_settings['post_types'] ) : $settings['post_types']; + + $new_settings['render'] = sanitize_text_field( $new_settings['render'] ?? $settings['render'] ); + + return $new_settings; + } +} diff --git a/includes/Classifai/Services/ServicesManager.php b/includes/Classifai/Services/ServicesManager.php index 9c76503ae..764302108 100644 --- a/includes/Classifai/Services/ServicesManager.php +++ b/includes/Classifai/Services/ServicesManager.php @@ -77,6 +77,7 @@ public function register_language_processing_features( array $features ): array '\Classifai\Features\TitleGeneration', '\Classifai\Features\ExcerptGeneration', '\Classifai\Features\ContentResizing', + '\Classifai\Features\KeyTakeaways', '\Classifai\Features\TextToSpeech', '\Classifai\Features\AudioTranscriptsGeneration', '\Classifai\Features\Moderation', diff --git a/src/js/settings/components/feature-additional-settings/index.js b/src/js/settings/components/feature-additional-settings/index.js index 99644db2e..adb58a066 100644 --- a/src/js/settings/components/feature-additional-settings/index.js +++ b/src/js/settings/components/feature-additional-settings/index.js @@ -15,6 +15,7 @@ import { TextToSpeechSettings } from './text-to-speech'; import { TitleGenerationSettings } from './title-generation'; import { ContentResizingSettings } from './content-resizing'; import { ExcerptGenerationSettings } from './excerpt-generation'; +import { KeyTakeawaysSettings } from './key-takeaways'; import { ClassificationSettings } from './classification'; import { ModerationSettings } from './moderation'; import { Smart404Settings } from './smart-404'; @@ -45,6 +46,9 @@ const AdditionalSettingsFields = () => { case 'feature_content_resizing': return ; + case 'feature_key_takeaways': + return ; + case 'feature_descriptive_text_generator': return ; diff --git a/src/js/settings/components/feature-additional-settings/key-takeaways.js b/src/js/settings/components/feature-additional-settings/key-takeaways.js new file mode 100644 index 000000000..e79d42849 --- /dev/null +++ b/src/js/settings/components/feature-additional-settings/key-takeaways.js @@ -0,0 +1,105 @@ +/** + * WordPress dependencies + */ +import { useSelect, useDispatch } from '@wordpress/data'; +import { __ } from '@wordpress/i18n'; +import { CheckboxControl, RadioControl } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { SettingsRow } from '../settings-row'; +import { STORE_NAME } from '../../data/store'; +import { PromptRepeater } from './prompt-repeater'; + +/** + * Component for Key Takeaways feature settings. + * + * This component is used within the FeatureSettings component to allow users + * to configure the Key Takeaways feature. + * + * @return {React.ReactElement} KeyTakeawaysSettings component. + */ +export const KeyTakeawaysSettings = () => { + const featureSettings = useSelect( ( select ) => + select( STORE_NAME ).getFeatureSettings() + ); + const { postTypes } = window.classifAISettings; + const { setFeatureSettings } = useDispatch( STORE_NAME ); + const setPrompts = ( prompts ) => { + setFeatureSettings( { + key_takeaways_prompt: prompts, + } ); + }; + + return ( + <> + + + + + { Object.keys( postTypes || {} ).map( ( key ) => { + return ( + { + setFeatureSettings( { + post_types: { + ...featureSettings.post_types, + [ key ]: value ? key : '0', + }, + } ); + } } + __nextHasNoMarginBottom + /> + ); + } ) } + + + { + setFeatureSettings( { + render: value, + } ); + } } + options={ [ + { + label: __( 'List', 'classifai' ), + value: 'list', + }, + { + label: __( 'Paragraph', 'classifai' ), + value: 'paragraph', + }, + ] } + selected={ featureSettings.render } + /> + + + ); +}; diff --git a/webpack.config.js b/webpack.config.js index 98e9aa3b4..b6b337104 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -9,6 +9,9 @@ module.exports = { }, entry: { admin: [ './src/js/admin.js' ], + 'key-takeaways-block': [ + './includes/Classifai/Blocks/key-takeaways/index.js', + ], 'recommended-content-block': [ './includes/Classifai/Blocks/recommended-content-block/index.js', ], From 606a44ac402871e9e54eb7a2e3745ad1f62f2de4 Mon Sep 17 00:00:00 2001 From: Darin Kotter Date: Thu, 23 Jan 2025 11:36:19 -0700 Subject: [PATCH 03/15] Ensure the block is registered properly so it can be used --- .../Classifai/Blocks/key-takeaways/block.json | 7 +++-- .../Classifai/Blocks/key-takeaways/edit.js | 20 ++++++------ .../Classifai/Blocks/key-takeaways/render.php | 22 +++++++++++++ includes/Classifai/Features/KeyTakeaways.php | 23 +++++++++++--- .../key-takeaways.js | 31 ------------------- 5 files changed, 54 insertions(+), 49 deletions(-) create mode 100644 includes/Classifai/Blocks/key-takeaways/render.php diff --git a/includes/Classifai/Blocks/key-takeaways/block.json b/includes/Classifai/Blocks/key-takeaways/block.json index 432031f12..48a645f56 100644 --- a/includes/Classifai/Blocks/key-takeaways/block.json +++ b/includes/Classifai/Blocks/key-takeaways/block.json @@ -5,9 +5,9 @@ "description": "Generate a list of key takeaways from a post", "textdomain": "classifai", "name": "classifai/key-takeaways", - "category": "classifai-blocks", + "category": "text", "attributes": { - "layout": { + "render": { "type": "string", "default": "list" } @@ -16,5 +16,6 @@ "html": false }, "editorScript": "key-takeaways-editor-script", - "style": "key-takeaways-style" + "style": "key-takeaways-style", + "render": "file:./render.php" } diff --git a/includes/Classifai/Blocks/key-takeaways/edit.js b/includes/Classifai/Blocks/key-takeaways/edit.js index 0fadce1bd..20ac4e678 100644 --- a/includes/Classifai/Blocks/key-takeaways/edit.js +++ b/includes/Classifai/Blocks/key-takeaways/edit.js @@ -3,33 +3,33 @@ */ import { useBlockProps, BlockControls } from '@wordpress/block-editor'; import { ToolbarGroup } from '@wordpress/components'; -import { list, grid } from '@wordpress/icons'; +import { postList, paragraph } from '@wordpress/icons'; import { __ } from '@wordpress/i18n'; const BlockEdit = ( props ) => { const { attributes, setAttributes } = props; - const { layout } = attributes; + const { render } = attributes; const blockProps = useBlockProps(); - const layoutControls = [ + const renderControls = [ { - icon: list, + icon: postList, title: __( 'List view', 'classifai' ), - onClick: () => setAttributes( { layout: 'list' } ), - isActive: layout === 'list', + onClick: () => setAttributes( { render: 'list' } ), + isActive: render === 'list', }, { - icon: grid, + icon: paragraph, title: __( 'Paragraph view', 'classifai' ), - onClick: () => setAttributes( { layout: 'paragraph' } ), - isActive: layout === 'paragraph', + onClick: () => setAttributes( { render: 'paragraph' } ), + isActive: render === 'paragraph', }, ]; return ( <> - +
diff --git a/includes/Classifai/Blocks/key-takeaways/render.php b/includes/Classifai/Blocks/key-takeaways/render.php new file mode 100644 index 000000000..b979a91c7 --- /dev/null +++ b/includes/Classifai/Blocks/key-takeaways/render.php @@ -0,0 +1,22 @@ + + +
> + +
    +
  • Point 1
  • +
  • Point 2
  • +
+ + This is a summary. + +
diff --git a/includes/Classifai/Features/KeyTakeaways.php b/includes/Classifai/Features/KeyTakeaways.php index ef4261e4d..69b96a0d4 100644 --- a/includes/Classifai/Features/KeyTakeaways.php +++ b/includes/Classifai/Features/KeyTakeaways.php @@ -83,6 +83,16 @@ static function () { */ public function feature_setup() { add_action( 'enqueue_block_assets', [ $this, 'enqueue_editor_assets' ] ); + $this->register_block(); + } + + /** + * Register the block used for this feature. + */ + public function register_block() { + register_block_type_from_metadata( + CLASSIFAI_PLUGIN_DIR . '/includes/Classifai/Blocks/key-takeaways', // this is the directory where the block.json is found. + ); } /** @@ -191,6 +201,14 @@ public function enqueue_editor_assets() { if ( empty( $post ) || ! is_admin() ) { return; } + + wp_register_script( + 'key-takeaways-editor-script', + CLASSIFAI_PLUGIN_URL . 'dist/key-takeaways-block.js', + get_asset_info( 'key-takeaways', 'dependencies' ), + get_asset_info( 'key-takeaways', 'version' ), + true + ); } /** @@ -216,9 +234,6 @@ public function get_feature_default_settings(): array { 'original' => 1, ], ], - 'post_types' => [ - 'post' => 'post', - ], 'render' => 'list', 'provider' => ChatGPT::ID, ]; @@ -257,8 +272,6 @@ public function sanitize_default_feature_settings( array $new_settings ): array $new_settings['key_takeaways_prompt'] = sanitize_prompts( 'key_takeaways_prompt', $new_settings ); - $new_settings['post_types'] = isset( $new_settings['post_types'] ) ? array_map( 'sanitize_text_field', $new_settings['post_types'] ) : $settings['post_types']; - $new_settings['render'] = sanitize_text_field( $new_settings['render'] ?? $settings['render'] ); return $new_settings; diff --git a/src/js/settings/components/feature-additional-settings/key-takeaways.js b/src/js/settings/components/feature-additional-settings/key-takeaways.js index e79d42849..b3cbd2c64 100644 --- a/src/js/settings/components/feature-additional-settings/key-takeaways.js +++ b/src/js/settings/components/feature-additional-settings/key-takeaways.js @@ -24,7 +24,6 @@ export const KeyTakeawaysSettings = () => { const featureSettings = useSelect( ( select ) => select( STORE_NAME ).getFeatureSettings() ); - const { postTypes } = window.classifAISettings; const { setFeatureSettings } = useDispatch( STORE_NAME ); const setPrompts = ( prompts ) => { setFeatureSettings( { @@ -43,36 +42,6 @@ export const KeyTakeawaysSettings = () => { setPrompts={ setPrompts } /> - - { Object.keys( postTypes || {} ).map( ( key ) => { - return ( - { - setFeatureSettings( { - post_types: { - ...featureSettings.post_types, - [ key ]: value ? key : '0', - }, - } ); - } } - __nextHasNoMarginBottom - /> - ); - } ) } - Date: Thu, 23 Jan 2025 14:20:32 -0700 Subject: [PATCH 04/15] Connect up with OpenAI to generate the key takeaways --- .../Classifai/Blocks/key-takeaways/block.json | 7 +- .../Classifai/Blocks/key-takeaways/edit.js | 64 +++++++- includes/Classifai/Features/KeyTakeaways.php | 36 ++++- .../Classifai/Providers/OpenAI/ChatGPT.php | 141 ++++++++++++++++++ 4 files changed, 239 insertions(+), 9 deletions(-) diff --git a/includes/Classifai/Blocks/key-takeaways/block.json b/includes/Classifai/Blocks/key-takeaways/block.json index 48a645f56..2fa80ab82 100644 --- a/includes/Classifai/Blocks/key-takeaways/block.json +++ b/includes/Classifai/Blocks/key-takeaways/block.json @@ -10,10 +10,15 @@ "render": { "type": "string", "default": "list" + }, + "takeaways": { + "type": "array", + "default": [] } }, "supports": { - "html": false + "html": false, + "multiple": false }, "editorScript": "key-takeaways-editor-script", "style": "key-takeaways-style", diff --git a/includes/Classifai/Blocks/key-takeaways/edit.js b/includes/Classifai/Blocks/key-takeaways/edit.js index 20ac4e678..3106c5c45 100644 --- a/includes/Classifai/Blocks/key-takeaways/edit.js +++ b/includes/Classifai/Blocks/key-takeaways/edit.js @@ -2,15 +2,58 @@ * WordPress dependencies */ import { useBlockProps, BlockControls } from '@wordpress/block-editor'; +import { select } from '@wordpress/data'; import { ToolbarGroup } from '@wordpress/components'; +import { useEffect, useState } from '@wordpress/element'; import { postList, paragraph } from '@wordpress/icons'; import { __ } from '@wordpress/i18n'; +import apiFetch from '@wordpress/api-fetch'; const BlockEdit = ( props ) => { + const [ isLoading, setIsLoading ] = useState( false ); const { attributes, setAttributes } = props; - const { render } = attributes; + const { render, takeaways } = attributes; const blockProps = useBlockProps(); + useEffect( () => { + if ( ! isLoading && takeaways.length === 0 ) { + const postId = select( 'core/editor' ).getCurrentPostId(); + const postContent = + select( 'core/editor' ).getEditedPostAttribute( 'content' ); + const postTitle = + select( 'core/editor' ).getEditedPostAttribute( 'title' ); + + setIsLoading( true ); + + apiFetch( { + path: '/classifai/v1/key-takeaways/', + method: 'POST', + data: { + id: postId, + content: postContent, + title: postTitle, + render, + }, + } ).then( + async ( res ) => { + // Ensure takeaways is always an array. + if ( ! Array.isArray( res ) ) { + res = [ res ]; + } + + setAttributes( { takeaways: res } ); + setIsLoading( false ); + }, + ( err ) => { + setAttributes( { + takeaways: [ `Error: ${ err?.message }` ], + } ); + setIsLoading( false ); + } + ); + } + }, [] ); + const renderControls = [ { icon: postList, @@ -31,11 +74,24 @@ const BlockEdit = ( props ) => { -
+
- CONTENT GOES HERE + { render === 'list' && ( +
    + { takeaways.map( ( takeaway, index ) => ( +
  • { takeaway }
  • + ) ) } +
+ ) } + { render === 'paragraph' && ( + <> + { takeaways.map( ( takeaway, index ) => ( +

{ takeaway }

+ ) ) } + + ) }
-
+
); }; diff --git a/includes/Classifai/Features/KeyTakeaways.php b/includes/Classifai/Features/KeyTakeaways.php index 69b96a0d4..c5c8e7d1b 100644 --- a/includes/Classifai/Features/KeyTakeaways.php +++ b/includes/Classifai/Features/KeyTakeaways.php @@ -28,11 +28,11 @@ class KeyTakeaways extends Feature { const ID = 'feature_key_takeaways'; /** - * Prompt for generating a key takeaway. + * Prompt for generating the key takeaways. * * @var string */ - public $prompt = 'Summarize the following message using a maximum of {{WORDS}} words.'; + public $prompt = 'The content you will be provided with is from an already written article. This article has the title of: {{TITLE}}. Your task is to carefully read through this article and provide a summary that captures all the important points. This summary should be concise and limited to about 2-4 points but should also be detailed enough to allow someone to quickly grasp the full article. Read the article a few times before providing the summary and trim the summary down to be as concise as possible.'; /** * Constructor. @@ -107,12 +107,22 @@ public function register_endpoints() { 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'rest_endpoint_callback' ], 'args' => [ - 'id' => [ + 'id' => [ 'required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint', 'description' => esc_html__( 'Post ID to generate key takeaways for.', 'classifai' ), ], + 'render' => [ + 'type' => 'string', + 'enum' => [ + 'list', + 'paragraph', + ], + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + 'description' => esc_html__( 'How the key takeaways should be rendered.', 'classifai' ), + ], ], 'permission_callback' => [ $this, 'generate_key_takeaways_permissions_check' ], ], @@ -127,6 +137,22 @@ public function register_endpoints() { 'validate_callback' => 'rest_validate_request_arg', 'description' => esc_html__( 'Content to generate key takeaways from.', 'classifai' ), ], + 'title' => [ + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + 'description' => esc_html__( 'Title of content to generate key takeaways from.', 'classifai' ), + ], + 'render' => [ + 'type' => 'string', + 'enum' => [ + 'list', + 'paragraph', + ], + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + 'description' => esc_html__( 'How the key takeaways should be rendered.', 'classifai' ), + ], ], 'permission_callback' => [ $this, 'generate_key_takeaways_permissions_check' ], ], @@ -181,9 +207,11 @@ public function rest_endpoint_callback( WP_REST_Request $request ) { return rest_ensure_response( $this->run( $request->get_param( 'id' ), - 'key-takeaways', + 'key_takeaways', [ 'content' => $request->get_param( 'content' ), + 'title' => $request->get_param( 'title' ), + 'render' => $request->get_param( 'render' ), ] ) ); diff --git a/includes/Classifai/Providers/OpenAI/ChatGPT.php b/includes/Classifai/Providers/OpenAI/ChatGPT.php index 7e126b11f..310f444db 100644 --- a/includes/Classifai/Providers/OpenAI/ChatGPT.php +++ b/includes/Classifai/Providers/OpenAI/ChatGPT.php @@ -9,6 +9,7 @@ use Classifai\Features\DescriptiveTextGenerator; use Classifai\Features\ExcerptGeneration; use Classifai\Features\TitleGeneration; +use Classifai\Features\KeyTakeaways; use Classifai\Providers\Provider; use Classifai\Normalizer; use WP_Error; @@ -215,6 +216,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 'key_takeaways': + $return = $this->generate_key_takeaways( $post_id, $args ); + break; } return $return; @@ -663,6 +667,143 @@ public function resize_content( int $post_id, array $args = array() ) { return $return; } + /** + * Generated key takeaways from content. + * + * @param int $post_id The Post ID we're processing + * @param array $args Arguments passed in. + * @return string|WP_Error + */ + public function generate_key_takeaways( int $post_id = 0, 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 key takeaways.', 'classifai' ) ); + } + + $feature = new KeyTakeaways(); + $settings = $feature->get_settings(); + $args = wp_parse_args( + array_filter( $args ), + [ + 'content' => '', + 'title' => get_the_title( $post_id ), + 'render' => 'list', + ] + ); + + // These checks (and the one above) happen in the REST permission_callback, + // but we run them again here in case this method is called directly. + if ( empty( $settings ) || ( isset( $settings[ static::ID ]['authenticated'] ) && false === $settings[ static::ID ]['authenticated'] ) || ( ! $feature->is_feature_enabled() && ( ! defined( 'WP_CLI' ) || ! WP_CLI ) ) ) { + return new WP_Error( 'not_enabled', esc_html__( 'Key Takeaways generation is disabled or OpenAI authentication failed. Please check your settings.', 'classifai' ) ); + } + + $request = new APIRequest( $settings[ static::ID ]['api_key'] ?? '', $feature->get_option_name() ); + + $prompt = esc_textarea( get_default_prompt( $settings['key_takeaways_prompt'] ) ?? $feature->prompt ); + + // Replace our variables in the prompt. + $prompt_search = array( '{{TITLE}}' ); + $prompt_replace = array( $args['title'] ); + $prompt = str_replace( $prompt_search, $prompt_replace, $prompt ); + + /** + * Filter the prompt we will send to ChatGPT. + * + * @since x.x.x + * @hook classifai_chatgpt_key_takeaways_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. + * + * @return {string} Prompt. + */ + $prompt = apply_filters( 'classifai_chatgpt_key_takeaways_prompt', $prompt, $post_id ); + + /** + * Filter the request body before sending to ChatGPT. + * + * @since x.x.x + * @hook classifai_chatgpt_key_takeaways_request_body + * + * @param {array} $body Request body that will be sent to ChatGPT. + * @param {int} $post_id ID of post we are summarizing. + * + * @return {array} Request body. + */ + $body = apply_filters( + 'classifai_chatgpt_key_takeaways_request_body', + [ + 'model' => $this->chatgpt_model, + 'messages' => [ + [ + 'role' => 'system', + 'content' => 'You will be provided with content delimited by triple quotes. ' . $prompt, + ], + [ + 'role' => 'user', + 'content' => '"""' . $this->get_content( $post_id, 0, false, $args['content'] ) . '"""', + ], + ], + 'response_format' => [ + 'type' => 'json_schema', + 'json_schema' => [ + 'name' => 'key_takeaways', + 'schema' => [ + 'type' => 'object', + 'properties' => [ + 'takeaways' => [ + 'type' => 'array', + 'items' => [ + 'type' => 'string', + ], + ], + ], + 'required' => [ 'takeaways' ], + 'additionalProperties' => false, + ], + 'strict' => true, + ], + ], + 'temperature' => 0.9, + ], + $post_id + ); + + // Make our API request. + $response = $request->post( + $this->chatgpt_url, + [ + 'body' => wp_json_encode( $body ), + ] + ); + + // Extract out the response, if it exists. + if ( ! is_wp_error( $response ) && ! empty( $response['choices'] ) ) { + foreach ( $response['choices'] as $choice ) { + if ( isset( $choice['message'], $choice['message']['content'] ) ) { + // We expect the response to be valid json since we requested that schema. + $takeaways = json_decode( $choice['message']['content'], true ); + + if ( isset( $takeaways['takeaways'] ) && is_array( $takeaways['takeaways'] ) ) { + $response = array_map( + function ( $takeaway ) { + return sanitize_text_field( trim( $takeaway, ' "\'' ) ); + }, + $takeaways['takeaways'] + ); + } + } + + // If the request was refused, return an error. + if ( isset( $choice['message'], $choice['message']['refusal'] ) ) { + // translators: %s: error message. + return new WP_Error( 'refusal', sprintf( esc_html__( 'OpenAI request failed: %s', 'classifai' ), esc_html( $choice['message']['refusal'] ) ) ); + } + } + } + + return $response; + } + /** * Get our content, trimming if needed. * From e3a469ea1f48491e4edc6c3e8e8189a2edd5508e Mon Sep 17 00:00:00 2001 From: Darin Kotter Date: Thu, 23 Jan 2025 14:34:31 -0700 Subject: [PATCH 05/15] Ensure we render results properly on the FE --- .../Classifai/Blocks/key-takeaways/render.php | 30 +++++++++++++------ includes/Classifai/Features/KeyTakeaways.php | 2 +- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/includes/Classifai/Blocks/key-takeaways/render.php b/includes/Classifai/Blocks/key-takeaways/render.php index b979a91c7..87be08cac 100644 --- a/includes/Classifai/Blocks/key-takeaways/render.php +++ b/includes/Classifai/Blocks/key-takeaways/render.php @@ -7,16 +7,28 @@ * @var WP_Block $block Block instance. */ -$layout = $attributes['render'] ?? 'list'; +$layout = $attributes['render'] ?? 'list'; +$takeaways = $attributes['takeaways'] ?? []; ?>
> - -
    -
  • Point 1
  • -
  • Point 2
  • -
- - This is a summary. - + '; + foreach ( (array) $takeaways as $takeaway ) { + printf( + '
  • %s
  • ', + esc_html( $takeaway ) + ); + } + echo ''; + } else { + foreach ( (array) $takeaways as $takeaway ) { + printf( + '

    %s

    ', + esc_html( $takeaway ) + ); + } + } + ?>
    diff --git a/includes/Classifai/Features/KeyTakeaways.php b/includes/Classifai/Features/KeyTakeaways.php index c5c8e7d1b..635e92ec2 100644 --- a/includes/Classifai/Features/KeyTakeaways.php +++ b/includes/Classifai/Features/KeyTakeaways.php @@ -32,7 +32,7 @@ class KeyTakeaways extends Feature { * * @var string */ - public $prompt = 'The content you will be provided with is from an already written article. This article has the title of: {{TITLE}}. Your task is to carefully read through this article and provide a summary that captures all the important points. This summary should be concise and limited to about 2-4 points but should also be detailed enough to allow someone to quickly grasp the full article. Read the article a few times before providing the summary and trim the summary down to be as concise as possible.'; + public $prompt = 'The content you will be provided with is from an already written article. This article has the title of: {{TITLE}}. Your task is to carefully read through this article and provide a summary that captures all the important points. This summary should be concise and limited to about 2-4 points but should also be detailed enough to allow someone to quickly grasp the full article. Read the article a few times before providing the summary and trim each point down to be as concise as possible.'; /** * Constructor. From 1c40cfaf9c8e4a77b8e42a0282b1b68848638216 Mon Sep 17 00:00:00 2001 From: Darin Kotter Date: Thu, 23 Jan 2025 14:54:36 -0700 Subject: [PATCH 06/15] Add loading state when getting data --- .../Classifai/Blocks/key-takeaways/edit.js | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/includes/Classifai/Blocks/key-takeaways/edit.js b/includes/Classifai/Blocks/key-takeaways/edit.js index 3106c5c45..9714251f3 100644 --- a/includes/Classifai/Blocks/key-takeaways/edit.js +++ b/includes/Classifai/Blocks/key-takeaways/edit.js @@ -3,12 +3,17 @@ */ import { useBlockProps, BlockControls } from '@wordpress/block-editor'; import { select } from '@wordpress/data'; -import { ToolbarGroup } from '@wordpress/components'; +import { Placeholder, ToolbarGroup, Spinner } from '@wordpress/components'; import { useEffect, useState } from '@wordpress/element'; import { postList, paragraph } from '@wordpress/icons'; import { __ } from '@wordpress/i18n'; import apiFetch from '@wordpress/api-fetch'; +/** + * Internal dependencies + */ +import { ReactComponent as icon } from '../../../../assets/img/block-icon.svg'; + const BlockEdit = ( props ) => { const [ isLoading, setIsLoading ] = useState( false ); const { attributes, setAttributes } = props; @@ -74,6 +79,19 @@ const BlockEdit = ( props ) => { + { isLoading && ( + + + + ) }
    { render === 'list' && ( From 1ef61c349b071342b613c2a45826383c183da6bd Mon Sep 17 00:00:00 2001 From: Darin Kotter Date: Thu, 23 Jan 2025 15:12:22 -0700 Subject: [PATCH 07/15] Remove setting that is now at the block level. Add ability to refresh results --- .../Classifai/Blocks/key-takeaways/block.json | 2 +- .../Classifai/Blocks/key-takeaways/edit.js | 69 +++++++++++++------ includes/Classifai/Features/KeyTakeaways.php | 5 -- .../key-takeaways.js | 28 -------- 4 files changed, 49 insertions(+), 55 deletions(-) diff --git a/includes/Classifai/Blocks/key-takeaways/block.json b/includes/Classifai/Blocks/key-takeaways/block.json index 2fa80ab82..417bbc06f 100644 --- a/includes/Classifai/Blocks/key-takeaways/block.json +++ b/includes/Classifai/Blocks/key-takeaways/block.json @@ -2,7 +2,7 @@ "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 3, "title": "Key Takeaways", - "description": "Generate a list of key takeaways from a post", + "description": "Generate a list of key takeaways from post content", "textdomain": "classifai", "name": "classifai/key-takeaways", "category": "text", diff --git a/includes/Classifai/Blocks/key-takeaways/edit.js b/includes/Classifai/Blocks/key-takeaways/edit.js index 9714251f3..f24f006b2 100644 --- a/includes/Classifai/Blocks/key-takeaways/edit.js +++ b/includes/Classifai/Blocks/key-takeaways/edit.js @@ -1,9 +1,19 @@ /** * WordPress dependencies */ -import { useBlockProps, BlockControls } from '@wordpress/block-editor'; +import { + useBlockProps, + BlockControls, + InspectorControls, +} from '@wordpress/block-editor'; import { select } from '@wordpress/data'; -import { Placeholder, ToolbarGroup, Spinner } from '@wordpress/components'; +import { + Placeholder, + ToolbarGroup, + Spinner, + PanelBody, + Button, +} from '@wordpress/components'; import { useEffect, useState } from '@wordpress/element'; import { postList, paragraph } from '@wordpress/icons'; import { __ } from '@wordpress/i18n'; @@ -16,18 +26,20 @@ import { ReactComponent as icon } from '../../../../assets/img/block-icon.svg'; const BlockEdit = ( props ) => { const [ isLoading, setIsLoading ] = useState( false ); + const [ run, setRun ] = useState( false ); const { attributes, setAttributes } = props; const { render, takeaways } = attributes; const blockProps = useBlockProps(); useEffect( () => { - if ( ! isLoading && takeaways.length === 0 ) { + if ( ( ! isLoading && takeaways.length === 0 ) || run ) { const postId = select( 'core/editor' ).getCurrentPostId(); const postContent = select( 'core/editor' ).getEditedPostAttribute( 'content' ); const postTitle = select( 'core/editor' ).getEditedPostAttribute( 'title' ); + setRun( false ); setIsLoading( true ); apiFetch( { @@ -57,7 +69,7 @@ const BlockEdit = ( props ) => { } ); } - }, [] ); + }, [ run ] ); const renderControls = [ { @@ -79,6 +91,18 @@ const BlockEdit = ( props ) => { + + +