PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.4
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.4
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 3.4.2 All 202 releases
← All changes | includes/Core/Settings.php +542 -83 4.6.2 → 4.9.4 View file →
@@ -9,8 +9,11 @@
9 9 use WP_Error;
10 10 use WP_User;
11 11 use WPDeveloper\BetterDocs\Admin\Builder\GlobalFields;
12 12 use WPDeveloper\BetterDocs\Admin\Builder\Rules;
13 +use WPDeveloper\BetterDocs\AI\ModelRegistry;
14 +use WPDeveloper\BetterDocs\AI\ProviderFactory;
15 +use WPDeveloper\BetterDocs\REST\AIEdit;
13 16 use WPDeveloper\BetterDocs\Utils\AIHelper;
14 17 use WPDeveloper\BetterDocs\Utils\Base;
15 18 use WPDeveloper\BetterDocs\Utils\Database;
16 19 use WPDeveloper\BetterDocs\Utils\Helper;
@@ -76,8 +79,31 @@
76 79 return esc_url( admin_url( 'admin.php?page=betterdocs-settings' ) );
77 80 }
78 81
79 82 /**
83 + * Settings keys holding secret API keys. These are masked before reaching
84 + * the browser, stripped for non-admins, and never persisted as their mask.
85 + *
86 + * Covers the AI Chatbot key plus every content-suite platform key. OpenAI's
87 + * is `ai_autowrite_api_key` (see ProviderFactory::key_field_for), hence the
88 + * dedupe.
89 + *
90 + * @return array<int,string>
91 + */
92 + public static function sensitive_api_key_fields() {
93 + $fields = array(
94 + ProviderFactory::OPENAI_KEY_FIELD,
95 + 'ai_chatbot_api_key',
96 + );
97 + foreach ( array_keys( ModelRegistry::platforms() ) as $platform ) {
98 + $fields[] = ProviderFactory::key_field_for( $platform );
99 + }
100 + // Add-ons (e.g. the AI Chatbot) register their own per-platform keys here
101 + // so they are masked in the browser and stripped for non-admins.
102 + return apply_filters( 'betterdocs_sensitive_api_key_fields', array_values( array_unique( $fields ) ) );
103 + }
104 +
105 + /**
80 106 * This method is responsible for enqueueing scripts in settings panel
81 107 *
82 108 * @param string $hook
83 109 *
@@ -94,9 +120,9 @@
94 120
95 121 $settings = GlobalFields::normalize( $this->settings_args() );
96 122
97 123 // Mask sensitive API keys before they reach the browser. Non-admins still get them stripped entirely below.
98 - $sensitive_api_keys = array( 'ai_autowrite_api_key', 'ai_chatbot_api_key' );
124 + $sensitive_api_keys = self::sensitive_api_key_fields();
99 125 foreach ( $sensitive_api_keys as $api_key_field ) {
100 126 if ( ! empty( $settings[ 'values' ][ $api_key_field ] ) ) {
101 127 $settings[ 'values' ][ $api_key_field ] = Helper::mask_api_key( $settings[ 'values' ][ $api_key_field ] );
102 128 }
@@ -257,8 +283,12 @@
257 283 'enable_breadcrumb_category' => true,
258 284 'enable_breadcrumb_title' => true,
259 285 'enable_sidebar_cat_list' => true,
260 286 'enable_print_icon' => true,
287 + 'print_enable_logo' => false,
288 + 'print_logo' => array(),
289 + 'print_enable_footer' => false,
290 + 'print_footer_text' => '',
261 291 'enable_tags' => true,
262 292 'email_feedback' => true,
263 293 'feedback_link_text' => __( 'Still stuck? How can we help?', 'betterdocs' ),
264 294 'reaction_feedback_text' => __( 'Thanks for your feedback', 'betterdocs' ),
@@ -271,8 +301,9 @@
271 301 'enable_credit' => false,
272 302 'enable_archive_sidebar' => true,
273 303 'archive_nested_subcategory' => true,
274 304 'archive_enable_pagination' => false,
305 + 'archive_lazy_load_descendants' => false,
275 306 'enable_content_restriction' => false,
276 307 'enable_reporting' => false,
277 308 'enable_sample_data' => false,
278 309 'reporting_day' => 'monday',
@@ -279,14 +310,34 @@
279 310 'reporting_email' => get_option( 'admin_email' ),
280 311 'enable_write_with_ai' => true,
281 312 'enable_faq_write_with_ai' => true,
282 313 'enable_glossaries_write_with_ai' => true,
314 + 'enable_docs_ai_suite' => true,
283 315 'write_with_ai_model' => 'gpt-4o-mini',
284 316 'ai_autowrite_api_key' => '',
285 317 'ai_autowrite_max_token' => 2500,
318 + 'write_with_ai_instructions' => array(
319 + array(
320 + 'id' => 'default',
321 + 'title' => __( 'Default/Core', 'betterdocs' ),
322 + 'content' => WriteWithAI::default_instruction_content()
323 + )
324 + ),
325 + 'ai_edit_actions' => AIEdit::default_actions(),
286 326 'enable_article_summary' => false,
287 327 'article_summary_model' => 'gpt-4o-mini',
288 328 'article_summary_max_token' => 1500,
329 + // Multi-platform AI (content suite). `ai_platform` selects the active
330 + // provider; `ai_model` is the single global model; keys are stored
331 + // per platform so switching never loses a saved key. OpenAI reuses
332 + // `ai_autowrite_api_key` above — it has always held an OpenAI key,
333 + // so nothing needs migrating.
334 + 'ai_platform' => 'openai',
335 + 'ai_model' => 'gpt-4o-mini',
336 + 'ai_api_key_gemini' => '',
337 + 'ai_api_key_claude' => '',
338 + 'ai_api_key_deepseek' => '',
339 + 'ai_api_key_openrouter' => '',
289 340 'enable_estimated_reading_time' => true,
290 341 'enable_encyclopedia' => false,
291 342 'enable_glossaries' => false,
292 343 'show_glossary_suggestions' => true,
@@ -296,9 +347,40 @@
296 347 'singular_estimated_reading_time_text' => __( 'min read', 'betterdocs' ),
297 348 'betterdocs_access_control_repeater' => array(),
298 349 'internal_knowledge_base_type' => 'basic',
299 350 'betterdocs_access_control_repeater_kb' => array(),
300 - 'enable_git_integration' => false
351 + 'enable_git_integration' => false,
352 + 'enable_ai_actions' => true,
353 + 'enable_markdown_endpoint' => true,
354 + 'ai_actions_button_label' => __( 'Copy page', 'betterdocs' ),
355 + 'ai_actions_prompt_template' => __( 'Read from {URL} so I can ask questions about it.', 'betterdocs' ),
356 + 'ai_actions_copy_page' => true,
357 + 'ai_actions_view_markdown' => true,
358 + 'ai_actions_chatgpt' => true,
359 + 'ai_actions_claude' => true,
360 + 'ai_actions_gemini' => true,
361 + 'ai_actions_perplexity' => false,
362 + 'ai_actions_grok' => false,
363 + 'enable_listen' => true,
364 + 'listen_button_label' => __( 'Listen', 'betterdocs' ),
365 + 'listen_show_speed' => true,
366 + // Words a synthetic voice gets through in a minute at rate 1.
367 + // Deliberately lower than the 200 the reading-time pill assumes — that
368 + // number describes silent reading. Only the player's duration estimate
369 + // uses this; see Core\Listen::WORDS_PER_MINUTE.
370 + 'listen_words_per_minute' => 180,
371 + /**
372 + * MCP master switch. Off by default; the toggle lives on the
373 + * BetterDocs → MCP page, not in the settings tree, and writes
374 + * through POST betterdocs/v1/settings.
375 + *
376 + * The key has to be listed here or `get()` cannot see it at all:
377 + * it answers `$default` for anything absent from the defaults
378 + * array, whatever the stored option holds.
379 + *
380 + * @since 4.9.0
381 + */
382 + 'enable_mcp' => ''
301 383 );
302 384
303 385 $_default = apply_filters( 'betterdocs_default_settings', $_default );
304 386 // $_default = apply_filters_deprecated(
@@ -591,19 +673,27 @@
591 673 }
592 674
593 675 $_old_settings = $this->database->get( $this->base_key, $this->get_default() );
594 676
595 - // The frontend only ever sees masked API keys. If a submitted value matches the mask of
596 - // the stored value, the user did not change it — restore the original so the mask string
597 - // is never persisted.
598 - $sensitive_api_keys = array( 'ai_autowrite_api_key', 'ai_chatbot_api_key' );
677 + // The frontend only ever sees masked API keys. A submitted value that still looks like a
678 + // mask (contains a run of asterisks — no real key does) means "unchanged": restore the
679 + // stored original so a mask string is never persisted. Guarding on the mask SHAPE rather
680 + // than strict equality with mask(stored) closes the corruption loop QA hit: once a mask
681 + // slips into storage, mask(mask) === mask, so an equality-only guard would faithfully
682 + // preserve the corrupted value forever (round-2 follow-up #1). When the stored value is
683 + // itself mask-shaped it is unrecoverable — clear it so the UI and the GeoIP/GA4 status
684 + // surfaces honestly report a missing key instead of failing downstream with 401s.
685 + $sensitive_api_keys = self::sensitive_api_key_fields();
599 686 foreach ( $sensitive_api_keys as $api_key_field ) {
600 687 if ( ! isset( $settings[ $api_key_field ] ) ) {
601 688 continue;
602 689 }
603 - $stored = isset( $_old_settings[ $api_key_field ] ) ? $_old_settings[ $api_key_field ] : '';
604 - if ( '' !== $stored && trim( (string) $settings[ $api_key_field ] ) === Helper::mask_api_key( $stored ) ) {
605 - $settings[ $api_key_field ] = $stored;
690 + $incoming = trim( (string) $settings[ $api_key_field ] );
691 + $stored = isset( $_old_settings[ $api_key_field ] ) ? (string) $_old_settings[ $api_key_field ] : '';
692 + $stored_is_mask = '' !== $stored && preg_match( '/\*{4,}/', $stored );
693 +
694 + if ( '' !== $incoming && preg_match( '/\*{4,}/', $incoming ) ) {
695 + $settings[ $api_key_field ] = $stored_is_mask ? '' : $stored;
606 696 }
607 697 }
608 698
609 699 // Minimum-token policy: reject saves where a feature's max_token field is below the
@@ -618,9 +708,9 @@
618 708 array(
619 709 'tab' => 'tab-betterdocs-ai',
620 710 'context' => 'write_with_ai',
621 711 'token_key' => 'ai_autowrite_max_token',
622 - 'model_key' => 'write_with_ai_model',
712 + 'model_key' => 'ai_model',
623 713 'label' => __( 'Write with AI', 'betterdocs' )
624 714 ),
625 715 array(
626 716 'tab' => 'tab-betterdocs-ai',
@@ -625,9 +715,9 @@
625 715 array(
626 716 'tab' => 'tab-betterdocs-ai',
627 717 'context' => 'article_summary',
628 718 'token_key' => 'article_summary_max_token',
629 - 'model_key' => 'article_summary_model',
719 + 'model_key' => 'ai_model',
630 720 'label' => __( 'AI Doc Summarizer', 'betterdocs' )
631 721 )
632 722 );
633 723 foreach ( $token_pairs as $pair ) {
@@ -662,21 +752,38 @@
662 752 betterdocs()->kbmigration->migrate();
663 753 }
664 754 $_settings = wp_parse_args( $_normalized_settings, $_old_settings );
665 755
666 - // Check if there are actual changes before saving.
667 - // update_option returns false when values serialize identically, which can happen
668 - // due to object caching or type normalization even when user made changes.
669 - $_has_changes = $_settings != $_old_settings;
756 + // Detect whether this save actually changes the effective settings.
757 + //
758 + // The stored option and the submitted payload are normalized differently:
759 + // an optional field can be ABSENT from storage yet arrive as '' (e.g.
760 + // Feedback URL), and array fields can be stored empty ( [] ) while their
761 + // normalized/default form is non-empty (e.g. Instant Answer's
762 + // display_ia_texonomy defaults to ['all']). Comparing the raw arrays
763 + // ( $_settings != $_old_settings ) therefore reported a phantom change on
764 + // every save, leaving the tab perpetually "dirty" and always toasting
765 + // "Changes Saved Successfully." instead of "There are no changes to be
766 + // saved." — see WPDevelopers/betterdocs-pro#78.
767 + //
768 + // Compare like-for-like instead: fill defaults on both sides and run both
769 + // through the same normalization, so semantically-equal states (absent vs
770 + // '', [] vs ['all'], 'on' vs true) collapse to identical values and only a
771 + // real edit registers. This is also more reliable than update_option()'s
772 + // return, which is false whenever values serialize identically under object
773 + // caching or type coercion even when the user did change something (#49).
774 + $_defaults = array_merge( $this->get_default(), $this->get_pro_defaults() );
775 + $_old_normalized = $this->get_normalized_values( wp_parse_args( $_old_settings, $_defaults ), $_defaults );
776 + $_new_normalized = $this->get_normalized_values( wp_parse_args( $_settings, $_defaults ), $_defaults );
777 + $_has_changes = $_new_normalized != $_old_normalized;
670 778
671 779 $_saved = $this->database->save( $this->base_key, $_settings );
672 780
673 781 do_action_ref_array( 'betterdocs::settings::saved', array( $_saved, $_settings, $_old_settings, &$this ) );
674 782
675 - // Return true if save succeeded OR if there were changes to attempt saving.
676 - // This handles cases where update_option returns false due to identical serialization
677 - // (e.g., object caching, type coercion during serialization).
678 - return $_saved || $_has_changes;
783 + // The success / no-changes toast reflects whether the user made a real
784 + // change, not update_option()'s (unreliable) return value.
785 + return $_has_changes;
679 786 }
680 787
681 788 public function views( $hook ) {
682 789 return betterdocs()->views->get( 'admin/settings' );
@@ -999,8 +1106,17 @@
999 1106 'enable_disable_text_active' => true,
1000 1107 'default' => 1,
1001 1108 'priority' => 1
1002 1109 ),
1110 + 'archive_lazy_load_descendants' => array(
1111 + 'name' => 'archive_lazy_load_descendants',
1112 + 'type' => 'toggle',
1113 + 'label' => __( 'Lazy Load Sidebar Docs & Subcategories', 'betterdocs' ),
1114 + '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' ),
1115 + 'enable_disable_text_active' => true,
1116 + 'default' => false,
1117 + 'priority' => 2
1118 + ),
1003 1119 'nested_subcategory' => array(
1004 1120 'name' => 'nested_subcategory',
1005 1121 'type' => 'toggle',
1006 1122 'label' => __( 'Nested Sub Category', 'betterdocs' ),
@@ -1005,9 +1121,9 @@
1005 1121 'type' => 'toggle',
1006 1122 'label' => __( 'Nested Sub Category', 'betterdocs' ),
1007 1123 'enable_disable_text_active' => true,
1008 1124 'default' => '',
1009 - 'priority' => 2
1125 + 'priority' => 3
1010 1126 ),
1011 1127 'column_number' => array(
1012 1128 'name' => 'column_number',
1013 1129 'type' => 'number',
@@ -1013,9 +1129,9 @@
1013 1129 'type' => 'number',
1014 1130 'label' => __( 'Number Of Columns', 'betterdocs' ),
1015 1131 'label_subtitle' => __( 'This setting is not applicable for sleek layout.', 'betterdocs' ),
1016 1132 'default' => 3,
1017 - 'priority' => 3
1133 + 'priority' => 4
1018 1134 ),
1019 1135 'posts_number' => apply_filters( 'betterdocs_posts_number', array(
1020 1136 'name' => 'posts_number',
1021 1137 'type' => 'number',
@@ -1021,9 +1137,9 @@
1021 1137 'type' => 'number',
1022 1138 'label' => __( 'Number Of Docs', 'betterdocs' ),
1023 1139 'label_subtitle' => __( 'This setting is not applicable for handbook layout.', 'betterdocs' ),
1024 1140 'default' => 10,
1025 - 'priority' => 4
1141 + 'priority' => 5
1026 1142 ) ),
1027 1143 'post_count' => array(
1028 1144 'name' => 'post_count',
1029 1145 'type' => 'toggle',
@@ -1029,9 +1145,9 @@
1029 1145 'type' => 'toggle',
1030 1146 'label' => __( 'Doc Count', 'betterdocs' ),
1031 1147 'enable_disable_text_active' => true,
1032 1148 'default' => 1,
1033 - 'priority' => 5
1149 + 'priority' => 6
1034 1150 ),
1035 1151 'count_text' => array(
1036 1152 'name' => 'count_text',
1037 1153 'type' => 'text',
@@ -1036,9 +1152,9 @@
1036 1152 'name' => 'count_text',
1037 1153 'type' => 'text',
1038 1154 'label' => __( 'Count Text', 'betterdocs' ),
1039 1155 'default' => __( 'Docs', 'betterdocs' ),
1040 - 'priority' => 6
1156 + 'priority' => 7
1041 1157 ),
1042 1158 'count_text_singular' => array(
1043 1159 'name' => 'count_text_singular',
1044 1160 'type' => 'text',
@@ -1043,9 +1159,9 @@
1043 1159 'name' => 'count_text_singular',
1044 1160 'type' => 'text',
1045 1161 'label' => __( 'Count Text Singular', 'betterdocs' ),
1046 1162 'default' => __( 'Doc', 'betterdocs' ),
1047 - 'priority' => 7
1163 + 'priority' => 8
1048 1164 ),
1049 1165 'exploremore_btn' => array(
1050 1166 'name' => 'exploremore_btn',
1051 1167 'type' => 'toggle',
@@ -1051,9 +1167,9 @@
1051 1167 'type' => 'toggle',
1052 1168 'label' => __( 'Explore More Button', 'betterdocs' ),
1053 1169 'enable_disable_text_active' => true,
1054 1170 'default' => true,
1055 - 'priority' => 8
1171 + 'priority' => 9
1056 1172 ),
1057 1173 'exploremore_btn_txt' => array(
1058 1174 'name' => 'exploremore_btn_txt',
1059 1175 'type' => 'text',
@@ -1058,9 +1174,9 @@
1058 1174 'name' => 'exploremore_btn_txt',
1059 1175 'type' => 'text',
1060 1176 'label' => __( 'Explore More Button Text', 'betterdocs' ),
1061 1177 'default' => __( 'Explore More', 'betterdocs' ),
1062 - 'priority' => 9,
1178 + 'priority' => 10,
1063 1179 'rules' => Rules::is( 'exploremore_btn', true )
1064 1180 ),
1065 1181 'betterdocs_popular_docs_text' => array(
1066 1182 'name' => 'betterdocs_popular_docs_text',
@@ -1066,9 +1182,9 @@
1066 1182 'name' => 'betterdocs_popular_docs_text',
1067 1183 'type' => 'text',
1068 1184 'label' => __( 'Popular Docs Text', 'betterdocs' ),
1069 1185 'default' => __( 'Popular Docs', 'betterdocs' ),
1070 - 'priority' => 10,
1186 + 'priority' => 11,
1071 1187 'is_pro' => true
1072 1188 ),
1073 1189 'betterdocs_popular_docs_number' => array(
1074 1190 'name' => 'betterdocs_popular_docs_number',
@@ -1074,9 +1190,9 @@
1074 1190 'name' => 'betterdocs_popular_docs_number',
1075 1191 'type' => 'number',
1076 1192 'label' => __( 'Popular Docs Number', 'betterdocs' ),
1077 1193 'default' => 10,
1078 - 'priority' => 11,
1194 + 'priority' => 12,
1079 1195 'is_pro' => true
1080 1196 )
1081 1197 )
1082 1198 ),
@@ -1320,8 +1436,44 @@
1320 1436 'enable_disable_text_active' => true,
1321 1437 'default' => 1,
1322 1438 'priority' => 3
1323 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 + ),
1324 1476 'enable_tags' => array(
1325 1477 'name' => 'enable_tags',
1326 1478 'type' => 'toggle',
1327 1479 'label' => __( 'Tags', 'betterdocs' ),
@@ -1326,9 +1478,9 @@
1326 1478 'type' => 'toggle',
1327 1479 'label' => __( 'Tags', 'betterdocs' ),
1328 1480 'enable_disable_text_active' => true,
1329 1481 'default' => 1,
1330 - 'priority' => 4
1482 + 'priority' => 8
1331 1483 ),
1332 1484 'show_last_update_time' => array(
1333 1485 'name' => 'show_last_update_time',
1334 1486 'type' => 'toggle',
@@ -1334,9 +1486,9 @@
1334 1486 'type' => 'toggle',
1335 1487 'label' => __( 'Last Update Time', 'betterdocs' ),
1336 1488 'enable_disable_text_active' => true,
1337 1489 'default' => 1,
1338 - 'priority' => 5
1490 + 'priority' => 9
1339 1491 ),
1340 1492 'enable_navigation' => array(
1341 1493 'name' => 'enable_navigation',
1342 1494 'type' => 'toggle',
@@ -1342,9 +1494,9 @@
1342 1494 'type' => 'toggle',
1343 1495 'label' => __( 'Navigation', 'betterdocs' ),
1344 1496 'enable_disable_text_active' => true,
1345 1497 'default' => 1,
1346 - 'priority' => 6
1498 + 'priority' => 10
1347 1499 ),
1348 1500 'enable_comment' => array(
1349 1501 'name' => 'enable_comment',
1350 1502 'type' => 'toggle',
@@ -1350,9 +1502,9 @@
1350 1502 'type' => 'toggle',
1351 1503 'label' => __( 'Comment', 'betterdocs' ),
1352 1504 'enable_disable_text_active' => true,
1353 1505 'default' => '',
1354 - 'priority' => 7
1506 + 'priority' => 11
1355 1507 ),
1356 1508 'enable_credit' => array(
1357 1509 'name' => 'enable_credit',
1358 1510 'type' => 'toggle',
@@ -1358,9 +1510,9 @@
1358 1510 'type' => 'toggle',
1359 1511 'label' => __( 'Show Powered by BetterDocs', 'betterdocs' ),
1360 1512 'enable_disable_text_active' => true,
1361 1513 'default' => '',
1362 - 'priority' => 8
1514 + 'priority' => 12
1363 1515 ),
1364 1516 'reaction_feedback_text' => array(
1365 1517 'name' => 'reaction_feedback_text',
1366 1518 'type' => 'text',
@@ -1365,9 +1517,9 @@
1365 1517 'name' => 'reaction_feedback_text',
1366 1518 'type' => 'text',
1367 1519 'label' => __( 'Reaction Feedback Text', 'betterdocs' ),
1368 1520 'default' => __( 'Thanks for your feedback.', 'betterdocs' ),
1369 - 'priority' => 9
1521 + 'priority' => 13
1370 1522 ),
1371 1523 'enable_estimated_reading_time' => array(
1372 1524 'name' => 'enable_estimated_reading_time',
1373 1525 'type' => 'toggle',
@@ -1373,9 +1525,9 @@
1373 1525 'type' => 'toggle',
1374 1526 'label' => __( 'Estimated Reading Time', 'betterdocs' ),
1375 1527 'enable_disable_text_active' => true,
1376 1528 'default' => 0,
1377 - 'priority' => 10
1529 + 'priority' => 14
1378 1530 ),
1379 1531 'estimated_reading_time_title' => array(
1380 1532 'name' => 'estimated_reading_time_title',
1381 1533 'type' => 'text',
@@ -1380,9 +1532,9 @@
1380 1532 'name' => 'estimated_reading_time_title',
1381 1533 'type' => 'text',
1382 1534 'label' => __( 'Estimated Reading Time Title', 'betterdocs' ),
1383 1535 'default' => '',
1384 - 'priority' => 11,
1536 + 'priority' => 15,
1385 1537 'rules' => Rules::is( 'enable_estimated_reading_time', true )
1386 1538 ),
1387 1539 'estimated_reading_time_text' => array(
1388 1540 'name' => 'estimated_reading_time_text',
@@ -1388,9 +1540,9 @@
1388 1540 'name' => 'estimated_reading_time_text',
1389 1541 'type' => 'text',
1390 1542 'label' => __( 'Estimated Reading Time Text', 'betterdocs' ),
1391 1543 'default' => __( 'min read', 'betterdocs' ),
1392 - 'priority' => 12,
1544 + 'priority' => 16,
1393 1545 'rules' => Rules::is( 'enable_estimated_reading_time', true )
1394 1546 ),
1395 1547 'singular_estimated_reading_time_text' => array(
1396 1548 'name' => 'singular_estimated_reading_time_text',
@@ -1396,9 +1548,9 @@
1396 1548 'name' => 'singular_estimated_reading_time_text',
1397 1549 'type' => 'text',
1398 1550 'label' => __( 'Estimated Reading Time Text Singular', 'betterdocs' ),
1399 1551 'default' => __( 'min read', 'betterdocs' ),
1400 - 'priority' => 13,
1552 + 'priority' => 17,
1401 1553 'rules' => Rules::is( 'enable_estimated_reading_time', true )
1402 1554 )
1403 1555 )
1404 1556 ),
@@ -1665,8 +1817,202 @@
1665 1817 'enable_disable_text_active' => true,
1666 1818 'default' => false
1667 1819 )
1668 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 + ) )
1669 2015 )
1670 2016 )
1671 2017 )
1672 2018 )
@@ -2278,18 +2624,84 @@
2278 2624 'open-ai-settings' => array(
2279 2625 'id' => 'open-ai-settings',
2280 2626 'name' => 'open-ai-settings',
2281 2627 'type' => 'section',
2282 - 'label' => __( 'API Settings', 'betterdocs' ),
2628 + 'label' => __( 'AI Platform & API', 'betterdocs' ),
2283 2629 'priority' => 1,
2284 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.
2285 2650 'ai_autowrite_api_key' => array(
2286 2651 'name' => 'ai_autowrite_api_key',
2287 2652 'type' => 'text',
2288 - 'label' => __( 'API Key', 'betterdocs' ),
2289 - 'label_subtitle' => sprintf( /* translators: %s is a link to the documentation about generating an OpenAI API key. */__( 'Check out this <a target="_blank" href="%s">documentation</a> to find out how to generate your OpenAI API Key.', 'betterdocs' ), esc_url( 'https://betterdocs.co/docs/write-with-ai/' ) ),
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/' ) ),
2290 2655 'default' => '',
2291 - '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()
2292 2704 )
2293 2705 )
2294 2706 ),
2295 2707 'write-with-ai' => array(
@@ -2298,8 +2710,39 @@
2298 2710 'type' => 'section',
2299 2711 'label' => __( 'Write with AI', 'betterdocs' ),
2300 2712 'priority' => 5,
2301 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(
2302 2745 'enable_write_with_ai' => array(
2303 2746 'name' => 'enable_write_with_ai',
2304 2747 'type' => 'toggle',
2305 2748 'priority' => 0,
@@ -2323,29 +2766,20 @@
2323 2766 'priority' => 6,
2324 2767 'label' => __( 'Write Glossaries with AI', 'betterdocs' ),
2325 2768 'label_subtitle' => __( 'Generate AI based Glossary definitions from the Glossaries admin page', 'betterdocs' ),
2326 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,
2327 2780 'default' => true
2328 2781 ),
2329 - 'write_with_ai_model' => array(
2330 - 'name' => 'write_with_ai_model',
2331 - 'type' => 'select',
2332 - 'label' => __( 'OpenAI Model*', 'betterdocs' ),
2333 - 'priority' => 20,
2334 - 'multiple' => false,
2335 - 'default' => 'gpt-4o-mini',
2336 - 'options' => GlobalFields::normalize_fields( array(
2337 - 'gpt-4o-mini' => 'GPT-4o Mini',
2338 - 'gpt-4o' => 'GPT-4o',
2339 - 'gpt-4.1-nano' => 'GPT-4.1 Nano',
2340 - 'gpt-4.1-mini' => 'GPT-4.1 Mini',
2341 - 'gpt-4.1' => 'GPT-4.1',
2342 - 'gpt-5-nano' => 'GPT-5 Nano',
2343 - 'gpt-5-mini' => 'GPT-5 Mini',
2344 - 'gpt-5' => 'GPT-5',
2345 - 'gpt-5.5' => 'GPT-5.5'
2346 - ) )
2347 - ),
2348 2782 'ai_autowrite_max_token' => array(
2349 2783 'name' => 'ai_autowrite_max_token',
2350 2784 'type' => 'min_token_number',
2351 2785 'label' => __( 'Set Max Tokens', 'betterdocs' ),
@@ -2352,13 +2786,57 @@
2352 2786 'label_subtitle' => sprintf( // translators: %s is a link to more information about token limits.
2353 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' ) ),
2354 2788 'default' => 2500,
2355 2789 'priority' => 10,
2356 - 'model_field' => 'write_with_ai_model',
2790 + 'model_field' => 'ai_model',
2357 2791 'context' => 'write_with_ai',
2358 2792 'min_token_map' => AIHelper::get_min_tokens_map( 'write_with_ai' ),
2359 2793 'classes' => 'wprf-type-text'
2360 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 + )
2361 2839 )
2362 2840 ),
2363 2841 'article-summary' => array(
2364 2842 'id' => 'article-summary',
@@ -2383,31 +2861,12 @@
2383 2861 'label_subtitle' => sprintf( // translators: %s is a link to more information about token limits.
2384 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' ) ),
2385 2863 'default' => 1500,
2386 2864 'priority' => 5,
2387 - 'model_field' => 'article_summary_model',
2865 + 'model_field' => 'ai_model',
2388 2866 'context' => 'article_summary',
2389 2867 'min_token_map' => AIHelper::get_min_tokens_map( 'article_summary' ),
2390 2868 'classes' => 'wprf-type-text'
2391 - ),
2392 - 'article_summary_model' => array(
2393 - 'name' => 'article_summary_model',
2394 - 'type' => 'select',
2395 - 'label' => __( 'OpenAI Model*', 'betterdocs' ),
2396 - 'priority' => 10,
2397 - 'multiple' => false,
2398 - 'default' => 'gpt-4o-mini',
2399 - 'options' => GlobalFields::normalize_fields( array(
2400 - 'gpt-4o-mini' => 'GPT-4o Mini',
2401 - 'gpt-4o' => 'GPT-4o',
2402 - 'gpt-4.1-nano' => 'GPT-4.1 Nano',
2403 - 'gpt-4.1-mini' => 'GPT-4.1 Mini',
2404 - 'gpt-4.1' => 'GPT-4.1',
2405 - 'gpt-5-nano' => 'GPT-5 Nano',
2406 - 'gpt-5-mini' => 'GPT-5 Mini',
2407 - 'gpt-5' => 'GPT-5',
2408 - 'gpt-5.5' => 'GPT-5.5'
2409 - ) )
2410 2869 )
2411 2870 )
2412 2871 )
2413 2872 )