This commit is contained in:
2026-02-16 00:43:39 +01:00
parent afe9d6216d
commit 59c086384b
10 changed files with 578 additions and 36 deletions

View File

@@ -8,7 +8,8 @@
"WebFetch(domain:help.kliken.com)",
"WebFetch(domain:www.storegrowers.com)",
"WebFetch(domain:platform.openai.com)",
"WebFetch(domain:openai.com)"
"WebFetch(domain:openai.com)",
"Bash(sass:*)"
]
}
}

View File

@@ -33,16 +33,22 @@
"lmtime": 1769729268048,
"modified": false
},
"class.Clients.php": {
"type": "-",
"size": 1443,
"lmtime": 0,
"modified": false
},
"class.Cron.php": {
"type": "-",
"size": 22459,
"size": 27380,
"lmtime": 1764273350638,
"modified": false
"modified": true
},
"class.Products.php": {
"type": "-",
"size": 14277,
"lmtime": 1769726635358,
"size": 17445,
"lmtime": 1771171703249,
"modified": false
},
"class.Site.php": {
@@ -53,8 +59,8 @@
},
"class.Users.php": {
"type": "-",
"size": 3091,
"lmtime": 0,
"size": 4159,
"lmtime": 1771170208170,
"modified": false
}
},
@@ -65,6 +71,12 @@
"lmtime": 1769729268049,
"modified": false
},
"class.Clients.php": {
"type": "-",
"size": 732,
"lmtime": 0,
"modified": false
},
"class.Cron.php": {
"type": "-",
"size": 26120,
@@ -73,8 +85,8 @@
},
"class.Products.php": {
"type": "-",
"size": 6956,
"lmtime": 1769726808348,
"size": 7481,
"lmtime": 1771170224109,
"modified": false
},
"class.Users.php": {
@@ -83,6 +95,28 @@
"lmtime": 0,
"modified": false
}
},
"services": {
"class.GoogleAdsApi.php": {
"type": "-",
"size": 8751,
"lmtime": 0,
"modified": false
},
"class.OpenAiApi.php": {
"type": "-",
"size": 12013,
"lmtime": 1771171891986,
"modified": false
}
}
},
".claude": {
"settings.local.json": {
"type": "-",
"size": 359,
"lmtime": 1771170998225,
"modified": false
}
},
"config.php": {
@@ -97,16 +131,17 @@
"lmtime": 0,
"modified": false
},
"docs": {},
".htaccess": {
"type": "-",
"size": 612,
"size": 601,
"lmtime": 0,
"modified": false
"modified": true
},
"index.php": {
"type": "-",
"size": 2456,
"lmtime": 0,
"size": 3756,
"lmtime": 1771170199988,
"modified": false
},
"layout": {
@@ -118,20 +153,32 @@
},
"style.css": {
"type": "-",
"size": 19795,
"lmtime": 1763678459404,
"size": 33580,
"lmtime": 1771173952595,
"modified": false
},
"style.css.map": {
"type": "-",
"size": 36969,
"lmtime": 1763678459404,
"size": 42515,
"lmtime": 1771169108538,
"modified": false
},
"style-old.css": {
"type": "-",
"size": 19795,
"lmtime": 0,
"modified": false
},
"style-old.scss": {
"type": "-",
"size": 25910,
"lmtime": 0,
"modified": false
},
"style.scss": {
"type": "-",
"size": 25910,
"lmtime": 1763678459031,
"size": 36710,
"lmtime": 1771173944772,
"modified": false
}
},
@@ -151,6 +198,26 @@
}
}
},
"migrations": {
"001_google_ads_settings.sql": {
"type": "-",
"size": 889,
"lmtime": 0,
"modified": false
},
"demo_data.sql": {
"type": "-",
"size": 18951,
"lmtime": 0,
"modified": false
},
"002_products_data_url.sql": {
"type": "-",
"size": 86,
"lmtime": 1771171362268,
"modified": false
}
},
"robots.txt": {
"type": "-",
"size": 25,
@@ -161,8 +228,8 @@
"products": {
"main_view.php": {
"type": "-",
"size": 19064,
"lmtime": 1770756800564,
"size": 21771,
"lmtime": 1771173883840,
"modified": false
},
"product_history.php": {
@@ -199,6 +266,20 @@
"lmtime": 1769729268050,
"modified": false
}
},
"users": {
"login-form.php": {
"type": "-",
"size": 3273,
"lmtime": 0,
"modified": false
},
"settings.php": {
"type": "-",
"size": 8355,
"lmtime": 1771171025998,
"modified": false
}
}
},
"tmp": {},

