PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 1.0.2
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v1.0.2
2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.10.0 All 48 releases
thinkrank / includes / ai / class-manager.php

class-manager.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 1.0.2, at includes/ai/class-manager.php

831 lines 28.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * AI Manager Class
4 *
5 * Handles AI provider integration and management
6 *
7 * @package ThinkRank\AI
8 * @since 1.0.0
9 */
10
11 declare(strict_types=1);
12
13 namespace ThinkRank\AI;
14
15 use ThinkRank\Core\Settings;
16
17
18 // Prevent direct access
19 if (!defined('ABSPATH')) {
20 exit;
21 }
22
23 /**
24 * AI Manager Class
25 *
26 * Single Responsibility: Manage AI providers and requests
27 *
28 * @since 1.0.0
29 */
30 class Manager {
31
32 /**
33 * Settings instance
34 *
35 * @var Settings
36 */
37 private Settings $settings;
38
39
40
41 /**
42 * Cache manager instance
43 *
44 * @var Cache_Manager
45 */
46 private Cache_Manager $cache;
47
48 /**
49 * Current AI client
50 *
51 * @var OpenAI_Client|Claude_Client|null
52 */
53 private $client = null;
54
55 /**
56 * Rate limiter
57 *
58 * @var array
59 */
60 private array $rate_limits = [];
61
62 /**
63 * Constructor
64 *
65 * @param Settings|null $settings Settings instance
66 */
67 public function __construct(?Settings $settings = null) {
68 $this->settings = $settings ?? new Settings();
69 $this->cache = new Cache_Manager((int) $this->settings->get('cache_duration', 3600));
70 }
71
72 /**
73 * Initialize AI manager
74 *
75 * @return void
76 */
77 public function init(): void {
78 // Initialize AI client based on settings
79 add_action('init', [$this, 'initialize_client']);
80
81 // Schedule cache cleanup
82 add_action('thinkrank_daily_cleanup', [$this, 'cleanup_cache']);
83
84 // Add AJAX handlers for AI requests
85 add_action('wp_ajax_thinkrank_generate_metadata', [$this, 'ajax_generate_metadata']);
86 add_action('wp_ajax_thinkrank_test_api_connection', [$this, 'ajax_test_connection']);
87 }
88
89 /**
90 * Initialize AI client
91 *
92 * @return void
93 */
94 public function initialize_client(): void {
95 $provider = $this->settings->get('ai_provider', 'openai');
96
97 try {
98 switch ($provider) {
99 case 'openai':
100 $api_key = $this->settings->get('openai_api_key');
101 if ($api_key) {
102 $model = $this->settings->get('openai_model', 'gpt-5-nano');
103
104 $available_models = $this->get_available_providers()['openai']['models'];
105 if (!in_array($model, $available_models, true)) {
106 $model = 'gpt-5-nano';
107 }
108 // Use 120-second timeout for complex AI operations
109 $timeout = 120;
110 $this->client = new OpenAI_Client($api_key, $model, $timeout);
111
112 // OpenAI client created successfully
113 }
114 break;
115
116 case 'claude':
117 $api_key = $this->settings->get('claude_api_key');
118 if ($api_key) {
119 $model = $this->settings->get('claude_model', 'claude-3-7-sonnet-latest');
120
121 $available_models = $this->get_available_providers()['claude']['models'];
122 if (!in_array($model, $available_models, true)) {
123 $model = 'claude-3-7-sonnet-latest';
124 }
125 // Use 120-second timeout for complex AI operations
126 $timeout = 120;
127 $this->client = new Claude_Client($api_key, $model, $timeout);
128
129 // Claude client created successfully
130 }
131 break;
132
133 case 'gemini':
134 $api_key = $this->settings->get('gemini_api_key');
135 if ($api_key) {
136 $model = $this->settings->get('gemini_model', 'gemini-2.5-flash');
137
138 $available_models = $this->get_available_providers()['gemini']['models'];
139 if (!in_array($model, $available_models, true)) {
140 $model = 'gemini-2.5-flash';
141 }
142 // Use 120-second timeout for complex AI operations
143 $timeout = 120;
144 $this->client = new Gemini_Client($api_key, $model, $timeout);
145 }
146 break;
147
148 default:
149 throw new \Exception("Unsupported AI provider: {$provider}");
150 }
151 } catch (\Exception $e) {
152 // AI client initialization failed, will be handled later
153 }
154 }
155
156 /**
157 * Force re-initialization of client (useful after settings change)
158 *
159 * @return void
160 */
161 public function reinitialize_client(): void {
162 $this->client = null;
163 $this->initialize_client();
164 }
165
166 /**
167 * Get the AI client instance
168 *
169 * @return OpenAI_Client|Claude_Client|null AI client instance
170 * @throws \Exception If client cannot be initialized
171 */
172 public function get_client() {
173 // Initialize client if not already done
174 if (!$this->client) {
175 $this->initialize_client();
176 }
177
178 // If still not available, throw error
179 if (!$this->client) {
180 throw new \Exception('AI client not initialized. Please configure your API key.');
181 }
182
183 return $this->client;
184 }
185
186 /**
187 * Generate SEO metadata for content
188 *
189 * @param string $content Content to analyze
190 * @param array $options Generation options
191 * @return array Generated metadata
192 * @throws \Exception If generation fails
193 */
194 public function generate_seo_metadata(string $content, array $options = []): array {
195 // Check if client is available, try to initialize if not
196 if (!$this->client) {
197 $this->initialize_client();
198
199 // If still not available, throw error
200 if (!$this->client) {
201 throw new \Exception('AI client not initialized. Please configure your API key.');
202 }
203 }
204
205 // Check rate limits
206 if (!$this->check_rate_limit()) {
207 throw new \Exception('Rate limit exceeded. Please try again later.');
208 }
209
210 // Get current user for logging
211 $user_id = get_current_user_id();
212
213 // Generate cache key
214 $cache_key = $this->cache->generate_content_key($content, $options);
215
216 // Check cache first
217 $cached_result = $this->cache->get($cache_key);
218 if ($cached_result !== null) {
219 return $cached_result['data'];
220 }
221
222 try {
223 // Generate metadata using AI
224 $metadata = $this->client->generate_seo_metadata($content, $options);
225
226 // Ensure user has configured their API key
227 $user_has_api_key = !empty($this->settings->get('openai_api_key')) || !empty($this->settings->get('claude_api_key')) || !empty($this->settings->get('gemini_api_key'));
228
229 if (!$user_has_api_key) {
230 throw new \Exception('Please configure your OpenAI, Claude, or Gemini API key in ThinkRank settings to use AI features.');
231 }
232
233 // Cache the result
234 $this->cache->set($cache_key, $metadata);
235
236 // Log usage with actual model information and raw AI text (Content Brief pattern)
237 $actual_model = $this->client ? $this->client->get_model() : null;
238 $ai_text = $metadata['_ai_text'] ?? null;
239 $this->log_ai_usage($user_id, 'SEO Metadata', $metadata['tokens_used'] ?? 0, $actual_model, $ai_text);
240
241 // Remove AI text from returned data to keep it clean
242 unset($metadata['_ai_text']);
243
244 return $metadata;
245
246 } catch (\Exception $e) {
247 throw $e;
248 }
249 }
250
251 /**
252 * Analyze content for SEO optimization
253 *
254 * @param string $content Content to analyze
255 * @param array $metadata Existing metadata
256 * @return array Analysis results
257 * @throws \Exception If analysis fails
258 */
259 public function analyze_content(string $content, array $metadata = []): array {
260 if (!$this->client) {
261 throw new \Exception('AI client not initialized');
262 }
263
264 $user_id = get_current_user_id();
265
266 // Ensure user has configured their API key
267 $user_has_api_key = !empty($this->settings->get('openai_api_key')) || !empty($this->settings->get('claude_api_key')) || !empty($this->settings->get('gemini_api_key'));
268
269 if (!$user_has_api_key) {
270 throw new \Exception('Please configure your OpenAI, Claude, or Gemini API key in ThinkRank settings to use AI features.');
271 }
272
273 // Check rate limits
274 if (!$this->check_rate_limit($user_id, 'content_analysis')) {
275 throw new \Exception('Rate limit exceeded. Please try again later.');
276 }
277
278 // Check cache first
279 $cache_key = 'content_analysis_' . md5($content . serialize($metadata));
280 $cached_result = $this->cache->get($cache_key);
281 if ($cached_result) {
282 return $cached_result;
283 }
284
285 try {
286 // Analyze content using AI
287 $analysis = $this->client->analyze_content($content, $metadata);
288
289 // Cache the result
290 $this->cache->set($cache_key, $analysis);
291
292 // Log usage with actual model information and raw AI text (Content Brief pattern)
293 $actual_model = $this->client ? $this->client->get_model() : null;
294 $ai_text = $analysis['_ai_text'] ?? null;
295 $this->log_ai_usage($user_id, 'Content Analysis', $analysis['tokens_used'] ?? 0, $actual_model, $ai_text);
296
297 // Remove AI text from returned data to keep it clean
298 unset($analysis['_ai_text']);
299
300 return $analysis;
301
302 } catch (\Exception $e) {
303 throw $e;
304 }
305 }
306
307 /**
308 * Test API connection
309 *
310 * @return array Test result
311 */
312 public function test_api_connection(): array {
313 if (!$this->client) {
314 return [
315 'success' => false,
316 'message' => 'AI client not initialized. Please configure your API key.',
317 ];
318 }
319
320 try {
321 $success = $this->client->test_connection();
322
323 return [
324 'success' => $success,
325 'message' => $success
326 ? 'API connection successful!'
327 : 'API connection failed. Please check your API key.',
328 ];
329
330 } catch (\Exception $e) {
331 return [
332 'success' => false,
333 'message' => 'Connection test failed: ' . $e->getMessage(),
334 ];
335 }
336 }
337
338 /**
339 * Optimize site identity using AI
340 *
341 * @since 1.0.0
342 *
343 * @param array $site_data Site data to optimize
344 * @param array $options Optimization options
345 * @return array Optimization results
346 * @throws \Exception If optimization fails
347 */
348 public function optimize_site_identity(array $site_data, array $options = []): array {
349 // Validate input
350 if (empty($site_data)) {
351 throw new \Exception('Site data cannot be empty');
352 }
353
354 $user_id = get_current_user_id();
355
356 // Ensure user has configured their API key
357 $user_has_api_key = !empty($this->settings->get('openai_api_key')) || !empty($this->settings->get('claude_api_key')) || !empty($this->settings->get('gemini_api_key'));
358
359 if (!$user_has_api_key) {
360 throw new \Exception('Please configure your OpenAI, Claude, or Gemini API key in ThinkRank settings to use AI features.');
361 }
362
363 // Generate cache key using existing pattern
364 $cache_key = 'site_identity_' . md5(serialize($site_data) . serialize($options)) . '_' . $user_id;
365
366 // Check existing cache infrastructure
367 $cached_result = $this->cache->get($cache_key);
368 if ($cached_result !== null && !empty($cached_result['optimized_data'])) {
369 return $cached_result;
370 }
371
372 // Check rate limiting
373 if (!$this->check_rate_limit()) {
374 throw new \Exception('Rate limit exceeded for AI optimization requests.');
375 }
376
377 // Get AI client
378 $client = $this->get_client();
379
380 if (!$client) {
381 throw new \Exception('AI client not initialized. Please check your API key configuration.');
382 }
383
384 // Perform AI optimization
385 $optimization_results = $client->optimize_site_identity($site_data, $options);
386
387 // Validate that we got meaningful results
388 if (empty($optimization_results) || empty($optimization_results['optimized_data'])) {
389 throw new \Exception('AI optimization returned empty results. Please try again.');
390 }
391
392 // Add metadata
393 $optimization_results['ai_model'] = $client->get_model();
394 $optimization_results['provider'] = $this->settings->get('ai_provider', 'openai');
395 $optimization_results['generated_at'] = gmdate('Y-m-d H:i:s');
396 $optimization_results['user_id'] = $user_id;
397
398 // Cache the results (24 hours)
399 $this->cache->set($cache_key, $optimization_results, 86400);
400
401 // Record usage with actual model information and raw AI text (Content Brief pattern)
402 $actual_model = $optimization_results['ai_model'] ?? ($client ? $client->get_model() : null);
403 $ai_text = $optimization_results['_ai_text'] ?? null;
404 $this->log_ai_usage($user_id, 'Site Identity Optimization', $optimization_results['tokens_used'] ?? 0, $actual_model, $ai_text);
405
406 // Remove AI text from returned data to keep it clean
407 unset($optimization_results['_ai_text']);
408
409 return $optimization_results;
410 }
411
412 /**
413 * Optimize LLMs.txt content using AI
414 *
415 * @since 1.0.0
416 *
417 * @param array $website_data Website data to optimize
418 * @param array $options Optimization options
419 * @return array Optimization results
420 * @throws \Exception If optimization fails
421 */
422 public function optimize_llms_txt(array $website_data, array $options = []): array {
423 // Validate input
424 if (empty($website_data)) {
425 throw new \Exception('Website data cannot be empty');
426 }
427
428 $user_id = get_current_user_id();
429
430 // Ensure user has configured their API key
431 $user_has_api_key = !empty($this->settings->get('openai_api_key')) || !empty($this->settings->get('claude_api_key')) || !empty($this->settings->get('gemini_api_key'));
432
433 if (!$user_has_api_key) {
434 throw new \Exception('Please configure your OpenAI, Claude, or Gemini API key in ThinkRank settings to use AI features.');
435 }
436
437 // Generate cache key
438 $cache_key = 'llms_txt_' . md5(serialize($website_data) . serialize($options)) . '_' . $user_id;
439
440 // Check cache first
441 $cached_result = $this->cache->get($cache_key);
442 if ($cached_result !== null && !empty($cached_result['optimized_data'])) {
443 return $cached_result;
444 }
445
446 // Check rate limiting
447 if (!$this->check_rate_limit()) {
448 throw new \Exception('Rate limit exceeded for AI optimization requests.');
449 }
450
451 // Get AI client
452 $client = $this->get_client();
453
454 if (!$client) {
455 throw new \Exception('AI client not initialized. Please check your API key configuration.');
456 }
457
458 // Perform AI optimization
459 $optimization_results = $client->optimize_llms_txt($website_data, $options);
460
461 // Validate that we got meaningful results
462 if (empty($optimization_results) || empty($optimization_results['optimized_data'])) {
463 throw new \Exception('AI optimization returned empty results. Please try again.');
464 }
465
466 // Add metadata
467 $optimization_results['ai_model'] = $client->get_model();
468 $optimization_results['provider'] = $this->settings->get('ai_provider', 'openai');
469 $optimization_results['generated_at'] = gmdate('Y-m-d H:i:s');
470 $optimization_results['user_id'] = $user_id;
471
472 // Cache the results (24 hours)
473 $this->cache->set($cache_key, $optimization_results, 86400);
474
475 // Record usage with actual model information and raw AI text (Content Brief pattern)
476 $actual_model = $optimization_results['ai_model'] ?? ($client ? $client->get_model() : null);
477 $ai_text = $optimization_results['_ai_text'] ?? null;
478 $this->log_ai_usage($user_id, 'LLMs.txt Optimization', $optimization_results['tokens_used'] ?? 0, $actual_model, $ai_text);
479
480 // Remove AI text from returned data to keep it clean
481 unset($optimization_results['_ai_text']);
482
483 return $optimization_results;
484 }
485
486 /**
487 * Get available AI providers
488 *
489 * @return array Available providers
490 */
491 public function get_available_providers(): array {
492 return [
493 'openai' => [
494 'name' => 'OpenAI',
495 'description' => 'GPT‑5 series and GPT‑4o',
496 'models' => ['gpt-5-nano', 'gpt-5-mini', 'gpt-5', 'gpt-4o'],
497 'requires_key' => true,
498 ],
499 'claude' => [
500 'name' => 'Claude (Anthropic)',
501 'description' => 'Claude 4 and 3.7 models',
502 'models' => ['claude-sonnet-4-0', 'claude-opus-4-0', 'claude-3-7-sonnet-latest', 'claude-3-5-sonnet-latest', 'claude-3-5-haiku-latest'],
503 'requires_key' => true,
504 ],
505 'gemini' => [
506 'name' => 'Google Gemini',
507 'description' => 'Gemini 2.5 and 2.0 models',
508 'models' => ['gemini-2.5-flash', 'gemini-2.5-flash-lite', 'gemini-2.5-pro', 'gemini-2.0-flash', 'gemini-1.5-flash'],
509 'requires_key' => true,
510 ],
511 ];
512 }
513
514 /**
515 * Get current provider status
516 *
517 * @return array Provider status
518 */
519 public function get_provider_status(): array {
520 $provider = $this->settings->get('ai_provider', 'openai');
521 $api_key = $this->settings->get($provider . '_api_key');
522
523 return [
524 'provider' => $provider,
525 'configured' => !empty($api_key),
526 'connected' => $this->client !== null,
527 ];
528 }
529
530 /**
531 * AJAX handler for generating metadata
532 *
533 * @return void
534 */
535 public function ajax_generate_metadata(): void {
536 // Verify nonce
537 $nonce = sanitize_text_field(wp_unslash($_POST['nonce'] ?? ''));
538 if (!wp_verify_nonce($nonce, 'thinkrank_ai_nonce')) {
539 wp_die('Security check failed');
540 }
541
542 // Check permissions
543 if (!current_user_can('edit_posts')) {
544 wp_die('Insufficient permissions');
545 }
546
547 $content = sanitize_textarea_field(wp_unslash($_POST['content'] ?? ''));
548 $options = [
549 'target_keyword' => sanitize_text_field(wp_unslash($_POST['target_keyword'] ?? '')),
550 'content_type' => sanitize_text_field(wp_unslash($_POST['content_type'] ?? 'blog_post')),
551 'tone' => sanitize_text_field(wp_unslash($_POST['tone'] ?? 'professional')),
552 ];
553
554 try {
555 $metadata = $this->generate_seo_metadata($content, $options);
556
557 wp_send_json_success([
558 'metadata' => $metadata,
559 'message' => __('SEO metadata generated successfully!', 'thinkrank'),
560 ]);
561
562 } catch (\Exception $e) {
563 wp_send_json_error([
564 'message' => $e->getMessage(),
565 ]);
566 }
567 }
568
569 /**
570 * AJAX handler for testing API connection
571 *
572 * @return void
573 */
574 public function ajax_test_connection(): void {
575 // Verify nonce
576 $nonce = sanitize_text_field(wp_unslash($_POST['nonce'] ?? ''));
577 if (!wp_verify_nonce($nonce, 'thinkrank_ai_nonce')) {
578 wp_die('Security check failed');
579 }
580
581 // Check permissions
582 if (!current_user_can('manage_options')) {
583 wp_die('Insufficient permissions');
584 }
585
586 $result = $this->test_api_connection();
587
588 if ($result['success']) {
589 wp_send_json_success($result);
590 } else {
591 wp_send_json_error($result);
592 }
593 }
594
595 /**
596 * Check rate limits
597 *
598 * @return bool True if within limits
599 */
600 private function check_rate_limit(): bool {
601 $user_id = get_current_user_id();
602 $max_requests = $this->settings->get('max_requests_per_minute', 10);
603 $current_time = time();
604 $window_start = $current_time - 60; // 1 minute window
605
606 // Clean old entries
607 $this->rate_limits = array_filter(
608 $this->rate_limits,
609 function($timestamp) use ($window_start) {
610 return $timestamp > $window_start;
611 }
612 );
613
614 // Count requests for this user
615 $user_requests = array_filter(
616 $this->rate_limits,
617 function($timestamp, $key) use ($user_id) {
618 return strpos($key, "user_{$user_id}_") === 0;
619 },
620 ARRAY_FILTER_USE_BOTH
621 );
622
623 if (count($user_requests) >= $max_requests) {
624 return false;
625 }
626
627 // Add current request
628 $this->rate_limits["user_{$user_id}_{$current_time}"] = $current_time;
629
630 return true;
631 }
632
633
634
635 /**
636 * Log AI usage with actual model information
637 *
638 * @param int $user_id User ID
639 * @param string $action Action performed
640 * @param int $tokens_used Tokens consumed
641 * @param string|null $actual_model Actual model used (from AI response)
642 * @param string|null $raw_response Raw AI response for debugging
643 * @return void
644 */
645 private function log_ai_usage(int $user_id, string $action, int $tokens_used, ?string $actual_model = null, ?string $raw_response = null): void {
646 global $wpdb;
647
648 $table_name = $wpdb->prefix . 'thinkrank_ai_usage';
649
650 // Prepare metadata with actual model information and raw response
651 $metadata = [];
652 if ($actual_model) {
653 $metadata['actual_model'] = $actual_model;
654 }
655 if ($raw_response) {
656 $metadata['raw_response'] = $raw_response;
657 }
658
659 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- AI usage logging requires direct database access
660 $wpdb->insert(
661 $table_name,
662 [
663 'user_id' => $user_id,
664 'action' => $action,
665 'tokens_used' => $tokens_used,
666 'provider' => $this->settings->get('ai_provider', 'openai'),
667 'metadata' => !empty($metadata) ? wp_json_encode($metadata) : null,
668 'created_at' => current_time('mysql'),
669 ],
670 ['%d', '%s', '%d', '%s', '%s', '%s']
671 );
672 }
673
674 /**
675 * Cleanup expired cache entries
676 *
677 * @return void
678 */
679 public function cleanup_cache(): void {
680 $this->cache->clean_expired();
681 }
682
683 /**
684 * Optimize homepage meta content using AI (copying Site Identity pattern exactly)
685 *
686 * @since 1.0.0
687 *
688 * @param array $content_data Meta content data to optimize
689 * @param array $options Optimization options
690 * @return array Optimization results
691 * @throws \Exception If optimization fails
692 */
693 public function optimize_homepage_meta(array $content_data, array $options = []): array {
694 // Validate input
695 if (empty($content_data)) {
696 throw new \Exception('Content data cannot be empty');
697 }
698
699 $user_id = get_current_user_id();
700
701 // Ensure user has configured their API key (copying Site Identity pattern)
702 $user_has_api_key = !empty($this->settings->get('openai_api_key')) || !empty($this->settings->get('claude_api_key')) || !empty($this->settings->get('gemini_api_key'));
703
704 if (!$user_has_api_key) {
705 throw new \Exception('Please configure your OpenAI, Claude, or Gemini API key in ThinkRank settings to use AI features.');
706 }
707
708 // Generate cache key using existing pattern
709 $cache_key = 'homepage_meta_' . md5(serialize($content_data) . serialize($options)) . '_' . $user_id;
710
711 // Check existing cache infrastructure
712 $cached_result = $this->cache->get($cache_key);
713 if ($cached_result !== null && !empty($cached_result['optimized_data'])) {
714 return $cached_result;
715 }
716
717 // Check rate limiting
718 if (!$this->check_rate_limit()) {
719 throw new \Exception('Rate limit exceeded for AI optimization requests.');
720 }
721
722 // Get AI client
723 $client = $this->get_client();
724
725 if (!$client) {
726 throw new \Exception('AI client not initialized. Please check your API key configuration.');
727 }
728
729 // Perform AI optimization
730 $optimization_results = $client->optimize_homepage_meta($content_data, $options);
731
732 // Validate that we got meaningful results
733 if (empty($optimization_results) || empty($optimization_results['optimized_data'])) {
734 throw new \Exception('AI optimization returned empty results. Please try again.');
735 }
736
737 // Add metadata
738 $optimization_results['ai_model'] = $client->get_model();
739 $optimization_results['provider'] = $this->settings->get('ai_provider', 'openai');
740 $optimization_results['generated_at'] = gmdate('Y-m-d H:i:s');
741 $optimization_results['user_id'] = $user_id;
742
743 // Cache the results (24 hours)
744 $this->cache->set($cache_key, $optimization_results, 86400);
745
746 // Record usage with actual model information and raw AI text (Content Brief pattern)
747 $actual_model = $optimization_results['ai_model'] ?? ($client ? $client->get_model() : null);
748 $ai_text = $optimization_results['_ai_text'] ?? null;
749 $this->log_ai_usage($user_id, 'Homepage Meta Optimization', $optimization_results['tokens_used'] ?? 0, $actual_model, $ai_text);
750
751 // Remove AI text from returned data to keep it clean
752 unset($optimization_results['_ai_text']);
753
754 return $optimization_results;
755 }
756
757 /**
758 * Optimize homepage hero content using AI (copying Site Identity pattern exactly)
759 *
760 * @since 1.0.0
761 *
762 * @param array $hero_data Hero content data to optimize
763 * @param array $options Optimization options
764 * @return array Optimization results
765 * @throws \Exception If optimization fails
766 */
767 public function optimize_homepage_hero(array $hero_data, array $options = []): array {
768 // Validate input
769 if (empty($hero_data)) {
770 throw new \Exception('Hero data cannot be empty');
771 }
772
773 $user_id = get_current_user_id();
774
775 // Ensure user has configured their API key (copying Site Identity pattern)
776 $user_has_api_key = !empty($this->settings->get('openai_api_key')) || !empty($this->settings->get('claude_api_key')) || !empty($this->settings->get('gemini_api_key'));
777
778 if (!$user_has_api_key) {
779 throw new \Exception('Please configure your OpenAI, Claude, or Gemini API key in ThinkRank settings to use AI features.');
780 }
781
782 // Generate cache key using existing pattern
783 $cache_key = 'homepage_hero_' . md5(serialize($hero_data) . serialize($options)) . '_' . $user_id;
784
785 // Check existing cache infrastructure
786 $cached_result = $this->cache->get($cache_key);
787 if ($cached_result !== null && !empty($cached_result['optimized_data'])) {
788 return $cached_result;
789 }
790
791 // Check rate limiting
792 if (!$this->check_rate_limit()) {
793 throw new \Exception('Rate limit exceeded for AI optimization requests.');
794 }
795
796 // Get AI client
797 $client = $this->get_client();
798
799 if (!$client) {
800 throw new \Exception('AI client not initialized. Please check your API key configuration.');
801 }
802
803 // Perform AI optimization
804 $optimization_results = $client->optimize_homepage_hero($hero_data, $options);
805
806 // Validate that we got meaningful results
807 if (empty($optimization_results) || empty($optimization_results['optimized_data'])) {
808 throw new \Exception('AI optimization returned empty results. Please try again.');
809 }
810
811 // Add metadata
812 $optimization_results['ai_model'] = $client->get_model();
813 $optimization_results['provider'] = $this->settings->get('ai_provider', 'openai');
814 $optimization_results['generated_at'] = gmdate('Y-m-d H:i:s');
815 $optimization_results['user_id'] = $user_id;
816
817 // Cache the results (24 hours)
818 $this->cache->set($cache_key, $optimization_results, 86400);
819
820 // Record usage with actual model information and raw AI text (Content Brief pattern)
821 $actual_model = $optimization_results['ai_model'] ?? ($client ? $client->get_model() : null);
822 $ai_text = $optimization_results['_ai_text'] ?? null;
823 $this->log_ai_usage($user_id, 'Homepage Hero Optimization', $optimization_results['tokens_used'] ?? 0, $actual_model, $ai_text);
824
825 // Remove AI text from returned data to keep it clean
826 unset($optimization_results['_ai_text']);
827
828 return $optimization_results;
829 }
830 }
831