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 +305 -75 4.5.54.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 }
@@ -202,8 +228,9 @@
202 228 */
203 229 public function get_default() {
204 230 $_default = array(
205 231 'multiple_kb' => '',
232 + 'enable_ai_sample_docs' => true,
206 233 'enable_export_faq' => true,
207 234 'builtin_doc_page' => true,
208 235 'breadcrumb_doc_title' => __( 'Docs', 'betterdocs' ),
209 236 'enable_category_hierarchy_slugs' => false,
@@ -256,8 +283,12 @@
256 283 'enable_breadcrumb_category' => true,
257 284 'enable_breadcrumb_title' => true,
258 285 'enable_sidebar_cat_list' => true,
259 286 'enable_print_icon' => true,
287 + 'print_enable_logo' => false,
288 + 'print_logo' => array(),
289 + 'print_enable_footer' => false,
290 + 'print_footer_text' => '',
260 291 'enable_tags' => true,
261 292 'email_feedback' => true,
262 293 'feedback_link_text' => __( 'Still stuck? How can we help?', 'betterdocs' ),
263 294 'reaction_feedback_text' => __( 'Thanks for your feedback', 'betterdocs' ),
@@ -270,8 +301,9 @@
270 301 'enable_credit' => false,
271 302 'enable_archive_sidebar' => true,
272 303 'archive_nested_subcategory' => true,
273 304 'archive_enable_pagination' => false,
305 + 'archive_lazy_load_descendants' => false,
274 306 'enable_content_restriction' => false,
275 307 'enable_reporting' => false,
276 308 'enable_sample_data' => false,
277 309 'reporting_day' => 'monday',
@@ -278,14 +310,34 @@
278 310 'reporting_email' => get_option( 'admin_email' ),
279 311 'enable_write_with_ai' => true,
280 312 'enable_faq_write_with_ai' => true,
281 313 'enable_glossaries_write_with_ai' => true,
314 + 'enable_docs_ai_suite' => true,
282 315 'write_with_ai_model' => 'gpt-4o-mini',
283 316 'ai_autowrite_api_key' => '',
284 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(),
285 326 'enable_article_summary' => false,
286 327 'article_summary_model' => 'gpt-4o-mini',
287 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' => '',
288 340 'enable_estimated_reading_time' => true,
289 341 'enable_encyclopedia' => false,
290 342 'enable_glossaries' => false,
291 343 'show_glossary_suggestions' => true,
@@ -295,9 +347,21 @@
295 347 'singular_estimated_reading_time_text' => __( 'min read', 'betterdocs' ),
296 348 'betterdocs_access_control_repeater' => array(),
297 349 'internal_knowledge_base_type' => 'basic',
298 350 'betterdocs_access_control_repeater_kb' => array(),
299 - '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' => ''
300 364 );
301 365
302 366 $_default = apply_filters( 'betterdocs_default_settings', $_default );
303 367 // $_default = apply_filters_deprecated(
@@ -590,19 +654,27 @@
590 654 }
591 655
592 656 $_old_settings = $this->database->get( $this->base_key, $this->get_default() );
593 657
594 - // The frontend only ever sees masked API keys. If a submitted value matches the mask of
595 - // the stored value, the user did not change it — restore the original so the mask string
596 - // is never persisted.
597 - $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();
598 667 foreach ( $sensitive_api_keys as $api_key_field ) {
599 668 if ( ! isset( $settings[ $api_key_field ] ) ) {
600 669 continue;
601 670 }
602 - $stored = isset( $_old_settings[ $api_key_field ] ) ? $_old_settings[ $api_key_field ] : '';
603 - if ( '' !== $stored && trim( (string) $settings[ $api_key_field ] ) === Helper::mask_api_key( $stored ) ) {
604 - $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;
605 677 }
606 678 }
607 679
608 680 // Minimum-token policy: reject saves where a feature's max_token field is below the
@@ -617,9 +689,9 @@
617 689 array(
618 690 'tab' => 'tab-betterdocs-ai',
619 691 'context' => 'write_with_ai',
620 692 'token_key' => 'ai_autowrite_max_token',
621 - 'model_key' => 'write_with_ai_model',
693 + 'model_key' => 'ai_model',
622 694 'label' => __( 'Write with AI', 'betterdocs' )
623 695 ),
624 696 array(
625 697 'tab' => 'tab-betterdocs-ai',
@@ -624,9 +696,9 @@
624 696 array(
625 697 'tab' => 'tab-betterdocs-ai',
626 698 'context' => 'article_summary',
627 699 'token_key' => 'article_summary_max_token',
628 - 'model_key' => 'article_summary_model',
700 + 'model_key' => 'ai_model',
629 701 'label' => __( 'AI Doc Summarizer', 'betterdocs' )
630 702 )
631 703 );
632 704 foreach ( $token_pairs as $pair ) {
@@ -998,8 +1070,17 @@
998 1070 'enable_disable_text_active' => true,
999 1071 'default' => 1,
1000 1072 'priority' => 1
1001 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 + ),
1002 1083 'nested_subcategory' => array(
1003 1084 'name' => 'nested_subcategory',
1004 1085 'type' => 'toggle',
1005 1086 'label' => __( 'Nested Sub Category', 'betterdocs' ),
@@ -1004,9 +1085,9 @@
1004 1085 'type' => 'toggle',
1005 1086 'label' => __( 'Nested Sub Category', 'betterdocs' ),
1006 1087 'enable_disable_text_active' => true,
1007 1088 'default' => '',
1008 - 'priority' => 2
1089 + 'priority' => 3
1009 1090 ),
1010 1091 'column_number' => array(
1011 1092 'name' => 'column_number',
1012 1093 'type' => 'number',
@@ -1012,9 +1093,9 @@
1012 1093 'type' => 'number',
1013 1094 'label' => __( 'Number Of Columns', 'betterdocs' ),
1014 1095 'label_subtitle' => __( 'This setting is not applicable for sleek layout.', 'betterdocs' ),
1015 1096 'default' => 3,
1016 - 'priority' => 3
1097 + 'priority' => 4
1017 1098 ),
1018 1099 'posts_number' => apply_filters( 'betterdocs_posts_number', array(
1019 1100 'name' => 'posts_number',
1020 1101 'type' => 'number',
@@ -1020,9 +1101,9 @@
1020 1101 'type' => 'number',
1021 1102 'label' => __( 'Number Of Docs', 'betterdocs' ),
1022 1103 'label_subtitle' => __( 'This setting is not applicable for handbook layout.', 'betterdocs' ),
1023 1104 'default' => 10,
1024 - 'priority' => 4
1105 + 'priority' => 5
1025 1106 ) ),
1026 1107 'post_count' => array(
1027 1108 'name' => 'post_count',
1028 1109 'type' => 'toggle',
@@ -1028,9 +1109,9 @@
1028 1109 'type' => 'toggle',
1029 1110 'label' => __( 'Doc Count', 'betterdocs' ),
1030 1111 'enable_disable_text_active' => true,
1031 1112 'default' => 1,
1032 - 'priority' => 5
1113 + 'priority' => 6
1033 1114 ),
1034 1115 'count_text' => array(
1035 1116 'name' => 'count_text',
1036 1117 'type' => 'text',
@@ -1035,9 +1116,9 @@
1035 1116 'name' => 'count_text',
1036 1117 'type' => 'text',
1037 1118 'label' => __( 'Count Text', 'betterdocs' ),
1038 1119 'default' => __( 'Docs', 'betterdocs' ),
1039 - 'priority' => 6
1120 + 'priority' => 7
1040 1121 ),
1041 1122 'count_text_singular' => array(
1042 1123 'name' => 'count_text_singular',
1043 1124 'type' => 'text',
@@ -1042,9 +1123,9 @@
1042 1123 'name' => 'count_text_singular',
1043 1124 'type' => 'text',
1044 1125 'label' => __( 'Count Text Singular', 'betterdocs' ),
1045 1126 'default' => __( 'Doc', 'betterdocs' ),
1046 - 'priority' => 7
1127 + 'priority' => 8
1047 1128 ),
1048 1129 'exploremore_btn' => array(
1049 1130 'name' => 'exploremore_btn',
1050 1131 'type' => 'toggle',
@@ -1050,9 +1131,9 @@
1050 1131 'type' => 'toggle',
1051 1132 'label' => __( 'Explore More Button', 'betterdocs' ),
1052 1133 'enable_disable_text_active' => true,
1053 1134 'default' => true,
1054 - 'priority' => 8
1135 + 'priority' => 9
1055 1136 ),
1056 1137 'exploremore_btn_txt' => array(
1057 1138 'name' => 'exploremore_btn_txt',
1058 1139 'type' => 'text',
@@ -1057,9 +1138,9 @@
1057 1138 'name' => 'exploremore_btn_txt',
1058 1139 'type' => 'text',
1059 1140 'label' => __( 'Explore More Button Text', 'betterdocs' ),
1060 1141 'default' => __( 'Explore More', 'betterdocs' ),
1061 - 'priority' => 9,
1142 + 'priority' => 10,
1062 1143 'rules' => Rules::is( 'exploremore_btn', true )
1063 1144 ),
1064 1145 'betterdocs_popular_docs_text' => array(
1065 1146 'name' => 'betterdocs_popular_docs_text',
@@ -1065,9 +1146,9 @@
1065 1146 'name' => 'betterdocs_popular_docs_text',
1066 1147 'type' => 'text',
1067 1148 'label' => __( 'Popular Docs Text', 'betterdocs' ),
1068 1149 'default' => __( 'Popular Docs', 'betterdocs' ),
1069 - 'priority' => 10,
1150 + 'priority' => 11,
1070 1151 'is_pro' => true
1071 1152 ),
1072 1153 'betterdocs_popular_docs_number' => array(
1073 1154 'name' => 'betterdocs_popular_docs_number',
@@ -1073,9 +1154,9 @@
1073 1154 'name' => 'betterdocs_popular_docs_number',
1074 1155 'type' => 'number',
1075 1156 'label' => __( 'Popular Docs Number', 'betterdocs' ),
1076 1157 'default' => 10,
1077 - 'priority' => 11,
1158 + 'priority' => 12,
1078 1159 'is_pro' => true
1079 1160 )
1080 1161 )
1081 1162 ),
@@ -1319,8 +1400,44 @@
1319 1400 'enable_disable_text_active' => true,
1320 1401 'default' => 1,
1321 1402 'priority' => 3
1322 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 + ),
1323 1440 'enable_tags' => array(
1324 1441 'name' => 'enable_tags',
1325 1442 'type' => 'toggle',
1326 1443 'label' => __( 'Tags', 'betterdocs' ),
@@ -1325,9 +1442,9 @@
1325 1442 'type' => 'toggle',
1326 1443 'label' => __( 'Tags', 'betterdocs' ),
1327 1444 'enable_disable_text_active' => true,
1328 1445 'default' => 1,
1329 - 'priority' => 4
1446 + 'priority' => 8
1330 1447 ),
1331 1448 'show_last_update_time' => array(
1332 1449 'name' => 'show_last_update_time',
1333 1450 'type' => 'toggle',
@@ -1333,9 +1450,9 @@
1333 1450 'type' => 'toggle',
1334 1451 'label' => __( 'Last Update Time', 'betterdocs' ),
1335 1452 'enable_disable_text_active' => true,
1336 1453 'default' => 1,
1337 - 'priority' => 5
1454 + 'priority' => 9
1338 1455 ),
1339 1456 'enable_navigation' => array(
1340 1457 'name' => 'enable_navigation',
1341 1458 'type' => 'toggle',
@@ -1341,9 +1458,9 @@
1341 1458 'type' => 'toggle',
1342 1459 'label' => __( 'Navigation', 'betterdocs' ),
1343 1460 'enable_disable_text_active' => true,
1344 1461 'default' => 1,
1345 - 'priority' => 6
1462 + 'priority' => 10
1346 1463 ),
1347 1464 'enable_comment' => array(
1348 1465 'name' => 'enable_comment',
1349 1466 'type' => 'toggle',
@@ -1349,9 +1466,9 @@
1349 1466 'type' => 'toggle',
1350 1467 'label' => __( 'Comment', 'betterdocs' ),
1351 1468 'enable_disable_text_active' => true,
1352 1469 'default' => '',
1353 - 'priority' => 7
1470 + 'priority' => 11
1354 1471 ),
1355 1472 'enable_credit' => array(
1356 1473 'name' => 'enable_credit',
1357 1474 'type' => 'toggle',
@@ -1357,9 +1474,9 @@
1357 1474 'type' => 'toggle',
1358 1475 'label' => __( 'Show Powered by BetterDocs', 'betterdocs' ),
1359 1476 'enable_disable_text_active' => true,
1360 1477 'default' => '',
1361 - 'priority' => 8
1478 + 'priority' => 12
1362 1479 ),
1363 1480 'reaction_feedback_text' => array(
1364 1481 'name' => 'reaction_feedback_text',
1365 1482 'type' => 'text',
@@ -1364,9 +1481,9 @@
1364 1481 'name' => 'reaction_feedback_text',
1365 1482 'type' => 'text',
1366 1483 'label' => __( 'Reaction Feedback Text', 'betterdocs' ),
1367 1484 'default' => __( 'Thanks for your feedback.', 'betterdocs' ),
1368 - 'priority' => 9
1485 + 'priority' => 13
1369 1486 ),
1370 1487 'enable_estimated_reading_time' => array(
1371 1488 'name' => 'enable_estimated_reading_time',
1372 1489 'type' => 'toggle',
@@ -1372,9 +1489,9 @@
1372 1489 'type' => 'toggle',
1373 1490 'label' => __( 'Estimated Reading Time', 'betterdocs' ),
1374 1491 'enable_disable_text_active' => true,
1375 1492 'default' => 0,
1376 - 'priority' => 10
1493 + 'priority' => 14
1377 1494 ),
1378 1495 'estimated_reading_time_title' => array(
1379 1496 'name' => 'estimated_reading_time_title',
1380 1497 'type' => 'text',
@@ -1379,9 +1496,9 @@
1379 1496 'name' => 'estimated_reading_time_title',
1380 1497 'type' => 'text',
1381 1498 'label' => __( 'Estimated Reading Time Title', 'betterdocs' ),
1382 1499 'default' => '',
1383 - 'priority' => 11,
1500 + 'priority' => 15,
1384 1501 'rules' => Rules::is( 'enable_estimated_reading_time', true )
1385 1502 ),
1386 1503 'estimated_reading_time_text' => array(
1387 1504 'name' => 'estimated_reading_time_text',
@@ -1387,9 +1504,9 @@
1387 1504 'name' => 'estimated_reading_time_text',
1388 1505 'type' => 'text',
1389 1506 'label' => __( 'Estimated Reading Time Text', 'betterdocs' ),
1390 1507 'default' => __( 'min read', 'betterdocs' ),
1391 - 'priority' => 12,
1508 + 'priority' => 16,
1392 1509 'rules' => Rules::is( 'enable_estimated_reading_time', true )
1393 1510 ),
1394 1511 'singular_estimated_reading_time_text' => array(
1395 1512 'name' => 'singular_estimated_reading_time_text',
@@ -1395,9 +1512,9 @@
1395 1512 'name' => 'singular_estimated_reading_time_text',
1396 1513 'type' => 'text',
1397 1514 'label' => __( 'Estimated Reading Time Text Singular', 'betterdocs' ),
1398 1515 'default' => __( 'min read', 'betterdocs' ),
1399 - 'priority' => 13,
1516 + 'priority' => 17,
1400 1517 'rules' => Rules::is( 'enable_estimated_reading_time', true )
1401 1518 )
1402 1519 )
1403 1520 ),
@@ -2277,18 +2394,84 @@
2277 2394 'open-ai-settings' => array(
2278 2395 'id' => 'open-ai-settings',
2279 2396 'name' => 'open-ai-settings',
2280 2397 'type' => 'section',
2281 - 'label' => __( 'API Settings', 'betterdocs' ),
2398 + 'label' => __( 'AI Platform & API', 'betterdocs' ),
2282 2399 'priority' => 1,
2283 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.
2284 2420 'ai_autowrite_api_key' => array(
2285 2421 'name' => 'ai_autowrite_api_key',
2286 2422 'type' => 'text',
2287 - 'label' => __( 'API Key', 'betterdocs' ),
2288 - '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/' ) ),
2289 2425 'default' => '',
2290 - '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()
2291 2474 )
2292 2475 )
2293 2476 ),
2294 2477 'write-with-ai' => array(
@@ -2297,8 +2480,39 @@
2297 2480 'type' => 'section',
2298 2481 'label' => __( 'Write with AI', 'betterdocs' ),
2299 2482 'priority' => 5,
2300 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(
2301 2515 'enable_write_with_ai' => array(
2302 2516 'name' => 'enable_write_with_ai',
2303 2517 'type' => 'toggle',
2304 2518 'priority' => 0,
@@ -2322,29 +2536,20 @@
2322 2536 'priority' => 6,
2323 2537 'label' => __( 'Write Glossaries with AI', 'betterdocs' ),
2324 2538 'label_subtitle' => __( 'Generate AI based Glossary definitions from the Glossaries admin page', 'betterdocs' ),
2325 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,
2326 2550 'default' => true
2327 2551 ),
2328 - 'write_with_ai_model' => array(
2329 - 'name' => 'write_with_ai_model',
2330 - 'type' => 'select',
2331 - 'label' => __( 'OpenAI Model*', 'betterdocs' ),
2332 - 'priority' => 20,
2333 - 'multiple' => false,
2334 - 'default' => 'gpt-4o-mini',
2335 - 'options' => GlobalFields::normalize_fields( array(
2336 - 'gpt-4o-mini' => 'GPT-4o Mini',
2337 - 'gpt-4o' => 'GPT-4o',
2338 - 'gpt-4.1-nano' => 'GPT-4.1 Nano',
2339 - 'gpt-4.1-mini' => 'GPT-4.1 Mini',
2340 - 'gpt-4.1' => 'GPT-4.1',
2341 - 'gpt-5-nano' => 'GPT-5 Nano',
2342 - 'gpt-5-mini' => 'GPT-5 Mini',
2343 - 'gpt-5' => 'GPT-5',
2344 - 'gpt-5.5' => 'GPT-5.5'
2345 - ) )
2346 - ),
2347 2552 'ai_autowrite_max_token' => array(
2348 2553 'name' => 'ai_autowrite_max_token',
2349 2554 'type' => 'min_token_number',
2350 2555 'label' => __( 'Set Max Tokens', 'betterdocs' ),
@@ -2351,13 +2556,57 @@
2351 2556 'label_subtitle' => sprintf( // translators: %s is a link to more information about token limits.
2352 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' ) ),
2353 2558 'default' => 2500,
2354 2559 'priority' => 10,
2355 - 'model_field' => 'write_with_ai_model',
2560 + 'model_field' => 'ai_model',
2356 2561 'context' => 'write_with_ai',
2357 2562 'min_token_map' => AIHelper::get_min_tokens_map( 'write_with_ai' ),
2358 2563 'classes' => 'wprf-type-text'
2359 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 + )
2360 2609 )
2361 2610 ),
2362 2611 'article-summary' => array(
2363 2612 'id' => 'article-summary',
@@ -2382,31 +2631,12 @@
2382 2631 'label_subtitle' => sprintf( // translators: %s is a link to more information about token limits.
2383 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' ) ),
2384 2633 'default' => 1500,
2385 2634 'priority' => 5,
2386 - 'model_field' => 'article_summary_model',
2635 + 'model_field' => 'ai_model',
2387 2636 'context' => 'article_summary',
2388 2637 'min_token_map' => AIHelper::get_min_tokens_map( 'article_summary' ),
2389 2638 'classes' => 'wprf-type-text'
2390 - ),
2391 - 'article_summary_model' => array(
2392 - 'name' => 'article_summary_model',
2393 - 'type' => 'select',
2394 - 'label' => __( 'OpenAI Model*', 'betterdocs' ),
2395 - 'priority' => 10,
2396 - 'multiple' => false,
2397 - 'default' => 'gpt-4o-mini',
2398 - 'options' => GlobalFields::normalize_fields( array(
2399 - 'gpt-4o-mini' => 'GPT-4o Mini',
2400 - 'gpt-4o' => 'GPT-4o',
2401 - 'gpt-4.1-nano' => 'GPT-4.1 Nano',
2402 - 'gpt-4.1-mini' => 'GPT-4.1 Mini',
2403 - 'gpt-4.1' => 'GPT-4.1',
2404 - 'gpt-5-nano' => 'GPT-5 Nano',
2405 - 'gpt-5-mini' => 'GPT-5 Mini',
2406 - 'gpt-5' => 'GPT-5',
2407 - 'gpt-5.5' => 'GPT-5.5'
2408 - ) )
2409 2639 )
2410 2640 )
2411 2641 )
2412 2642 )