View File

@@ -85,11 +85,33 @@ class Products
{
$product_id = \S::get( 'product_id' );
$field = \S::get( 'field' );
$provider = \S::get( 'provider' ) ?: 'openai';
if ( !\services\OpenAiApi::is_configured() )
if ( $provider === 'claude' )
{
echo json_encode( [ 'status' => 'error', 'message' => 'Klucz API OpenAI nie jest skonfigurowany. Przejdź do Ustawień.' ] );
exit;
if ( \services\GoogleAdsApi::get_setting( 'claude_enabled' ) === '0' )
{
echo json_encode( [ 'status' => 'error', 'message' => 'Claude jest wyłączony. Włącz go w Ustawieniach.' ] );
exit;
}
if ( !\services\ClaudeApi::is_configured() )
{
echo json_encode( [ 'status' => 'error', 'message' => 'Klucz API Claude nie jest skonfigurowany. Przejdź do Ustawień.' ] );
exit;
}
}
else
{
if ( \services\GoogleAdsApi::get_setting( 'openai_enabled' ) === '0' )
{
echo json_encode( [ 'status' => 'error', 'message' => 'OpenAI jest wyłączony. Włącz go w Ustawieniach.' ] );
exit;
}
if ( !\services\OpenAiApi::is_configured() )
{
echo json_encode( [ 'status' => 'error', 'message' => 'Klucz API OpenAI nie jest skonfigurowany. Przejdź do Ustawień.' ] );
exit;
}
}
$product = \factory\Products::get_product_full_context( $product_id );
@@ -124,21 +146,25 @@ class Products
'page_content' => $page_content,
];
$api = $provider === 'claude' ? \services\ClaudeApi::class : \services\OpenAiApi::class;
switch ( $field )
{
case 'title':
$result = \services\OpenAiApi::suggest_title( $context );
$result = $api::suggest_title( $context );
break;
case 'description':
$result = \services\OpenAiApi::suggest_description( $context );
$result = $api::suggest_description( $context );
break;
case 'category':
$result = \services\OpenAiApi::suggest_category( $context );
$result = $api::suggest_category( $context );
break;
default:
$result = [ 'status' => 'error', 'message' => 'Nieznane pole: ' . $field ];
}
$result['provider'] = $provider;
if ( $product_url && !$page_content )
$result['warning'] = 'Nie udało się pobrać treści ze strony produktu (strona może blokować dostęp). Sugestia oparta tylko na nazwie produktu.';
elseif ( $page_content )

View File

