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 +554 -98 4.5.5 → 4.9.5 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,
@@ -212,8 +239,12 @@
212 239 'category_slug' => 'docs-category',
213 240 'tag_slug' => 'docs-tag',
214 241 'permalink_structure' => 'docs/',
215 242 'enable_faq_schema' => false,
243 + 'analytics_from' => 'everyone',
244 + 'unique_visitor_count' => true,
245 + 'exclude_bot_analytics' => true,
246 + 'child_category_exclude' => false,
216 247 'live_search' => true,
217 248 'advance_search' => false,
218 249 'popular_keyword_limit' => 5,
219 250 'search_letter_limit' => 3,
@@ -256,8 +287,12 @@
256 287 'enable_breadcrumb_category' => true,
257 288 'enable_breadcrumb_title' => true,
258 289 'enable_sidebar_cat_list' => true,
259 290 'enable_print_icon' => true,
291 + 'print_enable_logo' => false,
292 + 'print_logo' => array(),
293 + 'print_enable_footer' => false,
294 + 'print_footer_text' => '',
260 295 'enable_tags' => true,
261 296 'email_feedback' => true,
262 297 'feedback_link_text' => __( 'Still stuck? How can we help?', 'betterdocs' ),
263 298 'reaction_feedback_text' => __( 'Thanks for your feedback', 'betterdocs' ),
@@ -270,8 +305,9 @@
270 305 'enable_credit' => false,
271 306 'enable_archive_sidebar' => true,
272 307 'archive_nested_subcategory' => true,
273 308 'archive_enable_pagination' => false,
309 + 'archive_lazy_load_descendants' => false,
274 310 'enable_content_restriction' => false,
275 311 'enable_reporting' => false,
276 312 'enable_sample_data' => false,
277 313 'reporting_day' => 'monday',
@@ -278,14 +314,34 @@
278 314 'reporting_email' => get_option( 'admin_email' ),
279 315 'enable_write_with_ai' => true,
280 316 'enable_faq_write_with_ai' => true,
281 317 'enable_glossaries_write_with_ai' => true,
318 + 'enable_docs_ai_suite' => true,
282 319 'write_with_ai_model' => 'gpt-4o-mini',
283 320 'ai_autowrite_api_key' => '',
284 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(),
285 330 'enable_article_summary' => false,
286 331 'article_summary_model' => 'gpt-4o-mini',
287 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' => '',
288 344 'enable_estimated_reading_time' => true,
289 345 'enable_encyclopedia' => false,
290 346 'enable_glossaries' => false,
291 347 'show_glossary_suggestions' => true,
@@ -295,9 +351,40 @@
295 351 'singular_estimated_reading_time_text' => __( 'min read', 'betterdocs' ),
296 352 'betterdocs_access_control_repeater' => array(),
297 353 'internal_knowledge_base_type' => 'basic',
298 354 'betterdocs_access_control_repeater_kb' => array(),
299 - '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' => ''
300 387 );
301 388
302 389 $_default = apply_filters( 'betterdocs_default_settings', $_default );
303 390 // $_default = apply_filters_deprecated(
@@ -590,19 +677,27 @@
590 677 }
591 678
592 679 $_old_settings = $this->database->get( $this->base_key, $this->get_default() );
593 680
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' );
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();
598 690 foreach ( $sensitive_api_keys as $api_key_field ) {
599 691 if ( ! isset( $settings[ $api_key_field ] ) ) {
600 692 continue;
601 693 }
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;
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;
605 700 }
606 701 }
607 702
608 703 // Minimum-token policy: reject saves where a feature's max_token field is below the
@@ -617,9 +712,9 @@
617 712 array(
618 713 'tab' => 'tab-betterdocs-ai',
619 714 'context' => 'write_with_ai',
620 715 'token_key' => 'ai_autowrite_max_token',
621 - 'model_key' => 'write_with_ai_model',
716 + 'model_key' => 'ai_model',
622 717 'label' => __( 'Write with AI', 'betterdocs' )
623 718 ),
624 719 array(
625 720 'tab' => 'tab-betterdocs-ai',
@@ -624,9 +719,9 @@
624 719 array(
625 720 'tab' => 'tab-betterdocs-ai',
626 721 'context' => 'article_summary',
627 722 'token_key' => 'article_summary_max_token',
628 - 'model_key' => 'article_summary_model',
723 + 'model_key' => 'ai_model',
629 724 'label' => __( 'AI Doc Summarizer', 'betterdocs' )
630 725 )
631 726 );
632 727 foreach ( $token_pairs as $pair ) {
@@ -661,21 +756,38 @@
661 756 betterdocs()->kbmigration->migrate();
662 757 }
663 758 $_settings = wp_parse_args( $_normalized_settings, $_old_settings );
664 759
665 - // Check if there are actual changes before saving.
666 - // update_option returns false when values serialize identically, which can happen
667 - // due to object caching or type normalization even when user made changes.
668 - $_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;
669 782
670 783 $_saved = $this->database->save( $this->base_key, $_settings );
671 784
672 785 do_action_ref_array( 'betterdocs::settings::saved', array( $_saved, $_settings, $_old_settings, &$this ) );
673 786
674 - // Return true if save succeeded OR if there were changes to attempt saving.
675 - // This handles cases where update_option returns false due to identical serialization
676 - // (e.g., object caching, type coercion during serialization).
677 - 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;
678 790 }
679 791
680 792 public function views( $hook ) {
681 793 return betterdocs()->views->get( 'admin/settings' );
@@ -873,10 +985,9 @@
873 985 'guests' => __( 'Guests Only', 'betterdocs' ),
874 986 'registered_users' => __( 'Registered Users Only', 'betterdocs' )
875 987 ) ),
876 988 'default' => 'everyone',
877 - 'priority' => 13,
878 - 'is_pro' => true
989 + 'priority' => 13
879 990 ),
880 991 'unique_visitor_count' => array(
881 992 'name' => 'unique_visitor_count',
882 993 'type' => 'toggle',
@@ -881,11 +992,10 @@
881 992 'name' => 'unique_visitor_count',
882 993 'type' => 'toggle',
883 994 'label' => __( 'Unique Visitor Count', 'betterdocs' ),
884 995 'enable_disable_text_active' => true,
885 - 'default' => false,
886 - 'priority' => 14,
887 - 'is_pro' => true
996 + 'default' => true,
997 + 'priority' => 14
888 998 ),
889 999 'exclude_bot_analytics' => array(
890 1000 'name' => 'exclude_bot_analytics',
891 1001 'type' => 'toggle',
@@ -890,11 +1000,10 @@
890 1000 'name' => 'exclude_bot_analytics',
891 1001 'type' => 'toggle',
892 1002 'label' => __( 'Exclude Bot Analytics', 'betterdocs' ),
893 1003 'enable_disable_text_active' => true,
894 - 'default' => false,
895 - 'priority' => 15,
896 - 'is_pro' => true
1004 + 'default' => true,
1005 + 'priority' => 15
897 1006 )
898 1007
899 1008 )
900 1009 ) )
@@ -998,8 +1107,17 @@
998 1107 'enable_disable_text_active' => true,
999 1108 'default' => 1,
1000 1109 'priority' => 1
1001 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 + ),
1002 1120 'nested_subcategory' => array(
1003 1121 'name' => 'nested_subcategory',
1004 1122 'type' => 'toggle',
1005 1123 'label' => __( 'Nested Sub Category', 'betterdocs' ),
@@ -1004,9 +1122,9 @@
1004 1122 'type' => 'toggle',
1005 1123 'label' => __( 'Nested Sub Category', 'betterdocs' ),
1006 1124 'enable_disable_text_active' => true,
1007 1125 'default' => '',
1008 - 'priority' => 2
1126 + 'priority' => 3
1009 1127 ),
1010 1128 'column_number' => array(
1011 1129 'name' => 'column_number',
1012 1130 'type' => 'number',
@@ -1012,9 +1130,9 @@
1012 1130 'type' => 'number',
1013 1131 'label' => __( 'Number Of Columns', 'betterdocs' ),
1014 1132 'label_subtitle' => __( 'This setting is not applicable for sleek layout.', 'betterdocs' ),
1015 1133 'default' => 3,
1016 - 'priority' => 3
1134 + 'priority' => 4
1017 1135 ),
1018 1136 'posts_number' => apply_filters( 'betterdocs_posts_number', array(
1019 1137 'name' => 'posts_number',
1020 1138 'type' => 'number',
@@ -1020,9 +1138,9 @@
1020 1138 'type' => 'number',
1021 1139 'label' => __( 'Number Of Docs', 'betterdocs' ),
1022 1140 'label_subtitle' => __( 'This setting is not applicable for handbook layout.', 'betterdocs' ),
1023 1141 'default' => 10,
1024 - 'priority' => 4
1142 + 'priority' => 5
1025 1143 ) ),
1026 1144 'post_count' => array(
1027 1145 'name' => 'post_count',
1028 1146 'type' => 'toggle',
@@ -1028,9 +1146,9 @@
1028 1146 'type' => 'toggle',
1029 1147 'label' => __( 'Doc Count', 'betterdocs' ),
1030 1148 'enable_disable_text_active' => true,
1031 1149 'default' => 1,
1032 - 'priority' => 5
1150 + 'priority' => 6
1033 1151 ),
1034 1152 'count_text' => array(
1035 1153 'name' => 'count_text',
1036 1154 'type' => 'text',
@@ -1035,9 +1153,9 @@
1035 1153 'name' => 'count_text',
1036 1154 'type' => 'text',
1037 1155 'label' => __( 'Count Text', 'betterdocs' ),
1038 1156 'default' => __( 'Docs', 'betterdocs' ),
1039 - 'priority' => 6
1157 + 'priority' => 7
1040 1158 ),
1041 1159 'count_text_singular' => array(
1042 1160 'name' => 'count_text_singular',
1043 1161 'type' => 'text',
@@ -1042,9 +1160,9 @@
1042 1160 'name' => 'count_text_singular',
1043 1161 'type' => 'text',
1044 1162 'label' => __( 'Count Text Singular', 'betterdocs' ),
1045 1163 'default' => __( 'Doc', 'betterdocs' ),
1046 - 'priority' => 7
1164 + 'priority' => 8
1047 1165 ),
1048 1166 'exploremore_btn' => array(
1049 1167 'name' => 'exploremore_btn',
1050 1168 'type' => 'toggle',
@@ -1050,9 +1168,9 @@
1050 1168 'type' => 'toggle',
1051 1169 'label' => __( 'Explore More Button', 'betterdocs' ),
1052 1170 'enable_disable_text_active' => true,
1053 1171 'default' => true,
1054 - 'priority' => 8
1172 + 'priority' => 9
1055 1173 ),
1056 1174 'exploremore_btn_txt' => array(
1057 1175 'name' => 'exploremore_btn_txt',
1058 1176 'type' => 'text',
@@ -1057,9 +1175,9 @@
1057 1175 'name' => 'exploremore_btn_txt',
1058 1176 'type' => 'text',
1059 1177 'label' => __( 'Explore More Button Text', 'betterdocs' ),
1060 1178 'default' => __( 'Explore More', 'betterdocs' ),
1061 - 'priority' => 9,
1179 + 'priority' => 10,
1062 1180 'rules' => Rules::is( 'exploremore_btn', true )
1063 1181 ),
1064 1182 'betterdocs_popular_docs_text' => array(
1065 1183 'name' => 'betterdocs_popular_docs_text',
@@ -1065,9 +1183,9 @@
1065 1183 'name' => 'betterdocs_popular_docs_text',
1066 1184 'type' => 'text',
1067 1185 'label' => __( 'Popular Docs Text', 'betterdocs' ),
1068 1186 'default' => __( 'Popular Docs', 'betterdocs' ),
1069 - 'priority' => 10,
1187 + 'priority' => 11,
1070 1188 'is_pro' => true
1071 1189 ),
1072 1190 'betterdocs_popular_docs_number' => array(
1073 1191 'name' => 'betterdocs_popular_docs_number',
@@ -1073,9 +1191,9 @@
1073 1191 'name' => 'betterdocs_popular_docs_number',
1074 1192 'type' => 'number',
1075 1193 'label' => __( 'Popular Docs Number', 'betterdocs' ),
1076 1194 'default' => 10,
1077 - 'priority' => 11,
1195 + 'priority' => 12,
1078 1196 'is_pro' => true
1079 1197 )
1080 1198 )
1081 1199 ),
@@ -1108,11 +1226,10 @@
1108 1226 'name' => 'child_category_exclude',
1109 1227 'type' => 'toggle',
1110 1228 'label' => __( 'Exclude Child Terms In Category Search', 'betterdocs' ),
1111 1229 'enable_disable_text_active' => true,
1112 - 'default' => '',
1113 - 'priority' => 3,
1114 - 'is_pro' => true
1230 + 'default' => false,
1231 + 'priority' => 3
1115 1232 ) ),
1116 1233 'popular_keyword_limit' => apply_filters( 'betterdocs_popular_keyword_limit_settings', array(
1117 1234 'name' => 'popular_keyword_limit',
1118 1235 'type' => 'number',
@@ -1319,8 +1436,44 @@
1319 1436 'enable_disable_text_active' => true,
1320 1437 'default' => 1,
1321 1438 'priority' => 3
1322 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 + ),
1323 1476 'enable_tags' => array(
1324 1477 'name' => 'enable_tags',
1325 1478 'type' => 'toggle',
1326 1479 'label' => __( 'Tags', 'betterdocs' ),
@@ -1325,9 +1478,9 @@
1325 1478 'type' => 'toggle',
1326 1479 'label' => __( 'Tags', 'betterdocs' ),
1327 1480 'enable_disable_text_active' => true,
1328 1481 'default' => 1,
1329 - 'priority' => 4
1482 + 'priority' => 8
1330 1483 ),
1331 1484 'show_last_update_time' => array(
1332 1485 'name' => 'show_last_update_time',
1333 1486 'type' => 'toggle',
@@ -1333,9 +1486,9 @@
1333 1486 'type' => 'toggle',
1334 1487 'label' => __( 'Last Update Time', 'betterdocs' ),
1335 1488 'enable_disable_text_active' => true,
1336 1489 'default' => 1,
1337 - 'priority' => 5
1490 + 'priority' => 9
1338 1491 ),
1339 1492 'enable_navigation' => array(
1340 1493 'name' => 'enable_navigation',
1341 1494 'type' => 'toggle',
@@ -1341,9 +1494,9 @@
1341 1494 'type' => 'toggle',
1342 1495 'label' => __( 'Navigation', 'betterdocs' ),
1343 1496 'enable_disable_text_active' => true,
1344 1497 'default' => 1,
1345 - 'priority' => 6
1498 + 'priority' => 10
1346 1499 ),
1347 1500 'enable_comment' => array(
1348 1501 'name' => 'enable_comment',
1349 1502 'type' => 'toggle',
@@ -1349,9 +1502,9 @@
1349 1502 'type' => 'toggle',
1350 1503 'label' => __( 'Comment', 'betterdocs' ),
1351 1504 'enable_disable_text_active' => true,
1352 1505 'default' => '',
1353 - 'priority' => 7
1506 + 'priority' => 11
1354 1507 ),
1355 1508 'enable_credit' => array(
1356 1509 'name' => 'enable_credit',
1357 1510 'type' => 'toggle',
@@ -1357,9 +1510,9 @@
1357 1510 'type' => 'toggle',
1358 1511 'label' => __( 'Show Powered by BetterDocs', 'betterdocs' ),
1359 1512 'enable_disable_text_active' => true,
1360 1513 'default' => '',
1361 - 'priority' => 8
1514 + 'priority' => 12
1362 1515 ),
1363 1516 'reaction_feedback_text' => array(
1364 1517 'name' => 'reaction_feedback_text',
1365 1518 'type' => 'text',
@@ -1364,9 +1517,9 @@
1364 1517 'name' => 'reaction_feedback_text',
1365 1518 'type' => 'text',
1366 1519 'label' => __( 'Reaction Feedback Text', 'betterdocs' ),
1367 1520 'default' => __( 'Thanks for your feedback.', 'betterdocs' ),
1368 - 'priority' => 9
1521 + 'priority' => 13
1369 1522 ),
1370 1523 'enable_estimated_reading_time' => array(
1371 1524 'name' => 'enable_estimated_reading_time',
1372 1525 'type' => 'toggle',
@@ -1372,9 +1525,9 @@
1372 1525 'type' => 'toggle',
1373 1526 'label' => __( 'Estimated Reading Time', 'betterdocs' ),
1374 1527 'enable_disable_text_active' => true,
1375 1528 'default' => 0,
1376 - 'priority' => 10
1529 + 'priority' => 14
1377 1530 ),
1378 1531 'estimated_reading_time_title' => array(
1379 1532 'name' => 'estimated_reading_time_title',
1380 1533 'type' => 'text',
@@ -1379,9 +1532,9 @@
1379 1532 'name' => 'estimated_reading_time_title',
1380 1533 'type' => 'text',
1381 1534 'label' => __( 'Estimated Reading Time Title', 'betterdocs' ),
1382 1535 'default' => '',
1383 - 'priority' => 11,
1536 + 'priority' => 15,
1384 1537 'rules' => Rules::is( 'enable_estimated_reading_time', true )
1385 1538 ),
1386 1539 'estimated_reading_time_text' => array(
1387 1540 'name' => 'estimated_reading_time_text',
@@ -1387,9 +1540,9 @@
1387 1540 'name' => 'estimated_reading_time_text',
1388 1541 'type' => 'text',
1389 1542 'label' => __( 'Estimated Reading Time Text', 'betterdocs' ),
1390 1543 'default' => __( 'min read', 'betterdocs' ),
1391 - 'priority' => 12,
1544 + 'priority' => 16,
1392 1545 'rules' => Rules::is( 'enable_estimated_reading_time', true )
1393 1546 ),
1394 1547 'singular_estimated_reading_time_text' => array(
1395 1548 'name' => 'singular_estimated_reading_time_text',
@@ -1395,9 +1548,9 @@
1395 1548 'name' => 'singular_estimated_reading_time_text',
1396 1549 'type' => 'text',
1397 1550 'label' => __( 'Estimated Reading Time Text Singular', 'betterdocs' ),
1398 1551 'default' => __( 'min read', 'betterdocs' ),
1399 - 'priority' => 13,
1552 + 'priority' => 17,
1400 1553 'rules' => Rules::is( 'enable_estimated_reading_time', true )
1401 1554 )
1402 1555 )
1403 1556 ),
@@ -1664,8 +1817,202 @@
1664 1817 'enable_disable_text_active' => true,
1665 1818 'default' => false
1666 1819 )
1667 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 + ) )
1668 2015 )
1669 2016 )
1670 2017 )
1671 2018 )
@@ -2277,18 +2624,84 @@
2277 2624 'open-ai-settings' => array(
2278 2625 'id' => 'open-ai-settings',
2279 2626 'name' => 'open-ai-settings',
2280 2627 'type' => 'section',
2281 - 'label' => __( 'API Settings', 'betterdocs' ),
2628 + 'label' => __( 'AI Platform & API', 'betterdocs' ),
2282 2629 'priority' => 1,
2283 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.
2284 2650 'ai_autowrite_api_key' => array(
2285 2651 'name' => 'ai_autowrite_api_key',
2286 2652 '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/' ) ),
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/' ) ),
2289 2655 'default' => '',
2290 - '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()
2291 2704 )
2292 2705 )
2293 2706 ),
2294 2707 'write-with-ai' => array(
@@ -2297,8 +2710,39 @@
2297 2710 'type' => 'section',
2298 2711 'label' => __( 'Write with AI', 'betterdocs' ),
2299 2712 'priority' => 5,
2300 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(
2301 2745 'enable_write_with_ai' => array(
2302 2746 'name' => 'enable_write_with_ai',
2303 2747 'type' => 'toggle',
2304 2748 'priority' => 0,
@@ -2322,29 +2766,20 @@
2322 2766 'priority' => 6,
2323 2767 'label' => __( 'Write Glossaries with AI', 'betterdocs' ),
2324 2768 'label_subtitle' => __( 'Generate AI based Glossary definitions from the Glossaries admin page', 'betterdocs' ),
2325 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,
2326 2780 'default' => true
2327 2781 ),
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 2782 'ai_autowrite_max_token' => array(
2348 2783 'name' => 'ai_autowrite_max_token',
2349 2784 'type' => 'min_token_number',
2350 2785 'label' => __( 'Set Max Tokens', 'betterdocs' ),
@@ -2351,13 +2786,57 @@
2351 2786 'label_subtitle' => sprintf( // translators: %s is a link to more information about token limits.
2352 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' ) ),
2353 2788 'default' => 2500,
2354 2789 'priority' => 10,
2355 - 'model_field' => 'write_with_ai_model',
2790 + 'model_field' => 'ai_model',
2356 2791 'context' => 'write_with_ai',
2357 2792 'min_token_map' => AIHelper::get_min_tokens_map( 'write_with_ai' ),
2358 2793 'classes' => 'wprf-type-text'
2359 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 + )
2360 2839 )
2361 2840 ),
2362 2841 'article-summary' => array(
2363 2842 'id' => 'article-summary',
@@ -2382,31 +2861,12 @@
2382 2861 'label_subtitle' => sprintf( // translators: %s is a link to more information about token limits.
2383 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' ) ),
2384 2863 'default' => 1500,
2385 2864 'priority' => 5,
2386 - 'model_field' => 'article_summary_model',
2865 + 'model_field' => 'ai_model',
2387 2866 'context' => 'article_summary',
2388 2867 'min_token_map' => AIHelper::get_min_tokens_map( 'article_summary' ),
2389 2868 '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 2869 )
2410 2870 )
2411 2871 )
2412 2872 )
@@ -2513,15 +2973,11 @@
2513 2973 return array(
2514 2974 'multiple_kb' => false,
2515 2975 'enable_glossaries' => false,
2516 2976 'enable_encyclopedia' => false,
2517 - 'analytics_from' => false,
2518 - 'unique_visitor_count' => false,
2519 - 'exclude_bot_analytics' => false,
2520 2977 'show_attachment' => false,
2521 2978 'show_related_docs' => false,
2522 2979 'advance_search' => false,
2523 - 'child_category_exclude' => false,
2524 2980 'kb_based_search' => false,
2525 2981 'enable_disable' => false,
2526 2982 'enable_content_restriction' => false
2527 2983 );