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 +314 -77 4.5.34.9.2 View file →
@@ -1,12 +1,19 @@
1 1 <?php
2 +namespace WPDeveloper\BetterDocs\Core;
2 3
3 -namespace WPDeveloper\BetterDocs\Core;
4 +if ( ! defined( 'ABSPATH' ) ) {
5 + exit;
6 +}
4 7
8 +
5 9 use WP_Error;
6 10 use WP_User;
7 11 use WPDeveloper\BetterDocs\Admin\Builder\GlobalFields;
8 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;
9 16 use WPDeveloper\BetterDocs\Utils\AIHelper;
10 17 use WPDeveloper\BetterDocs\Utils\Base;
11 18 use WPDeveloper\BetterDocs\Utils\Database;
12 19 use WPDeveloper\BetterDocs\Utils\Helper;
@@ -72,8 +79,31 @@
72 79 return esc_url( admin_url( 'admin.php?page=betterdocs-settings' ) );
73 80 }
74 81
75 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 + /**
76 106 * This method is responsible for enqueueing scripts in settings panel
77 107 *
78 108 * @param string $hook
79 109 *
@@ -90,9 +120,9 @@
90 120
91 121 $settings = GlobalFields::normalize( $this->settings_args() );
92 122
93 123 // Mask sensitive API keys before they reach the browser. Non-admins still get them stripped entirely below.
94 - $sensitive_api_keys = array( 'ai_autowrite_api_key', 'ai_chatbot_api_key' );
124 + $sensitive_api_keys = self::sensitive_api_key_fields();
95 125 foreach ( $sensitive_api_keys as $api_key_field ) {
96 126 if ( ! empty( $settings[ 'values' ][ $api_key_field ] ) ) {
97 127 $settings[ 'values' ][ $api_key_field ] = Helper::mask_api_key( $settings[ 'values' ][ $api_key_field ] );
98 128 }
@@ -198,8 +228,9 @@
198 228 */
199 229 public function get_default() {
200 230 $_default = array(
201 231 'multiple_kb' => '',
232 + 'enable_ai_sample_docs' => true,
202 233 'enable_export_faq' => true,
203 234 'builtin_doc_page' => true,
204 235 'breadcrumb_doc_title' => __( 'Docs', 'betterdocs' ),
205 236 'enable_category_hierarchy_slugs' => false,
@@ -252,8 +283,12 @@
252 283 'enable_breadcrumb_category' => true,
253 284 'enable_breadcrumb_title' => true,
254 285 'enable_sidebar_cat_list' => true,
255 286 'enable_print_icon' => true,
287 + 'print_enable_logo' => false,
288 + 'print_logo' => array(),
289 + 'print_enable_footer' => false,
290 + 'print_footer_text' => '',
256 291 'enable_tags' => true,
257 292 'email_feedback' => true,
258 293 'feedback_link_text' => __( 'Still stuck? How can we help?', 'betterdocs' ),
259 294 'reaction_feedback_text' => __( 'Thanks for your feedback', 'betterdocs' ),
@@ -266,8 +301,9 @@
266 301 'enable_credit' => false,
267 302 'enable_archive_sidebar' => true,
268 303 'archive_nested_subcategory' => true,
269 304 'archive_enable_pagination' => false,
305 + 'archive_lazy_load_descendants' => false,
270 306 'enable_content_restriction' => false,
271 307 'enable_reporting' => false,
272 308 'enable_sample_data' => false,
273 309 'reporting_day' => 'monday',
@@ -274,14 +310,34 @@
274 310 'reporting_email' => get_option( 'admin_email' ),
275 311 'enable_write_with_ai' => true,
276 312 'enable_faq_write_with_ai' => true,
277 313 'enable_glossaries_write_with_ai' => true,
314 + 'enable_docs_ai_suite' => true,
278 315 'write_with_ai_model' => 'gpt-4o-mini',
279 316 'ai_autowrite_api_key' => '',
280 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(),
281 326 'enable_article_summary' => false,
282 327 'article_summary_model' => 'gpt-4o-mini',
283 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' => '',
284 340 'enable_estimated_reading_time' => true,
285 341 'enable_encyclopedia' => false,
286 342 'enable_glossaries' => false,
287 343 'show_glossary_suggestions' => true,
@@ -291,9 +347,21 @@
291 347 'singular_estimated_reading_time_text' => __( 'min read', 'betterdocs' ),
292 348 'betterdocs_access_control_repeater' => array(),
293 349 'internal_knowledge_base_type' => 'basic',
294 350 'betterdocs_access_control_repeater_kb' => array(),
295 - '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' => ''
296 364 );
297 365
298 366 $_default = apply_filters( 'betterdocs_default_settings', $_default );
299 367 // $_default = apply_filters_deprecated(
@@ -425,9 +493,10 @@
425 493 wp_send_json_error();
426 494 }
427 495
428 496 if ( isset( $_POST[ 'mode' ] ) ) {
429 - if ( $this->save( 'dark_mode', rest_sanitize_boolean( $_POST[ 'mode' ] ) ) ) { // phpcs:ignore
497 + $mode = sanitize_text_field( wp_unslash( $_POST[ 'mode' ] ) );
498 + if ( $this->save( 'dark_mode', rest_sanitize_boolean( $mode ) ) ) {
430 499 wp_send_json_success();
431 500 }
432 501 }
433 502
@@ -585,19 +654,27 @@
585 654 }
586 655
587 656 $_old_settings = $this->database->get( $this->base_key, $this->get_default() );
588 657
589 - // The frontend only ever sees masked API keys. If a submitted value matches the mask of
590 - // the stored value, the user did not change it — restore the original so the mask string
591 - // is never persisted.
592 - $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();
593 667 foreach ( $sensitive_api_keys as $api_key_field ) {
594 668 if ( ! isset( $settings[ $api_key_field ] ) ) {
595 669 continue;
596 670 }
597 - $stored = isset( $_old_settings[ $api_key_field ] ) ? $_old_settings[ $api_key_field ] : '';
598 - if ( '' !== $stored && trim( (string) $settings[ $api_key_field ] ) === Helper::mask_api_key( $stored ) ) {
599 - $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;
600 677 }
601 678 }
602 679
603 680 // Minimum-token policy: reject saves where a feature's max_token field is below the
@@ -612,9 +689,9 @@
612 689 array(
613 690 'tab' => 'tab-betterdocs-ai',
614 691 'context' => 'write_with_ai',
615 692 'token_key' => 'ai_autowrite_max_token',
616 - 'model_key' => 'write_with_ai_model',
693 + 'model_key' => 'ai_model',
617 694 'label' => __( 'Write with AI', 'betterdocs' )
618 695 ),
619 696 array(
620 697 'tab' => 'tab-betterdocs-ai',
@@ -619,9 +696,9 @@
619 696 array(
620 697 'tab' => 'tab-betterdocs-ai',
621 698 'context' => 'article_summary',
622 699 'token_key' => 'article_summary_max_token',
623 - 'model_key' => 'article_summary_model',
700 + 'model_key' => 'ai_model',
624 701 'label' => __( 'AI Doc Summarizer', 'betterdocs' )
625 702 )
626 703 );
627 704 foreach ( $token_pairs as $pair ) {
@@ -993,8 +1070,17 @@
993 1070 'enable_disable_text_active' => true,
994 1071 'default' => 1,
995 1072 'priority' => 1
996 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 + ),
997 1083 'nested_subcategory' => array(
998 1084 'name' => 'nested_subcategory',
999 1085 'type' => 'toggle',
1000 1086 'label' => __( 'Nested Sub Category', 'betterdocs' ),
@@ -999,9 +1085,9 @@
999 1085 'type' => 'toggle',
1000 1086 'label' => __( 'Nested Sub Category', 'betterdocs' ),
1001 1087 'enable_disable_text_active' => true,
1002 1088 'default' => '',
1003 - 'priority' => 2
1089 + 'priority' => 3
1004 1090 ),
1005 1091 'column_number' => array(
1006 1092 'name' => 'column_number',
1007 1093 'type' => 'number',
@@ -1007,9 +1093,9 @@
1007 1093 'type' => 'number',
1008 1094 'label' => __( 'Number Of Columns', 'betterdocs' ),
1009 1095 'label_subtitle' => __( 'This setting is not applicable for sleek layout.', 'betterdocs' ),
1010 1096 'default' => 3,
1011 - 'priority' => 3
1097 + 'priority' => 4
1012 1098 ),
1013 1099 'posts_number' => apply_filters( 'betterdocs_posts_number', array(
1014 1100 'name' => 'posts_number',
1015 1101 'type' => 'number',
@@ -1015,9 +1101,9 @@
1015 1101 'type' => 'number',
1016 1102 'label' => __( 'Number Of Docs', 'betterdocs' ),
1017 1103 'label_subtitle' => __( 'This setting is not applicable for handbook layout.', 'betterdocs' ),
1018 1104 'default' => 10,
1019 - 'priority' => 4
1105 + 'priority' => 5
1020 1106 ) ),
1021 1107 'post_count' => array(
1022 1108 'name' => 'post_count',
1023 1109 'type' => 'toggle',
@@ -1023,9 +1109,9 @@
1023 1109 'type' => 'toggle',
1024 1110 'label' => __( 'Doc Count', 'betterdocs' ),
1025 1111 'enable_disable_text_active' => true,
1026 1112 'default' => 1,
1027 - 'priority' => 5
1113 + 'priority' => 6
1028 1114 ),
1029 1115 'count_text' => array(
1030 1116 'name' => 'count_text',
1031 1117 'type' => 'text',
@@ -1030,9 +1116,9 @@
1030 1116 'name' => 'count_text',
1031 1117 'type' => 'text',
1032 1118 'label' => __( 'Count Text', 'betterdocs' ),
1033 1119 'default' => __( 'Docs', 'betterdocs' ),
1034 - 'priority' => 6
1120 + 'priority' => 7
1035 1121 ),
1036 1122 'count_text_singular' => array(
1037 1123 'name' => 'count_text_singular',
1038 1124 'type' => 'text',
@@ -1037,9 +1123,9 @@
1037 1123 'name' => 'count_text_singular',
1038 1124 'type' => 'text',
1039 1125 'label' => __( 'Count Text Singular', 'betterdocs' ),
1040 1126 'default' => __( 'Doc', 'betterdocs' ),
1041 - 'priority' => 7
1127 + 'priority' => 8
1042 1128 ),
1043 1129 'exploremore_btn' => array(
1044 1130 'name' => 'exploremore_btn',
1045 1131 'type' => 'toggle',
@@ -1045,9 +1131,9 @@
1045 1131 'type' => 'toggle',
1046 1132 'label' => __( 'Explore More Button', 'betterdocs' ),
1047 1133 'enable_disable_text_active' => true,
1048 1134 'default' => true,
1049 - 'priority' => 8
1135 + 'priority' => 9
1050 1136 ),
1051 1137 'exploremore_btn_txt' => array(
1052 1138 'name' => 'exploremore_btn_txt',
1053 1139 'type' => 'text',
@@ -1052,9 +1138,9 @@
1052 1138 'name' => 'exploremore_btn_txt',
1053 1139 'type' => 'text',
1054 1140 'label' => __( 'Explore More Button Text', 'betterdocs' ),
1055 1141 'default' => __( 'Explore More', 'betterdocs' ),
1056 - 'priority' => 9,
1142 + 'priority' => 10,
1057 1143 'rules' => Rules::is( 'exploremore_btn', true )
1058 1144 ),
1059 1145 'betterdocs_popular_docs_text' => array(
1060 1146 'name' => 'betterdocs_popular_docs_text',
@@ -1060,9 +1146,9 @@
1060 1146 'name' => 'betterdocs_popular_docs_text',
1061 1147 'type' => 'text',
1062 1148 'label' => __( 'Popular Docs Text', 'betterdocs' ),
1063 1149 'default' => __( 'Popular Docs', 'betterdocs' ),
1064 - 'priority' => 10,
1150 + 'priority' => 11,
1065 1151 'is_pro' => true
1066 1152 ),
1067 1153 'betterdocs_popular_docs_number' => array(
1068 1154 'name' => 'betterdocs_popular_docs_number',
@@ -1068,9 +1154,9 @@
1068 1154 'name' => 'betterdocs_popular_docs_number',
1069 1155 'type' => 'number',
1070 1156 'label' => __( 'Popular Docs Number', 'betterdocs' ),
1071 1157 'default' => 10,
1072 - 'priority' => 11,
1158 + 'priority' => 12,
1073 1159 'is_pro' => true
1074 1160 )
1075 1161 )
1076 1162 ),
@@ -1097,8 +1183,9 @@
1097 1183 'default' => '',
1098 1184 'priority' => 2,
1099 1185 'is_pro' => true
1100 1186 ) ),
1187 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- legacy public filter name for category-exclude settings field, retained for back-compat.
1101 1188 'child_category_exclude' => apply_filters( 'child_category_exclude', array(
1102 1189 'name' => 'child_category_exclude',
1103 1190 'type' => 'toggle',
1104 1191 'label' => __( 'Exclude Child Terms In Category Search', 'betterdocs' ),
@@ -1250,8 +1337,9 @@
1250 1337 )
1251 1338 )
1252 1339 )
1253 1340 ),
1341 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- legacy public filter name for single-doc setting section, retained for back-compat.
1254 1342 'layout_single_doc' => apply_filters( 'single_doc_setting_section', array(
1255 1343 'id' => 'layout_single_doc',
1256 1344 'name' => 'layout_single_doc',
1257 1345 'type' => 'section',
@@ -1312,8 +1400,44 @@
1312 1400 'enable_disable_text_active' => true,
1313 1401 'default' => 1,
1314 1402 'priority' => 3
1315 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 + ),
1316 1440 'enable_tags' => array(
1317 1441 'name' => 'enable_tags',
1318 1442 'type' => 'toggle',
1319 1443 'label' => __( 'Tags', 'betterdocs' ),
@@ -1318,9 +1442,9 @@
1318 1442 'type' => 'toggle',
1319 1443 'label' => __( 'Tags', 'betterdocs' ),
1320 1444 'enable_disable_text_active' => true,
1321 1445 'default' => 1,
1322 - 'priority' => 4
1446 + 'priority' => 8
1323 1447 ),
1324 1448 'show_last_update_time' => array(
1325 1449 'name' => 'show_last_update_time',
1326 1450 'type' => 'toggle',
@@ -1326,9 +1450,9 @@
1326 1450 'type' => 'toggle',
1327 1451 'label' => __( 'Last Update Time', 'betterdocs' ),
1328 1452 'enable_disable_text_active' => true,
1329 1453 'default' => 1,
1330 - 'priority' => 5
1454 + 'priority' => 9
1331 1455 ),
1332 1456 'enable_navigation' => array(
1333 1457 'name' => 'enable_navigation',
1334 1458 'type' => 'toggle',
@@ -1334,9 +1458,9 @@
1334 1458 'type' => 'toggle',
1335 1459 'label' => __( 'Navigation', 'betterdocs' ),
1336 1460 'enable_disable_text_active' => true,
1337 1461 'default' => 1,
1338 - 'priority' => 6
1462 + 'priority' => 10
1339 1463 ),
1340 1464 'enable_comment' => array(
1341 1465 'name' => 'enable_comment',
1342 1466 'type' => 'toggle',
@@ -1342,9 +1466,9 @@
1342 1466 'type' => 'toggle',
1343 1467 'label' => __( 'Comment', 'betterdocs' ),
1344 1468 'enable_disable_text_active' => true,
1345 1469 'default' => '',
1346 - 'priority' => 7
1470 + 'priority' => 11
1347 1471 ),
1348 1472 'enable_credit' => array(
1349 1473 'name' => 'enable_credit',
1350 1474 'type' => 'toggle',
@@ -1350,9 +1474,9 @@
1350 1474 'type' => 'toggle',
1351 1475 'label' => __( 'Show Powered by BetterDocs', 'betterdocs' ),
1352 1476 'enable_disable_text_active' => true,
1353 1477 'default' => '',
1354 - 'priority' => 8
1478 + 'priority' => 12
1355 1479 ),
1356 1480 'reaction_feedback_text' => array(
1357 1481 'name' => 'reaction_feedback_text',
1358 1482 'type' => 'text',
@@ -1357,9 +1481,9 @@
1357 1481 'name' => 'reaction_feedback_text',
1358 1482 'type' => 'text',
1359 1483 'label' => __( 'Reaction Feedback Text', 'betterdocs' ),
1360 1484 'default' => __( 'Thanks for your feedback.', 'betterdocs' ),
1361 - 'priority' => 9
1485 + 'priority' => 13
1362 1486 ),
1363 1487 'enable_estimated_reading_time' => array(
1364 1488 'name' => 'enable_estimated_reading_time',
1365 1489 'type' => 'toggle',
@@ -1365,9 +1489,9 @@
1365 1489 'type' => 'toggle',
1366 1490 'label' => __( 'Estimated Reading Time', 'betterdocs' ),
1367 1491 'enable_disable_text_active' => true,
1368 1492 'default' => 0,
1369 - 'priority' => 10
1493 + 'priority' => 14
1370 1494 ),
1371 1495 'estimated_reading_time_title' => array(
1372 1496 'name' => 'estimated_reading_time_title',
1373 1497 'type' => 'text',
@@ -1372,9 +1496,9 @@
1372 1496 'name' => 'estimated_reading_time_title',
1373 1497 'type' => 'text',
1374 1498 'label' => __( 'Estimated Reading Time Title', 'betterdocs' ),
1375 1499 'default' => '',
1376 - 'priority' => 11,
1500 + 'priority' => 15,
1377 1501 'rules' => Rules::is( 'enable_estimated_reading_time', true )
1378 1502 ),
1379 1503 'estimated_reading_time_text' => array(
1380 1504 'name' => 'estimated_reading_time_text',
@@ -1380,9 +1504,9 @@
1380 1504 'name' => 'estimated_reading_time_text',
1381 1505 'type' => 'text',
1382 1506 'label' => __( 'Estimated Reading Time Text', 'betterdocs' ),
1383 1507 'default' => __( 'min read', 'betterdocs' ),
1384 - 'priority' => 12,
1508 + 'priority' => 16,
1385 1509 'rules' => Rules::is( 'enable_estimated_reading_time', true )
1386 1510 ),
1387 1511 'singular_estimated_reading_time_text' => array(
1388 1512 'name' => 'singular_estimated_reading_time_text',
@@ -1388,9 +1512,9 @@
1388 1512 'name' => 'singular_estimated_reading_time_text',
1389 1513 'type' => 'text',
1390 1514 'label' => __( 'Estimated Reading Time Text Singular', 'betterdocs' ),
1391 1515 'default' => __( 'min read', 'betterdocs' ),
1392 - 'priority' => 13,
1516 + 'priority' => 17,
1393 1517 'rules' => Rules::is( 'enable_estimated_reading_time', true )
1394 1518 )
1395 1519 )
1396 1520 ),
@@ -2270,18 +2394,84 @@
2270 2394 'open-ai-settings' => array(
2271 2395 'id' => 'open-ai-settings',
2272 2396 'name' => 'open-ai-settings',
2273 2397 'type' => 'section',
2274 - 'label' => __( 'API Settings', 'betterdocs' ),
2398 + 'label' => __( 'AI Platform & API', 'betterdocs' ),
2275 2399 'priority' => 1,
2276 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.
2277 2420 'ai_autowrite_api_key' => array(
2278 2421 'name' => 'ai_autowrite_api_key',
2279 2422 'type' => 'text',
2280 - 'label' => __( 'API Key', 'betterdocs' ),
2281 - '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/' ) ),
2282 2425 'default' => '',
2283 - '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()
2284 2474 )
2285 2475 )
2286 2476 ),
2287 2477 'write-with-ai' => array(
@@ -2290,8 +2480,39 @@
2290 2480 'type' => 'section',
2291 2481 'label' => __( 'Write with AI', 'betterdocs' ),
2292 2482 'priority' => 5,
2293 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(
2294 2515 'enable_write_with_ai' => array(
2295 2516 'name' => 'enable_write_with_ai',
2296 2517 'type' => 'toggle',
2297 2518 'priority' => 0,
@@ -2315,29 +2536,20 @@
2315 2536 'priority' => 6,
2316 2537 'label' => __( 'Write Glossaries with AI', 'betterdocs' ),
2317 2538 'label_subtitle' => __( 'Generate AI based Glossary definitions from the Glossaries admin page', 'betterdocs' ),
2318 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,
2319 2550 'default' => true
2320 2551 ),
2321 - 'write_with_ai_model' => array(
2322 - 'name' => 'write_with_ai_model',
2323 - 'type' => 'select',
2324 - 'label' => __( 'OpenAI Model*', 'betterdocs' ),
2325 - 'priority' => 20,
2326 - 'multiple' => false,
2327 - 'default' => 'gpt-4o-mini',
2328 - 'options' => GlobalFields::normalize_fields( array(
2329 - 'gpt-4o-mini' => 'GPT-4o Mini',
2330 - 'gpt-4o' => 'GPT-4o',
2331 - 'gpt-4.1-nano' => 'GPT-4.1 Nano',
2332 - 'gpt-4.1-mini' => 'GPT-4.1 Mini',
2333 - 'gpt-4.1' => 'GPT-4.1',
2334 - 'gpt-5-nano' => 'GPT-5 Nano',
2335 - 'gpt-5-mini' => 'GPT-5 Mini',
2336 - 'gpt-5' => 'GPT-5',
2337 - 'gpt-5.5' => 'GPT-5.5'
2338 - ) )
2339 - ),
2340 2552 'ai_autowrite_max_token' => array(
2341 2553 'name' => 'ai_autowrite_max_token',
2342 2554 'type' => 'min_token_number',
2343 2555 'label' => __( 'Set Max Tokens', 'betterdocs' ),
@@ -2344,13 +2556,57 @@
2344 2556 'label_subtitle' => sprintf( // translators: %s is a link to more information about token limits.
2345 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' ) ),
2346 2558 'default' => 2500,
2347 2559 'priority' => 10,
2348 - 'model_field' => 'write_with_ai_model',
2560 + 'model_field' => 'ai_model',
2349 2561 'context' => 'write_with_ai',
2350 2562 'min_token_map' => AIHelper::get_min_tokens_map( 'write_with_ai' ),
2351 2563 'classes' => 'wprf-type-text'
2352 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 + )
2353 2609 )
2354 2610 ),
2355 2611 'article-summary' => array(
2356 2612 'id' => 'article-summary',
@@ -2375,31 +2631,12 @@
2375 2631 'label_subtitle' => sprintf( // translators: %s is a link to more information about token limits.
2376 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' ) ),
2377 2633 'default' => 1500,
2378 2634 'priority' => 5,
2379 - 'model_field' => 'article_summary_model',
2635 + 'model_field' => 'ai_model',
2380 2636 'context' => 'article_summary',
2381 2637 'min_token_map' => AIHelper::get_min_tokens_map( 'article_summary' ),
2382 2638 'classes' => 'wprf-type-text'
2383 - ),
2384 - 'article_summary_model' => array(
2385 - 'name' => 'article_summary_model',
2386 - 'type' => 'select',
2387 - 'label' => __( 'OpenAI Model*', 'betterdocs' ),
2388 - 'priority' => 10,
2389 - 'multiple' => false,
2390 - 'default' => 'gpt-4o-mini',
2391 - 'options' => GlobalFields::normalize_fields( array(
2392 - 'gpt-4o-mini' => 'GPT-4o Mini',
2393 - 'gpt-4o' => 'GPT-4o',
2394 - 'gpt-4.1-nano' => 'GPT-4.1 Nano',
2395 - 'gpt-4.1-mini' => 'GPT-4.1 Mini',
2396 - 'gpt-4.1' => 'GPT-4.1',
2397 - 'gpt-5-nano' => 'GPT-5 Nano',
2398 - 'gpt-5-mini' => 'GPT-5 Mini',
2399 - 'gpt-5' => 'GPT-5',
2400 - 'gpt-5.5' => 'GPT-5.5'
2401 - ) )
2402 2639 )
2403 2640 )
2404 2641 )
2405 2642 )