PluginProbe
BotWriter – AI Writer & SEO Content Generator / trunk
BotWriter – AI Writer & SEO Content Generator vtrunk
3.4.10 3.4.9 3.4.8 3.4.7 3.4.6 3.4.4 3.4.2 3.4.1 3.4.0 3.3.9 3.3.8 3.3.7 3.3.6 3.3.5 3.3.4 3.3.3 3.3.2 3.3.1 3.3.0 3.2.8 trunk 1.3.0 1.3.1 1.3.2 1.3.3 All 51 releases
botwriter / includes / settings.php

settings.php in BotWriter – AI Writer & SEO Content Generator trunk, at includes/settings.php

1,910 lines 94.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * BotWriter Settings Page
4 *
5 * Modular settings page with tabbed interface for Text AI and Image AI providers.
6 * Each provider has its own configuration file in the settings/ folder.
7 * Auto-saves via AJAX when any field changes.
8 *
9 * @package BotWriter
10 */
11
12 if (!defined('ABSPATH')) {
13 exit;
14 }
15
16 // Include models manager
17 require_once plugin_dir_path(__FILE__) . 'models.php';
18 require_once plugin_dir_path(__FILE__) . 'image-models.php';
19
20 // Include provider configuration files
21 $botwriter_settings_dir = plugin_dir_path(__FILE__) . 'settings/';
22 require_once $botwriter_settings_dir . 'openai.php';
23 require_once $botwriter_settings_dir . 'anthropic.php';
24 require_once $botwriter_settings_dir . 'google.php';
25 require_once $botwriter_settings_dir . 'mistral.php';
26 require_once $botwriter_settings_dir . 'groq.php';
27 require_once $botwriter_settings_dir . 'openrouter.php';
28 require_once $botwriter_settings_dir . 'dalle.php';
29 require_once $botwriter_settings_dir . 'gemini.php';
30 require_once $botwriter_settings_dir . 'fal.php';
31 require_once $botwriter_settings_dir . 'replicate.php';
32 require_once $botwriter_settings_dir . 'stability.php';
33 require_once $botwriter_settings_dir . 'cloudflare.php';
34 require_once $botwriter_settings_dir . 'stockphoto.php';
35 require_once $botwriter_settings_dir . 'none.php';
36
37 // Global notice accumulator for settings-related warnings
38 $botwriter_notice = '';
39
40 // Register AJAX handlers
41 add_action('wp_ajax_botwriter_save_settings', 'botwriter_ajax_save_settings');
42 add_action('wp_ajax_botwriter_test_api_key', 'botwriter_ajax_test_api_key');
43 add_action('wp_ajax_botwriter_test_model', 'botwriter_ajax_test_model');
44 add_action('wp_ajax_botwriter_reset_models', 'botwriter_ajax_reset_models');
45 add_action('wp_ajax_botwriter_debug_log_fetch', 'botwriter_ajax_debug_log_fetch');
46 add_action('wp_ajax_botwriter_debug_log_clear', 'botwriter_ajax_debug_log_clear');
47
48 /**
49 * Render the provider API key transmission disclosure used by provider settings tabs.
50 */
51 function botwriter_api_key_transmission_notice() {
52 ?>
53 <div class="notice notice-warning inline botwriter-api-key-disclosure">
54 <p><?php esc_html_e('BotWriter may transmit this API key securely to the BotWriter Worker service to process AI generation requests. Only enable this if you trust this service.', 'botwriter'); ?></p>
55 </div>
56 <?php
57 }
58
59 /**
60 * AJAX handler to test API key connectivity
61 * Uses the /models endpoint which doesn't consume tokens
62 */
63 function botwriter_ajax_test_api_key() {
64 // Verify nonce
65 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'botwriter_settings_nonce')) {
66 wp_send_json_error(array('message' => __('Security check failed.', 'botwriter')));
67 }
68
69 // Verify permissions
70 if (!current_user_can('manage_options')) {
71 wp_send_json_error(array('message' => __('Permission denied.', 'botwriter')));
72 }
73
74 $provider = isset($_POST['provider']) ? sanitize_text_field(wp_unslash($_POST['provider'])) : '';
75 $api_key = isset($_POST['api_key']) ? sanitize_text_field(wp_unslash($_POST['api_key'])) : '';
76
77 if (empty($provider)) {
78 wp_send_json_error(array('message' => __('No provider specified.', 'botwriter')));
79 }
80
81 if (empty($api_key)) {
82 wp_send_json_error(array('message' => __('Please enter an API key first.', 'botwriter')));
83 }
84
85 // Provider endpoints for testing (using /models which is free and doesn't consume tokens)
86 $providers_config = array(
87 // Text providers
88 'openai' => array(
89 'url' => 'https://api.openai.com/v1/models',
90 'headers' => array(
91 'Authorization' => 'Bearer ' . $api_key,
92 ),
93 ),
94 'anthropic' => array(
95 'url' => 'https://api.anthropic.com/v1/models',
96 'headers' => array(
97 'x-api-key' => $api_key,
98 'anthropic-version' => '2023-06-01',
99 ),
100 ),
101 'google' => array(
102 'url' => 'https://generativelanguage.googleapis.com/v1beta/models?key=' . $api_key,
103 'headers' => array(),
104 ),
105 'mistral' => array(
106 'url' => 'https://api.mistral.ai/v1/models',
107 'headers' => array(
108 'Authorization' => 'Bearer ' . $api_key,
109 ),
110 ),
111 'groq' => array(
112 'url' => 'https://api.groq.com/openai/v1/models',
113 'headers' => array(
114 'Authorization' => 'Bearer ' . $api_key,
115 ),
116 ),
117 'openrouter' => array(
118 'url' => 'https://openrouter.ai/api/v1/models',
119 'headers' => array(
120 'Authorization' => 'Bearer ' . $api_key,
121 ),
122 ),
123 // Image providers
124 'dalle' => array(
125 'url' => 'https://api.openai.com/v1/models',
126 'headers' => array(
127 'Authorization' => 'Bearer ' . $api_key,
128 ),
129 ),
130 'fal' => array(
131 'url' => 'https://api.fal.ai/v1/models',
132 'headers' => array(
133 'Authorization' => 'Key ' . $api_key,
134 ),
135 ),
136 'replicate' => array(
137 'url' => 'https://api.replicate.com/v1/account',
138 'headers' => array(
139 'Authorization' => 'Bearer ' . $api_key,
140 ),
141 ),
142 'stability' => array(
143 'url' => 'https://api.stability.ai/v1/user/account',
144 'headers' => array(
145 'Authorization' => 'Bearer ' . $api_key,
146 ),
147 ),
148 'cloudflare' => array(
149 'url' => 'https://api.cloudflare.com/client/v4/user/tokens/verify',
150 'headers' => array(
151 'Authorization' => 'Bearer ' . $api_key,
152 ),
153 ),
154 // Gemini image uses Google text provider API key
155 'gemini' => array(
156 'url' => 'https://generativelanguage.googleapis.com/v1beta/models?key=' . $api_key,
157 'headers' => array(),
158 ),
159 );
160
161 if (!isset($providers_config[$provider])) {
162 wp_send_json_error(array('message' => __('Unknown provider.', 'botwriter')));
163 }
164
165 $config = $providers_config[$provider];
166 $ssl_verify = get_option('botwriter_sslverify', 'yes') === 'yes';
167
168 $response = wp_remote_get($config['url'], array(
169 'timeout' => 15,
170 'sslverify' => $ssl_verify,
171 'headers' => $config['headers'],
172 ));
173
174 if (is_wp_error($response)) {
175 wp_send_json_error(array(
176 'message' => sprintf(
177 /* translators: %s: Error message from the API */
178 __('Connection error: %s', 'botwriter'),
179 $response->get_error_message()
180 ),
181 ));
182 }
183
184 $code = wp_remote_retrieve_response_code($response);
185 $body = wp_remote_retrieve_body($response);
186 $data = json_decode($body, true);
187
188 // Check for success (200 OK)
189 if ($code === 200) {
190 // Extract models list
191 $models = array();
192 $models_data = array();
193
194 if (isset($data['data']) && is_array($data['data'])) {
195 $models_data = $data['data'];
196 } elseif (isset($data['models']) && is_array($data['models'])) {
197 $models_data = $data['models'];
198 }
199
200 // Build models array with id and name
201 foreach ($models_data as $model) {
202 $model_id = '';
203 $model_name = '';
204
205 // Different providers have different response structures
206 if (isset($model['id'])) {
207 $model_id = $model['id'];
208 $model_name = isset($model['name']) ? $model['name'] : $model['id'];
209 } elseif (isset($model['name'])) {
210 $model_id = $model['name'];
211 $model_name = isset($model['displayName']) ? $model['displayName'] : (isset($model['display_name']) ? $model['display_name'] : $model['name']);
212 }
213
214 // Google Gemini returns model names with "models/" prefix - remove it
215 if (strpos($model_id, 'models/') === 0) {
216 $model_id = substr($model_id, 7); // Remove "models/" prefix
217 }
218 if (strpos($model_name, 'models/') === 0) {
219 $model_name = substr($model_name, 7);
220 }
221
222 // Filter OpenAI models: exclude non-text models
223 if ($provider === 'openai') {
224 $exclude_patterns = array('dall-e', 'whisper', 'tts', 'embedding', 'moderation');
225 $should_exclude = false;
226 foreach ($exclude_patterns as $pattern) {
227 if (stripos($model_id, $pattern) !== false) {
228 $should_exclude = true;
229 break;
230 }
231 }
232 if ($should_exclude) {
233 continue;
234 }
235 }
236
237 // Filter Google models: only those supporting generateContent
238 if ($provider === 'google') {
239 $supported_methods = isset($model['supportedGenerationMethods']) ? $model['supportedGenerationMethods'] : array();
240 if (!in_array('generateContent', $supported_methods)) {
241 continue;
242 }
243 }
244
245 if (!empty($model_id)) {
246 $models[] = array(
247 'id' => $model_id,
248 'name' => $model_name,
249 );
250 }
251 }
252
253 // Sort models alphabetically by id
254 usort($models, function($a, $b) {
255 return strcasecmp($a['id'], $b['id']);
256 });
257
258 $model_count = count($models);
259
260 // Save models to database using our models manager
261 if ($model_count > 0) {
262 botwriter_update_provider_all_models($provider, $models);
263 }
264
265 $message = __('API key is valid!', 'botwriter');
266 if ($model_count > 0) {
267 $message .= ' ' . sprintf(
268 /* translators: %d: Number of models available */
269 _n('%d model available.', '%d models available.', $model_count, 'botwriter'),
270 $model_count
271 );
272 }
273
274 wp_send_json_success(array(
275 'message' => $message,
276 'models' => $models,
277 'provider' => $provider,
278 ));
279 }
280
281 // Handle error responses
282 $error_message = __('Invalid API key.', 'botwriter');
283
284 if ($code === 401 || $code === 403) {
285 $error_message = __('Invalid or unauthorized API key.', 'botwriter');
286 } elseif ($code === 429) {
287 $error_message = __('Rate limit exceeded. Please try again later.', 'botwriter');
288 } elseif ($code === 500 || $code === 502 || $code === 503) {
289 $error_message = __('Provider service temporarily unavailable.', 'botwriter');
290 }
291
292 // Try to get error message from response
293 if (isset($data['error']['message'])) {
294 $error_message = sanitize_text_field($data['error']['message']);
295 } elseif (isset($data['message'])) {
296 $error_message = sanitize_text_field($data['message']);
297 }
298
299 wp_send_json_error(array('message' => $error_message));
300 }
301
302 /**
303 * AJAX handler to test model connectivity
304 * Sends a minimal prompt to verify the model works
305 */
306 function botwriter_ajax_test_model() {
307 // Verify nonce
308 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'botwriter_settings_nonce')) {
309 wp_send_json_error(array('message' => __('Security check failed.', 'botwriter')));
310 }
311
312 // Verify permissions
313 if (!current_user_can('manage_options')) {
314 wp_send_json_error(array('message' => __('Permission denied.', 'botwriter')));
315 }
316
317 $provider = isset($_POST['provider']) ? sanitize_text_field(wp_unslash($_POST['provider'])) : '';
318 $model = isset($_POST['model']) ? sanitize_text_field(wp_unslash($_POST['model'])) : '';
319
320 if (empty($provider)) {
321 wp_send_json_error(array('message' => __('No provider specified.', 'botwriter')));
322 }
323
324 if (empty($model)) {
325 wp_send_json_error(array('message' => __('No model specified.', 'botwriter')));
326 }
327
328 // Get API key for provider
329 $api_key_option = 'botwriter_' . $provider . '_api_key';
330 $api_key = botwriter_decrypt_api_key(get_option($api_key_option));
331
332 if (empty($api_key)) {
333 wp_send_json_error(array('message' => __('Please configure the API key first.', 'botwriter')));
334 }
335
336 $ssl_verify = get_option('botwriter_sslverify', 'yes') === 'yes';
337 $test_message = 'Say "Ready!" in one word.';
338
339 // Provider-specific API calls
340 switch ($provider) {
341 case 'openai':
342 // GPT-5 and GPT-4.1 models require max_completion_tokens instead of max_tokens
343 $is_new_model = preg_match('/^(gpt-5|gpt-4\.1|o\d)/i', $model);
344 $token_param = $is_new_model ? 'max_completion_tokens' : 'max_tokens';
345
346 $response = wp_remote_post('https://api.openai.com/v1/chat/completions', array(
347 'timeout' => 30,
348 'sslverify' => $ssl_verify,
349 'headers' => array(
350 'Authorization' => 'Bearer ' . $api_key,
351 'Content-Type' => 'application/json',
352 ),
353 'body' => wp_json_encode(array(
354 'model' => $model,
355 'messages' => array(
356 array('role' => 'user', 'content' => $test_message)
357 ),
358 $token_param => 10,
359 )),
360 ));
361 break;
362
363 case 'anthropic':
364 $response = wp_remote_post('https://api.anthropic.com/v1/messages', array(
365 'timeout' => 30,
366 'sslverify' => $ssl_verify,
367 'headers' => array(
368 'x-api-key' => $api_key,
369 'anthropic-version' => '2023-06-01',
370 'Content-Type' => 'application/json',
371 ),
372 'body' => wp_json_encode(array(
373 'model' => $model,
374 'max_tokens' => 10,
375 'messages' => array(
376 array('role' => 'user', 'content' => $test_message)
377 ),
378 )),
379 ));
380 break;
381
382 case 'google':
383 $url = 'https://generativelanguage.googleapis.com/v1beta/models/' . $model . ':generateContent?key=' . $api_key;
384 $response = wp_remote_post($url, array(
385 'timeout' => 30,
386 'sslverify' => $ssl_verify,
387 'headers' => array(
388 'Content-Type' => 'application/json',
389 ),
390 'body' => wp_json_encode(array(
391 'contents' => array(
392 array(
393 'parts' => array(
394 array('text' => $test_message)
395 )
396 )
397 ),
398 'generationConfig' => array(
399 'maxOutputTokens' => 10,
400 ),
401 )),
402 ));
403 break;
404
405 case 'mistral':
406 $response = wp_remote_post('https://api.mistral.ai/v1/chat/completions', array(
407 'timeout' => 30,
408 'sslverify' => $ssl_verify,
409 'headers' => array(
410 'Authorization' => 'Bearer ' . $api_key,
411 'Content-Type' => 'application/json',
412 ),
413 'body' => wp_json_encode(array(
414 'model' => $model,
415 'messages' => array(
416 array('role' => 'user', 'content' => $test_message)
417 ),
418 'max_tokens' => 10,
419 )),
420 ));
421 break;
422
423 case 'groq':
424 $response = wp_remote_post('https://api.groq.com/openai/v1/chat/completions', array(
425 'timeout' => 30,
426 'sslverify' => $ssl_verify,
427 'headers' => array(
428 'Authorization' => 'Bearer ' . $api_key,
429 'Content-Type' => 'application/json',
430 ),
431 'body' => wp_json_encode(array(
432 'model' => $model,
433 'messages' => array(
434 array('role' => 'user', 'content' => $test_message)
435 ),
436 'max_tokens' => 10,
437 )),
438 ));
439 break;
440
441 case 'openrouter':
442 $response = wp_remote_post('https://openrouter.ai/api/v1/chat/completions', array(
443 'timeout' => 30,
444 'sslverify' => $ssl_verify,
445 'headers' => array(
446 'Authorization' => 'Bearer ' . $api_key,
447 'Content-Type' => 'application/json',
448 'HTTP-Referer' => home_url(),
449 'X-Title' => 'BotWriter',
450 ),
451 'body' => wp_json_encode(array(
452 'model' => $model,
453 'messages' => array(
454 array('role' => 'user', 'content' => $test_message)
455 ),
456 'max_tokens' => 10,
457 )),
458 ));
459 break;
460
461 default:
462 wp_send_json_error(array('message' => __('Unknown provider.', 'botwriter')));
463 return;
464 }
465
466 if (is_wp_error($response)) {
467 wp_send_json_error(array(
468 'message' => sprintf(
469 /* translators: %s: Error message */
470 __('Connection error: %s', 'botwriter'),
471 $response->get_error_message()
472 ),
473 ));
474 }
475
476 $code = wp_remote_retrieve_response_code($response);
477 $body = wp_remote_retrieve_body($response);
478 $data = json_decode($body, true);
479
480 // Extract response text based on provider
481 $reply = '';
482 if ($code === 200) {
483 switch ($provider) {
484 case 'openai':
485 case 'mistral':
486 case 'groq':
487 case 'openrouter':
488 $reply = $data['choices'][0]['message']['content'] ?? '';
489 break;
490 case 'anthropic':
491 $reply = $data['content'][0]['text'] ?? '';
492 break;
493 case 'google':
494 $reply = $data['candidates'][0]['content']['parts'][0]['text'] ?? '';
495 break;
496 }
497
498 if (!empty($reply)) {
499 wp_send_json_success(array(
500 'message' => sprintf(
501 /* translators: %s: Model response */
502 __('Model responded: "%s"', 'botwriter'),
503 esc_html(trim($reply))
504 ),
505 ));
506 } else {
507 wp_send_json_success(array('message' => __('Model is working!', 'botwriter')));
508 }
509 }
510
511 // Handle error responses
512 $error_message = __('Model test failed.', 'botwriter');
513
514 if ($code === 401 || $code === 403) {
515 $error_message = __('Invalid or unauthorized API key.', 'botwriter');
516 } elseif ($code === 404) {
517 $error_message = __('Model not found. It may not be available for your account.', 'botwriter');
518 } elseif ($code === 429) {
519 $error_message = __('Rate limit exceeded. Please try again later.', 'botwriter');
520 } elseif ($code === 500 || $code === 502 || $code === 503) {
521 $error_message = __('Provider service temporarily unavailable.', 'botwriter');
522 }
523
524 // Try to get error message from response
525 if (isset($data['error']['message'])) {
526 $error_message = sanitize_text_field($data['error']['message']);
527 } elseif (isset($data['message'])) {
528 $error_message = sanitize_text_field($data['message']);
529 }
530
531 wp_send_json_error(array('message' => $error_message));
532 }
533
534 /**
535 * AJAX handler to reset models to default
536 */
537 function botwriter_ajax_reset_models() {
538 // Verify nonce
539 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'botwriter_settings_nonce')) {
540 wp_send_json_error(array('message' => __('Security check failed.', 'botwriter')));
541 }
542
543 // Verify permissions
544 if (!current_user_can('manage_options')) {
545 wp_send_json_error(array('message' => __('Permission denied.', 'botwriter')));
546 }
547
548 // Reset models to default
549 if (botwriter_reset_models_to_default()) {
550 wp_send_json_success(array(
551 'message' => __('Models reset to defaults successfully!', 'botwriter'),
552 ));
553 } else {
554 wp_send_json_error(array(
555 'message' => __('Failed to reset models. Please try again.', 'botwriter'),
556 ));
557 }
558 }
559
560 /**
561 * AJAX: return the contents of the BotWriter debug log file (last N lines).
562 */
563 function botwriter_ajax_debug_log_fetch() {
564 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'botwriter_settings_nonce')) {
565 wp_send_json_error(array('message' => __('Security check failed.', 'botwriter')));
566 }
567 if (!current_user_can('manage_options')) {
568 wp_send_json_error(array('message' => __('Permission denied.', 'botwriter')));
569 }
570
571 $path = botwriter_debug_log_path();
572 if (!$path) {
573 wp_send_json_error(array('message' => __('Uploads directory is not available.', 'botwriter')));
574 }
575
576 global $wp_filesystem;
577 if (!function_exists('WP_Filesystem')) {
578 require_once ABSPATH . 'wp-admin/includes/file.php';
579 }
580 WP_Filesystem();
581 if (!$wp_filesystem) {
582 wp_send_json_error(array('message' => __('Filesystem initialization failed.', 'botwriter')));
583 }
584
585 if (!$wp_filesystem->exists($path)) {
586 wp_send_json_success(array(
587 'enabled' => botwriter_debug_log_enabled(),
588 'path' => $path,
589 'size' => 0,
590 'content' => '',
591 'modified' => 0,
592 ));
593 }
594
595 $all_content = $wp_filesystem->get_contents($path);
596 if ($all_content === false) {
597 wp_send_json_error(array('message' => __('Could not read log file. Check permissions.', 'botwriter')));
598 }
599
600 // Return up to the last ~512 KB of the file to keep the response light.
601 $max_bytes = 512 * 1024;
602 $size = method_exists($wp_filesystem, 'size') ? (int) $wp_filesystem->size($path) : strlen((string) $all_content);
603 $truncated = $size > $max_bytes;
604
605 if ($truncated) {
606 $content = substr((string) $all_content, -$max_bytes);
607 // Drop the (likely partial) first line.
608 $first_break = strpos($content, "\n");
609 if ($first_break !== false) {
610 $content = substr($content, $first_break + 1);
611 }
612 } else {
613 $content = (string) $all_content;
614 }
615
616 $modified = method_exists($wp_filesystem, 'mtime') ? (int) $wp_filesystem->mtime($path) : 0;
617
618 wp_send_json_success(array(
619 'enabled' => botwriter_debug_log_enabled(),
620 'path' => $path,
621 'size' => (int) $size,
622 'content' => (string) $content,
623 'modified' => $modified,
624 'truncated' => $truncated,
625 ));
626 }
627
628 /**
629 * AJAX: clear (truncate) the BotWriter debug log file.
630 */
631 function botwriter_ajax_debug_log_clear() {
632 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'botwriter_settings_nonce')) {
633 wp_send_json_error(array('message' => __('Security check failed.', 'botwriter')));
634 }
635 if (!current_user_can('manage_options')) {
636 wp_send_json_error(array('message' => __('Permission denied.', 'botwriter')));
637 }
638
639 $path = botwriter_debug_log_path();
640 if (!$path) {
641 wp_send_json_error(array('message' => __('Uploads directory is not available.', 'botwriter')));
642 }
643
644 global $wp_filesystem;
645 if (!function_exists('WP_Filesystem')) {
646 require_once ABSPATH . 'wp-admin/includes/file.php';
647 }
648 WP_Filesystem();
649 if (!$wp_filesystem) {
650 wp_send_json_error(array('message' => __('Filesystem initialization failed.', 'botwriter')));
651 }
652
653 $ok = $wp_filesystem->put_contents($path, '', FS_CHMOD_FILE);
654 if (!$ok) {
655 wp_send_json_error(array('message' => __('Could not clear log file. Check permissions.', 'botwriter')));
656 }
657
658 wp_send_json_success(array('message' => __('Log cleared.', 'botwriter')));
659 }
660
661 /**
662 * AJAX handler to save individual settings
663 */
664 function botwriter_ajax_save_settings() {
665 // Verify nonce
666 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'botwriter_settings_nonce')) {
667 wp_send_json_error(['message' => __('Security check failed.', 'botwriter')]);
668 }
669
670 // Verify permissions
671 if (!current_user_can('manage_options')) {
672 wp_send_json_error(['message' => __('Permission denied.', 'botwriter')]);
673 }
674
675 $field = isset($_POST['field']) ? sanitize_text_field(wp_unslash($_POST['field'])) : '';
676 $value = isset($_POST['value']) ? sanitize_text_field(wp_unslash($_POST['value'])) : '';
677
678 if (empty($field)) {
679 wp_send_json_error(['message' => __('No field specified.', 'botwriter')]);
680 }
681
682 // List of allowed fields
683 $allowed_fields = [
684 'botwriter_text_provider',
685 'botwriter_image_provider',
686 'botwriter_openai_api_key',
687 'botwriter_anthropic_api_key',
688 'botwriter_google_api_key',
689 'botwriter_mistral_api_key',
690 'botwriter_groq_api_key',
691 'botwriter_openrouter_api_key',
692 'botwriter_fal_api_key',
693 'botwriter_replicate_api_key',
694 'botwriter_stability_api_key',
695 'botwriter_cloudflare_api_key',
696 'botwriter_cloudflare_account_id',
697 'botwriter_openai_model',
698 'botwriter_anthropic_model',
699 'botwriter_google_model',
700 'botwriter_mistral_model',
701 'botwriter_groq_model',
702 'botwriter_openrouter_model',
703 'botwriter_dalle_model',
704 'botwriter_gemini_image_model',
705 'botwriter_fal_model',
706 'botwriter_replicate_model',
707 'botwriter_stability_model',
708 'botwriter_cloudflare_model',
709 'botwriter_ai_image_size',
710 'botwriter_ai_image_quality',
711 'botwriter_ai_image_style',
712 'botwriter_ai_image_style_custom',
713 'botwriter_image_postprocess_enabled',
714 'botwriter_image_output_format',
715 'botwriter_image_max_width',
716 'botwriter_image_compression',
717 'botwriter_image_max_filesize',
718 'botwriter_cron_active',
719 'botwriter_paused_tasks',
720 'botwriter_tags_disabled',
721 'botwriter_meta_disabled',
722 'botwriter_image_error_continue',
723 'botwriter_editor_assistant_enabled',
724 'botwriter_seo_module_enabled',
725 // SEO Post-processing fields
726 'botwriter_seo_sync_meta_enabled',
727 'botwriter_seo_ai_meta_enabled',
728 'botwriter_seo_auto_internal_links_enabled',
729 'botwriter_seo_internal_links_scope',
730 'botwriter_seo_auto_internal_links_max_links',
731 'botwriter_seo_ai_internal_links_enabled',
732 'botwriter_seo_featured_image_alt_enabled',
733 'botwriter_seo_publish_focus_keyword_enabled',
734 'botwriter_seo_publish_faq_enabled',
735 'botwriter_seo_publish_faq_mode',
736 'botwriter_seo_social_meta_enabled',
737 // SEO Translation fields
738 'botwriter_seo_translation_enabled',
739 'botwriter_seo_target_language',
740 'botwriter_seo_translate_title',
741 'botwriter_seo_translate_tags',
742 'botwriter_seo_translate_image',
743 // Stock photo fields
744 'botwriter_stockphoto_preferred',
745 'botwriter_stockphoto_selection',
746 'botwriter_stockphoto_attribution',
747 // Duplicate detection (RSS / WordPress sources)
748 'botwriter_dedup_enabled',
749 'botwriter_dedup_title_threshold',
750 // Debug logging to file
751 'botwriter_debug_logging',
752 ];
753
754 if (!in_array($field, $allowed_fields)) {
755 wp_send_json_error(['message' => __('Invalid field.', 'botwriter')]);
756 }
757
758 // API key fields that need encryption
759 $api_key_fields = [
760 'botwriter_openai_api_key',
761 'botwriter_anthropic_api_key',
762 'botwriter_google_api_key',
763 'botwriter_mistral_api_key',
764 'botwriter_groq_api_key',
765 'botwriter_openrouter_api_key',
766 'botwriter_fal_api_key',
767 'botwriter_replicate_api_key',
768 'botwriter_stability_api_key',
769 'botwriter_cloudflare_api_key',
770 ];
771
772 if (in_array($field, $api_key_fields)) {
773 $value = sanitize_text_field($value);
774 if (!empty($value)) {
775 // Special validation for OpenAI key
776 if ($field === 'botwriter_openai_api_key') {
777 if (strpos($value, 'sk-') !== 0) {
778 wp_send_json_error(['message' => __('OpenAI API Key must start with "sk-".', 'botwriter')]);
779 }
780 }
781 $value = botwriter_encrypt_api_key_generic($value);
782 }
783 } elseif ($field === 'botwriter_ai_image_size') {
784 $raw_value = sanitize_text_field($value);
785 $value = function_exists('botwriter_normalize_ai_image_size')
786 ? (string) botwriter_normalize_ai_image_size($raw_value)
787 : $raw_value;
788
789 botwriter_log('Settings save: image size normalized', array(
790 'field' => $field,
791 'raw_value' => $raw_value,
792 'saved_value' => $value,
793 'changed' => ($raw_value !== $value),
794 ));
795 } elseif ($field === 'botwriter_gemini_image_model') {
796 $raw_value = sanitize_text_field($value);
797 $value = function_exists('botwriter_normalize_image_model')
798 ? (string) botwriter_normalize_image_model('gemini', $raw_value)
799 : $raw_value;
800
801 botwriter_log('Settings save: Gemini image model normalized', array(
802 'field' => $field,
803 'raw_value' => $raw_value,
804 'saved_value' => $value,
805 'changed' => ($raw_value !== $value),
806 ));
807 } elseif (in_array($field, [
808 'botwriter_openai_model',
809 'botwriter_anthropic_model',
810 'botwriter_google_model',
811 'botwriter_mistral_model',
812 'botwriter_groq_model',
813 'botwriter_openrouter_model',
814 ], true)) {
815 $raw_value = sanitize_text_field($value);
816 $provider_by_field = [
817 'botwriter_openai_model' => 'openai',
818 'botwriter_anthropic_model' => 'anthropic',
819 'botwriter_google_model' => 'google',
820 'botwriter_mistral_model' => 'mistral',
821 'botwriter_groq_model' => 'groq',
822 'botwriter_openrouter_model' => 'openrouter',
823 ];
824 $provider = $provider_by_field[$field] ?? 'openai';
825
826 if (function_exists('botwriter_normalize_text_model')) {
827 $value = (string) botwriter_normalize_text_model($provider, $raw_value);
828 } else {
829 $value = $raw_value;
830 }
831
832 if ($value === '') {
833 if (function_exists('botwriter_get_provider_text_model_defaults')) {
834 $defaults = botwriter_get_provider_text_model_defaults();
835 $value = (string) ($defaults[$provider] ?? 'gpt-5.4-mini');
836 } else {
837 $value = $raw_value;
838 }
839 }
840
841 botwriter_log('Settings save: text model normalized', array(
842 'field' => $field,
843 'provider' => $provider,
844 'raw_value' => $raw_value,
845 'saved_value' => $value,
846 'changed' => ($raw_value !== $value),
847 ));
848 } elseif ($field === 'botwriter_paused_tasks') {
849 $value = max(2, intval($value));
850 } elseif ($field === 'botwriter_seo_auto_internal_links_max_links') {
851 $value = max(1, min(8, intval($value)));
852 } elseif ($field === 'botwriter_seo_internal_links_scope') {
853 $value = sanitize_key($value);
854 if (!in_array($value, array('any', 'posts', 'products'), true)) {
855 $value = 'any';
856 }
857 } elseif ($field === 'botwriter_seo_publish_faq_mode') {
858 $value = sanitize_key($value);
859 if ($value !== 'visible_schema' && $value !== 'schema_only') {
860 $value = 'visible_schema';
861 }
862 } elseif ($field === 'botwriter_dedup_title_threshold') {
863 $value = max(0, min(100, intval($value)));
864 } elseif ($field === 'botwriter_cron_active' || $field === 'botwriter_tags_disabled'
865 || $field === 'botwriter_meta_disabled'
866 || $field === 'botwriter_editor_assistant_enabled'
867 || $field === 'botwriter_seo_module_enabled'
868 || $field === 'botwriter_seo_sync_meta_enabled'
869 || $field === 'botwriter_seo_ai_meta_enabled'
870 || $field === 'botwriter_seo_auto_internal_links_enabled'
871 || $field === 'botwriter_seo_ai_internal_links_enabled'
872 || $field === 'botwriter_seo_featured_image_alt_enabled'
873 || $field === 'botwriter_seo_publish_focus_keyword_enabled'
874 || $field === 'botwriter_seo_publish_faq_enabled'
875 || $field === 'botwriter_seo_social_meta_enabled'
876 || $field === 'botwriter_seo_translation_enabled' || $field === 'botwriter_seo_translate_title'
877 || $field === 'botwriter_seo_translate_tags' || $field === 'botwriter_seo_translate_image'
878 || $field === 'botwriter_dedup_enabled'
879 || $field === 'botwriter_debug_logging') {
880 $value = ($value === '1' || $value === 'true' || $value === true) ? '1' : '0';
881 } else {
882 $value = sanitize_text_field($value);
883 }
884
885 // Save the option
886 update_option($field, $value);
887
888 // Keep legacy/meta settings compatibility.
889 if ($field === 'botwriter_meta_disabled') {
890 $enabled = ($value === '1') ? '0' : '1';
891 update_option('botwriter_seo_ai_meta_enabled', $enabled);
892 update_option('botwriter_seo_sync_meta_enabled', $enabled);
893 }
894
895 if ($field === 'botwriter_seo_ai_meta_enabled') {
896 update_option('botwriter_meta_disabled', $value === '1' ? '0' : '1');
897 update_option('botwriter_seo_sync_meta_enabled', $value);
898 }
899
900 // Handle cron activation/deactivation
901 if ($field === 'botwriter_cron_active') {
902 if ($value === '1') {
903 botwriter_scheduled_events_plugin_activate();
904 } else {
905 botwriter_scheduled_events_plugin_deactivate();
906 }
907 }
908
909 wp_send_json_success(['message' => __('Saved', 'botwriter'), 'field' => $field]);
910 }
911
912 /**
913 * Main settings page handler
914 */
915 function botwriter_settings_page_handler() {
916 // Verify permissions
917 if (!current_user_can('manage_options')) {
918 wp_die(esc_html__('You do not have permission to access this page.', 'botwriter'));
919 }
920
921 // Add metabox
922 add_meta_box(
923 'botwriter_settings',
924 __('WP BotWriter Settings', 'botwriter'),
925 'botwriter_settings_meta_box_handler',
926 'botwriter_settings_page',
927 'normal',
928 'default'
929 );
930
931 ?>
932 <div class="wrap botwriter-settings-wrap">
933 <h2><?php esc_html_e('Settings', 'botwriter'); ?>
934 <span id="botwriter-save-status" class="botwriter-save-indicator"></span>
935 </h2>
936
937 <div id="botwriter-settings-form">
938 <input type="hidden" id="botwriter_settings_nonce" value="<?php echo esc_attr(wp_create_nonce('botwriter_settings_nonce')); ?>" />
939 <input id="botwriter_domain_name" type="hidden" name="url" value="<?php echo esc_attr(get_site_url()); ?>" />
940
941 <div class="metabox-holder" id="poststuff">
942 <div id="post-body">
943 <div id="post-body-content">
944 <?php do_meta_boxes('botwriter_settings_page', 'normal', null); ?>
945 </div>
946 </div>
947 </div>
948 </div>
949 </div>
950 <?php
951 }
952
953 /**
954 * Main metabox handler - renders the settings form content
955 */
956 function botwriter_settings_meta_box_handler() {
957 // Get current values with defaults
958 $text_provider = get_option('botwriter_text_provider', 'openai');
959 $image_provider = get_option('botwriter_image_provider', 'stockphoto');
960 $show_wp_cron_disabled_warning = function_exists('botwriter_should_show_wp_cron_disabled_warning') && botwriter_should_show_wp_cron_disabled_warning();
961 $active_tasks_count = $show_wp_cron_disabled_warning && function_exists('botwriter_get_enabled_tasks_count')
962 ? (int) botwriter_get_enabled_tasks_count()
963 : 0;
964
965 $settings = botwriter_get_all_settings();
966
967 $seo_plugins_status = array(
968 array(
969 'name' => 'Yoast SEO',
970 'detected' => (defined('WPSEO_VERSION') || class_exists('WPSEO_Meta')),
971 ),
972 array(
973 'name' => 'Rank Math',
974 'detected' => class_exists('RankMath'),
975 ),
976 array(
977 'name' => 'All in One SEO',
978 'detected' => function_exists('aioseo'),
979 ),
980 array(
981 'name' => 'SEOPress',
982 'detected' => defined('SEOPRESS_VERSION'),
983 ),
984 array(
985 'name' => 'The SEO Framework',
986 'detected' => function_exists('the_seo_framework'),
987 ),
988 );
989
990 $seo_plugins_detected_count = 0;
991 foreach ($seo_plugins_status as $seo_plugin_row) {
992 if (!empty($seo_plugin_row['detected'])) {
993 $seo_plugins_detected_count++;
994 }
995 }
996
997 $seo_languages = array(
998 'en' => 'English', 'es' => 'Spanish (Español)', 'fr' => 'French (Français)',
999 'de' => 'German (Deutsch)', 'it' => 'Italian (Italiano)', 'pt' => 'Portuguese (Português)',
1000 'nl' => 'Dutch (Nederlands)', 'ru' => 'Russian (Русский)',
1001 'ja' => 'Japanese (日本語)', 'ko' => 'Korean (한국어)',
1002 'zh' => 'Chinese (中文)', 'ar' => 'Arabic (العربية)',
1003 'hi' => 'Hindi (हिन्दी)', 'tr' => 'Turkish (Türkçe)',
1004 'pl' => 'Polish (Polski)', 'sv' => 'Swedish (Svenska)',
1005 'da' => 'Danish (Dansk)', 'no' => 'Norwegian (Norsk)',
1006 'fi' => 'Finnish (Suomi)', 'cs' => 'Czech (Čeština)',
1007 'ro' => 'Romanian (Română)', 'hu' => 'Hungarian (Magyar)',
1008 'el' => 'Greek (Ελληνικά)', 'th' => 'Thai (ไทย)',
1009 'vi' => 'Vietnamese (Tiếng Việt)', 'id' => 'Indonesian (Bahasa)',
1010 'ms' => 'Malay (Melayu)', 'uk' => 'Ukrainian (Українська)',
1011 );
1012
1013 // Provider lists
1014 $text_providers = [
1015 'openai' => 'OpenAI (GPT-5, GPT-4o)',
1016 'anthropic' => 'Anthropic (Claude)',
1017 'google' => 'Google (Gemini) - FREE TIER',
1018 'mistral' => 'Mistral AI - FREE TIER',
1019 'groq' => 'Groq (Ultra Fast) - FREE TIER',
1020 'openrouter' => 'OpenRouter (Multiple) - FREE TIER',
1021 ];
1022
1023 $image_providers = [
1024 'dalle' => 'DALL-E (OpenAI)',
1025 'gemini' => 'Google Gemini',
1026 'fal' => 'Fal.ai (Flux)',
1027 'replicate' => 'Replicate',
1028 'stability' => 'Stability AI',
1029 'cloudflare' => 'Cloudflare AI (FREE)',
1030 'stockphoto' => '📷 [FREE] Stock Images',
1031 'none' => '🚫 No Image Generation',
1032 ];
1033
1034 ?>
1035
1036 <!-- Main Tabs Navigation (Text AI / Image AI) -->
1037 <div class="botwriter-main-tabs">
1038 <a href="#" class="main-tab main-tab-active" data-main-tab="text">
1039 <span class="dashicons dashicons-edit"></span>
1040 <?php esc_html_e('Text AI Providers', 'botwriter'); ?>
1041 </a>
1042 <a href="#" class="main-tab" data-main-tab="images">
1043 <span class="dashicons dashicons-format-image"></span>
1044 <?php esc_html_e('Image AI Providers', 'botwriter'); ?>
1045 </a>
1046 <a href="#" class="main-tab" data-main-tab="imagesettings">
1047 <span class="dashicons dashicons-admin-appearance"></span>
1048 <?php esc_html_e('Image Settings', 'botwriter'); ?>
1049 </a>
1050 <a href="#" class="main-tab" data-main-tab="general">
1051 <span class="dashicons dashicons-admin-generic"></span>
1052 <?php esc_html_e('General Settings', 'botwriter'); ?>
1053 </a>
1054 <a href="#" class="main-tab" data-main-tab="seo">
1055 <span class="dashicons dashicons-translation"></span>
1056 <?php esc_html_e('SEO', 'botwriter'); ?>
1057 </a>
1058 </div>
1059
1060 <!-- Tab: Text AI Providers -->
1061 <div id="main-tab-text" class="botwriter-main-tab-content active">
1062 <div class="provider-selection-header">
1063 <h3><?php esc_html_e('Select Text AI Provider', 'botwriter'); ?></h3>
1064 <div class="form-row">
1065 <select name="botwriter_text_provider" id="botwriter_text_provider" class="form-select provider-select">
1066 <?php foreach ($text_providers as $id => $name): ?>
1067 <option value="<?php echo esc_attr($id); ?>" <?php selected($text_provider, $id); ?>><?php echo esc_html($name); ?></option>
1068 <?php endforeach; ?>
1069 </select>
1070 </div>
1071 </div>
1072
1073 <!-- Provider Content Sections -->
1074 <?php
1075 foreach ($text_providers as $id => $name):
1076 $is_active = ($id === $text_provider);
1077 $render_func = 'botwriter_render_' . $id . '_settings';
1078 ?>
1079 <div class="provider-content <?php echo $is_active ? 'active' : ''; ?>"
1080 id="provider-<?php echo esc_attr($id); ?>" data-provider="<?php echo esc_attr($id); ?>">
1081 <?php
1082 if (function_exists($render_func)) {
1083 $render_func($settings, $is_active);
1084 } else {
1085 echo '<p>' . esc_html__('Provider configuration not available.', 'botwriter') . '</p>';
1086 }
1087 ?>
1088 </div>
1089 <?php endforeach; ?>
1090 </div>
1091
1092 <!-- Tab: Image AI Providers -->
1093 <div id="main-tab-images" class="botwriter-main-tab-content">
1094 <div class="provider-selection-header">
1095 <h3><?php esc_html_e('Select Image AI Provider', 'botwriter'); ?></h3>
1096 <div class="form-row">
1097 <select name="botwriter_image_provider" id="botwriter_image_provider" class="form-select provider-select">
1098 <?php foreach ($image_providers as $id => $name): ?>
1099 <option value="<?php echo esc_attr($id); ?>" <?php selected($image_provider, $id); ?>><?php echo esc_html($name); ?></option>
1100 <?php endforeach; ?>
1101 </select>
1102 </div>
1103 </div>
1104
1105 <!-- Image Provider Content Sections -->
1106 <?php
1107 foreach ($image_providers as $id => $name):
1108 $is_active = ($id === $image_provider);
1109 $render_func = 'botwriter_render_' . $id . '_settings';
1110 ?>
1111 <div class="provider-content <?php echo $is_active ? 'active' : ''; ?>"
1112 id="image-provider-<?php echo esc_attr($id); ?>" data-provider="<?php echo esc_attr($id); ?>">
1113 <?php
1114 if (function_exists($render_func)) {
1115 $render_func($settings, $is_active);
1116 } else {
1117 echo '<p>' . esc_html__('Provider configuration not available.', 'botwriter') . '</p>';
1118 }
1119 ?>
1120 </div>
1121 <?php endforeach; ?>
1122 </div>
1123
1124 <!-- Tab: Image Settings -->
1125 <div id="main-tab-imagesettings" class="botwriter-main-tab-content">
1126 <div class="image-settings-section">
1127 <div id="stockphoto_notice_imagesettings" class="notice notice-info inline bw-notice-info-inline" style="margin-bottom: 20px; <?php echo $image_provider === 'stockphoto' ? '' : 'display:none;'; ?>">
1128 <p><span class="dashicons dashicons-info"></span> <?php esc_html_e('Stock photo mode: Style and quality settings do not apply. Image Format is used to set the search orientation (landscape/portrait/square).', 'botwriter'); ?></p>
1129 </div>
1130 <h4 class="section-title">
1131 <span class="dashicons dashicons-format-image"></span>
1132 <?php esc_html_e('Image Generation Settings', 'botwriter'); ?>
1133 </h4>
1134
1135 <div class="settings-grid-2col">
1136 <div class="setting-card">
1137 <div class="form-row">
1138 <label><span class="dashicons dashicons-image-crop"></span> <?php esc_html_e('Image Format:', 'botwriter'); ?></label>
1139 <select name="botwriter_ai_image_size" class="form-select" id="image_size_select">
1140 <option value="landscape" <?php selected($settings['botwriter_ai_image_size'], 'landscape'); ?>><?php esc_html_e('🖼️ Landscape (16:9) - Blog headers', 'botwriter'); ?></option>
1141 <option value="square" <?php selected($settings['botwriter_ai_image_size'], 'square'); ?>><?php esc_html_e('⬛ Square (1:1) - Social media', 'botwriter'); ?></option>
1142 <option value="portrait" <?php selected($settings['botwriter_ai_image_size'], 'portrait'); ?>><?php esc_html_e('📱 Portrait (9:16) - Mobile/Stories', 'botwriter'); ?></option>
1143 </select>
1144 </div>
1145
1146 <div class="format-preview" id="format_preview">
1147 <div class="preview-box landscape">
1148 <span>16:9</span>
1149 </div>
1150 </div>
1151
1152 <div class="format-specs">
1153 <p class="description"><strong><?php esc_html_e('Approximate dimensions by provider:', 'botwriter'); ?></strong></p>
1154 <table class="specs-table" id="size_specs_table">
1155 <tr><td>DALL-E</td><td id="dalle_size">1792×1024</td></tr>
1156 <tr><td>Fal.ai/Flux</td><td id="fal_size">1344×768</td></tr>
1157 <tr><td>Stability AI</td><td id="stability_size">1344×768</td></tr>
1158 <tr><td>Replicate</td><td id="replicate_size">1344×768</td></tr>
1159 </table>
1160 </div>
1161 </div>
1162
1163 <div class="setting-card">
1164 <div class="form-row">
1165 <label><span class="dashicons dashicons-dashboard"></span> <?php esc_html_e('Image Quality:', 'botwriter'); ?></label>
1166 <select name="botwriter_ai_image_quality" class="form-select" id="image_quality_select">
1167 <option value="low" <?php selected($settings['botwriter_ai_image_quality'], 'low'); ?>><?php esc_html_e('⚡ Low - Faster & Cheaper', 'botwriter'); ?></option>
1168 <option value="medium" <?php selected($settings['botwriter_ai_image_quality'], 'medium'); ?>><?php esc_html_e('⚖️ Medium - Balanced', 'botwriter'); ?></option>
1169 <option value="high" <?php selected($settings['botwriter_ai_image_quality'], 'high'); ?>><?php esc_html_e('✨ High - Best Quality', 'botwriter'); ?></option>
1170 </select>
1171 </div>
1172
1173 <div class="quality-indicator" id="quality_indicator">
1174 <div class="quality-bar medium">
1175 <div class="bar-fill"></div>
1176 </div>
1177 <div class="quality-labels">
1178 <span><?php esc_html_e('Speed', 'botwriter'); ?></span>
1179 <span><?php esc_html_e('Quality', 'botwriter'); ?></span>
1180 </div>
1181 </div>
1182
1183 <div class="quality-specs">
1184 <p class="description"><strong><?php esc_html_e('Provider settings mapping:', 'botwriter'); ?></strong></p>
1185 <table class="specs-table" id="quality_specs_table">
1186 <tr><td>DALL-E</td><td id="dalle_quality">standard</td></tr>
1187 <tr><td>Fal.ai</td><td id="fal_quality">20 steps</td></tr>
1188 <tr><td>Stability AI</td><td id="stability_quality">sd3-large</td></tr>
1189 <tr><td>Replicate</td><td id="replicate_quality">25 steps</td></tr>
1190 </table>
1191 </div>
1192 </div>
1193 </div>
1194
1195 <!-- Image Style Settings -->
1196 <div class="settings-grid-2col" style="margin-top: 20px;">
1197 <div class="setting-card">
1198 <div class="form-row">
1199 <label><span class="dashicons dashicons-art"></span> <?php esc_html_e('Image Style:', 'botwriter'); ?></label>
1200 <select name="botwriter_ai_image_style" class="form-select botwriter-autosave" id="image_style_select">
1201 <option value="realistic" <?php selected($settings['botwriter_ai_image_style'], 'realistic'); ?>><?php esc_html_e('📷 Realistic / Photographic', 'botwriter'); ?></option>
1202 <option value="digital-art" <?php selected($settings['botwriter_ai_image_style'], 'digital-art'); ?>><?php esc_html_e('🎨 Digital Art', 'botwriter'); ?></option>
1203 <option value="illustration" <?php selected($settings['botwriter_ai_image_style'], 'illustration'); ?>><?php esc_html_e('✏️ Illustration', 'botwriter'); ?></option>
1204 <option value="cartoon" <?php selected($settings['botwriter_ai_image_style'], 'cartoon'); ?>><?php esc_html_e('🎭 Cartoon', 'botwriter'); ?></option>
1205 <option value="comic" <?php selected($settings['botwriter_ai_image_style'], 'comic'); ?>><?php esc_html_e('💥 Comic Book', 'botwriter'); ?></option>
1206 <option value="anime" <?php selected($settings['botwriter_ai_image_style'], 'anime'); ?>><?php esc_html_e('🌸 Anime / Manga', 'botwriter'); ?></option>
1207 <option value="3d-render" <?php selected($settings['botwriter_ai_image_style'], '3d-render'); ?>><?php esc_html_e('🧊 3D Render', 'botwriter'); ?></option>
1208 <option value="watercolor" <?php selected($settings['botwriter_ai_image_style'], 'watercolor'); ?>><?php esc_html_e('🖌️ Watercolor', 'botwriter'); ?></option>
1209 <option value="oil-painting" <?php selected($settings['botwriter_ai_image_style'], 'oil-painting'); ?>><?php esc_html_e('🎨 Oil Painting', 'botwriter'); ?></option>
1210 <option value="minimalist" <?php selected($settings['botwriter_ai_image_style'], 'minimalist'); ?>><?php esc_html_e('◻️ Minimalist', 'botwriter'); ?></option>
1211 <option value="vintage" <?php selected($settings['botwriter_ai_image_style'], 'vintage'); ?>><?php esc_html_e('📻 Vintage / Retro', 'botwriter'); ?></option>
1212 <option value="cinematic" <?php selected($settings['botwriter_ai_image_style'], 'cinematic'); ?>><?php esc_html_e('🎬 Cinematic', 'botwriter'); ?></option>
1213 <option value="none" <?php selected($settings['botwriter_ai_image_style'], 'none'); ?>><?php esc_html_e('⚪ None (no style prefix)', 'botwriter'); ?></option>
1214 <option value="custom" <?php selected($settings['botwriter_ai_image_style'], 'custom'); ?>><?php esc_html_e('✨ Custom (enter below)', 'botwriter'); ?></option>
1215 </select>
1216 </div>
1217 <div class="form-row" id="custom_style_row" style="<?php echo $settings['botwriter_ai_image_style'] === 'custom' ? '' : 'display:none;'; ?>">
1218 <label><?php esc_html_e('Custom Style:', 'botwriter'); ?></label>
1219 <input type="text" name="botwriter_ai_image_style_custom" class="form-control botwriter-autosave" value="<?php echo esc_attr($settings['botwriter_ai_image_style_custom']); ?>" placeholder="<?php esc_attr_e('e.g., "Studio Ghibli style", "pencil sketch"', 'botwriter'); ?>">
1220 </div>
1221 <p class="description"><?php esc_html_e('Style prefix added to image prompts. This affects the visual appearance of generated images.', 'botwriter'); ?></p>
1222 </div>
1223
1224 <div class="setting-card">
1225 <div class="form-row">
1226 <label><span class="dashicons dashicons-admin-settings"></span> <?php esc_html_e('Image Post-Processing:', 'botwriter'); ?></label>
1227 <label class="toggle-switch">
1228 <input type="checkbox" name="botwriter_image_postprocess_enabled" class="botwriter-autosave" value="1" <?php checked($settings['botwriter_image_postprocess_enabled'], '1'); ?>>
1229 <span class="slider round"></span>
1230 </label>
1231 <span class="toggle-label"><?php esc_html_e('Enable optimization', 'botwriter'); ?></span>
1232 </div>
1233 <p class="description"><?php esc_html_e('Optimize images for web: convert to WebP, resize, and compress for faster loading and Google Discover.', 'botwriter'); ?></p>
1234 </div>
1235 </div>
1236
1237 <!-- Post-processing options (shown when enabled) -->
1238 <div id="postprocess_options" class="settings-grid-2col" style="margin-top: 15px; <?php echo $settings['botwriter_image_postprocess_enabled'] === '1' ? '' : 'display:none;'; ?>">
1239 <div class="setting-card">
1240 <div class="form-row">
1241 <label><span class="dashicons dashicons-format-image"></span> <?php esc_html_e('Output Format:', 'botwriter'); ?></label>
1242 <select name="botwriter_image_output_format" class="form-select botwriter-autosave">
1243 <option value="webp" <?php selected($settings['botwriter_image_output_format'], 'webp'); ?>><?php esc_html_e('WebP (recommended)', 'botwriter'); ?></option>
1244 <option value="jpeg" <?php selected($settings['botwriter_image_output_format'], 'jpeg'); ?>><?php esc_html_e('JPEG', 'botwriter'); ?></option>
1245 <option value="png" <?php selected($settings['botwriter_image_output_format'], 'png'); ?>><?php esc_html_e('PNG', 'botwriter'); ?></option>
1246 <option value="original" <?php selected($settings['botwriter_image_output_format'], 'original'); ?>><?php esc_html_e('Keep Original', 'botwriter'); ?></option>
1247 </select>
1248 </div>
1249 <div class="form-row">
1250 <label><span class="dashicons dashicons-leftright"></span> <?php esc_html_e('Max Width (px):', 'botwriter'); ?></label>
1251 <input type="number" name="botwriter_image_max_width" class="form-control small-input botwriter-autosave" value="<?php echo esc_attr($settings['botwriter_image_max_width']); ?>" min="400" max="2560" step="100">
1252 <p class="description"><?php esc_html_e('Images wider than this will be resized. Recommended: 1200px for blog headers.', 'botwriter'); ?></p>
1253 </div>
1254 </div>
1255 <div class="setting-card">
1256 <div class="form-row">
1257 <label><span class="dashicons dashicons-dashboard"></span> <?php esc_html_e('Compression Quality:', 'botwriter'); ?></label>
1258 <input type="range" name="botwriter_image_compression" class="form-range botwriter-autosave" value="<?php echo esc_attr($settings['botwriter_image_compression']); ?>" min="50" max="100" step="5" id="compression_slider">
1259 <span id="compression_value"><?php echo esc_html($settings['botwriter_image_compression']); ?>%</span>
1260 </div>
1261 <p class="description"><?php esc_html_e('Lower = smaller file size but less quality. Recommended: 80-85% for web.', 'botwriter'); ?></p>
1262 <div class="form-row">
1263 <label><span class="dashicons dashicons-database"></span> <?php esc_html_e('Max File Size (KB):', 'botwriter'); ?></label>
1264 <input type="number" name="botwriter_image_max_filesize" class="form-control small-input botwriter-autosave" value="<?php echo esc_attr($settings['botwriter_image_max_filesize']); ?>" min="50" max="500" step="10">
1265 <p class="description"><?php esc_html_e('Target max file size. Set 0 to disable. Google Discover recommends ≤120KB.', 'botwriter'); ?></p>
1266 </div>
1267 </div>
1268 </div>
1269
1270 <div class="info-tip highlight-tip">
1271 <span class="dashicons dashicons-lightbulb"></span>
1272 <div>
1273 <p><strong><?php esc_html_e('💡 Pro Tip:', 'botwriter'); ?></strong></p>
1274 <p><?php esc_html_e('You can disable AI image generation for individual tasks in the task editor. This is useful to save costs on articles where you plan to add your own images.', 'botwriter'); ?></p>
1275 </div>
1276 </div>
1277
1278 <div class="cost-comparison">
1279 <h5><span class="dashicons dashicons-money-alt"></span> <?php esc_html_e('Estimated Cost per Image', 'botwriter'); ?></h5>
1280 <div class="cost-cards">
1281 <div class="cost-card">
1282 <span class="provider">DALL-E</span>
1283 <span class="cost" id="dalle_cost">$0.04 - $0.08</span>
1284 </div>
1285 <div class="cost-card">
1286 <span class="provider">Fal.ai</span>
1287 <span class="cost" id="fal_cost">$0.03 - $0.05</span>
1288 </div>
1289 <div class="cost-card cheapest">
1290 <span class="provider">Stability</span>
1291 <span class="cost" id="stability_cost">$0.02 - $0.04</span>
1292 <span class="badge"><?php esc_html_e('Cheapest', 'botwriter'); ?></span>
1293 </div>
1294 <div class="cost-card">
1295 <span class="provider">Replicate</span>
1296 <span class="cost" id="replicate_cost">$0.003 - $0.05</span>
1297 </div>
1298 </div>
1299 </div>
1300 </div>
1301 </div>
1302
1303 <!-- Tab: General Settings -->
1304 <div id="main-tab-general" class="botwriter-main-tab-content">
1305 <div class="general-settings-section">
1306 <h4 class="section-title">
1307 <span class="dashicons dashicons-clock"></span>
1308 <?php esc_html_e('Task Scheduling', 'botwriter'); ?>
1309 </h4>
1310
1311 <?php if ($show_wp_cron_disabled_warning): ?>
1312 <div class="notice notice-warning inline bw-notice-info-inline" style="margin-bottom: 15px;">
1313 <p>
1314 <strong><?php esc_html_e('WordPress cron is disabled (DISABLE_WP_CRON).', 'botwriter'); ?></strong>
1315 <?php
1316 echo esc_html(sprintf(
1317 /* translators: %d: number of active tasks */
1318 _n(
1319 'BotWriter currently has %d active task.',
1320 'BotWriter currently has %d active tasks.',
1321 $active_tasks_count,
1322 'botwriter'
1323 ),
1324 $active_tasks_count
1325 ));
1326 ?>
1327 <?php esc_html_e('Automatic task execution will not run unless your hosting administrator configures a server cron for wp-cron.php.', 'botwriter'); ?>
1328 </p>
1329 <p><?php esc_html_e('Please enable WP-Cron or contact your hosting administrator.', 'botwriter'); ?></p>
1330 </div>
1331 <?php endif; ?>
1332
1333 <div class="form-row">
1334 <label><?php esc_html_e('Pause between daily posts of the same task:', 'botwriter'); ?></label>
1335 <div class="input-with-suffix">
1336 <input type="number" name="botwriter_paused_tasks" value="<?php echo esc_attr($settings['botwriter_paused_tasks']); ?>" min="2" class="small-input botwriter-autosave">
1337 <span class="suffix"><?php esc_html_e('minutes', 'botwriter'); ?></span>
1338 </div>
1339 </div>
1340
1341 <div class="form-row checkbox-row">
1342 <label>
1343 <input type="checkbox" name="botwriter_cron_active" value="1" <?php checked(get_option('botwriter_cron_active'), '1'); ?> class="botwriter-autosave">
1344 <?php esc_html_e('Enable automatic task execution', 'botwriter'); ?>
1345 </label>
1346 </div>
1347 </div>
1348
1349 <div class="general-settings-section">
1350 <h4 class="section-title">
1351 <span class="dashicons dashicons-controls-repeat"></span>
1352 <?php esc_html_e('Duplicate prevention', 'botwriter'); ?>
1353 </h4>
1354
1355 <div class="form-row checkbox-row">
1356 <label>
1357 <input type="checkbox" name="botwriter_dedup_enabled" value="1" <?php checked($settings['botwriter_dedup_enabled'], '1'); ?> class="botwriter-autosave">
1358 <?php esc_html_e('Avoid publishing similar articles across tasks', 'botwriter'); ?>
1359 </label>
1360 <p class="description"><?php esc_html_e('When enabled, the plugin checks history from all tasks (last 7 days) and skips RSS / WordPress source articles that match a previously published one by URL, title or content. This prevents the same news from being rewritten multiple times when several tasks read overlapping feeds.', 'botwriter'); ?></p>
1361 </div>
1362
1363 <div class="form-row">
1364 <label><?php esc_html_e('Title similarity threshold:', 'botwriter'); ?></label>
1365 <div class="input-with-suffix">
1366 <input type="number" name="botwriter_dedup_title_threshold" value="<?php echo esc_attr($settings['botwriter_dedup_title_threshold']); ?>" min="0" max="100" step="1" class="small-input botwriter-autosave">
1367 <span class="suffix">%</span>
1368 </div>
1369 <p class="description"><?php esc_html_e('A candidate article is considered a duplicate if its title is at least this similar to a recently published one. Lower = stricter (more articles skipped). 0 disables title comparison. Default: 70.', 'botwriter'); ?></p>
1370 </div>
1371 </div>
1372
1373 <div class="general-settings-section">
1374 <h4 class="section-title">
1375 <span class="dashicons dashicons-hammer"></span>
1376 <?php esc_html_e('Tools', 'botwriter'); ?>
1377 </h4>
1378
1379 <div class="form-row">
1380 <button type="button" id="botwriter-reset-super-task-settings" class="button button-secondary">
1381 <span class="dashicons dashicons-update" style="vertical-align: middle; margin-right: 5px;"></span>
1382 <?php esc_html_e('Reset Super Task', 'botwriter'); ?>
1383 </button>
1384 <p class="description"><?php esc_html_e('Use this to reset a stuck Super Task and start fresh.', 'botwriter'); ?></p>
1385 </div>
1386
1387 <div class="form-row" style="margin-top: 15px;">
1388 <a href="#" class="button button-secondary" id="btn-reset-models">
1389 <span class="dashicons dashicons-database-remove" style="vertical-align: middle; margin-right: 5px;"></span>
1390 <?php esc_html_e('Reset Models to Default', 'botwriter'); ?>
1391 </a>
1392 <span id="reset-models-result" style="margin-left: 10px;"></span>
1393 <p class="description"><?php esc_html_e('Reset all AI model lists to factory defaults. Use this if model lists are corrupted or outdated.', 'botwriter'); ?></p>
1394 </div>
1395 </div>
1396
1397 <div class="general-settings-section">
1398 <h4 class="section-title">
1399 <span class="dashicons dashicons-admin-settings"></span>
1400 <?php esc_html_e('Others', 'botwriter'); ?>
1401 </h4>
1402
1403 <div class="form-row checkbox-row">
1404 <label>
1405 <input type="checkbox" name="botwriter_tags_disabled" value="1" <?php checked(get_option('botwriter_tags_disabled'), '1'); ?> class="botwriter-autosave">
1406 <?php esc_html_e('Disable Tags', 'botwriter'); ?>
1407 </label>
1408 <p class="description"><?php esc_html_e('When enabled, posts created by BotWriter will not have tags assigned.', 'botwriter'); ?></p>
1409 </div>
1410
1411 <div class="form-row checkbox-row">
1412 <label>
1413 <input type="checkbox" name="botwriter_image_error_continue" value="1" <?php checked(get_option('botwriter_image_error_continue', '0'), '1'); ?> class="botwriter-autosave">
1414 <?php esc_html_e('Publish without image if image generation fails', 'botwriter'); ?>
1415 </label>
1416 <p class="description"><?php esc_html_e('When enabled, if the AI image generation fails (quota exceeded, API error, etc.), the post will still be published without a featured image instead of being marked as an error. Useful if you prefer to never lose a post due to image issues.', 'botwriter'); ?></p>
1417 </div>
1418
1419 <div class="form-row checkbox-row">
1420 <label>
1421 <input type="checkbox" name="botwriter_editor_assistant_enabled" value="1" <?php checked(get_option('botwriter_editor_assistant_enabled', '1'), '1'); ?> class="botwriter-autosave">
1422 <?php esc_html_e('Enable Post Editor AI Assistant Widget', 'botwriter'); ?>
1423 </label>
1424 <p class="description"><?php esc_html_e('Show the floating BotWriter assistant widget while editing posts. Disable if you prefer a clean editor UI.', 'botwriter'); ?></p>
1425 </div>
1426
1427 <div class="form-row checkbox-row">
1428 <label>
1429 <input type="checkbox" name="botwriter_seo_module_enabled" value="1" <?php checked(get_option('botwriter_seo_module_enabled', '1'), '1'); ?> class="botwriter-autosave">
1430 <?php esc_html_e('SEO Module', 'botwriter'); ?>
1431 </label>
1432 <p class="description"><?php esc_html_e('Enable or disable loading of the complete BotWriter SEO module. Disable to reduce menu/UI complexity and skip SEO module bootstrapping.', 'botwriter'); ?></p>
1433 </div>
1434 </div>
1435
1436 <div class="general-settings-section">
1437 <h4 class="section-title">
1438 <span class="dashicons dashicons-editor-code"></span>
1439 <?php esc_html_e('Debug log', 'botwriter'); ?>
1440 </h4>
1441 <p class="description bw-mb-10">
1442 <?php
1443 $log_path = botwriter_debug_log_path();
1444 printf(
1445 /* translators: %s: absolute path to the debug log file */
1446 esc_html__('When enabled, BotWriter writes diagnostic events to %s. Useful when reporting issues or troubleshooting duplicate posts, RSS fetch failures, etc.', 'botwriter'),
1447 '<code>' . esc_html($log_path ? $log_path : 'wp-content/uploads/botwriter-debug.log') . '</code>'
1448 );
1449 ?>
1450 </p>
1451
1452 <div class="form-row checkbox-row">
1453 <label>
1454 <input type="checkbox" name="botwriter_debug_logging" value="1" <?php checked($settings['botwriter_debug_logging'], '1'); ?> class="botwriter-autosave">
1455 <?php esc_html_e('Enable debug logging to file', 'botwriter'); ?>
1456 </label>
1457 </div>
1458
1459 <div class="form-row">
1460 <div class="bw-debug-log-toolbar" style="display:flex;gap:8px;align-items:center;margin-bottom:8px;">
1461 <button type="button" class="button" id="botwriter-debug-log-refresh">
1462 <span class="dashicons dashicons-update" style="margin-top:4px;"></span>
1463 <?php esc_html_e('Refresh', 'botwriter'); ?>
1464 </button>
1465 <button type="button" class="button" id="botwriter-debug-log-clear">
1466 <span class="dashicons dashicons-trash" style="margin-top:4px;"></span>
1467 <?php esc_html_e('Clear', 'botwriter'); ?>
1468 </button>
1469 <span id="botwriter-debug-log-status" style="color:#666;font-size:12px;"></span>
1470 </div>
1471 <textarea id="botwriter-debug-log-viewer" readonly rows="18" spellcheck="false"
1472 style="width:100%;background:#000;color:#e6e6e6;font-family:Menlo,Consolas,'Courier New',monospace;font-size:12px;line-height:1.45;padding:12px;border:1px solid #333;border-radius:4px;white-space:pre;overflow:auto;"
1473 placeholder="<?php esc_attr_e('Click Refresh to load the log…', 'botwriter'); ?>"></textarea>
1474 </div>
1475 </div>
1476
1477 </div>
1478
1479 <!-- Tab: SEO -->
1480 <div id="main-tab-seo" class="botwriter-main-tab-content">
1481 <div class="general-settings-section">
1482 <h4 class="section-title">
1483 <span class="dashicons dashicons-chart-line"></span>
1484 <?php esc_html_e('SEO Post-Processing Overview', 'botwriter'); ?>
1485 </h4>
1486 <p class="description bw-mb-10">
1487 <?php esc_html_e('Configure what BotWriter runs right after publishing to improve SEO automatically.', 'botwriter'); ?>
1488 </p>
1489
1490 <div class="bw-seo-status-grid">
1491 <?php foreach ($seo_plugins_status as $seo_plugin_row) : ?>
1492 <div class="bw-seo-status-pill <?php echo !empty($seo_plugin_row['detected']) ? 'is-active' : 'is-inactive'; ?>">
1493 <span class="dashicons <?php echo !empty($seo_plugin_row['detected']) ? 'dashicons-yes-alt' : 'dashicons-minus'; ?>"></span>
1494 <span><?php echo esc_html($seo_plugin_row['name']); ?></span>
1495 </div>
1496 <?php endforeach; ?>
1497 </div>
1498
1499 <p class="description">
1500 <?php
1501 echo esc_html(sprintf(
1502 /* translators: 1: detected plugins, 2: total supported plugins */
1503 __('Detected %1$d of %2$d supported SEO plugins. Meta synchronization will only write to plugins that are active on this site.', 'botwriter'),
1504 (int) $seo_plugins_detected_count,
1505 (int) count($seo_plugins_status)
1506 ));
1507 ?>
1508 </p>
1509 </div>
1510
1511 <div class="general-settings-section">
1512 <h4 class="section-title">
1513 <span class="dashicons dashicons-admin-tools"></span>
1514 <?php esc_html_e('Publish-time SEO Actions', 'botwriter'); ?>
1515 </h4>
1516
1517 <div class="form-row checkbox-row">
1518 <label>
1519 <input type="checkbox" name="botwriter_seo_ai_meta_enabled" value="1" <?php checked($settings['botwriter_seo_ai_meta_enabled'], '1'); ?> class="botwriter-autosave">
1520 <strong><?php esc_html_e('Generate SEO meta description with AI', 'botwriter'); ?></strong>
1521 </label>
1522 <p class="description"><?php esc_html_e('When enabled, BotWriter generates one SEO meta description and applies it automatically to the post excerpt plus active SEO plugin fields.', 'botwriter'); ?></p>
1523 </div>
1524
1525 <div class="form-row checkbox-row">
1526 <label>
1527 <input type="checkbox" name="botwriter_seo_auto_internal_links_enabled" value="1" <?php checked($settings['botwriter_seo_auto_internal_links_enabled'], '1'); ?> class="botwriter-autosave">
1528 <strong><?php esc_html_e('Enable automatic internal links on publish', 'botwriter'); ?></strong>
1529 </label>
1530 <p class="description"><?php esc_html_e('BotWriter first tries deterministic insertion from your existing content. If it cannot find a natural deterministic match and an API key is available, it falls back to AI selection.', 'botwriter'); ?></p>
1531 </div>
1532
1533 <div class="form-row">
1534 <label><?php esc_html_e('Automatic internal link targets:', 'botwriter'); ?></label>
1535 <select name="botwriter_seo_internal_links_scope" class="botwriter-autosave bw-select-wide">
1536 <option value="any" <?php selected($settings['botwriter_seo_internal_links_scope'], 'any'); ?>><?php esc_html_e('Any (posts, pages, products)', 'botwriter'); ?></option>
1537 <option value="posts" <?php selected($settings['botwriter_seo_internal_links_scope'], 'posts'); ?>><?php esc_html_e('Posts only', 'botwriter'); ?></option>
1538 <option value="products" <?php selected($settings['botwriter_seo_internal_links_scope'], 'products'); ?>><?php esc_html_e('Products only', 'botwriter'); ?></option>
1539 </select>
1540 <p class="description"><?php esc_html_e('Controls which post types are eligible as automatic internal-link targets during publish-time SEO processing.', 'botwriter'); ?></p>
1541 </div>
1542
1543 <div class="form-row">
1544 <label><?php esc_html_e('Maximum automatic internal links per post:', 'botwriter'); ?></label>
1545 <div class="input-with-suffix">
1546 <input type="number" name="botwriter_seo_auto_internal_links_max_links" value="<?php echo esc_attr($settings['botwriter_seo_auto_internal_links_max_links']); ?>" min="1" max="8" class="small-input botwriter-autosave">
1547 <span class="suffix"><?php esc_html_e('links', 'botwriter'); ?></span>
1548 </div>
1549 <p class="description"><?php esc_html_e('Recommended range: 2-5 links for most articles. Heading tags (H1-H6) are always excluded from link insertion.', 'botwriter'); ?></p>
1550 </div>
1551
1552 <div class="form-row checkbox-row">
1553 <label>
1554 <input type="checkbox" name="botwriter_seo_featured_image_alt_enabled" value="1" <?php checked($settings['botwriter_seo_featured_image_alt_enabled'], '1'); ?> class="botwriter-autosave">
1555 <strong><?php esc_html_e('Set featured image ALT text automatically', 'botwriter'); ?></strong>
1556 </label>
1557 </div>
1558
1559 <div class="form-row checkbox-row">
1560 <label>
1561 <input type="checkbox" name="botwriter_seo_publish_focus_keyword_enabled" value="1" <?php checked($settings['botwriter_seo_publish_focus_keyword_enabled'], '1'); ?> class="botwriter-autosave">
1562 <strong><?php esc_html_e('Generate and sync focus keyword on publish', 'botwriter'); ?></strong>
1563 </label>
1564 <p class="description"><?php esc_html_e('When enabled, BotWriter generates one primary focus keyword and synchronizes it to BotWriter native SEO plus compatible plugin fields (Yoast, Rank Math, AIOSEO, SEOPress, TSF).', 'botwriter'); ?></p>
1565 </div>
1566
1567 <div class="form-row checkbox-row">
1568 <label>
1569 <input type="checkbox" name="botwriter_seo_publish_faq_enabled" value="1" <?php checked($settings['botwriter_seo_publish_faq_enabled'], '1'); ?> class="botwriter-autosave">
1570 <strong><?php esc_html_e('Generate FAQ + FAQ schema on publish', 'botwriter'); ?></strong>
1571 </label>
1572 </div>
1573
1574 <div class="form-row" id="botwriter-seo-faq-mode-row" style="<?php echo $settings['botwriter_seo_publish_faq_enabled'] === '1' ? '' : 'display:none;'; ?>">
1575 <label><?php esc_html_e('FAQ output mode:', 'botwriter'); ?></label>
1576 <select name="botwriter_seo_publish_faq_mode" class="botwriter-autosave bw-select-wide">
1577 <option value="visible_schema" <?php selected($settings['botwriter_seo_publish_faq_mode'], 'visible_schema'); ?>><?php esc_html_e('Visible block + FAQ schema', 'botwriter'); ?></option>
1578 <option value="schema_only" <?php selected($settings['botwriter_seo_publish_faq_mode'], 'schema_only'); ?>><?php esc_html_e('Schema only (hidden block)', 'botwriter'); ?></option>
1579 </select>
1580 <p class="description"><?php esc_html_e('Applied when FAQ generation is enabled.', 'botwriter'); ?></p>
1581 </div>
1582
1583 <div class="form-row checkbox-row">
1584 <label>
1585 <input type="checkbox" name="botwriter_seo_social_meta_enabled" value="1" <?php checked($settings['botwriter_seo_social_meta_enabled'], '1'); ?> class="botwriter-autosave">
1586 <strong><?php esc_html_e('Emit social meta tags (Open Graph + Twitter Cards)', 'botwriter'); ?></strong>
1587 </label>
1588 <p class="description"><?php esc_html_e('Outputs deterministic social tags only when no major SEO plugin is active, to avoid duplicate head metadata.', 'botwriter'); ?></p>
1589 </div>
1590 </div>
1591
1592 <div class="general-settings-section">
1593 <h4 class="section-title">
1594 <span class="dashicons dashicons-translation"></span>
1595 <?php esc_html_e('AI Slug Translation', 'botwriter'); ?>
1596 </h4>
1597 <p class="description bw-mb-10">
1598 <?php esc_html_e('Use AI to translate post/tag/image slugs into your target SEO language. Useful for multilingual workflows where content language and URL language are different.', 'botwriter'); ?>
1599 </p>
1600
1601 <div class="form-row checkbox-row">
1602 <label>
1603 <input type="checkbox" name="botwriter_seo_translation_enabled" value="1" <?php checked($settings['botwriter_seo_translation_enabled'], '1'); ?> class="botwriter-autosave">
1604 <strong><?php esc_html_e('Enable AI slug translation', 'botwriter'); ?></strong>
1605 </label>
1606 </div>
1607
1608 <div class="form-row">
1609 <label for="botwriter_seo_target_language"><?php esc_html_e('Translate slugs to:', 'botwriter'); ?></label>
1610 <select name="botwriter_seo_target_language" id="botwriter_seo_target_language" class="botwriter-autosave bw-select-wide">
1611 <?php foreach ($seo_languages as $code => $name) : ?>
1612 <option value="<?php echo esc_attr($code); ?>" <?php selected($settings['botwriter_seo_target_language'], $code); ?>>
1613 <?php echo esc_html($name); ?>
1614 </option>
1615 <?php endforeach; ?>
1616 </select>
1617 <p class="description"><?php esc_html_e('Example: post written in Spanish but URL slug translated to English for international SEO.', 'botwriter'); ?></p>
1618 </div>
1619
1620 <div class="form-row checkbox-row">
1621 <label>
1622 <input type="checkbox" name="botwriter_seo_translate_title" value="1" <?php checked($settings['botwriter_seo_translate_title'], '1'); ?> class="botwriter-autosave">
1623 <?php esc_html_e('Post Slug (URL)', 'botwriter'); ?>
1624 </label>
1625 </div>
1626
1627 <div class="form-row checkbox-row">
1628 <label>
1629 <input type="checkbox" name="botwriter_seo_translate_tags" value="1" <?php checked($settings['botwriter_seo_translate_tags'], '1'); ?> class="botwriter-autosave">
1630 <?php esc_html_e('Tag Slugs', 'botwriter'); ?>
1631 </label>
1632 </div>
1633
1634 <div class="form-row checkbox-row">
1635 <label>
1636 <input type="checkbox" name="botwriter_seo_translate_image" value="1" <?php checked($settings['botwriter_seo_translate_image'], '1'); ?> class="botwriter-autosave">
1637 <?php esc_html_e('Image Filenames', 'botwriter'); ?>
1638 </label>
1639 </div>
1640 </div>
1641
1642 </div>
1643 <?php
1644 }
1645
1646 /**
1647 * Get all settings with defaults
1648 */
1649 function botwriter_get_all_settings() {
1650 $legacy_meta_disabled = get_option('botwriter_meta_disabled', '0');
1651 $legacy_meta_enabled = ($legacy_meta_disabled === '1') ? '0' : '1';
1652
1653 $dalle_default = function_exists('botwriter_get_provider_default_image_model')
1654 ? botwriter_get_provider_default_image_model('dalle')
1655 : 'gpt-image-1';
1656 $gemini_default = function_exists('botwriter_get_provider_default_image_model')
1657 ? botwriter_get_provider_default_image_model('gemini')
1658 : 'gemini-3.1-flash-lite-image';
1659 $fal_default = function_exists('botwriter_get_provider_default_image_model')
1660 ? botwriter_get_provider_default_image_model('fal')
1661 : 'fal-ai/flux-pro/v1.1';
1662 $replicate_default = function_exists('botwriter_get_provider_default_image_model')
1663 ? botwriter_get_provider_default_image_model('replicate')
1664 : 'black-forest-labs/flux-1.1-pro';
1665 $stability_default = function_exists('botwriter_get_provider_default_image_model')
1666 ? botwriter_get_provider_default_image_model('stability')
1667 : 'sd3.5-large-turbo';
1668 $cloudflare_default = function_exists('botwriter_get_provider_default_image_model')
1669 ? botwriter_get_provider_default_image_model('cloudflare')
1670 : 'flux-1-schnell';
1671
1672 if ($dalle_default === '') {
1673 $dalle_default = 'gpt-image-1';
1674 }
1675 if ($gemini_default === '') {
1676 $gemini_default = 'gemini-3.1-flash-lite-image';
1677 }
1678 if ($fal_default === '') {
1679 $fal_default = 'fal-ai/flux-pro/v1.1';
1680 }
1681 if ($replicate_default === '') {
1682 $replicate_default = 'black-forest-labs/flux-1.1-pro';
1683 }
1684 if ($stability_default === '') {
1685 $stability_default = 'sd3.5-large-turbo';
1686 }
1687 if ($cloudflare_default === '') {
1688 $cloudflare_default = 'flux-1-schnell';
1689 }
1690
1691 return array(
1692 'botwriter_ai_image_size' => function_exists('botwriter_normalize_ai_image_size')
1693 ? botwriter_normalize_ai_image_size((string) get_option('botwriter_ai_image_size', 'square'))
1694 : get_option('botwriter_ai_image_size', 'square'),
1695 'botwriter_ai_image_quality' => get_option('botwriter_ai_image_quality', 'medium'),
1696 'botwriter_ai_image_style' => get_option('botwriter_ai_image_style', 'realistic'),
1697 'botwriter_ai_image_style_custom' => get_option('botwriter_ai_image_style_custom', ''),
1698 'botwriter_image_postprocess_enabled' => get_option('botwriter_image_postprocess_enabled', '0'),
1699 'botwriter_image_output_format' => get_option('botwriter_image_output_format', 'webp'),
1700 'botwriter_image_max_width' => get_option('botwriter_image_max_width', '1200'),
1701 'botwriter_image_compression' => get_option('botwriter_image_compression', '85'),
1702 'botwriter_image_max_filesize' => get_option('botwriter_image_max_filesize', '120'),
1703 'botwriter_sslverify' => get_option('botwriter_sslverify', 'yes'),
1704 'botwriter_cron_active' => get_option('botwriter_cron_active', '1'),
1705 'botwriter_paused_tasks' => get_option('botwriter_paused_tasks', '2'),
1706 'botwriter_tags_disabled' => get_option('botwriter_tags_disabled', '0'),
1707 'botwriter_meta_disabled' => get_option('botwriter_meta_disabled', '0'),
1708 'botwriter_editor_assistant_enabled' => get_option('botwriter_editor_assistant_enabled', '1'),
1709 // SEO post-processing
1710 'botwriter_seo_sync_meta_enabled' => get_option('botwriter_seo_sync_meta_enabled', $legacy_meta_enabled),
1711 'botwriter_seo_ai_meta_enabled' => get_option('botwriter_seo_ai_meta_enabled', $legacy_meta_enabled),
1712 'botwriter_seo_auto_internal_links_enabled' => get_option('botwriter_seo_auto_internal_links_enabled', '0'),
1713 'botwriter_seo_internal_links_scope' => in_array(sanitize_key((string) get_option('botwriter_seo_internal_links_scope', 'any')), array('any', 'posts', 'products'), true)
1714 ? sanitize_key((string) get_option('botwriter_seo_internal_links_scope', 'any'))
1715 : 'any',
1716 'botwriter_seo_auto_internal_links_max_links' => get_option('botwriter_seo_auto_internal_links_max_links', '3'),
1717 'botwriter_seo_ai_internal_links_enabled' => get_option('botwriter_seo_ai_internal_links_enabled', '0'),
1718 'botwriter_seo_featured_image_alt_enabled' => get_option('botwriter_seo_featured_image_alt_enabled', '1'),
1719 'botwriter_seo_publish_focus_keyword_enabled' => get_option('botwriter_seo_publish_focus_keyword_enabled', '0'),
1720 'botwriter_seo_publish_faq_enabled' => get_option('botwriter_seo_publish_faq_enabled', '0'),
1721 'botwriter_seo_publish_faq_mode' => get_option('botwriter_seo_publish_faq_mode', 'visible_schema'),
1722 'botwriter_seo_social_meta_enabled' => get_option('botwriter_seo_social_meta_enabled', '0'),
1723 // SEO Translation
1724 'botwriter_seo_translation_enabled' => get_option('botwriter_seo_translation_enabled', '0'),
1725 'botwriter_seo_target_language' => get_option('botwriter_seo_target_language', 'en'),
1726 'botwriter_seo_translate_title' => get_option('botwriter_seo_translate_title', '1'),
1727 'botwriter_seo_translate_tags' => get_option('botwriter_seo_translate_tags', '1'),
1728 'botwriter_seo_translate_image' => get_option('botwriter_seo_translate_image', '1'),
1729 // Duplicate detection
1730 'botwriter_dedup_enabled' => get_option('botwriter_dedup_enabled', '1'),
1731 'botwriter_dedup_title_threshold' => get_option('botwriter_dedup_title_threshold', '70'),
1732 // Debug logging to file
1733 'botwriter_debug_logging' => get_option('botwriter_debug_logging', '0'),
1734 // API keys (encrypted, just check if they exist for display purposes)
1735 'botwriter_openai_api_key' => get_option('botwriter_openai_api_key', ''),
1736 'botwriter_google_api_key' => get_option('botwriter_google_api_key', ''),
1737 'botwriter_anthropic_api_key' => get_option('botwriter_anthropic_api_key', ''),
1738 'botwriter_mistral_api_key' => get_option('botwriter_mistral_api_key', ''),
1739 'botwriter_groq_api_key' => get_option('botwriter_groq_api_key', ''),
1740 'botwriter_openrouter_api_key' => get_option('botwriter_openrouter_api_key', ''),
1741 'botwriter_fal_api_key' => get_option('botwriter_fal_api_key', ''),
1742 'botwriter_replicate_api_key' => get_option('botwriter_replicate_api_key', ''),
1743 'botwriter_stability_api_key' => get_option('botwriter_stability_api_key', ''),
1744 'botwriter_cloudflare_api_key' => get_option('botwriter_cloudflare_api_key', ''),
1745 // Text models
1746 'botwriter_openai_model' => function_exists('botwriter_get_provider_text_model')
1747 ? botwriter_get_provider_text_model('openai')
1748 : get_option('botwriter_openai_model', 'gpt-5.4-mini'),
1749 'botwriter_anthropic_model' => function_exists('botwriter_get_provider_text_model')
1750 ? botwriter_get_provider_text_model('anthropic')
1751 : get_option('botwriter_anthropic_model', 'claude-sonnet-4-6'),
1752 'botwriter_google_model' => function_exists('botwriter_get_provider_text_model')
1753 ? botwriter_get_provider_text_model('google')
1754 : get_option('botwriter_google_model', 'gemini-3.5-flash'),
1755 'botwriter_mistral_model' => function_exists('botwriter_get_provider_text_model')
1756 ? botwriter_get_provider_text_model('mistral')
1757 : get_option('botwriter_mistral_model', 'mistral-large-latest'),
1758 'botwriter_groq_model' => function_exists('botwriter_get_provider_text_model')
1759 ? botwriter_get_provider_text_model('groq')
1760 : get_option('botwriter_groq_model', 'llama-3.3-70b-versatile'),
1761 'botwriter_openrouter_model' => function_exists('botwriter_get_provider_text_model')
1762 ? botwriter_get_provider_text_model('openrouter')
1763 : get_option('botwriter_openrouter_model', 'anthropic/claude-sonnet-4.6'),
1764 // Image models
1765 'botwriter_dalle_model' => function_exists('botwriter_get_current_image_model_by_provider')
1766 ? botwriter_get_current_image_model_by_provider('dalle')
1767 : get_option('botwriter_dalle_model', $dalle_default),
1768 'botwriter_gemini_image_model' => function_exists('botwriter_get_current_image_model_by_provider')
1769 ? botwriter_get_current_image_model_by_provider('gemini')
1770 : get_option('botwriter_gemini_image_model', $gemini_default),
1771 'botwriter_fal_model' => function_exists('botwriter_get_current_image_model_by_provider')
1772 ? botwriter_get_current_image_model_by_provider('fal')
1773 : get_option('botwriter_fal_model', $fal_default),
1774 'botwriter_replicate_model' => function_exists('botwriter_get_current_image_model_by_provider')
1775 ? botwriter_get_current_image_model_by_provider('replicate')
1776 : get_option('botwriter_replicate_model', $replicate_default),
1777 'botwriter_stability_model' => function_exists('botwriter_get_current_image_model_by_provider')
1778 ? botwriter_get_current_image_model_by_provider('stability')
1779 : get_option('botwriter_stability_model', $stability_default),
1780 'botwriter_cloudflare_model' => function_exists('botwriter_get_current_image_model_by_provider')
1781 ? botwriter_get_current_image_model_by_provider('cloudflare')
1782 : get_option('botwriter_cloudflare_model', $cloudflare_default),
1783 'botwriter_cloudflare_account_id' => get_option('botwriter_cloudflare_account_id', ''),
1784 // Stock photo settings
1785 'botwriter_stockphoto_preferred' => get_option('botwriter_stockphoto_preferred', 'random'),
1786 'botwriter_stockphoto_selection' => get_option('botwriter_stockphoto_selection', 'random_top10'),
1787 'botwriter_stockphoto_attribution' => get_option('botwriter_stockphoto_attribution', 'caption'),
1788 );
1789 }
1790
1791 // Generic function to encrypt any API key
1792 function botwriter_encrypt_api_key_generic($api_key) {
1793 if (empty($api_key)) {
1794 return '';
1795 }
1796
1797 if (!function_exists('openssl_encrypt')) {
1798 return $api_key;
1799 }
1800
1801 if (!defined('AUTH_KEY')) {
1802 return $api_key;
1803 }
1804
1805 $key = hash('sha256', AUTH_KEY, true);
1806 $encrypted = openssl_encrypt($api_key, 'AES-256-ECB', $key, 0);
1807
1808 if ($encrypted === false) {
1809 return $api_key;
1810 }
1811
1812 return base64_encode($encrypted);
1813 }
1814
1815 // Legacy function for backwards compatibility (OpenAI key)
1816 function botwriter_encrypt_api_key($api_key) {
1817 global $botwriter_notice;
1818
1819 if (!function_exists('openssl_encrypt')) {
1820 update_option('botwriter_openai_api_key', $api_key);
1821 $botwriter_notice .= __('The OpenSSL extension is not available. The API Key will be stored unencrypted, which is not secure. Contact your hosting provider.', 'botwriter') . '<br>';
1822 return $api_key;
1823 }
1824
1825 if (!defined('AUTH_KEY')) {
1826 $botwriter_notice .= __('AUTH_KEY not found in wp-config.php. Please configure WordPress security keys.', 'botwriter') . '<br>';
1827 return get_option('botwriter_openai_api_key');
1828 }
1829
1830 $key = hash('sha256', AUTH_KEY, true);
1831 $encrypted = openssl_encrypt($api_key, 'AES-256-ECB', $key, 0);
1832 if ($encrypted === false) {
1833 $botwriter_notice .= __('Failed to encrypt the API Key.', 'botwriter') . '<br>';
1834 return get_option('botwriter_openai_api_key');
1835 }
1836
1837 $encrypted_base64 = base64_encode($encrypted);
1838 update_option('botwriter_openai_api_key', $encrypted_base64);
1839
1840 return $encrypted_base64;
1841 }
1842
1843 // Decrypt API key (works for any provider)
1844 function botwriter_decrypt_api_key($encrypted_api_key) {
1845 if (empty($encrypted_api_key)) {
1846 return '';
1847 }
1848
1849 if (!function_exists('openssl_decrypt')) {
1850 return $encrypted_api_key;
1851 }
1852
1853 if (!defined('AUTH_KEY')) {
1854 // Fallback: support legacy/plain-text keys if AUTH_KEY is unavailable.
1855 $plain_fallback = base64_decode($encrypted_api_key, true);
1856 return ($plain_fallback === false) ? $encrypted_api_key : '';
1857 }
1858
1859 $key = hash('sha256', AUTH_KEY, true);
1860 $encrypted = base64_decode($encrypted_api_key, true);
1861
1862 if ($encrypted === false) {
1863 // Legacy/plain-text values should still render in settings fields.
1864 return $encrypted_api_key;
1865 }
1866
1867 $decrypted = openssl_decrypt($encrypted, 'AES-256-ECB', $key, 0);
1868 if ($decrypted === false) {
1869 return '';
1870 }
1871
1872 return $decrypted;
1873 }
1874
1875 // Validate OpenAI API Key format and test it
1876 function botwriter_open_api_key_validate($input) {
1877 global $botwriter_notice;
1878
1879 $input = sanitize_text_field($input);
1880
1881 // Validate basic format (e.g., starts with "sk-")
1882 if (strpos($input, 'sk-') !== 0) {
1883 $botwriter_notice .= __('The OpenAI API Key must start with "sk-".', 'botwriter') . '<br>';
1884 return false;
1885 }
1886
1887 // Make a test request to the OpenAI API
1888 $response = wp_remote_get('https://api.openai.com/v1/models', array(
1889 'headers' => array(
1890 'Authorization' => 'Bearer ' . $input,
1891 'Content-Type' => 'application/json',
1892 ),
1893 'timeout' => 15,
1894 ));
1895
1896 if (is_wp_error($response)) {
1897 $botwriter_notice .= __('Error validating the API Key: ', 'botwriter') . $response->get_error_message() . '<br>';
1898 return false;
1899 }
1900
1901 $response_code = wp_remote_retrieve_response_code($response);
1902 if ($response_code !== 200) {
1903 /* translators: %s: HTTP response code returned by OpenAI when validating the API key. */
1904 $botwriter_notice .= sprintf(__('The OpenAI API Key is invalid or lacks access. Error code: %s', 'botwriter'), $response_code) . '<br>';
1905 return false;
1906 }
1907
1908 return true;
1909 }
1910