PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.3
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.3
4.9.3 4.9.2 4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 3.4.2 3.5.0 All 201 releases
← All changes | includes/Core/Settings.php +222 -78 4.7.0 → 4.9.3 View file →
@@ -9,8 +9,10 @@
9 9 use WP_Error;
10 10 use WP_User;
11 11 use WPDeveloper\BetterDocs\Admin\Builder\GlobalFields;
12 12 use WPDeveloper\BetterDocs\Admin\Builder\Rules;
13 +use WPDeveloper\BetterDocs\AI\ModelRegistry;
14 +use WPDeveloper\BetterDocs\AI\ProviderFactory;
13 15 use WPDeveloper\BetterDocs\REST\AIEdit;
14 16 use WPDeveloper\BetterDocs\Utils\AIHelper;
15 17 use WPDeveloper\BetterDocs\Utils\Base;
16 18 use WPDeveloper\BetterDocs\Utils\Database;
@@ -77,8 +79,31 @@
77 79 return esc_url( admin_url( 'admin.php?page=betterdocs-settings' ) );
78 80 }
79 81
80 82 /**
83 + * Settings keys holding secret API keys. These are masked before reaching
84 + * the browser, stripped for non-admins, and never persisted as their mask.
85 + *
86 + * Covers the AI Chatbot key plus every content-suite platform key. OpenAI's
87 + * is `ai_autowrite_api_key` (see ProviderFactory::key_field_for), hence the
88 + * dedupe.
89 + *
90 + * @return array<int,string>
91 + */
92 + public static function sensitive_api_key_fields() {
93 + $fields = array(
94 + ProviderFactory::OPENAI_KEY_FIELD,
95 + 'ai_chatbot_api_key',
96 + );
97 + foreach ( array_keys( ModelRegistry::platforms() ) as $platform ) {
98 + $fields[] = ProviderFactory::key_field_for( $platform );
99 + }
100 + // Add-ons (e.g. the AI Chatbot) register their own per-platform keys here
101 + // so they are masked in the browser and stripped for non-admins.
102 + return apply_filters( 'betterdocs_sensitive_api_key_fields', array_values( array_unique( $fields ) ) );
103 + }
104 +
105 + /**
81 106 * This method is responsible for enqueueing scripts in settings panel
82 107 *
83 108 * @param string $hook
84 109 *
@@ -95,9 +120,9 @@
95 120
96 121 $settings = GlobalFields::normalize( $this->settings_args() );
97 122
98 123 // Mask sensitive API keys before they reach the browser. Non-admins still get them stripped entirely below.
99 - $sensitive_api_keys = array( 'ai_autowrite_api_key', 'ai_chatbot_api_key', 'ga4_api_secret', 'maxmind_license_key', 'maxmind_account_id' );
124 + $sensitive_api_keys = self::sensitive_api_key_fields();
100 125 foreach ( $sensitive_api_keys as $api_key_field ) {
101 126 if ( ! empty( $settings[ 'values' ][ $api_key_field ] ) ) {
102 127 $settings[ 'values' ][ $api_key_field ] = Helper::mask_api_key( $settings[ 'values' ][ $api_key_field ] );
103 128 }
@@ -258,8 +283,12 @@
258 283 'enable_breadcrumb_category' => true,
259 284 'enable_breadcrumb_title' => true,
260 285 'enable_sidebar_cat_list' => true,
261 286 'enable_print_icon' => true,
287 + 'print_enable_logo' => false,
288 + 'print_logo' => array(),
289 + 'print_enable_footer' => false,
290 + 'print_footer_text' => '',
262 291 'enable_tags' => true,
263 292 'email_feedback' => true,
264 293 'feedback_link_text' => __( 'Still stuck? How can we help?', 'betterdocs' ),
265 294 'reaction_feedback_text' => __( 'Thanks for your feedback', 'betterdocs' ),
@@ -272,8 +301,9 @@
272 301 'enable_credit' => false,
273 302 'enable_archive_sidebar' => true,
274 303 'archive_nested_subcategory' => true,
275 304 'archive_enable_pagination' => false,
305 + 'archive_lazy_load_descendants' => false,
276 306 'enable_content_restriction' => false,
277 307 'enable_reporting' => false,
278 308 'enable_sample_data' => false,
279 309 'reporting_day' => 'monday',
@@ -295,8 +325,19 @@
295 325 'ai_edit_actions' => AIEdit::default_actions(),
296 326 'enable_article_summary' => false,
297 327 'article_summary_model' => 'gpt-4o-mini',
298 328 'article_summary_max_token' => 1500,
329 + // Multi-platform AI (content suite). `ai_platform` selects the active
330 + // provider; `ai_model` is the single global model; keys are stored
331 + // per platform so switching never loses a saved key. OpenAI reuses
332 + // `ai_autowrite_api_key` above — it has always held an OpenAI key,
333 + // so nothing needs migrating.
334 + 'ai_platform' => 'openai',
335 + 'ai_model' => 'gpt-4o-mini',
336 + 'ai_api_key_gemini' => '',
337 + 'ai_api_key_claude' => '',
338 + 'ai_api_key_deepseek' => '',
339 + 'ai_api_key_openrouter' => '',
299 340 'enable_estimated_reading_time' => true,
300 341 'enable_encyclopedia' => false,
301 342 'enable_glossaries' => false,
302 343 'show_glossary_suggestions' => true,
@@ -306,9 +347,21 @@
306 347 'singular_estimated_reading_time_text' => __( 'min read', 'betterdocs' ),
307 348 'betterdocs_access_control_repeater' => array(),
308 349 'internal_knowledge_base_type' => 'basic',
309 350 'betterdocs_access_control_repeater_kb' => array(),
310 - 'enable_git_integration' => false
351 + 'enable_git_integration' => false,
352 + /**
353 + * MCP master switch. Off by default; the toggle lives on the
354 + * BetterDocs → MCP page, not in the settings tree, and writes
355 + * through POST betterdocs/v1/settings.
356 + *
357 + * The key has to be listed here or `get()` cannot see it at all:
358 + * it answers `$default` for anything absent from the defaults
359 + * array, whatever the stored option holds.
360 + *
361 + * @since 4.9.0
362 + */
363 + 'enable_mcp' => ''
311 364 );
312 365
313 366 $_default = apply_filters( 'betterdocs_default_settings', $_default );
314 367 // $_default = apply_filters_deprecated(
@@ -609,9 +662,9 @@
609 662 // slips into storage, mask(mask) === mask, so an equality-only guard would faithfully
610 663 // preserve the corrupted value forever (round-2 follow-up #1). When the stored value is
611 664 // itself mask-shaped it is unrecoverable — clear it so the UI and the GeoIP/GA4 status
612 665 // surfaces honestly report a missing key instead of failing downstream with 401s.
613 - $sensitive_api_keys = array( 'ai_autowrite_api_key', 'ai_chatbot_api_key', 'ga4_api_secret', 'maxmind_license_key', 'maxmind_account_id' );
666 + $sensitive_api_keys = self::sensitive_api_key_fields();
614 667 foreach ( $sensitive_api_keys as $api_key_field ) {
615 668 if ( ! isset( $settings[ $api_key_field ] ) ) {
616 669 continue;
617 670 }
@@ -636,9 +689,9 @@
636 689 array(
637 690 'tab' => 'tab-betterdocs-ai',
638 691 'context' => 'write_with_ai',
639 692 'token_key' => 'ai_autowrite_max_token',
640 - 'model_key' => 'write_with_ai_model',
693 + 'model_key' => 'ai_model',
641 694 'label' => __( 'Write with AI', 'betterdocs' )
642 695 ),
643 696 array(
644 697 'tab' => 'tab-betterdocs-ai',
@@ -643,9 +696,9 @@
643 696 array(
644 697 'tab' => 'tab-betterdocs-ai',
645 698 'context' => 'article_summary',
646 699 'token_key' => 'article_summary_max_token',
647 - 'model_key' => 'article_summary_model',
700 + 'model_key' => 'ai_model',
648 701 'label' => __( 'AI Doc Summarizer', 'betterdocs' )
649 702 )
650 703 );
651 704 foreach ( $token_pairs as $pair ) {
@@ -680,21 +733,38 @@
680 733 betterdocs()->kbmigration->migrate();
681 734 }
682 735 $_settings = wp_parse_args( $_normalized_settings, $_old_settings );
683 736
684 - // Check if there are actual changes before saving.
685 - // update_option returns false when values serialize identically, which can happen
686 - // due to object caching or type normalization even when user made changes.
687 - $_has_changes = $_settings != $_old_settings;
737 + // Detect whether this save actually changes the effective settings.
738 + //
739 + // The stored option and the submitted payload are normalized differently:
740 + // an optional field can be ABSENT from storage yet arrive as '' (e.g.
741 + // Feedback URL), and array fields can be stored empty ( [] ) while their
742 + // normalized/default form is non-empty (e.g. Instant Answer's
743 + // display_ia_texonomy defaults to ['all']). Comparing the raw arrays
744 + // ( $_settings != $_old_settings ) therefore reported a phantom change on
745 + // every save, leaving the tab perpetually "dirty" and always toasting
746 + // "Changes Saved Successfully." instead of "There are no changes to be
747 + // saved." — see WPDevelopers/betterdocs-pro#78.
748 + //
749 + // Compare like-for-like instead: fill defaults on both sides and run both
750 + // through the same normalization, so semantically-equal states (absent vs
751 + // '', [] vs ['all'], 'on' vs true) collapse to identical values and only a
752 + // real edit registers. This is also more reliable than update_option()'s
753 + // return, which is false whenever values serialize identically under object
754 + // caching or type coercion even when the user did change something (#49).
755 + $_defaults = array_merge( $this->get_default(), $this->get_pro_defaults() );
756 + $_old_normalized = $this->get_normalized_values( wp_parse_args( $_old_settings, $_defaults ), $_defaults );
757 + $_new_normalized = $this->get_normalized_values( wp_parse_args( $_settings, $_defaults ), $_defaults );
758 + $_has_changes = $_new_normalized != $_old_normalized;
688 759
689 760 $_saved = $this->database->save( $this->base_key, $_settings );
690 761
691 762 do_action_ref_array( 'betterdocs::settings::saved', array( $_saved, $_settings, $_old_settings, &$this ) );
692 763
693 - // Return true if save succeeded OR if there were changes to attempt saving.
694 - // This handles cases where update_option returns false due to identical serialization
695 - // (e.g., object caching, type coercion during serialization).
696 - return $_saved || $_has_changes;
764 + // The success / no-changes toast reflects whether the user made a real
765 + // change, not update_option()'s (unreliable) return value.
766 + return $_has_changes;
697 767 }
698 768
699 769 public function views( $hook ) {
700 770 return betterdocs()->views->get( 'admin/settings' );
@@ -1017,8 +1087,17 @@
1017 1087 'enable_disable_text_active' => true,
1018 1088 'default' => 1,
1019 1089 'priority' => 1
1020 1090 ),
1091 + 'archive_lazy_load_descendants' => array(
1092 + 'name' => 'archive_lazy_load_descendants',
1093 + 'type' => 'toggle',
1094 + 'label' => __( 'Lazy Load Sidebar Docs & Subcategories', 'betterdocs' ),
1095 + 'label_subtitle' => __( 'Applies to the Sidebar layout wherever it appears — single docs, category archives, and the Sidebar widget/block. Off by default: enable to render only the active category upfront and fetch the rest on click (or on scroll for the Memphis layout), cutting initial HTML size on large knowledge bases.', 'betterdocs' ),
1096 + 'enable_disable_text_active' => true,
1097 + 'default' => false,
1098 + 'priority' => 2
1099 + ),
1021 1100 'nested_subcategory' => array(
1022 1101 'name' => 'nested_subcategory',
1023 1102 'type' => 'toggle',
1024 1103 'label' => __( 'Nested Sub Category', 'betterdocs' ),
@@ -1023,9 +1102,9 @@
1023 1102 'type' => 'toggle',
1024 1103 'label' => __( 'Nested Sub Category', 'betterdocs' ),
1025 1104 'enable_disable_text_active' => true,
1026 1105 'default' => '',
1027 - 'priority' => 2
1106 + 'priority' => 3
1028 1107 ),
1029 1108 'column_number' => array(
1030 1109 'name' => 'column_number',
1031 1110 'type' => 'number',
@@ -1031,9 +1110,9 @@
1031 1110 'type' => 'number',
1032 1111 'label' => __( 'Number Of Columns', 'betterdocs' ),
1033 1112 'label_subtitle' => __( 'This setting is not applicable for sleek layout.', 'betterdocs' ),
1034 1113 'default' => 3,
1035 - 'priority' => 3
1114 + 'priority' => 4
1036 1115 ),
1037 1116 'posts_number' => apply_filters( 'betterdocs_posts_number', array(
1038 1117 'name' => 'posts_number',
1039 1118 'type' => 'number',
@@ -1039,9 +1118,9 @@
1039 1118 'type' => 'number',
1040 1119 'label' => __( 'Number Of Docs', 'betterdocs' ),
1041 1120 'label_subtitle' => __( 'This setting is not applicable for handbook layout.', 'betterdocs' ),
1042 1121 'default' => 10,
1043 - 'priority' => 4
1122 + 'priority' => 5
1044 1123 ) ),
1045 1124 'post_count' => array(
1046 1125 'name' => 'post_count',
1047 1126 'type' => 'toggle',
@@ -1047,9 +1126,9 @@
1047 1126 'type' => 'toggle',
1048 1127 'label' => __( 'Doc Count', 'betterdocs' ),
1049 1128 'enable_disable_text_active' => true,
1050 1129 'default' => 1,
1051 - 'priority' => 5
1130 + 'priority' => 6
1052 1131 ),
1053 1132 'count_text' => array(
1054 1133 'name' => 'count_text',
1055 1134 'type' => 'text',
@@ -1054,9 +1133,9 @@
1054 1133 'name' => 'count_text',
1055 1134 'type' => 'text',
1056 1135 'label' => __( 'Count Text', 'betterdocs' ),
1057 1136 'default' => __( 'Docs', 'betterdocs' ),
1058 - 'priority' => 6
1137 + 'priority' => 7
1059 1138 ),
1060 1139 'count_text_singular' => array(
1061 1140 'name' => 'count_text_singular',
1062 1141 'type' => 'text',
@@ -1061,9 +1140,9 @@
1061 1140 'name' => 'count_text_singular',
1062 1141 'type' => 'text',
1063 1142 'label' => __( 'Count Text Singular', 'betterdocs' ),
1064 1143 'default' => __( 'Doc', 'betterdocs' ),
1065 - 'priority' => 7
1144 + 'priority' => 8
1066 1145 ),
1067 1146 'exploremore_btn' => array(
1068 1147 'name' => 'exploremore_btn',
1069 1148 'type' => 'toggle',
@@ -1069,9 +1148,9 @@
1069 1148 'type' => 'toggle',
1070 1149 'label' => __( 'Explore More Button', 'betterdocs' ),
1071 1150 'enable_disable_text_active' => true,
1072 1151 'default' => true,
1073 - 'priority' => 8
1152 + 'priority' => 9
1074 1153 ),
1075 1154 'exploremore_btn_txt' => array(
1076 1155 'name' => 'exploremore_btn_txt',
1077 1156 'type' => 'text',
@@ -1076,9 +1155,9 @@
1076 1155 'name' => 'exploremore_btn_txt',
1077 1156 'type' => 'text',
1078 1157 'label' => __( 'Explore More Button Text', 'betterdocs' ),
1079 1158 'default' => __( 'Explore More', 'betterdocs' ),
1080 - 'priority' => 9,
1159 + 'priority' => 10,
1081 1160 'rules' => Rules::is( 'exploremore_btn', true )
1082 1161 ),
1083 1162 'betterdocs_popular_docs_text' => array(
1084 1163 'name' => 'betterdocs_popular_docs_text',
@@ -1084,9 +1163,9 @@
1084 1163 'name' => 'betterdocs_popular_docs_text',
1085 1164 'type' => 'text',
1086 1165 'label' => __( 'Popular Docs Text', 'betterdocs' ),
1087 1166 'default' => __( 'Popular Docs', 'betterdocs' ),
1088 - 'priority' => 10,
1167 + 'priority' => 11,
1089 1168 'is_pro' => true
1090 1169 ),
1091 1170 'betterdocs_popular_docs_number' => array(
1092 1171 'name' => 'betterdocs_popular_docs_number',
@@ -1092,9 +1171,9 @@
1092 1171 'name' => 'betterdocs_popular_docs_number',
1093 1172 'type' => 'number',
1094 1173 'label' => __( 'Popular Docs Number', 'betterdocs' ),
1095 1174 'default' => 10,
1096 - 'priority' => 11,
1175 + 'priority' => 12,
1097 1176 'is_pro' => true
1098 1177 )
1099 1178 )
1100 1179 ),
@@ -1338,8 +1417,44 @@
1338 1417 'enable_disable_text_active' => true,
1339 1418 'default' => 1,
1340 1419 'priority' => 3
1341 1420 ),
1421 + 'print_enable_logo' => array(
1422 + 'name' => 'print_enable_logo',
1423 + 'type' => 'toggle',
1424 + 'label' => __( 'Logo on Printed Doc', 'betterdocs' ),
1425 + 'label_subtitle' => __( 'Show a logo at the top of the printed / PDF page', 'betterdocs' ),
1426 + 'enable_disable_text_active' => true,
1427 + 'default' => 0,
1428 + 'priority' => 4
1429 + ),
1430 + 'print_logo' => array(
1431 + 'name' => 'print_logo',
1432 + 'type' => 'media',
1433 + 'value' => '',
1434 + 'label' => __( 'Print Logo', 'betterdocs' ),
1435 + 'label_subtitle' => __( 'Leave empty to use your site logo, or the site icon when no site logo is set', 'betterdocs' ),
1436 + 'priority' => 5,
1437 + 'rules' => Rules::is( 'print_enable_logo', true )
1438 + ),
1439 + 'print_enable_footer' => array(
1440 + 'name' => 'print_enable_footer',
1441 + 'type' => 'toggle',
1442 + 'label' => __( 'Footer on Printed Doc', 'betterdocs' ),
1443 + 'label_subtitle' => __( 'Show a footer on every page of the printed / PDF document', 'betterdocs' ),
1444 + 'enable_disable_text_active' => true,
1445 + 'default' => 0,
1446 + 'priority' => 6
1447 + ),
1448 + 'print_footer_text' => array(
1449 + 'name' => 'print_footer_text',
1450 + 'type' => 'textarea',
1451 + 'label' => __( 'Print Footer Text', 'betterdocs' ),
1452 + 'label_subtitle' => __( 'Leave empty to use the site name and current year', 'betterdocs' ),
1453 + 'default' => '',
1454 + 'priority' => 7,
1455 + 'rules' => Rules::is( 'print_enable_footer', true )
1456 + ),
1342 1457 'enable_tags' => array(
1343 1458 'name' => 'enable_tags',
1344 1459 'type' => 'toggle',
1345 1460 'label' => __( 'Tags', 'betterdocs' ),
@@ -1344,9 +1459,9 @@
1344 1459 'type' => 'toggle',
1345 1460 'label' => __( 'Tags', 'betterdocs' ),
1346 1461 'enable_disable_text_active' => true,
1347 1462 'default' => 1,
1348 - 'priority' => 4
1463 + 'priority' => 8
1349 1464 ),
1350 1465 'show_last_update_time' => array(
1351 1466 'name' => 'show_last_update_time',
1352 1467 'type' => 'toggle',
@@ -1352,9 +1467,9 @@
1352 1467 'type' => 'toggle',
1353 1468 'label' => __( 'Last Update Time', 'betterdocs' ),
1354 1469 'enable_disable_text_active' => true,
1355 1470 'default' => 1,
1356 - 'priority' => 5
1471 + 'priority' => 9
1357 1472 ),
1358 1473 'enable_navigation' => array(
1359 1474 'name' => 'enable_navigation',
1360 1475 'type' => 'toggle',
@@ -1360,9 +1475,9 @@
1360 1475 'type' => 'toggle',
1361 1476 'label' => __( 'Navigation', 'betterdocs' ),
1362 1477 'enable_disable_text_active' => true,
1363 1478 'default' => 1,
1364 - 'priority' => 6
1479 + 'priority' => 10
1365 1480 ),
1366 1481 'enable_comment' => array(
1367 1482 'name' => 'enable_comment',
1368 1483 'type' => 'toggle',
@@ -1368,9 +1483,9 @@
1368 1483 'type' => 'toggle',
1369 1484 'label' => __( 'Comment', 'betterdocs' ),
1370 1485 'enable_disable_text_active' => true,
1371 1486 'default' => '',
1372 - 'priority' => 7
1487 + 'priority' => 11
1373 1488 ),
1374 1489 'enable_credit' => array(
1375 1490 'name' => 'enable_credit',
1376 1491 'type' => 'toggle',
@@ -1376,9 +1491,9 @@
1376 1491 'type' => 'toggle',
1377 1492 'label' => __( 'Show Powered by BetterDocs', 'betterdocs' ),
1378 1493 'enable_disable_text_active' => true,
1379 1494 'default' => '',
1380 - 'priority' => 8
1495 + 'priority' => 12
1381 1496 ),
1382 1497 'reaction_feedback_text' => array(
1383 1498 'name' => 'reaction_feedback_text',
1384 1499 'type' => 'text',
@@ -1383,9 +1498,9 @@
1383 1498 'name' => 'reaction_feedback_text',
1384 1499 'type' => 'text',
1385 1500 'label' => __( 'Reaction Feedback Text', 'betterdocs' ),
1386 1501 'default' => __( 'Thanks for your feedback.', 'betterdocs' ),
1387 - 'priority' => 9
1502 + 'priority' => 13
1388 1503 ),
1389 1504 'enable_estimated_reading_time' => array(
1390 1505 'name' => 'enable_estimated_reading_time',
1391 1506 'type' => 'toggle',
@@ -1391,9 +1506,9 @@
1391 1506 'type' => 'toggle',
1392 1507 'label' => __( 'Estimated Reading Time', 'betterdocs' ),
1393 1508 'enable_disable_text_active' => true,
1394 1509 'default' => 0,
1395 - 'priority' => 10
1510 + 'priority' => 14
1396 1511 ),
1397 1512 'estimated_reading_time_title' => array(
1398 1513 'name' => 'estimated_reading_time_title',
1399 1514 'type' => 'text',
@@ -1398,9 +1513,9 @@
1398 1513 'name' => 'estimated_reading_time_title',
1399 1514 'type' => 'text',
1400 1515 'label' => __( 'Estimated Reading Time Title', 'betterdocs' ),
1401 1516 'default' => '',
1402 - 'priority' => 11,
1517 + 'priority' => 15,
1403 1518 'rules' => Rules::is( 'enable_estimated_reading_time', true )
1404 1519 ),
1405 1520 'estimated_reading_time_text' => array(
1406 1521 'name' => 'estimated_reading_time_text',
@@ -1406,9 +1521,9 @@
1406 1521 'name' => 'estimated_reading_time_text',
1407 1522 'type' => 'text',
1408 1523 'label' => __( 'Estimated Reading Time Text', 'betterdocs' ),
1409 1524 'default' => __( 'min read', 'betterdocs' ),
1410 - 'priority' => 12,
1525 + 'priority' => 16,
1411 1526 'rules' => Rules::is( 'enable_estimated_reading_time', true )
1412 1527 ),
1413 1528 'singular_estimated_reading_time_text' => array(
1414 1529 'name' => 'singular_estimated_reading_time_text',
@@ -1414,9 +1529,9 @@
1414 1529 'name' => 'singular_estimated_reading_time_text',
1415 1530 'type' => 'text',
1416 1531 'label' => __( 'Estimated Reading Time Text Singular', 'betterdocs' ),
1417 1532 'default' => __( 'min read', 'betterdocs' ),
1418 - 'priority' => 13,
1533 + 'priority' => 17,
1419 1534 'rules' => Rules::is( 'enable_estimated_reading_time', true )
1420 1535 )
1421 1536 )
1422 1537 ),
@@ -2296,18 +2411,84 @@
2296 2411 'open-ai-settings' => array(
2297 2412 'id' => 'open-ai-settings',
2298 2413 'name' => 'open-ai-settings',
2299 2414 'type' => 'section',
2300 - 'label' => __( 'API Settings', 'betterdocs' ),
2415 + 'label' => __( 'AI Platform & API', 'betterdocs' ),
2301 2416 'priority' => 1,
2302 2417 'fields' => array(
2418 + 'ai_platform' => array(
2419 + 'name' => 'ai_platform',
2420 + 'type' => 'select',
2421 + 'label' => __( 'AI Platform', 'betterdocs' ),
2422 + 'label_subtitle' => __( 'Choose which AI platform powers Write with AI, AI Edit, the Doc Summarizer and Quality Score. The model list and API key field below adapt to your choice.', 'betterdocs' ),
2423 + 'priority' => 1,
2424 + 'multiple' => false,
2425 + 'default' => 'openai',
2426 + // DeepSeek and OpenRouter are temporarily hidden from the
2427 + // dropdown. Filter the options here, NOT ModelRegistry::platforms() —
2428 + // sensitive_api_key_fields() loops the registry to mask each
2429 + // platform's key field, so trimming the registry would silently
2430 + // un-mask those keys. Re-enable later by dropping the array_diff_key.
2431 + 'options' => GlobalFields::normalize_fields(
2432 + array_diff_key( ModelRegistry::platforms(), array_flip( array( 'deepseek', 'openrouter' ) ) )
2433 + )
2434 + ),
2435 + // OpenAI's key field keeps its original name so installs
2436 + // that already saved a Write with AI key keep it.
2303 2437 'ai_autowrite_api_key' => array(
2304 2438 'name' => 'ai_autowrite_api_key',
2305 2439 'type' => 'text',
2306 - 'label' => __( 'API Key', 'betterdocs' ),
2307 - 'label_subtitle' => sprintf( /* translators: %s is a link to the documentation about generating an OpenAI API key. */__( 'Check out this <a target="_blank" href="%s">documentation</a> to find out how to generate your OpenAI API Key.', 'betterdocs' ), esc_url( 'https://betterdocs.co/docs/write-with-ai/' ) ),
2440 + 'label' => __( 'OpenAI API Key', 'betterdocs' ),
2441 + 'label_subtitle' => sprintf( /* translators: %s: documentation URL */ __( 'Check out this <a target="_blank" href="%s">documentation</a> to generate your OpenAI API key.', 'betterdocs' ), esc_url( 'https://betterdocs.co/docs/write-with-ai/' ) ),
2308 2442 'default' => '',
2309 - 'priority' => 1
2443 + 'priority' => 2,
2444 + 'rules' => Rules::is( 'ai_platform', 'openai' )
2445 + ),
2446 + 'ai_api_key_gemini' => array(
2447 + 'name' => 'ai_api_key_gemini',
2448 + 'type' => 'text',
2449 + 'label' => __( 'Google Gemini API Key', 'betterdocs' ),
2450 + 'label_subtitle' => sprintf( /* translators: %s: Google AI Studio URL */ __( 'Generate a key from <a target="_blank" href="%s">Google AI Studio</a>.', 'betterdocs' ), esc_url( 'https://aistudio.google.com/app/apikey' ) ),
2451 + 'default' => '',
2452 + 'priority' => 2,
2453 + 'rules' => Rules::is( 'ai_platform', 'gemini' )
2454 + ),
2455 + 'ai_api_key_claude' => array(
2456 + 'name' => 'ai_api_key_claude',
2457 + 'type' => 'text',
2458 + 'label' => __( 'Anthropic Claude API Key', 'betterdocs' ),
2459 + 'label_subtitle' => sprintf( /* translators: %s: Anthropic Console URL */ __( 'Generate a key from the <a target="_blank" href="%s">Anthropic Console</a>.', 'betterdocs' ), esc_url( 'https://console.anthropic.com/settings/keys' ) ),
2460 + 'default' => '',
2461 + 'priority' => 2,
2462 + 'rules' => Rules::is( 'ai_platform', 'claude' )
2463 + ),
2464 + 'ai_api_key_deepseek' => array(
2465 + 'name' => 'ai_api_key_deepseek',
2466 + 'type' => 'text',
2467 + 'label' => __( 'DeepSeek API Key', 'betterdocs' ),
2468 + 'label_subtitle' => sprintf( /* translators: %s: DeepSeek platform URL */ __( 'Generate a key from the <a target="_blank" href="%s">DeepSeek platform</a>.', 'betterdocs' ), esc_url( 'https://platform.deepseek.com/api_keys' ) ),
2469 + 'default' => '',
2470 + 'priority' => 2,
2471 + 'rules' => Rules::is( 'ai_platform', 'deepseek' )
2472 + ),
2473 + 'ai_api_key_openrouter' => array(
2474 + 'name' => 'ai_api_key_openrouter',
2475 + 'type' => 'text',
2476 + 'label' => __( 'OpenRouter API Key', 'betterdocs' ),
2477 + 'label_subtitle' => sprintf( /* translators: %s: OpenRouter keys URL */ __( 'Generate a key from <a target="_blank" href="%s">OpenRouter</a>.', 'betterdocs' ), esc_url( 'https://openrouter.ai/keys' ) ),
2478 + 'default' => '',
2479 + 'priority' => 2,
2480 + 'rules' => Rules::is( 'ai_platform', 'openrouter' )
2481 + ),
2482 + 'ai_model' => array(
2483 + 'name' => 'ai_model',
2484 + 'type' => 'platform_model_select',
2485 + 'label' => __( 'Model', 'betterdocs' ),
2486 + 'label_subtitle' => __( 'Available models depend on the selected platform.', 'betterdocs' ),
2487 + 'priority' => 10,
2488 + 'default' => 'gpt-4o-mini',
2489 + 'catalogue' => ModelRegistry::catalogue(),
2490 + 'defaults' => ModelRegistry::defaults()
2310 2491 )
2311 2492 )
2312 2493 ),
2313 2494 'write-with-ai' => array(
@@ -2372,9 +2553,10 @@
2372 2553 'priority' => 6,
2373 2554 'label' => __( 'Write Glossaries with AI', 'betterdocs' ),
2374 2555 'label_subtitle' => __( 'Generate AI based Glossary definitions from the Glossaries admin page', 'betterdocs' ),
2375 2556 'enable_disable_text_active' => true,
2376 - 'default' => true
2557 + 'default' => true,
2558 + 'is_pro' => true
2377 2559 ),
2378 2560 'enable_docs_ai_suite' => array(
2379 2561 'name' => 'enable_docs_ai_suite',
2380 2562 'type' => 'toggle',
@@ -2383,27 +2565,8 @@
2383 2565 'label_subtitle' => __( 'Add BetterDocs AI actions for suggesting categories, tags and excerpts inside the native Docs editor panels.', 'betterdocs' ),
2384 2566 'enable_disable_text_active' => true,
2385 2567 'default' => true
2386 2568 ),
2387 - 'write_with_ai_model' => array(
2388 - 'name' => 'write_with_ai_model',
2389 - 'type' => 'select',
2390 - 'label' => __( 'OpenAI Model*', 'betterdocs' ),
2391 - 'priority' => 20,
2392 - 'multiple' => false,
2393 - 'default' => 'gpt-4o-mini',
2394 - 'options' => GlobalFields::normalize_fields( array(
2395 - 'gpt-4o-mini' => 'GPT-4o Mini',
2396 - 'gpt-4o' => 'GPT-4o',
2397 - 'gpt-4.1-nano' => 'GPT-4.1 Nano',
2398 - 'gpt-4.1-mini' => 'GPT-4.1 Mini',
2399 - 'gpt-4.1' => 'GPT-4.1',
2400 - 'gpt-5-nano' => 'GPT-5 Nano',
2401 - 'gpt-5-mini' => 'GPT-5 Mini',
2402 - 'gpt-5' => 'GPT-5',
2403 - 'gpt-5.5' => 'GPT-5.5'
2404 - ) )
2405 - ),
2406 2569 'ai_autowrite_max_token' => array(
2407 2570 'name' => 'ai_autowrite_max_token',
2408 2571 'type' => 'min_token_number',
2409 2572 'label' => __( 'Set Max Tokens', 'betterdocs' ),
@@ -2410,9 +2573,9 @@
2410 2573 'label_subtitle' => sprintf( // translators: %s is a link to more information about token limits.
2411 2574 __( 'Documentation will be generated based on the Token Limits you have set. For more information on Token Limits, you can check out this <a target="_blank" href="%s">link</a>.', 'betterdocs' ), esc_url( 'https://platform.openai.com/account/limits' ) ),
2412 2575 'default' => 2500,
2413 2576 'priority' => 10,
2414 - 'model_field' => 'write_with_ai_model',
2577 + 'model_field' => 'ai_model',
2415 2578 'context' => 'write_with_ai',
2416 2579 'min_token_map' => AIHelper::get_min_tokens_map( 'write_with_ai' ),
2417 2580 'classes' => 'wprf-type-text'
2418 2581 )
@@ -2485,31 +2648,12 @@
2485 2648 'label_subtitle' => sprintf( // translators: %s is a link to more information about token limits.
2486 2649 __( 'Single Doc summarizer will be generated based on the token limits you have set. For more information on Token Limits, you can check out this <a target="_blank" href="%s">link</a>.', 'betterdocs' ), esc_url( 'https://platform.openai.com/account/limits' ) ),
2487 2650 'default' => 1500,
2488 2651 'priority' => 5,
2489 - 'model_field' => 'article_summary_model',
2652 + 'model_field' => 'ai_model',
2490 2653 'context' => 'article_summary',
2491 2654 'min_token_map' => AIHelper::get_min_tokens_map( 'article_summary' ),
2492 2655 'classes' => 'wprf-type-text'
2493 - ),
2494 - 'article_summary_model' => array(
2495 - 'name' => 'article_summary_model',
2496 - 'type' => 'select',
2497 - 'label' => __( 'OpenAI Model*', 'betterdocs' ),
2498 - 'priority' => 10,
2499 - 'multiple' => false,
2500 - 'default' => 'gpt-4o-mini',
2501 - 'options' => GlobalFields::normalize_fields( array(
2502 - 'gpt-4o-mini' => 'GPT-4o Mini',
2503 - 'gpt-4o' => 'GPT-4o',
2504 - 'gpt-4.1-nano' => 'GPT-4.1 Nano',
2505 - 'gpt-4.1-mini' => 'GPT-4.1 Mini',
2506 - 'gpt-4.1' => 'GPT-4.1',
2507 - 'gpt-5-nano' => 'GPT-5 Nano',
2508 - 'gpt-5-mini' => 'GPT-5 Mini',
2509 - 'gpt-5' => 'GPT-5',
2510 - 'gpt-5.5' => 'GPT-5.5'
2511 - ) )
2512 2656 )
2513 2657 )
2514 2658 )
2515 2659 )