PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.2
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.2
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 3.5.1 All 200 releases
← All changes | includes/Core/Settings.php +304 -75 4.6.14.9.2 View file →
@@ -9,8 +9,11 @@
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;
15 +use WPDeveloper\BetterDocs\REST\AIEdit;
13 16 use WPDeveloper\BetterDocs\Utils\AIHelper;
14 17 use WPDeveloper\BetterDocs\Utils\Base;
15 18 use WPDeveloper\BetterDocs\Utils\Database;
16 19 use WPDeveloper\BetterDocs\Utils\Helper;
@@ -76,8 +79,31 @@
76 79 return esc_url( admin_url( 'admin.php?page=betterdocs-settings' ) );
77 80 }
78 81
79 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 + /**
80 106 * This method is responsible for enqueueing scripts in settings panel
81 107 *
82 108 * @param string $hook
83 109 *
@@ -94,9 +120,9 @@
94 120
95 121 $settings = GlobalFields::normalize( $this->settings_args() );
96 122
97 123 // Mask sensitive API keys before they reach the browser. Non-admins still get them stripped entirely below.
98 - $sensitive_api_keys = array( 'ai_autowrite_api_key', 'ai_chatbot_api_key' );
124 + $sensitive_api_keys = self::sensitive_api_key_fields();
99 125 foreach ( $sensitive_api_keys as $api_key_field ) {
100 126 if ( ! empty( $settings[ 'values' ][ $api_key_field ] ) ) {
101 127 $settings[ 'values' ][ $api_key_field ] = Helper::mask_api_key( $settings[ 'values' ][ $api_key_field ] );
102 128 }
@@ -257,8 +283,12 @@
257 283 'enable_breadcrumb_category' => true,
258 284 'enable_breadcrumb_title' => true,
259 285 'enable_sidebar_cat_list' => true,
260 286 'enable_print_icon' => true,
287 + 'print_enable_logo' => false,
288 + 'print_logo' => array(),
289 + 'print_enable_footer' => false,
290 + 'print_footer_text' => '',
261 291 'enable_tags' => true,
262 292 'email_feedback' => true,
263 293 'feedback_link_text' => __( 'Still stuck? How can we help?', 'betterdocs' ),
264 294 'reaction_feedback_text' => __( 'Thanks for your feedback', 'betterdocs' ),
@@ -271,8 +301,9 @@
271 301 'enable_credit' => false,
272 302 'enable_archive_sidebar' => true,
273 303 'archive_nested_subcategory' => true,
274 304 'archive_enable_pagination' => false,
305 + 'archive_lazy_load_descendants' => false,
275 306 'enable_content_restriction' => false,
276 307 'enable_reporting' => false,
277 308 'enable_sample_data' => false,
278 309 'reporting_day' => 'monday',
@@ -279,14 +310,34 @@
279 310 'reporting_email' => get_option( 'admin_email' ),
280 311 'enable_write_with_ai' => true,
281 312 'enable_faq_write_with_ai' => true,
282 313 'enable_glossaries_write_with_ai' => true,
314 + 'enable_docs_ai_suite' => true,
283 315 'write_with_ai_model' => 'gpt-4o-mini',
284 316 'ai_autowrite_api_key' => '',
285 317 'ai_autowrite_max_token' => 2500,
318 + 'write_with_ai_instructions' => array(
319 + array(
320 + 'id' => 'default',
321 + 'title' => __( 'Default/Core', 'betterdocs' ),
322 + 'content' => WriteWithAI::default_instruction_content()
323 + )
324 + ),
325 + 'ai_edit_actions' => AIEdit::default_actions(),
286 326 'enable_article_summary' => false,
287 327 'article_summary_model' => 'gpt-4o-mini',
288 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' => '',
289 340 'enable_estimated_reading_time' => true,
290 341 'enable_encyclopedia' => false,
291 342 'enable_glossaries' => false,
292 343 'show_glossary_suggestions' => true,
@@ -296,9 +347,21 @@
296 347 'singular_estimated_reading_time_text' => __( 'min read', 'betterdocs' ),
297 348 'betterdocs_access_control_repeater' => array(),
298 349 'internal_knowledge_base_type' => 'basic',
299 350 'betterdocs_access_control_repeater_kb' => array(),
300 - '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' => ''
301 364 );
302 365
303 366 $_default = apply_filters( 'betterdocs_default_settings', $_default );
304 367 // $_default = apply_filters_deprecated(
@@ -591,19 +654,27 @@
591 654 }
592 655
593 656 $_old_settings = $this->database->get( $this->base_key, $this->get_default() );
594 657
595 - // The frontend only ever sees masked API keys. If a submitted value matches the mask of
596 - // the stored value, the user did not change it — restore the original so the mask string
597 - // is never persisted.
598 - $sensitive_api_keys = array( 'ai_autowrite_api_key', 'ai_chatbot_api_key' );
658 + // The frontend only ever sees masked API keys. A submitted value that still looks like a
659 + // mask (contains a run of asterisks — no real key does) means "unchanged": restore the
660 + // stored original so a mask string is never persisted. Guarding on the mask SHAPE rather
661 + // than strict equality with mask(stored) closes the corruption loop QA hit: once a mask
662 + // slips into storage, mask(mask) === mask, so an equality-only guard would faithfully
663 + // preserve the corrupted value forever (round-2 follow-up #1). When the stored value is
664 + // itself mask-shaped it is unrecoverable — clear it so the UI and the GeoIP/GA4 status
665 + // surfaces honestly report a missing key instead of failing downstream with 401s.
666 + $sensitive_api_keys = self::sensitive_api_key_fields();
599 667 foreach ( $sensitive_api_keys as $api_key_field ) {
600 668 if ( ! isset( $settings[ $api_key_field ] ) ) {
601 669 continue;
602 670 }
603 - $stored = isset( $_old_settings[ $api_key_field ] ) ? $_old_settings[ $api_key_field ] : '';
604 - if ( '' !== $stored && trim( (string) $settings[ $api_key_field ] ) === Helper::mask_api_key( $stored ) ) {
605 - $settings[ $api_key_field ] = $stored;
671 + $incoming = trim( (string) $settings[ $api_key_field ] );
672 + $stored = isset( $_old_settings[ $api_key_field ] ) ? (string) $_old_settings[ $api_key_field ] : '';
673 + $stored_is_mask = '' !== $stored && preg_match( '/\*{4,}/', $stored );
674 +
675 + if ( '' !== $incoming && preg_match( '/\*{4,}/', $incoming ) ) {
676 + $settings[ $api_key_field ] = $stored_is_mask ? '' : $stored;
606 677 }
607 678 }
608 679
609 680 // Minimum-token policy: reject saves where a feature's max_token field is below the
@@ -618,9 +689,9 @@
618 689 array(
619 690 'tab' => 'tab-betterdocs-ai',
620 691 'context' => 'write_with_ai',
621 692 'token_key' => 'ai_autowrite_max_token',
622 - 'model_key' => 'write_with_ai_model',
693 + 'model_key' => 'ai_model',
623 694 'label' => __( 'Write with AI', 'betterdocs' )
624 695 ),
625 696 array(
626 697 'tab' => 'tab-betterdocs-ai',
@@ -625,9 +696,9 @@
625 696 array(
626 697 'tab' => 'tab-betterdocs-ai',
627 698 'context' => 'article_summary',
628 699 'token_key' => 'article_summary_max_token',
629 - 'model_key' => 'article_summary_model',
700 + 'model_key' => 'ai_model',
630 701 'label' => __( 'AI Doc Summarizer', 'betterdocs' )
631 702 )
632 703 );
633 704 foreach ( $token_pairs as $pair ) {
@@ -999,8 +1070,17 @@
999 1070 'enable_disable_text_active' => true,
1000 1071 'default' => 1,
1001 1072 'priority' => 1
1002 1073 ),
1074 + 'archive_lazy_load_descendants' => array(
1075 + 'name' => 'archive_lazy_load_descendants',
1076 + 'type' => 'toggle',
1077 + 'label' => __( 'Lazy Load Sidebar Docs & Subcategories', 'betterdocs' ),
1078 + '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' ),
1079 + 'enable_disable_text_active' => true,
1080 + 'default' => false,
1081 + 'priority' => 2
1082 + ),
1003 1083 'nested_subcategory' => array(
1004 1084 'name' => 'nested_subcategory',
1005 1085 'type' => 'toggle',
1006 1086 'label' => __( 'Nested Sub Category', 'betterdocs' ),
@@ -1005,9 +1085,9 @@
1005 1085 'type' => 'toggle',
1006 1086 'label' => __( 'Nested Sub Category', 'betterdocs' ),
1007 1087 'enable_disable_text_active' => true,
1008 1088 'default' => '',
1009 - 'priority' => 2
1089 + 'priority' => 3
1010 1090 ),
1011 1091 'column_number' => array(
1012 1092 'name' => 'column_number',
1013 1093 'type' => 'number',
@@ -1013,9 +1093,9 @@
1013 1093 'type' => 'number',
1014 1094 'label' => __( 'Number Of Columns', 'betterdocs' ),
1015 1095 'label_subtitle' => __( 'This setting is not applicable for sleek layout.', 'betterdocs' ),
1016 1096 'default' => 3,
1017 - 'priority' => 3
1097 + 'priority' => 4
1018 1098 ),
1019 1099 'posts_number' => apply_filters( 'betterdocs_posts_number', array(
1020 1100 'name' => 'posts_number',
1021 1101 'type' => 'number',
@@ -1021,9 +1101,9 @@
1021 1101 'type' => 'number',
1022 1102 'label' => __( 'Number Of Docs', 'betterdocs' ),
1023 1103 'label_subtitle' => __( 'This setting is not applicable for handbook layout.', 'betterdocs' ),
1024 1104 'default' => 10,
1025 - 'priority' => 4
1105 + 'priority' => 5
1026 1106 ) ),
1027 1107 'post_count' => array(
1028 1108 'name' => 'post_count',
1029 1109 'type' => 'toggle',
@@ -1029,9 +1109,9 @@
1029 1109 'type' => 'toggle',
1030 1110 'label' => __( 'Doc Count', 'betterdocs' ),
1031 1111 'enable_disable_text_active' => true,
1032 1112 'default' => 1,
1033 - 'priority' => 5
1113 + 'priority' => 6
1034 1114 ),
1035 1115 'count_text' => array(
1036 1116 'name' => 'count_text',
1037 1117 'type' => 'text',
@@ -1036,9 +1116,9 @@
1036 1116 'name' => 'count_text',
1037 1117 'type' => 'text',
1038 1118 'label' => __( 'Count Text', 'betterdocs' ),
1039 1119 'default' => __( 'Docs', 'betterdocs' ),
1040 - 'priority' => 6
1120 + 'priority' => 7
1041 1121 ),
1042 1122 'count_text_singular' => array(
1043 1123 'name' => 'count_text_singular',
1044 1124 'type' => 'text',
@@ -1043,9 +1123,9 @@
1043 1123 'name' => 'count_text_singular',
1044 1124 'type' => 'text',
1045 1125 'label' => __( 'Count Text Singular', 'betterdocs' ),
1046 1126 'default' => __( 'Doc', 'betterdocs' ),
1047 - 'priority' => 7
1127 + 'priority' => 8
1048 1128 ),
1049 1129 'exploremore_btn' => array(
1050 1130 'name' => 'exploremore_btn',
1051 1131 'type' => 'toggle',
@@ -1051,9 +1131,9 @@
1051 1131 'type' => 'toggle',
1052 1132 'label' => __( 'Explore More Button', 'betterdocs' ),
1053 1133 'enable_disable_text_active' => true,
1054 1134 'default' => true,
1055 - 'priority' => 8
1135 + 'priority' => 9
1056 1136 ),
1057 1137 'exploremore_btn_txt' => array(
1058 1138 'name' => 'exploremore_btn_txt',
1059 1139 'type' => 'text',
@@ -1058,9 +1138,9 @@
1058 1138 'name' => 'exploremore_btn_txt',
1059 1139 'type' => 'text',
1060 1140 'label' => __( 'Explore More Button Text', 'betterdocs' ),
1061 1141 'default' => __( 'Explore More', 'betterdocs' ),
1062 - 'priority' => 9,
1142 + 'priority' => 10,
1063 1143 'rules' => Rules::is( 'exploremore_btn', true )
1064 1144 ),
1065 1145 'betterdocs_popular_docs_text' => array(
1066 1146 'name' => 'betterdocs_popular_docs_text',
@@ -1066,9 +1146,9 @@
1066 1146 'name' => 'betterdocs_popular_docs_text',
1067 1147 'type' => 'text',
1068 1148 'label' => __( 'Popular Docs Text', 'betterdocs' ),
1069 1149 'default' => __( 'Popular Docs', 'betterdocs' ),
1070 - 'priority' => 10,
1150 + 'priority' => 11,
1071 1151 'is_pro' => true
1072 1152 ),
1073 1153 'betterdocs_popular_docs_number' => array(
1074 1154 'name' => 'betterdocs_popular_docs_number',
@@ -1074,9 +1154,9 @@
1074 1154 'name' => 'betterdocs_popular_docs_number',
1075 1155 'type' => 'number',
1076 1156 'label' => __( 'Popular Docs Number', 'betterdocs' ),
1077 1157 'default' => 10,
1078 - 'priority' => 11,
1158 + 'priority' => 12,
1079 1159 'is_pro' => true
1080 1160 )
1081 1161 )
1082 1162 ),
@@ -1320,8 +1400,44 @@
1320 1400 'enable_disable_text_active' => true,
1321 1401 'default' => 1,
1322 1402 'priority' => 3
1323 1403 ),
1404 + 'print_enable_logo' => array(
1405 + 'name' => 'print_enable_logo',
1406 + 'type' => 'toggle',
1407 + 'label' => __( 'Logo on Printed Doc', 'betterdocs' ),
1408 + 'label_subtitle' => __( 'Show a logo at the top of the printed / PDF page', 'betterdocs' ),
1409 + 'enable_disable_text_active' => true,
1410 + 'default' => 0,
1411 + 'priority' => 4
1412 + ),
1413 + 'print_logo' => array(
1414 + 'name' => 'print_logo',
1415 + 'type' => 'media',
1416 + 'value' => '',
1417 + 'label' => __( 'Print Logo', 'betterdocs' ),
1418 + 'label_subtitle' => __( 'Leave empty to use your site logo, or the site icon when no site logo is set', 'betterdocs' ),
1419 + 'priority' => 5,
1420 + 'rules' => Rules::is( 'print_enable_logo', true )
1421 + ),
1422 + 'print_enable_footer' => array(
1423 + 'name' => 'print_enable_footer',
1424 + 'type' => 'toggle',
1425 + 'label' => __( 'Footer on Printed Doc', 'betterdocs' ),
1426 + 'label_subtitle' => __( 'Show a footer on every page of the printed / PDF document', 'betterdocs' ),
1427 + 'enable_disable_text_active' => true,
1428 + 'default' => 0,
1429 + 'priority' => 6
1430 + ),
1431 + 'print_footer_text' => array(
1432 + 'name' => 'print_footer_text',
1433 + 'type' => 'textarea',
1434 + 'label' => __( 'Print Footer Text', 'betterdocs' ),
1435 + 'label_subtitle' => __( 'Leave empty to use the site name and current year', 'betterdocs' ),
1436 + 'default' => '',
1437 + 'priority' => 7,
1438 + 'rules' => Rules::is( 'print_enable_footer', true )
1439 + ),
1324 1440 'enable_tags' => array(
1325 1441 'name' => 'enable_tags',
1326 1442 'type' => 'toggle',
1327 1443 'label' => __( 'Tags', 'betterdocs' ),
@@ -1326,9 +1442,9 @@
1326 1442 'type' => 'toggle',
1327 1443 'label' => __( 'Tags', 'betterdocs' ),
1328 1444 'enable_disable_text_active' => true,
1329 1445 'default' => 1,
1330 - 'priority' => 4
1446 + 'priority' => 8
1331 1447 ),
1332 1448 'show_last_update_time' => array(
1333 1449 'name' => 'show_last_update_time',
1334 1450 'type' => 'toggle',
@@ -1334,9 +1450,9 @@
1334 1450 'type' => 'toggle',
1335 1451 'label' => __( 'Last Update Time', 'betterdocs' ),
1336 1452 'enable_disable_text_active' => true,
1337 1453 'default' => 1,
1338 - 'priority' => 5
1454 + 'priority' => 9
1339 1455 ),
1340 1456 'enable_navigation' => array(
1341 1457 'name' => 'enable_navigation',
1342 1458 'type' => 'toggle',
@@ -1342,9 +1458,9 @@
1342 1458 'type' => 'toggle',
1343 1459 'label' => __( 'Navigation', 'betterdocs' ),
1344 1460 'enable_disable_text_active' => true,
1345 1461 'default' => 1,
1346 - 'priority' => 6
1462 + 'priority' => 10
1347 1463 ),
1348 1464 'enable_comment' => array(
1349 1465 'name' => 'enable_comment',
1350 1466 'type' => 'toggle',
@@ -1350,9 +1466,9 @@
1350 1466 'type' => 'toggle',
1351 1467 'label' => __( 'Comment', 'betterdocs' ),
1352 1468 'enable_disable_text_active' => true,
1353 1469 'default' => '',
1354 - 'priority' => 7
1470 + 'priority' => 11
1355 1471 ),
1356 1472 'enable_credit' => array(
1357 1473 'name' => 'enable_credit',
1358 1474 'type' => 'toggle',
@@ -1358,9 +1474,9 @@
1358 1474 'type' => 'toggle',
1359 1475 'label' => __( 'Show Powered by BetterDocs', 'betterdocs' ),
1360 1476 'enable_disable_text_active' => true,
1361 1477 'default' => '',
1362 - 'priority' => 8
1478 + 'priority' => 12
1363 1479 ),
1364 1480 'reaction_feedback_text' => array(
1365 1481 'name' => 'reaction_feedback_text',
1366 1482 'type' => 'text',
@@ -1365,9 +1481,9 @@
1365 1481 'name' => 'reaction_feedback_text',
1366 1482 'type' => 'text',
1367 1483 'label' => __( 'Reaction Feedback Text', 'betterdocs' ),
1368 1484 'default' => __( 'Thanks for your feedback.', 'betterdocs' ),
1369 - 'priority' => 9
1485 + 'priority' => 13
1370 1486 ),
1371 1487 'enable_estimated_reading_time' => array(
1372 1488 'name' => 'enable_estimated_reading_time',
1373 1489 'type' => 'toggle',
@@ -1373,9 +1489,9 @@
1373 1489 'type' => 'toggle',
1374 1490 'label' => __( 'Estimated Reading Time', 'betterdocs' ),
1375 1491 'enable_disable_text_active' => true,
1376 1492 'default' => 0,
1377 - 'priority' => 10
1493 + 'priority' => 14
1378 1494 ),
1379 1495 'estimated_reading_time_title' => array(
1380 1496 'name' => 'estimated_reading_time_title',
1381 1497 'type' => 'text',
@@ -1380,9 +1496,9 @@
1380 1496 'name' => 'estimated_reading_time_title',
1381 1497 'type' => 'text',
1382 1498 'label' => __( 'Estimated Reading Time Title', 'betterdocs' ),
1383 1499 'default' => '',
1384 - 'priority' => 11,
1500 + 'priority' => 15,
1385 1501 'rules' => Rules::is( 'enable_estimated_reading_time', true )
1386 1502 ),
1387 1503 'estimated_reading_time_text' => array(
1388 1504 'name' => 'estimated_reading_time_text',
@@ -1388,9 +1504,9 @@
1388 1504 'name' => 'estimated_reading_time_text',
1389 1505 'type' => 'text',
1390 1506 'label' => __( 'Estimated Reading Time Text', 'betterdocs' ),
1391 1507 'default' => __( 'min read', 'betterdocs' ),
1392 - 'priority' => 12,
1508 + 'priority' => 16,
1393 1509 'rules' => Rules::is( 'enable_estimated_reading_time', true )
1394 1510 ),
1395 1511 'singular_estimated_reading_time_text' => array(
1396 1512 'name' => 'singular_estimated_reading_time_text',
@@ -1396,9 +1512,9 @@
1396 1512 'name' => 'singular_estimated_reading_time_text',
1397 1513 'type' => 'text',
1398 1514 'label' => __( 'Estimated Reading Time Text Singular', 'betterdocs' ),
1399 1515 'default' => __( 'min read', 'betterdocs' ),
1400 - 'priority' => 13,
1516 + 'priority' => 17,
1401 1517 'rules' => Rules::is( 'enable_estimated_reading_time', true )
1402 1518 )
1403 1519 )
1404 1520 ),
@@ -2278,18 +2394,84 @@
2278 2394 'open-ai-settings' => array(
2279 2395 'id' => 'open-ai-settings',
2280 2396 'name' => 'open-ai-settings',
2281 2397 'type' => 'section',
2282 - 'label' => __( 'API Settings', 'betterdocs' ),
2398 + 'label' => __( 'AI Platform & API', 'betterdocs' ),
2283 2399 'priority' => 1,
2284 2400 'fields' => array(
2401 + 'ai_platform' => array(
2402 + 'name' => 'ai_platform',
2403 + 'type' => 'select',
2404 + 'label' => __( 'AI Platform', 'betterdocs' ),
2405 + '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' ),
2406 + 'priority' => 1,
2407 + 'multiple' => false,
2408 + 'default' => 'openai',
2409 + // DeepSeek and OpenRouter are temporarily hidden from the
2410 + // dropdown. Filter the options here, NOT ModelRegistry::platforms() —
2411 + // sensitive_api_key_fields() loops the registry to mask each
2412 + // platform's key field, so trimming the registry would silently
2413 + // un-mask those keys. Re-enable later by dropping the array_diff_key.
2414 + 'options' => GlobalFields::normalize_fields(
2415 + array_diff_key( ModelRegistry::platforms(), array_flip( array( 'deepseek', 'openrouter' ) ) )
2416 + )
2417 + ),
2418 + // OpenAI's key field keeps its original name so installs
2419 + // that already saved a Write with AI key keep it.
2285 2420 'ai_autowrite_api_key' => array(
2286 2421 'name' => 'ai_autowrite_api_key',
2287 2422 'type' => 'text',
2288 - 'label' => __( 'API Key', 'betterdocs' ),
2289 - '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/' ) ),
2423 + 'label' => __( 'OpenAI API Key', 'betterdocs' ),
2424 + '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/' ) ),
2290 2425 'default' => '',
2291 - 'priority' => 1
2426 + 'priority' => 2,
2427 + 'rules' => Rules::is( 'ai_platform', 'openai' )
2428 + ),
2429 + 'ai_api_key_gemini' => array(
2430 + 'name' => 'ai_api_key_gemini',
2431 + 'type' => 'text',
2432 + 'label' => __( 'Google Gemini API Key', 'betterdocs' ),
2433 + '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' ) ),
2434 + 'default' => '',
2435 + 'priority' => 2,
2436 + 'rules' => Rules::is( 'ai_platform', 'gemini' )
2437 + ),
2438 + 'ai_api_key_claude' => array(
2439 + 'name' => 'ai_api_key_claude',
2440 + 'type' => 'text',
2441 + 'label' => __( 'Anthropic Claude API Key', 'betterdocs' ),
2442 + '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' ) ),
2443 + 'default' => '',
2444 + 'priority' => 2,
2445 + 'rules' => Rules::is( 'ai_platform', 'claude' )
2446 + ),
2447 + 'ai_api_key_deepseek' => array(
2448 + 'name' => 'ai_api_key_deepseek',
2449 + 'type' => 'text',
2450 + 'label' => __( 'DeepSeek API Key', 'betterdocs' ),
2451 + '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' ) ),
2452 + 'default' => '',
2453 + 'priority' => 2,
2454 + 'rules' => Rules::is( 'ai_platform', 'deepseek' )
2455 + ),
2456 + 'ai_api_key_openrouter' => array(
2457 + 'name' => 'ai_api_key_openrouter',
2458 + 'type' => 'text',
2459 + 'label' => __( 'OpenRouter API Key', 'betterdocs' ),
2460 + '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' ) ),
2461 + 'default' => '',
2462 + 'priority' => 2,
2463 + 'rules' => Rules::is( 'ai_platform', 'openrouter' )
2464 + ),
2465 + 'ai_model' => array(
2466 + 'name' => 'ai_model',
2467 + 'type' => 'platform_model_select',
2468 + 'label' => __( 'Model', 'betterdocs' ),
2469 + 'label_subtitle' => __( 'Available models depend on the selected platform.', 'betterdocs' ),
2470 + 'priority' => 10,
2471 + 'default' => 'gpt-4o-mini',
2472 + 'catalogue' => ModelRegistry::catalogue(),
2473 + 'defaults' => ModelRegistry::defaults()
2292 2474 )
2293 2475 )
2294 2476 ),
2295 2477 'write-with-ai' => array(
@@ -2298,8 +2480,39 @@
2298 2480 'type' => 'section',
2299 2481 'label' => __( 'Write with AI', 'betterdocs' ),
2300 2482 'priority' => 5,
2301 2483 'fields' => array(
2484 + 'write-with-ai-subtabs' => array(
2485 + 'id' => 'write-with-ai-subtabs',
2486 + 'name' => 'write_with_ai_subtabs',
2487 + 'label' => __( 'Write with AI', 'betterdocs' ),
2488 + 'classes' => 'tab-nested-layout',
2489 + 'type' => 'tab',
2490 + 'active' => 'wwa-configuration',
2491 + 'completionTrack' => true,
2492 + 'sidebar' => false,
2493 + 'save' => false,
2494 + 'title' => false,
2495 + 'config' => array(
2496 + 'active' => 'wwa-configuration',
2497 + 'sidebar' => false,
2498 + 'title' => false
2499 + ),
2500 + 'submit' => array(
2501 + 'show' => false
2502 + ),
2503 + 'step' => array(
2504 + 'show' => false
2505 + ),
2506 + 'priority' => 5,
2507 + 'fields' => array(
2508 + 'wwa-configuration' => array(
2509 + 'id' => 'wwa-configuration',
2510 + 'name' => 'wwa-configuration',
2511 + 'type' => 'section',
2512 + 'label' => __( 'Configuration', 'betterdocs' ),
2513 + 'priority' => 1,
2514 + 'fields' => array(
2302 2515 'enable_write_with_ai' => array(
2303 2516 'name' => 'enable_write_with_ai',
2304 2517 'type' => 'toggle',
2305 2518 'priority' => 0,
@@ -2323,29 +2536,20 @@
2323 2536 'priority' => 6,
2324 2537 'label' => __( 'Write Glossaries with AI', 'betterdocs' ),
2325 2538 'label_subtitle' => __( 'Generate AI based Glossary definitions from the Glossaries admin page', 'betterdocs' ),
2326 2539 'enable_disable_text_active' => true,
2540 + 'default' => true,
2541 + 'is_pro' => true
2542 + ),
2543 + 'enable_docs_ai_suite' => array(
2544 + 'name' => 'enable_docs_ai_suite',
2545 + 'type' => 'toggle',
2546 + 'priority' => 7,
2547 + 'label' => __( 'Enhance Docs with AI', 'betterdocs' ),
2548 + 'label_subtitle' => __( 'Add BetterDocs AI actions for suggesting categories, tags and excerpts inside the native Docs editor panels.', 'betterdocs' ),
2549 + 'enable_disable_text_active' => true,
2327 2550 'default' => true
2328 2551 ),
2329 - 'write_with_ai_model' => array(
2330 - 'name' => 'write_with_ai_model',
2331 - 'type' => 'select',
2332 - 'label' => __( 'OpenAI Model*', 'betterdocs' ),
2333 - 'priority' => 20,
2334 - 'multiple' => false,
2335 - 'default' => 'gpt-4o-mini',
2336 - 'options' => GlobalFields::normalize_fields( array(
2337 - 'gpt-4o-mini' => 'GPT-4o Mini',
2338 - 'gpt-4o' => 'GPT-4o',
2339 - 'gpt-4.1-nano' => 'GPT-4.1 Nano',
2340 - 'gpt-4.1-mini' => 'GPT-4.1 Mini',
2341 - 'gpt-4.1' => 'GPT-4.1',
2342 - 'gpt-5-nano' => 'GPT-5 Nano',
2343 - 'gpt-5-mini' => 'GPT-5 Mini',
2344 - 'gpt-5' => 'GPT-5',
2345 - 'gpt-5.5' => 'GPT-5.5'
2346 - ) )
2347 - ),
2348 2552 'ai_autowrite_max_token' => array(
2349 2553 'name' => 'ai_autowrite_max_token',
2350 2554 'type' => 'min_token_number',
2351 2555 'label' => __( 'Set Max Tokens', 'betterdocs' ),
@@ -2352,13 +2556,57 @@
2352 2556 'label_subtitle' => sprintf( // translators: %s is a link to more information about token limits.
2353 2557 __( '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' ) ),
2354 2558 'default' => 2500,
2355 2559 'priority' => 10,
2356 - 'model_field' => 'write_with_ai_model',
2560 + 'model_field' => 'ai_model',
2357 2561 'context' => 'write_with_ai',
2358 2562 'min_token_map' => AIHelper::get_min_tokens_map( 'write_with_ai' ),
2359 2563 'classes' => 'wprf-type-text'
2360 2564 )
2565 + )
2566 + ),
2567 + 'wwa-personalize' => array(
2568 + 'id' => 'wwa-personalize',
2569 + 'name' => 'wwa-personalize',
2570 + 'type' => 'section',
2571 + 'label' => __( 'Write AI Personalize', 'betterdocs' ),
2572 + 'priority' => 5,
2573 + 'fields' => array(
2574 + 'write_with_ai_instructions' => array(
2575 + 'name' => 'write_with_ai_instructions',
2576 + 'type' => 'wwa_instructions',
2577 + 'label' => __( 'Instructions', 'betterdocs' ),
2578 + 'label_subtitle' => __( 'Add reusable instruction sets to steer how BetterDocs AI writes. The "Default/Core" set is always applied; extra sets can be picked in the Write with AI popup.', 'betterdocs' ),
2579 + 'priority' => 1,
2580 + 'default' => array(
2581 + array(
2582 + 'id' => 'default',
2583 + 'title' => __( 'Default/Core', 'betterdocs' ),
2584 + 'content' => WriteWithAI::default_instruction_content()
2585 + )
2586 + )
2587 + )
2588 + )
2589 + ),
2590 + 'edit-ai-personalize' => array(
2591 + 'id' => 'edit-ai-personalize',
2592 + 'name' => 'edit-ai-personalize',
2593 + 'type' => 'section',
2594 + 'label' => __( 'Edit AI Personalize', 'betterdocs' ),
2595 + 'priority' => 7,
2596 + 'fields' => array(
2597 + 'ai_edit_actions' => array(
2598 + 'name' => 'ai_edit_actions',
2599 + 'type' => 'ai_edit_actions',
2600 + 'label' => __( 'Actions', 'betterdocs' ),
2601 + 'label_subtitle' => __( 'Choose which Edit with AI actions appear in the editor. Toggle actions on or off, edit each action\'s instruction, or add your own custom actions. All predefined actions are enabled by default.', 'betterdocs' ),
2602 + 'priority' => 1,
2603 + 'default' => AIEdit::default_actions()
2604 + )
2605 + )
2606 + )
2607 + )
2608 + )
2361 2609 )
2362 2610 ),
2363 2611 'article-summary' => array(
2364 2612 'id' => 'article-summary',
@@ -2383,31 +2631,12 @@
2383 2631 'label_subtitle' => sprintf( // translators: %s is a link to more information about token limits.
2384 2632 __( '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' ) ),
2385 2633 'default' => 1500,
2386 2634 'priority' => 5,
2387 - 'model_field' => 'article_summary_model',
2635 + 'model_field' => 'ai_model',
2388 2636 'context' => 'article_summary',
2389 2637 'min_token_map' => AIHelper::get_min_tokens_map( 'article_summary' ),
2390 2638 'classes' => 'wprf-type-text'
2391 - ),
2392 - 'article_summary_model' => array(
2393 - 'name' => 'article_summary_model',
2394 - 'type' => 'select',
2395 - 'label' => __( 'OpenAI Model*', 'betterdocs' ),
2396 - 'priority' => 10,
2397 - 'multiple' => false,
2398 - 'default' => 'gpt-4o-mini',
2399 - 'options' => GlobalFields::normalize_fields( array(
2400 - 'gpt-4o-mini' => 'GPT-4o Mini',
2401 - 'gpt-4o' => 'GPT-4o',
2402 - 'gpt-4.1-nano' => 'GPT-4.1 Nano',
2403 - 'gpt-4.1-mini' => 'GPT-4.1 Mini',
2404 - 'gpt-4.1' => 'GPT-4.1',
2405 - 'gpt-5-nano' => 'GPT-5 Nano',
2406 - 'gpt-5-mini' => 'GPT-5 Mini',
2407 - 'gpt-5' => 'GPT-5',
2408 - 'gpt-5.5' => 'GPT-5.5'
2409 - ) )
2410 2639 )
2411 2640 )
2412 2641 )
2413 2642 )