Add Google and Groq AI providers, enhance provider manager, and implement conversation and logging services

- Introduced `Groq_AI_Provider_Google` and `Groq_AI_Provider_Groq` classes for handling AI interactions with Google and Groq respectively.
- Enhanced `Groq_AI_Provider_Manager` to register and manage multiple AI providers.
- Implemented `Groq_AI_Conversation_Manager` for managing conversation IDs and context hashes.
- Added `Groq_AI_Generation_Logger` for logging AI generation events and managing log tables.
- Developed `Groq_AI_Prompt_Builder` for constructing prompts and processing AI responses.
- Established `Groq_AI_Settings_Manager` for managing plugin settings, including context fields and module configurations.
This commit is contained in:
Roberto Guagliardo
2025-12-05 23:58:15 +01:00
commit 5171f93a93
26 changed files with 4040 additions and 0 deletions

View File

@@ -0,0 +1,160 @@
<?php
if ( ! class_exists( 'WP_List_Table' ) ) {
require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
}
class Groq_AI_Logs_Table extends WP_List_Table {
/** @var Groq_AI_Product_Text_Plugin */
private $plugin;
/** @var string */
private $table;
/** @var string */
private $posts_table;
public function __construct( Groq_AI_Product_Text_Plugin $plugin ) {
$this->plugin = $plugin;
global $wpdb;
$this->table = $wpdb->prefix . 'groq_ai_generation_logs';
$this->posts_table = $wpdb->posts;
parent::__construct(
[
'singular' => 'groq_ai_log',
'plural' => 'groq_ai_logs',
'ajax' => false,
]
);
}
public function get_columns() {
return [
'created_at' => __( 'Datum', 'groq-ai-product-text' ),
'user_id' => __( 'Gebruiker', 'groq-ai-product-text' ),
'post_title' => __( 'Product', 'groq-ai-product-text' ),
'provider' => __( 'Provider', 'groq-ai-product-text' ),
'model' => __( 'Model', 'groq-ai-product-text' ),
'status' => __( 'Status', 'groq-ai-product-text' ),
'tokens_total' => __( 'Tokens', 'groq-ai-product-text' ),
];
}
protected function get_sortable_columns() {
return [
'created_at' => [ 'created_at', true ],
'provider' => [ 'provider', false ],
'model' => [ 'model', false ],
'status' => [ 'status', false ],
];
}
protected function get_default_primary_column_name() {
return 'created_at';
}
public function prepare_items() {
global $wpdb;
$per_page = 20;
$current_page = $this->get_pagenum();
$offset = ( $current_page - 1 ) * $per_page;
$orderby = isset( $_REQUEST['orderby'] ) ? sanitize_sql_orderby( wp_unslash( $_REQUEST['orderby'] ) ) : 'created_at';
if ( ! $orderby ) {
$orderby = 'created_at';
}
$order = isset( $_REQUEST['order'] ) ? strtoupper( sanitize_text_field( wp_unslash( $_REQUEST['order'] ) ) ) : 'DESC';
$order = in_array( $order, [ 'ASC', 'DESC' ], true ) ? $order : 'DESC';
$search = isset( $_REQUEST['s'] ) ? wp_unslash( trim( $_REQUEST['s'] ) ) : '';
$where = '1=1';
$params = [];
if ( $search ) {
$like = '%' . $wpdb->esc_like( $search ) . '%';
$where .= ' AND (provider LIKE %s OR model LIKE %s OR prompt LIKE %s OR response LIKE %s OR error_message LIKE %s )';
$params = array_merge( $params, [ $like, $like, $like, $like, $like ] );
}
$total_query = "SELECT COUNT(*) FROM {$this->table} l LEFT JOIN {$this->posts_table} p ON p.ID = l.post_id WHERE {$where}";
$total_items = (int) $wpdb->get_var(
$params ? $wpdb->prepare( $total_query, $params ) : $total_query
);
$query = "SELECT l.*, p.post_title FROM {$this->table} l LEFT JOIN {$this->posts_table} p ON p.ID = l.post_id WHERE {$where} ORDER BY {$orderby} {$order} LIMIT %d OFFSET %d";
$params_with_limits = array_merge( $params, [ $per_page, $offset ] );
$this->_column_headers = [ $this->get_columns(), [], $this->get_sortable_columns() ];
$this->items = $params
? $wpdb->get_results( $wpdb->prepare( $query, $params_with_limits ), ARRAY_A )
: $wpdb->get_results( $wpdb->prepare( $query, $per_page, $offset ), ARRAY_A );
$this->set_pagination_args(
[
'total_items' => $total_items,
'per_page' => $per_page,
'total_pages' => ceil( $total_items / $per_page ),
]
);
}
protected function column_default( $item, $column_name ) {
switch ( $column_name ) {
case 'created_at':
return esc_html( mysql2date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $item['created_at'] ) );
case 'provider':
case 'model':
case 'status':
return esc_html( $item[ $column_name ] );
case 'tokens_total':
return isset( $item['tokens_total'] ) ? absint( $item['tokens_total'] ) : '—';
case 'post_title':
if ( ! $item['post_id'] ) {
return '—';
}
$title = $item['post_title'] ? $item['post_title'] : sprintf( __( 'Product #%d', 'groq-ai-product-text' ), (int) $item['post_id'] );
$link = get_edit_post_link( $item['post_id'] );
return $link ? sprintf( '<a href="%s">%s</a>', esc_url( $link ), esc_html( $title ) ) : esc_html( $title );
case 'user_id':
if ( empty( $item['user_id'] ) ) {
return '—';
}
$user = get_userdata( $item['user_id'] );
return $user ? esc_html( $user->display_name ) : (int) $item['user_id'];
case 'error_message':
return $item['error_message'] ? esc_html( $item['error_message'] ) : '—';
default:
return isset( $item[ $column_name ] ) ? esc_html( $item[ $column_name ] ) : '';
}
}
public function no_items() {
esc_html_e( 'Nog geen AI-logboeken gevonden.', 'groq-ai-product-text' );
}
protected function column_created_at( $item ) {
$date = esc_html( mysql2date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $item['created_at'] ) );
$payload = [
'created_at' => $item['created_at'],
'user' => $this->column_default( $item, 'user_id' ),
'post_title' => $item['post_title'],
'provider' => $item['provider'],
'model' => $item['model'],
'status' => $item['status'],
'tokens_prompt' => isset( $item['tokens_prompt'] ) ? (int) $item['tokens_prompt'] : null,
'tokens_completion' => isset( $item['tokens_completion'] ) ? (int) $item['tokens_completion'] : null,
'tokens_total' => isset( $item['tokens_total'] ) ? (int) $item['tokens_total'] : null,
'prompt' => $item['prompt'],
'response' => $item['response'],
'error_message' => $item['error_message'],
];
$encoded = esc_attr( wp_json_encode( $payload ) );
return sprintf(
'<a href="#" class="groq-ai-log-row" data-groq-log="%s">%s</a>',
$encoded,
$date
);
}
}

