diff --git a/assets/js/term-admin.js b/assets/js/term-admin.js new file mode 100644 index 0000000..89efeac --- /dev/null +++ b/assets/js/term-admin.js @@ -0,0 +1,102 @@ +(function () { + if (!window.GroqAITermGenerator) { + return; + } + + const form = document.getElementById('groq-ai-term-form'); + if (!form) { + return; + } + + const promptField = document.getElementById('groq-ai-term-prompt'); + const outputField = document.getElementById('groq-ai-term-generated'); + const rawField = document.getElementById('groq-ai-term-raw'); + const statusField = document.getElementById('groq-ai-term-status'); + const applyButton = document.getElementById('groq-ai-term-apply'); + const includeTopProducts = document.getElementById('groq-ai-term-include-top-products'); + const topProductsLimit = document.getElementById('groq-ai-term-top-products-limit'); + + function setStatus(message, type) { + if (!statusField) { + return; + } + statusField.textContent = message || ''; + statusField.setAttribute('data-status', type || ''); + } + + function setLoading(isLoading) { + form.classList.toggle('is-loading', !!isLoading); + const buttons = form.querySelectorAll('button, input[type="submit"]'); + buttons.forEach((btn) => { + btn.disabled = !!isLoading; + }); + } + + function buildPayload(prompt) { + const payload = new URLSearchParams(); + payload.append('action', 'groq_ai_generate_term_text'); + payload.append('nonce', GroqAITermGenerator.nonce); + payload.append('taxonomy', GroqAITermGenerator.taxonomy); + payload.append('term_id', GroqAITermGenerator.termId); + payload.append('prompt', prompt); + payload.append('include_top_products', includeTopProducts && includeTopProducts.checked ? '1' : '0'); + payload.append('top_products_limit', topProductsLimit ? String(topProductsLimit.value || '') : '10'); + return payload; + } + + if (applyButton) { + applyButton.addEventListener('click', () => { + const descriptionField = document.getElementById('description'); + if (!descriptionField || !outputField) { + return; + } + descriptionField.value = outputField.value || ''; + setStatus('Tekst ingevuld in het beschrijving-veld. Vergeet niet op "Opslaan" te klikken.', 'success'); + }); + } + + form.addEventListener('submit', (event) => { + event.preventDefault(); + const prompt = promptField ? (promptField.value || '').trim() : ''; + if (!prompt) { + setStatus('Vul eerst een prompt in.', 'error'); + return; + } + + setLoading(true); + setStatus('AI is bezig met schrijven...', 'loading'); + if (rawField) { + rawField.textContent = ''; + } + + fetch(GroqAITermGenerator.ajaxUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + }, + body: buildPayload(prompt).toString(), + }) + .then((response) => response.json()) + .then((json) => { + if (!json.success) { + const errorMessage = json.data && json.data.message ? json.data.message : 'Onbekende fout'; + throw new Error(errorMessage); + } + + if (outputField) { + outputField.value = (json.data && json.data.description ? json.data.description : '').trim(); + } + if (rawField) { + rawField.textContent = (json.data && json.data.raw ? String(json.data.raw) : '').trim(); + } + + setStatus('Tekst gegenereerd. Je kunt hem toepassen en opslaan.', 'success'); + }) + .catch((error) => { + setStatus(error && error.message ? error.message : 'Er ging iets mis bij het genereren.', 'error'); + }) + .finally(() => { + setLoading(false); + }); + }); +})(); diff --git a/groq-ai-product-text.php b/groq-ai-product-text.php index 8332188..e139ced 100644 --- a/groq-ai-product-text.php +++ b/groq-ai-product-text.php @@ -2,7 +2,7 @@ /** * Plugin Name: SitiAI Product Teksten * Description: Genereer productteksten met diverse AI-aanbieders rechtstreeks vanuit WooCommerce. - * Version: 1.3.0 + * Version: 1.4.0 * Author: SitiAI * Text Domain: siti-ai-product-content-generator * Domain Path: /languages @@ -54,6 +54,10 @@ require_once __DIR__ . '/includes/Services/Settings/class-groq-ai-settings-manag require_once __DIR__ . '/includes/Services/Prompt/class-groq-ai-prompt-builder.php'; require_once __DIR__ . '/includes/Services/Conversations/class-groq-ai-conversation-manager.php'; require_once __DIR__ . '/includes/Services/Logging/class-groq-ai-generation-logger.php'; +require_once __DIR__ . '/includes/Services/Google/class-groq-ai-google-oauth-client.php'; +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-settings-page.php'; require_once __DIR__ . '/includes/Admin/class-groq-ai-logs-table.php'; require_once __DIR__ . '/includes/Admin/class-groq-ai-product-ui.php'; @@ -108,6 +112,7 @@ final class Groq_AI_Product_Text_Plugin { add_action( 'init', [ $this, 'load_textdomain' ] ); add_action( 'plugins_loaded', [ $this, 'maybe_create_logs_table' ] ); add_action( 'load-plugins.php', [ $this, 'maybe_deactivate_if_woocommerce_missing' ] ); + add_filter( 'groq_ai_term_google_context', [ $this, 'inject_google_term_context' ], 10, 3 ); } public function load_textdomain() { @@ -179,10 +184,47 @@ final class Groq_AI_Product_Text_Plugin { } ); + $this->container->set( + 'google_oauth_client', + function () { + return new Groq_AI_Google_OAuth_Client(); + } + ); + + $this->container->set( + 'gsc_client', + function ( Groq_AI_Service_Container $container ) { + return new Groq_AI_Google_Search_Console_Client( $container->get( 'google_oauth_client' ) ); + } + ); + + $this->container->set( + 'ga_client', + function ( Groq_AI_Service_Container $container ) { + return new Groq_AI_Google_Analytics_Data_Client( $container->get( 'google_oauth_client' ) ); + } + ); + + $this->container->set( + 'google_context_builder', + function ( Groq_AI_Service_Container $container ) { + return new Groq_AI_Google_Context_Builder( $container->get( 'gsc_client' ), $container->get( 'ga_client' ) ); + } + ); + // Instantiate controller immediately so hooks are registered. $this->container->get( 'ajax_controller' ); } + public function inject_google_term_context( $existing, $term, $settings ) { + $builder = $this->container->get( 'google_context_builder' ); + if ( ! $builder ) { + return (string) $existing; + } + + return $builder->build_term_google_context( $existing, $term, $settings ); + } + public function get_option_key() { return self::OPTION_KEY; } diff --git a/includes/Admin/class-groq-ai-settings-page.php b/includes/Admin/class-groq-ai-settings-page.php index ff3ef15..f0fc26b 100644 --- a/includes/Admin/class-groq-ai-settings-page.php +++ b/includes/Admin/class-groq-ai-settings-page.php @@ -3,6 +3,7 @@ class Groq_AI_Product_Text_Settings_Page { private $plugin; private $provider_manager; + private $brand_taxonomy = null; public function __construct( $plugin, Groq_AI_Provider_Manager $provider_manager ) { $this->plugin = $plugin; @@ -12,6 +13,11 @@ class Groq_AI_Product_Text_Settings_Page { add_action( 'admin_init', [ $this, 'register_settings' ] ); add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_settings_assets' ] ); add_action( 'admin_head', [ $this, 'hide_menu_links' ] ); + 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' ] ); } public function register_settings_pages() { @@ -23,6 +29,33 @@ 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 ), @@ -60,13 +93,338 @@ 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' ) + ); + } + + public function render_categories_overview_page() { + if ( ! current_user_can( 'manage_options' ) ) { + return; + } + + $terms = get_terms( + [ + 'taxonomy' => 'product_cat', + 'hide_empty' => false, + 'number' => 0, + ] + ); + if ( is_wp_error( $terms ) ) { + $terms = []; + } + ?> +
+

+

+ + + + + + + + + + + + + + + get_term_page_url( 'product_cat', $term->term_id ); + $words = $this->count_words( $term->description ); + $count = isset( $term->count ) ? absint( $term->count ) : 0; + ?> + + + + + + + + + +
+ name ); ?> + slug ); ?>
+
+ detect_brand_taxonomy(); + if ( '' === $taxonomy ) { + ?> +
+

+

+
+ $taxonomy, + 'hide_empty' => false, + 'number' => 0, + ] + ); + if ( is_wp_error( $terms ) ) { + $terms = []; + } + ?> +
+

+

+ +

+ + + + + + + + + + + + + + + get_term_page_url( $taxonomy, $term->term_id ); + $words = $this->count_words( $term->description ); + $count = isset( $term->count ) ? absint( $term->count ) : 0; + ?> + + + + + + + + + +
+ name ); ?> + slug ); ?>
+
+ +
+

+

+
+ +
+

+

+
+ count_words( $term->description ); + $meta_prompt = get_term_meta( $term_id, 'groq_ai_term_custom_prompt', true ); + $default_prompt = (string) $meta_prompt; + if ( '' === trim( $default_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 ); + } + ?> +

+

+ : name ); ?> +

+

+ +

+ +

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

+
+

+

+ +   + +

+ +

+ + +

+
+

+ +

+

+			
+
+ get_settings_page_url() ); + exit; + } + + $result = wp_update_term( + $term_id, + $taxonomy, + [ + 'description' => $description, + ] + ); + + if ( ! is_wp_error( $result ) ) { + update_term_meta( $term_id, 'groq_ai_term_custom_prompt', $custom_prompt ); + } + + wp_safe_redirect( $this->get_term_page_url( $taxonomy, $term_id ) ); + exit; + } + public function register_settings() { register_setting( 'groq_ai_product_text_group', $this->plugin->get_option_key(), [ $this->plugin, 'sanitize_settings' ] ); @@ -77,6 +435,13 @@ class Groq_AI_Product_Text_Settings_Page { 'groq-ai-product-text' ); + add_settings_section( + 'groq_ai_product_text_google', + __( 'Google koppeling (OAuth)', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + '__return_false', + 'groq-ai-product-text' + ); + add_settings_field( 'groq_ai_provider', __( 'AI-aanbieder', GROQ_AI_PRODUCT_TEXT_DOMAIN ), @@ -93,6 +458,54 @@ class Groq_AI_Product_Text_Settings_Page { 'groq_ai_product_text_general' ); + add_settings_field( + 'groq_ai_google_oauth_client_id', + __( 'Google Client ID', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + [ $this, 'render_google_oauth_client_id_field' ], + 'groq-ai-product-text', + 'groq_ai_product_text_google' + ); + + add_settings_field( + 'groq_ai_google_oauth_client_secret', + __( 'Google Client secret', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + [ $this, 'render_google_oauth_client_secret_field' ], + 'groq-ai-product-text', + 'groq_ai_product_text_google' + ); + + add_settings_field( + 'groq_ai_google_oauth_status', + __( 'Google status', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + [ $this, 'render_google_oauth_status_field' ], + 'groq-ai-product-text', + 'groq_ai_product_text_google' + ); + + add_settings_field( + 'groq_ai_google_gsc_site_url', + __( 'Search Console site URL', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + [ $this, 'render_google_gsc_site_url_field' ], + 'groq-ai-product-text', + 'groq_ai_product_text_google' + ); + + add_settings_field( + 'groq_ai_google_ga4_property_id', + __( 'GA4 property ID', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + [ $this, 'render_google_ga4_property_id_field' ], + 'groq-ai-product-text', + 'groq_ai_product_text_google' + ); + + add_settings_field( + 'groq_ai_google_context_toggles', + __( 'Google data gebruiken', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + [ $this, 'render_google_context_toggles_field' ], + 'groq-ai-product-text', + 'groq_ai_product_text_google' + ); + foreach ( $this->provider_manager->get_providers() as $provider ) { add_settings_field( 'groq_ai_api_key_' . $provider->get_key(), @@ -223,6 +636,7 @@ class Groq_AI_Product_Text_Settings_Page { ?>

+ render_google_oauth_admin_notice(); ?>

@@ -246,6 +660,521 @@ class Groq_AI_Product_Text_Settings_Page { +

+

+
+ plugin->get_settings(); + $value = isset( $settings['google_oauth_client_id'] ) ? $settings['google_oauth_client_id'] : ''; + ?> + +

+ +

+ plugin->get_settings(); + $value = isset( $settings['google_oauth_client_secret'] ) ? $settings['google_oauth_client_secret'] : ''; + ?> + +

+ +

+ plugin->get_settings(); + $connected = ! empty( $settings['google_oauth_refresh_token'] ); + $email = isset( $settings['google_oauth_connected_email'] ) ? $settings['google_oauth_connected_email'] : ''; + $connected_at = isset( $settings['google_oauth_connected_at'] ) ? absint( $settings['google_oauth_connected_at'] ) : 0; + $redirect_uri = $this->get_google_oauth_redirect_uri(); + + $start_url = wp_nonce_url( + admin_url( 'admin-post.php?action=groq_ai_google_oauth_start' ), + 'groq_ai_google_oauth_start', + '_wpnonce' + ); + + $disconnect_url = wp_nonce_url( + admin_url( 'admin-post.php?action=groq_ai_google_oauth_disconnect' ), + 'groq_ai_google_oauth_disconnect', + '_wpnonce' + ); + ?> +

+ +

+

+ +
+ +

+ +

+ + + — + + + () + +

+

+ + + + + + +

+ +

+

+ + + +

+ +

+ +

+

+ + + + + + +

+ plugin->get_settings(); + $messages = []; + $status = 'success'; + + $oauth = new Groq_AI_Google_OAuth_Client(); + $token = $oauth->get_access_token( $settings ); + if ( is_wp_error( $token ) ) { + $status = 'error'; + $messages[] = sprintf( __( 'OAuth: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $token->get_error_message() ); + } else { + $messages[] = __( 'OAuth: OK (access token opgehaald).', GROQ_AI_PRODUCT_TEXT_DOMAIN ); + $info = $oauth->get_access_token_info( $token ); + if ( is_array( $info ) ) { + $scope = isset( $info['scope'] ) ? trim( (string) $info['scope'] ) : ''; + if ( '' !== $scope ) { + $messages[] = sprintf( __( 'OAuth scopes: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $scope ); + if ( false === strpos( $scope, 'https://www.googleapis.com/auth/webmasters' ) ) { + $messages[] = __( 'Tip: je access token mist Search Console scope. Klik op "Opnieuw verbinden" zodat je toestemming opnieuw wordt gevraagd.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); + } + } + } + } + + $range_days = 7; + $end_date = gmdate( 'Y-m-d' ); + $start_date = gmdate( 'Y-m-d', time() - ( $range_days * DAY_IN_SECONDS ) ); + + if ( 'error' !== $status && ! empty( $settings['google_enable_gsc'] ) ) { + $gsc = new Groq_AI_Google_Search_Console_Client( $oauth ); + $sites = $gsc->list_sites( $settings ); + if ( is_wp_error( $sites ) ) { + $status = 'error'; + $messages[] = sprintf( __( 'Search Console: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $sites->get_error_message() ); + } else { + $count = is_array( $sites ) ? count( $sites ) : 0; + $messages[] = sprintf( __( 'Search Console: OK (%d properties zichtbaar).', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $count ); + $site_url = isset( $settings['google_gsc_site_url'] ) ? trim( (string) $settings['google_gsc_site_url'] ) : ''; + if ( '' !== $site_url && is_array( $sites ) && ! in_array( $site_url, $sites, true ) ) { + $messages[] = __( 'Let op: de ingestelde site URL is niet gevonden in jouw zichtbare GSC properties.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); + } + } + } + + if ( 'error' !== $status && ! empty( $settings['google_enable_ga'] ) ) { + $property_id = isset( $settings['google_ga4_property_id'] ) ? trim( (string) $settings['google_ga4_property_id'] ) : ''; + if ( '' !== $property_id ) { + $ga = new Groq_AI_Google_Analytics_Data_Client( $oauth ); + $stats = $ga->get_property_sessions_summary( $settings, $property_id, $start_date, $end_date ); + if ( is_wp_error( $stats ) ) { + $status = 'error'; + $messages[] = sprintf( __( 'Analytics: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $stats->get_error_message() ); + } else { + $sessions = isset( $stats['sessions'] ) ? absint( $stats['sessions'] ) : 0; + $messages[] = sprintf( __( 'Analytics: OK (sessies laatste %1$d dagen: ~%2$d).', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $range_days, $sessions ); + } + } else { + $messages[] = __( 'Analytics: overgeslagen (GA4 property ID niet ingevuld).', GROQ_AI_PRODUCT_TEXT_DOMAIN ); + } + } + + $url = add_query_arg( + [ + 'groq_ai_google_oauth' => $status, + 'groq_ai_google_oauth_message' => implode( ' ', $messages ), + ], + $this->get_settings_page_url() + ); + wp_safe_redirect( $url ); + exit; + } + + public function render_google_gsc_site_url_field() { + $settings = $this->plugin->get_settings(); + $value = isset( $settings['google_gsc_site_url'] ) ? (string) $settings['google_gsc_site_url'] : ''; + ?> + +

+ +

+ plugin->get_settings(); + $value = isset( $settings['google_ga4_property_id'] ) ? (string) $settings['google_ga4_property_id'] : ''; + ?> + +

+ +

+ plugin->get_settings(); + $gsc = ! empty( $settings['google_enable_gsc'] ); + $ga = ! empty( $settings['google_enable_ga'] ); + ?> + + + plugin->get_settings(); + $client_id = isset( $settings['google_oauth_client_id'] ) ? trim( (string) $settings['google_oauth_client_id'] ) : ''; + $client_secret = isset( $settings['google_oauth_client_secret'] ) ? trim( (string) $settings['google_oauth_client_secret'] ) : ''; + + if ( '' === $client_id || '' === $client_secret ) { + $url = add_query_arg( + [ + 'groq_ai_google_oauth' => 'error', + 'groq_ai_google_oauth_message' => __( 'Vul eerst Google Client ID en Client secret in.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + ], + $this->get_settings_page_url() + ); + wp_safe_redirect( $url ); + exit; + } + + $state = wp_generate_password( 32, false, false ); + set_transient( $this->get_google_oauth_state_key(), $state, 10 * MINUTE_IN_SECONDS ); + + $scope = implode( ' ', $this->get_google_oauth_scopes() ); + $redirect_uri = $this->get_google_oauth_redirect_uri(); + + $auth_url = add_query_arg( + [ + 'client_id' => $client_id, + 'redirect_uri' => $redirect_uri, + 'response_type' => 'code', + 'access_type' => 'offline', + 'prompt' => 'consent', + 'include_granted_scopes' => 'true', + 'scope' => $scope, + 'state' => $state, + ], + 'https://accounts.google.com/o/oauth2/v2/auth' + ); + $auth_url = esc_url_raw( $auth_url ); + $parsed = wp_parse_url( $auth_url ); + $host = isset( $parsed['host'] ) ? strtolower( (string) $parsed['host'] ) : ''; + if ( 'accounts.google.com' !== $host ) { + $url = add_query_arg( + [ + 'groq_ai_google_oauth' => 'error', + 'groq_ai_google_oauth_message' => __( 'OAuth URL ongeldig. Controleer plugin instellingen.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + ], + $this->get_settings_page_url() + ); + wp_safe_redirect( $url ); + exit; + } + + // Let op: wp_safe_redirect staat standaard geen externe hosts toe en valt dan terug naar /wp-admin. + wp_redirect( $auth_url ); + exit; + } + + public function handle_google_oauth_callback() { + if ( ! current_user_can( 'manage_options' ) ) { + wp_die( esc_html__( 'Geen toestemming.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ); + } + + $expected_state = get_transient( $this->get_google_oauth_state_key() ); + delete_transient( $this->get_google_oauth_state_key() ); + + $state = isset( $_GET['state'] ) ? sanitize_text_field( wp_unslash( $_GET['state'] ) ) : ''; + $code = isset( $_GET['code'] ) ? sanitize_text_field( wp_unslash( $_GET['code'] ) ) : ''; + $error = isset( $_GET['error'] ) ? sanitize_text_field( wp_unslash( $_GET['error'] ) ) : ''; + + if ( '' !== $error ) { + $url = add_query_arg( + [ + 'groq_ai_google_oauth' => 'error', + 'groq_ai_google_oauth_message' => sprintf( __( 'Google OAuth error: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $error ), + ], + $this->get_settings_page_url() + ); + wp_safe_redirect( $url ); + exit; + } + + if ( empty( $expected_state ) || '' === $state || $state !== $expected_state ) { + $url = add_query_arg( + [ + 'groq_ai_google_oauth' => 'error', + 'groq_ai_google_oauth_message' => __( 'Ongeldige OAuth state. Probeer opnieuw te verbinden.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + ], + $this->get_settings_page_url() + ); + wp_safe_redirect( $url ); + exit; + } + + if ( '' === $code ) { + $url = add_query_arg( + [ + 'groq_ai_google_oauth' => 'error', + 'groq_ai_google_oauth_message' => __( 'Geen OAuth code ontvangen.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + ], + $this->get_settings_page_url() + ); + wp_safe_redirect( $url ); + exit; + } + + $settings = $this->plugin->get_settings(); + $client_id = isset( $settings['google_oauth_client_id'] ) ? trim( (string) $settings['google_oauth_client_id'] ) : ''; + $client_secret = isset( $settings['google_oauth_client_secret'] ) ? trim( (string) $settings['google_oauth_client_secret'] ) : ''; + $redirect_uri = $this->get_google_oauth_redirect_uri(); + + if ( '' === $client_id || '' === $client_secret ) { + $url = add_query_arg( + [ + 'groq_ai_google_oauth' => 'error', + 'groq_ai_google_oauth_message' => __( 'Client ID/secret ontbreken. Sla eerst de instellingen op.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + ], + $this->get_settings_page_url() + ); + wp_safe_redirect( $url ); + exit; + } + + $token_response = wp_remote_post( + 'https://oauth2.googleapis.com/token', + [ + 'timeout' => 20, + 'headers' => [ + 'Content-Type' => 'application/x-www-form-urlencoded', + ], + 'body' => [ + 'code' => $code, + 'client_id' => $client_id, + 'client_secret' => $client_secret, + 'redirect_uri' => $redirect_uri, + 'grant_type' => 'authorization_code', + ], + ] + ); + + if ( is_wp_error( $token_response ) ) { + $url = add_query_arg( + [ + 'groq_ai_google_oauth' => 'error', + 'groq_ai_google_oauth_message' => $token_response->get_error_message(), + ], + $this->get_settings_page_url() + ); + wp_safe_redirect( $url ); + exit; + } + + $status_code = wp_remote_retrieve_response_code( $token_response ); + $body = wp_remote_retrieve_body( $token_response ); + $data = json_decode( (string) $body, true ); + + if ( 200 !== $status_code || ! is_array( $data ) ) { + $url = add_query_arg( + [ + 'groq_ai_google_oauth' => 'error', + 'groq_ai_google_oauth_message' => __( 'Token exchange mislukt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + ], + $this->get_settings_page_url() + ); + wp_safe_redirect( $url ); + exit; + } + + $access_token = isset( $data['access_token'] ) ? sanitize_text_field( (string) $data['access_token'] ) : ''; + $refresh_token = isset( $data['refresh_token'] ) ? sanitize_text_field( (string) $data['refresh_token'] ) : ''; + + if ( '' === $refresh_token ) { + $refresh_token = isset( $settings['google_oauth_refresh_token'] ) ? sanitize_text_field( (string) $settings['google_oauth_refresh_token'] ) : ''; + } + + $connected_email = ''; + if ( '' !== $access_token ) { + $userinfo_response = wp_remote_get( + 'https://openidconnect.googleapis.com/v1/userinfo', + [ + 'timeout' => 20, + 'headers' => [ + 'Authorization' => 'Bearer ' . $access_token, + ], + ] + ); + if ( ! is_wp_error( $userinfo_response ) && 200 === wp_remote_retrieve_response_code( $userinfo_response ) ) { + $userinfo_body = wp_remote_retrieve_body( $userinfo_response ); + $userinfo_data = json_decode( (string) $userinfo_body, true ); + if ( is_array( $userinfo_data ) && ! empty( $userinfo_data['email'] ) ) { + $connected_email = sanitize_email( (string) $userinfo_data['email'] ); + } + } + } + + $options = get_option( $this->plugin->get_option_key(), [] ); + if ( ! is_array( $options ) ) { + $options = []; + } + + $options['google_oauth_refresh_token'] = $refresh_token; + $options['google_oauth_connected_email'] = $connected_email; + $options['google_oauth_connected_at'] = time(); + update_option( $this->plugin->get_option_key(), $options ); + + $url = add_query_arg( + [ + 'groq_ai_google_oauth' => 'success', + 'groq_ai_google_oauth_message' => __( 'Google succesvol verbonden.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + ], + $this->get_settings_page_url() + ); + wp_safe_redirect( $url ); + exit; + } + + public function handle_google_oauth_disconnect() { + if ( ! current_user_can( 'manage_options' ) ) { + wp_die( esc_html__( 'Geen toestemming.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ); + } + + check_admin_referer( 'groq_ai_google_oauth_disconnect' ); + + $options = get_option( $this->plugin->get_option_key(), [] ); + if ( ! is_array( $options ) ) { + $options = []; + } + + $options['google_oauth_refresh_token'] = ''; + $options['google_oauth_connected_email'] = ''; + $options['google_oauth_connected_at'] = 0; + update_option( $this->plugin->get_option_key(), $options ); + + $url = add_query_arg( + [ + 'groq_ai_google_oauth' => 'success', + 'groq_ai_google_oauth_message' => __( 'Google koppeling verwijderd.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + ], + $this->get_settings_page_url() + ); + wp_safe_redirect( $url ); + exit; + } + public function render_modules_page() { if ( ! current_user_can( 'manage_options' ) ) { return; @@ -648,7 +1577,14 @@ class Groq_AI_Product_Text_Settings_Page { } public function enqueue_settings_assets( $hook ) { - if ( ! in_array( $hook, [ 'settings_page_groq-ai-product-text', 'settings_page_groq-ai-product-text-modules', 'settings_page_groq-ai-product-text-prompts' ], true ) ) { + if ( ! in_array( $hook, [ + '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', + ], true ) ) { return; } @@ -674,6 +1610,29 @@ class Groq_AI_Product_Text_Settings_Page { true ); + if ( 'settings_page_groq-ai-product-text-term' === $hook ) { + 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, + ] + ); + } + $current_settings = $this->plugin->get_settings(); $data = [ 'optionKey' => $this->plugin->get_option_key(), diff --git a/includes/Core/class-groq-ai-ajax-controller.php b/includes/Core/class-groq-ai-ajax-controller.php index 26726a0..40ff885 100644 --- a/includes/Core/class-groq-ai-ajax-controller.php +++ b/includes/Core/class-groq-ai-ajax-controller.php @@ -9,6 +9,108 @@ class Groq_AI_Ajax_Controller { add_action( 'wp_ajax_groq_ai_generate_text', [ $this, 'handle_generate_text' ] ); add_action( 'wp_ajax_groq_ai_refresh_models', [ $this, 'handle_refresh_models' ] ); + add_action( 'wp_ajax_groq_ai_generate_term_text', [ $this, 'handle_generate_term_text' ] ); + } + + public function handle_generate_term_text() { + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( [ 'message' => __( 'Je hebt geen toestemming voor deze actie.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ], 403 ); + } + + check_ajax_referer( 'groq_ai_generate_term', 'nonce' ); + + $prompt = isset( $_POST['prompt'] ) ? sanitize_textarea_field( wp_unslash( $_POST['prompt'] ) ) : ''; + $taxonomy = isset( $_POST['taxonomy'] ) ? sanitize_key( wp_unslash( $_POST['taxonomy'] ) ) : ''; + $term_id = isset( $_POST['term_id'] ) ? absint( $_POST['term_id'] ) : 0; + $include_top_products = ! empty( $_POST['include_top_products'] ); + $top_products_limit = isset( $_POST['top_products_limit'] ) ? absint( $_POST['top_products_limit'] ) : 10; + $top_products_limit = max( 1, min( 25, $top_products_limit ) ); + + if ( '' === $prompt || '' === $taxonomy || ! taxonomy_exists( $taxonomy ) || ! $term_id ) { + wp_send_json_error( [ 'message' => __( 'Prompt, taxonomy en term_id zijn verplicht.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ], 400 ); + } + + $term = get_term( $term_id, $taxonomy ); + if ( ! $term || is_wp_error( $term ) ) { + wp_send_json_error( [ 'message' => __( 'Term niet gevonden.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ], 404 ); + } + + $settings = $this->plugin->get_settings(); + $provider_manager = $this->plugin->get_provider_manager(); + $provider_key = $settings['provider']; + $provider = $provider_manager->get_provider( $provider_key ); + + if ( ! $provider ) { + $provider = $provider_manager->get_provider( 'groq' ); + $provider_key = 'groq'; + } + + $conversation_id = $this->plugin->get_conversation_manager()->ensure_id( $provider_key, $settings['store_context'] ); + $prompt_builder = $this->plugin->get_prompt_builder(); + $system_prompt = method_exists( $prompt_builder, 'build_term_system_prompt' ) + ? $prompt_builder->build_term_system_prompt( $settings, $conversation_id, $term ) + : $prompt_builder->build_system_prompt( $settings, $conversation_id ); + + $model = $this->plugin->get_selected_model( $provider, $settings ); + $context_block = ''; + if ( method_exists( $prompt_builder, 'build_term_context_block' ) ) { + $context_block = $prompt_builder->build_term_context_block( + $term, + [ + 'include_top_products' => $include_top_products, + 'top_products_limit' => $top_products_limit, + ], + $settings + ); + } + $prompt_with_context = method_exists( $prompt_builder, 'prepend_term_context_to_prompt' ) + ? $prompt_builder->prepend_term_context_to_prompt( $prompt, $context_block ) + : $prompt_builder->prepend_context_to_prompt( $prompt, $context_block ); + + $response_format = null; + $use_response_format = $this->plugin->should_use_response_format( $provider, $settings ); + if ( $use_response_format && method_exists( $prompt_builder, 'get_term_response_format_definition' ) ) { + $response_format = $prompt_builder->get_term_response_format_definition( $settings ); + $final_prompt = $prompt_with_context; + } elseif ( method_exists( $prompt_builder, 'append_term_response_instructions' ) ) { + $final_prompt = $prompt_builder->append_term_response_instructions( $prompt_with_context, $settings ); + } else { + $final_prompt = $prompt_builder->append_response_instructions( $prompt_with_context, $settings ); + } + + $result = $provider->generate_content( + [ + 'prompt' => $final_prompt, + 'system_prompt' => $system_prompt, + 'model' => $model, + 'settings' => $settings, + 'temperature' => 0.7, + 'conversation_id' => $conversation_id, + 'response_format' => $response_format, + ] + ); + + if ( is_wp_error( $result ) ) { + wp_send_json_error( [ 'message' => $result->get_error_message() ], 500 ); + } + + $response_text = $this->extract_content_text( $result ); + $parsed = null; + if ( method_exists( $prompt_builder, 'parse_term_structured_response' ) ) { + $parsed = $prompt_builder->parse_term_structured_response( $response_text, $settings ); + } + if ( ! is_array( $parsed ) ) { + $parsed = [ + 'description' => trim( (string) $response_text ), + ]; + } + + wp_send_json_success( + [ + 'description' => isset( $parsed['description'] ) ? $parsed['description'] : '', + 'raw' => $response_text, + ] + ); } public function handle_generate_text() { diff --git a/includes/Services/Google/class-groq-ai-google-analytics-data-client.php b/includes/Services/Google/class-groq-ai-google-analytics-data-client.php new file mode 100644 index 0000000..590dcc6 --- /dev/null +++ b/includes/Services/Google/class-groq-ai-google-analytics-data-client.php @@ -0,0 +1,182 @@ +oauth = $oauth; + } + + /** + * Simple connectivity check for GA4 Data API. + * + * @param array $settings + * @param string $property_id + * @param string $start_date + * @param string $end_date + * @return array|WP_Error + */ + public function get_property_sessions_summary( $settings, $property_id, $start_date, $end_date ) { + $property_id = trim( (string) $property_id ); + if ( '' === $property_id ) { + return new WP_Error( 'groq_ai_ga_missing', __( 'GA4 property ID ontbreekt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ); + } + + $token = $this->oauth->get_access_token( $settings ); + if ( is_wp_error( $token ) ) { + return $token; + } + + $endpoint = 'https://analyticsdata.googleapis.com/v1beta/properties/' . rawurlencode( $property_id ) . ':runReport'; + $body = [ + 'dateRanges' => [ + [ + 'startDate' => $start_date, + 'endDate' => $end_date, + ], + ], + 'metrics' => [ + [ 'name' => 'sessions' ], + [ 'name' => 'engagedSessions' ], + ], + 'limit' => 1, + ]; + + $response = wp_remote_post( + $endpoint, + [ + 'timeout' => 20, + 'headers' => [ + 'Authorization' => 'Bearer ' . $token, + 'Content-Type' => 'application/json', + ], + 'body' => wp_json_encode( $body ), + ] + ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + $status_code = wp_remote_retrieve_response_code( $response ); + $raw_body = wp_remote_retrieve_body( $response ); + $data = json_decode( (string) $raw_body, true ); + + if ( 200 !== $status_code || ! is_array( $data ) ) { + return new WP_Error( 'groq_ai_ga_error', __( 'GA4 Data API call mislukt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ); + } + + $rows = isset( $data['rows'] ) && is_array( $data['rows'] ) ? $data['rows'] : []; + $sessions = 0; + $engaged = 0; + foreach ( $rows as $row ) { + $metric_values = isset( $row['metricValues'] ) && is_array( $row['metricValues'] ) ? $row['metricValues'] : []; + if ( isset( $metric_values[0]['value'] ) ) { + $sessions += absint( $metric_values[0]['value'] ); + } + if ( isset( $metric_values[1]['value'] ) ) { + $engaged += absint( $metric_values[1]['value'] ); + } + } + + return [ + 'sessions' => $sessions, + 'engagedSessions' => $engaged, + ]; + } + + /** + * Returns approximate GA4 sessions for a landing page path. + * + * @param array $settings + * @param string $property_id + * @param string $page_path e.g. /product-category/foo/ + * @param string $start_date YYYY-MM-DD + * @param string $end_date YYYY-MM-DD + * @return array|WP_Error + */ + public function get_sessions_for_landing_page_path( $settings, $property_id, $page_path, $start_date, $end_date ) { + $property_id = trim( (string) $property_id ); + $page_path = trim( (string) $page_path ); + + if ( '' === $property_id || '' === $page_path ) { + return new WP_Error( 'groq_ai_ga_missing', __( 'GA4 property ID of page path ontbreekt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ); + } + + $token = $this->oauth->get_access_token( $settings ); + if ( is_wp_error( $token ) ) { + return $token; + } + + $endpoint = 'https://analyticsdata.googleapis.com/v1beta/properties/' . rawurlencode( $property_id ) . ':runReport'; + + $body = [ + 'dateRanges' => [ + [ + 'startDate' => $start_date, + 'endDate' => $end_date, + ], + ], + 'dimensions' => [ + [ 'name' => 'landingPagePlusQueryString' ], + ], + 'metrics' => [ + [ 'name' => 'sessions' ], + [ 'name' => 'engagedSessions' ], + ], + 'dimensionFilter' => [ + 'filter' => [ + 'fieldName' => 'landingPagePlusQueryString', + 'stringFilter' => [ + 'matchType' => 'CONTAINS', + 'value' => $page_path, + ], + ], + ], + 'limit' => 5, + ]; + + $response = wp_remote_post( + $endpoint, + [ + 'timeout' => 20, + 'headers' => [ + 'Authorization' => 'Bearer ' . $token, + 'Content-Type' => 'application/json', + ], + 'body' => wp_json_encode( $body ), + ] + ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + $status_code = wp_remote_retrieve_response_code( $response ); + $raw_body = wp_remote_retrieve_body( $response ); + $data = json_decode( (string) $raw_body, true ); + + if ( 200 !== $status_code || ! is_array( $data ) ) { + return new WP_Error( 'groq_ai_ga_error', __( 'GA4 Data API call mislukt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ); + } + + $rows = isset( $data['rows'] ) && is_array( $data['rows'] ) ? $data['rows'] : []; + $sessions = 0; + $engaged = 0; + foreach ( $rows as $row ) { + $metric_values = isset( $row['metricValues'] ) && is_array( $row['metricValues'] ) ? $row['metricValues'] : []; + if ( isset( $metric_values[0]['value'] ) ) { + $sessions += absint( $metric_values[0]['value'] ); + } + if ( isset( $metric_values[1]['value'] ) ) { + $engaged += absint( $metric_values[1]['value'] ); + } + } + + return [ + 'sessions' => $sessions, + 'engagedSessions' => $engaged, + ]; + } +} diff --git a/includes/Services/Google/class-groq-ai-google-context-builder.php b/includes/Services/Google/class-groq-ai-google-context-builder.php new file mode 100644 index 0000000..0a515b2 --- /dev/null +++ b/includes/Services/Google/class-groq-ai-google-context-builder.php @@ -0,0 +1,113 @@ +gsc = $gsc; + $this->ga = $ga; + } + + /** + * @param string $existing + * @param WP_Term $term + * @param array $settings + * @return string + */ + public function build_term_google_context( $existing, $term, $settings ) { + if ( ! $term || ! is_object( $term ) ) { + return (string) $existing; + } + + $enabled_gsc = ! empty( $settings['google_enable_gsc'] ); + $enabled_ga = ! empty( $settings['google_enable_ga'] ); + + if ( ! $enabled_gsc && ! $enabled_ga ) { + return (string) $existing; + } + + $term_id = isset( $term->term_id ) ? absint( $term->term_id ) : 0; + $taxonomy = isset( $term->taxonomy ) ? sanitize_key( (string) $term->taxonomy ) : ''; + + $range_days = 28; + $end_date = gmdate( 'Y-m-d' ); + $start_date = gmdate( 'Y-m-d', time() - ( $range_days * DAY_IN_SECONDS ) ); + + $term_link = get_term_link( $term ); + if ( is_wp_error( $term_link ) ) { + $term_link = ''; + } + + $page_path = ''; + if ( is_string( $term_link ) && '' !== $term_link ) { + $parts = wp_parse_url( $term_link ); + if ( is_array( $parts ) && isset( $parts['path'] ) ) { + $page_path = (string) $parts['path']; + } + } + + $cache_key = 'groq_ai_google_term_ctx_' . md5( $taxonomy . '|' . $term_id . '|' . $start_date . '|' . $end_date ); + $cached = get_transient( $cache_key ); + if ( is_string( $cached ) && '' !== $cached ) { + return trim( (string) $existing . "\n\n" . $cached ); + } + + $lines = []; + $lines[] = sprintf( + /* translators: %d: days */ + __( 'Google data (laatste %d dagen):', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + $range_days + ); + + if ( $enabled_gsc ) { + $site_url = isset( $settings['google_gsc_site_url'] ) ? trim( (string) $settings['google_gsc_site_url'] ) : ''; + if ( '' !== $site_url && '' !== $term_link ) { + $queries = $this->gsc->get_top_queries_for_page( $settings, $site_url, $term_link, $start_date, $end_date, 10 ); + if ( is_wp_error( $queries ) ) { + $lines[] = __( 'Search Console: kon queries niet ophalen.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); + } elseif ( empty( $queries ) ) { + $lines[] = __( 'Search Console: geen query data gevonden voor deze pagina.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); + } else { + $lines[] = __( 'Search Console top zoekopdrachten (query → clicks/impr):', GROQ_AI_PRODUCT_TEXT_DOMAIN ); + foreach ( $queries as $row ) { + $q = isset( $row['query'] ) ? (string) $row['query'] : ''; + $c = isset( $row['clicks'] ) ? (float) $row['clicks'] : 0.0; + $i = isset( $row['impressions'] ) ? (float) $row['impressions'] : 0.0; + if ( '' === $q ) { + continue; + } + $lines[] = sprintf( '- %s → %d/%d', $q, (int) round( $c ), (int) round( $i ) ); + } + } + } + } + + if ( $enabled_ga ) { + $property_id = isset( $settings['google_ga4_property_id'] ) ? trim( (string) $settings['google_ga4_property_id'] ) : ''; + if ( '' !== $property_id && '' !== $page_path ) { + $stats = $this->ga->get_sessions_for_landing_page_path( $settings, $property_id, $page_path, $start_date, $end_date ); + if ( is_wp_error( $stats ) ) { + $lines[] = __( 'Analytics: kon sessies niet ophalen.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); + } else { + $sessions = isset( $stats['sessions'] ) ? absint( $stats['sessions'] ) : 0; + $engaged = isset( $stats['engagedSessions'] ) ? absint( $stats['engagedSessions'] ) : 0; + $lines[] = sprintf( __( 'Analytics (GA4): sessies ~%1$d, engaged sessies ~%2$d', GROQ_AI_PRODUCT_TEXT_DOMAIN ), $sessions, $engaged ); + } + } + } + + // If we only have the header, skip. + if ( count( $lines ) <= 1 ) { + return (string) $existing; + } + + $context = implode( "\n", $lines ); + set_transient( $cache_key, $context, 15 * MINUTE_IN_SECONDS ); + + return trim( (string) $existing . "\n\n" . $context ); + } +} diff --git a/includes/Services/Google/class-groq-ai-google-oauth-client.php b/includes/Services/Google/class-groq-ai-google-oauth-client.php new file mode 100644 index 0000000..5bbdad7 --- /dev/null +++ b/includes/Services/Google/class-groq-ai-google-oauth-client.php @@ -0,0 +1,101 @@ + 20, + 'headers' => [ + 'Content-Type' => 'application/x-www-form-urlencoded', + ], + 'body' => [ + 'client_id' => $client_id, + 'client_secret' => $client_secret, + 'refresh_token' => $refresh_token, + 'grant_type' => 'refresh_token', + ], + ] + ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + $status_code = wp_remote_retrieve_response_code( $response ); + $body = wp_remote_retrieve_body( $response ); + $data = json_decode( (string) $body, true ); + + if ( 200 !== $status_code || ! is_array( $data ) ) { + return new WP_Error( 'groq_ai_google_oauth_refresh_failed', __( 'Google token refresh mislukt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ); + } + + $access_token = isset( $data['access_token'] ) ? sanitize_text_field( (string) $data['access_token'] ) : ''; + $expires_in = isset( $data['expires_in'] ) ? absint( $data['expires_in'] ) : 0; + + if ( '' === $access_token ) { + return new WP_Error( 'groq_ai_google_oauth_refresh_failed', __( 'Geen access token ontvangen van Google.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ); + } + + $ttl = max( 60, $expires_in - 60 ); + set_transient( $cache_key, $access_token, $ttl ); + + return $access_token; + } + + /** + * Diagnostics helper: returns scopes for a given access token. + * + * @param string $access_token + * @return array|WP_Error { 'scope' => string, 'expires_in' => int } + */ + public function get_access_token_info( $access_token ) { + $access_token = trim( (string) $access_token ); + if ( '' === $access_token ) { + return new WP_Error( 'groq_ai_google_tokeninfo_missing', __( 'Geen access token om te inspecteren.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ); + } + + $response = wp_remote_get( + add_query_arg( [ 'access_token' => $access_token ], 'https://oauth2.googleapis.com/tokeninfo' ), + [ 'timeout' => 15 ] + ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + $status_code = wp_remote_retrieve_response_code( $response ); + $body = wp_remote_retrieve_body( $response ); + $data = json_decode( (string) $body, true ); + + if ( 200 !== $status_code || ! is_array( $data ) ) { + return new WP_Error( 'groq_ai_google_tokeninfo_failed', __( 'Kon tokeninfo niet ophalen.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ); + } + + $scope = isset( $data['scope'] ) ? trim( (string) $data['scope'] ) : ''; + $expires_in = isset( $data['expires_in'] ) ? absint( $data['expires_in'] ) : 0; + + return [ + 'scope' => $scope, + 'expires_in' => $expires_in, + ]; + } +} diff --git a/includes/Services/Google/class-groq-ai-google-search-console-client.php b/includes/Services/Google/class-groq-ai-google-search-console-client.php new file mode 100644 index 0000000..0d69f3e --- /dev/null +++ b/includes/Services/Google/class-groq-ai-google-search-console-client.php @@ -0,0 +1,195 @@ +oauth = $oauth; + } + + /** + * @param int $status_code + * @param string $raw_body + * @return WP_Error + */ + private function build_http_error( $status_code, $raw_body ) { + $status_code = absint( $status_code ); + $raw_body = (string) $raw_body; + + $message = __( 'Search Console API call mislukt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); + $details = ''; + + $data = json_decode( $raw_body, true ); + if ( is_array( $data ) ) { + // Google APIs often respond with: { error: { code, message, status, details/errors } } + $err = isset( $data['error'] ) && is_array( $data['error'] ) ? $data['error'] : []; + $google_message = isset( $err['message'] ) ? trim( (string) $err['message'] ) : ''; + $google_status = isset( $err['status'] ) ? trim( (string) $err['status'] ) : ''; + if ( '' !== $google_status || '' !== $google_message ) { + $details = trim( $google_status . ( $google_status && $google_message ? ': ' : '' ) . $google_message ); + } + } + + if ( '' !== $details ) { + $message = sprintf( + /* translators: 1: HTTP status, 2: details */ + __( 'Search Console API call mislukt (HTTP %1$d): %2$s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + $status_code, + $details + ); + } else { + $message = sprintf( + /* translators: %d: HTTP status */ + __( 'Search Console API call mislukt (HTTP %d).', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + $status_code + ); + } + + return new WP_Error( 'groq_ai_gsc_error', $message ); + } + + /** + * @param array $settings + * @return array|WP_Error Array of siteUrl strings. + */ + public function list_sites( $settings ) { + $token = $this->oauth->get_access_token( $settings ); + if ( is_wp_error( $token ) ) { + return $token; + } + + $response = wp_remote_get( + 'https://searchconsole.googleapis.com/webmasters/v3/sites', + [ + 'timeout' => 20, + 'headers' => [ + 'Authorization' => 'Bearer ' . $token, + 'Accept' => 'application/json', + ], + ] + ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + $status_code = wp_remote_retrieve_response_code( $response ); + $raw_body = wp_remote_retrieve_body( $response ); + $data = json_decode( (string) $raw_body, true ); + + if ( 200 !== $status_code || ! is_array( $data ) ) { + return $this->build_http_error( $status_code, $raw_body ); + } + + $entries = isset( $data['siteEntry'] ) && is_array( $data['siteEntry'] ) ? $data['siteEntry'] : []; + $sites = []; + foreach ( $entries as $entry ) { + if ( ! is_array( $entry ) ) { + continue; + } + $site_url = isset( $entry['siteUrl'] ) ? trim( (string) $entry['siteUrl'] ) : ''; + if ( '' !== $site_url ) { + $sites[] = $site_url; + } + } + + $sites = array_values( array_unique( $sites ) ); + sort( $sites, SORT_NATURAL | SORT_FLAG_CASE ); + + return $sites; + } + + /** + * @param array $settings + * @param string $site_url + * @param string $page_url + * @param string $start_date YYYY-MM-DD + * @param string $end_date YYYY-MM-DD + * @param int $limit + * @return array|WP_Error + */ + public function get_top_queries_for_page( $settings, $site_url, $page_url, $start_date, $end_date, $limit = 10 ) { + $site_url = trim( (string) $site_url ); + $page_url = trim( (string) $page_url ); + $limit = max( 1, min( 25, absint( $limit ) ) ); + + if ( '' === $site_url || '' === $page_url ) { + return new WP_Error( 'groq_ai_gsc_missing', __( 'Search Console site URL of pagina URL ontbreekt.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ); + } + + $token = $this->oauth->get_access_token( $settings ); + if ( is_wp_error( $token ) ) { + return $token; + } + + $endpoint = 'https://searchconsole.googleapis.com/webmasters/v3/sites/' . rawurlencode( $site_url ) . '/searchAnalytics/query'; + + $body = [ + 'startDate' => $start_date, + 'endDate' => $end_date, + 'dimensions' => [ 'query' ], + 'rowLimit' => $limit, + 'dimensionFilterGroups' => [ + [ + 'filters' => [ + [ + 'dimension' => 'page', + 'operator' => 'equals', + 'expression' => $page_url, + ], + ], + ], + ], + 'aggregationType' => 'auto', + ]; + + $response = wp_remote_post( + $endpoint, + [ + 'timeout' => 20, + 'headers' => [ + 'Authorization' => 'Bearer ' . $token, + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + ], + 'body' => wp_json_encode( $body ), + ] + ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + $status_code = wp_remote_retrieve_response_code( $response ); + $raw_body = wp_remote_retrieve_body( $response ); + $data = json_decode( (string) $raw_body, true ); + + if ( 200 !== $status_code || ! is_array( $data ) ) { + return $this->build_http_error( $status_code, $raw_body ); + } + + $rows = isset( $data['rows'] ) && is_array( $data['rows'] ) ? $data['rows'] : []; + $result = []; + + foreach ( $rows as $row ) { + if ( ! is_array( $row ) ) { + continue; + } + $keys = isset( $row['keys'] ) && is_array( $row['keys'] ) ? $row['keys'] : []; + $query = isset( $keys[0] ) ? sanitize_text_field( (string) $keys[0] ) : ''; + if ( '' === $query ) { + continue; + } + $result[] = [ + 'query' => $query, + 'clicks' => isset( $row['clicks'] ) ? (float) $row['clicks'] : 0.0, + 'impressions' => isset( $row['impressions'] ) ? (float) $row['impressions'] : 0.0, + 'ctr' => isset( $row['ctr'] ) ? (float) $row['ctr'] : 0.0, + 'position' => isset( $row['position'] ) ? (float) $row['position'] : 0.0, + ]; + } + + return $result; + } +} diff --git a/includes/Services/Prompt/class-groq-ai-prompt-builder.php b/includes/Services/Prompt/class-groq-ai-prompt-builder.php index 7a511e7..568e8d5 100644 --- a/includes/Services/Prompt/class-groq-ai-prompt-builder.php +++ b/includes/Services/Prompt/class-groq-ai-prompt-builder.php @@ -29,6 +29,32 @@ class Groq_AI_Prompt_Builder { ); } + public function build_term_system_prompt( $settings, $conversation_id, $term ) { + $context = isset( $settings['store_context'] ) ? trim( $settings['store_context'] ) : ''; + $term_name = is_object( $term ) && isset( $term->name ) ? (string) $term->name : ''; + $base_instruction = __( 'Je bent een copywriter voor een WooCommerce winkel en schrijft SEO-vriendelijke categorie- en merkpagina teksten.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); + + if ( $context ) { + $base_instruction = sprintf( + __( 'Je bent een copywriter voor een WooCommerce winkel. Gebruik de volgende winkelcontext indien beschikbaar: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + $context + ); + } + + if ( '' !== $term_name ) { + $base_instruction .= ' ' . sprintf( + __( 'Je schrijft nu voor de term: %s.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + $term_name + ); + } + + return sprintf( + __( 'Conversatie-ID: %1$s. %2$s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + $conversation_id, + $base_instruction + ); + } + public function append_response_instructions( $prompt, $settings ) { $instructions = (string) ( $this->get_structured_response_instructions( $settings ) ?? '' ); $prompt = trim( (string) $prompt ); @@ -232,6 +258,243 @@ class Groq_AI_Prompt_Builder { return $intro . "\n" . $context . "\n\n" . $prompt; } + public function build_term_context_block( $term, $options = [], $settings = null ) { + if ( ! $term || ! is_object( $term ) ) { + return ''; + } + + $taxonomy = isset( $term->taxonomy ) ? sanitize_key( (string) $term->taxonomy ) : ''; + $term_id = isset( $term->term_id ) ? absint( $term->term_id ) : 0; + if ( '' === $taxonomy || ! $term_id ) { + return ''; + } + + $include_top_products = ! empty( $options['include_top_products'] ); + $top_products_limit = isset( $options['top_products_limit'] ) ? absint( $options['top_products_limit'] ) : 10; + $top_products_limit = max( 1, min( 25, $top_products_limit ) ); + + $parts = []; + $parts[] = sprintf( __( 'Term: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), wp_strip_all_tags( (string) $term->name ) ); + if ( isset( $term->slug ) && '' !== (string) $term->slug ) { + $parts[] = sprintf( __( 'Slug: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), sanitize_title( (string) $term->slug ) ); + } + if ( isset( $term->count ) ) { + $parts[] = sprintf( __( 'Aantal producten: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), (string) absint( $term->count ) ); + } + if ( isset( $term->description ) && '' !== trim( (string) $term->description ) ) { + $parts[] = sprintf( __( 'Huidige omschrijving: %s', GROQ_AI_PRODUCT_TEXT_DOMAIN ), wp_strip_all_tags( (string) $term->description ) ); + } + + if ( $include_top_products ) { + $top_products = $this->get_top_products_for_term( $taxonomy, $term_id, $top_products_limit ); + if ( ! empty( $top_products ) ) { + $lines = []; + foreach ( $top_products as $product_row ) { + $lines[] = sprintf( '- %s', $product_row ); + } + $parts[] = __( 'Top verkochte producten (indicatief):', GROQ_AI_PRODUCT_TEXT_DOMAIN ) . "\n" . implode( "\n", $lines ); + } + } + + $google_context = apply_filters( 'groq_ai_term_google_context', '', $term, $settings ); + $google_context = trim( (string) $google_context ); + if ( '' !== $google_context ) { + $parts[] = $google_context; + } + + return implode( "\n\n", array_filter( $parts ) ); + } + + public function prepend_term_context_to_prompt( $prompt, $context ) { + $context = trim( (string) $context ); + if ( '' === $context ) { + return $prompt; + } + $intro = __( 'Gebruik de volgende categorie/term-context bij het schrijven:', GROQ_AI_PRODUCT_TEXT_DOMAIN ); + return $intro . "\n" . $context . "\n\n" . $prompt; + } + + public function get_term_response_format_definition( $settings = null ) { + $rankmath_enabled = $this->settings_manager->is_module_enabled( 'rankmath', $settings ); + $keyword_limit = $this->settings_manager->get_rankmath_focus_keyword_limit( $settings ); + $title_pixels = $this->settings_manager->get_rankmath_meta_title_pixel_limit( $settings ); + $desc_pixels = $this->settings_manager->get_rankmath_meta_description_pixel_limit( $settings ); + + $properties = [ + 'description' => [ + 'type' => 'string', + 'description' => __( 'HTML-omschrijving voor de categorie/term met paragrafen en eventueel lijstjes.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'minLength' => 20, + ], + ]; + + if ( $rankmath_enabled ) { + $properties['meta_title'] = [ + 'type' => 'string', + 'description' => sprintf( + __( 'SEO-meta title (max. %1$d tekens en %2$d pixels).', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 60, + $title_pixels + ), + 'maxLength' => 120, + ]; + $properties['meta_description'] = [ + 'type' => 'string', + 'description' => sprintf( + __( 'SEO-meta description (max. %1$d tekens en %2$d pixels).', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 160, + $desc_pixels + ), + 'maxLength' => 320, + ]; + $properties['focus_keywords'] = [ + 'type' => 'array', + 'description' => __( 'Lijst met korte zoekwoorden zonder hashtags of extra tekst.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + 'maxItems' => max( 1, $keyword_limit ), + 'items' => [ + 'type' => 'string', + 'minLength' => 1, + ], + ]; + } + + $schema = [ + 'type' => 'object', + 'properties' => $properties, + 'required' => [ 'description' ], + 'additionalProperties' => false, + ]; + + return [ + 'type' => 'json_schema', + 'json_schema' => [ + 'name' => 'groq_ai_term_text', + 'schema' => $schema, + ], + ]; + } + + public function append_term_response_instructions( $prompt, $settings ) { + $instructions = (string) ( $this->get_term_structured_response_instructions( $settings ) ?? '' ); + $prompt = trim( (string) $prompt ); + if ( '' === $instructions ) { + return $prompt; + } + if ( false !== strpos( $prompt, $instructions ) ) { + return $prompt; + } + return $prompt . "\n\n" . $instructions; + } + + public function parse_term_structured_response( $raw, $settings = null ) { + if ( empty( $raw ) ) { + return new WP_Error( 'groq_ai_empty_response', __( 'Geen data ontvangen van de AI.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ); + } + + $clean = trim( (string) $raw ); + if ( preg_match( '/```(?:json)?\s*(.*?)```/is', $clean, $matches ) ) { + $clean = trim( $matches[1] ); + } + + $decoded = json_decode( $clean, true ); + if ( ! is_array( $decoded ) ) { + // Fallback: treat as plain text. + return [ + 'description' => trim( (string) $raw ), + ]; + } + + $description = isset( $decoded['description'] ) ? trim( (string) $decoded['description'] ) : ''; + if ( '' === $description ) { + return new WP_Error( 'groq_ai_parse_error', __( 'De AI-respons bevatte geen description veld.', GROQ_AI_PRODUCT_TEXT_DOMAIN ) ); + } + + $result = [ + 'description' => $description, + ]; + + if ( isset( $decoded['meta_title'] ) ) { + $result['meta_title'] = $this->truncate_meta_field( (string) $decoded['meta_title'], 60 ); + } + if ( isset( $decoded['meta_description'] ) ) { + $result['meta_description'] = $this->truncate_meta_field( (string) $decoded['meta_description'], 160 ); + } + if ( isset( $decoded['focus_keywords'] ) ) { + if ( is_array( $decoded['focus_keywords'] ) ) { + $keywords = []; + foreach ( $decoded['focus_keywords'] as $kw ) { + $kw = trim( (string) $kw ); + if ( '' !== $kw ) { + $keywords[] = $kw; + } + } + $keywords = array_values( array_unique( $keywords ) ); + $result['focus_keywords'] = implode( ', ', $keywords ); + } + } + + return $result; + } + + private function get_term_structured_response_instructions( $settings = null ) { + $schema_parts = [ + '"description":"..."', + ]; + + $rankmath_enabled = $this->settings_manager->is_module_enabled( 'rankmath', $settings ); + if ( $rankmath_enabled ) { + $schema_parts[] = '"meta_title":"..."'; + $schema_parts[] = '"meta_description":"..."'; + $schema_parts[] = '"focus_keywords":["...","..."]'; + } + + $json_structure = '{' . implode( ',', $schema_parts ) . '}'; + + $instruction = sprintf( + __( 'Geef ALLEEN een geldig JSON-object terug met deze structuur: %s. Gebruik dubbele aanhalingstekens, geen Markdown of extra tekst. Gebruik \n voor regeleinden.', GROQ_AI_PRODUCT_TEXT_DOMAIN ), + $json_structure + ); + + $instruction .= ' ' . __( 'Zorg dat description geldige HTML bevat (gebruik minimaal

-tags en waar relevant lijstjes of benadrukking). Voeg geen extra tekst buiten het JSON-object toe.', GROQ_AI_PRODUCT_TEXT_DOMAIN ); + return $instruction; + } + + private function get_top_products_for_term( $taxonomy, $term_id, $limit = 10 ) { + $taxonomy = sanitize_key( (string) $taxonomy ); + $term_id = absint( $term_id ); + $limit = max( 1, min( 25, absint( $limit ) ) ); + + $query = new WP_Query( + [ + 'post_type' => 'product', + 'post_status' => 'publish', + 'posts_per_page' => $limit, + 'no_found_rows' => true, + 'meta_key' => 'total_sales', + 'orderby' => 'meta_value_num', + 'order' => 'DESC', + 'tax_query' => [ + [ + 'taxonomy' => $taxonomy, + 'field' => 'term_id', + 'terms' => [ $term_id ], + ], + ], + ] + ); + + $rows = []; + if ( $query->have_posts() ) { + foreach ( $query->posts as $post ) { + $title = isset( $post->post_title ) ? wp_strip_all_tags( (string) $post->post_title ) : ''; + $rows[] = $title; + } + } + wp_reset_postdata(); + + return array_values( array_filter( $rows ) ); + } + public function get_response_format_definition( $settings = null ) { $rankmath_enabled = $this->settings_manager->is_module_enabled( 'rankmath', $settings ); $keyword_limit = $this->settings_manager->get_rankmath_focus_keyword_limit( $settings ); diff --git a/includes/Services/Settings/class-groq-ai-settings-manager.php b/includes/Services/Settings/class-groq-ai-settings-manager.php index c1532c1..b02a2b7 100644 --- a/includes/Services/Settings/class-groq-ai-settings-manager.php +++ b/includes/Services/Settings/class-groq-ai-settings-manager.php @@ -35,6 +35,15 @@ class Groq_AI_Settings_Manager { 'groq_api_key' => '', 'openai_api_key' => '', 'google_api_key' => '', + 'google_oauth_client_id' => '', + 'google_oauth_client_secret' => '', + 'google_oauth_refresh_token' => '', + 'google_oauth_connected_email' => '', + 'google_oauth_connected_at' => 0, + 'google_enable_gsc' => true, + 'google_enable_ga' => true, + 'google_gsc_site_url' => '', + 'google_ga4_property_id' => '', 'context_fields' => $this->get_default_context_fields(), 'modules' => $this->get_default_modules_settings(), 'image_context_mode' => 'url', @@ -81,6 +90,15 @@ class Groq_AI_Settings_Manager { 'groq_api_key' => '', 'openai_api_key' => '', 'google_api_key' => '', + 'google_oauth_client_id' => '', + 'google_oauth_client_secret' => '', + 'google_oauth_refresh_token' => '', + 'google_oauth_connected_email' => '', + 'google_oauth_connected_at' => 0, + 'google_enable_gsc' => true, + 'google_enable_ga' => true, + 'google_gsc_site_url' => '', + 'google_ga4_property_id' => '', 'context_fields' => $this->get_default_context_fields(), 'modules' => $this->get_default_modules_settings(), 'image_context_mode' => 'url', @@ -127,6 +145,15 @@ class Groq_AI_Settings_Manager { 'groq_api_key' => sanitize_text_field( $input['groq_api_key'] ), 'openai_api_key' => sanitize_text_field( $input['openai_api_key'] ), 'google_api_key' => sanitize_text_field( $input['google_api_key'] ), + 'google_oauth_client_id' => sanitize_text_field( $input['google_oauth_client_id'] ), + 'google_oauth_client_secret' => sanitize_text_field( $input['google_oauth_client_secret'] ), + 'google_oauth_refresh_token' => sanitize_text_field( $input['google_oauth_refresh_token'] ), + 'google_oauth_connected_email' => sanitize_text_field( $input['google_oauth_connected_email'] ), + 'google_oauth_connected_at' => absint( $input['google_oauth_connected_at'] ), + 'google_enable_gsc' => ! empty( $raw_input['google_enable_gsc'] ), + 'google_enable_ga' => ! empty( $raw_input['google_enable_ga'] ), + 'google_gsc_site_url' => esc_url_raw( (string) $input['google_gsc_site_url'] ), + 'google_ga4_property_id' => sanitize_text_field( (string) $input['google_ga4_property_id'] ), 'response_format_compat' => ! empty( $raw_input['response_format_compat'] ), 'image_context_mode' => $image_mode, 'image_context_limit' => $image_limit,