diff --git a/groq-ai-product-text.php b/groq-ai-product-text.php index e07ee56..3f38034 100644 --- a/groq-ai-product-text.php +++ b/groq-ai-product-text.php @@ -58,9 +58,15 @@ require_once __DIR__ . '/includes/Services/Google/class-groq-ai-google-oauth-cli require_once __DIR__ . '/includes/Services/Google/class-groq-ai-google-search-console-client.php'; require_once __DIR__ . '/includes/Services/Google/class-groq-ai-google-analytics-data-client.php'; require_once __DIR__ . '/includes/Services/Google/class-groq-ai-google-context-builder.php'; +require_once __DIR__ . '/includes/Admin/class-groq-ai-admin-base.php'; +require_once __DIR__ . '/includes/Admin/class-groq-ai-term-admin-base.php'; +require_once __DIR__ . '/includes/Admin/class-groq-ai-categories-admin.php'; +require_once __DIR__ . '/includes/Admin/class-groq-ai-brands-admin.php'; require_once __DIR__ . '/includes/Admin/class-groq-ai-settings-page.php'; +require_once __DIR__ . '/includes/Admin/class-groq-ai-logs-admin.php'; require_once __DIR__ . '/includes/Admin/class-groq-ai-logs-table.php'; require_once __DIR__ . '/includes/Admin/class-groq-ai-product-ui.php'; +require_once __DIR__ . '/includes/Admin/class-groq-ai-settings-renderer.php'; if( ! class_exists( 'SitiWebUpdater' ) ){ include_once( plugin_dir_path( __FILE__ ) . 'SitiWebUpdater.php' ); @@ -85,9 +91,18 @@ final class Groq_AI_Product_Text_Plugin { /** @var Groq_AI_Service_Container */ private $container; - /** @var */ + /** @var Groq_AI_Product_Text_Settings_Page */ private $settings_page; + /** @var Groq_AI_Categories_Admin */ + private $categories_admin; + + /** @var Groq_AI_Brands_Admin */ + private $brands_admin; + + /** @var Groq_AI_Logs_Admin */ + private $logs_admin; + /** @var Groq_AI_Product_Text_Product_UI */ private $product_ui; @@ -106,6 +121,9 @@ final class Groq_AI_Product_Text_Plugin { $this->register_services(); $this->settings_page = new Groq_AI_Product_Text_Settings_Page( $this, $this->get_provider_manager() ); + $this->categories_admin = new Groq_AI_Categories_Admin( $this ); + $this->brands_admin = new Groq_AI_Brands_Admin( $this ); + $this->logs_admin = new Groq_AI_Logs_Admin( $this ); $this->product_ui = new Groq_AI_Product_Text_Product_UI( $this ); add_action( 'init', [ $this, 'load_textdomain' ] ); @@ -366,6 +384,14 @@ final class Groq_AI_Product_Text_Plugin { return $this->get_settings_manager()->get_loggable_settings_snapshot( $settings ); } + public function create_settings_renderer( $values = null ) { + if ( null === $values ) { + $values = $this->get_settings(); + } + + return new Groq_AI_Settings_Renderer( self::OPTION_KEY, $values ); + } + public function should_use_response_format( Groq_AI_Provider_Interface $provider, $settings ) { return ! $this->is_response_format_compat_enabled( $settings ) && $provider->supports_response_format(); } diff --git a/includes/Admin/class-groq-ai-admin-base.php b/includes/Admin/class-groq-ai-admin-base.php new file mode 100644 index 0000000..0742318 --- /dev/null +++ b/includes/Admin/class-groq-ai-admin-base.php @@ -0,0 +1,46 @@ +plugin = $plugin; + } + + protected function get_page_url( $slug = 'groq-ai-product-text', $args = [] ) { + $slug = sanitize_key( (string) $slug ); + $url = add_query_arg( + [ + 'page' => $slug, + ], + admin_url( 'options-general.php' ) + ); + + if ( ! empty( $args ) ) { + $url = add_query_arg( $args, $url ); + } + + return $url; + } + + protected function current_user_can_manage() { + return current_user_can( 'manage_options' ); + } + + protected function enqueue_admin_styles() { + wp_enqueue_style( + 'groq-ai-settings', + plugins_url( 'assets/css/admin.css', GROQ_AI_PRODUCT_TEXT_FILE ), + [], + GROQ_AI_PRODUCT_TEXT_VERSION + ); + + wp_enqueue_style( + 'groq-ai-settings-extra', + plugins_url( 'assets/css/settings.css', GROQ_AI_PRODUCT_TEXT_FILE ), + [ 'groq-ai-settings' ], + GROQ_AI_PRODUCT_TEXT_VERSION + ); + } +} diff --git a/includes/Admin/class-groq-ai-brands-admin.php b/includes/Admin/class-groq-ai-brands-admin.php new file mode 100644 index 0000000..c7cd397 --- /dev/null +++ b/includes/Admin/class-groq-ai-brands-admin.php @@ -0,0 +1,177 @@ +register_term_page(); + } + + public function render_brands_overview_page() { + if ( ! $this->current_user_can_manage() ) { + return; + } + + $taxonomy = $this->detect_brand_taxonomy(); + if ( '' === $taxonomy ) { + ?> +
+

+

+
+ get_term_overview_data( $taxonomy ); + $rows = isset( $overview['rows'] ) ? $overview['rows'] : []; + $empty_count = isset( $overview['empty_count'] ) ? (int) $overview['empty_count'] : 0; + ?> +
+

+

+ +

