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:
160
includes/Admin/class-groq-ai-logs-table.php
Normal file
160
includes/Admin/class-groq-ai-logs-table.php
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
212
includes/Admin/class-groq-ai-product-ui.php
Normal file
212
includes/Admin/class-groq-ai-product-ui.php
Normal 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' ); ?>">×</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
|
||||
}
|
||||
}
|
||||
569
includes/Admin/class-groq-ai-settings-page.php
Normal file
569
includes/Admin/class-groq-ai-settings-page.php
Normal 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, USP’s 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' ); ?>">×</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 );
|
||||
}
|
||||
}
|
||||
21
includes/Contracts/interface-groq-ai-provider.php
Normal file
21
includes/Contracts/interface-groq-ai-provider.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
interface Groq_AI_Provider_Interface {
|
||||
public function get_key();
|
||||
|
||||
public function get_label();
|
||||
|
||||
public function get_default_model();
|
||||
|
||||
public function get_available_models();
|
||||
|
||||
public function get_option_key();
|
||||
|
||||
public function generate_content( array $args );
|
||||
|
||||
public function supports_live_models();
|
||||
|
||||
public function fetch_live_models( $api_key );
|
||||
|
||||
public function supports_response_format();
|
||||
}
|
||||
156
includes/Core/class-groq-ai-ajax-controller.php
Normal file
156
includes/Core/class-groq-ai-ajax-controller.php
Normal file
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
class Groq_AI_Ajax_Controller {
|
||||
/** @var Groq_AI_Product_Text_Plugin */
|
||||
private $plugin;
|
||||
|
||||
public function __construct( Groq_AI_Product_Text_Plugin $plugin ) {
|
||||
$this->plugin = $plugin;
|
||||
|
||||
add_action( 'wp_ajax_groq_ai_generate_text', [ $this, 'handle_generate_text' ] );
|
||||
add_action( 'wp_ajax_groq_ai_refresh_models', [ $this, 'handle_refresh_models' ] );
|
||||
}
|
||||
|
||||
public function handle_generate_text() {
|
||||
if ( ! current_user_can( 'edit_products' ) ) {
|
||||
wp_send_json_error( [ 'message' => __( 'Je hebt geen toestemming voor deze actie.', 'groq-ai-product-text' ) ], 403 );
|
||||
}
|
||||
|
||||
check_ajax_referer( 'groq_ai_generate', 'nonce' );
|
||||
|
||||
$prompt = isset( $_POST['prompt'] ) ? sanitize_textarea_field( wp_unslash( $_POST['prompt'] ) ) : '';
|
||||
$post_id = isset( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0;
|
||||
|
||||
$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 = $prompt_builder->build_system_prompt( $settings, $conversation_id );
|
||||
$model = $this->plugin->get_selected_model( $provider, $settings );
|
||||
$context_fields = $prompt_builder->parse_context_fields_from_request( isset( $_POST['context_fields'] ) ? $_POST['context_fields'] : '', $settings );
|
||||
$product_context_text = $prompt_builder->build_product_context_block( $post_id, $context_fields );
|
||||
$prompt_with_context = $prompt_builder->prepend_context_to_prompt( $prompt, $product_context_text );
|
||||
|
||||
$response_format = null;
|
||||
$use_response_format = $this->plugin->should_use_response_format( $provider, $settings );
|
||||
if ( $use_response_format ) {
|
||||
$response_format = $prompt_builder->get_response_format_definition( $settings );
|
||||
$final_prompt = $prompt_with_context;
|
||||
} 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 ) ) {
|
||||
$this->plugin->get_generation_logger()->log_generation_event(
|
||||
[
|
||||
'provider' => $provider_key,
|
||||
'model' => $model,
|
||||
'prompt' => $final_prompt,
|
||||
'response' => '',
|
||||
'usage' => [],
|
||||
'post_id' => $post_id,
|
||||
'status' => 'error',
|
||||
'error_message' => $result->get_error_message(),
|
||||
]
|
||||
);
|
||||
wp_send_json_error( [ 'message' => $result->get_error_message() ], 500 );
|
||||
}
|
||||
|
||||
$response_text = $this->extract_content_text( $result );
|
||||
$response_usage = is_array( $result ) && isset( $result['usage'] ) ? $result['usage'] : [];
|
||||
|
||||
$response = $prompt_builder->parse_structured_response( $response_text, $settings );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
$this->plugin->get_generation_logger()->log_generation_event(
|
||||
[
|
||||
'provider' => $provider_key,
|
||||
'model' => $model,
|
||||
'prompt' => $final_prompt,
|
||||
'response' => $response_text,
|
||||
'usage' => $response_usage,
|
||||
'post_id' => $post_id,
|
||||
'status' => 'error',
|
||||
'error_message' => $response->get_error_message(),
|
||||
]
|
||||
);
|
||||
wp_send_json_error( [ 'message' => $response->get_error_message() ], 500 );
|
||||
}
|
||||
|
||||
$this->plugin->get_generation_logger()->log_generation_event(
|
||||
[
|
||||
'provider' => $provider_key,
|
||||
'model' => $model,
|
||||
'prompt' => $final_prompt,
|
||||
'response' => $response_text,
|
||||
'usage' => $response_usage,
|
||||
'post_id' => $post_id,
|
||||
'status' => 'success',
|
||||
]
|
||||
);
|
||||
|
||||
wp_send_json_success(
|
||||
[
|
||||
'fields' => $response,
|
||||
'raw' => $response_text,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function handle_refresh_models() {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_send_json_error( [ 'message' => __( 'Geen toestemming.', 'groq-ai-product-text' ) ], 403 );
|
||||
}
|
||||
|
||||
check_ajax_referer( 'groq_ai_refresh_models', 'nonce' );
|
||||
|
||||
$provider_key = isset( $_POST['provider'] ) ? sanitize_text_field( wp_unslash( $_POST['provider'] ) ) : '';
|
||||
$api_key = isset( $_POST['apiKey'] ) ? sanitize_text_field( wp_unslash( $_POST['apiKey'] ) ) : '';
|
||||
|
||||
if ( empty( $provider_key ) || empty( $api_key ) ) {
|
||||
wp_send_json_error( [ 'message' => __( 'Provider en API-sleutel zijn verplicht.', 'groq-ai-product-text' ) ], 400 );
|
||||
}
|
||||
|
||||
$provider = $this->plugin->get_provider_manager()->get_provider( $provider_key );
|
||||
|
||||
if ( ! $provider || ! $provider->supports_live_models() ) {
|
||||
wp_send_json_error( [ 'message' => __( 'Deze aanbieder ondersteunt het ophalen van modellen niet.', 'groq-ai-product-text' ) ], 400 );
|
||||
}
|
||||
|
||||
$result = $provider->fetch_live_models( $api_key );
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
wp_send_json_error( [ 'message' => $result->get_error_message() ], 500 );
|
||||
}
|
||||
|
||||
wp_send_json_success( [ 'models' => array_values( array_unique( $result ) ) ] );
|
||||
}
|
||||
|
||||
private function extract_content_text( $result ) {
|
||||
if ( is_array( $result ) && isset( $result['content'] ) ) {
|
||||
return (string) $result['content'];
|
||||
}
|
||||
|
||||
return (string) $result;
|
||||
}
|
||||
}
|
||||
41
includes/Core/class-groq-ai-service-container.php
Normal file
41
includes/Core/class-groq-ai-service-container.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Lightweight container voor Groq AI plugin services.
|
||||
*
|
||||
* Doel:
|
||||
* - Centraliseren van service creatie en dependency sharing.
|
||||
* - Mogelijke vervanging voor de huidige singleton/inline instanties in groq-ai-product-text.php.
|
||||
*/
|
||||
class Groq_AI_Service_Container {
|
||||
/** @var array<string,mixed> */
|
||||
private $services = [];
|
||||
|
||||
/**
|
||||
* Registreer een service factory.
|
||||
*
|
||||
* @param string $key
|
||||
* @param callable $factory
|
||||
*/
|
||||
public function set( $key, callable $factory ) {
|
||||
$this->services[ $key ] = $factory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Haal een service op en initialiseer deze lazy.
|
||||
*
|
||||
* @param string $key
|
||||
* @return mixed
|
||||
*/
|
||||
public function get( $key ) {
|
||||
if ( ! isset( $this->services[ $key ] ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ( is_callable( $this->services[ $key ] ) ) {
|
||||
$this->services[ $key ] = call_user_func( $this->services[ $key ], $this );
|
||||
}
|
||||
|
||||
return $this->services[ $key ];
|
||||
}
|
||||
}
|
||||
141
includes/Providers/class-groq-ai-abstract-openai-provider.php
Normal file
141
includes/Providers/class-groq-ai-abstract-openai-provider.php
Normal file
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
abstract class Groq_AI_Abstract_OpenAI_Provider implements Groq_AI_Provider_Interface {
|
||||
public function get_available_models() {
|
||||
return [];
|
||||
}
|
||||
|
||||
public function supports_response_format() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function supports_live_models() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function fetch_live_models( $api_key ) {
|
||||
$endpoint = $this->get_models_endpoint();
|
||||
if ( empty( $endpoint ) ) {
|
||||
return new WP_Error( 'groq_ai_models_endpoint_missing', __( 'Geen model-endpoint beschikbaar voor deze aanbieder.', 'groq-ai-product-text' ) );
|
||||
}
|
||||
|
||||
$response = wp_remote_get(
|
||||
$endpoint,
|
||||
[
|
||||
'headers' => [
|
||||
'Authorization' => 'Bearer ' . $api_key,
|
||||
'Content-Type' => 'application/json',
|
||||
],
|
||||
'timeout' => 20,
|
||||
]
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$body = json_decode( wp_remote_retrieve_body( $response ), true );
|
||||
|
||||
if ( isset( $body['error']['message'] ) ) {
|
||||
return new WP_Error( 'groq_ai_provider_error', (string) $body['error']['message'] );
|
||||
}
|
||||
|
||||
if ( empty( $body['data'] ) || ! is_array( $body['data'] ) ) {
|
||||
return new WP_Error( 'groq_ai_empty_response', __( 'Geen modeldata ontvangen.', 'groq-ai-product-text' ) );
|
||||
}
|
||||
|
||||
$models = [];
|
||||
foreach ( $body['data'] as $model ) {
|
||||
if ( ! empty( $model['id'] ) ) {
|
||||
$models[] = sanitize_text_field( $model['id'] );
|
||||
}
|
||||
}
|
||||
|
||||
if ( empty( $models ) ) {
|
||||
return new WP_Error( 'groq_ai_empty_response', __( 'Geen modeldata ontvangen.', 'groq-ai-product-text' ) );
|
||||
}
|
||||
|
||||
return $models;
|
||||
}
|
||||
|
||||
public function generate_content( array $args ) {
|
||||
$settings = isset( $args['settings'] ) ? (array) $args['settings'] : [];
|
||||
$prompt = isset( $args['prompt'] ) ? $args['prompt'] : '';
|
||||
$system_prompt = isset( $args['system_prompt'] ) ? $args['system_prompt'] : '';
|
||||
$model = ! empty( $args['model'] ) ? $args['model'] : $this->get_default_model();
|
||||
$api_key = $this->get_api_key( $settings );
|
||||
|
||||
if ( empty( $api_key ) ) {
|
||||
return new WP_Error( 'groq_ai_missing_api_key', sprintf( __( 'Stel eerst de API-sleutel voor %s in.', 'groq-ai-product-text' ), $this->get_label() ) );
|
||||
}
|
||||
|
||||
$messages = [
|
||||
[
|
||||
'role' => 'system',
|
||||
'content' => $system_prompt,
|
||||
],
|
||||
[
|
||||
'role' => 'user',
|
||||
'content' => $prompt,
|
||||
],
|
||||
];
|
||||
|
||||
$request_body = [
|
||||
'model' => $model,
|
||||
'messages' => $messages,
|
||||
'temperature' => isset( $args['temperature'] ) ? (float) $args['temperature'] : 0.7,
|
||||
'max_tokens' => 1024,
|
||||
];
|
||||
|
||||
if ( ! empty( $args['response_format'] ) ) {
|
||||
$request_body['response_format'] = $args['response_format'];
|
||||
}
|
||||
|
||||
$response = wp_remote_post(
|
||||
$this->get_endpoint(),
|
||||
[
|
||||
'headers' => [
|
||||
'Authorization' => 'Bearer ' . $api_key,
|
||||
'Content-Type' => 'application/json',
|
||||
],
|
||||
'body' => wp_json_encode( $request_body ),
|
||||
'timeout' => isset( $args['timeout'] ) ? (int) $args['timeout'] : 60,
|
||||
]
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$body = json_decode( wp_remote_retrieve_body( $response ), true );
|
||||
|
||||
if ( isset( $body['error']['message'] ) ) {
|
||||
return new WP_Error( 'groq_ai_provider_error', (string) $body['error']['message'] );
|
||||
}
|
||||
|
||||
if ( empty( $body['choices'][0]['message']['content'] ) ) {
|
||||
return new WP_Error(
|
||||
'groq_ai_empty_response',
|
||||
sprintf( __( 'Geen antwoord ontvangen van %s.', 'groq-ai-product-text' ), $this->get_label() )
|
||||
);
|
||||
}
|
||||
|
||||
$content = trim( $body['choices'][0]['message']['content'] );
|
||||
$usage = isset( $body['usage'] ) && is_array( $body['usage'] ) ? $body['usage'] : [];
|
||||
|
||||
return [
|
||||
'content' => $content,
|
||||
'usage' => $usage,
|
||||
'raw_response' => $body,
|
||||
];
|
||||
}
|
||||
|
||||
abstract protected function get_endpoint();
|
||||
|
||||
abstract protected function get_models_endpoint();
|
||||
|
||||
protected function get_api_key( $settings ) {
|
||||
$field = $this->get_option_key();
|
||||
return isset( $settings[ $field ] ) ? $settings[ $field ] : '';
|
||||
}
|
||||
}
|
||||
158
includes/Providers/class-groq-ai-provider-google.php
Normal file
158
includes/Providers/class-groq-ai-provider-google.php
Normal file
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
class Groq_AI_Provider_Google implements Groq_AI_Provider_Interface {
|
||||
public function get_key() {
|
||||
return 'google';
|
||||
}
|
||||
|
||||
public function get_label() {
|
||||
return __( 'Google AI (Gemini)', 'groq-ai-product-text' );
|
||||
}
|
||||
|
||||
public function get_default_model() {
|
||||
return 'gemini-1.5-flash';
|
||||
}
|
||||
|
||||
public function get_available_models() {
|
||||
return [
|
||||
'gemini-1.5-flash',
|
||||
'gemini-1.5-pro',
|
||||
'gemini-pro',
|
||||
];
|
||||
}
|
||||
|
||||
public function get_option_key() {
|
||||
return 'google_api_key';
|
||||
}
|
||||
|
||||
public function supports_live_models() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function supports_response_format() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function fetch_live_models( $api_key ) {
|
||||
$endpoint = add_query_arg(
|
||||
[ 'key' => $api_key, 'pageSize' => 100 ],
|
||||
'https://generativelanguage.googleapis.com/v1beta/models'
|
||||
);
|
||||
|
||||
$response = wp_remote_get(
|
||||
$endpoint,
|
||||
[
|
||||
'timeout' => 20,
|
||||
]
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$body = json_decode( wp_remote_retrieve_body( $response ), true );
|
||||
|
||||
if ( isset( $body['error']['message'] ) ) {
|
||||
return new WP_Error( 'groq_ai_provider_error', (string) $body['error']['message'] );
|
||||
}
|
||||
|
||||
if ( empty( $body['models'] ) || ! is_array( $body['models'] ) ) {
|
||||
return new WP_Error( 'groq_ai_empty_response', __( 'Geen modeldata ontvangen.', 'groq-ai-product-text' ) );
|
||||
}
|
||||
|
||||
$models = [];
|
||||
foreach ( $body['models'] as $model ) {
|
||||
if ( ! empty( $model['name'] ) ) {
|
||||
$parts = explode( '/', $model['name'] );
|
||||
$models[] = sanitize_text_field( end( $parts ) );
|
||||
}
|
||||
}
|
||||
|
||||
if ( empty( $models ) ) {
|
||||
return new WP_Error( 'groq_ai_empty_response', __( 'Geen modeldata ontvangen.', 'groq-ai-product-text' ) );
|
||||
}
|
||||
|
||||
return $models;
|
||||
}
|
||||
|
||||
public function generate_content( array $args ) {
|
||||
$settings = isset( $args['settings'] ) ? (array) $args['settings'] : [];
|
||||
$prompt = isset( $args['prompt'] ) ? $args['prompt'] : '';
|
||||
$system_prompt = isset( $args['system_prompt'] ) ? $args['system_prompt'] : '';
|
||||
$model = ! empty( $args['model'] ) ? $args['model'] : $this->get_default_model();
|
||||
$api_key = isset( $settings[ $this->get_option_key() ] ) ? $settings[ $this->get_option_key() ] : '';
|
||||
|
||||
if ( empty( $api_key ) ) {
|
||||
return new WP_Error( 'groq_ai_missing_api_key', sprintf( __( 'Stel eerst de API-sleutel voor %s in.', 'groq-ai-product-text' ), $this->get_label() ) );
|
||||
}
|
||||
|
||||
$endpoint = add_query_arg(
|
||||
'key',
|
||||
$api_key,
|
||||
sprintf( 'https://generativelanguage.googleapis.com/v1beta/models/%s:generateContent', rawurlencode( $model ) )
|
||||
);
|
||||
|
||||
$payload = [
|
||||
'contents' => [
|
||||
[
|
||||
'role' => 'user',
|
||||
'parts' => [
|
||||
[
|
||||
'text' => $system_prompt . "\n\n" . $prompt,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'generationConfig' => [
|
||||
'temperature' => isset( $args['temperature'] ) ? (float) $args['temperature'] : 0.7,
|
||||
'maxOutputTokens' => 1024,
|
||||
],
|
||||
];
|
||||
|
||||
$response = wp_remote_post(
|
||||
$endpoint,
|
||||
[
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/json',
|
||||
],
|
||||
'body' => wp_json_encode( $payload ),
|
||||
'timeout' => isset( $args['timeout'] ) ? (int) $args['timeout'] : 60,
|
||||
]
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$body = json_decode( wp_remote_retrieve_body( $response ), true );
|
||||
|
||||
if ( isset( $body['error']['message'] ) ) {
|
||||
return new WP_Error( 'groq_ai_provider_error', (string) $body['error']['message'] );
|
||||
}
|
||||
|
||||
if ( empty( $body['candidates'][0]['content']['parts'] ) ) {
|
||||
return new WP_Error(
|
||||
'groq_ai_empty_response',
|
||||
sprintf( __( 'Geen antwoord ontvangen van %s.', 'groq-ai-product-text' ), $this->get_label() )
|
||||
);
|
||||
}
|
||||
|
||||
$parts = $body['candidates'][0]['content']['parts'];
|
||||
$texts = [];
|
||||
|
||||
foreach ( $parts as $part ) {
|
||||
if ( isset( $part['text'] ) ) {
|
||||
$texts[] = $part['text'];
|
||||
}
|
||||
}
|
||||
|
||||
$content = trim( implode( "\n\n", array_filter( $texts ) ) );
|
||||
$usage = isset( $body['usageMetadata'] ) && is_array( $body['usageMetadata'] ) ? $body['usageMetadata'] : [];
|
||||
|
||||
return [
|
||||
'content' => $content,
|
||||
'usage' => $usage,
|
||||
'raw_response' => $body,
|
||||
];
|
||||
}
|
||||
}
|
||||
36
includes/Providers/class-groq-ai-provider-groq.php
Normal file
36
includes/Providers/class-groq-ai-provider-groq.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
class Groq_AI_Provider_Groq extends Groq_AI_Abstract_OpenAI_Provider {
|
||||
public function get_key() {
|
||||
return 'groq';
|
||||
}
|
||||
|
||||
public function get_label() {
|
||||
return __( 'Groq', 'groq-ai-product-text' );
|
||||
}
|
||||
|
||||
public function get_default_model() {
|
||||
return 'llama3-70b-8192';
|
||||
}
|
||||
|
||||
public function get_available_models() {
|
||||
return [
|
||||
'llama3-70b-8192',
|
||||
'llama3-8b-8192',
|
||||
'mixtral-8x7b-32768',
|
||||
'gemma-7b-it',
|
||||
];
|
||||
}
|
||||
|
||||
public function get_option_key() {
|
||||
return 'groq_api_key';
|
||||
}
|
||||
|
||||
protected function get_endpoint() {
|
||||
return 'https://api.groq.com/openai/v1/chat/completions';
|
||||
}
|
||||
|
||||
protected function get_models_endpoint() {
|
||||
return 'https://api.groq.com/openai/v1/models';
|
||||
}
|
||||
}
|
||||
24
includes/Providers/class-groq-ai-provider-manager.php
Normal file
24
includes/Providers/class-groq-ai-provider-manager.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
class Groq_AI_Provider_Manager {
|
||||
/** @var Groq_AI_Provider_Interface[] */
|
||||
private $providers = [];
|
||||
|
||||
public function __construct() {
|
||||
$this->register_provider( new Groq_AI_Provider_Groq() );
|
||||
$this->register_provider( new Groq_AI_Provider_OpenAI() );
|
||||
$this->register_provider( new Groq_AI_Provider_Google() );
|
||||
}
|
||||
|
||||
public function register_provider( Groq_AI_Provider_Interface $provider ) {
|
||||
$this->providers[ $provider->get_key() ] = $provider;
|
||||
}
|
||||
|
||||
public function get_providers() {
|
||||
return $this->providers;
|
||||
}
|
||||
|
||||
public function get_provider( $key ) {
|
||||
return isset( $this->providers[ $key ] ) ? $this->providers[ $key ] : null;
|
||||
}
|
||||
}
|
||||
36
includes/Providers/class-groq-ai-provider-openai.php
Normal file
36
includes/Providers/class-groq-ai-provider-openai.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
class Groq_AI_Provider_OpenAI extends Groq_AI_Abstract_OpenAI_Provider {
|
||||
public function get_key() {
|
||||
return 'openai';
|
||||
}
|
||||
|
||||
public function get_label() {
|
||||
return __( 'OpenAI', 'groq-ai-product-text' );
|
||||
}
|
||||
|
||||
public function get_default_model() {
|
||||
return 'gpt-4o-mini';
|
||||
}
|
||||
|
||||
public function get_available_models() {
|
||||
return [
|
||||
'gpt-4o',
|
||||
'gpt-4o-mini',
|
||||
'gpt-4.1-mini',
|
||||
'gpt-3.5-turbo',
|
||||
];
|
||||
}
|
||||
|
||||
public function get_option_key() {
|
||||
return 'openai_api_key';
|
||||
}
|
||||
|
||||
protected function get_endpoint() {
|
||||
return 'https://api.openai.com/v1/chat/completions';
|
||||
}
|
||||
|
||||
protected function get_models_endpoint() {
|
||||
return 'https://api.openai.com/v1/models';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Beheert conversatie-ID's per provider + context-hash.
|
||||
*
|
||||
* Verplaatst vanuit groq-ai-product-text.php:
|
||||
* - ensure_conversation_id()
|
||||
* - get/save_conversation_states()
|
||||
* - get_context_hash()
|
||||
*/
|
||||
class Groq_AI_Conversation_Manager {
|
||||
/** @var string */
|
||||
private $option_key;
|
||||
|
||||
public function __construct( $option_key ) {
|
||||
$this->option_key = $option_key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourneert of creëert een conversatie-ID.
|
||||
*
|
||||
* @param string $provider_key
|
||||
* @param string $store_context
|
||||
* @return string
|
||||
*/
|
||||
public function ensure_id( $provider_key, $store_context ) {
|
||||
$states = $this->get_states();
|
||||
$context_hash = $this->get_context_hash( $store_context );
|
||||
|
||||
if ( isset( $states[ $provider_key ]['hash'], $states[ $provider_key ]['id'] ) && $states[ $provider_key ]['hash'] === $context_hash ) {
|
||||
return $states[ $provider_key ]['id'];
|
||||
}
|
||||
|
||||
$conversation_id = wp_generate_uuid4();
|
||||
$states[ $provider_key ] = [
|
||||
'hash' => $context_hash,
|
||||
'id' => $conversation_id,
|
||||
];
|
||||
$this->save_states( $states );
|
||||
|
||||
return $conversation_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
private function get_states() {
|
||||
$states = get_option( $this->option_key, [] );
|
||||
|
||||
return is_array( $states ) ? $states : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $states
|
||||
* @return void
|
||||
*/
|
||||
private function save_states( $states ) {
|
||||
update_option( $this->option_key, $states, false );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $store_context
|
||||
* @return string
|
||||
*/
|
||||
private function get_context_hash( $store_context ) {
|
||||
return md5( wp_json_encode( trim( (string) $store_context ) ) );
|
||||
}
|
||||
}
|
||||
138
includes/Services/Logging/class-groq-ai-generation-logger.php
Normal file
138
includes/Services/Logging/class-groq-ai-generation-logger.php
Normal file
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Loggingservice voor AI-generaties en DB-tabellen.
|
||||
*
|
||||
* Te importeren logica:
|
||||
* - log_generation_event()
|
||||
* - log_debug()
|
||||
* - get_logs_table_name()/create_logs_table()/maybe_create_logs_table()
|
||||
*/
|
||||
class Groq_AI_Generation_Logger {
|
||||
const OPTION_TABLE_CREATED = 'groq_ai_logs_table_created';
|
||||
|
||||
/** @var WC_Logger|null */
|
||||
private $woo_logger = null;
|
||||
|
||||
/** @var bool|null */
|
||||
private $logs_table_exists = null;
|
||||
|
||||
public function log_generation_event( array $args ) {
|
||||
if ( ! $this->logs_table_exists() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$table = $this->get_logs_table_name();
|
||||
|
||||
$usage = isset( $args['usage'] ) && is_array( $args['usage'] ) ? $args['usage'] : [];
|
||||
$prompt_tokens = isset( $usage['prompt_tokens'] ) ? absint( $usage['prompt_tokens'] ) : null;
|
||||
$completion_tokens = isset( $usage['completion_tokens'] ) ? absint( $usage['completion_tokens'] ) : null;
|
||||
$total_tokens = isset( $usage['total_tokens'] ) ? absint( $usage['total_tokens'] ) : null;
|
||||
|
||||
$wpdb->insert(
|
||||
$table,
|
||||
[
|
||||
'created_at' => current_time( 'mysql' ),
|
||||
'user_id' => get_current_user_id(),
|
||||
'post_id' => isset( $args['post_id'] ) ? absint( $args['post_id'] ) : 0,
|
||||
'provider' => isset( $args['provider'] ) ? sanitize_text_field( $args['provider'] ) : '',
|
||||
'model' => isset( $args['model'] ) ? sanitize_text_field( $args['model'] ) : '',
|
||||
'prompt' => isset( $args['prompt'] ) ? $args['prompt'] : '',
|
||||
'response' => isset( $args['response'] ) ? $args['response'] : '',
|
||||
'tokens_prompt' => $prompt_tokens,
|
||||
'tokens_completion' => $completion_tokens,
|
||||
'tokens_total' => $total_tokens,
|
||||
'status' => isset( $args['status'] ) ? sanitize_text_field( $args['status'] ) : 'success',
|
||||
'error_message' => isset( $args['error_message'] ) ? $args['error_message'] : '',
|
||||
'usage_json' => ! empty( $usage ) ? wp_json_encode( $usage ) : null,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function log_debug( $message, $context = [] ) {
|
||||
if ( class_exists( 'WC_Logger' ) ) {
|
||||
if ( ! $this->woo_logger ) {
|
||||
$this->woo_logger = wc_get_logger();
|
||||
}
|
||||
|
||||
if ( $this->woo_logger ) {
|
||||
$context_string = ! empty( $context ) ? ' ' . wp_json_encode( $context ) : '';
|
||||
$this->woo_logger->debug( '[GroqAI] ' . $message . $context_string, [ 'source' => 'groq-ai-product-text' ] );
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
$entry = '[GroqAI] ' . $message;
|
||||
|
||||
if ( ! empty( $context ) ) {
|
||||
$entry .= ' ' . wp_json_encode( $context );
|
||||
}
|
||||
|
||||
error_log( $entry );
|
||||
}
|
||||
}
|
||||
|
||||
public function maybe_create_table() {
|
||||
if ( get_option( self::OPTION_TABLE_CREATED ) ) {
|
||||
$this->logs_table_exists = true;
|
||||
return;
|
||||
}
|
||||
|
||||
$this->create_table();
|
||||
}
|
||||
|
||||
public function create_table() {
|
||||
global $wpdb;
|
||||
|
||||
$table = $this->get_logs_table_name();
|
||||
$charset_collate = $wpdb->get_charset_collate();
|
||||
|
||||
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
||||
|
||||
$sql = "CREATE TABLE {$table} (
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
created_at datetime NOT NULL,
|
||||
user_id bigint(20) unsigned DEFAULT NULL,
|
||||
post_id bigint(20) unsigned DEFAULT NULL,
|
||||
provider varchar(50) NOT NULL,
|
||||
model varchar(100) NOT NULL,
|
||||
prompt longtext NOT NULL,
|
||||
response longtext DEFAULT NULL,
|
||||
tokens_prompt int unsigned DEFAULT NULL,
|
||||
tokens_completion int unsigned DEFAULT NULL,
|
||||
tokens_total int unsigned DEFAULT NULL,
|
||||
status varchar(20) NOT NULL,
|
||||
error_message text DEFAULT NULL,
|
||||
usage_json longtext DEFAULT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY provider (provider),
|
||||
KEY post_id (post_id)
|
||||
) {$charset_collate};";
|
||||
|
||||
dbDelta( $sql );
|
||||
|
||||
$this->logs_table_exists = true;
|
||||
update_option( self::OPTION_TABLE_CREATED, 1 );
|
||||
}
|
||||
|
||||
private function get_logs_table_name() {
|
||||
global $wpdb;
|
||||
|
||||
return $wpdb->prefix . 'groq_ai_generation_logs';
|
||||
}
|
||||
|
||||
private function logs_table_exists() {
|
||||
if ( null !== $this->logs_table_exists ) {
|
||||
return $this->logs_table_exists;
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$table = $this->get_logs_table_name();
|
||||
$result = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) );
|
||||
$this->logs_table_exists = ( $result === $table );
|
||||
|
||||
return $this->logs_table_exists;
|
||||
}
|
||||
}
|
||||
353
includes/Services/Prompt/class-groq-ai-prompt-builder.php
Normal file
353
includes/Services/Prompt/class-groq-ai-prompt-builder.php
Normal file
@@ -0,0 +1,353 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Bouwt prompts, verwerkt responses en verzamelt context.
|
||||
*/
|
||||
class Groq_AI_Prompt_Builder {
|
||||
/** @var Groq_AI_Settings_Manager */
|
||||
private $settings_manager;
|
||||
|
||||
public function __construct( Groq_AI_Settings_Manager $settings_manager ) {
|
||||
$this->settings_manager = $settings_manager;
|
||||
}
|
||||
|
||||
public function build_system_prompt( $settings, $conversation_id ) {
|
||||
$context = isset( $settings['store_context'] ) ? trim( $settings['store_context'] ) : '';
|
||||
$base_instruction = __( 'Je bent een copywriter voor een WooCommerce winkel en schrijft overtuigende productbeschrijvingen.', 'groq-ai-product-text' );
|
||||
|
||||
if ( $context ) {
|
||||
$base_instruction = sprintf(
|
||||
__( 'Je bent een copywriter voor een WooCommerce winkel. Gebruik de volgende context indien beschikbaar: %s', 'groq-ai-product-text' ),
|
||||
$context
|
||||
);
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
__( 'Conversatie-ID: %1$s. %2$s', 'groq-ai-product-text' ),
|
||||
$conversation_id,
|
||||
$base_instruction
|
||||
);
|
||||
}
|
||||
|
||||
public function append_response_instructions( $prompt, $settings ) {
|
||||
$instructions = (string) ( $this->get_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_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' ) );
|
||||
}
|
||||
|
||||
$clean = trim( $raw );
|
||||
|
||||
if ( preg_match( '/```(?:json)?\s*(.*?)```/is', $clean, $matches ) ) {
|
||||
$clean = trim( $matches[1] );
|
||||
}
|
||||
|
||||
$decoded = json_decode( $clean, true );
|
||||
|
||||
if ( ! is_array( $decoded ) ) {
|
||||
return new WP_Error( 'groq_ai_parse_error', __( 'Kon de AI-respons niet als JSON lezen. Probeer het opnieuw.', 'groq-ai-product-text' ) );
|
||||
}
|
||||
|
||||
$fields = [
|
||||
'title' => trim( (string) ( $decoded['title'] ?? '' ) ),
|
||||
'short_description' => trim( (string) ( $decoded['short_description'] ?? '' ) ),
|
||||
'description' => trim( (string) ( $decoded['description'] ?? '' ) ),
|
||||
];
|
||||
|
||||
if ( $this->settings_manager->is_module_enabled( 'rankmath', $settings ) ) {
|
||||
$keyword_limit = $this->settings_manager->get_rankmath_focus_keyword_limit( $settings );
|
||||
$focus_keywords = [];
|
||||
$raw_keyword_set = isset( $decoded['focus_keywords'] ) ? $decoded['focus_keywords'] : [];
|
||||
|
||||
if ( is_array( $raw_keyword_set ) ) {
|
||||
foreach ( $raw_keyword_set as $keyword ) {
|
||||
$keyword = trim( (string) $keyword );
|
||||
if ( '' !== $keyword ) {
|
||||
$focus_keywords[] = $keyword;
|
||||
}
|
||||
}
|
||||
} elseif ( is_string( $raw_keyword_set ) ) {
|
||||
$parts = preg_split( '/[,\\n]+/', $raw_keyword_set );
|
||||
if ( is_array( $parts ) ) {
|
||||
foreach ( $parts as $part ) {
|
||||
$part = trim( (string) $part );
|
||||
if ( '' !== $part ) {
|
||||
$focus_keywords[] = $part;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$focus_keywords = array_slice( array_unique( $focus_keywords ), 0, $keyword_limit );
|
||||
|
||||
$fields['meta_title'] = $this->truncate_meta_field( (string) ( $decoded['meta_title'] ?? '' ), 60 );
|
||||
$fields['meta_description'] = $this->truncate_meta_field( (string) ( $decoded['meta_description'] ?? '' ), 160 );
|
||||
$fields['focus_keywords'] = implode( ', ', $focus_keywords );
|
||||
}
|
||||
|
||||
if ( implode( '', $fields ) === '' ) {
|
||||
return new WP_Error( 'groq_ai_parse_error', __( 'De AI-respons bevatte geen bruikbare velden.', 'groq-ai-product-text' ) );
|
||||
}
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
public function parse_context_fields_from_request( $raw, $settings ) {
|
||||
if ( empty( $raw ) ) {
|
||||
return $settings['context_fields'];
|
||||
}
|
||||
|
||||
$decoded = json_decode( wp_unslash( $raw ), true );
|
||||
|
||||
if ( ! is_array( $decoded ) ) {
|
||||
return $settings['context_fields'];
|
||||
}
|
||||
|
||||
$normalized = $this->settings_manager->normalize_context_fields( $decoded );
|
||||
|
||||
if ( ! array_filter( $normalized ) ) {
|
||||
return $settings['context_fields'];
|
||||
}
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
public function build_product_context_block( $post_id, $fields ) {
|
||||
$post_id = absint( $post_id );
|
||||
|
||||
if ( ! $post_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$parts = [];
|
||||
|
||||
if ( ! empty( $fields['title'] ) ) {
|
||||
$title = get_the_title( $post_id );
|
||||
if ( $title ) {
|
||||
$parts[] = sprintf( __( 'Titel: %s', 'groq-ai-product-text' ), wp_strip_all_tags( $title ) );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $fields['short_description'] ) ) {
|
||||
$excerpt = get_post_field( 'post_excerpt', $post_id );
|
||||
if ( $excerpt ) {
|
||||
$parts[] = sprintf( __( 'Korte beschrijving: %s', 'groq-ai-product-text' ), wp_strip_all_tags( $excerpt ) );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $fields['description'] ) ) {
|
||||
$content = get_post_field( 'post_content', $post_id );
|
||||
if ( $content ) {
|
||||
$parts[] = sprintf( __( 'Beschrijving: %s', 'groq-ai-product-text' ), wp_strip_all_tags( $content ) );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $fields['attributes'] ) ) {
|
||||
$attributes = $this->get_product_attributes_text( $post_id );
|
||||
if ( $attributes ) {
|
||||
$parts[] = sprintf( __( 'Attributen: %s', 'groq-ai-product-text' ), $attributes );
|
||||
}
|
||||
}
|
||||
|
||||
return implode( "\n\n", array_filter( $parts ) );
|
||||
}
|
||||
|
||||
public function prepend_context_to_prompt( $prompt, $context ) {
|
||||
$context = trim( (string) $context );
|
||||
|
||||
if ( '' === $context ) {
|
||||
return $prompt;
|
||||
}
|
||||
|
||||
$intro = __( 'Gebruik de volgende productcontext bij het schrijven:', 'groq-ai-product-text' );
|
||||
|
||||
return $intro . "\n" . $context . "\n\n" . $prompt;
|
||||
}
|
||||
|
||||
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 );
|
||||
$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 = [
|
||||
'title' => [
|
||||
'type' => 'string',
|
||||
'description' => __( 'Korte, overtuigende producttitel in het Nederlands.', 'groq-ai-product-text' ),
|
||||
'minLength' => 3,
|
||||
],
|
||||
'short_description' => [
|
||||
'type' => 'string',
|
||||
'description' => __( "Korte HTML-beschrijving in <p>-tags (maximaal 2 alinea's).", 'groq-ai-product-text' ),
|
||||
'minLength' => 10,
|
||||
],
|
||||
'description' => [
|
||||
'type' => 'string',
|
||||
'description' => __( 'Uitgebreide HTML-productbeschrijving met paragrafen en eventueel lijsten.', 'groq-ai-product-text' ),
|
||||
'minLength' => 20,
|
||||
],
|
||||
];
|
||||
|
||||
if ( $rankmath_enabled ) {
|
||||
$properties['meta_title'] = [
|
||||
'type' => 'string',
|
||||
'description' => sprintf(
|
||||
/* translators: 1: maximum character count, 2: maximum pixels */
|
||||
__( 'SEO-meta title (max. %1$d tekens en %2$d pixels).', 'groq-ai-product-text' ),
|
||||
60,
|
||||
$title_pixels
|
||||
),
|
||||
'maxLength' => 120,
|
||||
];
|
||||
$properties['meta_description'] = [
|
||||
'type' => 'string',
|
||||
'description' => sprintf(
|
||||
/* translators: 1: maximum character count, 2: maximum pixels */
|
||||
__( 'SEO-meta description (max. %1$d tekens en %2$d pixels).', 'groq-ai-product-text' ),
|
||||
160,
|
||||
$desc_pixels
|
||||
),
|
||||
'maxLength' => 320,
|
||||
];
|
||||
$properties['focus_keywords'] = [
|
||||
'type' => 'array',
|
||||
'description' => __( 'Lijst met korte zoekwoorden zonder hashtags of extra tekst.', 'groq-ai-product-text' ),
|
||||
'maxItems' => max( 1, $keyword_limit ),
|
||||
'items' => [
|
||||
'type' => 'string',
|
||||
'minLength' => 1,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$schema = [
|
||||
'type' => 'object',
|
||||
'properties' => $properties,
|
||||
'required' => [ 'title', 'short_description', 'description' ],
|
||||
'additionalProperties' => false,
|
||||
];
|
||||
|
||||
return [
|
||||
'type' => 'json_schema',
|
||||
'json_schema' => [
|
||||
'name' => 'groq_ai_product_text',
|
||||
'schema' => $schema,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function get_structured_response_instructions( $settings = null ) {
|
||||
$schema_parts = [
|
||||
'"title":"..."',
|
||||
'"short_description":"..."',
|
||||
'"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(
|
||||
/* translators: %s: JSON structure example */
|
||||
__( 'Geef ALLEEN een geldig JSON-object terug met deze structuur: %s. Gebruik dubbele aanhalingstekens, geen Markdown of extra tekst. Gebruik \\n voor regeleinden. Zorg dat zowel short_description als description nooit leeg zijn.', 'groq-ai-product-text' ),
|
||||
$json_structure
|
||||
);
|
||||
|
||||
if ( $rankmath_enabled ) {
|
||||
$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 );
|
||||
$instruction .= ' ' . sprintf(
|
||||
/* translators: 1: focus keyword limit, 2: meta title pixel limit, 3: meta description pixel limit */
|
||||
__( 'Beperk meta_title tot maximaal 60 tekens en %2$d pixels en meta_description tot maximaal 160 tekens en %3$d pixels. Lever maximaal %1$d focuskeywords in het focus_keywords-array (korte termen zonder hashtag of extra tekst).', 'groq-ai-product-text' ),
|
||||
$keyword_limit,
|
||||
$title_pixels,
|
||||
$desc_pixels
|
||||
);
|
||||
}
|
||||
|
||||
$instruction .= ' ' . __( 'Zorg dat short_description en description geldige HTML bevatten (gebruik minimaal <p>-tags en waar relevant lijstjes of benadrukking). Voeg geen extra tekst buiten het JSON-object toe.', 'groq-ai-product-text' );
|
||||
|
||||
return $instruction;
|
||||
}
|
||||
|
||||
private function truncate_meta_field( $text, $limit ) {
|
||||
$text = trim( (string) $text );
|
||||
|
||||
if ( '' === $text || $limit <= 0 ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ( function_exists( 'mb_strlen' ) ) {
|
||||
if ( mb_strlen( $text ) <= $limit ) {
|
||||
return $text;
|
||||
}
|
||||
|
||||
return mb_substr( $text, 0, $limit );
|
||||
}
|
||||
|
||||
if ( strlen( $text ) <= $limit ) {
|
||||
return $text;
|
||||
}
|
||||
|
||||
return substr( $text, 0, $limit );
|
||||
}
|
||||
|
||||
private function get_product_attributes_text( $post_id ) {
|
||||
if ( ! function_exists( 'wc_get_product' ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$product = wc_get_product( $post_id );
|
||||
|
||||
if ( ! $product ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$attributes = $product->get_attributes();
|
||||
|
||||
if ( empty( $attributes ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$lines = [];
|
||||
|
||||
foreach ( $attributes as $attribute ) {
|
||||
if ( $attribute->is_taxonomy() ) {
|
||||
$terms = wc_get_product_terms( $post_id, $attribute->get_name(), [ 'fields' => 'names' ] );
|
||||
$value = implode( ', ', array_map( 'sanitize_text_field', (array) $terms ) );
|
||||
$label = wc_attribute_label( $attribute->get_name() );
|
||||
} else {
|
||||
$options = $attribute->get_options();
|
||||
$value = implode( ', ', array_map( 'sanitize_text_field', (array) $options ) );
|
||||
$label = sanitize_text_field( $attribute->get_name() );
|
||||
}
|
||||
|
||||
$value = trim( $value );
|
||||
|
||||
if ( '' !== $value ) {
|
||||
$lines[] = sprintf( '%s: %s', $label, $value );
|
||||
}
|
||||
}
|
||||
|
||||
return implode( '; ', $lines );
|
||||
}
|
||||
}
|
||||
295
includes/Services/Settings/class-groq-ai-settings-manager.php
Normal file
295
includes/Services/Settings/class-groq-ai-settings-manager.php
Normal file
@@ -0,0 +1,295 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Beheert alle plugininstellingen: ophalen, standaardiseren en sanitizen.
|
||||
*/
|
||||
class Groq_AI_Settings_Manager {
|
||||
/** @var string */
|
||||
private $option_key;
|
||||
|
||||
/** @var Groq_AI_Provider_Manager */
|
||||
private $provider_manager;
|
||||
|
||||
/** @var array|null */
|
||||
private $context_field_definitions = null;
|
||||
|
||||
/** @var array|null */
|
||||
private $default_modules = null;
|
||||
|
||||
public function __construct( $option_key, Groq_AI_Provider_Manager $provider_manager ) {
|
||||
$this->option_key = $option_key;
|
||||
$this->provider_manager = $provider_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Geeft samengestelde instellingen terug (voormalige get_settings()).
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function all() {
|
||||
$defaults = [
|
||||
'provider' => 'groq',
|
||||
'model' => '',
|
||||
'store_context' => '',
|
||||
'default_prompt' => '',
|
||||
'groq_api_key' => '',
|
||||
'openai_api_key' => '',
|
||||
'google_api_key' => '',
|
||||
'context_fields' => $this->get_default_context_fields(),
|
||||
'modules' => $this->get_default_modules_settings(),
|
||||
'response_format_compat' => false,
|
||||
];
|
||||
|
||||
$settings = get_option( $this->option_key, [] );
|
||||
$settings = wp_parse_args( (array) $settings, $defaults );
|
||||
$settings['context_fields'] = $this->normalize_context_fields( isset( $settings['context_fields'] ) ? $settings['context_fields'] : [] );
|
||||
$settings['modules'] = $this->sanitize_modules_settings( isset( $settings['modules'] ) ? $settings['modules'] : [] );
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizelogica voor register_setting callback.
|
||||
*
|
||||
* @param array $input
|
||||
* @return array
|
||||
*/
|
||||
public function sanitize( $input ) {
|
||||
$base_defaults = [
|
||||
'provider' => 'groq',
|
||||
'model' => '',
|
||||
'store_context' => '',
|
||||
'default_prompt' => '',
|
||||
'groq_api_key' => '',
|
||||
'openai_api_key' => '',
|
||||
'google_api_key' => '',
|
||||
'context_fields' => $this->get_default_context_fields(),
|
||||
'modules' => $this->get_default_modules_settings(),
|
||||
'response_format_compat' => false,
|
||||
];
|
||||
|
||||
$current_settings = $this->all();
|
||||
$defaults = wp_parse_args( $current_settings, $base_defaults );
|
||||
$raw_input = (array) $input;
|
||||
$input = wp_parse_args( $raw_input, $defaults );
|
||||
$context_posted = array_key_exists( 'context_fields', $raw_input );
|
||||
$modules_posted = array_key_exists( 'modules', $raw_input );
|
||||
|
||||
$provider = sanitize_text_field( $input['provider'] );
|
||||
if ( ! $this->provider_manager->get_provider( $provider ) ) {
|
||||
$provider = 'groq';
|
||||
}
|
||||
|
||||
return [
|
||||
'provider' => $provider,
|
||||
'model' => sanitize_text_field( $input['model'] ),
|
||||
'store_context' => sanitize_textarea_field( $input['store_context'] ),
|
||||
'default_prompt' => sanitize_textarea_field( $input['default_prompt'] ),
|
||||
'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'] ),
|
||||
'response_format_compat' => ! empty( $raw_input['response_format_compat'] ),
|
||||
'context_fields' => $this->normalize_context_fields( $context_posted ? $raw_input['context_fields'] : $defaults['context_fields'] ),
|
||||
'modules' => $this->sanitize_modules_settings(
|
||||
$modules_posted ? $raw_input['modules'] : [],
|
||||
$defaults['modules'],
|
||||
isset( $current_settings['modules'] ) ? (array) $current_settings['modules'] : $this->get_default_modules_settings(),
|
||||
$modules_posted
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
public function get_context_field_definitions() {
|
||||
if ( null === $this->context_field_definitions ) {
|
||||
$this->context_field_definitions = [
|
||||
'title' => [
|
||||
'label' => __( 'Producttitel', 'groq-ai-product-text' ),
|
||||
'description' => __( 'Voeg de huidige producttitel toe als context.', 'groq-ai-product-text' ),
|
||||
'default' => true,
|
||||
],
|
||||
'short_description' => [
|
||||
'label' => __( 'Korte beschrijving', 'groq-ai-product-text' ),
|
||||
'description' => __( 'Gebruik de bestaande korte beschrijving (indien aanwezig).', 'groq-ai-product-text' ),
|
||||
'default' => true,
|
||||
],
|
||||
'description' => [
|
||||
'label' => __( 'Volledige beschrijving', 'groq-ai-product-text' ),
|
||||
'description' => __( 'Stuurt de huidige productbeschrijving mee als bronmateriaal.', 'groq-ai-product-text' ),
|
||||
'default' => true,
|
||||
],
|
||||
'attributes' => [
|
||||
'label' => __( 'Attributen', 'groq-ai-product-text' ),
|
||||
'description' => __( 'Voeg gestructureerde productattributen toe (zoals kleur, maat, materiaal).', 'groq-ai-product-text' ),
|
||||
'default' => false,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
return $this->context_field_definitions;
|
||||
}
|
||||
|
||||
public function get_default_context_fields() {
|
||||
$definitions = $this->get_context_field_definitions();
|
||||
$defaults = [];
|
||||
|
||||
foreach ( $definitions as $key => $data ) {
|
||||
$defaults[ $key ] = ! empty( $data['default'] );
|
||||
}
|
||||
|
||||
return $defaults;
|
||||
}
|
||||
|
||||
public function normalize_context_fields( $fields ) {
|
||||
$definitions = $this->get_context_field_definitions();
|
||||
$normalized = [];
|
||||
|
||||
foreach ( $definitions as $key => $data ) {
|
||||
$normalized[ $key ] = false;
|
||||
}
|
||||
|
||||
if ( ! is_array( $fields ) ) {
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
foreach ( $fields as $key => $value ) {
|
||||
if ( is_int( $key ) ) {
|
||||
$key = sanitize_text_field( $value );
|
||||
$value = true;
|
||||
}
|
||||
|
||||
if ( array_key_exists( $key, $normalized ) ) {
|
||||
$normalized[ $key ] = (bool) $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
public function get_default_modules_settings() {
|
||||
if ( null === $this->default_modules ) {
|
||||
$this->default_modules = [
|
||||
'rankmath' => [
|
||||
'enabled' => true,
|
||||
'focus_keyword_limit' => 3,
|
||||
'meta_title_pixel_limit' => 580,
|
||||
'meta_description_pixel_limit' => 920,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
return $this->default_modules;
|
||||
}
|
||||
|
||||
public function get_module_config( $module, $settings = null ) {
|
||||
if ( null === $settings ) {
|
||||
$settings = $this->all();
|
||||
}
|
||||
|
||||
$defaults = $this->get_default_modules_settings();
|
||||
$modules = isset( $settings['modules'] ) && is_array( $settings['modules'] ) ? $settings['modules'] : [];
|
||||
$config = isset( $modules[ $module ] ) ? (array) $modules[ $module ] : [];
|
||||
|
||||
return wp_parse_args( $config, isset( $defaults[ $module ] ) ? $defaults[ $module ] : [] );
|
||||
}
|
||||
|
||||
public function is_module_enabled( $module, $settings = null ) {
|
||||
$config = $this->get_module_config( $module, $settings );
|
||||
|
||||
return ! empty( $config['enabled'] );
|
||||
}
|
||||
|
||||
public function get_rankmath_focus_keyword_limit( $settings = null ) {
|
||||
$config = $this->get_module_config( 'rankmath', $settings );
|
||||
$limit = isset( $config['focus_keyword_limit'] ) ? absint( $config['focus_keyword_limit'] ) : 3;
|
||||
|
||||
return max( 1, min( 10, $limit ) );
|
||||
}
|
||||
|
||||
public function get_rankmath_meta_title_pixel_limit( $settings = null ) {
|
||||
$config = $this->get_module_config( 'rankmath', $settings );
|
||||
$value = isset( $config['meta_title_pixel_limit'] ) ? absint( $config['meta_title_pixel_limit'] ) : 580;
|
||||
|
||||
return max( 200, min( 1200, $value ) );
|
||||
}
|
||||
|
||||
public function get_rankmath_meta_description_pixel_limit( $settings = null ) {
|
||||
$config = $this->get_module_config( 'rankmath', $settings );
|
||||
$value = isset( $config['meta_description_pixel_limit'] ) ? absint( $config['meta_description_pixel_limit'] ) : 920;
|
||||
|
||||
return max( 200, min( 2000, $value ) );
|
||||
}
|
||||
|
||||
public function is_response_format_compat_enabled( $settings = null ) {
|
||||
if ( null === $settings ) {
|
||||
$settings = $this->all();
|
||||
}
|
||||
|
||||
return ! empty( $settings['response_format_compat'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|null $modules
|
||||
* @param array|null $defaults
|
||||
* @param array|null $current
|
||||
* @param bool $is_posted
|
||||
* @return array
|
||||
*/
|
||||
private function sanitize_modules_settings( $modules, $defaults = null, $current = null, $is_posted = false ) {
|
||||
$module_defaults = $this->get_default_modules_settings();
|
||||
|
||||
if ( ! is_array( $defaults ) ) {
|
||||
$defaults = $module_defaults;
|
||||
}
|
||||
|
||||
if ( ! is_array( $current ) ) {
|
||||
$current = $defaults;
|
||||
}
|
||||
|
||||
$current = wp_parse_args( $current, $defaults );
|
||||
|
||||
if ( ! is_array( $modules ) ) {
|
||||
$modules = [];
|
||||
}
|
||||
|
||||
if ( ! $is_posted ) {
|
||||
$clean = [];
|
||||
foreach ( $module_defaults as $module_key => $module_default_config ) {
|
||||
$raw = isset( $modules[ $module_key ] ) ? (array) $modules[ $module_key ] : [];
|
||||
$clean[ $module_key ] = wp_parse_args( $raw, isset( $current[ $module_key ] ) ? $current[ $module_key ] : $module_default_config );
|
||||
}
|
||||
|
||||
return $clean;
|
||||
}
|
||||
|
||||
$result = $current;
|
||||
|
||||
foreach ( $module_defaults as $module_key => $module_default_config ) {
|
||||
$raw = isset( $modules[ $module_key ] ) ? (array) $modules[ $module_key ] : [];
|
||||
$current_config = isset( $current[ $module_key ] ) ? (array) $current[ $module_key ] : $module_default_config;
|
||||
|
||||
$result[ $module_key ]['enabled'] = isset( $raw['enabled'] ) ? (bool) $raw['enabled'] : ( isset( $current_config['enabled'] ) ? (bool) $current_config['enabled'] : false );
|
||||
|
||||
if ( 'rankmath' === $module_key ) {
|
||||
$limit = isset( $raw['focus_keyword_limit'] ) ? absint( $raw['focus_keyword_limit'] ) : ( isset( $current_config['focus_keyword_limit'] ) ? absint( $current_config['focus_keyword_limit'] ) : $module_default_config['focus_keyword_limit'] );
|
||||
if ( $limit <= 0 ) {
|
||||
$limit = $module_default_config['focus_keyword_limit'];
|
||||
}
|
||||
$result[ $module_key ]['focus_keyword_limit'] = max( 1, min( 10, $limit ) );
|
||||
|
||||
$title_pixel_limit = isset( $raw['meta_title_pixel_limit'] ) ? absint( $raw['meta_title_pixel_limit'] ) : ( isset( $current_config['meta_title_pixel_limit'] ) ? absint( $current_config['meta_title_pixel_limit'] ) : $module_default_config['meta_title_pixel_limit'] );
|
||||
if ( $title_pixel_limit <= 0 ) {
|
||||
$title_pixel_limit = $module_default_config['meta_title_pixel_limit'];
|
||||
}
|
||||
$result[ $module_key ]['meta_title_pixel_limit'] = max( 200, min( 1200, $title_pixel_limit ) );
|
||||
|
||||
$pixel_limit = isset( $raw['meta_description_pixel_limit'] ) ? absint( $raw['meta_description_pixel_limit'] ) : ( isset( $current_config['meta_description_pixel_limit'] ) ? absint( $current_config['meta_description_pixel_limit'] ) : $module_default_config['meta_description_pixel_limit'] );
|
||||
if ( $pixel_limit <= 0 ) {
|
||||
$pixel_limit = $module_default_config['meta_description_pixel_limit'];
|
||||
}
|
||||
$result[ $module_key ]['meta_description_pixel_limit'] = max( 200, min( 2000, $pixel_limit ) );
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user