View File

@@ -0,0 +1,212 @@
<?php
class Groq_AI_Product_Text_Product_UI {
private $plugin;
public function __construct( $plugin ) {
$this->plugin = $plugin;
add_action( 'add_meta_boxes', [ $this, 'register_meta_box' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_assets' ] );
add_action( 'admin_footer', [ $this, 'render_modal_markup' ] );
}
public function register_meta_box() {
add_meta_box(
'groq-ai-generator-box',
__( 'Gebruik AI', 'groq-ai-product-text' ),
[ $this, 'render_meta_box' ],
'product',
'side',
'high'
);
}
public function render_meta_box() {
if ( ! current_user_can( 'edit_products' ) ) {
echo '<p>' . esc_html__( 'Je hebt geen toestemming om deze actie uit te voeren.', 'groq-ai-product-text' ) . '</p>';
return;
}
?>
<p><?php esc_html_e( 'Laat de geselecteerde AI een concepttekst genereren op basis van een prompt.', 'groq-ai-product-text' ); ?></p>
<button type="button" class="button button-primary groq-ai-open-modal" data-target="groq-ai-modal"><?php esc_html_e( 'Gebruik AI', 'groq-ai-product-text' ); ?></button>
<p class="description" style="margin-top:8px;">
<?php esc_html_e( 'Klik om een prompt in te voeren en een voorsteltekst te genereren. Plak het resultaat in de beschrijving of korte beschrijving.', 'groq-ai-product-text' ); ?>
</p>
<?php
}
public function enqueue_admin_assets( $hook ) {
$screen = get_current_screen();
if ( $screen && 'product' === $screen->post_type && in_array( $screen->base, [ 'post', 'post-new' ], true ) ) {
wp_enqueue_style(
'groq-ai-admin',
plugins_url( 'assets/css/admin.css', GROQ_AI_PRODUCT_TEXT_FILE ),
[],
GROQ_AI_PRODUCT_TEXT_VERSION
);
wp_enqueue_script(
'groq-ai-admin',
plugins_url( 'assets/js/admin.js', GROQ_AI_PRODUCT_TEXT_FILE ),
[ 'jquery' ],
GROQ_AI_PRODUCT_TEXT_VERSION,
true
);
global $post;
$post_id = ( $post && isset( $post->ID ) ) ? (int) $post->ID : 0;
$settings = $this->plugin->get_settings();
wp_localize_script(
'groq-ai-admin',
'GroqAIGenerator',
[
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'groq_ai_generate' ),
'defaultPrompt' => $settings['default_prompt'],
'postId' => $post_id,
'contextDefaults' => isset( $settings['context_fields'] ) ? $settings['context_fields'] : $this->plugin->get_default_context_fields(),
]
);
}
}
public function render_modal_markup() {
$screen = get_current_screen();
if ( ! $screen || 'product' !== $screen->post_type ) {
return;
}
$settings = $this->plugin->get_settings();
$rankmath_enabled = $this->plugin->is_rankmath_active() && $this->plugin->is_module_enabled( 'rankmath', $settings );
?>
<div id="groq-ai-modal" class="groq-ai-modal" aria-hidden="true">
<div class="groq-ai-modal__dialog" role="dialog" aria-modal="true" aria-labelledby="groq-ai-modal-title">
<button type="button" class="groq-ai-modal__close" aria-label="<?php esc_attr_e( 'Sluiten', 'groq-ai-product-text' ); ?>">&times;</button>
<div class="groq-ai-modal__dialog-inner">
<h2 id="groq-ai-modal-title"><?php esc_html_e( 'Siti AI prompt', 'groq-ai-product-text' ); ?></h2>
<form id="groq-ai-form">
<label for="groq-ai-prompt" class="screen-reader-text"><?php esc_html_e( 'Prompt', 'groq-ai-product-text' ); ?></label>
<textarea id="groq-ai-prompt" rows="6" placeholder="<?php esc_attr_e( 'Beschrijf hier wat de AI moet schrijven...', 'groq-ai-product-text' ); ?>"></textarea>
<div class="groq-ai-modal__actions">
<button type="submit" class="button button-primary">
<?php esc_html_e( 'Genereer tekst', 'groq-ai-product-text' ); ?>
</button>
</div>
<div class="groq-ai-advanced-settings">
<button type="button" class="groq-ai-advanced-toggle" aria-expanded="false" aria-controls="groq-ai-advanced-panel">
<span class="groq-ai-advanced-toggle__icon" aria-hidden="true"></span>
<?php esc_html_e( 'Geavanceerde instellingen', 'groq-ai-product-text' ); ?>
</button>
<div id="groq-ai-advanced-panel" class="groq-ai-context-options" hidden>
<h3><?php esc_html_e( 'Gebruik deze productinformatie in de prompt', 'groq-ai-product-text' ); ?></h3>
<p class="description"><?php esc_html_e( 'Je kunt tijdelijk onderdelen uitzetten of weer inschakelen. Standaard zijn de opties aangevinkt zoals ingesteld op de instellingenpagina.', 'groq-ai-product-text' ); ?></p>
<div class="groq-ai-context-options__grid">
<?php
$context_definitions = $this->plugin->get_context_field_definitions();
$context_defaults = isset( $settings['context_fields'] ) ? $settings['context_fields'] : $this->plugin->get_default_context_fields();
foreach ( $context_definitions as $context_key => $context_info ) :
$checked = ! empty( $context_defaults[ $context_key ] );
?>
<label class="groq-ai-context-option">
<input type="checkbox" class="groq-ai-context-toggle" data-field="<?php echo esc_attr( $context_key ); ?>" <?php checked( $checked ); ?> />
<div>
<strong><?php echo esc_html( $context_info['label'] ); ?></strong>
<?php if ( ! empty( $context_info['description'] ) ) : ?>
<p class="description"><?php echo esc_html( $context_info['description'] ); ?></p>
<?php endif; ?>
</div>
</label>
<?php endforeach; ?>
</div>
</div>
</div>
</form>
<div class="groq-ai-modal__result" hidden>
<h3><?php esc_html_e( 'Resultaat', 'groq-ai-product-text' ); ?></h3>
<div class="groq-ai-result-grid">
<div class="groq-ai-result-field" data-field="title" data-target-input="#title" data-label="<?php esc_attr_e( 'Producttitel', 'groq-ai-product-text' ); ?>">
<div class="groq-ai-result-field__header">
<strong><?php esc_html_e( 'Producttitel', 'groq-ai-product-text' ); ?></strong>
<div class="groq-ai-result-field__actions">
<button type="button" class="button button-secondary groq-ai-copy-field" data-field="title"><?php esc_html_e( 'Kopieer', 'groq-ai-product-text' ); ?></button>
<button type="button" class="button groq-ai-apply-field" data-field="title"><?php esc_html_e( 'Vul titel in', 'groq-ai-product-text' ); ?></button>
<span class="groq-ai-apply-status" aria-hidden="true"></span>
</div>
</div>
<textarea rows="2"></textarea>
</div>
<div class="groq-ai-result-field" data-field="short_description" data-target-input="#excerpt" data-label="<?php esc_attr_e( 'Korte beschrijving', 'groq-ai-product-text' ); ?>">
<div class="groq-ai-result-field__header">
<strong><?php esc_html_e( 'Korte beschrijving', 'groq-ai-product-text' ); ?></strong>
<div class="groq-ai-result-field__actions">
<button type="button" class="button button-secondary groq-ai-copy-field" data-field="short_description"><?php esc_html_e( 'Kopieer', 'groq-ai-product-text' ); ?></button>
<button type="button" class="button groq-ai-apply-field" data-field="short_description"><?php esc_html_e( 'Vul korte beschrijving in', 'groq-ai-product-text' ); ?></button>
<span class="groq-ai-apply-status" aria-hidden="true"></span>
</div>
</div>
<textarea rows="3"></textarea>
</div>
<div class="groq-ai-result-field" data-field="description" data-target-input="#content" data-label="<?php esc_attr_e( 'Beschrijving', 'groq-ai-product-text' ); ?>">
<div class="groq-ai-result-field__header">
<strong><?php esc_html_e( 'Beschrijving', 'groq-ai-product-text' ); ?></strong>
<div class="groq-ai-result-field__actions">
<button type="button" class="button button-secondary groq-ai-copy-field" data-field="description"><?php esc_html_e( 'Kopieer', 'groq-ai-product-text' ); ?></button>
<button type="button" class="button groq-ai-apply-field" data-field="description"><?php esc_html_e( 'Vul beschrijving in', 'groq-ai-product-text' ); ?></button>
<span class="groq-ai-apply-status" aria-hidden="true"></span>
</div>
</div>
<textarea rows="6"></textarea>
</div>
<?php if ( $rankmath_enabled ) : ?>
<div class="groq-ai-result-field" data-field="meta_title" data-target-input="#rank_math_title" data-rankmath-action="updateTitle" data-label="<?php esc_attr_e( 'Rank Math meta titel', 'groq-ai-product-text' ); ?>">
<div class="groq-ai-result-field__header">
<strong><?php esc_html_e( 'Rank Math meta titel', 'groq-ai-product-text' ); ?></strong>
<div class="groq-ai-result-field__actions">
<button type="button" class="button button-secondary groq-ai-copy-field" data-field="meta_title"><?php esc_html_e( 'Kopieer', 'groq-ai-product-text' ); ?></button>
<button type="button" class="button groq-ai-apply-field" data-field="meta_title"><?php esc_html_e( 'Vul meta titel in', 'groq-ai-product-text' ); ?></button>
<span class="groq-ai-apply-status" aria-hidden="true"></span>
</div>
</div>
<textarea rows="2"></textarea>
</div>
<div class="groq-ai-result-field" data-field="meta_description" data-target-input="#rank_math_description" data-rankmath-action="updateDescription" data-label="<?php esc_attr_e( 'Rank Math meta description', 'groq-ai-product-text' ); ?>">
<div class="groq-ai-result-field__header">
<strong><?php esc_html_e( 'Rank Math meta description', 'groq-ai-product-text' ); ?></strong>
<div class="groq-ai-result-field__actions">
<button type="button" class="button button-secondary groq-ai-copy-field" data-field="meta_description"><?php esc_html_e( 'Kopieer', 'groq-ai-product-text' ); ?></button>
<button type="button" class="button groq-ai-apply-field" data-field="meta_description"><?php esc_html_e( 'Vul meta description in', 'groq-ai-product-text' ); ?></button>
<span class="groq-ai-apply-status" aria-hidden="true"></span>
</div>
</div>
<textarea rows="3"></textarea>
</div>
<div class="groq-ai-result-field" data-field="focus_keywords" data-target-input="#rank_math_focus_keyword" data-rankmath-action="updateKeywords" data-label="<?php esc_attr_e( 'Rank Math focus keyphrase', 'groq-ai-product-text' ); ?>">
<div class="groq-ai-result-field__header">
<strong><?php esc_html_e( 'Rank Math focus keyphrase', 'groq-ai-product-text' ); ?></strong>
<div class="groq-ai-result-field__actions">
<button type="button" class="button button-secondary groq-ai-copy-field" data-field="focus_keywords"><?php esc_html_e( 'Kopieer', 'groq-ai-product-text' ); ?></button>
<button type="button" class="button groq-ai-apply-field" data-field="focus_keywords"><?php esc_html_e( 'Vul focus keyphrase in', 'groq-ai-product-text' ); ?></button>
<span class="groq-ai-apply-status" aria-hidden="true"></span>
</div>
</div>
<textarea rows="2" placeholder="<?php esc_attr_e( 'bijv. luxe massage apparaat, wellness cadeau', 'groq-ai-product-text' ); ?>"></textarea>
</div>
<?php endif; ?>
</div>
<div class="groq-ai-modal__raw">
<h4><?php esc_html_e( 'Ruwe JSON-output', 'groq-ai-product-text' ); ?></h4>
<pre id="groq-ai-output"></pre>
<button type="button" class="button groq-ai-copy-json"><?php esc_html_e( 'Kopieer JSON', 'groq-ai-product-text' ); ?></button>
</div>
</div>
<div class="groq-ai-modal__status" aria-live="polite"></div>
</div>
</div>
</div>
<?php
}
}

View File

@@ -0,0 +1,569 @@
<?php
class Groq_AI_Product_Text_Settings_Page {
private $plugin;
private $provider_manager;
public function __construct( $plugin, Groq_AI_Provider_Manager $provider_manager ) {
$this->plugin = $plugin;
$this->provider_manager = $provider_manager;
add_action( 'admin_menu', [ $this, 'register_settings_pages' ] );
add_action( 'admin_init', [ $this, 'register_settings' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_settings_assets' ] );
add_action( 'admin_head', [ $this, 'hide_menu_links' ] );
}
public function register_settings_pages() {
add_options_page(
__( 'Siti AI Productteksten', 'groq-ai-product-text' ),
__( 'Siti AI', 'groq-ai-product-text' ),
'manage_options',
'groq-ai-product-text',
[ $this, 'render_settings_page' ]
);
add_submenu_page(
'options-general.php',
__( 'Siti AI Modules', 'groq-ai-product-text' ),
__( 'Siti AI Modules', 'groq-ai-product-text' ),
'manage_options',
'groq-ai-product-text-modules',
[ $this, 'render_modules_page' ]
);
add_submenu_page(
'options-general.php',
__( 'Siti AI AI-logboek', 'groq-ai-product-text' ),
__( 'Siti AI AI-logboek', 'groq-ai-product-text' ),
'manage_options',
'groq-ai-product-text-logs',
[ $this, 'render_logs_page' ]
);
}
public function hide_menu_links() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
?>
<style>
#adminmenu a[href="options-general.php?page=groq-ai-product-text-modules"],
#adminmenu a[href="options-general.php?page=groq-ai-product-text-logs"] {
display: none !important;
}
</style>
<?php
}
public function register_settings() {
register_setting( 'groq_ai_product_text_group', $this->plugin->get_option_key(), [ $this->plugin, 'sanitize_settings' ] );
add_settings_section(
'groq_ai_product_text_general',
__( 'Algemene instellingen', 'groq-ai-product-text' ),
'__return_false',
'groq-ai-product-text'
);
add_settings_field(
'groq_ai_provider',
__( 'AI-aanbieder', 'groq-ai-product-text' ),
[ $this, 'render_provider_field' ],
'groq-ai-product-text',
'groq_ai_product_text_general'
);
add_settings_field(
'groq_ai_model',
__( 'Model', 'groq-ai-product-text' ),
[ $this, 'render_model_field' ],
'groq-ai-product-text',
'groq_ai_product_text_general'
);
foreach ( $this->provider_manager->get_providers() as $provider ) {
add_settings_field(
'groq_ai_api_key_' . $provider->get_key(),
sprintf( __( '%s API-sleutel', 'groq-ai-product-text' ), $provider->get_label() ),
[ $this, 'render_provider_api_key_field' ],
'groq-ai-product-text',
'groq_ai_product_text_general',
[
'provider' => $provider,
]
);
}
add_settings_field(
'groq_ai_store_context',
__( 'Winkelcontext', 'groq-ai-product-text' ),
[ $this, 'render_store_context_field' ],
'groq-ai-product-text',
'groq_ai_product_text_general'
);
add_settings_field(
'groq_ai_default_prompt',
__( 'Standaard prompt', 'groq-ai-product-text' ),
[ $this, 'render_default_prompt_field' ],
'groq-ai-product-text',
'groq_ai_product_text_general'
);
add_settings_field(
'groq_ai_context_fields',
__( 'Standaard productcontext', 'groq-ai-product-text' ),
[ $this, 'render_context_fields_field' ],
'groq-ai-product-text',
'groq_ai_product_text_general'
);
add_settings_field(
'groq_ai_response_format_compat',
__( 'Response-format compatibiliteit', 'groq-ai-product-text' ),
[ $this, 'render_response_format_compat_field' ],
'groq-ai-product-text',
'groq_ai_product_text_general'
);
add_settings_section(
'groq_ai_product_text_modules_rankmath',
__( 'Rank Math SEO', 'groq-ai-product-text' ),
'__return_false',
'groq-ai-product-text-modules'
);
add_settings_field(
'groq_ai_module_rankmath',
__( 'Rank Math SEO', 'groq-ai-product-text' ),
[ $this, 'render_rankmath_module_field' ],
'groq-ai-product-text-modules',
'groq_ai_product_text_modules_rankmath'
);
}
public function render_settings_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$settings = $this->plugin->get_settings();
?>
<div class="wrap">
<h1><?php esc_html_e( 'Siti AI Productteksten', 'groq-ai-product-text' ); ?></h1>
<p style="margin-bottom:16px;">
<a href="<?php echo esc_url( admin_url( 'admin.php?page=groq-ai-product-text-modules' ) ); ?>" class="button button-secondary">
<?php esc_html_e( 'Ga naar modules', 'groq-ai-product-text' ); ?>
</a>
<a href="<?php echo esc_url( admin_url( 'admin.php?page=groq-ai-product-text-logs' ) ); ?>" class="button">
<?php esc_html_e( 'Bekijk AI-logboek', 'groq-ai-product-text' ); ?>
</a>
</p>
<p><?php esc_html_e( 'Kies je AI-aanbieder, stel de juiste API-sleutel en het gewenste model in en beheer optionele winkelcontext of standaard prompt.', 'groq-ai-product-text' ); ?></p>
<form action="options.php" method="post">
<?php
settings_fields( 'groq_ai_product_text_group' );
do_settings_sections( 'groq-ai-product-text' );
submit_button();
?>
</form>
<div class="groq-ai-prompt-helper">
<h2><?php esc_html_e( 'Prompt generator', 'groq-ai-product-text' ); ?></h2>
<p><?php esc_html_e( 'Gebruik deze velden om belangrijke informatie voor de AI bij te houden (bijvoorbeeld tone of voice, USPs of doelgroepen). Voeg ze toe aan je prompt met kopiëren en plakken.', 'groq-ai-product-text' ); ?></p>
<textarea class="large-text" rows="6" readonly><?php echo esc_textarea( $this->plugin->build_prompt_template_preview( $settings ) ); ?></textarea>
</div>
</div>
<?php
}
public function render_modules_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
?>
<div class="wrap">
<h1><?php esc_html_e( 'Siti AI Modules', 'groq-ai-product-text' ); ?></h1>
<p><?php esc_html_e( 'Beheer aparte integraties zoals Rank Math. Het uitschakelen van een module verwijdert de bijbehorende AI-uitvoer automatisch uit de productmodal.', 'groq-ai-product-text' ); ?></p>
<form action="options.php" method="post">
<?php
settings_fields( 'groq_ai_product_text_group' );
do_settings_sections( 'groq-ai-product-text-modules' );
submit_button();
?>
</form>
</div>
<?php
}
public function render_logs_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$logs_table = new Groq_AI_Logs_Table( $this->plugin );
$logs_table->prepare_items();
?>
<div class="wrap">
<h1><?php esc_html_e( 'AI-logboek', 'groq-ai-product-text' ); ?></h1>
<p><?php esc_html_e( 'Bekijk recente AI-generaties inclusief status, gebruiker en tokens.', 'groq-ai-product-text' ); ?></p>
<form method="get">
<input type="hidden" name="page" value="groq-ai-product-text-logs" />
<?php $logs_table->search_box( __( 'Zoek logs', 'groq-ai-product-text' ), 'groq-ai-logs' ); ?>
<?php $logs_table->display(); ?>
</form>
</div>
<div id="groq-ai-log-modal" class="groq-ai-log-modal" aria-hidden="true">
<div class="groq-ai-log-modal__dialog" role="dialog" aria-modal="true" aria-labelledby="groq-ai-log-modal-title">
<button type="button" class="groq-ai-log-modal__close" aria-label="<?php esc_attr_e( 'Sluiten', 'groq-ai-product-text' ); ?>">&times;</button>
<div class="groq-ai-log-modal__content">
<h2 id="groq-ai-log-modal-title"><?php esc_html_e( 'Logdetails', 'groq-ai-product-text' ); ?></h2>
<p class="description groq-ai-log-meta"></p>
<div class="groq-ai-log-fields">
<label>
<span><?php esc_html_e( 'Prompt', 'groq-ai-product-text' ); ?></span>
<textarea id="groq-ai-log-prompt" readonly rows="6"></textarea>
</label>
<label>
<span><?php esc_html_e( 'Response', 'groq-ai-product-text' ); ?></span>
<textarea id="groq-ai-log-response" readonly rows="6"></textarea>
</label>
<div class="groq-ai-log-tokens">
<div>
<strong><?php esc_html_e( 'Tokens prompt', 'groq-ai-product-text' ); ?></strong>
<span id="groq-ai-log-tokens-prompt">—</span>
</div>
<div>
<strong><?php esc_html_e( 'Tokens response', 'groq-ai-product-text' ); ?></strong>
<span id="groq-ai-log-tokens-completion">—</span>
</div>
<div>
<strong><?php esc_html_e( 'Tokens totaal', 'groq-ai-product-text' ); ?></strong>
<span id="groq-ai-log-tokens-total">—</span>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.groq-ai-log-modal{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.65);display:none;align-items:center;justify-content:center;z-index:100000;}
.groq-ai-log-modal.is-open{display:flex;}
.groq-ai-log-modal__dialog{background:#fff;max-width:900px;width:90%;padding:20px;box-shadow:0 10px 40px rgba(0,0,0,0.3);position:relative;}
.groq-ai-log-modal__close{position:absolute;top:10px;right:10px;border:none;background:transparent;font-size:24px;cursor:pointer;}
.groq-ai-log-fields label{display:block;margin-bottom:15px;}
.groq-ai-log-fields textarea{width:100%;}
.groq-ai-log-tokens{display:flex;gap:20px;margin-top:10px;}
.groq-ai-log-row{display:inline-block;}
</style>
<script>
(function(){
const modal=document.getElementById('groq-ai-log-modal');
if(!modal){return;}
const closeBtn=modal.querySelector('.groq-ai-log-modal__close');
const promptField=document.getElementById('groq-ai-log-prompt');
const responseField=document.getElementById('groq-ai-log-response');
const tokensPrompt=document.getElementById('groq-ai-log-tokens-prompt');
const tokensCompletion=document.getElementById('groq-ai-log-tokens-completion');
const tokensTotal=document.getElementById('groq-ai-log-tokens-total');
const meta=document.querySelector('.groq-ai-log-meta');
function openModal(data){
if(!data){return;}
if(promptField){promptField.value=data.prompt||'';}
if(responseField){responseField.value=data.response||'';}
if(tokensPrompt){tokensPrompt.textContent=Number.isFinite(data.tokens_prompt)?data.tokens_prompt:'—';}
if(tokensCompletion){tokensCompletion.textContent=Number.isFinite(data.tokens_completion)?data.tokens_completion:'—';}
if(tokensTotal){tokensTotal.textContent=Number.isFinite(data.tokens_total)?data.tokens_total:'—';}
if(meta){
meta.textContent=(data.provider||'')+' • '+(data.model||'')+' • '+(data.post_title||'')+' • '+(data.status||'');
}
modal.classList.add('is-open');
modal.setAttribute('aria-hidden','false');
}
function closeModal(){
modal.classList.remove('is-open');
modal.setAttribute('aria-hidden','true');
}
document.addEventListener('click',function(e){
const link=e.target.closest('.groq-ai-log-row');
if(link){
e.preventDefault();
let payload=link.getAttribute('data-groq-log');
if(payload){
try{
const data=JSON.parse(payload);
openModal(data);
}catch(err){
console.error('Invalid log payload',err);
}
}
}
if(e.target===modal){
closeModal();
}
});
if(closeBtn){
closeBtn.addEventListener('click',closeModal);
}
document.addEventListener('keyup',function(e){
if(e.key==='Escape' && modal.classList.contains('is-open')){
closeModal();
}
});
})();
</script>
<?php
}
public function render_provider_field() {
$settings = $this->plugin->get_settings();
$providers = $this->provider_manager->get_providers();
?>
<select name="<?php echo esc_attr( $this->plugin->get_option_key() ); ?>[provider]">
<?php foreach ( $providers as $provider ) : ?>
<option value="<?php echo esc_attr( $provider->get_key() ); ?>" <?php selected( $settings['provider'], $provider->get_key() ); ?>>
<?php echo esc_html( $provider->get_label() ); ?>
</option>
<?php endforeach; ?>
</select>
<p class="description"><?php esc_html_e( 'Bepaal welke AI-dienst wordt aangesproken wanneer je teksten genereert.', 'groq-ai-product-text' ); ?></p>
<?php
}
public function render_model_field() {
$settings = $this->plugin->get_settings();
$current_model = $settings['model'];
$current_provider = $settings['provider'];
?>
<div class="groq-ai-model-field">
<select
id="groq-ai-model-select"
class="groq-ai-model-select"
name="<?php echo esc_attr( $this->plugin->get_option_key() ); ?>[model]"
data-current-model="<?php echo esc_attr( $current_model ); ?>"
>
<option value=""><?php esc_html_e( 'Selecteer een model via "Live modellen ophalen"', 'groq-ai-product-text' ); ?></option>
</select>
<p class="description"><?php esc_html_e( 'Gebruik de knop hieronder om rechtstreeks via het API-endpoint beschikbare modellen op te halen. Zonder een live lijst blijft de selectie leeg.', 'groq-ai-product-text' ); ?></p>
<button type="button" class="button" id="groq-ai-refresh-models" style="margin-top:10px;">
<?php esc_html_e( 'Live modellen ophalen', 'groq-ai-product-text' ); ?>
</button>
<p id="groq-ai-refresh-models-status" class="description" aria-live="polite"></p>
</div>
<?php
}
public function render_provider_api_key_field( $args ) {
$settings = $this->plugin->get_settings();
/** @var Groq_AI_Provider_Interface $provider */
$provider = $args['provider'];
$field = $provider->get_option_key();
$provider_key = $provider->get_key();
?>
<div class="groq-ai-provider-field" data-provider-row="<?php echo esc_attr( $provider_key ); ?>">
<input type="password" name="<?php echo esc_attr( $this->plugin->get_option_key() ); ?>[<?php echo esc_attr( $field ); ?>]" value="<?php echo esc_attr( $settings[ $field ] ); ?>" class="regular-text" autocomplete="off" />
<p class="description">
<?php
printf(
/* translators: %s: provider name */
esc_html__( 'Voeg hier de API-sleutel voor %s toe.', 'groq-ai-product-text' ),
esc_html( $provider->get_label() )
);
?>
</p>
</div>
<?php
}
public function render_store_context_field() {
$settings = $this->plugin->get_settings();
?>
<textarea name="<?php echo esc_attr( $this->plugin->get_option_key() ); ?>[store_context]" class="large-text" rows="4"><?php echo esc_textarea( $settings['store_context'] ); ?></textarea>
<p class="description"><?php esc_html_e( 'Beschrijf het merk, de tone of voice en andere relevante winkelinformatie.', 'groq-ai-product-text' ); ?></p>
<?php
}
public function render_default_prompt_field() {
$settings = $this->plugin->get_settings();
?>
<textarea name="<?php echo esc_attr( $this->plugin->get_option_key() ); ?>[default_prompt]" class="large-text" rows="4" placeholder="<?php esc_attr_e( 'Bijvoorbeeld: Schrijf een overtuigende productbeschrijving met nadruk op kwaliteit en levertijd.', 'groq-ai-product-text' ); ?>"><?php echo esc_textarea( $settings['default_prompt'] ); ?></textarea>
<p class="description"><?php esc_html_e( 'Deze tekst verschijnt vooraf ingevuld in de AI-popup, maar kan per product worden aangepast.', 'groq-ai-product-text' ); ?></p>
<?php
}
public function render_context_fields_field() {
$settings = $this->plugin->get_settings();
$values = isset( $settings['context_fields'] ) ? $settings['context_fields'] : $this->plugin->get_default_context_fields();
$definitions = $this->plugin->get_context_field_definitions();
?>
<div class="groq-ai-context-defaults">
<?php foreach ( $definitions as $key => $definition ) :
$checked = ! empty( $values[ $key ] );
?>
<label>
<input type="checkbox" name="<?php echo esc_attr( $this->plugin->get_option_key() ); ?>[context_fields][<?php echo esc_attr( $key ); ?>]" value="1" <?php checked( $checked ); ?> />
<strong><?php echo esc_html( $definition['label'] ); ?></strong>
</label>
<?php if ( ! empty( $definition['description'] ) ) : ?>
<p class="description" style="margin-top:-8px;margin-bottom:12px;">
<?php echo esc_html( $definition['description'] ); ?>
</p>
<?php endif; ?>
<?php endforeach; ?>
</div>
<?php
}
public function render_response_format_compat_field() {
$settings = $this->plugin->get_settings();
$is_enabled = ! empty( $settings['response_format_compat'] );
?>
<label>
<input type="checkbox" name="<?php echo esc_attr( $this->plugin->get_option_key() ); ?>[response_format_compat]" value="1" <?php checked( $is_enabled ); ?> />
<?php esc_html_e( 'Compatibele modus inschakelen (instructies toevoegen aan de prompt).', 'groq-ai-product-text' ); ?>
</label>
<p class="description">
<?php esc_html_e( 'Standaard gebruikt de plugin de response_format-functie van aanbieders zoals Groq en OpenAI voor gegarandeerde JSON-uitvoer. Schakel deze optie alleen in wanneer je problemen ervaart met oudere modellen of eigen integraties die deze functie niet ondersteunen.', 'groq-ai-product-text' ); ?>
</p>
<?php
}
public function render_rankmath_module_field() {
$settings = $this->plugin->get_settings();
$defaults = $this->plugin->get_default_modules_settings();
$modules = isset( $settings['modules'] ) ? $settings['modules'] : $defaults;
$config = isset( $modules['rankmath'] ) ? $modules['rankmath'] : ( $defaults['rankmath'] ?? [] );
$rankmath_active = $this->plugin->is_rankmath_active();
$enabled = $rankmath_active && ! empty( $config['enabled'] );
$keyword_limit = isset( $config['focus_keyword_limit'] ) ? absint( $config['focus_keyword_limit'] ) : ( $defaults['rankmath']['focus_keyword_limit'] ?? 3 );
$keyword_limit = $keyword_limit > 0 ? $keyword_limit : 3;
$title_pixels = isset( $config['meta_title_pixel_limit'] ) ? absint( $config['meta_title_pixel_limit'] ) : ( $defaults['rankmath']['meta_title_pixel_limit'] ?? 580 );
$title_pixels = $title_pixels > 0 ? $title_pixels : 580;
$pixel_limit = isset( $config['meta_description_pixel_limit'] ) ? absint( $config['meta_description_pixel_limit'] ) : ( $defaults['rankmath']['meta_description_pixel_limit'] ?? 920 );
$pixel_limit = $pixel_limit > 0 ? $pixel_limit : 920;
$rankmath_active = $this->plugin->is_rankmath_active();
?>
<div class="groq-ai-module-field">
<input type="hidden" name="<?php echo esc_attr( $this->plugin->get_option_key() ); ?>[modules][rankmath][enabled]" value="0" />
<label>
<input type="checkbox" name="<?php echo esc_attr( $this->plugin->get_option_key() ); ?>[modules][rankmath][enabled]" value="1" <?php checked( $enabled ); ?> <?php disabled( ! $rankmath_active ); ?> />
<?php esc_html_e( 'Activeer Rank Math integratie (meta title, meta description en focus keywords genereren).', 'groq-ai-product-text' ); ?>
</label>
<p class="description" style="margin-top:4px;">
<?php
if ( ! $rankmath_active ) {
esc_html_e( 'Installeer en activeer Rank Math om deze opties te gebruiken. Velden zijn momenteel alleen-lezen.', 'groq-ai-product-text' );
} else {
esc_html_e( 'Wanneer ingeschakeld worden extra velden in de AI-modal getoond en automatisch gekoppeld aan Rank Math.', 'groq-ai-product-text' );
}
?>
</p>
<label for="groq-ai-rankmath-keywords">
<?php esc_html_e( 'Aantal focus keywords om te genereren', 'groq-ai-product-text' ); ?>
</label>
<input
type="number"
id="groq-ai-rankmath-keywords"
min="1"
max="99"
name="<?php echo esc_attr( $this->plugin->get_option_key() ); ?>[modules][rankmath][focus_keyword_limit]"
value="<?php echo esc_attr( $keyword_limit ); ?>"
style="width: 80px;"
<?php disabled( ! $rankmath_active ); ?>
/>
<p class="description">
<?php esc_html_e( 'Bepaal hoeveel zoekwoorden de AI maximaal mag teruggeven (bijvoorbeeld 3).', 'groq-ai-product-text' ); ?>
</p>
<label for="groq-ai-rankmath-title-pixels">
<?php esc_html_e( 'Maximale meta title breedte (pixels)', 'groq-ai-product-text' ); ?>
</label>
<input
type="number"
id="groq-ai-rankmath-title-pixels"
min="1"
max="1200"
step="1"
name="<?php echo esc_attr( $this->plugin->get_option_key() ); ?>[modules][rankmath][meta_title_pixel_limit]"
value="<?php echo esc_attr( $title_pixels ); ?>"
style="width: 100px;"
<?php disabled( ! $rankmath_active ); ?>
/>
<p class="description">
<?php esc_html_e( 'Bepaal hoe breed (in pixels) de meta title maximaal mag zijn volgens de SERP-richtlijnen.', 'groq-ai-product-text' ); ?>
</p>
<label for="groq-ai-rankmath-pixels">
<?php esc_html_e( 'Maximale meta description breedte (pixels)', 'groq-ai-product-text' ); ?>
</label>
<input
type="number"
id="groq-ai-rankmath-pixels"
min="1"
max="2000"
step="1"
name="<?php echo esc_attr( $this->plugin->get_option_key() ); ?>[modules][rankmath][meta_description_pixel_limit]"
value="<?php echo esc_attr( $pixel_limit ); ?>"
style="width: 100px;"
<?php disabled( ! $rankmath_active ); ?>
/>
<p class="description">
<?php esc_html_e( 'Gebruik het SERP-voorbeeld als referentie. De AI krijgt door dat de meta description deze pixelbreedte niet mag overschrijden.', 'groq-ai-product-text' ); ?>
</p>
</div>
<?php
}
public function enqueue_settings_assets( $hook ) {
if ( ! in_array( $hook, [ 'settings_page_groq-ai-product-text', 'settings_page_groq-ai-product-text-modules' ], true ) ) {
return;
}
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
);
wp_enqueue_script(
'groq-ai-settings',
plugins_url( 'assets/js/settings.js', GROQ_AI_PRODUCT_TEXT_FILE ),
[],
GROQ_AI_PRODUCT_TEXT_VERSION,
true
);
$current_settings = $this->plugin->get_settings();
$data = [
'optionKey' => $this->plugin->get_option_key(),
'providers' => [],
'currentProvider' => $current_settings['provider'],
'currentModel' => $current_settings['model'],
'providerRows' => [],
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'refreshNonce' => wp_create_nonce( 'groq_ai_refresh_models' ),
'placeholders' => [
'selectModel' => __( 'Selecteer een model via "Live modellen ophalen"', 'groq-ai-product-text' ),
],
];
foreach ( $this->provider_manager->get_providers() as $provider ) {
$data['providers'][ $provider->get_key() ] = [
'default_label' => sprintf( __( 'Gebruik standaardmodel (%s)', 'groq-ai-product-text' ), $provider->get_default_model() ),
'models' => [],
'supports_live' => $provider->supports_live_models(),
];
$data['providerRows'][ $provider->get_key() ] = 'groq_ai_api_key_' . $provider->get_key();
}
wp_localize_script( 'groq-ai-settings', 'GroqAISettingsData', $data );
}
}