diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index 099e738..dfc3df7 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -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:*)"
]
}
}
diff --git a/.vscode/ftp-kr.sync.cache.json b/.vscode/ftp-kr.sync.cache.json
index 1eb8b0a..deb87ce 100644
--- a/.vscode/ftp-kr.sync.cache.json
+++ b/.vscode/ftp-kr.sync.cache.json
@@ -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": {},
diff --git a/autoload/controls/class.Products.php b/autoload/controls/class.Products.php
index cc77640..820f8ee 100644
--- a/autoload/controls/class.Products.php
+++ b/autoload/controls/class.Products.php
@@ -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 )
diff --git a/autoload/controls/class.Users.php b/autoload/controls/class.Users.php
index dd2ee36..58eb883 100644
--- a/autoload/controls/class.Users.php
+++ b/autoload/controls/class.Users.php
@@ -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(
diff --git a/autoload/services/class.ClaudeApi.php b/autoload/services/class.ClaudeApi.php
new file mode 100644
index 0000000..ff2efd8
--- /dev/null
+++ b/autoload/services/class.ClaudeApi.php
@@ -0,0 +1,248 @@
+ $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
do oddzielania akapitów/sekcji
+- Używaj pogrubienia dla kluczowych cech (np. nazwy elementów zestawu, materiał)
+- Używaj