PluginProbe
BotWriter – AI Writer & SEO Content Generator / 3.2.8
BotWriter – AI Writer & SEO Content Generator v3.2.8
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 3.2.8, at includes/settings.php

1,420 lines 68.9 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
19 // Include provider configuration files
20 $botwriter_settings_dir = plugin_dir_path(__FILE__) . 'settings/';
21 require_once $botwriter_settings_dir . 'openai.php';
22 require_once $botwriter_settings_dir . 'anthropic.php';
23 require_once $botwriter_settings_dir . 'google.php';
24 require_once $botwriter_settings_dir . 'mistral.php';
25 require_once $botwriter_settings_dir . 'groq.php';
26 require_once $botwriter_settings_dir . 'openrouter.php';
27 require_once $botwriter_settings_dir . 'dalle.php';
28 require_once $botwriter_settings_dir . 'gemini.php';
29 require_once $botwriter_settings_dir . 'fal.php';
30 require_once $botwriter_settings_dir . 'replicate.php';
31 require_once $botwriter_settings_dir . 'stability.php';
32 require_once $botwriter_settings_dir . 'cloudflare.php';
33 require_once $botwriter_settings_dir . 'stockphoto.php';
34 require_once $botwriter_settings_dir . 'none.php';
35
36 // Global notice accumulator for settings-related warnings
37 $botwriter_notice = '';
38
39 // Register AJAX handlers
40 add_action('wp_ajax_botwriter_save_settings', 'botwriter_ajax_save_settings');
41 add_action('wp_ajax_botwriter_test_api_key', 'botwriter_ajax_test_api_key');
42 add_action('wp_ajax_botwriter_test_model', 'botwriter_ajax_test_model');
43 add_action('wp_ajax_botwriter_reset_models', 'botwriter_ajax_reset_models');
44
45 /**
46 * AJAX handler to test API key connectivity
47 * Uses the /models endpoint which doesn't consume tokens
48 */
49 function botwriter_ajax_test_api_key() {
50 // Verify nonce
51 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'botwriter_settings_nonce')) {
52 wp_send_json_error(array('message' => __('Security check failed.', 'botwriter')));
53 }
54
55 // Verify permissions
56 if (!current_user_can('manage_options')) {
57 wp_send_json_error(array('message' => __('Permission denied.', 'botwriter')));
58 }
59
60 $provider = isset($_POST['provider']) ? sanitize_text_field(wp_unslash($_POST['provider'])) : '';
61 $api_key = isset($_POST['api_key']) ? sanitize_text_field(wp_unslash($_POST['api_key'])) : '';
62
63 if (empty($provider)) {
64 wp_send_json_error(array('message' => __('No provider specified.', 'botwriter')));
65 }
66
67 if (empty($api_key)) {
68 wp_send_json_error(array('message' => __('Please enter an API key first.', 'botwriter')));
69 }
70
71 // Provider endpoints for testing (using /models which is free and doesn't consume tokens)
72 $providers_config = array(
73 // Text providers
74 'openai' => array(
75 'url' => 'https://api.openai.com/v1/models',
76 'headers' => array(
77 'Authorization' => 'Bearer ' . $api_key,
78 ),
79 ),
80 'anthropic' => array(
81 'url' => 'https://api.anthropic.com/v1/models',
82 'headers' => array(
83 'x-api-key' => $api_key,
84 'anthropic-version' => '2023-06-01',
85 ),
86 ),
87 'google' => array(
88 'url' => 'https://generativelanguage.googleapis.com/v1beta/models?key=' . $api_key,
89 'headers' => array(),
90 ),
91 'mistral' => array(
92 'url' => 'https://api.mistral.ai/v1/models',
93 'headers' => array(
94 'Authorization' => 'Bearer ' . $api_key,
95 ),
96 ),
97 'groq' => array(
98 'url' => 'https://api.groq.com/openai/v1/models',
99 'headers' => array(
100 'Authorization' => 'Bearer ' . $api_key,
101 ),
102 ),
103 'openrouter' => array(
104 'url' => 'https://openrouter.ai/api/v1/models',
105 'headers' => array(
106 'Authorization' => 'Bearer ' . $api_key,
107 ),
108 ),
109 // Image providers
110 'dalle' => array(
111 'url' => 'https://api.openai.com/v1/models',
112 'headers' => array(
113 'Authorization' => 'Bearer ' . $api_key,
114 ),
115 ),
116 'fal' => array(
117 'url' => 'https://api.fal.ai/v1/models',
118 'headers' => array(
119 'Authorization' => 'Key ' . $api_key,
120 ),
121 ),
122 'replicate' => array(
123 'url' => 'https://api.replicate.com/v1/account',
124 'headers' => array(
125 'Authorization' => 'Bearer ' . $api_key,
126 ),
127 ),
128 'stability' => array(
129 'url' => 'https://api.stability.ai/v1/user/account',
130 'headers' => array(
131 'Authorization' => 'Bearer ' . $api_key,
132 ),
133 ),
134 'cloudflare' => array(
135 'url' => 'https://api.cloudflare.com/client/v4/user/tokens/verify',
136 'headers' => array(
137 'Authorization' => 'Bearer ' . $api_key,
138 ),
139 ),
140 // Gemini image uses Google text provider API key
141 'gemini' => array(
142 'url' => 'https://generativelanguage.googleapis.com/v1beta/models?key=' . $api_key,
143 'headers' => array(),
144 ),
145 );
146
147 if (!isset($providers_config[$provider])) {
148 wp_send_json_error(array('message' => __('Unknown provider.', 'botwriter')));
149 }
150
151 $config = $providers_config[$provider];
152 $ssl_verify = get_option('botwriter_sslverify', 'yes') === 'yes';
153
154 $response = wp_remote_get($config['url'], array(
155 'timeout' => 15,
156 'sslverify' => $ssl_verify,
157 'headers' => $config['headers'],
158 ));
159
160 if (is_wp_error($response)) {
161 wp_send_json_error(array(
162 'message' => sprintf(
163 /* translators: %s: Error message from the API */
164 __('Connection error: %s', 'botwriter'),
165 $response->get_error_message()
166 ),
167 ));
168 }
169
170 $code = wp_remote_retrieve_response_code($response);
171 $body = wp_remote_retrieve_body($response);
172 $data = json_decode($body, true);
173
174 // Check for success (200 OK)
175 if ($code === 200) {
176 // Extract models list
177 $models = array();
178 $models_data = array();
179
180 if (isset($data['data']) && is_array($data['data'])) {
181 $models_data = $data['data'];
182 } elseif (isset($data['models']) && is_array($data['models'])) {
183 $models_data = $data['models'];
184 }
185
186 // Build models array with id and name
187 foreach ($models_data as $model) {
188 $model_id = '';
189 $model_name = '';
190
191 // Different providers have different response structures
192 if (isset($model['id'])) {
193 $model_id = $model['id'];
194 $model_name = isset($model['name']) ? $model['name'] : $model['id'];
195 } elseif (isset($model['name'])) {
196 $model_id = $model['name'];
197 $model_name = isset($model['displayName']) ? $model['displayName'] : (isset($model['display_name']) ? $model['display_name'] : $model['name']);
198 }
199
200 // Google Gemini returns model names with "models/" prefix - remove it
201 if (strpos($model_id, 'models/') === 0) {
202 $model_id = substr($model_id, 7); // Remove "models/" prefix
203 }
204 if (strpos($model_name, 'models/') === 0) {
205 $model_name = substr($model_name, 7);
206 }
207
208 // Filter OpenAI models: exclude non-text models
209 if ($provider === 'openai') {
210 $exclude_patterns = array('dall-e', 'whisper', 'tts', 'embedding', 'moderation');
211 $should_exclude = false;
212 foreach ($exclude_patterns as $pattern) {
213 if (stripos($model_id, $pattern) !== false) {
214 $should_exclude = true;
215 break;
216 }
217 }
218 if ($should_exclude) {
219 continue;
220 }
221 }
222
223 // Filter Google models: only those supporting generateContent
224 if ($provider === 'google') {
225 $supported_methods = isset($model['supportedGenerationMethods']) ? $model['supportedGenerationMethods'] : array();
226 if (!in_array('generateContent', $supported_methods)) {
227 continue;
228 }
229 }
230
231 if (!empty($model_id)) {
232 $models[] = array(
233 'id' => $model_id,
234 'name' => $model_name,
235 );
236 }
237 }
238
239 // Sort models alphabetically by id
240 usort($models, function($a, $b) {
241 return strcasecmp($a['id'], $b['id']);
242 });
243
244 $model_count = count($models);
245
246 // Save models to database using our models manager
247 if ($model_count > 0) {
248 botwriter_update_provider_all_models($provider, $models);
249 }
250
251 $message = __('API key is valid!', 'botwriter');
252 if ($model_count > 0) {
253 $message .= ' ' . sprintf(
254 /* translators: %d: Number of models available */
255 _n('%d model available.', '%d models available.', $model_count, 'botwriter'),
256 $model_count
257 );
258 }
259
260 wp_send_json_success(array(
261 'message' => $message,
262 'models' => $models,
263 'provider' => $provider,
264 ));
265 }
266
267 // Handle error responses
268 $error_message = __('Invalid API key.', 'botwriter');
269
270 if ($code === 401 || $code === 403) {
271 $error_message = __('Invalid or unauthorized API key.', 'botwriter');
272 } elseif ($code === 429) {
273 $error_message = __('Rate limit exceeded. Please try again later.', 'botwriter');
274 } elseif ($code === 500 || $code === 502 || $code === 503) {
275 $error_message = __('Provider service temporarily unavailable.', 'botwriter');
276 }
277
278 // Try to get error message from response
279 if (isset($data['error']['message'])) {
280 $error_message = sanitize_text_field($data['error']['message']);
281 } elseif (isset($data['message'])) {
282 $error_message = sanitize_text_field($data['message']);
283 }
284
285 wp_send_json_error(array('message' => $error_message));
286 }
287
288 /**
289 * AJAX handler to test model connectivity
290 * Sends a minimal prompt to verify the model works
291 */
292 function botwriter_ajax_test_model() {
293 // Verify nonce
294 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'botwriter_settings_nonce')) {
295 wp_send_json_error(array('message' => __('Security check failed.', 'botwriter')));
296 }
297
298 // Verify permissions
299 if (!current_user_can('manage_options')) {
300 wp_send_json_error(array('message' => __('Permission denied.', 'botwriter')));
301 }
302
303 $provider = isset($_POST['provider']) ? sanitize_text_field(wp_unslash($_POST['provider'])) : '';
304 $model = isset($_POST['model']) ? sanitize_text_field(wp_unslash($_POST['model'])) : '';
305
306 if (empty($provider)) {
307 wp_send_json_error(array('message' => __('No provider specified.', 'botwriter')));
308 }
309
310 if (empty($model)) {
311 wp_send_json_error(array('message' => __('No model specified.', 'botwriter')));
312 }
313
314 // Get API key for provider
315 $api_key_option = 'botwriter_' . $provider . '_api_key';
316 $api_key = botwriter_decrypt_api_key(get_option($api_key_option));
317
318 if (empty($api_key)) {
319 wp_send_json_error(array('message' => __('Please configure the API key first.', 'botwriter')));
320 }
321
322 $ssl_verify = get_option('botwriter_sslverify', 'yes') === 'yes';
323 $test_message = 'Say "Ready!" in one word.';
324
325 // Provider-specific API calls
326 switch ($provider) {
327 case 'openai':
328 // GPT-5 and GPT-4.1 models require max_completion_tokens instead of max_tokens
329 $is_new_model = preg_match('/^(gpt-5|gpt-4\.1|o\d)/i', $model);
330 $token_param = $is_new_model ? 'max_completion_tokens' : 'max_tokens';
331
332 $response = wp_remote_post('https://api.openai.com/v1/chat/completions', array(
333 'timeout' => 30,
334 'sslverify' => $ssl_verify,
335 'headers' => array(
336 'Authorization' => 'Bearer ' . $api_key,
337 'Content-Type' => 'application/json',
338 ),
339 'body' => wp_json_encode(array(
340 'model' => $model,
341 'messages' => array(
342 array('role' => 'user', 'content' => $test_message)
343 ),
344 $token_param => 10,
345 )),
346 ));
347 break;
348
349 case 'anthropic':
350 $response = wp_remote_post('https://api.anthropic.com/v1/messages', array(
351 'timeout' => 30,
352 'sslverify' => $ssl_verify,
353 'headers' => array(
354 'x-api-key' => $api_key,
355 'anthropic-version' => '2023-06-01',
356 'Content-Type' => 'application/json',
357 ),
358 'body' => wp_json_encode(array(
359 'model' => $model,
360 'max_tokens' => 10,
361 'messages' => array(
362 array('role' => 'user', 'content' => $test_message)
363 ),
364 )),
365 ));
366 break;
367
368 case 'google':
369 $url = 'https://generativelanguage.googleapis.com/v1beta/models/' . $model . ':generateContent?key=' . $api_key;
370 $response = wp_remote_post($url, array(
371 'timeout' => 30,
372 'sslverify' => $ssl_verify,
373 'headers' => array(
374 'Content-Type' => 'application/json',
375 ),
376 'body' => wp_json_encode(array(
377 'contents' => array(
378 array(
379 'parts' => array(
380 array('text' => $test_message)
381 )
382 )
383 ),
384 'generationConfig' => array(
385 'maxOutputTokens' => 10,
386 ),
387 )),
388 ));
389 break;
390
391 case 'mistral':
392 $response = wp_remote_post('https://api.mistral.ai/v1/chat/completions', array(
393 'timeout' => 30,
394 'sslverify' => $ssl_verify,
395 'headers' => array(
396 'Authorization' => 'Bearer ' . $api_key,
397 'Content-Type' => 'application/json',
398 ),
399 'body' => wp_json_encode(array(
400 'model' => $model,
401 'messages' => array(
402 array('role' => 'user', 'content' => $test_message)
403 ),
404 'max_tokens' => 10,
405 )),
406 ));
407 break;
408
409 case 'groq':
410 $response = wp_remote_post('https://api.groq.com/openai/v1/chat/completions', array(
411 'timeout' => 30,
412 'sslverify' => $ssl_verify,
413 'headers' => array(
414 'Authorization' => 'Bearer ' . $api_key,
415 'Content-Type' => 'application/json',
416 ),
417 'body' => wp_json_encode(array(
418 'model' => $model,
419 'messages' => array(
420 array('role' => 'user', 'content' => $test_message)
421 ),
422 'max_tokens' => 10,
423 )),
424 ));
425 break;
426
427 case 'openrouter':
428 $response = wp_remote_post('https://openrouter.ai/api/v1/chat/completions', array(
429 'timeout' => 30,
430 'sslverify' => $ssl_verify,
431 'headers' => array(
432 'Authorization' => 'Bearer ' . $api_key,
433 'Content-Type' => 'application/json',
434 'HTTP-Referer' => home_url(),
435 'X-Title' => 'BotWriter',
436 ),
437 'body' => wp_json_encode(array(
438 'model' => $model,
439 'messages' => array(
440 array('role' => 'user', 'content' => $test_message)
441 ),
442 'max_tokens' => 10,
443 )),
444 ));
445 break;
446
447 default:
448 wp_send_json_error(array('message' => __('Unknown provider.', 'botwriter')));
449 return;
450 }
451
452 if (is_wp_error($response)) {
453 wp_send_json_error(array(
454 'message' => sprintf(
455 /* translators: %s: Error message */
456 __('Connection error: %s', 'botwriter'),
457 $response->get_error_message()
458 ),
459 ));
460 }
461
462 $code = wp_remote_retrieve_response_code($response);
463 $body = wp_remote_retrieve_body($response);
464 $data = json_decode($body, true);
465
466 // Extract response text based on provider
467 $reply = '';
468 if ($code === 200) {
469 switch ($provider) {
470 case 'openai':
471 case 'mistral':
472 case 'groq':
473 case 'openrouter':
474 $reply = $data['choices'][0]['message']['content'] ?? '';
475 break;
476 case 'anthropic':
477 $reply = $data['content'][0]['text'] ?? '';
478 break;
479 case 'google':
480 $reply = $data['candidates'][0]['content']['parts'][0]['text'] ?? '';
481 break;
482 }
483
484 if (!empty($reply)) {
485 wp_send_json_success(array(
486 'message' => sprintf(
487 /* translators: %s: Model response */
488 __('Model responded: "%s"', 'botwriter'),
489 esc_html(trim($reply))
490 ),
491 ));
492 } else {
493 wp_send_json_success(array('message' => __('Model is working!', 'botwriter')));
494 }
495 }
496
497 // Handle error responses
498 $error_message = __('Model test failed.', 'botwriter');
499
500 if ($code === 401 || $code === 403) {
501 $error_message = __('Invalid or unauthorized API key.', 'botwriter');
502 } elseif ($code === 404) {
503 $error_message = __('Model not found. It may not be available for your account.', 'botwriter');
504 } elseif ($code === 429) {
505 $error_message = __('Rate limit exceeded. Please try again later.', 'botwriter');
506 } elseif ($code === 500 || $code === 502 || $code === 503) {
507 $error_message = __('Provider service temporarily unavailable.', 'botwriter');
508 }
509
510 // Try to get error message from response
511 if (isset($data['error']['message'])) {
512 $error_message = sanitize_text_field($data['error']['message']);
513 } elseif (isset($data['message'])) {
514 $error_message = sanitize_text_field($data['message']);
515 }
516
517 wp_send_json_error(array('message' => $error_message));
518 }
519
520 /**
521 * AJAX handler to reset models to default
522 */
523 function botwriter_ajax_reset_models() {
524 // Verify nonce
525 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'botwriter_settings_nonce')) {
526 wp_send_json_error(array('message' => __('Security check failed.', 'botwriter')));
527 }
528
529 // Verify permissions
530 if (!current_user_can('manage_options')) {
531 wp_send_json_error(array('message' => __('Permission denied.', 'botwriter')));
532 }
533
534 // Reset models to default
535 if (botwriter_reset_models_to_default()) {
536 wp_send_json_success(array(
537 'message' => __('Models reset to defaults successfully!', 'botwriter'),
538 ));
539 } else {
540 wp_send_json_error(array(
541 'message' => __('Failed to reset models. Please try again.', 'botwriter'),
542 ));
543 }
544 }
545
546 /**
547 * AJAX handler to save individual settings
548 */
549 function botwriter_ajax_save_settings() {
550 // Verify nonce
551 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'botwriter_settings_nonce')) {
552 wp_send_json_error(['message' => __('Security check failed.', 'botwriter')]);
553 }
554
555 // Verify permissions
556 if (!current_user_can('manage_options')) {
557 wp_send_json_error(['message' => __('Permission denied.', 'botwriter')]);
558 }
559
560 $field = isset($_POST['field']) ? sanitize_text_field(wp_unslash($_POST['field'])) : '';
561 $value = isset($_POST['value']) ? wp_unslash($_POST['value']) : '';
562
563 if (empty($field)) {
564 wp_send_json_error(['message' => __('No field specified.', 'botwriter')]);
565 }
566
567 // List of allowed fields
568 $allowed_fields = [
569 'botwriter_text_provider',
570 'botwriter_image_provider',
571 'botwriter_openai_api_key',
572 'botwriter_anthropic_api_key',
573 'botwriter_google_api_key',
574 'botwriter_mistral_api_key',
575 'botwriter_groq_api_key',
576 'botwriter_openrouter_api_key',
577 'botwriter_fal_api_key',
578 'botwriter_replicate_api_key',
579 'botwriter_stability_api_key',
580 'botwriter_cloudflare_api_key',
581 'botwriter_cloudflare_account_id',
582 'botwriter_openai_model',
583 'botwriter_anthropic_model',
584 'botwriter_google_model',
585 'botwriter_mistral_model',
586 'botwriter_groq_model',
587 'botwriter_openrouter_model',
588 'botwriter_dalle_model',
589 'botwriter_gemini_image_model',
590 'botwriter_fal_model',
591 'botwriter_replicate_model',
592 'botwriter_stability_model',
593 'botwriter_cloudflare_model',
594 'botwriter_ai_image_size',
595 'botwriter_ai_image_quality',
596 'botwriter_ai_image_style',
597 'botwriter_ai_image_style_custom',
598 'botwriter_image_postprocess_enabled',
599 'botwriter_image_output_format',
600 'botwriter_image_max_width',
601 'botwriter_image_compression',
602 'botwriter_image_max_filesize',
603 'botwriter_sslverify',
604 'botwriter_cron_active',
605 'botwriter_paused_tasks',
606 'botwriter_tags_disabled',
607 'botwriter_meta_disabled',
608 'botwriter_image_error_continue',
609 // SEO Translation fields
610 'botwriter_seo_translation_enabled',
611 'botwriter_seo_target_language',
612 'botwriter_seo_translate_title',
613 'botwriter_seo_translate_tags',
614 'botwriter_seo_translate_image',
615 // Stock photo fields
616 'botwriter_stockphoto_preferred',
617 'botwriter_stockphoto_selection',
618 'botwriter_stockphoto_attribution',
619 ];
620
621 if (!in_array($field, $allowed_fields)) {
622 wp_send_json_error(['message' => __('Invalid field.', 'botwriter')]);
623 }
624
625 // API key fields that need encryption
626 $api_key_fields = [
627 'botwriter_openai_api_key',
628 'botwriter_anthropic_api_key',
629 'botwriter_google_api_key',
630 'botwriter_mistral_api_key',
631 'botwriter_groq_api_key',
632 'botwriter_openrouter_api_key',
633 'botwriter_fal_api_key',
634 'botwriter_replicate_api_key',
635 'botwriter_stability_api_key',
636 'botwriter_cloudflare_api_key',
637 ];
638
639 if (in_array($field, $api_key_fields)) {
640 $value = sanitize_text_field($value);
641 if (!empty($value)) {
642 // Special validation for OpenAI key
643 if ($field === 'botwriter_openai_api_key') {
644 if (strpos($value, 'sk-') !== 0) {
645 wp_send_json_error(['message' => __('OpenAI API Key must start with "sk-".', 'botwriter')]);
646 }
647 }
648 $value = botwriter_encrypt_api_key_generic($value);
649 }
650 } elseif ($field === 'botwriter_paused_tasks') {
651 $value = max(2, intval($value));
652 } elseif ($field === 'botwriter_cron_active' || $field === 'botwriter_tags_disabled'
653 || $field === 'botwriter_meta_disabled'
654 || $field === 'botwriter_seo_translation_enabled' || $field === 'botwriter_seo_translate_title'
655 || $field === 'botwriter_seo_translate_tags' || $field === 'botwriter_seo_translate_image') {
656 $value = ($value === '1' || $value === 'true' || $value === true) ? '1' : '0';
657 } else {
658 $value = sanitize_text_field($value);
659 }
660
661 // Save the option
662 update_option($field, $value);
663
664 // Handle cron activation/deactivation
665 if ($field === 'botwriter_cron_active') {
666 if ($value === '1') {
667 botwriter_scheduled_events_plugin_activate();
668 } else {
669 botwriter_scheduled_events_plugin_deactivate();
670 }
671 }
672
673 wp_send_json_success(['message' => __('Saved', 'botwriter'), 'field' => $field]);
674 }
675
676 /**
677 * Main settings page handler
678 */
679 function botwriter_settings_page_handler() {
680 // Verify permissions
681 if (!current_user_can('manage_options')) {
682 wp_die(esc_html__('You do not have permission to access this page.', 'botwriter'));
683 }
684
685 // Add metabox
686 add_meta_box(
687 'botwriter_settings',
688 __('WP BotWriter Settings', 'botwriter'),
689 'botwriter_settings_meta_box_handler',
690 'botwriter_settings_page',
691 'normal',
692 'default'
693 );
694
695 ?>
696 <div class="wrap botwriter-settings-wrap">
697 <h2><?php esc_html_e('Settings', 'botwriter'); ?>
698 <span id="botwriter-save-status" class="botwriter-save-indicator"></span>
699 </h2>
700
701 <div id="botwriter-settings-form">
702 <input type="hidden" id="botwriter_settings_nonce" value="<?php echo esc_attr(wp_create_nonce('botwriter_settings_nonce')); ?>" />
703 <input id="botwriter_domain_name" type="hidden" name="url" value="<?php echo esc_attr(get_site_url()); ?>" />
704
705 <div class="metabox-holder" id="poststuff">
706 <div id="post-body">
707 <div id="post-body-content">
708 <?php do_meta_boxes('botwriter_settings_page', 'normal', null); ?>
709 </div>
710 </div>
711 </div>
712 </div>
713 </div>
714 <?php
715 }
716
717 /**
718 * Main metabox handler - renders the settings form content
719 */
720 function botwriter_settings_meta_box_handler() {
721 // Get current values with defaults
722 $text_provider = get_option('botwriter_text_provider', 'openai');
723 $image_provider = get_option('botwriter_image_provider', 'dalle');
724 $show_wp_cron_disabled_warning = function_exists('botwriter_should_show_wp_cron_disabled_warning') && botwriter_should_show_wp_cron_disabled_warning();
725 $active_tasks_count = $show_wp_cron_disabled_warning && function_exists('botwriter_get_enabled_tasks_count')
726 ? (int) botwriter_get_enabled_tasks_count()
727 : 0;
728
729 $settings = botwriter_get_all_settings();
730
731 // Provider lists
732 $text_providers = [
733 'openai' => 'OpenAI (GPT-5, GPT-4o)',
734 'anthropic' => 'Anthropic (Claude)',
735 'google' => 'Google (Gemini) - FREE TIER',
736 'mistral' => 'Mistral AI - FREE TIER',
737 'groq' => 'Groq (Ultra Fast) - FREE TIER',
738 'openrouter' => 'OpenRouter (Multiple) - FREE TIER',
739 ];
740
741 $image_providers = [
742 'dalle' => 'DALL-E (OpenAI)',
743 'gemini' => 'Google Gemini',
744 'fal' => 'Fal.ai (Flux)',
745 'replicate' => 'Replicate',
746 'stability' => 'Stability AI',
747 'cloudflare' => 'Cloudflare AI (FREE)',
748 'stockphoto' => '📷 [FREE] Stock Images',
749 'none' => '🚫 No Image Generation',
750 ];
751
752 ?>
753
754 <!-- Main Tabs Navigation (Text AI / Image AI) -->
755 <div class="botwriter-main-tabs">
756 <a href="#" class="main-tab main-tab-active" data-main-tab="text">
757 <span class="dashicons dashicons-edit"></span>
758 <?php esc_html_e('Text AI Providers', 'botwriter'); ?>
759 </a>
760 <a href="#" class="main-tab" data-main-tab="images">
761 <span class="dashicons dashicons-format-image"></span>
762 <?php esc_html_e('Image AI Providers', 'botwriter'); ?>
763 </a>
764 <a href="#" class="main-tab" data-main-tab="imagesettings">
765 <span class="dashicons dashicons-admin-appearance"></span>
766 <?php esc_html_e('Image Settings', 'botwriter'); ?>
767 </a>
768 <a href="#" class="main-tab" data-main-tab="general">
769 <span class="dashicons dashicons-admin-generic"></span>
770 <?php esc_html_e('General Settings', 'botwriter'); ?>
771 </a>
772 <a href="#" class="main-tab" data-main-tab="seo">
773 <span class="dashicons dashicons-translation"></span>
774 <?php esc_html_e('SEO Translation', 'botwriter'); ?>
775 </a>
776 </div>
777
778 <!-- Tab: Text AI Providers -->
779 <div id="main-tab-text" class="botwriter-main-tab-content active">
780 <div class="provider-selection-header">
781 <h3><?php esc_html_e('Select Text AI Provider', 'botwriter'); ?></h3>
782 <div class="form-row">
783 <select name="botwriter_text_provider" id="botwriter_text_provider" class="form-select provider-select">
784 <?php foreach ($text_providers as $id => $name): ?>
785 <option value="<?php echo esc_attr($id); ?>" <?php selected($text_provider, $id); ?>><?php echo esc_html($name); ?></option>
786 <?php endforeach; ?>
787 </select>
788 </div>
789 </div>
790
791 <!-- Provider Content Sections -->
792 <?php
793 foreach ($text_providers as $id => $name):
794 $is_active = ($id === $text_provider);
795 $render_func = 'botwriter_render_' . $id . '_settings';
796 ?>
797 <div class="provider-content <?php echo $is_active ? 'active' : ''; ?>"
798 id="provider-<?php echo esc_attr($id); ?>" data-provider="<?php echo esc_attr($id); ?>">
799 <?php
800 if (function_exists($render_func)) {
801 $render_func($settings, $is_active);
802 } else {
803 echo '<p>' . esc_html__('Provider configuration not available.', 'botwriter') . '</p>';
804 }
805 ?>
806 </div>
807 <?php endforeach; ?>
808 </div>
809
810 <!-- Tab: Image AI Providers -->
811 <div id="main-tab-images" class="botwriter-main-tab-content">
812 <div class="provider-selection-header">
813 <h3><?php esc_html_e('Select Image AI Provider', 'botwriter'); ?></h3>
814 <div class="form-row">
815 <select name="botwriter_image_provider" id="botwriter_image_provider" class="form-select provider-select">
816 <?php foreach ($image_providers as $id => $name): ?>
817 <option value="<?php echo esc_attr($id); ?>" <?php selected($image_provider, $id); ?>><?php echo esc_html($name); ?></option>
818 <?php endforeach; ?>
819 </select>
820 </div>
821 </div>
822
823 <!-- Image Provider Content Sections -->
824 <?php
825 foreach ($image_providers as $id => $name):
826 $is_active = ($id === $image_provider);
827 $render_func = 'botwriter_render_' . $id . '_settings';
828 ?>
829 <div class="provider-content <?php echo $is_active ? 'active' : ''; ?>"
830 id="image-provider-<?php echo esc_attr($id); ?>" data-provider="<?php echo esc_attr($id); ?>">
831 <?php
832 if (function_exists($render_func)) {
833 $render_func($settings, $is_active);
834 } else {
835 echo '<p>' . esc_html__('Provider configuration not available.', 'botwriter') . '</p>';
836 }
837 ?>
838 </div>
839 <?php endforeach; ?>
840 </div>
841
842 <!-- Tab: Image Settings -->
843 <div id="main-tab-imagesettings" class="botwriter-main-tab-content">
844 <div class="image-settings-section">
845 <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;'; ?>">
846 <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>
847 </div>
848 <h4 class="section-title">
849 <span class="dashicons dashicons-format-image"></span>
850 <?php esc_html_e('Image Generation Settings', 'botwriter'); ?>
851 </h4>
852
853 <div class="settings-grid-2col">
854 <div class="setting-card">
855 <div class="form-row">
856 <label><span class="dashicons dashicons-image-crop"></span> <?php esc_html_e('Image Format:', 'botwriter'); ?></label>
857 <select name="botwriter_ai_image_size" class="form-select" id="image_size_select">
858 <option value="landscape" <?php selected($settings['botwriter_ai_image_size'], 'landscape'); ?>><?php esc_html_e('🖼️ Landscape (16:9) - Blog headers', 'botwriter'); ?></option>
859 <option value="square" <?php selected($settings['botwriter_ai_image_size'], 'square'); ?>><?php esc_html_e('⬛ Square (1:1) - Social media', 'botwriter'); ?></option>
860 <option value="portrait" <?php selected($settings['botwriter_ai_image_size'], 'portrait'); ?>><?php esc_html_e('📱 Portrait (9:16) - Mobile/Stories', 'botwriter'); ?></option>
861 </select>
862 </div>
863
864 <div class="format-preview" id="format_preview">
865 <div class="preview-box landscape">
866 <span>16:9</span>
867 </div>
868 </div>
869
870 <div class="format-specs">
871 <p class="description"><strong><?php esc_html_e('Approximate dimensions by provider:', 'botwriter'); ?></strong></p>
872 <table class="specs-table" id="size_specs_table">
873 <tr><td>DALL-E</td><td id="dalle_size">1792×1024</td></tr>
874 <tr><td>Fal.ai/Flux</td><td id="fal_size">1344×768</td></tr>
875 <tr><td>Stability AI</td><td id="stability_size">1344×768</td></tr>
876 <tr><td>Replicate</td><td id="replicate_size">1344×768</td></tr>
877 </table>
878 </div>
879 </div>
880
881 <div class="setting-card">
882 <div class="form-row">
883 <label><span class="dashicons dashicons-dashboard"></span> <?php esc_html_e('Image Quality:', 'botwriter'); ?></label>
884 <select name="botwriter_ai_image_quality" class="form-select" id="image_quality_select">
885 <option value="low" <?php selected($settings['botwriter_ai_image_quality'], 'low'); ?>><?php esc_html_e('⚡ Low - Faster & Cheaper', 'botwriter'); ?></option>
886 <option value="medium" <?php selected($settings['botwriter_ai_image_quality'], 'medium'); ?>><?php esc_html_e('⚖️ Medium - Balanced', 'botwriter'); ?></option>
887 <option value="high" <?php selected($settings['botwriter_ai_image_quality'], 'high'); ?>><?php esc_html_e('✨ High - Best Quality', 'botwriter'); ?></option>
888 </select>
889 </div>
890
891 <div class="quality-indicator" id="quality_indicator">
892 <div class="quality-bar medium">
893 <div class="bar-fill"></div>
894 </div>
895 <div class="quality-labels">
896 <span><?php esc_html_e('Speed', 'botwriter'); ?></span>
897 <span><?php esc_html_e('Quality', 'botwriter'); ?></span>
898 </div>
899 </div>
900
901 <div class="quality-specs">
902 <p class="description"><strong><?php esc_html_e('Provider settings mapping:', 'botwriter'); ?></strong></p>
903 <table class="specs-table" id="quality_specs_table">
904 <tr><td>DALL-E</td><td id="dalle_quality">standard</td></tr>
905 <tr><td>Fal.ai</td><td id="fal_quality">20 steps</td></tr>
906 <tr><td>Stability AI</td><td id="stability_quality">sd3-large</td></tr>
907 <tr><td>Replicate</td><td id="replicate_quality">25 steps</td></tr>
908 </table>
909 </div>
910 </div>
911 </div>
912
913 <!-- Image Style Settings -->
914 <div class="settings-grid-2col" style="margin-top: 20px;">
915 <div class="setting-card">
916 <div class="form-row">
917 <label><span class="dashicons dashicons-art"></span> <?php esc_html_e('Image Style:', 'botwriter'); ?></label>
918 <select name="botwriter_ai_image_style" class="form-select botwriter-autosave" id="image_style_select">
919 <option value="realistic" <?php selected($settings['botwriter_ai_image_style'], 'realistic'); ?>><?php esc_html_e('📷 Realistic / Photographic', 'botwriter'); ?></option>
920 <option value="digital-art" <?php selected($settings['botwriter_ai_image_style'], 'digital-art'); ?>><?php esc_html_e('🎨 Digital Art', 'botwriter'); ?></option>
921 <option value="illustration" <?php selected($settings['botwriter_ai_image_style'], 'illustration'); ?>><?php esc_html_e('✏️ Illustration', 'botwriter'); ?></option>
922 <option value="cartoon" <?php selected($settings['botwriter_ai_image_style'], 'cartoon'); ?>><?php esc_html_e('🎭 Cartoon', 'botwriter'); ?></option>
923 <option value="comic" <?php selected($settings['botwriter_ai_image_style'], 'comic'); ?>><?php esc_html_e('💥 Comic Book', 'botwriter'); ?></option>
924 <option value="anime" <?php selected($settings['botwriter_ai_image_style'], 'anime'); ?>><?php esc_html_e('🌸 Anime / Manga', 'botwriter'); ?></option>
925 <option value="3d-render" <?php selected($settings['botwriter_ai_image_style'], '3d-render'); ?>><?php esc_html_e('🧊 3D Render', 'botwriter'); ?></option>
926 <option value="watercolor" <?php selected($settings['botwriter_ai_image_style'], 'watercolor'); ?>><?php esc_html_e('🖌️ Watercolor', 'botwriter'); ?></option>
927 <option value="oil-painting" <?php selected($settings['botwriter_ai_image_style'], 'oil-painting'); ?>><?php esc_html_e('🎨 Oil Painting', 'botwriter'); ?></option>
928 <option value="minimalist" <?php selected($settings['botwriter_ai_image_style'], 'minimalist'); ?>><?php esc_html_e('◻️ Minimalist', 'botwriter'); ?></option>
929 <option value="vintage" <?php selected($settings['botwriter_ai_image_style'], 'vintage'); ?>><?php esc_html_e('📻 Vintage / Retro', 'botwriter'); ?></option>
930 <option value="cinematic" <?php selected($settings['botwriter_ai_image_style'], 'cinematic'); ?>><?php esc_html_e('🎬 Cinematic', 'botwriter'); ?></option>
931 <option value="none" <?php selected($settings['botwriter_ai_image_style'], 'none'); ?>><?php esc_html_e('⚪ None (no style prefix)', 'botwriter'); ?></option>
932 <option value="custom" <?php selected($settings['botwriter_ai_image_style'], 'custom'); ?>><?php esc_html_e('✨ Custom (enter below)', 'botwriter'); ?></option>
933 </select>
934 </div>
935 <div class="form-row" id="custom_style_row" style="<?php echo $settings['botwriter_ai_image_style'] === 'custom' ? '' : 'display:none;'; ?>">
936 <label><?php esc_html_e('Custom Style:', 'botwriter'); ?></label>
937 <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'); ?>">
938 </div>
939 <p class="description"><?php esc_html_e('Style prefix added to image prompts. This affects the visual appearance of generated images.', 'botwriter'); ?></p>
940 </div>
941
942 <div class="setting-card">
943 <div class="form-row">
944 <label><span class="dashicons dashicons-admin-settings"></span> <?php esc_html_e('Image Post-Processing:', 'botwriter'); ?></label>
945 <label class="toggle-switch">
946 <input type="checkbox" name="botwriter_image_postprocess_enabled" class="botwriter-autosave" value="1" <?php checked($settings['botwriter_image_postprocess_enabled'], '1'); ?>>
947 <span class="slider round"></span>
948 </label>
949 <span class="toggle-label"><?php esc_html_e('Enable optimization', 'botwriter'); ?></span>
950 </div>
951 <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>
952 </div>
953 </div>
954
955 <!-- Post-processing options (shown when enabled) -->
956 <div id="postprocess_options" class="settings-grid-2col" style="margin-top: 15px; <?php echo $settings['botwriter_image_postprocess_enabled'] === '1' ? '' : 'display:none;'; ?>">
957 <div class="setting-card">
958 <div class="form-row">
959 <label><span class="dashicons dashicons-format-image"></span> <?php esc_html_e('Output Format:', 'botwriter'); ?></label>
960 <select name="botwriter_image_output_format" class="form-select botwriter-autosave">
961 <option value="webp" <?php selected($settings['botwriter_image_output_format'], 'webp'); ?>><?php esc_html_e('WebP (recommended)', 'botwriter'); ?></option>
962 <option value="jpeg" <?php selected($settings['botwriter_image_output_format'], 'jpeg'); ?>><?php esc_html_e('JPEG', 'botwriter'); ?></option>
963 <option value="png" <?php selected($settings['botwriter_image_output_format'], 'png'); ?>><?php esc_html_e('PNG', 'botwriter'); ?></option>
964 <option value="original" <?php selected($settings['botwriter_image_output_format'], 'original'); ?>><?php esc_html_e('Keep Original', 'botwriter'); ?></option>
965 </select>
966 </div>
967 <div class="form-row">
968 <label><span class="dashicons dashicons-leftright"></span> <?php esc_html_e('Max Width (px):', 'botwriter'); ?></label>
969 <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">
970 <p class="description"><?php esc_html_e('Images wider than this will be resized. Recommended: 1200px for blog headers.', 'botwriter'); ?></p>
971 </div>
972 </div>
973 <div class="setting-card">
974 <div class="form-row">
975 <label><span class="dashicons dashicons-dashboard"></span> <?php esc_html_e('Compression Quality:', 'botwriter'); ?></label>
976 <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">
977 <span id="compression_value"><?php echo esc_html($settings['botwriter_image_compression']); ?>%</span>
978 </div>
979 <p class="description"><?php esc_html_e('Lower = smaller file size but less quality. Recommended: 80-85% for web.', 'botwriter'); ?></p>
980 <div class="form-row">
981 <label><span class="dashicons dashicons-database"></span> <?php esc_html_e('Max File Size (KB):', 'botwriter'); ?></label>
982 <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">
983 <p class="description"><?php esc_html_e('Target max file size. Set 0 to disable. Google Discover recommends ≤120KB.', 'botwriter'); ?></p>
984 </div>
985 </div>
986 </div>
987
988 <div class="info-tip highlight-tip">
989 <span class="dashicons dashicons-lightbulb"></span>
990 <div>
991 <p><strong><?php esc_html_e('💡 Pro Tip:', 'botwriter'); ?></strong></p>
992 <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>
993 </div>
994 </div>
995
996 <div class="cost-comparison">
997 <h5><span class="dashicons dashicons-money-alt"></span> <?php esc_html_e('Estimated Cost per Image', 'botwriter'); ?></h5>
998 <div class="cost-cards">
999 <div class="cost-card">
1000 <span class="provider">DALL-E</span>
1001 <span class="cost" id="dalle_cost">$0.04 - $0.08</span>
1002 </div>
1003 <div class="cost-card">
1004 <span class="provider">Fal.ai</span>
1005 <span class="cost" id="fal_cost">$0.03 - $0.05</span>
1006 </div>
1007 <div class="cost-card cheapest">
1008 <span class="provider">Stability</span>
1009 <span class="cost" id="stability_cost">$0.02 - $0.04</span>
1010 <span class="badge"><?php esc_html_e('Cheapest', 'botwriter'); ?></span>
1011 </div>
1012 <div class="cost-card">
1013 <span class="provider">Replicate</span>
1014 <span class="cost" id="replicate_cost">$0.003 - $0.05</span>
1015 </div>
1016 </div>
1017 </div>
1018 </div>
1019 </div>
1020
1021 <!-- Tab: General Settings -->
1022 <div id="main-tab-general" class="botwriter-main-tab-content">
1023 <div class="general-settings-section">
1024 <h4 class="section-title">
1025 <span class="dashicons dashicons-clock"></span>
1026 <?php esc_html_e('Task Scheduling', 'botwriter'); ?>
1027 </h4>
1028
1029 <?php if ($show_wp_cron_disabled_warning): ?>
1030 <div class="notice notice-warning inline bw-notice-info-inline" style="margin-bottom: 15px;">
1031 <p>
1032 <strong><?php esc_html_e('WordPress cron is disabled (DISABLE_WP_CRON).', 'botwriter'); ?></strong>
1033 <?php
1034 echo esc_html(sprintf(
1035 /* translators: %d: number of active tasks */
1036 _n(
1037 'BotWriter currently has %d active task.',
1038 'BotWriter currently has %d active tasks.',
1039 $active_tasks_count,
1040 'botwriter'
1041 ),
1042 $active_tasks_count
1043 ));
1044 ?>
1045 <?php esc_html_e('Automatic task execution will not run unless your hosting administrator configures a server cron for wp-cron.php.', 'botwriter'); ?>
1046 </p>
1047 <p><?php esc_html_e('Please enable WP-Cron or contact your hosting administrator.', 'botwriter'); ?></p>
1048 </div>
1049 <?php endif; ?>
1050
1051 <div class="form-row">
1052 <label><?php esc_html_e('Pause between daily posts of the same task:', 'botwriter'); ?></label>
1053 <div class="input-with-suffix">
1054 <input type="number" name="botwriter_paused_tasks" value="<?php echo esc_attr($settings['botwriter_paused_tasks']); ?>" min="2" class="small-input botwriter-autosave">
1055 <span class="suffix"><?php esc_html_e('minutes', 'botwriter'); ?></span>
1056 </div>
1057 </div>
1058
1059 <div class="form-row checkbox-row">
1060 <label>
1061 <input type="checkbox" name="botwriter_cron_active" value="1" <?php checked(get_option('botwriter_cron_active'), '1'); ?> class="botwriter-autosave">
1062 <?php esc_html_e('Enable automatic task execution', 'botwriter'); ?>
1063 </label>
1064 </div>
1065 </div>
1066
1067 <div class="general-settings-section">
1068 <h4 class="section-title">
1069 <span class="dashicons dashicons-admin-tools"></span>
1070 <?php esc_html_e('Advanced Settings', 'botwriter'); ?>
1071 </h4>
1072
1073 <div class="form-row checkbox-row warning-option">
1074 <label>
1075 <input type="checkbox" name="botwriter_sslverify" value="no" <?php checked($settings['botwriter_sslverify'], 'no'); ?> class="botwriter-autosave">
1076 <span class="warning-text"><?php esc_html_e('Disable SSL Verification (not recommended)', 'botwriter'); ?></span>
1077 </label>
1078 <p class="description"><?php esc_html_e('Only enable this if you have SSL certificate issues with API connections.', 'botwriter'); ?></p>
1079 </div>
1080 </div>
1081
1082 <div class="general-settings-section">
1083 <h4 class="section-title">
1084 <span class="dashicons dashicons-hammer"></span>
1085 <?php esc_html_e('Tools', 'botwriter'); ?>
1086 </h4>
1087
1088 <div class="form-row">
1089 <button type="button" id="botwriter-reset-super-task-settings" class="button button-secondary">
1090 <span class="dashicons dashicons-update" style="vertical-align: middle; margin-right: 5px;"></span>
1091 <?php esc_html_e('Reset Super Task', 'botwriter'); ?>
1092 </button>
1093 <p class="description"><?php esc_html_e('Use this to reset a stuck Super Task and start fresh.', 'botwriter'); ?></p>
1094 </div>
1095
1096 <div class="form-row" style="margin-top: 15px;">
1097 <a href="#" class="button button-secondary" id="btn-reset-models">
1098 <span class="dashicons dashicons-database-remove" style="vertical-align: middle; margin-right: 5px;"></span>
1099 <?php esc_html_e('Reset Models to Default', 'botwriter'); ?>
1100 </a>
1101 <span id="reset-models-result" style="margin-left: 10px;"></span>
1102 <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>
1103 </div>
1104 </div>
1105
1106 <div class="general-settings-section">
1107 <h4 class="section-title">
1108 <span class="dashicons dashicons-admin-settings"></span>
1109 <?php esc_html_e('Others', 'botwriter'); ?>
1110 </h4>
1111
1112 <div class="form-row checkbox-row">
1113 <label>
1114 <input type="checkbox" name="botwriter_tags_disabled" value="1" <?php checked(get_option('botwriter_tags_disabled'), '1'); ?> class="botwriter-autosave">
1115 <?php esc_html_e('Disable Tags', 'botwriter'); ?>
1116 </label>
1117 <p class="description"><?php esc_html_e('When enabled, posts created by BotWriter will not have tags assigned.', 'botwriter'); ?></p>
1118 </div>
1119
1120 <div class="form-row checkbox-row">
1121 <label>
1122 <input type="checkbox" name="botwriter_meta_disabled" value="1" <?php checked(get_option('botwriter_meta_disabled', '0'), '1'); ?> class="botwriter-autosave">
1123 <?php esc_html_e('Disable Meta Description', 'botwriter'); ?>
1124 </label>
1125 <p class="description"><?php esc_html_e('When enabled, BotWriter will NOT auto-generate an SEO meta description for new posts. The meta is generated using a fast AI call after each post is created and is saved to the post excerpt and to popular SEO plugins (Yoast, Rank Math, AIOSEO, SEOPress, The SEO Framework).', 'botwriter'); ?></p>
1126 </div>
1127
1128 <div class="form-row checkbox-row">
1129 <label>
1130 <input type="checkbox" name="botwriter_image_error_continue" value="1" <?php checked(get_option('botwriter_image_error_continue', '0'), '1'); ?> class="botwriter-autosave">
1131 <?php esc_html_e('Publish without image if image generation fails', 'botwriter'); ?>
1132 </label>
1133 <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>
1134 </div>
1135 </div>
1136
1137 </div>
1138
1139 <!-- Tab: SEO Translation -->
1140 <div id="main-tab-seo" class="botwriter-main-tab-content">
1141 <div class="general-settings-section">
1142 <h4 class="section-title">
1143 <span class="dashicons dashicons-translation"></span>
1144 <?php esc_html_e('SEO Slug Translation', 'botwriter'); ?>
1145 </h4>
1146 <p class="description bw-mb-10">
1147 <?php esc_html_e('By default, WordPress generates URL slugs from your post title in whatever language you write it. If you create content in Spanish, your slugs will be in Spanish (e.g. /receta-paella-valenciana/). The same applies to tag URLs and image filenames.', 'botwriter'); ?>
1148 </p>
1149 <p class="description bw-mb-10">
1150 <?php esc_html_e('This feature is useful if you write content in a non-English language but want your URLs in English for international SEO — or vice versa. If you already write in your target slug language, you do not need this.', 'botwriter'); ?>
1151 </p>
1152 <p class="description bw-mb-15">
1153 <?php esc_html_e('It uses a single, lightweight API call per post with your configured Text AI provider — cost is negligible (fractions of a cent per post).', 'botwriter'); ?>
1154 </p>
1155
1156 <div class="form-row checkbox-row">
1157 <label>
1158 <input type="checkbox" name="botwriter_seo_translation_enabled" value="1" <?php checked(get_option('botwriter_seo_translation_enabled', '0'), '1'); ?> class="botwriter-autosave">
1159 <strong><?php esc_html_e('Enable SEO Slug Translation', 'botwriter'); ?></strong>
1160 </label>
1161 <p class="description"><?php esc_html_e('When enabled, slugs will be translated using your configured text AI provider before each post is published.', 'botwriter'); ?></p>
1162 </div>
1163 </div>
1164
1165 <div class="general-settings-section">
1166 <h4 class="section-title">
1167 <span class="dashicons dashicons-admin-settings"></span>
1168 <?php esc_html_e('Target Language', 'botwriter'); ?>
1169 </h4>
1170
1171 <div class="form-row">
1172 <label for="botwriter_seo_target_language"><?php esc_html_e('Translate slugs to:', 'botwriter'); ?></label>
1173 <?php
1174 $seo_lang = get_option('botwriter_seo_target_language', 'en');
1175 $seo_languages = array(
1176 'en' => 'English', 'es' => 'Spanish (Español)', 'fr' => 'French (Français)',
1177 'de' => 'German (Deutsch)', 'it' => 'Italian (Italiano)', 'pt' => 'Portuguese (Português)',
1178 'nl' => 'Dutch (Nederlands)', 'ru' => 'Russian (Русский)',
1179 'ja' => 'Japanese (日本語)', 'ko' => 'Korean (한국어)',
1180 'zh' => 'Chinese (中文)', 'ar' => 'Arabic (العربية)',
1181 'hi' => 'Hindi (हिन्दी)', 'tr' => 'Turkish (Türkçe)',
1182 'pl' => 'Polish (Polski)', 'sv' => 'Swedish (Svenska)',
1183 'da' => 'Danish (Dansk)', 'no' => 'Norwegian (Norsk)',
1184 'fi' => 'Finnish (Suomi)', 'cs' => 'Czech (Čeština)',
1185 'ro' => 'Romanian (Română)', 'hu' => 'Hungarian (Magyar)',
1186 'el' => 'Greek (Ελληνικά)', 'th' => 'Thai (ไทย)',
1187 'vi' => 'Vietnamese (Tiếng Việt)', 'id' => 'Indonesian (Bahasa)',
1188 'ms' => 'Malay (Melayu)', 'uk' => 'Ukrainian (Українська)',
1189 );
1190 ?>
1191 <select name="botwriter_seo_target_language" id="botwriter_seo_target_language" class="botwriter-autosave bw-select-wide">
1192 <?php foreach ($seo_languages as $code => $name) : ?>
1193 <option value="<?php echo esc_attr($code); ?>" <?php selected($seo_lang, $code); ?>>
1194 <?php echo esc_html($name); ?>
1195 </option>
1196 <?php endforeach; ?>
1197 </select>
1198 <p class="description"><?php esc_html_e('Select the language your URL slugs should be translated to. Typically, choose English for best global SEO.', 'botwriter'); ?></p>
1199 </div>
1200 </div>
1201
1202 <div class="general-settings-section">
1203 <h4 class="section-title">
1204 <span class="dashicons dashicons-list-view"></span>
1205 <?php esc_html_e('What to Translate', 'botwriter'); ?>
1206 </h4>
1207
1208 <div class="form-row checkbox-row">
1209 <label>
1210 <input type="checkbox" name="botwriter_seo_translate_title" value="1" <?php checked(get_option('botwriter_seo_translate_title', '1'), '1'); ?> class="botwriter-autosave">
1211 <?php esc_html_e('Post Slug (URL)', 'botwriter'); ?>
1212 </label>
1213 <p class="description"><?php esc_html_e('Translate the post URL slug. Example: "receta-paella-valenciana" → "valencian-paella-recipe"', 'botwriter'); ?></p>
1214 </div>
1215
1216 <div class="form-row checkbox-row">
1217 <label>
1218 <input type="checkbox" name="botwriter_seo_translate_tags" value="1" <?php checked(get_option('botwriter_seo_translate_tags', '1'), '1'); ?> class="botwriter-autosave">
1219 <?php esc_html_e('Tag Slugs', 'botwriter'); ?>
1220 </label>
1221 <p class="description"><?php esc_html_e('Translate tag URL slugs for consistent multi-language taxonomy URLs.', 'botwriter'); ?></p>
1222 </div>
1223
1224 <div class="form-row checkbox-row">
1225 <label>
1226 <input type="checkbox" name="botwriter_seo_translate_image" value="1" <?php checked(get_option('botwriter_seo_translate_image', '1'), '1'); ?> class="botwriter-autosave">
1227 <?php esc_html_e('Image Filenames', 'botwriter'); ?>
1228 </label>
1229 <p class="description"><?php esc_html_e('Use translated slugs for AI-generated image filenames, improving image SEO.', 'botwriter'); ?></p>
1230 </div>
1231 </div>
1232
1233 <div class="general-settings-section">
1234 <h4 class="section-title">
1235 <span class="dashicons dashicons-info-outline"></span>
1236 <?php esc_html_e('How It Works', 'botwriter'); ?>
1237 </h4>
1238 <p class="description">
1239 <?php esc_html_e('When a post is generated, BotWriter makes one extra API call using your configured Text AI provider to translate the title, tags, and image names into SEO-friendly slugs. This uses a fast, lightweight model (e.g. GPT-4o-mini, Gemini Flash, Haiku) with ~200 tokens — costing less than $0.001 per post. No additional API key is needed.', 'botwriter'); ?>
1240 </p>
1241 </div>
1242 </div>
1243 <?php
1244 }
1245
1246 /**
1247 * Get all settings with defaults
1248 */
1249 function botwriter_get_all_settings() {
1250 return array(
1251 'botwriter_ai_image_size' => get_option('botwriter_ai_image_size', 'square'),
1252 'botwriter_ai_image_quality' => get_option('botwriter_ai_image_quality', 'medium'),
1253 'botwriter_ai_image_style' => get_option('botwriter_ai_image_style', 'realistic'),
1254 'botwriter_ai_image_style_custom' => get_option('botwriter_ai_image_style_custom', ''),
1255 'botwriter_image_postprocess_enabled' => get_option('botwriter_image_postprocess_enabled', '0'),
1256 'botwriter_image_output_format' => get_option('botwriter_image_output_format', 'webp'),
1257 'botwriter_image_max_width' => get_option('botwriter_image_max_width', '1200'),
1258 'botwriter_image_compression' => get_option('botwriter_image_compression', '85'),
1259 'botwriter_image_max_filesize' => get_option('botwriter_image_max_filesize', '120'),
1260 'botwriter_sslverify' => get_option('botwriter_sslverify', 'yes'),
1261 'botwriter_cron_active' => get_option('botwriter_cron_active', '1'),
1262 'botwriter_paused_tasks' => get_option('botwriter_paused_tasks', '2'),
1263 'botwriter_tags_disabled' => get_option('botwriter_tags_disabled', '0'),
1264 'botwriter_meta_disabled' => get_option('botwriter_meta_disabled', '0'),
1265 // SEO Translation
1266 'botwriter_seo_translation_enabled' => get_option('botwriter_seo_translation_enabled', '0'),
1267 'botwriter_seo_target_language' => get_option('botwriter_seo_target_language', 'en'),
1268 'botwriter_seo_translate_title' => get_option('botwriter_seo_translate_title', '1'),
1269 'botwriter_seo_translate_tags' => get_option('botwriter_seo_translate_tags', '1'),
1270 'botwriter_seo_translate_image' => get_option('botwriter_seo_translate_image', '1'),
1271 // API keys (encrypted, just check if they exist for display purposes)
1272 'botwriter_openai_api_key' => get_option('botwriter_openai_api_key', ''),
1273 'botwriter_google_api_key' => get_option('botwriter_google_api_key', ''),
1274 'botwriter_anthropic_api_key' => get_option('botwriter_anthropic_api_key', ''),
1275 'botwriter_mistral_api_key' => get_option('botwriter_mistral_api_key', ''),
1276 'botwriter_groq_api_key' => get_option('botwriter_groq_api_key', ''),
1277 'botwriter_openrouter_api_key' => get_option('botwriter_openrouter_api_key', ''),
1278 'botwriter_fal_api_key' => get_option('botwriter_fal_api_key', ''),
1279 'botwriter_replicate_api_key' => get_option('botwriter_replicate_api_key', ''),
1280 'botwriter_stability_api_key' => get_option('botwriter_stability_api_key', ''),
1281 'botwriter_cloudflare_api_key' => get_option('botwriter_cloudflare_api_key', ''),
1282 // Text models
1283 'botwriter_openai_model' => get_option('botwriter_openai_model', 'gpt-5-mini'),
1284 'botwriter_anthropic_model' => get_option('botwriter_anthropic_model', 'claude-sonnet-4-5-20250929'),
1285 'botwriter_google_model' => get_option('botwriter_google_model', 'gemini-2.5-flash'),
1286 'botwriter_mistral_model' => get_option('botwriter_mistral_model', 'mistral-large-latest'),
1287 'botwriter_groq_model' => get_option('botwriter_groq_model', 'llama-3.3-70b-versatile'),
1288 'botwriter_openrouter_model' => get_option('botwriter_openrouter_model', 'anthropic/claude-sonnet-4'),
1289 // Image models
1290 'botwriter_dalle_model' => get_option('botwriter_dalle_model', 'gpt-image-1'),
1291 'botwriter_gemini_image_model' => get_option('botwriter_gemini_image_model', 'gemini-2.5-flash-image'),
1292 'botwriter_fal_model' => get_option('botwriter_fal_model', 'fal-ai/flux-pro/v1.1'),
1293 'botwriter_replicate_model' => get_option('botwriter_replicate_model', 'black-forest-labs/flux-1.1-pro'),
1294 'botwriter_stability_model' => get_option('botwriter_stability_model', 'sd3.5-large-turbo'),
1295 'botwriter_cloudflare_model' => get_option('botwriter_cloudflare_model', 'flux-1-schnell'),
1296 'botwriter_cloudflare_account_id' => get_option('botwriter_cloudflare_account_id', ''),
1297 // Stock photo settings
1298 'botwriter_stockphoto_preferred' => get_option('botwriter_stockphoto_preferred', 'pixabay'),
1299 'botwriter_stockphoto_selection' => get_option('botwriter_stockphoto_selection', 'random_top10'),
1300 'botwriter_stockphoto_attribution' => get_option('botwriter_stockphoto_attribution', 'caption'),
1301 );
1302 }
1303
1304 // Generic function to encrypt any API key
1305 function botwriter_encrypt_api_key_generic($api_key) {
1306 if (empty($api_key)) {
1307 return '';
1308 }
1309
1310 if (!function_exists('openssl_encrypt')) {
1311 return $api_key;
1312 }
1313
1314 if (!defined('AUTH_KEY')) {
1315 return $api_key;
1316 }
1317
1318 $key = hash('sha256', AUTH_KEY, true);
1319 $encrypted = openssl_encrypt($api_key, 'AES-256-ECB', $key, 0);
1320
1321 if ($encrypted === false) {
1322 return $api_key;
1323 }
1324
1325 return base64_encode($encrypted);
1326 }
1327
1328 // Legacy function for backwards compatibility (OpenAI key)
1329 function botwriter_encrypt_api_key($api_key) {
1330 global $botwriter_notice;
1331
1332 if (!function_exists('openssl_encrypt')) {
1333 update_option('botwriter_openai_api_key', $api_key);
1334 $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>';
1335 return $api_key;
1336 }
1337
1338 if (!defined('AUTH_KEY')) {
1339 $botwriter_notice .= __('AUTH_KEY not found in wp-config.php. Please configure WordPress security keys.', 'botwriter') . '<br>';
1340 return get_option('botwriter_openai_api_key');
1341 }
1342
1343 $key = hash('sha256', AUTH_KEY, true);
1344 $encrypted = openssl_encrypt($api_key, 'AES-256-ECB', $key, 0);
1345 if ($encrypted === false) {
1346 $botwriter_notice .= __('Failed to encrypt the API Key.', 'botwriter') . '<br>';
1347 return get_option('botwriter_openai_api_key');
1348 }
1349
1350 $encrypted_base64 = base64_encode($encrypted);
1351 update_option('botwriter_openai_api_key', $encrypted_base64);
1352
1353 return $encrypted_base64;
1354 }
1355
1356 // Decrypt API key (works for any provider)
1357 function botwriter_decrypt_api_key($encrypted_api_key) {
1358 if (empty($encrypted_api_key)) {
1359 return '';
1360 }
1361
1362 if (!function_exists('openssl_decrypt')) {
1363 return $encrypted_api_key;
1364 }
1365
1366 if (!defined('AUTH_KEY')) {
1367 return '';
1368 }
1369
1370 $key = hash('sha256', AUTH_KEY, true);
1371 $encrypted = base64_decode($encrypted_api_key);
1372
1373 if (!$encrypted) {
1374 return '';
1375 }
1376
1377 $decrypted = openssl_decrypt($encrypted, 'AES-256-ECB', $key, 0);
1378 if ($decrypted === false) {
1379 return '';
1380 }
1381
1382 return $decrypted;
1383 }
1384
1385 // Validate OpenAI API Key format and test it
1386 function botwriter_open_api_key_validate($input) {
1387 global $botwriter_notice;
1388
1389 $input = sanitize_text_field($input);
1390
1391 // Validate basic format (e.g., starts with "sk-")
1392 if (strpos($input, 'sk-') !== 0) {
1393 $botwriter_notice .= __('The OpenAI API Key must start with "sk-".', 'botwriter') . '<br>';
1394 return false;
1395 }
1396
1397 // Make a test request to the OpenAI API
1398 $response = wp_remote_get('https://api.openai.com/v1/models', array(
1399 'headers' => array(
1400 'Authorization' => 'Bearer ' . $input,
1401 'Content-Type' => 'application/json',
1402 ),
1403 'timeout' => 15,
1404 ));
1405
1406 if (is_wp_error($response)) {
1407 $botwriter_notice .= __('Error validating the API Key: ', 'botwriter') . $response->get_error_message() . '<br>';
1408 return false;
1409 }
1410
1411 $response_code = wp_remote_retrieve_response_code($response);
1412 if ($response_code !== 200) {
1413 /* translators: %s: HTTP response code returned by OpenAI when validating the API key. */
1414 $botwriter_notice .= sprintf(__('The OpenAI API Key is invalid or lacks access. Error code: %s', 'botwriter'), $response_code) . '<br>';
1415 return false;
1416 }
1417
1418 return true;
1419 }
1420