feat: Add Gemini AI integration for product title and description optimization

- Implemented Gemini API service for generating optimized product titles and descriptions based on Google Merchant Center guidelines.
- Added settings for Gemini API key and model selection in user settings.
- Enhanced product management views to support AI-generated suggestions for titles and descriptions.
- Enabled state saving for various data tables across campaign, terms, logs, and products views.
- Introduced AI prompt templates for generating product descriptions and categories.
This commit is contained in:
2026-02-22 00:44:03 +01:00
parent 192eb11f66
commit 7573312038
17 changed files with 1259 additions and 536 deletions

View File

@@ -104,10 +104,12 @@
<?php
$openai_enabled = \services\GoogleAdsApi::get_setting( 'openai_enabled' ) !== '0';
$claude_enabled = \services\GoogleAdsApi::get_setting( 'claude_enabled' ) !== '0';
$gemini_enabled = \services\GoogleAdsApi::get_setting( 'gemini_enabled' ) !== '0';
?>
<script type="text/javascript">
var AI_OPENAI_ENABLED = <?= $openai_enabled ? 'true' : 'false'; ?>;
var AI_CLAUDE_ENABLED = <?= $claude_enabled ? 'true' : 'false'; ?>;
var AI_GEMINI_ENABLED = <?= $gemini_enabled ? 'true' : 'false'; ?>;
var PRODUCTS_COLUMNS_STORAGE_KEY = 'products.columns.visibility';
var PRODUCTS_LOCKED_COLUMNS = [ 0, 20 ];
@@ -308,6 +310,7 @@ function products_render_columns_picker( table_instance )
$( function()
{
var products_table = new DataTable( '#products', {
stateSave: true,
ajax: {
type: 'POST',
url: '/products/get_products/',
@@ -372,7 +375,7 @@ $( function()
products_table.ajax.reload( null, false );
}
function submit_delete_campaign_ad_group( campaign_id, ad_group_id, delete_scope )
function submit_delete_campaign_ad_group( campaign_id, ad_group_id, delete_scope, on_success )
{
function parse_json_loose( raw )
{
@@ -412,6 +415,12 @@ $( function()
function handle_success( message )
{
show_toast( message || 'Grupa reklam zostala usunieta.', 'success' );
if ( typeof on_success === 'function' )
{
on_success();
}
localStorage.removeItem( 'products_ad_group_id' );
load_products_ad_groups( campaign_id, '' ).done( function() {
$.when( load_scope_alerts(), load_zero_impressions_products() ).always( function() {
@@ -1138,7 +1147,12 @@ $( function()
btnClass: 'btn-default',
action: function()
{
return submit_delete_campaign_ad_group( campaign_id, ad_group_id, 'local' );
var modal = this;
submit_delete_campaign_ad_group( campaign_id, ad_group_id, 'local', function()
{
modal.close();
} );
return false;
}
},
google: {
@@ -1146,7 +1160,12 @@ $( function()
btnClass: 'btn-red',
action: function()
{
return submit_delete_campaign_ad_group( campaign_id, ad_group_id, 'google' );
var modal = this;
submit_delete_campaign_ad_group( campaign_id, ad_group_id, 'google', function()
{
modal.close();
} );
return false;
}
},
cancel: {
@@ -1383,8 +1402,10 @@ $( function()
'<input type="text" value="" product_id="' + $( this ).attr( 'product_id' ) + '" placeholder="Tytuł produktu" class="name form-control" required />' +
( AI_OPENAI_ENABLED ? '<button type="button" class="btn btn-sm btn-ai-suggest" data-field="title" data-provider="openai" title="Zaproponuj tytuł przez ChatGPT"><i class="fa-solid fa-wand-magic-sparkles"></i> GPT</button>' : '' ) +
( AI_CLAUDE_ENABLED ? '<button type="button" class="btn btn-sm btn-ai-suggest btn-ai-claude" data-field="title" data-provider="claude" title="Zaproponuj tytuł przez Claude"><i class="fa-solid fa-brain"></i> Claude</button>' : '' ) +
( AI_GEMINI_ENABLED ? '<button type="button" class="btn btn-sm btn-ai-suggest btn-ai-gemini" data-field="title" data-provider="gemini" title="Zaproponuj tytuł przez Gemini"><i class="fa-solid fa-diamond"></i> Gemini</button>' : '' ) +
'</div>' +
'<small>0/150 znaków</small>' +
'<div class="title-ai-alternatives" style="margin-top:8px;display:none;"></div>' +
'</div>' +
'<div class="form-group">' +
'<label>URL strony produktu <small class="text-muted">(opcjonalnie, dla lepszego kontekstu AI)</small></label>' +
@@ -1405,6 +1426,7 @@ $( function()
'</div>' +
( AI_OPENAI_ENABLED ? '<button type="button" class="btn btn-sm btn-ai-suggest" data-field="description" data-provider="openai" title="Zaproponuj opis przez ChatGPT"><i class="fa-solid fa-wand-magic-sparkles"></i> GPT</button>' : '' ) +
( AI_CLAUDE_ENABLED ? '<button type="button" class="btn btn-sm btn-ai-suggest btn-ai-claude" data-field="description" data-provider="claude" title="Zaproponuj opis przez Claude"><i class="fa-solid fa-brain"></i> Claude</button>' : '' ) +
( AI_GEMINI_ENABLED ? '<button type="button" class="btn btn-sm btn-ai-suggest btn-ai-gemini" data-field="description" data-provider="gemini" title="Zaproponuj opis przez Gemini"><i class="fa-solid fa-diamond"></i> Gemini</button>' : '' ) +
'</div>' +
'</div>' +
'<div class="form-group">' +
@@ -1415,6 +1437,7 @@ $( function()
'</select>' +
( AI_OPENAI_ENABLED ? '<button type="button" class="btn btn-sm btn-ai-suggest" data-field="category" data-provider="openai" title="Zaproponuj kategorię przez ChatGPT"><i class="fa-solid fa-wand-magic-sparkles"></i> GPT</button>' : '' ) +
( AI_CLAUDE_ENABLED ? '<button type="button" class="btn btn-sm btn-ai-suggest btn-ai-claude" data-field="category" data-provider="claude" title="Zaproponuj kategorię przez Claude"><i class="fa-solid fa-brain"></i> Claude</button>' : '' ) +
( AI_GEMINI_ENABLED ? '<button type="button" class="btn btn-sm btn-ai-suggest btn-ai-gemini" data-field="category" data-provider="gemini" title="Zaproponuj kategorię przez Gemini"><i class="fa-solid fa-diamond"></i> Gemini</button>' : '' ) +
'</div>' +
'</div>' +
'</form>',
@@ -1478,8 +1501,56 @@ $( function()
var $description = this.$content.find( '.description' );
var $productUrl = this.$content.find( '.product-url' );
var $googleCategory = this.$content.find( '.google-category' );
var $titleAlternatives = this.$content.find( '.title-ai-alternatives' );
var product_id = $inputField.attr( 'product_id' );
function set_title_value( value ) {
value = String( value || '' );
$inputField.val( value );
var len = value.length;
$charCount.text( len + '/150 znaków' );
$inputField.toggleClass( 'is-invalid', len > 150 );
}
function render_title_alternatives( bestTitle, candidates ) {
var current = $.trim( String( bestTitle || '' ) );
var seen = {};
var list = [];
( candidates || [] ).forEach( function( item ) {
var title = $.trim( String( item || '' ) );
if ( !title ) {
return;
}
var key = title.toLowerCase();
if ( key === current.toLowerCase() || seen[ key ] ) {
return;
}
seen[ key ] = true;
list.push( title );
} );
if ( !list.length ) {
$titleAlternatives.hide().empty();
return;
}
var html = '<div class="js-title-alts-list" style="margin-top:8px;"><small class="text-muted">Alternatywy:</small>';
list.forEach( function( title, idx ) {
html += '<div style="margin-top:4px;">'
+ '<button type="button" class="btn btn-xs btn-default js-title-alt-apply" data-title-alt="' + escape_html( title ) + '" style="width:100%;text-align:left;">'
+ ( idx + 1 ) + '. ' + escape_html( title )
+ '</button>'
+ '</div>';
} );
html += '</div>';
$titleAlternatives.html( html ).show();
}
$.ajax({
url: '/products/get_product_data/',
type: 'POST',
@@ -1488,8 +1559,7 @@ $( function()
var data = JSON.parse( response );
if ( data.status == 'ok' ) {
if ( data.product_details.title ) {
$inputField.val( data.product_details.title );
$charCount.text( data.product_details.title.length + '/150 znaków' );
set_title_value( data.product_details.title );
}
if ( data.product_details.description ) {
$description.val( data.product_details.description );
@@ -1544,7 +1614,7 @@ $( function()
var field = $btn.data( 'field' );
var provider = $btn.data( 'provider' ) || 'openai';
var originalHtml = $btn.html();
var providerLabel = provider === 'claude' ? 'Claude' : 'ChatGPT';
var providerLabel = provider === 'claude' ? 'Claude' : ( provider === 'gemini' ? 'Gemini' : 'ChatGPT' );
$btn.prop( 'disabled', true ).html( '<i class="fa-solid fa-spinner fa-spin"></i>' );
@@ -1556,10 +1626,8 @@ $( function()
var data = JSON.parse( response );
if ( data.status == 'ok' ) {
if ( field == 'title' ) {
$inputField.val( data.suggestion );
var len = data.suggestion.length;
$charCount.text( len + '/150 znaków' );
$inputField.toggleClass( 'is-invalid', len > 150 );
set_title_value( data.suggestion );
render_title_alternatives( data.suggestion, data.title_candidates || [] );
} else if ( field == 'description' ) {
$description.val( data.suggestion );
} else if ( field == 'category' ) {
@@ -1598,6 +1666,22 @@ $( function()
});
});
this.$content.on( 'click', '.js-title-alts-toggle', function() {
var $list = jc.$content.find( '.js-title-alts-list' );
$list.toggle();
$( this ).html(
$list.is( ':visible' )
? '<i class="fa-solid fa-eye-slash"></i> Ukryj alternatywy'
: '<i class="fa-solid fa-list"></i> Pokaż alternatywy (' + $list.find( '.js-title-alt-apply' ).length + ')'
);
} );
this.$content.on( 'click', '.js-title-alt-apply', function() {
var selectedTitle = $( this ).attr( 'data-title-alt' ) || '';
set_title_value( selectedTitle );
} );
$form.on( 'submit', function( e ) {
e.preventDefault();
jc.$$formSubmit.trigger( 'click' );