update
This commit is contained in:
@@ -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:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
121
.vscode/ftp-kr.sync.cache.json
vendored
121
.vscode/ftp-kr.sync.cache.json
vendored
@@ -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": {},
|
||||
|
||||
@@ -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 )
|
||||
|
||||
@@ -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(
|
||||
|
||||
248
autoload/services/class.ClaudeApi.php
Normal file
248
autoload/services/class.ClaudeApi.php
Normal 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 );
|
||||
}
|
||||
}
|
||||
@@ -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'],
|
||||
];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 ---
|
||||
|
||||
@@ -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 );
|
||||
|
||||
@@ -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 )
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user