+ render_term_bulk_panel( __( 'merken', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $empty_count ); ?> +

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+ enqueue_admin_styles(); + + $taxonomy = $this->detect_brand_taxonomy(); + if ( '' === $taxonomy ) { + return; + } + + wp_enqueue_script( + 'groq-ai-term-bulk', + plugins_url( 'assets/js/term-bulk.js', GROQ_AI_PRODUCT_TEXT_FILE ), + [], + GROQ_AI_PRODUCT_TEXT_VERSION, + true + ); + + $this->localize_term_bulk_script( + $taxonomy, + [ + 'allowRegenerate' => true, + 'strings' => [ + 'statusIdle' => __( 'Bulk gestart. AI werkt de geselecteerde merken bij…', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'statusProgress' => __( 'Merk %1$s van %2$s: %3$s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'statusDone' => __( 'Klaar! %d merken bijgewerkt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'statusStopped' => __( 'Bulk generatie gestopt. %d merken bijgewerkt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'statusEmpty' => __( 'Geen merken zonder omschrijving gevonden.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'logSuccess' => __( '%1$s gevuld (%2$d woorden).', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'logError' => __( '%1$s mislukt: %2$s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'confirmStop' => __( 'Weet je zeker dat je wilt stoppen? Het huidige merk kan onafgemaakt blijven.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'confirmRegenerate' => __( 'Wil je %s opnieuw laten schrijven?', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'regenerateProgress' => __( '%s wordt opnieuw geschreven…', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'regenerateDone' => __( '%s is bijgewerkt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'regenerateError' => __( 'Kon %1$s niet bijwerken: %2$s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'regenerateBlocked' => __( 'Wacht tot de bulk generatie klaar is voordat je een merk opnieuw genereert.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + ], + ] + ); + } + + private function detect_brand_taxonomy() { + if ( null !== $this->brand_taxonomy ) { + return $this->brand_taxonomy; + } + + $candidates = [ + 'product_brand', + 'pwb-brand', + 'yith_product_brand', + 'berocket_brand', + ]; + + if ( taxonomy_exists( 'pa_brand' ) ) { + array_unshift( $candidates, 'pa_brand' ); + } + + $candidates = apply_filters( 'groq_ai_brand_taxonomy_candidates', $candidates ); + $found = ''; + foreach ( $candidates as $tax ) { + $tax = sanitize_key( (string) $tax ); + if ( $tax && taxonomy_exists( $tax ) ) { + $found = $tax; + break; + } + } + + $found = apply_filters( 'groq_ai_brand_taxonomy', $found ); + $this->brand_taxonomy = sanitize_key( (string) $found ); + + return $this->brand_taxonomy; + } +} diff --git a/includes/Admin/class-groq-ai-categories-admin.php b/includes/Admin/class-groq-ai-categories-admin.php new file mode 100644 index 0000000..26f1c16 --- /dev/null +++ b/includes/Admin/class-groq-ai-categories-admin.php @@ -0,0 +1,119 @@ +register_term_page(); + } + + public function render_categories_overview_page() { + if ( ! $this->current_user_can_manage() ) { + return; + } + + $taxonomy = 'product_cat'; + $overview = $this->get_term_overview_data( $taxonomy ); + $rows = isset( $overview['rows'] ) ? $overview['rows'] : []; + $empty_count = isset( $overview['empty_count'] ) ? (int) $overview['empty_count'] : 0; + ?> +
+

+

+ render_term_bulk_panel( __( 'categorieën', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $empty_count ); ?> + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+ enqueue_admin_styles(); + + wp_enqueue_script( + 'groq-ai-term-bulk', + plugins_url( 'assets/js/term-bulk.js', GROQ_AI_PRODUCT_TEXT_FILE ), + [], + GROQ_AI_PRODUCT_TEXT_VERSION, + true + ); + + $this->localize_term_bulk_script( + 'product_cat', + [ + 'allowRegenerate' => true, + 'strings' => [ + 'statusIdle' => __( 'Bulk gestart. AI werkt de geselecteerde categorieën bij…', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'statusProgress' => __( 'Categorie %1$s van %2$s: %3$s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'statusDone' => __( 'Klaar! %d categorieën bijgewerkt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'statusStopped' => __( 'Bulk generatie gestopt. %d categorieën bijgewerkt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'statusEmpty' => __( 'Geen categorieën zonder omschrijving gevonden.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'logSuccess' => __( '%1$s gevuld (%2$d woorden).', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'logError' => __( '%1$s mislukt: %2$s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'confirmStop' => __( 'Weet je zeker dat je wilt stoppen? De huidige categorie kan onafgemaakt blijven.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'confirmRegenerate' => __( 'Wil je categorie %s opnieuw laten schrijven?', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'regenerateProgress' => __( '%s wordt opnieuw geschreven…', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'regenerateDone' => __( '%s is bijgewerkt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'regenerateError' => __( 'Kon %1$s niet bijwerken: %2$s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'regenerateBlocked' => __( 'Wacht tot de bulk generatie klaar is voordat je een categorie opnieuw genereert.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + ], + ] + ); + } +} diff --git a/includes/Admin/class-groq-ai-logs-admin.php b/includes/Admin/class-groq-ai-logs-admin.php new file mode 100644 index 0000000..5aebd6b --- /dev/null +++ b/includes/Admin/class-groq-ai-logs-admin.php @@ -0,0 +1,180 @@ +current_user_can_manage() ) { + return; + } + + $logs_table = new Groq_AI_Logs_Table( $this->plugin ); + $logs_table->prepare_items(); + ?> +
+

+
+ + search_box( __( 'Zoek logboek', GROQ_AI_PRODUCT_TEXT_DOMAIN ), 'groq-ai-logs' ); ?> + display(); ?> +
+
+ current_user_can_manage() ) { + return; + } + + $log_id = isset( $_GET['log_id'] ) ? absint( $_GET['log_id'] ) : 0; + $back_url = $this->get_page_url( 'groq-ai-product-text-logs' ); + $log = null; + + if ( $log_id ) { + global $wpdb; + $table = $wpdb->prefix . 'groq_ai_generation_logs'; + $query = $wpdb->prepare( + "SELECT l.*, p.post_title FROM {$table} l LEFT JOIN {$wpdb->posts} p ON p.ID = l.post_id WHERE l.id = %d", + $log_id + ); + $log = $wpdb->get_row( $query, ARRAY_A ); + } + + ?> +
+

+

+ +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ display_name ) : esc_html( (string) $log['user_id'] ); + } else { + echo '—'; + } + ?> +
+ ' . esc_html( $title ) . '' : esc_html( $title ); + } else { + echo '—'; + } + ?> +
+ +
+ +

+
+					
+				
+ +

+
+					
+				
+ + +

+
+							
+						
+ + + +

+
+							
+						
+ + +
+ plugin = $plugin; - $this->provider_manager = $provider_manager; + public function __construct( Groq_AI_Product_Text_Plugin $plugin, Groq_AI_Provider_Manager $provider_manager ) { + parent::__construct( $plugin ); + $this->provider_manager = $provider_manager; add_action( 'admin_menu', [ $this, 'register_settings_pages' ] ); add_action( 'admin_init', [ $this, 'register_settings' ] ); @@ -17,7 +14,6 @@ class Groq_AI_Product_Text_Settings_Page { add_action( 'admin_post_groq_ai_google_oauth_start', [ $this, 'handle_google_oauth_start' ] ); add_action( 'admin_post_groq_ai_google_oauth_callback', [ $this, 'handle_google_oauth_callback' ] ); add_action( 'admin_post_groq_ai_google_oauth_disconnect', [ $this, 'handle_google_oauth_disconnect' ] ); - add_action( 'admin_post_groq_ai_save_term_content', [ $this, 'handle_save_term_content' ] ); add_action( 'admin_post_groq_ai_google_test_connection', [ $this, 'handle_google_test_connection' ] ); } @@ -30,33 +26,6 @@ class Groq_AI_Product_Text_Settings_Page { [ $this, 'render_settings_page' ] ); - add_submenu_page( - 'options-general.php', - __( 'Siti AI Categorie teksten', GROQ_AI_PRODUCT_TEXT_DOMAIN ), - __( 'Siti AI Categorieën', GROQ_AI_PRODUCT_TEXT_DOMAIN ), - 'manage_options', - 'groq-ai-product-text-categories', - [ $this, 'render_categories_overview_page' ] - ); - - add_submenu_page( - 'options-general.php', - __( 'Siti AI Merk teksten', GROQ_AI_PRODUCT_TEXT_DOMAIN ), - __( 'Siti AI Merken', GROQ_AI_PRODUCT_TEXT_DOMAIN ), - 'manage_options', - 'groq-ai-product-text-brands', - [ $this, 'render_brands_overview_page' ] - ); - - add_submenu_page( - 'options-general.php', - __( 'Siti AI Term tekst', GROQ_AI_PRODUCT_TEXT_DOMAIN ), - __( 'Siti AI Term tekst', GROQ_AI_PRODUCT_TEXT_DOMAIN ), - 'manage_options', - 'groq-ai-product-text-term', - [ $this, 'render_term_generator_page' ] - ); - add_submenu_page( 'options-general.php', __( 'Siti AI Modules', GROQ_AI_PRODUCT_TEXT_DOMAIN ), @@ -66,15 +35,6 @@ class Groq_AI_Product_Text_Settings_Page { [ $this, 'render_modules_page' ] ); - add_submenu_page( - 'options-general.php', - __( 'Siti AI AI-logboek', GROQ_AI_PRODUCT_TEXT_DOMAIN ), - __( 'Siti AI AI-logboek', GROQ_AI_PRODUCT_TEXT_DOMAIN ), - 'manage_options', - 'groq-ai-product-text-logs', - [ $this, 'render_logs_page' ] - ); - add_submenu_page( 'options-general.php', __( 'Siti AI Prompt instellingen', GROQ_AI_PRODUCT_TEXT_DOMAIN ), @@ -84,32 +44,7 @@ class Groq_AI_Product_Text_Settings_Page { [ $this, 'render_prompt_settings_page' ] ); - add_submenu_page( - 'options-general.php', - __( 'Siti AI Log detail', GROQ_AI_PRODUCT_TEXT_DOMAIN ), - __( 'Siti AI Log detail', GROQ_AI_PRODUCT_TEXT_DOMAIN ), - 'manage_options', - 'groq-ai-product-text-log', - [ $this, 'render_log_detail_page' ] - ); - } - private function get_page_url( $slug = 'groq-ai-product-text', $args = [] ) { - $slug = sanitize_key( (string) $slug ); - $url = add_query_arg( - [ - 'page' => $slug, - ], - admin_url( 'options-general.php' ) - ); - - if ( ! empty( $args ) ) { - $url = add_query_arg( $args, $url ); - } - - return $url; - } - private function get_request_redirect_url( $field, $page_slug = 'groq-ai-product-text' ) { $default = $this->get_page_url( $page_slug ); $value = isset( $_REQUEST[ $field ] ) ? wp_unslash( $_REQUEST[ $field ] ) : ''; @@ -151,22 +86,6 @@ class Groq_AI_Product_Text_Settings_Page { exit; } - private function redirect_with_term_notice( $taxonomy, $term_id, $type, $message = '', $status = 'success' ) { - $url = ( $taxonomy && $term_id ) ? $this->get_term_page_url( $taxonomy, $term_id ) : $this->get_page_url( 'groq-ai-product-text-categories' ); - - $args = [ - 'groq_ai_term_notice' => sanitize_key( (string) $type ), - 'groq_ai_term_status' => sanitize_key( (string) $status ), - ]; - - if ( '' !== $message ) { - $args['groq_ai_term_notice_message'] = rawurlencode( (string) $message ); - } - - wp_safe_redirect( add_query_arg( $args, $url ) ); - exit; - } - private function get_google_redirect_uri() { return add_query_arg( 'action', @@ -220,6 +139,7 @@ class Groq_AI_Product_Text_Settings_Page { $google_safety_settings = $this->plugin->get_google_safety_settings( $settings ); $google_safety_categories = $this->plugin->get_google_safety_categories(); $google_safety_thresholds = $this->plugin->get_google_safety_thresholds(); + $renderer = $this->plugin->create_settings_renderer( $settings ); ?>
@@ -248,143 +168,158 @@ class Groq_AI_Product_Text_Settings_Page {

- - - - - - - - - - get_key(); - $option_field = $provider->get_option_key(); - $value = isset( $settings[ $option_field ] ) ? (string) $settings[ $option_field ] : ''; - ?> - - - - - - + open_table(); + $provider_options = []; + foreach ( $providers as $provider ) { + $provider_options[ $provider->get_key() ] = $provider->get_label(); + } + + $renderer->field( + [ + 'label' => __( 'Aanbieder', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'key' => 'provider', + 'type' => 'select', + 'options' => $provider_options, + 'attributes' => [ + 'id' => 'groq-ai-provider', + ], + 'description' => __( 'Selecteer welke aanbieder de product- en termteksten schrijft.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + ] + ); + + $renderer->field( + [ + 'label' => __( 'Model', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'key' => 'model', + 'renderer' => [ $this, 'render_model_select_field' ], + 'attributes' => [ + 'id' => 'groq-ai-model-select', + ], + ] + ); + + foreach ( $providers as $provider ) { + $provider_key = $provider->get_key(); + $option_field = $provider->get_option_key(); + $renderer->field( + [ + 'label' => __( 'API-sleutel', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'key' => $option_field, + 'type' => 'password', + 'attributes' => [ + 'id' => 'groq-ai-api-' . $provider_key, + 'class' => 'regular-text', + 'autocomplete' => 'off', + ], + 'row_attributes' => [ + 'id' => 'groq_ai_api_key_' . $provider_key, + 'data-provider-row' => $provider_key, + ], + 'description' => sprintf( esc_html__( 'Voer de API-sleutel in voor %s.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), esc_html( $provider->get_label() ) ), + 'renderer' => [ $this, 'render_provider_api_key_field' ], + 'provider_key' => $provider_key, + 'google_safety_categories' => $google_safety_categories, + 'google_safety_thresholds' => $google_safety_thresholds, + 'google_safety_settings' => $google_safety_settings, + ] + ); + } + + $renderer->close_table(); + ?>

- - - - - - - - - - - - - - + open_table(); + $renderer->field( + [ + 'label' => __( 'Maximale output tokens', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'key' => 'max_output_tokens', + 'type' => 'number', + 'attributes' => [ + 'min' => 128, + 'max' => 8192, + ], + 'description' => __( 'Limitering van het aantal tokens per output voor compatibiliteit met verschillende modellen.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + ] + ); + $renderer->field( + [ + 'label' => __( 'Term meta key (onderste tekst)', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'key' => 'term_bottom_description_meta_key', + 'placeholder' => 'groq_ai_term_bottom_description', + 'description' => __( 'Optioneel: overschrijf in welke term meta key de onderste omschrijving moet landen.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + ] + ); + $renderer->field( + [ + 'label' => __( 'Response format fallback', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'renderer' => function ( $field, $renderer ) { + $this->render_response_format_compat_field(); + }, + ] + ); + $renderer->close_table(); + ?> +

- - - - - - - - - - - - - - - - - - + open_table(); + $renderer->field( + [ + 'label' => __( 'Google OAuth client ID', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'key' => 'google_oauth_client_id', + 'attributes' => [ 'autocomplete' => 'off' ], + 'description' => __( 'Stel deze plugin in als OAuth-client in Google Cloud Console en gebruik onderstaande redirect-URL.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + ] + ); + $renderer->field( + [ + 'label' => __( 'Google OAuth client secret', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'key' => 'google_oauth_client_secret', + 'type' => 'password', + 'attributes' => [ 'autocomplete' => 'off' ], + 'description' => sprintf( + '%s
%s', + esc_html__( 'Redirect URI voor OAuth (voeg exact zo toe in Google Cloud → Credentials):', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + esc_html( $oauth_redirect ) + ), + ] + ); + $renderer->field( + [ + 'label' => __( 'Search Console koppeling', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'type' => 'checkbox', + 'key' => 'google_enable_gsc', + 'checkbox_label' => __( 'Search Console data gebruiken in term prompts', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'renderer' => function ( $field, $renderer ) use ( $option_key, $settings ) { + $value = ! empty( $settings['google_gsc_site_url'] ) ? $settings['google_gsc_site_url'] : ''; + printf( + '

', + esc_attr( $option_key ), + esc_attr( $value ) + ); + }, + ] + ); + $renderer->field( + [ + 'label' => __( 'Analytics koppeling', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'type' => 'checkbox', + 'key' => 'google_enable_ga', + 'checkbox_label' => __( 'GA4 data meesturen (landing page statistieken)', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'renderer' => function ( $field, $renderer ) use ( $option_key, $settings ) { + $value = ! empty( $settings['google_ga4_property_id'] ) ? $settings['google_ga4_property_id'] : ''; + printf( + '

', + esc_attr( $option_key ), + esc_attr( $value ) + ); + }, + ] + ); + $renderer->close_table(); + ?>

@@ -458,325 +393,6 @@ class Groq_AI_Product_Text_Settings_Page { brand_taxonomy ) { - return $this->brand_taxonomy; - } - - $candidates = [ - 'product_brand', - 'pwb-brand', - 'yith_product_brand', - 'berocket_brand', - ]; - - // Attribute-taxonomy fallback (vaak pa_brand). - if ( taxonomy_exists( 'pa_brand' ) ) { - array_unshift( $candidates, 'pa_brand' ); - } - - $candidates = apply_filters( 'groq_ai_brand_taxonomy_candidates', $candidates ); - $found = ''; - foreach ( $candidates as $tax ) { - $tax = sanitize_key( (string) $tax ); - if ( $tax && taxonomy_exists( $tax ) ) { - $found = $tax; - break; - } - } - - $found = apply_filters( 'groq_ai_brand_taxonomy', $found ); - $this->brand_taxonomy = sanitize_key( (string) $found ); - return $this->brand_taxonomy; - } - - private function get_term_page_url( $taxonomy, $term_id ) { - return add_query_arg( - [ - 'page' => 'groq-ai-product-text-term', - 'taxonomy' => sanitize_key( (string) $taxonomy ), - 'term_id' => absint( $term_id ), - ], - admin_url( 'options-general.php' ) - ); - } - - private function get_term_overview_data( $taxonomy ) { - $taxonomy = sanitize_key( (string) $taxonomy ); - - if ( isset( $this->term_overview_cache[ $taxonomy ] ) ) { - return $this->term_overview_cache[ $taxonomy ]; - } - - $rows = []; - $empty_rows = []; - - if ( '' !== $taxonomy && taxonomy_exists( $taxonomy ) ) { - $terms = get_terms( - [ - 'taxonomy' => $taxonomy, - 'hide_empty' => false, - 'orderby' => 'name', - 'order' => 'ASC', - 'number' => 0, - ] - ); - - if ( is_wp_error( $terms ) ) { - $terms = []; - } - - foreach ( $terms as $term ) { - if ( ! $term || ! is_object( $term ) || empty( $term->term_id ) ) { - continue; - } - - $words = $this->count_words( isset( $term->description ) ? $term->description : '' ); - $has_description = $words > 0; - - $row = [ - 'id' => absint( $term->term_id ), - 'name' => (string) $term->name, - 'slug' => (string) $term->slug, - 'count' => isset( $term->count ) ? absint( $term->count ) : 0, - 'words' => $words, - 'has_description' => $has_description, - 'url' => $this->get_term_page_url( $taxonomy, $term->term_id ), - ]; - - $rows[] = $row; - if ( ! $has_description ) { - $empty_rows[] = $row; - } - } - } - - $data = [ - 'rows' => $rows, - 'empty_rows' => $empty_rows, - 'empty_count' => count( $empty_rows ), - ]; - - $this->term_overview_cache[ $taxonomy ] = $data; - - return $data; - } - - private function render_term_bulk_panel( $label_plural, $empty_count ) { - $label_plural = (string) $label_plural; - ?> -
-

- 0 ) { - printf( - /* translators: 1: amount, 2: label plural (e.g. categorieën) */ - esc_html__( 'Er zijn %1$d %2$s zonder omschrijving. Klik op de knop hieronder om automatisch teksten te genereren.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), - (int) $empty_count, - esc_html( $label_plural ) - ); - } else { - printf( - esc_html__( 'Alle %s hebben al een omschrijving.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), - esc_html( $label_plural ) - ); - } - ?> -

-

- - - -

-
-
    -
    - get_term_overview_data( $taxonomy ); - $rows = isset( $overview['rows'] ) ? $overview['rows'] : []; - - $terms = []; - foreach ( $rows as $row ) { - $terms[] = [ - 'id' => isset( $row['id'] ) ? (int) $row['id'] : 0, - 'name' => isset( $row['name'] ) ? (string) $row['name'] : '', - 'slug' => isset( $row['slug'] ) ? (string) $row['slug'] : '', - 'count' => isset( $row['count'] ) ? (int) $row['count'] : 0, - 'words' => isset( $row['words'] ) ? (int) $row['words'] : 0, - 'hasDescription' => ! empty( $row['has_description'] ), - ]; - } - - $defaults = [ - 'ajaxUrl' => admin_url( 'admin-ajax.php' ), - 'nonce' => wp_create_nonce( 'groq_ai_bulk_generate_terms' ), - 'taxonomy' => $taxonomy, - 'terms' => $terms, - 'allowRegenerate' => false, - 'strings' => [], - ]; - - $config = wp_parse_args( $overrides, $defaults ); - - wp_localize_script( 'groq-ai-term-bulk', 'GroqAITermBulk', $config ); - } - - public function render_categories_overview_page() { - if ( ! current_user_can( 'manage_options' ) ) { - return; - } - - $taxonomy = 'product_cat'; - $overview = $this->get_term_overview_data( $taxonomy ); - $rows = isset( $overview['rows'] ) ? $overview['rows'] : []; - $empty_count = isset( $overview['empty_count'] ) ? (int) $overview['empty_count'] : 0; - ?> -
    -

    -

    - render_term_bulk_panel( __( 'categorieën', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $empty_count ); ?> - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - - - -
    -
    - detect_brand_taxonomy(); - if ( '' === $taxonomy ) { - ?> -
    -

    -

    -
    - get_term_overview_data( $taxonomy ); - $rows = isset( $overview['rows'] ) ? $overview['rows'] : []; - $empty_count = isset( $overview['empty_count'] ) ? (int) $overview['empty_count'] : 0; - ?> -
    -

    -

    - -

    - render_term_bulk_panel( __( 'merken', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $empty_count ); ?> -

    - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - - - -
    -
    - plugin ); - $logs_table->prepare_items(); - ?> -
    -

    -
    - - search_box( __( 'Zoek logboek', GROQ_AI_PRODUCT_TEXT_DOMAIN ), 'groq-ai-logs' ); ?> - display(); ?> -
    -
    - get_page_url( 'groq-ai-product-text-logs' ); - $log = null; - - if ( $log_id ) { - global $wpdb; - $table = $wpdb->prefix . 'groq_ai_generation_logs'; - $query = $wpdb->prepare( - "SELECT l.*, p.post_title FROM {$table} l LEFT JOIN {$wpdb->posts} p ON p.ID = l.post_id WHERE l.id = %d", - $log_id - ); - $log = $wpdb->get_row( $query, ARRAY_A ); - } - - ?> -
    -

    -

    - -

    - -

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - display_name ) : esc_html( (string) $log['user_id'] ); - } else { - echo '—'; - } - ?> -
    - ' . esc_html( $title ) . '' : esc_html( $title ); - } else { - echo '—'; - } - ?> -
    - -
    - -

    -
    - -

    -
    - - -

    -
    - - - -

    -
    - - -
    - -
    -

    -

    -
    - -
    -

    -

    -
    - count_words( $term->description ); - $meta_prompt = get_term_meta( $term_id, 'groq_ai_term_custom_prompt', true ); - $settings = $this->plugin->get_settings(); - $bottom_meta_key = $this->resolve_term_bottom_description_meta_key( $term, $settings ); - $effective_bottom_meta_key = '' !== $bottom_meta_key ? $bottom_meta_key : 'groq_ai_term_bottom_description'; - $bottom_description = (string) get_term_meta( $term_id, $effective_bottom_meta_key, true ); - $rankmath_module_enabled = $this->plugin->is_module_enabled( 'rankmath', $settings ); - $rankmath_active = $this->plugin->is_rankmath_active(); - $rankmath_title = ''; - $rankmath_description = ''; - $rankmath_focus_keywords = ''; - if ( $rankmath_module_enabled ) { - $rankmath_keys = $this->resolve_rankmath_term_meta_keys( $term, $settings ); - $rankmath_title = (string) get_term_meta( $term_id, $rankmath_keys['title'], true ); - $rankmath_description = (string) get_term_meta( $term_id, $rankmath_keys['description'], true ); - $rankmath_focus_keywords = (string) get_term_meta( $term_id, $rankmath_keys['focus_keyword'], true ); - } - $default_prompt = $this->get_term_prompt_text( $term, $meta_prompt ); - ?> -
    -

    - : name ); ?> -

    - - -
    -

    -
    - -

    - -

    - -

    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - -
    - -

    -
    -

    -

    - -   - -

    - -

    - - -

    -
    -

    - -

    - - -

    - -

    - -

    - - -

    -
    
    -			
    -
    - 'rank_math_title', - 'description' => 'rank_math_description', - 'focus_keyword' => 'rank_math_focus_keyword', - ]; - $keys = apply_filters( 'groq_ai_rankmath_term_meta_keys', $keys, $term, $settings ); - if ( ! is_array( $keys ) ) { - $keys = []; - } - - return [ - 'title' => isset( $keys['title'] ) ? sanitize_key( (string) $keys['title'] ) : 'rank_math_title', - 'description' => isset( $keys['description'] ) ? sanitize_key( (string) $keys['description'] ) : 'rank_math_description', - 'focus_keyword' => isset( $keys['focus_keyword'] ) ? sanitize_key( (string) $keys['focus_keyword'] ) : 'rank_math_focus_keyword', - ]; - } - - private function get_term_prompt_text( $term, $custom_prompt = null ) { - $prompt = ( null !== $custom_prompt ) ? $custom_prompt : ''; - - if ( null === $custom_prompt && $term && isset( $term->term_id ) ) { - $prompt = get_term_meta( $term->term_id, 'groq_ai_term_custom_prompt', true ); - } - - $prompt = trim( (string) $prompt ); - if ( '' !== $prompt ) { - return $prompt; - } - - $default_prompt = __( 'Schrijf een SEO-vriendelijke categorieomschrijving in het Nederlands. Gebruik duidelijke tussenkoppen en

    -tags. Voeg geen prijsinformatie toe.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); - - return apply_filters( 'groq_ai_default_term_prompt', $default_prompt, $term ); - } - - - public function render_product_attribute_includes_field() { $settings = $this->plugin->get_settings(); $values = isset( $settings['product_attribute_includes'] ) && is_array( $settings['product_attribute_includes'] ) @@ -1333,6 +553,106 @@ class Groq_AI_Product_Text_Settings_Page { +

    + +
    + +

    + ', $this->format_html_attributes( $attributes ) ); + + if ( isset( $field_args['provider_key'] ) && 'google' === $field_args['provider_key'] ) { + $this->render_google_safety_fields( $field_args ); + } + } + + private function render_google_safety_fields( $field_args ) { + $categories = isset( $field_args['google_safety_categories'] ) && is_array( $field_args['google_safety_categories'] ) + ? $field_args['google_safety_categories'] + : []; + $thresholds = isset( $field_args['google_safety_thresholds'] ) && is_array( $field_args['google_safety_thresholds'] ) + ? $field_args['google_safety_thresholds'] + : []; + + if ( empty( $categories ) || empty( $thresholds ) ) { + return; + } + + $selected_settings = isset( $field_args['google_safety_settings'] ) && is_array( $field_args['google_safety_settings'] ) + ? $field_args['google_safety_settings'] + : []; + $option_key = $this->plugin->get_option_key(); + ?> +
    + +

    + $info ) : + $category_label = isset( $info['label'] ) ? $info['label'] : $category_key; + $category_description = isset( $info['description'] ) ? $info['description'] : ''; + $selected_threshold = isset( $selected_settings[ $category_key ] ) ? $selected_settings[ $category_key ] : ''; + $field_id = 'groq-ai-google-safety-' . sanitize_html_class( $category_key ); + ?> + + +
    + $value ) { + if ( '' === $value && 0 !== $value && '0' !== $value ) { + continue; + } + + $pairs[] = sprintf( '%s="%s"', esc_attr( $key ), esc_attr( $value ) ); + } + + return implode( ' ', $pairs ); + } + private function get_product_attribute_include_options() { $options = [ '__custom__' => __( 'Custom attributen (niet-taxonomie)', GROQ_AI_PRODUCT_TEXT_DOMAIN ), @@ -1471,9 +791,6 @@ class Groq_AI_Product_Text_Settings_Page { 'settings_page_groq-ai-product-text', 'settings_page_groq-ai-product-text-modules', 'settings_page_groq-ai-product-text-prompts', - 'settings_page_groq-ai-product-text-categories', - 'settings_page_groq-ai-product-text-brands', - 'settings_page_groq-ai-product-text-term', 'settings_page_groq-ai-product-text-logs', ]; @@ -1489,19 +806,15 @@ class Groq_AI_Product_Text_Settings_Page { return; } - wp_enqueue_style( - 'groq-ai-settings', - plugins_url( 'assets/css/admin.css', GROQ_AI_PRODUCT_TEXT_FILE ), - [], - GROQ_AI_PRODUCT_TEXT_VERSION - ); + $this->enqueue_admin_styles(); - wp_enqueue_style( - 'groq-ai-settings-extra', - plugins_url( 'assets/css/settings.css', GROQ_AI_PRODUCT_TEXT_FILE ), - [ 'groq-ai-settings' ], - GROQ_AI_PRODUCT_TEXT_VERSION - ); + $current_page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; + + $is_main_settings_screen = ( 0 === strpos( (string) $hook, 'settings_page_groq-ai-product-text' ) ) && ( 'groq-ai-product-text' === $current_page ); + + if ( ! $is_main_settings_screen ) { + return; + } wp_enqueue_script( 'groq-ai-settings', @@ -1511,96 +824,8 @@ class Groq_AI_Product_Text_Settings_Page { true ); - $current_page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; - - if ( 0 === strpos( (string) $hook, 'settings_page_groq-ai-product-text-term' ) ) { - wp_enqueue_script( - 'groq-ai-term-admin', - plugins_url( 'assets/js/term-admin.js', GROQ_AI_PRODUCT_TEXT_FILE ), - [], - GROQ_AI_PRODUCT_TEXT_VERSION, - true - ); - - $taxonomy = isset( $_GET['taxonomy'] ) ? sanitize_key( wp_unslash( $_GET['taxonomy'] ) ) : ''; - $term_id = isset( $_GET['term_id'] ) ? absint( $_GET['term_id'] ) : 0; - wp_localize_script( - 'groq-ai-term-admin', - 'GroqAITermGenerator', - [ - 'ajaxUrl' => admin_url( 'admin-ajax.php' ), - 'nonce' => wp_create_nonce( 'groq_ai_generate_term' ), - 'taxonomy' => $taxonomy, - 'termId' => $term_id, - ] - ); - } - - $bulk_taxonomy = ''; - $bulk_allow_regen = false; - $bulk_strings = []; - - if ( 0 === strpos( (string) $hook, 'settings_page_groq-ai-product-text-categories' ) ) { - $bulk_taxonomy = 'product_cat'; - $bulk_allow_regen = true; - $bulk_strings = [ - 'statusIdle' => __( 'Bulk gestart. AI werkt de geselecteerde categorieën bij…', GROQ_AI_PRODUCT_TEXT_DOMAIN ), - 'statusProgress' => __( 'Categorie %1$s van %2$s: %3$s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), - 'statusDone' => __( 'Klaar! %d categorieën bijgewerkt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), - 'statusStopped' => __( 'Bulk generatie gestopt. %d categorieën bijgewerkt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), - 'statusEmpty' => __( 'Geen categorieën zonder omschrijving gevonden.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), - 'logSuccess' => __( '%1$s gevuld (%2$d woorden).', GROQ_AI_PRODUCT_TEXT_DOMAIN ), - 'logError' => __( '%1$s mislukt: %2$s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), - 'confirmStop' => __( 'Weet je zeker dat je wilt stoppen? De huidige categorie kan onafgemaakt blijven.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), - 'confirmRegenerate' => __( 'Wil je categorie %s opnieuw laten schrijven?', GROQ_AI_PRODUCT_TEXT_DOMAIN ), - 'regenerateProgress' => __( '%s wordt opnieuw geschreven…', GROQ_AI_PRODUCT_TEXT_DOMAIN ), - 'regenerateDone' => __( '%s is bijgewerkt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), - 'regenerateError' => __( 'Kon %1$s niet bijwerken: %2$s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), - 'regenerateBlocked' => __( 'Wacht tot de bulk generatie klaar is voordat je een categorie opnieuw genereert.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), - ]; - } elseif ( 0 === strpos( (string) $hook, 'settings_page_groq-ai-product-text-brands' ) ) { - $detected_taxonomy = $this->detect_brand_taxonomy(); - if ( '' !== $detected_taxonomy ) { - $bulk_taxonomy = $detected_taxonomy; - $bulk_allow_regen = true; - $bulk_strings = [ - 'statusIdle' => __( 'Bulk gestart. AI werkt de geselecteerde merken bij…', GROQ_AI_PRODUCT_TEXT_DOMAIN ), - 'statusProgress' => __( 'Merk %1$s van %2$s: %3$s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), - 'statusDone' => __( 'Klaar! %d merken bijgewerkt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), - 'statusStopped' => __( 'Bulk generatie gestopt. %d merken bijgewerkt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), - 'statusEmpty' => __( 'Geen merken zonder omschrijving gevonden.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), - 'logSuccess' => __( '%1$s gevuld (%2$d woorden).', GROQ_AI_PRODUCT_TEXT_DOMAIN ), - 'logError' => __( '%1$s mislukt: %2$s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), - 'confirmStop' => __( 'Weet je zeker dat je wilt stoppen? Het huidige merk kan onafgemaakt blijven.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), - 'confirmRegenerate' => __( 'Wil je %s opnieuw laten schrijven?', GROQ_AI_PRODUCT_TEXT_DOMAIN ), - 'regenerateProgress' => __( '%s wordt opnieuw geschreven…', GROQ_AI_PRODUCT_TEXT_DOMAIN ), - 'regenerateDone' => __( '%s is bijgewerkt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), - 'regenerateError' => __( 'Kon %1$s niet bijwerken: %2$s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), - 'regenerateBlocked' => __( 'Wacht tot de bulk generatie klaar is voordat je een merk opnieuw genereert.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), - ]; - } - } - - if ( '' !== $bulk_taxonomy ) { - wp_enqueue_script( - 'groq-ai-term-bulk', - plugins_url( 'assets/js/term-bulk.js', GROQ_AI_PRODUCT_TEXT_FILE ), - [], - GROQ_AI_PRODUCT_TEXT_VERSION, - true - ); - - $this->localize_term_bulk_script( - $bulk_taxonomy, - [ - 'allowRegenerate' => $bulk_allow_regen, - 'strings' => $bulk_strings, - ] - ); - } - $current_settings = $this->plugin->get_settings(); - $data = [ + $data = [ 'optionKey' => $this->plugin->get_option_key(), 'providers' => [], 'currentProvider' => $current_settings['provider'], @@ -1615,15 +840,15 @@ class Groq_AI_Product_Text_Settings_Page { ]; foreach ( $this->provider_manager->get_providers() as $provider ) { - $provider_key = $provider->get_key(); - $cached_models = $this->plugin->get_cached_models_for_provider( $provider_key ); - $cached_models = Groq_AI_Model_Exclusions::filter_models( $provider_key, $cached_models ); - $data['providers'][ $provider->get_key() ] = [ + $provider_key = $provider->get_key(); + $cached_models = $this->plugin->get_cached_models_for_provider( $provider_key ); + $cached_models = Groq_AI_Model_Exclusions::filter_models( $provider_key, $cached_models ); + $data['providers'][ $provider_key ] = [ 'default_label' => sprintf( __( 'Gebruik standaardmodel (%s)', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $provider->get_default_model() ), 'models' => $cached_models, 'supports_live' => $provider->supports_live_models(), ]; - $data['providerRows'][ $provider->get_key() ] = 'groq_ai_api_key_' . $provider->get_key(); + $data['providerRows'][ $provider_key ] = 'groq_ai_api_key_' . $provider_key; } wp_localize_script( 'groq-ai-settings', 'GroqAISettingsData', $data ); @@ -1861,67 +1086,4 @@ class Groq_AI_Product_Text_Settings_Page { $this->redirect_with_google_notice( 'test', implode( ' ', $messages ), $redirect ); } - public function handle_save_term_content() { - if ( ! current_user_can( 'manage_options' ) ) { - wp_die( esc_html__( 'Je hebt geen toestemming om deze actie uit te voeren.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), '', [ 'response' => 403 ] ); - } - - check_admin_referer( 'groq_ai_save_term_content' ); - - $taxonomy = isset( $_POST['taxonomy'] ) ? sanitize_key( wp_unslash( $_POST['taxonomy'] ) ) : ''; - $term_id = isset( $_POST['term_id'] ) ? absint( $_POST['term_id'] ) : 0; - - if ( '' === $taxonomy || ! taxonomy_exists( $taxonomy ) || ! $term_id ) { - $this->redirect_with_term_notice( $taxonomy, $term_id, 'error', __( 'Ongeldige term.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), 'error' ); - } - - $term = get_term( $term_id, $taxonomy ); - if ( ! $term || is_wp_error( $term ) ) { - $this->redirect_with_term_notice( $taxonomy, $term_id, 'error', __( 'Term niet gevonden.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), 'error' ); - } - - $description = isset( $_POST['description'] ) ? wp_kses_post( wp_unslash( $_POST['description'] ) ) : ''; - $bottom_description = isset( $_POST['groq_ai_term_bottom_description'] ) ? wp_kses_post( wp_unslash( $_POST['groq_ai_term_bottom_description'] ) ) : ''; - $custom_prompt = isset( $_POST['groq_ai_term_custom_prompt'] ) ? sanitize_textarea_field( wp_unslash( $_POST['groq_ai_term_custom_prompt'] ) ) : ''; - - $update = wp_update_term( - $term_id, - $taxonomy, - [ - 'description' => $description, - ] - ); - - if ( is_wp_error( $update ) ) { - $this->redirect_with_term_notice( $taxonomy, $term_id, 'error', $update->get_error_message(), 'error' ); - } - - $settings = $this->plugin->get_settings(); - $bottom_meta_key = $this->resolve_term_bottom_description_meta_key( $term, $settings ); - $bottom_meta_key = '' !== $bottom_meta_key ? $bottom_meta_key : 'groq_ai_term_bottom_description'; - update_term_meta( $term_id, $bottom_meta_key, $bottom_description ); - - if ( '' === trim( $custom_prompt ) ) { - delete_term_meta( $term_id, 'groq_ai_term_custom_prompt' ); - } else { - update_term_meta( $term_id, 'groq_ai_term_custom_prompt', $custom_prompt ); - } - - if ( isset( $_POST['groq_ai_term_bottom_description'] ) && $bottom_meta_key !== 'groq_ai_term_bottom_description' ) { - update_term_meta( $term_id, 'groq_ai_term_bottom_description', $bottom_description ); - } - - if ( $this->plugin->is_module_enabled( 'rankmath', $settings ) ) { - $rankmath_keys = $this->resolve_rankmath_term_meta_keys( $term, $settings ); - $rankmath_title = isset( $_POST['groq_ai_rankmath_meta_title'] ) ? sanitize_text_field( wp_unslash( $_POST['groq_ai_rankmath_meta_title'] ) ) : ''; - $rankmath_description = isset( $_POST['groq_ai_rankmath_meta_description'] ) ? sanitize_textarea_field( wp_unslash( $_POST['groq_ai_rankmath_meta_description'] ) ) : ''; - $rankmath_keywords = isset( $_POST['groq_ai_rankmath_focus_keywords'] ) ? sanitize_text_field( wp_unslash( $_POST['groq_ai_rankmath_focus_keywords'] ) ) : ''; - - update_term_meta( $term_id, $rankmath_keys['title'], $rankmath_title ); - update_term_meta( $term_id, $rankmath_keys['description'], $rankmath_description ); - update_term_meta( $term_id, $rankmath_keys['focus_keyword'], $rankmath_keywords ); - } - - $this->redirect_with_term_notice( $taxonomy, $term_id, 'saved', __( 'Term opgeslagen.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ); - } } diff --git a/includes/Admin/class-groq-ai-settings-renderer.php b/includes/Admin/class-groq-ai-settings-renderer.php new file mode 100644 index 0000000..6908c6e --- /dev/null +++ b/includes/Admin/class-groq-ai-settings-renderer.php @@ -0,0 +1,262 @@ +option_key = $option_key; + $this->set_values( $values ); + } + + public function set_values( $values ) { + $this->values = is_array( $values ) ? $values : []; + } + + public function open_table( $args = [] ) { + $defaults = [ + 'class' => 'form-table', + 'role' => 'presentation', + ]; + $args = wp_parse_args( $args, $defaults ); + + printf( '', $this->build_attr_string( $args ) ); + } + + public function close_table() { + echo '
    '; + } + + public function field( $args ) { + $defaults = [ + 'key' => '', + 'name' => '', + 'id' => '', + 'label' => '', + 'description' => '', + 'type' => 'text', + 'placeholder' => '', + 'options' => [], + 'attributes' => [], + 'default' => '', + 'value' => null, + 'renderer' => null, + 'row_attributes' => [], + 'row_class' => '', + ]; + $args = wp_parse_args( $args, $defaults ); + + if ( '' === $args['name'] && '' !== $args['key'] ) { + $args['name'] = $this->build_field_name( $args['key'] ); + } + + if ( '' === $args['id'] && '' !== $args['key'] ) { + $args['id'] = $this->build_field_id( $args['key'] ); + } + + if ( null === $args['value'] && '' !== $args['key'] ) { + $args['value'] = $this->get_value( $args['key'], $args['default'] ); + } + + if ( ! isset( $args['attributes']['id'] ) && '' !== $args['id'] ) { + $args['attributes']['id'] = $args['id']; + } + + $type = $args['type']; + + $row_attributes = $this->prepare_row_attributes( $args ); + $row_attr_string = $row_attributes ? ' ' . $this->build_attr_string( $row_attributes ) : ''; + + echo ''; + $this->render_label_cell( $args ); + echo ''; + + if ( is_callable( $args['renderer'] ) ) { + call_user_func( $args['renderer'], $args, $this ); + } else { + switch ( $type ) { + case 'textarea': + $this->render_textarea( $args ); + break; + case 'password': + $this->render_input( 'password', $args ); + break; + case 'number': + $this->render_input( 'number', $args ); + break; + case 'select': + $this->render_select( $args ); + break; + case 'checkbox': + $this->render_checkbox( $args ); + break; + case 'toggle': + $this->render_toggle( $args ); + break; + default: + $this->render_input( 'text', $args ); + } + } + + $this->render_description( $args['description'] ); + + echo ''; + echo ''; + } + + private function render_label_cell( $args ) { + $label = $args['label']; + $id = $args['id']; + echo ''; + if ( '' !== $label ) { + printf( '', esc_attr( $id ), esc_html( $label ) ); + } + echo ''; + } + + private function render_input( $type, $args ) { + $attributes = $this->prepare_input_attributes( $args ); + printf( '', esc_attr( $type ), $attributes ); + } + + private function render_textarea( $args ) { + $attributes = $this->prepare_input_attributes( $args, [ 'rows' => 4, 'class' => 'large-text' ] ); + printf( '', $attributes, esc_textarea( $args['value'] ) ); + } + + private function render_select( $args ) { + $attributes = $this->prepare_input_attributes( $args ); + printf( ''; + } + + private function render_checkbox( $args ) { + $value = ! empty( $args['value'] ); + $attributes = $this->prepare_input_attributes( $args, [ 'class' => '' ] ); + printf( '', $attributes, checked( $value, true, false ), esc_html( $args['checkbox_label'] ?? '' ) ); + } + + private function render_toggle( $args ) { + $value = ! empty( $args['value'] ); + $attributes = $this->prepare_input_attributes( $args, [ 'class' => '' ] ); + printf( '', $attributes, checked( $value, true, false ), esc_html( $args['checkbox_label'] ?? '' ) ); + } + + private function render_description( $text ) { + $text = trim( (string) $text ); + if ( '' === $text ) { + return; + } + + printf( '

    %s

    ', wp_kses_post( $text ) ); + } + + private function prepare_input_attributes( $args, $defaults = [] ) { + $attributes = wp_parse_args( $args['attributes'], $defaults ); + $attributes['name'] = $args['name']; + + if ( ! isset( $attributes['id'] ) ) { + $attributes['id'] = $args['id']; + } + + if ( '' !== $args['placeholder'] ) { + $attributes['placeholder'] = $args['placeholder']; + } + + if ( ! isset( $attributes['class'] ) ) { + $attributes['class'] = 'regular-text'; + } + + if ( ! in_array( $args['type'], [ 'checkbox', 'toggle', 'select', 'textarea' ], true ) ) { + $attributes['value'] = $args['value']; + } + + return $this->build_attr_string( $attributes ); + } + private function prepare_row_attributes( $args ) { + $attributes = []; + if ( isset( $args['row_attributes'] ) && is_array( $args['row_attributes'] ) ) { + $attributes = $args['row_attributes']; + } + + $row_class = isset( $args['row_class'] ) ? trim( (string) $args['row_class'] ) : ''; + if ( '' !== $row_class ) { + if ( isset( $attributes['class'] ) ) { + $attributes['class'] .= ' ' . $row_class; + } else { + $attributes['class'] = $row_class; + } + } + + return array_filter( + $attributes, + function ( $value ) { + return '' !== $value || 0 === $value || '0' === $value; + } + ); + } + + private function build_attr_string( $attributes ) { + $buffer = []; + foreach ( $attributes as $key => $value ) { + if ( '' === $value && 0 !== $value && '0' !== $value ) { + continue; + } + + $buffer[] = sprintf( '%s="%s"', esc_attr( $key ), esc_attr( $value ) ); + } + + return implode( ' ', $buffer ); + } + + private function build_field_name( $key ) { + $segments = $this->split_key( $key ); + $name = $this->option_key; + + foreach ( $segments as $segment ) { + $name .= '[' . $segment . ']'; + } + + return $name; + } + + private function build_field_id( $key ) { + $segments = $this->split_key( $key ); + + return 'groq-ai-' . implode( '-', $segments ); + } + + private function get_value( $key, $default = '' ) { + $segments = $this->split_key( $key ); + $value = $this->values; + + foreach ( $segments as $segment ) { + if ( is_array( $value ) && array_key_exists( $segment, $value ) ) { + $value = $value[ $segment ]; + } else { + return $default; + } + } + + return $value; + } + + private function split_key( $key ) { + if ( is_array( $key ) ) { + return $key; + } + + $key = trim( (string) $key ); + if ( '' === $key ) { + return []; + } + + return array_map( 'sanitize_key', explode( '.', $key ) ); + } +} diff --git a/includes/Admin/class-groq-ai-term-admin-base.php b/includes/Admin/class-groq-ai-term-admin-base.php new file mode 100644 index 0000000..c1554b4 --- /dev/null +++ b/includes/Admin/class-groq-ai-term-admin-base.php @@ -0,0 +1,520 @@ +ensure_term_handler_registered(); + $this->ensure_term_assets_hook(); + } + + protected function register_term_page() { + if ( self::$term_page_registered ) { + return; + } + + add_submenu_page( + 'options-general.php', + __( 'Siti AI Term tekst', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + __( 'Siti AI Term tekst', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'manage_options', + 'groq-ai-product-text-term', + [ $this, 'render_term_generator_page' ] + ); + + self::$term_page_registered = true; + } + + protected function render_term_bulk_panel( $label_plural, $empty_count ) { + $label_plural = (string) $label_plural; + ?> +
    +

    + 0 ) { + printf( + /* translators: 1: amount, 2: label plural (e.g. categorieën) */ + esc_html__( 'Er zijn %1$d %2$s zonder omschrijving. Klik op de knop hieronder om automatisch teksten te genereren.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + (int) $empty_count, + esc_html( $label_plural ) + ); + } else { + printf( + esc_html__( 'Alle %s hebben al een omschrijving.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + esc_html( $label_plural ) + ); + } + ?> +

    +

    + + + +

    +
    +
      +
      + get_term_overview_data( $taxonomy ); + $rows = isset( $overview['rows'] ) ? $overview['rows'] : []; + + $terms = []; + foreach ( $rows as $row ) { + $terms[] = [ + 'id' => isset( $row['id'] ) ? (int) $row['id'] : 0, + 'name' => isset( $row['name'] ) ? (string) $row['name'] : '', + 'slug' => isset( $row['slug'] ) ? (string) $row['slug'] : '', + 'count' => isset( $row['count'] ) ? (int) $row['count'] : 0, + 'words' => isset( $row['words'] ) ? (int) $row['words'] : 0, + 'hasDescription' => ! empty( $row['has_description'] ), + ]; + } + + $defaults = [ + 'ajaxUrl' => admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'groq_ai_bulk_generate_terms' ), + 'taxonomy' => $taxonomy, + 'terms' => $terms, + 'allowRegenerate' => false, + 'strings' => [], + ]; + + $config = wp_parse_args( $overrides, $defaults ); + + wp_localize_script( 'groq-ai-term-bulk', 'GroqAITermBulk', $config ); + } + + protected function get_term_overview_data( $taxonomy ) { + $taxonomy = sanitize_key( (string) $taxonomy ); + + if ( isset( $this->term_overview_cache[ $taxonomy ] ) ) { + return $this->term_overview_cache[ $taxonomy ]; + } + + $rows = []; + $empty_rows = []; + + if ( '' !== $taxonomy && taxonomy_exists( $taxonomy ) ) { + $terms = get_terms( + [ + 'taxonomy' => $taxonomy, + 'hide_empty' => false, + 'orderby' => 'name', + 'order' => 'ASC', + 'number' => 0, + ] + ); + + if ( is_wp_error( $terms ) ) { + $terms = []; + } + + foreach ( $terms as $term ) { + if ( ! $term || ! is_object( $term ) || empty( $term->term_id ) ) { + continue; + } + + $words = $this->count_words( isset( $term->description ) ? $term->description : '' ); + $has_description = $words > 0; + + $row = [ + 'id' => absint( $term->term_id ), + 'name' => (string) $term->name, + 'slug' => (string) $term->slug, + 'count' => isset( $term->count ) ? absint( $term->count ) : 0, + 'words' => $words, + 'has_description' => $has_description, + 'url' => $this->get_term_page_url( $taxonomy, $term->term_id ), + ]; + + $rows[] = $row; + if ( ! $has_description ) { + $empty_rows[] = $row; + } + } + } + + $data = [ + 'rows' => $rows, + 'empty_rows' => $empty_rows, + 'empty_count' => count( $empty_rows ), + ]; + + $this->term_overview_cache[ $taxonomy ] = $data; + + return $data; + } + + private function count_words( $text ) { + $text = wp_strip_all_tags( (string) $text ); + $text = trim( preg_replace( '/\s+/u', ' ', $text ) ); + if ( '' === $text ) { + return 0; + } + if ( preg_match_all( '/\pL[\pL\pN\']*/u', $text, $matches ) ) { + return count( $matches[0] ); + } + return 0; + } + + protected function get_term_page_url( $taxonomy, $term_id ) { + return add_query_arg( + [ + 'page' => 'groq-ai-product-text-term', + 'taxonomy' => sanitize_key( (string) $taxonomy ), + 'term_id' => absint( $term_id ), + ], + admin_url( 'options-general.php' ) + ); + } + + public function enqueue_term_assets( $hook ) { + if ( 0 !== strpos( (string) $hook, 'settings_page_groq-ai-product-text-term' ) ) { + return; + } + + $this->enqueue_admin_styles(); + + wp_enqueue_script( + 'groq-ai-term-admin', + plugins_url( 'assets/js/term-admin.js', GROQ_AI_PRODUCT_TEXT_FILE ), + [], + GROQ_AI_PRODUCT_TEXT_VERSION, + true + ); + + $taxonomy = isset( $_GET['taxonomy'] ) ? sanitize_key( wp_unslash( $_GET['taxonomy'] ) ) : ''; + $term_id = isset( $_GET['term_id'] ) ? absint( $_GET['term_id'] ) : 0; + + wp_localize_script( + 'groq-ai-term-admin', + 'GroqAITermGenerator', + [ + 'ajaxUrl' => admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'groq_ai_generate_term' ), + 'taxonomy' => $taxonomy, + 'termId' => $term_id, + ] + ); + } + + public function render_term_generator_page() { + if ( ! $this->current_user_can_manage() ) { + return; + } + + $taxonomy = isset( $_GET['taxonomy'] ) ? sanitize_key( wp_unslash( $_GET['taxonomy'] ) ) : ''; + $term_id = isset( $_GET['term_id'] ) ? absint( $_GET['term_id'] ) : 0; + + if ( '' === $taxonomy || ! taxonomy_exists( $taxonomy ) || ! $term_id ) { + ?> +
      +

      +

      +
      + +
      +

      +

      +
      + count_words( $term->description ); + $meta_prompt = get_term_meta( $term_id, 'groq_ai_term_custom_prompt', true ); + $settings = $this->plugin->get_settings(); + $bottom_meta_key = $this->resolve_term_bottom_description_meta_key( $term, $settings ); + $effective_bottom_meta_key = '' !== $bottom_meta_key ? $bottom_meta_key : 'groq_ai_term_bottom_description'; + $bottom_description = (string) get_term_meta( $term_id, $effective_bottom_meta_key, true ); + $rankmath_module_enabled = $this->plugin->is_module_enabled( 'rankmath', $settings ); + $rankmath_active = $this->plugin->is_rankmath_active(); + $rankmath_title = ''; + $rankmath_description = ''; + $rankmath_focus_keywords = ''; + if ( $rankmath_module_enabled ) { + $rankmath_keys = $this->resolve_rankmath_term_meta_keys( $term, $settings ); + $rankmath_title = (string) get_term_meta( $term_id, $rankmath_keys['title'], true ); + $rankmath_description = (string) get_term_meta( $term_id, $rankmath_keys['description'], true ); + $rankmath_focus_keywords = (string) get_term_meta( $term_id, $rankmath_keys['focus_keyword'], true ); + } + $default_prompt = $this->get_term_prompt_text( $term, $meta_prompt ); + ?> +
      +

      + : name ); ?> +

      + + +
      +

      +
      + + + + + + + + + + + +
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      +
      +

      +

      + +

      + + +

      +
      +

      + +

      + + +

      + +

      + +

      + + +

      +
      
      +			
      +
      + 403 ] ); + } + + check_admin_referer( 'groq_ai_save_term_content' ); + + $taxonomy = isset( $_POST['taxonomy'] ) ? sanitize_key( wp_unslash( $_POST['taxonomy'] ) ) : ''; + $term_id = isset( $_POST['term_id'] ) ? absint( $_POST['term_id'] ) : 0; + + if ( '' === $taxonomy || ! taxonomy_exists( $taxonomy ) || ! $term_id ) { + $this->redirect_with_term_notice( $taxonomy, $term_id, 'error', __( 'Ongeldige term.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), 'error' ); + } + + $term = get_term( $term_id, $taxonomy ); + if ( ! $term || is_wp_error( $term ) ) { + $this->redirect_with_term_notice( $taxonomy, $term_id, 'error', __( 'Term niet gevonden.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), 'error' ); + } + + $description = isset( $_POST['description'] ) ? wp_kses_post( wp_unslash( $_POST['description'] ) ) : ''; + $bottom_description = isset( $_POST['groq_ai_term_bottom_description'] ) ? wp_kses_post( wp_unslash( $_POST['groq_ai_term_bottom_description'] ) ) : ''; + $custom_prompt = isset( $_POST['groq_ai_term_custom_prompt'] ) ? sanitize_textarea_field( wp_unslash( $_POST['groq_ai_term_custom_prompt'] ) ) : ''; + + $update = wp_update_term( + $term_id, + $taxonomy, + [ + 'description' => $description, + ] + ); + + if ( is_wp_error( $update ) ) { + $this->redirect_with_term_notice( $taxonomy, $term_id, 'error', $update->get_error_message(), 'error' ); + } + + $settings = $this->plugin->get_settings(); + $bottom_meta_key = $this->resolve_term_bottom_description_meta_key( $term, $settings ); + $bottom_meta_key = '' !== $bottom_meta_key ? $bottom_meta_key : 'groq_ai_term_bottom_description'; + update_term_meta( $term_id, $bottom_meta_key, $bottom_description ); + + if ( '' === trim( $custom_prompt ) ) { + delete_term_meta( $term_id, 'groq_ai_term_custom_prompt' ); + } else { + update_term_meta( $term_id, 'groq_ai_term_custom_prompt', $custom_prompt ); + } + + if ( isset( $_POST['groq_ai_term_bottom_description'] ) && $bottom_meta_key !== 'groq_ai_term_bottom_description' ) { + update_term_meta( $term_id, 'groq_ai_term_bottom_description', $bottom_description ); + } + + if ( $this->plugin->is_module_enabled( 'rankmath', $settings ) ) { + $rankmath_keys = $this->resolve_rankmath_term_meta_keys( $term, $settings ); + $rankmath_title = isset( $_POST['groq_ai_rankmath_meta_title'] ) ? sanitize_text_field( wp_unslash( $_POST['groq_ai_rankmath_meta_title'] ) ) : ''; + $rankmath_description = isset( $_POST['groq_ai_rankmath_meta_description'] ) ? sanitize_textarea_field( wp_unslash( $_POST['groq_ai_rankmath_meta_description'] ) ) : ''; + $rankmath_keywords = isset( $_POST['groq_ai_rankmath_focus_keywords'] ) ? sanitize_text_field( wp_unslash( $_POST['groq_ai_rankmath_focus_keywords'] ) ) : ''; + + update_term_meta( $term_id, $rankmath_keys['title'], $rankmath_title ); + update_term_meta( $term_id, $rankmath_keys['description'], $rankmath_description ); + update_term_meta( $term_id, $rankmath_keys['focus_keyword'], $rankmath_keywords ); + } + + $this->redirect_with_term_notice( $taxonomy, $term_id, 'saved', __( 'Term opgeslagen.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ); + } + + private function resolve_term_bottom_description_meta_key( $term, $settings ) { + $default_key = ''; + if ( is_array( $settings ) && isset( $settings['term_bottom_description_meta_key'] ) ) { + $default_key = sanitize_key( (string) $settings['term_bottom_description_meta_key'] ); + } + + $key = apply_filters( 'groq_ai_term_bottom_description_meta_key', $default_key, $term, $settings ); + $key = sanitize_key( (string) $key ); + return $key; + } + + private function resolve_rankmath_term_meta_keys( $term, $settings ) { + $keys = [ + 'title' => 'rank_math_title', + 'description' => 'rank_math_description', + 'focus_keyword' => 'rank_math_focus_keyword', + ]; + $keys = apply_filters( 'groq_ai_rankmath_term_meta_keys', $keys, $term, $settings ); + if ( ! is_array( $keys ) ) { + $keys = []; + } + + return [ + 'title' => isset( $keys['title'] ) ? sanitize_key( (string) $keys['title'] ) : 'rank_math_title', + 'description' => isset( $keys['description'] ) ? sanitize_key( (string) $keys['description'] ) : 'rank_math_description', + 'focus_keyword' => isset( $keys['focus_keyword'] ) ? sanitize_key( (string) $keys['focus_keyword'] ) : 'rank_math_focus_keyword', + ]; + } + + private function get_term_prompt_text( $term, $custom_prompt = null ) { + $prompt = ( null !== $custom_prompt ) ? $custom_prompt : ''; + + if ( null === $custom_prompt && $term && isset( $term->term_id ) ) { + $prompt = get_term_meta( $term->term_id, 'groq_ai_term_custom_prompt', true ); + } + + $prompt = trim( (string) $prompt ); + if ( '' !== $prompt ) { + return $prompt; + } + + $default_prompt = __( 'Schrijf een SEO-vriendelijke categorieomschrijving in het Nederlands. Gebruik duidelijke tussenkoppen en

      -tags. Voeg geen prijsinformatie toe.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); + + return apply_filters( 'groq_ai_default_term_prompt', $default_prompt, $term ); + } + + private function redirect_with_term_notice( $taxonomy, $term_id, $type, $message = '', $status = 'success' ) { + $url = ( $taxonomy && $term_id ) ? $this->get_term_page_url( $taxonomy, $term_id ) : $this->get_page_url( 'groq-ai-product-text-categories' ); + + $args = [ + 'groq_ai_term_notice' => sanitize_key( (string) $type ), + 'groq_ai_term_status' => sanitize_key( (string) $status ), + ]; + + if ( '' !== $message ) { + $args['groq_ai_term_notice_message'] = rawurlencode( (string) $message ); + } + + wp_safe_redirect( add_query_arg( $args, $url ) ); + exit; + } + + private function ensure_term_handler_registered() { + if ( self::$term_handler_registered ) { + return; + } + + add_action( 'admin_post_groq_ai_save_term_content', [ $this, 'handle_save_term_content' ] ); + self::$term_handler_registered = true; + } + + private function ensure_term_assets_hook() { + if ( self::$term_assets_hook_registered ) { + return; + } + + add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_term_assets' ] ); + self::$term_assets_hook_registered = true; + } +}