@@ -101,6 +101,7 @@ class Users
public static function settings_save_openai()
{
\services\GoogleAdsApi::set_setting( 'openai_enabled', \S::get( 'openai_enabled' ) ? '1' : '0' );
\services\GoogleAdsApi::set_setting( 'openai_api_key', \S::get( 'openai_api_key' ) );
\services\GoogleAdsApi::set_setting( 'openai_model', \S::get( 'openai_model' ) );
@@ -109,6 +110,17 @@ class Users
exit;
}
public static function settings_save_claude()
{
\services\GoogleAdsApi::set_setting( 'claude_enabled', \S::get( 'claude_enabled' ) ? '1' : '0' );
\services\GoogleAdsApi::set_setting( 'claude_api_key', \S::get( 'claude_api_key' ) );
\services\GoogleAdsApi::set_setting( 'claude_model', \S::get( 'claude_model' ) );
\S::alert( 'Ustawienia Claude zostały zapisane.' );
header( 'Location: /settings' );
exit;
}
public static function login()
{
if ( $user = \factory\Users::login(

View File

@@ -0,0 +1,248 @@
<?php
namespace services;
class ClaudeApi
{
static private $api_url = 'https://api.anthropic.com/v1/messages';
static private $system_prompt = 'Jesteś ekspertem od optymalizacji feedów produktowych Google Merchant Center i reklam Google Ads (Performance Max, Shopping).
OFICJALNE ZASADY GOOGLE MERCHANT CENTER — BEZWZGLĘDNIE PRZESTRZEGAJ:
TYTUŁY (title) — max 150 znaków:
Zalecana struktura: [Marka] [Typ produktu] [Kluczowe atrybuty: kolor, rozmiar, materiał — TYLKO jeśli wynikają z nazwy]
- Najważniejsze informacje umieść w pierwszych 70 znakach (reszta może być obcięta na mobile)
- Każdy wariant produktu musi mieć unikalny tytuł (różne kolory/rozmiary = różne tytuły)
- Tytuł MUSI być spójny z nazwą produktu na stronie docelowej
- Używaj naturalnego języka — pisz dla ludzi, nie dla algorytmów
ZAKAZANE w tytułach:
- Tekst promocyjny: bestseller, hit, okazja, wyprzedaż, rabat, najlepszy, TOP, #1, idealny, polecamy, nowość
- Wykrzykniki (!) i nadmierna interpunkcja
- WIELKIE LITERY (wyjątek: ustalone skróty jak LED, USB, TV)
- Emotikony, emoji, symbole dekoracyjne (★, ♥, ✓)
- Informacje o cenie, dostawie, promocji
- Wezwania do działania (kup teraz, sprawdź, zamów)
- Upychanie słów kluczowych (keyword stuffing) — np. powtarzanie tych samych fraz
- Kody wewnętrzne, SKU, numery katalogowe
- Cechy produktu których NIE MA w oryginalnej nazwie (nie wymyślaj koloru, materiału, rozmiaru)
OPISY (description) — max 5000 znaków, ale najważniejsze info w pierwszych 160-500 znakach:
- Opisz cechy, specyfikacje techniczne, zastosowanie, grupę docelową
- Wymień atrybuty niewidoczne na zdjęciu (materiał, wzór, przeznaczenie wiekowe)
- Ton neutralny, informacyjny — jak w katalogu produktowym
ZAKAZANE w opisach:
- Tekst promocyjny i reklamowy (te same słowa co w tytułach)
- Nazwa firmy/sklepu (chyba że to marka produktu)
- Informacje o wysyłce, cenach, promocjach
- Wezwania do działania
- Opisy innych produktów, akcesoriów nie wchodzących w skład oferty
- Tekst zastępczy (lorem ipsum, placeholder)
- Powielanie tego samego opisu dla wielu produktów
- HTML, tagi formatowania
OGÓLNE:
- Pisz poprawną polszczyzną (ortografia, gramatyka, interpunkcja)
- Używaj pisowni zdaniowej lub tytułowej — NIE CAPS
- Dane w feedzie MUSZĄ zgadzać się z danymi na stronie produktowej
- Opisuj TYLKO cechy wynikające z nazwy produktu — nie wymyślaj
Twoje odpowiedzi muszą być:
- Ściśle zgodne z polityką Google Merchant Center
- Zgodne z prawdą (opisuj tylko cechy widoczne w nazwie produktu)
- Zoptymalizowane pod kątem trafności wyszukiwań w Google Ads
- W języku polskim';
static public function is_configured()
{
return (bool) GoogleAdsApi::get_setting( 'claude_api_key' );
}
static private function call_api( $system_prompt, $user_prompt, $max_tokens = 500 )
{
$api_key = GoogleAdsApi::get_setting( 'claude_api_key' );
$model = GoogleAdsApi::get_setting( 'claude_model' ) ?: 'claude-sonnet-4-5-20250929';
$payload = [
'model' => $model,
'max_tokens' => $max_tokens,
'system' => $system_prompt,
'messages' => [
[ 'role' => 'user', 'content' => $user_prompt ]
]
];
$ch = curl_init( self::$api_url );
curl_setopt_array( $ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'x-api-key: ' . $api_key,
'anthropic-version: 2023-06-01'
],
CURLOPT_POSTFIELDS => json_encode( $payload ),
CURLOPT_TIMEOUT => 60
] );
$response = curl_exec( $ch );
$http_code = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
curl_close( $ch );
if ( $http_code !== 200 )
{
$error = json_decode( $response, true );
return [ 'status' => 'error', 'message' => $error['error']['message'] ?? 'Błąd API Claude (HTTP ' . $http_code . ')' ];
}
$data = json_decode( $response, true );
$content = '';
if ( !empty( $data['content'] ) )
{
foreach ( $data['content'] as $block )
{
if ( $block['type'] === 'text' )
{
$content .= $block['text'];
}
}
}
$content = trim( $content );
return [ 'status' => 'ok', 'suggestion' => $content ];
}
static private function build_context_text( $context )
{
$lines = [];
$lines[] = 'Nazwa produktu: ' . ( $context['original_name'] ?? '—' );
if ( !empty( $context['current_title'] ) )
$lines[] = 'Obecny tytuł (custom): ' . $context['current_title'];
if ( !empty( $context['current_description'] ) )
$lines[] = 'Obecny opis: ' . $context['current_description'];
if ( !empty( $context['current_category'] ) )
$lines[] = 'Obecna kategoria Google: ' . $context['current_category'];
if ( !empty( $context['offer_id'] ) )
$lines[] = 'ID oferty: ' . $context['offer_id'];
if ( !empty( $context['custom_label_4'] ) )
$lines[] = 'Status produktu: ' . $context['custom_label_4'];
$lines[] = '';
$lines[] = 'Metryki reklamowe (ostatnie 30 dni):';
$lines[] = '- Wyświetlenia: ' . ( $context['impressions_30'] ?? 0 );
$lines[] = '- Kliknięcia: ' . ( $context['clicks_30'] ?? 0 );
$lines[] = '- CTR: ' . ( $context['ctr'] ?? 0 ) . '%';
$lines[] = '- Koszt: ' . ( $context['cost'] ?? 0 ) . ' PLN';
$lines[] = '- Konwersje: ' . ( $context['conversions'] ?? 0 );
$lines[] = '- Wartość konwersji: ' . ( $context['conversions_value'] ?? 0 ) . ' PLN';
$lines[] = '- ROAS: ' . ( $context['roas'] ?? 0 ) . '%';
if ( !empty( $context['page_content'] ) )
{
$lines[] = '';
$lines[] = 'Treść ze strony produktu (użyj tych informacji do stworzenia dokładniejszego opisu):';
$lines[] = $context['page_content'];
}
return implode( "\n", $lines );
}
static public function suggest_title( $context )
{
$context_text = self::build_context_text( $context );
$prompt = 'Zaproponuj zoptymalizowany tytuł produktu dla Google Merchant Center.
WYMAGANIA:
- Max 150 znaków, najważniejsze info w pierwszych 70 znakach
- Struktura: [Marka jeśli jest w nazwie] [Typ produktu] [Kluczowe cechy z nazwy] [Dla kogo/okazja jeśli wynika z nazwy]
- Pisownia zdaniowa lub tytułowa, naturalny język
- Tytuł musi brzmieć jak wpis w katalogu produktowym
BEZWZGLĘDNY ZAKAZ (odrzucenie przez Google):
- Słowa promocyjne: bestseller, hit, idealny, najlepszy, polecamy, okazja, nowość, TOP
- Wykrzykniki (!), emotikony, symbole dekoracyjne
- WIELKIE LITERY (wyjątek: LED, USB, TV)
- Wezwania do działania, informacje o cenie/dostawie
- Cechy wymyślone — opisuj TYLKO to co wynika z oryginalnej nazwy lub treści strony produktu
- Jeśli podano treść ze strony produktu, wykorzystaj ją do wzbogacenia tytułu o rzeczywiste cechy (marka, materiał, kolor, rozmiar itp.)
' . $context_text . '
Zwróć TYLKO tytuł, bez cudzysłowów, bez wyjaśnień.';
return self::call_api( self::$system_prompt, $prompt );
}
static public function suggest_description( $context )
{
$context_text = self::build_context_text( $context );
$has_page = !empty( $context['page_content'] );
$length_guide = $has_page
? '- Napisz rozbudowany, szczegółowy opis: ok. 1000 znaków (800-1200)
- Wykorzystaj szczegóły ze strony produktu: skład zestawu, materiały, wymiary, kolory, przeznaczenie
- Każdy akapit/punkt powinien wnosić NOWĄ informację — NIE powtarzaj tych samych elementów'
: '- Napisz opis o długości ok. 1000 znaków (800-1200) — jeśli brak szczegółów, opisz ogólnie zastosowanie i grupę docelową
- Opisuj TYLKO to co wynika z oryginalnej nazwy — nie wymyślaj konkretnych parametrów';
$prompt = 'Napisz zoptymalizowany opis produktu dla Google Merchant Center.
WYMAGANIA:
' . $length_guide . '
- Najważniejsze info na początku (pierwsze 160 znaków widoczne w wynikach)
- Rzeczowo opisz: co to jest, z czego się składa, do czego służy, dla kogo
- Ton neutralny, informacyjny — jak w specyfikacji produktowej
- Każdy akapit musi zawierać INNĄ informację niż poprzedni — NIGDY nie powtarzaj tych samych elementów
FORMATOWANIE — Google Merchant Center obsługuje podstawowe HTML:
- Używaj <br> do oddzielania akapitów/sekcji
- Używaj <b>pogrubienia</b> dla kluczowych cech (np. nazwy elementów zestawu, materiał)
- Używaj <ul><li>...</li></ul> gdy wymieniasz elementy zestawu lub listę cech
- NIE używaj innych tagów HTML (h1, p, div, span, img, a, table itp.)
- NIE używaj Markdown — tylko dozwolone tagi HTML
BEZWZGLĘDNY ZAKAZ (odrzucenie przez Google):
- Słowa promocyjne: bestseller, hit, okazja, najlepszy, polecamy, nowość, idealny
- Wykrzykniki (!), emotikony, WIELKIE LITERY
- Wezwania do działania: kup teraz, zamów, sprawdź, dodaj do koszyka
- Informacje o cenie, dostawie, promocjach, nazwie sklepu
- Opisy akcesoriów/produktów nie wchodzących w skład oferty
- Cechy wymyślone — opisuj TYLKO to co wynika z nazwy lub treści strony produktu
' . $context_text . '
Zwróć TYLKO opis w formacie HTML (używając dozwolonych tagów), bez cudzysłowów, bez wyjaśnień.';
$tokens = $has_page ? 1500 : 1000;
return self::call_api( self::$system_prompt, $prompt, $tokens );
}
static public function suggest_category( $context, $categories = [] )
{
$context_text = self::build_context_text( $context );
$cats_text = '';
if ( !empty( $categories ) )
{
$cats_list = array_slice( $categories, 0, 50 );
$cats_text = "\n\nDostępne kategorie Google Product Taxonomy (format: id - tekst):\n";
foreach ( $cats_list as $cat )
{
$cats_text .= $cat['id'] . ' - ' . $cat['text'] . "\n";
}
}
$prompt = 'Dopasuj produkt do najlepszej kategorii Google Product Taxonomy.
Wybierz najbardziej szczegółową (najgłębszą) kategorię pasującą do produktu.
' . $context_text . $cats_text . '
Zwróć TYLKO ID kategorii (liczbę), bez wyjaśnień.';
return self::call_api( self::$system_prompt, $prompt );
}
}

View File

@@ -45,6 +45,7 @@ $route_aliases = [
'settings/save' => ['users', 'settings_save'],
'settings/save_google_ads' => ['users', 'settings_save_google_ads'],
'settings/save_openai' => ['users', 'settings_save_openai'],
'settings/save_claude' => ['users', 'settings_save_claude'],
'products/ai_suggest' => ['products', 'ai_suggest'],
'clients/save' => ['clients', 'save'],
];

View File

@@ -718,6 +718,44 @@ table {
.settings-card .settings-input-wrap .settings-toggle-pw:hover {
color: #6690F4;
}
.settings-card .settings-toggle-label {
display: inline-flex;
align-items: center;
gap: 10px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
user-select: none;
}
.settings-card .settings-toggle-checkbox {
display: none;
}
.settings-card .settings-toggle-checkbox + .settings-toggle-switch {
position: relative;
width: 44px;
height: 24px;
background: #ccc;
border-radius: 12px;
transition: background 0.2s;
flex-shrink: 0;
}
.settings-card .settings-toggle-checkbox + .settings-toggle-switch::after {
content: "";
position: absolute;
top: 3px;
left: 3px;
width: 18px;
height: 18px;
background: #fff;
border-radius: 50%;
transition: transform 0.2s;
}
.settings-card .settings-toggle-checkbox:checked + .settings-toggle-switch {
background: #22C55E;
}
.settings-card .settings-toggle-checkbox:checked + .settings-toggle-switch::after {
transform: translateX(20px);
}
.settings-card .settings-fields-grid {
display: grid;
grid-template-columns: 1fr 1fr;
@@ -1380,6 +1418,16 @@ table {
opacity: 0.7;
cursor: wait;
}
.btn-ai-suggest.btn-ai-claude {
border-color: #D97706;
background: linear-gradient(135deg, #FEF3C7, #FDE68A);
color: #92400E;
}
.btn-ai-suggest.btn-ai-claude:hover {
background: linear-gradient(135deg, #D97706, #B45309);
color: #FFF;
border-color: #B45309;
}
.form_container {
background: #FFFFFF;

View File

@@ -882,6 +882,50 @@ table {
}
}
.settings-toggle-label {
display: inline-flex;
align-items: center;
gap: 10px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
user-select: none;
}
.settings-toggle-checkbox {
display: none;
& + .settings-toggle-switch {
position: relative;
width: 44px;
height: 24px;
background: #ccc;
border-radius: 12px;
transition: background 0.2s;
flex-shrink: 0;
&::after {
content: '';
position: absolute;
top: 3px;
left: 3px;
width: 18px;
height: 18px;
background: #fff;
border-radius: 50%;
transition: transform 0.2s;
}
}
&:checked + .settings-toggle-switch {
background: #22C55E;
&::after {
transform: translateX(20px);
}
}
}
.settings-fields-grid {
display: grid;
grid-template-columns: 1fr 1fr;
@@ -1649,6 +1693,18 @@ table {
opacity: 0.7;
cursor: wait;
}
&.btn-ai-claude {
border-color: #D97706;
background: linear-gradient(135deg, #FEF3C7, #FDE68A);
color: #92400E;
&:hover {
background: linear-gradient(135deg, #D97706, #B45309);
color: #FFF;
border-color: #B45309;
}
}
}
// --- Form container ---

View File

@@ -58,7 +58,14 @@
</div>
</div>
<?php
$openai_enabled = \services\GoogleAdsApi::get_setting( 'openai_enabled' ) !== '0';
$claude_enabled = \services\GoogleAdsApi::get_setting( 'claude_enabled' ) !== '0';
?>
<script type="text/javascript">
var AI_OPENAI_ENABLED = <?= $openai_enabled ? 'true' : 'false'; ?>;
var AI_CLAUDE_ENABLED = <?= $claude_enabled ? 'true' : 'false'; ?>;
function show_toast( message, type )
{
var bg = type === 'error' ? '#dc3545' : '#28a745';
@@ -263,7 +270,8 @@ $( function()
'<label>Tytuł produktu</label>' +
'<div class="input-with-ai">' +
'<input type="text" value="" product_id="' + $( this ).attr( 'product_id' ) + '" placeholder="Tytuł produktu" class="name form-control" required />' +
'<button type="button" class="btn btn-sm btn-ai-suggest" data-field="title" title="Zaproponuj tytuł przez AI"><i class="fa-solid fa-wand-magic-sparkles"></i> AI</button>' +
( 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>' : '' ) +
'</div>' +
'<small>0/150 znaków</small>' +
'</div>' +
@@ -284,7 +292,8 @@ $( function()
'<textarea class="form-control description" style="height:220px;resize:vertical" placeholder="Opis produktu (opcjonalnie)"></textarea>' +
'<div class="desc-preview" style="display:none;height:220px;overflow-y:auto;padding:10px 12px;border:1px solid #ddd;border-radius:4px;background:#fff;font-size:13px;line-height:1.6"></div>' +
'</div>' +
'<button type="button" class="btn btn-sm btn-ai-suggest" data-field="description" title="Zaproponuj opis przez AI"><i class="fa-solid fa-wand-magic-sparkles"></i> AI</button>' +
( 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>' : '' ) +
'</div>' +
'</div>' +
'<div class="form-group">' +
@@ -293,7 +302,8 @@ $( function()
'<select class="form-control google-category" id="google_category" style="width: calc(100% - 60px)">' +
'<option value="">— wybierz kategorię —</option>' +
'</select>' +
'<button type="button" class="btn btn-sm btn-ai-suggest" data-field="category" title="Zaproponuj kategorię przez AI"><i class="fa-solid fa-wand-magic-sparkles"></i> AI</button>' +
( 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>' : '' ) +
'</div>' +
'</div>' +
'</form>',
@@ -420,14 +430,16 @@ $( function()
this.$content.on( 'click', '.btn-ai-suggest', function() {
var $btn = $( this );
var field = $btn.data( 'field' );
var provider = $btn.data( 'provider' ) || 'openai';
var originalHtml = $btn.html();
var providerLabel = provider === 'claude' ? 'Claude' : 'ChatGPT';
$btn.prop( 'disabled', true ).html( '<i class="fa-solid fa-spinner fa-spin"></i>' );
$.ajax({
url: '/products/ai_suggest/',
type: 'POST',
data: { product_id: product_id, field: field, product_url: $productUrl.val() },
data: { product_id: product_id, field: field, product_url: $productUrl.val(), provider: provider },
success: function( response ) {
var data = JSON.parse( response );
if ( data.status == 'ok' ) {
@@ -443,20 +455,22 @@ $( function()
if ( $googleCategory.find( 'option[value="' + catId + '"]' ).length ) {
$googleCategory.val( catId ).trigger( 'change' );
} else {
$.alert({ title: 'AI sugestia', content: 'Sugerowana kategoria: ' + catId, type: 'blue' });
$.alert({ title: 'AI sugestia (' + providerLabel + ')', content: 'Sugerowana kategoria: ' + catId, type: 'blue' });
}
}
if ( data.warning ) {
show_toast( data.warning, 'error' );
show_toast( providerLabel + ': ' + data.warning, 'error' );
} else if ( data.page_fetched ) {
show_toast( 'Sugestia wygenerowana z treścią strony produktu', 'success' );
show_toast( providerLabel + ': Sugestia wygenerowana z treścią strony produktu', 'success' );
} else {
show_toast( providerLabel + ': Sugestia wygenerowana', 'success' );
}
} else {
show_toast( data.message || 'Wystąpił błąd AI.', 'error' );
show_toast( providerLabel + ': ' + ( data.message || 'Wystąpił błąd AI.' ), 'error' );
}
},
error: function() {
$.alert({ title: 'Błąd', content: 'Nie udało się połączyć z API.', type: 'red' });
$.alert({ title: 'Błąd', content: 'Nie udało się połączyć z API ' + providerLabel + '.', type: 'red' });
},
complete: function() {
$btn.prop( 'disabled', false ).html( originalHtml );

View File

@@ -103,6 +103,15 @@
</div>
</div>
<form method="POST" id="openai-settings" action="/settings/save_openai">
<div class="settings-field" style="margin-bottom: 16px;">
<label class="settings-toggle-label">
<?php $openai_enabled = \services\GoogleAdsApi::get_setting( 'openai_enabled' ) !== '0'; ?>
<input type="hidden" name="openai_enabled" value="0" />
<input type="checkbox" name="openai_enabled" value="1" class="settings-toggle-checkbox" <?= $openai_enabled ? 'checked' : ''; ?> />
<span class="settings-toggle-switch"></span>
<span>Włącz OpenAI (ChatGPT)</span>
</label>
</div>
<div class="settings-fields-grid">
<div class="settings-field">
<label for="openai_api_key">API Key</label>
@@ -132,6 +141,52 @@
</div>
</div>
<div class="row" style="margin-top: 25px;">
<div class="col-12">
<div class="settings-card">
<div class="settings-card-header">
<div class="settings-card-icon"><i class="fa-solid fa-brain"></i></div>
<div>
<h3>Claude (Anthropic)</h3>
<small>Klucz API i model Claude do optymalizacji tytułów i opisów produktów przez AI</small>
</div>
</div>
<form method="POST" id="claude-settings" action="/settings/save_claude">
<div class="settings-field" style="margin-bottom: 16px;">
<label class="settings-toggle-label">
<?php $claude_enabled = \services\GoogleAdsApi::get_setting( 'claude_enabled' ) !== '0'; ?>
<input type="hidden" name="claude_enabled" value="0" />
<input type="checkbox" name="claude_enabled" value="1" class="settings-toggle-checkbox" <?= $claude_enabled ? 'checked' : ''; ?> />
<span class="settings-toggle-switch"></span>
<span>Włącz Claude (Anthropic)</span>
</label>
</div>
<div class="settings-fields-grid">
<div class="settings-field">
<label for="claude_api_key">API Key</label>
<div class="settings-input-wrap">
<input type="password" id="claude_api_key" name="claude_api_key" class="form-control" value="<?= htmlspecialchars( \services\GoogleAdsApi::get_setting( 'claude_api_key' ) ); ?>" placeholder="sk-ant-..." />
<button type="button" class="settings-toggle-pw" onclick="password_toggle( this, 'claude_api_key' )">
<i class="fa-solid fa-eye"></i>
</button>
</div>
</div>
<div class="settings-field">
<label for="claude_model">Model</label>
<?php $current_claude_model = \services\GoogleAdsApi::get_setting( 'claude_model' ) ?: 'claude-sonnet-4-5-20250929'; ?>
<select id="claude_model" name="claude_model" class="form-control">
<option value="claude-opus-4-6" <?= $current_claude_model === 'claude-opus-4-6' ? 'selected' : ''; ?>>Claude Opus 4.6 (najpotężniejszy)</option>
<option value="claude-sonnet-4-5-20250929" <?= $current_claude_model === 'claude-sonnet-4-5-20250929' ? 'selected' : ''; ?>>Claude Sonnet 4.5 (zbalansowany)</option>
<option value="claude-haiku-4-5-20251001" <?= $current_claude_model === 'claude-haiku-4-5-20251001' ? 'selected' : ''; ?>>Claude Haiku 4.5 (szybki, tani)</option>
</select>
</div>
</div>
<button type="button" class="btn btn-success" onclick="$( '#claude-settings' ).submit();"><i class="fa-solid fa-check mr5"></i>Zapisz ustawienia Claude</button>
</form>
</div>
</div>
</div>
<script type="text/javascript">
function password_toggle( btn, id )
{