PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.5
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.5
4.9.5 4.9.4 4.9.3 4.9.2 4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 All 203 releases
← All changes | includes/Core/Settings.php +563 -100 4.5.3 → 4.9.5 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,
@@ -208,8 +239,12 @@
208 239 'category_slug' => 'docs-category',
209 240 'tag_slug' => 'docs-tag',
210 241 'permalink_structure' => 'docs/',
211 242 'enable_faq_schema' => false,
243 + 'analytics_from' => 'everyone',
244 + 'unique_visitor_count' => true,
245 + 'exclude_bot_analytics' => true,
246 + 'child_category_exclude' => false,
212 247 'live_search' => true,
213 248 'advance_search' => false,
214 249 'popular_keyword_limit' => 5,
215 250 'search_letter_limit' => 3,
@@ -252,8 +287,12 @@
252 287 'enable_breadcrumb_category' => true,
253 288 'enable_breadcrumb_title' => true,
254 289 'enable_sidebar_cat_list' => true,
255 290 'enable_print_icon' => true,
291 + 'print_enable_logo' => false,
292 + 'print_logo' => array(),
293 + 'print_enable_footer' => false,
294 + 'print_footer_text' => '',
256 295 'enable_tags' => true,
257 296 'email_feedback' => true,
258 297 'feedback_link_text' => __( 'Still stuck? How can we help?', 'betterdocs' ),
259 298 'reaction_feedback_text' => __( 'Thanks for your feedback', 'betterdocs' ),
@@ -266,8 +305,9 @@
266 305 'enable_credit' => false,
267 306 'enable_archive_sidebar' => true,
268 307 'archive_nested_subcategory' => true,
269 308 'archive_enable_pagination' => false,
309 + 'archive_lazy_load_descendants' => false,
270 310 'enable_content_restriction' => false,
271 311 'enable_reporting' => false,
272 312 'enable_sample_data' => false,
273 313 'reporting_day' => 'monday',
@@ -274,14 +314,34 @@
274 314 'reporting_email' => get_option( 'admin_email' ),
275 315 'enable_write_with_ai' => true,
276 316 'enable_faq_write_with_ai' => true,
277 317 'enable_glossaries_write_with_ai' => true,
318 + 'enable_docs_ai_suite' => true,
278 319 'write_with_ai_model' => 'gpt-4o-mini',
279 320 'ai_autowrite_api_key' => '',
280 321 'ai_autowrite_max_token' => 2500,
322 + 'write_with_ai_instructions' => array(
323 + array(
324 + 'id' => 'default',
325 + 'title' => __( 'Default/Core', 'betterdocs' ),
326 + 'content' => WriteWithAI::default_instruction_content()
327 + )
328 + ),
329 + 'ai_edit_actions' => AIEdit::default_actions(),
281 330 'enable_article_summary' => false,
282 331 'article_summary_model' => 'gpt-4o-mini',
283 332 'article_summary_max_token' => 1500,
333 + // Multi-platform AI (content suite). `ai_platform` selects the active
334 + // provider; `ai_model` is the single global model; keys are stored
335 + // per platform so switching never loses a saved key. OpenAI reuses
336 + // `ai_autowrite_api_key` above — it has always held an OpenAI key,
337 + // so nothing needs migrating.
338 + 'ai_platform' => 'openai',
339 + 'ai_model' => 'gpt-4o-mini',
340 + 'ai_api_key_gemini' => '',
341 + 'ai_api_key_claude' => '',
342 + 'ai_api_key_deepseek' => '',
343 + 'ai_api_key_openrouter' => '',
284 344 'enable_estimated_reading_time' => true,
285 345 'enable_encyclopedia' => false,
286 346 'enable_glossaries' => false,
287 347 'show_glossary_suggestions' => true,
@@ -291,9 +351,40 @@
291 351 'singular_estimated_reading_time_text' => __( 'min read', 'betterdocs' ),
292 352 'betterdocs_access_control_repeater' => array(),
293 353 'internal_knowledge_base_type' => 'basic',
294 354 'betterdocs_access_control_repeater_kb' => array(),
295 - 'enable_git_integration' => false
355 + 'enable_git_integration' => false,
356 + 'enable_ai_actions' => true,
357 + 'enable_markdown_endpoint' => true,
358 + 'ai_actions_button_label' => __( 'Copy page', 'betterdocs' ),
359 + 'ai_actions_prompt_template' => __( 'Read from {URL} so I can ask questions about it.', 'betterdocs' ),
360 + 'ai_actions_copy_page' => true,
361 + 'ai_actions_view_markdown' => true,
362 + 'ai_actions_chatgpt' => true,
363 + 'ai_actions_claude' => true,
364 + 'ai_actions_gemini' => true,
365 + 'ai_actions_perplexity' => false,
366 + 'ai_actions_grok' => false,
367 + 'enable_listen' => true,
368 + 'listen_button_label' => __( 'Listen', 'betterdocs' ),
369 + 'listen_show_speed' => true,
370 + // Words a synthetic voice gets through in a minute at rate 1.
371 + // Deliberately lower than the 200 the reading-time pill assumes — that
372 + // number describes silent reading. Only the player's duration estimate
373 + // uses this; see Core\Listen::WORDS_PER_MINUTE.
374 + 'listen_words_per_minute' => 180,
375 + /**
376 + * MCP master switch. Off by default; the toggle lives on the
377 + * BetterDocs → MCP page, not in the settings tree, and writes
378 + * through POST betterdocs/v1/settings.
379 + *
380 + * The key has to be listed here or `get()` cannot see it at all:
381 + * it answers `$default` for anything absent from the defaults
382 + * array, whatever the stored option holds.
383 + *
384 + * @since 4.9.0
385 + */
386 + 'enable_mcp' => ''
296 387 );
297 388
298 389 $_default = apply_filters( 'betterdocs_default_settings', $_default );
299 390 // $_default = apply_filters_deprecated(
@@ -425,9 +516,10 @@
425 516 wp_send_json_error();
426 517 }
427 518
428 519 if ( isset( $_POST[ 'mode' ] ) ) {
429 - if ( $this->save( 'dark_mode', rest_sanitize_boolean( $_POST[ 'mode' ] ) ) ) { // phpcs:ignore
520 + $mode = sanitize_text_field( wp_unslash( $_POST[ 'mode' ] ) );
521 + if ( $this->save( 'dark_mode', rest_sanitize_boolean( $mode ) ) ) {
430 522 wp_send_json_success();
431 523 }
432 524 }
433 525
@@ -585,19 +677,27 @@
585 677 }
586 678
587 679 $_old_settings = $this->database->get( $this->base_key, $this->get_default() );
588 680
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' );
681 + // The frontend only ever sees masked API keys. A submitted value that still looks like a
682 + // mask (contains a run of asterisks — no real key does) means "unchanged": restore the
683 + // stored original so a mask string is never persisted. Guarding on the mask SHAPE rather
684 + // than strict equality with mask(stored) closes the corruption loop QA hit: once a mask
685 + // slips into storage, mask(mask) === mask, so an equality-only guard would faithfully
686 + // preserve the corrupted value forever (round-2 follow-up #1). When the stored value is
687 + // itself mask-shaped it is unrecoverable — clear it so the UI and the GeoIP/GA4 status
688 + // surfaces honestly report a missing key instead of failing downstream with 401s.
689 + $sensitive_api_keys = self::sensitive_api_key_fields();
593 690 foreach ( $sensitive_api_keys as $api_key_field ) {
594 691 if ( ! isset( $settings[ $api_key_field ] ) ) {
595 692 continue;
596 693 }
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;
694 + $incoming = trim( (string) $settings[ $api_key_field ] );
695 + $stored = isset( $_old_settings[ $api_key_field ] ) ? (string) $_old_settings[ $api_key_field ] : '';
696 + $stored_is_mask = '' !== $stored && preg_match( '/\*{4,}/', $stored );
697 +
698 + if ( '' !== $incoming && preg_match( '/\*{4,}/', $incoming ) ) {
699 + $settings[ $api_key_field ] = $stored_is_mask ? '' : $stored;
600 700 }
601 701 }
602 702
603 703 // Minimum-token policy: reject saves where a feature's max_token field is below the
@@ -612,9 +712,9 @@
612 712 array(
613 713 'tab' => 'tab-betterdocs-ai',
614 714 'context' => 'write_with_ai',
615 715 'token_key' => 'ai_autowrite_max_token',
616 - 'model_key' => 'write_with_ai_model',
716 + 'model_key' => 'ai_model',
617 717 'label' => __( 'Write with AI', 'betterdocs' )
618 718 ),
619 719 array(
620 720 'tab' => 'tab-betterdocs-ai',
@@ -619,9 +719,9 @@
619 719 array(
620 720 'tab' => 'tab-betterdocs-ai',
621 721 'context' => 'article_summary',
622 722 'token_key' => 'article_summary_max_token',
623 - 'model_key' => 'article_summary_model',
723 + 'model_key' => 'ai_model',
624 724 'label' => __( 'AI Doc Summarizer', 'betterdocs' )
625 725 )
626 726 );
627 727 foreach ( $token_pairs as $pair ) {
@@ -656,21 +756,38 @@
656 756 betterdocs()->kbmigration->migrate();
657 757 }
658 758 $_settings = wp_parse_args( $_normalized_settings, $_old_settings );
659 759
660 - // Check if there are actual changes before saving.
661 - // update_option returns false when values serialize identically, which can happen
662 - // due to object caching or type normalization even when user made changes.
663 - $_has_changes = $_settings != $_old_settings;
760 + // Detect whether this save actually changes the effective settings.
761 + //
762 + // The stored option and the submitted payload are normalized differently:
763 + // an optional field can be ABSENT from storage yet arrive as '' (e.g.
764 + // Feedback URL), and array fields can be stored empty ( [] ) while their
765 + // normalized/default form is non-empty (e.g. Instant Answer's
766 + // display_ia_texonomy defaults to ['all']). Comparing the raw arrays
767 + // ( $_settings != $_old_settings ) therefore reported a phantom change on
768 + // every save, leaving the tab perpetually "dirty" and always toasting
769 + // "Changes Saved Successfully." instead of "There are no changes to be
770 + // saved." — see WPDevelopers/betterdocs-pro#78.
771 + //
772 + // Compare like-for-like instead: fill defaults on both sides and run both
773 + // through the same normalization, so semantically-equal states (absent vs
774 + // '', [] vs ['all'], 'on' vs true) collapse to identical values and only a
775 + // real edit registers. This is also more reliable than update_option()'s
776 + // return, which is false whenever values serialize identically under object
777 + // caching or type coercion even when the user did change something (#49).
778 + $_defaults = array_merge( $this->get_default(), $this->get_pro_defaults() );
779 + $_old_normalized = $this->get_normalized_values( wp_parse_args( $_old_settings, $_defaults ), $_defaults );
780 + $_new_normalized = $this->get_normalized_values( wp_parse_args( $_settings, $_defaults ), $_defaults );
781 + $_has_changes = $_new_normalized != $_old_normalized;
664 782
665 783 $_saved = $this->database->save( $this->base_key, $_settings );
666 784
667 785 do_action_ref_array( 'betterdocs::settings::saved', array( $_saved, $_settings, $_old_settings, &$this ) );
668 786
669 - // Return true if save succeeded OR if there were changes to attempt saving.
670 - // This handles cases where update_option returns false due to identical serialization
671 - // (e.g., object caching, type coercion during serialization).
672 - return $_saved || $_has_changes;
787 + // The success / no-changes toast reflects whether the user made a real
788 + // change, not update_option()'s (unreliable) return value.
789 + return $_has_changes;
673 790 }
674 791
675 792 public function views( $hook ) {
676 793 return betterdocs()->views->get( 'admin/settings' );
@@ -868,10 +985,9 @@
868 985 'guests' => __( 'Guests Only', 'betterdocs' ),
869 986 'registered_users' => __( 'Registered Users Only', 'betterdocs' )
870 987 ) ),
871 988 'default' => 'everyone',
872 - 'priority' => 13,
873 - 'is_pro' => true
989 + 'priority' => 13
874 990 ),
875 991 'unique_visitor_count' => array(
876 992 'name' => 'unique_visitor_count',
877 993 'type' => 'toggle',
@@ -876,11 +992,10 @@
876 992 'name' => 'unique_visitor_count',
877 993 'type' => 'toggle',
878 994 'label' => __( 'Unique Visitor Count', 'betterdocs' ),
879 995 'enable_disable_text_active' => true,
880 - 'default' => false,
881 - 'priority' => 14,
882 - 'is_pro' => true
996 + 'default' => true,
997 + 'priority' => 14
883 998 ),
884 999 'exclude_bot_analytics' => array(
885 1000 'name' => 'exclude_bot_analytics',
886 1001 'type' => 'toggle',
@@ -885,11 +1000,10 @@
885 1000 'name' => 'exclude_bot_analytics',
886 1001 'type' => 'toggle',
887 1002 'label' => __( 'Exclude Bot Analytics', 'betterdocs' ),
888 1003 'enable_disable_text_active' => true,
889 - 'default' => false,
890 - 'priority' => 15,
891 - 'is_pro' => true
1004 + 'default' => true,
1005 + 'priority' => 15
892 1006 )
893 1007
894 1008 )
895 1009 ) )
@@ -993,8 +1107,17 @@
993 1107 'enable_disable_text_active' => true,
994 1108 'default' => 1,
995 1109 'priority' => 1
996 1110 ),
1111 + 'archive_lazy_load_descendants' => array(
1112 + 'name' => 'archive_lazy_load_descendants',
1113 + 'type' => 'toggle',
1114 + 'label' => __( 'Lazy Load Sidebar Docs & Subcategories', 'betterdocs' ),
1115 + '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' ),
1116 + 'enable_disable_text_active' => true,
1117 + 'default' => false,
1118 + 'priority' => 2
1119 + ),
997 1120 'nested_subcategory' => array(
998 1121 'name' => 'nested_subcategory',
999 1122 'type' => 'toggle',
1000 1123 'label' => __( 'Nested Sub Category', 'betterdocs' ),
@@ -999,9 +1122,9 @@
999 1122 'type' => 'toggle',
1000 1123 'label' => __( 'Nested Sub Category', 'betterdocs' ),
1001 1124 'enable_disable_text_active' => true,
1002 1125 'default' => '',
1003 - 'priority' => 2
1126 + 'priority' => 3
1004 1127 ),
1005 1128 'column_number' => array(
1006 1129 'name' => 'column_number',
1007 1130 'type' => 'number',
@@ -1007,9 +1130,9 @@
1007 1130 'type' => 'number',
1008 1131 'label' => __( 'Number Of Columns', 'betterdocs' ),
1009 1132 'label_subtitle' => __( 'This setting is not applicable for sleek layout.', 'betterdocs' ),
1010 1133 'default' => 3,
1011 - 'priority' => 3
1134 + 'priority' => 4
1012 1135 ),
1013 1136 'posts_number' => apply_filters( 'betterdocs_posts_number', array(
1014 1137 'name' => 'posts_number',
1015 1138 'type' => 'number',
@@ -1015,9 +1138,9 @@
1015 1138 'type' => 'number',
1016 1139 'label' => __( 'Number Of Docs', 'betterdocs' ),
1017 1140 'label_subtitle' => __( 'This setting is not applicable for handbook layout.', 'betterdocs' ),
1018 1141 'default' => 10,
1019 - 'priority' => 4
1142 + 'priority' => 5
1020 1143 ) ),
1021 1144 'post_count' => array(
1022 1145 'name' => 'post_count',
1023 1146 'type' => 'toggle',
@@ -1023,9 +1146,9 @@
1023 1146 'type' => 'toggle',
1024 1147 'label' => __( 'Doc Count', 'betterdocs' ),
1025 1148 'enable_disable_text_active' => true,
1026 1149 'default' => 1,
1027 - 'priority' => 5
1150 + 'priority' => 6
1028 1151 ),
1029 1152 'count_text' => array(
1030 1153 'name' => 'count_text',
1031 1154 'type' => 'text',
@@ -1030,9 +1153,9 @@
1030 1153 'name' => 'count_text',
1031 1154 'type' => 'text',
1032 1155 'label' => __( 'Count Text', 'betterdocs' ),
1033 1156 'default' => __( 'Docs', 'betterdocs' ),
1034 - 'priority' => 6
1157 + 'priority' => 7
1035 1158 ),
1036 1159 'count_text_singular' => array(
1037 1160 'name' => 'count_text_singular',
1038 1161 'type' => 'text',
@@ -1037,9 +1160,9 @@
1037 1160 'name' => 'count_text_singular',
1038 1161 'type' => 'text',
1039 1162 'label' => __( 'Count Text Singular', 'betterdocs' ),
1040 1163 'default' => __( 'Doc', 'betterdocs' ),
1041 - 'priority' => 7
1164 + 'priority' => 8
1042 1165 ),
1043 1166 'exploremore_btn' => array(
1044 1167 'name' => 'exploremore_btn',
1045 1168 'type' => 'toggle',
@@ -1045,9 +1168,9 @@
1045 1168 'type' => 'toggle',
1046 1169 'label' => __( 'Explore More Button', 'betterdocs' ),
1047 1170 'enable_disable_text_active' => true,
1048 1171 'default' => true,
1049 - 'priority' => 8
1172 + 'priority' => 9
1050 1173 ),
1051 1174 'exploremore_btn_txt' => array(
1052 1175 'name' => 'exploremore_btn_txt',
1053 1176 'type' => 'text',
@@ -1052,9 +1175,9 @@
1052 1175 'name' => 'exploremore_btn_txt',
1053 1176 'type' => 'text',
1054 1177 'label' => __( 'Explore More Button Text', 'betterdocs' ),
1055 1178 'default' => __( 'Explore More', 'betterdocs' ),
1056 - 'priority' => 9,
1179 + 'priority' => 10,
1057 1180 'rules' => Rules::is( 'exploremore_btn', true )
1058 1181 ),
1059 1182 'betterdocs_popular_docs_text' => array(
1060 1183 'name' => 'betterdocs_popular_docs_text',
@@ -1060,9 +1183,9 @@
1060 1183 'name' => 'betterdocs_popular_docs_text',
1061 1184 'type' => 'text',
1062 1185 'label' => __( 'Popular Docs Text', 'betterdocs' ),
1063 1186 'default' => __( 'Popular Docs', 'betterdocs' ),
1064 - 'priority' => 10,
1187 + 'priority' => 11,
1065 1188 'is_pro' => true
1066 1189 ),
1067 1190 'betterdocs_popular_docs_number' => array(
1068 1191 'name' => 'betterdocs_popular_docs_number',
@@ -1068,9 +1191,9 @@
1068 1191 'name' => 'betterdocs_popular_docs_number',
1069 1192 'type' => 'number',
1070 1193 'label' => __( 'Popular Docs Number', 'betterdocs' ),
1071 1194 'default' => 10,
1072 - 'priority' => 11,
1195 + 'priority' => 12,
1073 1196 'is_pro' => true
1074 1197 )
1075 1198 )
1076 1199 ),
@@ -1097,16 +1220,16 @@
1097 1220 'default' => '',
1098 1221 'priority' => 2,
1099 1222 'is_pro' => true
1100 1223 ) ),
1224 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- legacy public filter name for category-exclude settings field, retained for back-compat.
1101 1225 'child_category_exclude' => apply_filters( 'child_category_exclude', array(
1102 1226 'name' => 'child_category_exclude',
1103 1227 'type' => 'toggle',
1104 1228 'label' => __( 'Exclude Child Terms In Category Search', 'betterdocs' ),
1105 1229 'enable_disable_text_active' => true,
1106 - 'default' => '',
1107 - 'priority' => 3,
1108 - 'is_pro' => true
1230 + 'default' => false,
1231 + 'priority' => 3
1109 1232 ) ),
1110 1233 'popular_keyword_limit' => apply_filters( 'betterdocs_popular_keyword_limit_settings', array(
1111 1234 'name' => 'popular_keyword_limit',
1112 1235 'type' => 'number',
@@ -1250,8 +1373,9 @@
1250 1373 )
1251 1374 )
1252 1375 )
1253 1376 ),
1377 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- legacy public filter name for single-doc setting section, retained for back-compat.
1254 1378 'layout_single_doc' => apply_filters( 'single_doc_setting_section', array(
1255 1379 'id' => 'layout_single_doc',
1256 1380 'name' => 'layout_single_doc',
1257 1381 'type' => 'section',
@@ -1312,8 +1436,44 @@
1312 1436 'enable_disable_text_active' => true,
1313 1437 'default' => 1,
1314 1438 'priority' => 3
1315 1439 ),
1440 + 'print_enable_logo' => array(
1441 + 'name' => 'print_enable_logo',
1442 + 'type' => 'toggle',
1443 + 'label' => __( 'Logo on Printed Doc', 'betterdocs' ),
1444 + 'label_subtitle' => __( 'Show a logo at the top of the printed / PDF page', 'betterdocs' ),
1445 + 'enable_disable_text_active' => true,
1446 + 'default' => 0,
1447 + 'priority' => 4
1448 + ),
1449 + 'print_logo' => array(
1450 + 'name' => 'print_logo',
1451 + 'type' => 'media',
1452 + 'value' => '',
1453 + 'label' => __( 'Print Logo', 'betterdocs' ),
1454 + 'label_subtitle' => __( 'Leave empty to use your site logo, or the site icon when no site logo is set', 'betterdocs' ),
1455 + 'priority' => 5,
1456 + 'rules' => Rules::is( 'print_enable_logo', true )
1457 + ),
1458 + 'print_enable_footer' => array(
1459 + 'name' => 'print_enable_footer',
1460 + 'type' => 'toggle',
1461 + 'label' => __( 'Footer on Printed Doc', 'betterdocs' ),
1462 + 'label_subtitle' => __( 'Show a footer on every page of the printed / PDF document', 'betterdocs' ),
1463 + 'enable_disable_text_active' => true,
1464 + 'default' => 0,
1465 + 'priority' => 6
1466 + ),
1467 + 'print_footer_text' => array(
1468 + 'name' => 'print_footer_text',
1469 + 'type' => 'textarea',
1470 + 'label' => __( 'Print Footer Text', 'betterdocs' ),
1471 + 'label_subtitle' => __( 'Leave empty to use the site name and current year', 'betterdocs' ),
1472 + 'default' => '',
1473 + 'priority' => 7,
1474 + 'rules' => Rules::is( 'print_enable_footer', true )
1475 + ),
1316 1476 'enable_tags' => array(
1317 1477 'name' => 'enable_tags',
1318 1478 'type' => 'toggle',
1319 1479 'label' => __( 'Tags', 'betterdocs' ),
@@ -1318,9 +1478,9 @@
1318 1478 'type' => 'toggle',
1319 1479 'label' => __( 'Tags', 'betterdocs' ),
1320 1480 'enable_disable_text_active' => true,
1321 1481 'default' => 1,
1322 - 'priority' => 4
1482 + 'priority' => 8
1323 1483 ),
1324 1484 'show_last_update_time' => array(
1325 1485 'name' => 'show_last_update_time',
1326 1486 'type' => 'toggle',
@@ -1326,9 +1486,9 @@
1326 1486 'type' => 'toggle',
1327 1487 'label' => __( 'Last Update Time', 'betterdocs' ),
1328 1488 'enable_disable_text_active' => true,
1329 1489 'default' => 1,
1330 - 'priority' => 5
1490 + 'priority' => 9
1331 1491 ),
1332 1492 'enable_navigation' => array(
1333 1493 'name' => 'enable_navigation',
1334 1494 'type' => 'toggle',
@@ -1334,9 +1494,9 @@
1334 1494 'type' => 'toggle',
1335 1495 'label' => __( 'Navigation', 'betterdocs' ),
1336 1496 'enable_disable_text_active' => true,
1337 1497 'default' => 1,
1338 - 'priority' => 6
1498 + 'priority' => 10
1339 1499 ),
1340 1500 'enable_comment' => array(
1341 1501 'name' => 'enable_comment',
1342 1502 'type' => 'toggle',
@@ -1342,9 +1502,9 @@
1342 1502 'type' => 'toggle',
1343 1503 'label' => __( 'Comment', 'betterdocs' ),
1344 1504 'enable_disable_text_active' => true,
1345 1505 'default' => '',
1346 - 'priority' => 7
1506 + 'priority' => 11
1347 1507 ),
1348 1508 'enable_credit' => array(
1349 1509 'name' => 'enable_credit',
1350 1510 'type' => 'toggle',
@@ -1350,9 +1510,9 @@
1350 1510 'type' => 'toggle',
1351 1511 'label' => __( 'Show Powered by BetterDocs', 'betterdocs' ),
1352 1512 'enable_disable_text_active' => true,
1353 1513 'default' => '',
1354 - 'priority' => 8
1514 + 'priority' => 12
1355 1515 ),
1356 1516 'reaction_feedback_text' => array(
1357 1517 'name' => 'reaction_feedback_text',
1358 1518 'type' => 'text',
@@ -1357,9 +1517,9 @@
1357 1517 'name' => 'reaction_feedback_text',
1358 1518 'type' => 'text',
1359 1519 'label' => __( 'Reaction Feedback Text', 'betterdocs' ),
1360 1520 'default' => __( 'Thanks for your feedback.', 'betterdocs' ),
1361 - 'priority' => 9
1521 + 'priority' => 13
1362 1522 ),
1363 1523 'enable_estimated_reading_time' => array(
1364 1524 'name' => 'enable_estimated_reading_time',
1365 1525 'type' => 'toggle',
@@ -1365,9 +1525,9 @@
1365 1525 'type' => 'toggle',
1366 1526 'label' => __( 'Estimated Reading Time', 'betterdocs' ),
1367 1527 'enable_disable_text_active' => true,
1368 1528 'default' => 0,
1369 - 'priority' => 10
1529 + 'priority' => 14
1370 1530 ),
1371 1531 'estimated_reading_time_title' => array(
1372 1532 'name' => 'estimated_reading_time_title',
1373 1533 'type' => 'text',
@@ -1372,9 +1532,9 @@
1372 1532 'name' => 'estimated_reading_time_title',
1373 1533 'type' => 'text',
1374 1534 'label' => __( 'Estimated Reading Time Title', 'betterdocs' ),
1375 1535 'default' => '',
1376 - 'priority' => 11,
1536 + 'priority' => 15,
1377 1537 'rules' => Rules::is( 'enable_estimated_reading_time', true )
1378 1538 ),
1379 1539 'estimated_reading_time_text' => array(
1380 1540 'name' => 'estimated_reading_time_text',
@@ -1380,9 +1540,9 @@
1380 1540 'name' => 'estimated_reading_time_text',
1381 1541 'type' => 'text',
1382 1542 'label' => __( 'Estimated Reading Time Text', 'betterdocs' ),
1383 1543 'default' => __( 'min read', 'betterdocs' ),
1384 - 'priority' => 12,
1544 + 'priority' => 16,
1385 1545 'rules' => Rules::is( 'enable_estimated_reading_time', true )
1386 1546 ),
1387 1547 'singular_estimated_reading_time_text' => array(
1388 1548 'name' => 'singular_estimated_reading_time_text',
@@ -1388,9 +1548,9 @@
1388 1548 'name' => 'singular_estimated_reading_time_text',
1389 1549 'type' => 'text',
1390 1550 'label' => __( 'Estimated Reading Time Text Singular', 'betterdocs' ),
1391 1551 'default' => __( 'min read', 'betterdocs' ),
1392 - 'priority' => 13,
1552 + 'priority' => 17,
1393 1553 'rules' => Rules::is( 'enable_estimated_reading_time', true )
1394 1554 )
1395 1555 )
1396 1556 ),
@@ -1657,8 +1817,202 @@
1657 1817 'enable_disable_text_active' => true,
1658 1818 'default' => false
1659 1819 )
1660 1820 ) )
1821 + ),
1822 + 'layout_single_doc_ai_actions' => array(
1823 + 'id' => 'layout_single_doc_ai_actions',
1824 + 'name' => 'layout_single_doc_ai_actions',
1825 + 'type' => 'section',
1826 + 'label' => __( 'AI Actions', 'betterdocs' ),
1827 + 'priority' => 8,
1828 + 'fields' => apply_filters( 'betterdocs_single_doc_ai_actions', array(
1829 + 'enable_ai_actions' => array(
1830 + 'name' => 'enable_ai_actions',
1831 + 'type' => 'toggle',
1832 + 'priority' => 1,
1833 + 'label' => __( 'Enable AI Actions', 'betterdocs' ),
1834 + 'label_subtitle' => __( 'Show a "Copy page" button beside the doc title so readers can copy the page as Markdown or open it in an AI assistant.', 'betterdocs' ),
1835 + 'enable_disable_text_active' => true,
1836 + 'default' => true
1837 + ),
1838 + 'enable_markdown_endpoint' => array(
1839 + 'name' => 'enable_markdown_endpoint',
1840 + 'type' => 'toggle',
1841 + 'priority' => 2,
1842 + 'label' => __( 'Serve Docs as Markdown', 'betterdocs' ),
1843 + // Plain text, not `<doc-url>.md`: the subtitle is rendered as HTML, so an
1844 + // angle-bracketed placeholder is swallowed as an unknown tag and the
1845 + // sentence read "Publish every doc at .md".
1846 + 'label_subtitle' => __( 'Publish every doc as Markdown at its address plus .md (for example /docs/getting-started.md), so AI assistants can read a clean copy. The Listen player uses it too. Required by "Copy page", "View as Markdown", and the Claude and Google AI Studio actions.', 'betterdocs' ),
1847 + 'enable_disable_text_active' => true,
1848 + 'default' => true
1849 + ),
1850 + // AI Actions and the Markdown endpoint are deliberately independent: the
1851 + // `.md` copies also serve AI assistants directly and the Listen player.
1852 + // So switching AI Actions off leaves Markdown on — say so where the
1853 + // admin is looking, rather than leaving a lone "Enabled" toggle under a
1854 + // feature that reads as switched off.
1855 + 'ai_actions_markdown_notice' => array(
1856 + 'name' => 'ai_actions_markdown_notice',
1857 + 'type' => 'html',
1858 + 'priority' => 3,
1859 + 'html' => sprintf(
1860 + '<div class="betterdocs-ai-actions-markdown-notice" style="margin:8px 0 0;padding:12px 16px;background:#f0f6ff;border-left:4px solid #5a6bff;border-radius:4px;font-size:13px;color:#202223;line-height:1.5;">%s</div>',
1861 + esc_html__( 'AI Actions is off, but your docs are still published as Markdown (.md) for AI assistants and the Listen player. Turn off "Serve Docs as Markdown" above if you don\'t want that.', 'betterdocs' )
1862 + ),
1863 + 'rules' => Rules::logicalRule( array(
1864 + Rules::is( 'enable_ai_actions', false ),
1865 + Rules::is( 'enable_markdown_endpoint', true )
1866 + ), 'and' )
1867 + ),
1868 + 'ai_actions_button_label' => array(
1869 + 'name' => 'ai_actions_button_label',
1870 + 'type' => 'text',
1871 + 'priority' => 4,
1872 + 'label' => __( 'Button Label', 'betterdocs' ),
1873 + 'default' => __( 'Copy page', 'betterdocs' ),
1874 + 'rules' => Rules::is( 'enable_ai_actions', true )
1875 + ),
1876 + 'ai_actions_prompt_template' => array(
1877 + 'name' => 'ai_actions_prompt_template',
1878 + 'type' => 'text',
1879 + 'priority' => 5,
1880 + 'label' => __( 'AI Prompt Template', 'betterdocs' ),
1881 + 'label_subtitle' => __( 'Sent to the AI assistant when a reader opens this doc there. Use {URL} for the doc address.', 'betterdocs' ),
1882 + 'default' => __( 'Read from {URL} so I can ask questions about it.', 'betterdocs' ),
1883 + 'rules' => Rules::is( 'enable_ai_actions', true )
1884 + ),
1885 + 'ai_actions_copy_page' => array(
1886 + 'name' => 'ai_actions_copy_page',
1887 + 'type' => 'toggle',
1888 + 'priority' => 6,
1889 + 'label' => __( 'Copy Page', 'betterdocs' ),
1890 + 'enable_disable_text_active' => true,
1891 + 'default' => true,
1892 + 'rules' => Rules::logicalRule( array(
1893 + Rules::is( 'enable_ai_actions', true ),
1894 + Rules::is( 'enable_markdown_endpoint', true )
1895 + ), 'and' )
1896 + ),
1897 + 'ai_actions_view_markdown' => array(
1898 + 'name' => 'ai_actions_view_markdown',
1899 + 'type' => 'toggle',
1900 + 'priority' => 7,
1901 + 'label' => __( 'View as Markdown', 'betterdocs' ),
1902 + 'enable_disable_text_active' => true,
1903 + 'default' => true,
1904 + 'rules' => Rules::logicalRule( array(
1905 + Rules::is( 'enable_ai_actions', true ),
1906 + Rules::is( 'enable_markdown_endpoint', true )
1907 + ), 'and' )
1908 + ),
1909 + 'ai_actions_chatgpt' => array(
1910 + 'name' => 'ai_actions_chatgpt',
1911 + 'type' => 'toggle',
1912 + 'priority' => 8,
1913 + 'label' => __( 'Open in ChatGPT', 'betterdocs' ),
1914 + 'enable_disable_text_active' => true,
1915 + 'default' => true,
1916 + 'rules' => Rules::is( 'enable_ai_actions', true )
1917 + ),
1918 + 'ai_actions_claude' => array(
1919 + 'name' => 'ai_actions_claude',
1920 + 'type' => 'toggle',
1921 + 'priority' => 9,
1922 + 'label' => __( 'Open in Claude', 'betterdocs' ),
1923 + 'enable_disable_text_active' => true,
1924 + 'default' => true,
1925 + 'rules' => Rules::logicalRule( array(
1926 + Rules::is( 'enable_ai_actions', true ),
1927 + Rules::is( 'enable_markdown_endpoint', true )
1928 + ), 'and' )
1929 + ),
1930 + 'ai_actions_gemini' => array(
1931 + 'name' => 'ai_actions_gemini',
1932 + 'type' => 'toggle',
1933 + 'priority' => 10,
1934 + 'label' => __( 'Open in Google AI Studio', 'betterdocs' ),
1935 + 'label_subtitle' => __( 'Gemini has no way to receive a prompt from a link, so this opens Google AI Studio instead. A Google account is required.', 'betterdocs' ),
1936 + 'enable_disable_text_active' => true,
1937 + 'default' => true,
1938 + 'rules' => Rules::logicalRule( array(
1939 + Rules::is( 'enable_ai_actions', true ),
1940 + Rules::is( 'enable_markdown_endpoint', true )
1941 + ), 'and' )
1942 + ),
1943 + 'ai_actions_perplexity' => array(
1944 + 'name' => 'ai_actions_perplexity',
1945 + 'type' => 'toggle',
1946 + 'priority' => 11,
1947 + 'label' => __( 'Open in Perplexity', 'betterdocs' ),
1948 + 'enable_disable_text_active' => true,
1949 + 'default' => false,
1950 + 'rules' => Rules::logicalRule( array(
1951 + Rules::is( 'enable_ai_actions', true ),
1952 + Rules::is( 'enable_markdown_endpoint', true )
1953 + ), 'and' )
1954 + ),
1955 + 'ai_actions_grok' => array(
1956 + 'name' => 'ai_actions_grok',
1957 + 'type' => 'toggle',
1958 + 'priority' => 12,
1959 + 'label' => __( 'Open in Grok', 'betterdocs' ),
1960 + 'enable_disable_text_active' => true,
1961 + 'default' => false,
1962 + 'rules' => Rules::logicalRule( array(
1963 + Rules::is( 'enable_ai_actions', true ),
1964 + Rules::is( 'enable_markdown_endpoint', true )
1965 + ), 'and' )
1966 + )
1967 + ) )
1968 + ),
1969 + 'layout_single_doc_listen' => array(
1970 + 'id' => 'layout_single_doc_listen',
1971 + 'name' => 'layout_single_doc_listen',
1972 + 'type' => 'section',
1973 + 'label' => __( 'Listen', 'betterdocs' ),
1974 + 'priority' => 9,
1975 + 'fields' => apply_filters( 'betterdocs_single_doc_listen', array(
1976 + 'enable_listen' => array(
1977 + 'name' => 'enable_listen',
1978 + 'type' => 'toggle',
1979 + 'priority' => 1,
1980 + 'label' => __( 'Enable Listen', 'betterdocs' ),
1981 + 'label_subtitle' => __( 'Show a "Listen" button beside the reading time. Pressed, it turns into a small audio player that reads the doc aloud using the visitor\'s browser — no audio files and no third-party service.', 'betterdocs' ),
1982 + 'enable_disable_text_active' => true,
1983 + 'default' => true
1984 + ),
1985 + 'listen_button_label' => array(
1986 + 'name' => 'listen_button_label',
1987 + 'type' => 'text',
1988 + 'priority' => 2,
1989 + 'label' => __( 'Button Label', 'betterdocs' ),
1990 + 'default' => __( 'Listen', 'betterdocs' ),
1991 + 'rules' => Rules::is( 'enable_listen', true )
1992 + ),
1993 + 'listen_show_speed' => array(
1994 + 'name' => 'listen_show_speed',
1995 + 'type' => 'toggle',
1996 + 'priority' => 3,
1997 + 'label' => __( 'Speed Control', 'betterdocs' ),
1998 + 'label_subtitle' => __( 'Let readers cycle the playback speed between 0.75× and 2×.', 'betterdocs' ),
1999 + 'enable_disable_text_active' => true,
2000 + 'default' => true,
2001 + 'rules' => Rules::is( 'enable_listen', true )
2002 + ),
2003 + 'listen_words_per_minute' => array(
2004 + 'name' => 'listen_words_per_minute',
2005 + 'type' => 'number',
2006 + 'priority' => 4,
2007 + 'label' => __( 'Words Per Minute', 'betterdocs' ),
2008 + 'label_subtitle' => __( 'Used to estimate the total playing time the player shows. Browser voices read at roughly 180 words a minute at normal speed; raise this if the countdown finishes early.', 'betterdocs' ),
2009 + 'default' => 180,
2010 + 'min' => 60,
2011 + 'max' => 400,
2012 + 'rules' => Rules::is( 'enable_listen', true )
2013 + )
2014 + ) )
1661 2015 )
1662 2016 )
1663 2017 )
1664 2018 )
@@ -2270,18 +2624,84 @@
2270 2624 'open-ai-settings' => array(
2271 2625 'id' => 'open-ai-settings',
2272 2626 'name' => 'open-ai-settings',
2273 2627 'type' => 'section',
2274 - 'label' => __( 'API Settings', 'betterdocs' ),
2628 + 'label' => __( 'AI Platform & API', 'betterdocs' ),
2275 2629 'priority' => 1,
2276 2630 'fields' => array(
2631 + 'ai_platform' => array(
2632 + 'name' => 'ai_platform',
2633 + 'type' => 'select',
2634 + 'label' => __( 'AI Platform', 'betterdocs' ),
2635 + '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' ),
2636 + 'priority' => 1,
2637 + 'multiple' => false,
2638 + 'default' => 'openai',
2639 + // DeepSeek and OpenRouter are temporarily hidden from the
2640 + // dropdown. Filter the options here, NOT ModelRegistry::platforms() —
2641 + // sensitive_api_key_fields() loops the registry to mask each
2642 + // platform's key field, so trimming the registry would silently
2643 + // un-mask those keys. Re-enable later by dropping the array_diff_key.
2644 + 'options' => GlobalFields::normalize_fields(
2645 + array_diff_key( ModelRegistry::platforms(), array_flip( array( 'deepseek', 'openrouter' ) ) )
2646 + )
2647 + ),
2648 + // OpenAI's key field keeps its original name so installs
2649 + // that already saved a Write with AI key keep it.
2277 2650 'ai_autowrite_api_key' => array(
2278 2651 'name' => 'ai_autowrite_api_key',
2279 2652 '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/' ) ),
2653 + 'label' => __( 'OpenAI API Key', 'betterdocs' ),
2654 + '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 2655 'default' => '',
2283 - 'priority' => 1
2656 + 'priority' => 2,
2657 + 'rules' => Rules::is( 'ai_platform', 'openai' )
2658 + ),
2659 + 'ai_api_key_gemini' => array(
2660 + 'name' => 'ai_api_key_gemini',
2661 + 'type' => 'text',
2662 + 'label' => __( 'Google Gemini API Key', 'betterdocs' ),
2663 + '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' ) ),
2664 + 'default' => '',
2665 + 'priority' => 2,
2666 + 'rules' => Rules::is( 'ai_platform', 'gemini' )
2667 + ),
2668 + 'ai_api_key_claude' => array(
2669 + 'name' => 'ai_api_key_claude',
2670 + 'type' => 'text',
2671 + 'label' => __( 'Anthropic Claude API Key', 'betterdocs' ),
2672 + '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' ) ),
2673 + 'default' => '',
2674 + 'priority' => 2,
2675 + 'rules' => Rules::is( 'ai_platform', 'claude' )
2676 + ),
2677 + 'ai_api_key_deepseek' => array(
2678 + 'name' => 'ai_api_key_deepseek',
2679 + 'type' => 'text',
2680 + 'label' => __( 'DeepSeek API Key', 'betterdocs' ),
2681 + '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' ) ),
2682 + 'default' => '',
2683 + 'priority' => 2,
2684 + 'rules' => Rules::is( 'ai_platform', 'deepseek' )
2685 + ),
2686 + 'ai_api_key_openrouter' => array(
2687 + 'name' => 'ai_api_key_openrouter',
2688 + 'type' => 'text',
2689 + 'label' => __( 'OpenRouter API Key', 'betterdocs' ),
2690 + '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' ) ),
2691 + 'default' => '',
2692 + 'priority' => 2,
2693 + 'rules' => Rules::is( 'ai_platform', 'openrouter' )
2694 + ),
2695 + 'ai_model' => array(
2696 + 'name' => 'ai_model',
2697 + 'type' => 'platform_model_select',
2698 + 'label' => __( 'Model', 'betterdocs' ),
2699 + 'label_subtitle' => __( 'Available models depend on the selected platform.', 'betterdocs' ),
2700 + 'priority' => 10,
2701 + 'default' => 'gpt-4o-mini',
2702 + 'catalogue' => ModelRegistry::catalogue(),
2703 + 'defaults' => ModelRegistry::defaults()
2284 2704 )
2285 2705 )
2286 2706 ),
2287 2707 'write-with-ai' => array(
@@ -2290,8 +2710,39 @@
2290 2710 'type' => 'section',
2291 2711 'label' => __( 'Write with AI', 'betterdocs' ),
2292 2712 'priority' => 5,
2293 2713 'fields' => array(
2714 + 'write-with-ai-subtabs' => array(
2715 + 'id' => 'write-with-ai-subtabs',
2716 + 'name' => 'write_with_ai_subtabs',
2717 + 'label' => __( 'Write with AI', 'betterdocs' ),
2718 + 'classes' => 'tab-nested-layout',
2719 + 'type' => 'tab',
2720 + 'active' => 'wwa-configuration',
2721 + 'completionTrack' => true,
2722 + 'sidebar' => false,
2723 + 'save' => false,
2724 + 'title' => false,
2725 + 'config' => array(
2726 + 'active' => 'wwa-configuration',
2727 + 'sidebar' => false,
2728 + 'title' => false
2729 + ),
2730 + 'submit' => array(
2731 + 'show' => false
2732 + ),
2733 + 'step' => array(
2734 + 'show' => false
2735 + ),
2736 + 'priority' => 5,
2737 + 'fields' => array(
2738 + 'wwa-configuration' => array(
2739 + 'id' => 'wwa-configuration',
2740 + 'name' => 'wwa-configuration',
2741 + 'type' => 'section',
2742 + 'label' => __( 'Configuration', 'betterdocs' ),
2743 + 'priority' => 1,
2744 + 'fields' => array(
2294 2745 'enable_write_with_ai' => array(
2295 2746 'name' => 'enable_write_with_ai',
2296 2747 'type' => 'toggle',
2297 2748 'priority' => 0,
@@ -2315,29 +2766,20 @@
2315 2766 'priority' => 6,
2316 2767 'label' => __( 'Write Glossaries with AI', 'betterdocs' ),
2317 2768 'label_subtitle' => __( 'Generate AI based Glossary definitions from the Glossaries admin page', 'betterdocs' ),
2318 2769 'enable_disable_text_active' => true,
2770 + 'default' => true,
2771 + 'is_pro' => true
2772 + ),
2773 + 'enable_docs_ai_suite' => array(
2774 + 'name' => 'enable_docs_ai_suite',
2775 + 'type' => 'toggle',
2776 + 'priority' => 7,
2777 + 'label' => __( 'Enhance Docs with AI', 'betterdocs' ),
2778 + 'label_subtitle' => __( 'Add BetterDocs AI actions for suggesting categories, tags and excerpts inside the native Docs editor panels.', 'betterdocs' ),
2779 + 'enable_disable_text_active' => true,
2319 2780 'default' => true
2320 2781 ),
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 2782 'ai_autowrite_max_token' => array(
2341 2783 'name' => 'ai_autowrite_max_token',
2342 2784 'type' => 'min_token_number',
2343 2785 'label' => __( 'Set Max Tokens', 'betterdocs' ),
@@ -2344,13 +2786,57 @@
2344 2786 'label_subtitle' => sprintf( // translators: %s is a link to more information about token limits.
2345 2787 __( '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 2788 'default' => 2500,
2347 2789 'priority' => 10,
2348 - 'model_field' => 'write_with_ai_model',
2790 + 'model_field' => 'ai_model',
2349 2791 'context' => 'write_with_ai',
2350 2792 'min_token_map' => AIHelper::get_min_tokens_map( 'write_with_ai' ),
2351 2793 'classes' => 'wprf-type-text'
2352 2794 )
2795 + )
2796 + ),
2797 + 'wwa-personalize' => array(
2798 + 'id' => 'wwa-personalize',
2799 + 'name' => 'wwa-personalize',
2800 + 'type' => 'section',
2801 + 'label' => __( 'Write AI Personalize', 'betterdocs' ),
2802 + 'priority' => 5,
2803 + 'fields' => array(
2804 + 'write_with_ai_instructions' => array(
2805 + 'name' => 'write_with_ai_instructions',
2806 + 'type' => 'wwa_instructions',
2807 + 'label' => __( 'Instructions', 'betterdocs' ),
2808 + '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' ),
2809 + 'priority' => 1,
2810 + 'default' => array(
2811 + array(
2812 + 'id' => 'default',
2813 + 'title' => __( 'Default/Core', 'betterdocs' ),
2814 + 'content' => WriteWithAI::default_instruction_content()
2815 + )
2816 + )
2817 + )
2818 + )
2819 + ),
2820 + 'edit-ai-personalize' => array(
2821 + 'id' => 'edit-ai-personalize',
2822 + 'name' => 'edit-ai-personalize',
2823 + 'type' => 'section',
2824 + 'label' => __( 'Edit AI Personalize', 'betterdocs' ),
2825 + 'priority' => 7,
2826 + 'fields' => array(
2827 + 'ai_edit_actions' => array(
2828 + 'name' => 'ai_edit_actions',
2829 + 'type' => 'ai_edit_actions',
2830 + 'label' => __( 'Actions', 'betterdocs' ),
2831 + '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' ),
2832 + 'priority' => 1,
2833 + 'default' => AIEdit::default_actions()
2834 + )
2835 + )
2836 + )
2837 + )
2838 + )
2353 2839 )
2354 2840 ),
2355 2841 'article-summary' => array(
2356 2842 'id' => 'article-summary',
@@ -2375,31 +2861,12 @@
2375 2861 'label_subtitle' => sprintf( // translators: %s is a link to more information about token limits.
2376 2862 __( '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 2863 'default' => 1500,
2378 2864 'priority' => 5,
2379 - 'model_field' => 'article_summary_model',
2865 + 'model_field' => 'ai_model',
2380 2866 'context' => 'article_summary',
2381 2867 'min_token_map' => AIHelper::get_min_tokens_map( 'article_summary' ),
2382 2868 '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 2869 )
2403 2870 )
2404 2871 )
2405 2872 )
@@ -2506,15 +2973,11 @@
2506 2973 return array(
2507 2974 'multiple_kb' => false,
2508 2975 'enable_glossaries' => false,
2509 2976 'enable_encyclopedia' => false,
2510 - 'analytics_from' => false,
2511 - 'unique_visitor_count' => false,
2512 - 'exclude_bot_analytics' => false,
2513 2977 'show_attachment' => false,
2514 2978 'show_related_docs' => false,
2515 2979 'advance_search' => false,
2516 - 'child_category_exclude' => false,
2517 2980 'kb_based_search' => false,
2518 2981 'enable_disable' => false,
2519 2982 'enable_content_restriction' => false
2520 2983 );