feat: Update plugin to version 1.2.0 with new image context features

- Added support for image context in product prompts, allowing images to be sent as URLs or Base64.
- Introduced a new settings page for managing prompt configurations.
- Implemented caching for allowed models per provider to enhance performance.
- Enhanced logging to include image context usage details.
- Added model exclusions management to prevent the use of specific models.
- Updated AJAX controller to handle image context in requests.
- Refactored prompt builder to support image context in prompts.
This commit is contained in:
2025-12-11 20:01:46 +00:00
parent 0a605cf165
commit 732c7ad393
12 changed files with 728 additions and 34 deletions

View File

@@ -88,6 +88,14 @@ class SitiWebUpdater {
$this->get_repository_info(); // Get the repo info
if ( empty( $this->github_response ) || empty( $this->github_response['tag_name'] ) ) {
return $transient;
}
if ( empty( $checked[ $this->basename ] ) ) {
return $transient;
}
$out_of_date = version_compare( $this->github_response['tag_name'], $checked[ $this->basename ], 'gt' ); // Check if we're out of date
if( $out_of_date ) {
@@ -114,11 +122,15 @@ class SitiWebUpdater {
public function plugin_popup( $result, $action, $args ) {
if( ! empty( $args->slug ) ) { // If there is a slug
if( $args->slug == current( explode( '/' , $this->basename ) ) ) { // And it's our slug
$this->get_repository_info(); // Get our repo info
if ( empty( $this->github_response ) || empty( $this->github_response['tag_name'] ) ) {
return $result;
}
// Set it to an array
$plugin = array(
'name' => $this->plugin["Name"],
@@ -175,4 +187,4 @@ class SitiWebUpdater {
return $result;
}
}
}

View File

@@ -18,6 +18,7 @@
const modelSelect = document.getElementById('groq-ai-model-select');
const refreshButton = document.getElementById('groq-ai-refresh-models');
const refreshStatus = document.getElementById('groq-ai-refresh-models-status');
const excludedModels = data.excludedModels || {};
let currentModelValue = (modelSelect && modelSelect.dataset.currentModel) || data.currentModel || '';
function toggleProviderRows() {
@@ -42,6 +43,32 @@
});
}
function isModelAllowed(model, providerOverride) {
if (!model) {
return true;
}
const provider = providerOverride || (providerSelect ? providerSelect.value : data.currentProvider);
if (!provider || !excludedModels[provider]) {
return true;
}
return excludedModels[provider].indexOf(model) === -1;
}
function ensureCurrentModelAllowed(providerOverride) {
if (!currentModelValue) {
return;
}
if (!isModelAllowed(currentModelValue, providerOverride)) {
currentModelValue = '';
if (modelSelect) {
modelSelect.dataset.currentModel = '';
}
}
}
function buildModelOptions() {
if (!modelSelect || !data.providers) {
return;
@@ -53,6 +80,8 @@
return;
}
ensureCurrentModelAllowed(provider);
const models = Array.isArray(providerData.models) ? providerData.models : [];
const frag = document.createDocumentFragment();
const placeholder = document.createElement('option');
@@ -63,6 +92,9 @@
let hasCurrent = false;
models.forEach(function (model) {
if (!isModelAllowed(model, provider)) {
return;
}
const option = document.createElement('option');
option.value = model;
option.textContent = model;
@@ -72,7 +104,7 @@
frag.appendChild(option);
});
if (currentModelValue && !hasCurrent) {
if (currentModelValue && !hasCurrent && isModelAllowed(currentModelValue, provider)) {
const extraOption = document.createElement('option');
extraOption.value = currentModelValue;
extraOption.textContent = currentModelValue;

View File

@@ -2,7 +2,7 @@
/**
* Plugin Name: SitiAI Product Teksten
* Description: Genereer productteksten met diverse AI-aanbieders rechtstreeks vanuit WooCommerce.
* Version: 1.1.1
* Version: 1.2.0
* Author: SitiAI
*/
@@ -32,6 +32,7 @@ if ( ! defined( 'GROQ_AI_DEBUG_TRACE_ADDED' ) && defined( 'WP_DEBUG' ) && WP_DEB
}
require_once __DIR__ . '/includes/Core/class-groq-ai-service-container.php';
require_once __DIR__ . '/includes/Core/class-groq-ai-model-exclusions.php';
require_once __DIR__ . '/includes/Core/class-groq-ai-ajax-controller.php';
require_once __DIR__ . '/includes/Contracts/interface-groq-ai-provider.php';
require_once __DIR__ . '/includes/Providers/class-groq-ai-abstract-openai-provider.php';
@@ -60,6 +61,7 @@ $updater->initialize();
final class Groq_AI_Product_Text_Plugin {
const OPTION_KEY = 'groq_ai_product_text_settings';
const CONVERSATION_OPTION_KEY = 'groq_ai_product_text_conversations';
const MODELS_CACHE_OPTION_KEY = 'groq_ai_product_text_models';
private static $instance = null;
@@ -260,6 +262,10 @@ final class Groq_AI_Product_Text_Plugin {
return $this->get_settings_manager()->is_response_format_compat_enabled( $settings );
}
public function get_image_context_mode( $settings = null ) {
return $this->get_settings_manager()->get_image_context_mode( $settings );
}
public function should_use_response_format( Groq_AI_Provider_Interface $provider, $settings ) {
return ! $this->is_response_format_compat_enabled( $settings ) && $provider->supports_response_format();
}
@@ -289,7 +295,78 @@ final class Groq_AI_Product_Text_Plugin {
}
public function get_selected_model( Groq_AI_Provider_Interface $provider, $settings ) {
return ! empty( $settings['model'] ) ? $settings['model'] : $provider->get_default_model();
$provider_key = $provider->get_key();
$model = ! empty( $settings['model'] ) ? $settings['model'] : '';
$model = Groq_AI_Model_Exclusions::ensure_allowed( $provider_key, $model );
if ( '' === $model ) {
$default = Groq_AI_Model_Exclusions::ensure_allowed( $provider_key, $provider->get_default_model() );
if ( '' !== $default ) {
return $default;
}
$available = Groq_AI_Model_Exclusions::filter_models( $provider_key, $provider->get_available_models() );
if ( ! empty( $available ) ) {
return $available[0];
}
}
return $model;
}
public function get_cached_models_for_provider( $provider ) {
$provider = sanitize_key( (string) $provider );
$cache = $this->get_models_cache();
return isset( $cache[ $provider ] ) ? $cache[ $provider ] : [];
}
public function update_cached_models_for_provider( $provider, $models ) {
$provider = sanitize_key( (string) $provider );
$models = $this->sanitize_models_list( $models );
$cache = $this->get_models_cache();
$cache[ $provider ] = $models;
update_option( self::MODELS_CACHE_OPTION_KEY, $cache );
return $models;
}
private function get_models_cache() {
$cache = get_option( self::MODELS_CACHE_OPTION_KEY, [] );
if ( ! is_array( $cache ) ) {
$cache = [];
}
foreach ( $cache as $provider => $models ) {
$cache[ $provider ] = $this->sanitize_models_list( $models );
}
return $cache;
}
private function sanitize_models_list( $models ) {
if ( ! is_array( $models ) ) {
return [];
}
$models = array_map( 'sanitize_text_field', $models );
$models = array_filter(
$models,
function ( $model ) {
return '' !== $model;
}
);
$models = array_values( array_unique( $models ) );
if ( ! empty( $models ) ) {
sort( $models, SORT_NATURAL | SORT_FLAG_CASE );
}
return $models;
}
public function log_debug( $message, $context = [] ) {

View File

@@ -136,6 +136,7 @@ class Groq_AI_Logs_Table extends WP_List_Table {
protected function column_created_at( $item ) {
$date = esc_html( mysql2date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $item['created_at'] ) );
$usage = $this->get_usage_meta( $item );
$payload = [
'created_at' => $item['created_at'],
'user' => $this->column_default( $item, 'user_id' ),
@@ -149,6 +150,7 @@ class Groq_AI_Logs_Table extends WP_List_Table {
'prompt' => $item['prompt'],
'response' => $item['response'],
'error_message' => $item['error_message'],
'image_context' => isset( $usage['image_context'] ) ? $usage['image_context'] : null,
];
$encoded = esc_attr( wp_json_encode( $payload ) );
return sprintf(
@@ -157,4 +159,14 @@ class Groq_AI_Logs_Table extends WP_List_Table {
$date
);
}
private function get_usage_meta( $item ) {
if ( empty( $item['usage_json'] ) ) {
return [];
}
$data = json_decode( $item['usage_json'], true );
return is_array( $data ) ? $data : [];
}
}

View File

@@ -41,6 +41,15 @@ class Groq_AI_Product_Text_Settings_Page {
[ $this, 'render_logs_page' ]
);
add_submenu_page(
'options-general.php',
__( 'Siti AI Prompt instellingen', 'groq-ai-product-text' ),
__( 'Siti AI Prompt instellingen', 'groq-ai-product-text' ),
'manage_options',
'groq-ai-product-text-prompts',
[ $this, 'render_prompt_settings_page' ]
);
}
public function hide_menu_links() {
@@ -50,7 +59,8 @@ class Groq_AI_Product_Text_Settings_Page {
?>
<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"] {
#adminmenu a[href="options-general.php?page=groq-ai-product-text-logs"],
#adminmenu a[href="options-general.php?page=groq-ai-product-text-prompts"] {
display: none !important;
}
</style>
@@ -96,36 +106,51 @@ class Groq_AI_Product_Text_Settings_Page {
);
}
add_settings_section(
'groq_ai_product_text_prompts',
__( 'Prompt instellingen', 'groq-ai-product-text' ),
'__return_false',
'groq-ai-product-text-prompts'
);
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'
'groq-ai-product-text-prompts',
'groq_ai_product_text_prompts'
);
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'
'groq-ai-product-text-prompts',
'groq_ai_product_text_prompts'
);
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'
'groq-ai-product-text-prompts',
'groq_ai_product_text_prompts'
);
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'
'groq-ai-product-text-prompts',
'groq_ai_product_text_prompts'
);
add_settings_field(
'groq_ai_image_context_mode',
__( 'Afbeeldingen toevoegen', 'groq-ai-product-text' ),
[ $this, 'render_image_context_mode_field' ],
'groq-ai-product-text-prompts',
'groq_ai_product_text_prompts'
);
add_settings_section(
@@ -144,6 +169,26 @@ class Groq_AI_Product_Text_Settings_Page {
);
}
public function render_image_context_mode_field() {
$settings = $this->plugin->get_settings();
$mode = isset( $settings['image_context_mode'] ) ? $settings['image_context_mode'] : 'url';
$options = [
'none' => __( 'Nee, geen afbeeldingen', 'groq-ai-product-text' ),
'url' => __( 'Ja, voeg afbeeldings-URLs toe aan de prompt', 'groq-ai-product-text' ),
'base64' => __( 'Ja, verstuur afbeeldingen als Base64 (indien ondersteund)', 'groq-ai-product-text' ),
];
?>
<select name="<?php echo esc_attr( $this->plugin->get_option_key() ); ?>[image_context_mode]">
<?php foreach ( $options as $value => $label ) : ?>
<option value="<?php echo esc_attr( $value ); ?>" <?php selected( $mode, $value ); ?>><?php echo esc_html( $label ); ?></option>
<?php endforeach; ?>
</select>
<p class="description">
<?php esc_html_e( 'Bepaal hoe productafbeeldingen worden meegestuurd: helemaal niet, als URLs in de prompt of als Base64-bijlagen voor modellen die beeldcontext ondersteunen.', 'groq-ai-product-text' ); ?>
</p>
<?php
}
public function render_settings_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
@@ -154,6 +199,9 @@ class Groq_AI_Product_Text_Settings_Page {
<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-prompts' ) ); ?>" class="button button-primary">
<?php esc_html_e( 'Prompt instellingen', 'groq-ai-product-text' ); ?>
</a>
<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>
@@ -161,19 +209,14 @@ class Groq_AI_Product_Text_Settings_Page {
<?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>
<p><?php esc_html_e( 'Kies je AI-aanbieder, stel de juiste API-sleutel en het gewenste model in.', 'groq-ai-product-text' ); ?></p>
<form action="options.php" method="post">
<?php
settings_fields( 'groq_ai_product_text_group' );
do_settings_sections( 'groq-ai-product-text' );
submit_button();
?>
?>
</form>
<div class="groq-ai-prompt-helper">
<h2><?php esc_html_e( 'Prompt generator', 'groq-ai-product-text' ); ?></h2>
<p><?php esc_html_e( 'Gebruik deze velden om belangrijke informatie voor de AI bij te houden (bijvoorbeeld tone of voice, USPs of doelgroepen). Voeg ze toe aan je prompt met kopiëren en plakken.', 'groq-ai-product-text' ); ?></p>
<textarea class="large-text" rows="6" readonly><?php echo esc_textarea( $this->plugin->build_prompt_template_preview( $settings ) ); ?></textarea>
</div>
</div>
<?php
}
@@ -198,6 +241,37 @@ class Groq_AI_Product_Text_Settings_Page {
<?php
}
public function render_prompt_settings_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$settings = $this->plugin->get_settings();
?>
<div class="wrap">
<h1><?php esc_html_e( 'Prompt instellingen', 'groq-ai-product-text' ); ?></h1>
<p style="margin-bottom:16px;">
<a href="<?php echo esc_url( admin_url( 'options-general.php?page=groq-ai-product-text' ) ); ?>" class="button button-secondary">
<?php esc_html_e( 'Terug naar algemene instellingen', 'groq-ai-product-text' ); ?>
</a>
</p>
<p><?php esc_html_e( 'Beheer hier de winkelcontext, standaardprompt, productcontext en response-format instellingen. Deze keuzes bepalen hoe elke prompt richting de AI wordt opgebouwd.', '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-prompts' );
submit_button();
?>
</form>
<div class="groq-ai-prompt-helper">
<h2><?php esc_html_e( 'Prompt generator', 'groq-ai-product-text' ); ?></h2>
<p><?php esc_html_e( 'Gebruik deze velden om belangrijke informatie voor de AI bij te houden (bijvoorbeeld tone of voice, USPs of doelgroepen). Voeg ze toe aan je prompt met kopiëren en plakken.', 'groq-ai-product-text' ); ?></p>
<textarea class="large-text" rows="6" readonly><?php echo esc_textarea( $this->plugin->build_prompt_template_preview( $settings ) ); ?></textarea>
</div>
</div>
<?php
}
public function render_logs_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
@@ -244,6 +318,20 @@ class Groq_AI_Product_Text_Settings_Page {
<span id="groq-ai-log-tokens-total">—</span>
</div>
</div>
<div class="groq-ai-log-images">
<div>
<strong><?php esc_html_e( 'Afbeeldingsmodus', 'groq-ai-product-text' ); ?></strong>
<span id="groq-ai-log-images-mode">—</span>
</div>
<div>
<strong><?php esc_html_e( 'Beschikbare afbeeldingen', 'groq-ai-product-text' ); ?></strong>
<span id="groq-ai-log-images-available">—</span>
</div>
<div>
<strong><?php esc_html_e( 'Base64 meegestuurd', 'groq-ai-product-text' ); ?></strong>
<span id="groq-ai-log-images-base64">—</span>
</div>
</div>
</div>
</div>
</div>
@@ -256,6 +344,7 @@ class Groq_AI_Product_Text_Settings_Page {
.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-images{display:flex;gap:20px;margin-top:10px;}
.groq-ai-log-row{display:inline-block;}
</style>
<script>
@@ -268,6 +357,9 @@ class Groq_AI_Product_Text_Settings_Page {
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 imagesMode=document.getElementById('groq-ai-log-images-mode');
const imagesAvailable=document.getElementById('groq-ai-log-images-available');
const imagesBase64=document.getElementById('groq-ai-log-images-base64');
const meta=document.querySelector('.groq-ai-log-meta');
function openModal(data){
if(!data){return;}
@@ -276,6 +368,22 @@ class Groq_AI_Product_Text_Settings_Page {
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:'—';}
const imageContext=data.image_context||null;
if(imagesMode){
let mode='—';
if(imageContext){
mode=imageContext.effective_mode||imageContext.requested_mode||'—';
}
imagesMode.textContent=mode||'—';
}
if(imagesAvailable){
const available=imageContext&&Number.isFinite(imageContext.available)?imageContext.available:'—';
imagesAvailable.textContent=available;
}
if(imagesBase64){
const base64=imageContext&&Number.isFinite(imageContext.base64_sent)?imageContext.base64_sent:'—';
imagesBase64.textContent=base64;
}
if(meta){
meta.textContent=(data.provider||'')+' • '+(data.model||'')+' • '+(data.post_title||'')+' • '+(data.status||'');
}
@@ -515,7 +623,7 @@ class Groq_AI_Product_Text_Settings_Page {
}
public function enqueue_settings_assets( $hook ) {
if ( ! in_array( $hook, [ 'settings_page_groq-ai-product-text', 'settings_page_groq-ai-product-text-modules' ], true ) ) {
if ( ! in_array( $hook, [ 'settings_page_groq-ai-product-text', 'settings_page_groq-ai-product-text-modules', 'settings_page_groq-ai-product-text-prompts' ], true ) ) {
return;
}
@@ -550,15 +658,19 @@ class Groq_AI_Product_Text_Settings_Page {
'providerRows' => [],
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'refreshNonce' => wp_create_nonce( 'groq_ai_refresh_models' ),
'excludedModels' => Groq_AI_Model_Exclusions::get_all(),
'placeholders' => [
'selectModel' => __( 'Selecteer een model via "Live modellen ophalen"', 'groq-ai-product-text' ),
],
];
foreach ( $this->provider_manager->get_providers() as $provider ) {
$provider_key = $provider->get_key();
$cached_models = $this->plugin->get_cached_models_for_provider( $provider_key );
$cached_models = Groq_AI_Model_Exclusions::filter_models( $provider_key, $cached_models );
$data['providers'][ $provider->get_key() ] = [
'default_label' => sprintf( __( 'Gebruik standaardmodel (%s)', 'groq-ai-product-text' ), $provider->get_default_model() ),
'models' => [],
'models' => $cached_models,
'supports_live' => $provider->supports_live_models(),
];
$data['providerRows'][ $provider->get_key() ] = 'groq_ai_api_key_' . $provider->get_key();

View File

@@ -18,4 +18,6 @@ interface Groq_AI_Provider_Interface {
public function fetch_live_models( $api_key );
public function supports_response_format();
public function supports_image_context();
}

View File

@@ -36,9 +36,43 @@ class Groq_AI_Ajax_Controller {
$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 );
$image_context_mode = $this->plugin->get_image_context_mode( $settings );
if ( 'none' === $image_context_mode ) {
$context_fields['images'] = false;
}
$image_context_enabled = ! empty( $context_fields['images'] );
$use_base64_payloads = $image_context_enabled && 'base64' === $image_context_mode && $provider->supports_image_context();
$image_context_count = $image_context_enabled ? $prompt_builder->get_product_image_count( $post_id ) : 0;
$prompt_image_mode = 'none';
if ( $image_context_enabled ) {
if ( $use_base64_payloads ) {
$prompt_image_mode = 'base64';
} else {
$prompt_image_mode = 'url';
}
if ( 'base64' === $image_context_mode && ! $provider->supports_image_context() ) {
$prompt_image_mode = 'url';
}
}
$product_context_text = $prompt_builder->build_product_context_block( $post_id, $context_fields, $prompt_image_mode );
$image_context_payloads = [];
if ( $use_base64_payloads ) {
$image_context_payloads = $prompt_builder->get_product_image_payloads( $post_id );
}
$prompt_with_context = $prompt_builder->prepend_context_to_prompt( $prompt, $product_context_text );
$image_context_meta = [
'requested_mode' => $image_context_mode,
'effective_mode' => $prompt_image_mode,
'available' => $image_context_count,
'base64_sent' => $use_base64_payloads ? count( $image_context_payloads ) : 0,
];
$response_format = null;
$use_response_format = $this->plugin->should_use_response_format( $provider, $settings );
if ( $use_response_format ) {
@@ -57,6 +91,7 @@ class Groq_AI_Ajax_Controller {
'temperature' => 0.7,
'conversation_id' => $conversation_id,
'response_format' => $response_format,
'image_context' => $image_context_payloads,
]
);
@@ -67,7 +102,9 @@ class Groq_AI_Ajax_Controller {
'model' => $model,
'prompt' => $final_prompt,
'response' => '',
'usage' => [],
'usage' => [
'image_context' => $image_context_meta,
],
'post_id' => $post_id,
'status' => 'error',
'error_message' => $result->get_error_message(),
@@ -78,6 +115,10 @@ class Groq_AI_Ajax_Controller {
$response_text = $this->extract_content_text( $result );
$response_usage = is_array( $result ) && isset( $result['usage'] ) ? $result['usage'] : [];
if ( ! is_array( $response_usage ) ) {
$response_usage = [];
}
$response_usage['image_context'] = $image_context_meta;
$response = $prompt_builder->parse_structured_response( $response_text, $settings );
@@ -143,7 +184,10 @@ class Groq_AI_Ajax_Controller {
wp_send_json_error( [ 'message' => $result->get_error_message() ], 500 );
}
wp_send_json_success( [ 'models' => array_values( array_unique( $result ) ) ] );
$models = Groq_AI_Model_Exclusions::filter_models( $provider_key, array_values( array_unique( $result ) ) );
$models = $this->plugin->update_cached_models_for_provider( $provider_key, $models );
wp_send_json_success( [ 'models' => $models ] );
}
private function extract_content_text( $result ) {

View File

@@ -0,0 +1,155 @@
<?php
final class Groq_AI_Model_Exclusions {
private const DEFAULT_EXCLUSIONS = [
'groq' => [
'playai-tts-arabic',
'moonshotai/kimi-k2-instruct',
'meta-llama/llama-prompt-guard-2-22m',
'groq/compound-mini',
'meta-llama/llama-guard-4-12b',
'openai/gpt-oss-20b',
'groq/compound',
'openai/gpt-oss-safeguard-20b',
'whisper-large-v3-turbo',
'meta-llama/llama-4-scout-17b-16e-instruct',
'allam-2-7b',
'playai-tts',
'moonshotai/kimi-k2-instruct-0905',
'whisper-large-v3',
'meta-llama/llama-prompt-guard-2-86m',
],
'openai' => [],
'google' => [
'embedding-gecko-001',
'embedding-001',
'text-embedding-004',
'gemini-embedding-exp-03-07',
'gemini-embedding-exp',
'gemini-embedding-001',
'gemini-2.5-flash-image-preview',
'gemini-2.5-flash-image',
'gemini-2.5-flash-preview-tts',
'gemini-2.5-pro-preview-tts',
'gemini-2.5-flash-native-audio-latest',
'gemini-2.5-flash-native-audio-preview-09-2025',
'gemini-2.5-computer-use-preview-10-2025',
'gemini-3-pro-image-preview',
'nano-banana-pro-preview',
'gemini-robotics-er-1.5-preview',
'deep-research-pro-preview-12-2025',
'aqa',
'imagen-4.0-generate-preview-06-06',
'imagen-4.0-ultra-generate-preview-06-06',
'imagen-4.0-generate-001',
'imagen-4.0-ultra-generate-001',
'imagen-4.0-fast-generate-001',
'veo-2.0-generate-001',
'veo-3.0-generate-001',
'veo-3.0-fast-generate-001',
'veo-3.1-generate-preview',
'veo-3.1-fast-generate-preview',
],
];
/**
* Geeft de volledige lijst met uitgesloten modellen terug, gegroepeerd per aanbieder.
*
* @return array<string, string[]>
*/
public static function get_all() {
$list = apply_filters( 'groq_ai_model_exclusions', self::DEFAULT_EXCLUSIONS );
if ( ! is_array( $list ) ) {
$list = [];
}
$normalized = [];
foreach ( $list as $provider => $models ) {
$normalized[ self::normalize_provider( $provider ) ] = self::normalize_models_list( $models );
}
return $normalized;
}
/**
* @param string $provider
* @return string[]
*/
public static function get_for_provider( $provider ) {
$provider = self::normalize_provider( $provider );
$list = self::get_all();
return isset( $list[ $provider ] ) ? $list[ $provider ] : [];
}
public static function is_excluded( $provider, $model ) {
if ( '' === $model ) {
return false;
}
$model = sanitize_text_field( $model );
$provider = self::normalize_provider( $provider );
$list = self::get_for_provider( $provider );
return in_array( $model, $list, true );
}
/**
* @param string $provider
* @param string $model
* @return string
*/
public static function ensure_allowed( $provider, $model ) {
if ( self::is_excluded( $provider, $model ) ) {
return '';
}
return $model;
}
/**
* @param string $provider
* @param array $models
* @return array
*/
public static function filter_models( $provider, $models ) {
if ( ! is_array( $models ) ) {
return [];
}
$provider = self::normalize_provider( $provider );
return array_values(
array_filter(
array_map(
'sanitize_text_field',
$models
),
function ( $model ) use ( $provider ) {
return ! self::is_excluded( $provider, $model );
}
)
);
}
private static function normalize_provider( $provider ) {
return sanitize_key( (string) $provider );
}
private static function normalize_models_list( $models ) {
if ( ! is_array( $models ) ) {
$models = [];
}
$models = array_map( 'sanitize_text_field', $models );
$models = array_filter(
$models,
function ( $model ) {
return '' !== $model;
}
);
return array_values( array_unique( $models ) );
}
}

View File

@@ -138,4 +138,8 @@ abstract class Groq_AI_Abstract_OpenAI_Provider implements Groq_AI_Provider_Inte
$field = $this->get_option_key();
return isset( $settings[ $field ] ) ? $settings[ $field ] : '';
}
public function supports_image_context() {
return false;
}
}

View File

@@ -33,6 +33,10 @@ class Groq_AI_Provider_Google implements Groq_AI_Provider_Interface {
return false;
}
public function supports_image_context() {
return true;
}
public function fetch_live_models( $api_key ) {
$endpoint = add_query_arg(
[ 'key' => $api_key, 'pageSize' => 100 ],
@@ -92,15 +96,59 @@ class Groq_AI_Provider_Google implements Groq_AI_Provider_Interface {
sprintf( 'https://generativelanguage.googleapis.com/v1beta/models/%s:generateContent', rawurlencode( $model ) )
);
$image_context = isset( $args['image_context'] ) && is_array( $args['image_context'] ) ? $args['image_context'] : [];
$parts = [];
if ( '' !== trim( (string) $system_prompt ) ) {
$parts[] = [
'text' => $system_prompt,
];
}
if ( '' !== trim( (string) $prompt ) ) {
$parts[] = [
'text' => $prompt,
];
}
if ( ! empty( $image_context ) ) {
foreach ( $image_context as $image ) {
if ( empty( $image['data'] ) ) {
continue;
}
$label = isset( $image['label'] ) ? trim( (string) $image['label'] ) : '';
if ( '' !== $label ) {
$parts[] = [
'text' => sprintf(
/* translators: %s: image label */
__( 'Contextafbeelding: %s', 'groq-ai-product-text' ),
$label
),
];
}
$parts[] = [
'inline_data' => [
'mime_type' => ! empty( $image['mime_type'] ) ? $image['mime_type'] : 'image/jpeg',
'data' => $image['data'],
],
];
}
}
if ( empty( $parts ) ) {
$parts[] = [
'text' => $prompt,
];
}
$payload = [
'contents' => [
[
'role' => 'user',
'parts' => [
[
'text' => $system_prompt . "\n\n" . $prompt,
],
],
'parts' => $parts,
],
],
'generationConfig' => [

View File

@@ -125,7 +125,7 @@ class Groq_AI_Prompt_Builder {
return $normalized;
}
public function build_product_context_block( $post_id, $fields ) {
public function build_product_context_block( $post_id, $fields, $image_mode = 'url' ) {
$post_id = absint( $post_id );
if ( ! $post_id ) {
@@ -162,6 +162,13 @@ class Groq_AI_Prompt_Builder {
}
}
if ( ! empty( $fields['images'] ) && 'url' === $image_mode ) {
$images = $this->get_product_images_text( $post_id );
if ( $images ) {
$parts[] = sprintf( __( 'Afbeeldingen: %s', 'groq-ai-product-text' ), $images );
}
}
return implode( "\n\n", array_filter( $parts ) );
}
@@ -350,4 +357,144 @@ class Groq_AI_Prompt_Builder {
return implode( '; ', $lines );
}
private function get_product_images_text( $post_id ) {
$image_ids = $this->get_product_image_ids( $post_id );
if ( empty( $image_ids ) ) {
return '';
}
$entries = [];
foreach ( $image_ids as $index => $attachment_id ) {
$descriptor = $this->describe_product_image( $attachment_id, $index + 1 );
if ( ! $descriptor ) {
continue;
}
$entries[] = sprintf( '%s - %s', $descriptor['label'], $descriptor['url'] );
}
return implode( '; ', array_filter( $entries ) );
}
public function get_product_image_payloads( $post_id, $limit = 3, $max_filesize = 1572864 ) {
$image_ids = array_slice( $this->get_product_image_ids( $post_id ), 0, max( 1, (int) $limit ) );
if ( empty( $image_ids ) ) {
return [];
}
$payloads = [];
foreach ( $image_ids as $index => $attachment_id ) {
$descriptor = $this->describe_product_image( $attachment_id, $index + 1 );
if ( ! $descriptor || empty( $descriptor['path'] ) ) {
continue;
}
$path = $descriptor['path'];
if ( ! file_exists( $path ) || ! is_readable( $path ) ) {
continue;
}
$filesize = filesize( $path );
if ( false !== $filesize && $filesize > $max_filesize ) {
continue;
}
$data = @file_get_contents( $path );
if ( false === $data ) {
continue;
}
$payloads[] = [
'attachment_id' => $attachment_id,
'label' => $descriptor['label'],
'mime_type' => $descriptor['mime_type'],
'data' => base64_encode( $data ),
'url' => $descriptor['url'],
];
}
return $payloads;
}
public function get_product_image_count( $post_id ) {
return count( $this->get_product_image_ids( $post_id ) );
}
private function get_product_image_ids( $post_id ) {
$post_id = absint( $post_id );
if ( ! $post_id ) {
return [];
}
$image_ids = [];
$featured_id = get_post_thumbnail_id( $post_id );
if ( $featured_id ) {
$image_ids[] = $featured_id;
}
$gallery_ids = [];
if ( function_exists( 'wc_get_product' ) ) {
$product = wc_get_product( $post_id );
if ( $product ) {
$gallery_ids = (array) $product->get_gallery_image_ids();
}
}
if ( empty( $gallery_ids ) ) {
$raw_gallery = get_post_meta( $post_id, '_product_image_gallery', true );
if ( is_string( $raw_gallery ) && '' !== trim( $raw_gallery ) ) {
$gallery_ids = array_filter( array_map( 'absint', explode( ',', $raw_gallery ) ) );
}
}
if ( ! empty( $gallery_ids ) ) {
$image_ids = array_merge( $image_ids, $gallery_ids );
}
return array_values( array_unique( array_filter( array_map( 'absint', $image_ids ) ) ) );
}
private function describe_product_image( $attachment_id, $position ) {
$url = wp_get_attachment_url( $attachment_id );
if ( ! $url ) {
return null;
}
$label = trim( (string) get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ) );
if ( '' === $label ) {
$label = get_the_title( $attachment_id );
}
$label = trim( wp_strip_all_tags( (string) $label ) );
if ( '' === $label ) {
$label = sprintf( __( 'Afbeelding %d', 'groq-ai-product-text' ), $position );
}
$path = get_attached_file( $attachment_id );
$mime = get_post_mime_type( $attachment_id );
if ( ! $mime && $path ) {
$mime = wp_get_image_mime( $path );
}
if ( $mime && 0 !== strpos( $mime, 'image/' ) ) {
$mime = '';
}
return [
'attachment_id' => $attachment_id,
'label' => $label,
'url' => esc_url_raw( $url ),
'path' => $path,
'mime_type' => $mime ? $mime : 'image/jpeg',
];
}
}

View File

@@ -37,6 +37,7 @@ class Groq_AI_Settings_Manager {
'google_api_key' => '',
'context_fields' => $this->get_default_context_fields(),
'modules' => $this->get_default_modules_settings(),
'image_context_mode' => 'url',
'response_format_compat' => false,
];
@@ -44,6 +45,19 @@ class Groq_AI_Settings_Manager {
$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'] : [] );
$settings['model'] = Groq_AI_Model_Exclusions::ensure_allowed( $settings['provider'], isset( $settings['model'] ) ? $settings['model'] : '' );
$image_mode = isset( $settings['image_context_mode'] ) ? sanitize_text_field( $settings['image_context_mode'] ) : 'url';
if ( 'none' === $image_mode ) {
$settings['context_fields']['images'] = false;
$settings['image_context_mode'] = 'none';
} elseif ( in_array( $image_mode, [ 'url', 'base64' ], true ) ) {
$settings['context_fields']['images'] = true;
$settings['image_context_mode'] = $image_mode;
} else {
$settings['context_fields']['images'] = true;
$settings['image_context_mode'] = 'url';
}
return $settings;
}
@@ -65,6 +79,7 @@ class Groq_AI_Settings_Manager {
'google_api_key' => '',
'context_fields' => $this->get_default_context_fields(),
'modules' => $this->get_default_modules_settings(),
'image_context_mode' => 'url',
'response_format_compat' => false,
];
@@ -80,16 +95,34 @@ class Groq_AI_Settings_Manager {
$provider = 'groq';
}
$model = sanitize_text_field( $input['model'] );
$model = Groq_AI_Model_Exclusions::ensure_allowed( $provider, $model );
$image_mode = isset( $input['image_context_mode'] ) ? sanitize_text_field( $input['image_context_mode'] ) : $defaults['image_context_mode'];
$allowed_modes = [ 'none', 'base64', 'url' ];
if ( ! in_array( $image_mode, $allowed_modes, true ) ) {
$image_mode = 'url';
}
$context_fields = $this->normalize_context_fields( $context_posted ? $raw_input['context_fields'] : $defaults['context_fields'] );
if ( 'none' === $image_mode ) {
$context_fields['images'] = false;
} else {
$context_fields['images'] = true;
}
return [
'provider' => $provider,
'model' => sanitize_text_field( $input['model'] ),
'model' => $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'] ),
'image_context_mode' => $image_mode,
'context_fields' => $context_fields,
'modules' => $this->sanitize_modules_settings(
$modules_posted ? $raw_input['modules'] : [],
$defaults['modules'],
@@ -122,6 +155,11 @@ class Groq_AI_Settings_Manager {
'description' => __( 'Voeg gestructureerde productattributen toe (zoals kleur, maat, materiaal).', 'groq-ai-product-text' ),
'default' => false,
],
'images' => [
'label' => __( 'Afbeeldingen', 'groq-ai-product-text' ),
'description' => __( 'Voeg een korte lijst toe met productafbeeldingen (beschrijving + URL).', 'groq-ai-product-text' ),
'default' => false,
],
];
}
@@ -219,6 +257,17 @@ class Groq_AI_Settings_Manager {
return max( 200, min( 2000, $value ) );
}
public function get_image_context_mode( $settings = null ) {
if ( null === $settings ) {
$settings = $this->all();
}
$mode = isset( $settings['image_context_mode'] ) ? sanitize_text_field( $settings['image_context_mode'] ) : 'url';
$allowed_modes = [ 'none', 'base64', 'url' ];
return in_array( $mode, $allowed_modes, true ) ? $mode : 'url';
}
public function is_response_format_compat_enabled( $settings = null ) {
if ( null === $settings ) {
$settings = $this->all();