| @@ -10,8 +10,9 @@ | ||
| 10 | 10 | use WP_User; |
| 11 | 11 | use WPDeveloper\BetterDocs\Admin\Builder\GlobalFields; |
| 12 | 12 | use WPDeveloper\BetterDocs\Admin\Builder\Rules; |
| 13 | 13 | use WPDeveloper\BetterDocs\AI\ModelRegistry; |
| 14 | +use WPDeveloper\BetterDocs\AI\ProviderFactory; | |
| 14 | 15 | use WPDeveloper\BetterDocs\REST\AIEdit; |
| 15 | 16 | use WPDeveloper\BetterDocs\Utils\AIHelper; |
| 16 | 17 | use WPDeveloper\BetterDocs\Utils\Base; |
| 17 | 18 | use WPDeveloper\BetterDocs\Utils\Database; |
| @@ -53,9 +54,8 @@ | ||
| 53 | 54 | |
| 54 | 55 | add_action( 'wp_ajax_betterdocs_dark_mode', array( $this, 'dark_mode' ) ); |
| 55 | 56 | add_filter( 'betterdocs_settings_tab_advance', array( $this, 'hide_roles_management' ), 11, 1 ); |
| 56 | 57 | add_action( 'betterdocs::settings::saved', array( $this, 'fallback_slugs' ), 99, 3 ); |
| 57 | - add_action( 'admin_init', array( $this, 'maybe_migrate_ai_platform_settings' ) ); | |
| 58 | 58 | } |
| 59 | 59 | |
| 60 | 60 | public function fallback_slugs( $_saved, $_settings, $_old_settings = array() ) { |
| 61 | 61 | $_default = $this->get_default(); |
| @@ -82,72 +82,28 @@ | ||
| 82 | 82 | /** |
| 83 | 83 | * Settings keys holding secret API keys. These are masked before reaching |
| 84 | 84 | * the browser, stripped for non-admins, and never persisted as their mask. |
| 85 | 85 | * |
| 86 | - * Includes the legacy single-key fields plus the per-platform content-suite | |
| 87 | - * keys introduced with multi-platform support. | |
| 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. | |
| 88 | 89 | * |
| 89 | 90 | * @return array<int,string> |
| 90 | 91 | */ |
| 91 | 92 | public static function sensitive_api_key_fields() { |
| 92 | 93 | $fields = array( |
| 93 | - 'ai_autowrite_api_key', | |
| 94 | + ProviderFactory::OPENAI_KEY_FIELD, | |
| 94 | 95 | 'ai_chatbot_api_key', |
| 95 | 96 | ); |
| 96 | 97 | foreach ( array_keys( ModelRegistry::platforms() ) as $platform ) { |
| 97 | - $fields[] = 'ai_api_key_' . $platform; | |
| 98 | + $fields[] = ProviderFactory::key_field_for( $platform ); | |
| 98 | 99 | } |
| 99 | 100 | // Add-ons (e.g. the AI Chatbot) register their own per-platform keys here |
| 100 | 101 | // so they are masked in the browser and stripped for non-admins. |
| 101 | - return apply_filters( 'betterdocs_sensitive_api_key_fields', $fields ); | |
| 102 | + return apply_filters( 'betterdocs_sensitive_api_key_fields', array_values( array_unique( $fields ) ) ); | |
| 102 | 103 | } |
| 103 | 104 | |
| 104 | 105 | /** |
| 105 | - * One-time copy of pre-multi-platform AI settings onto the new keys so the | |
| 106 | - * settings UI shows the user's existing OpenAI key and model under the new | |
| 107 | - * platform-aware fields. Runtime already falls back via ProviderFactory, so | |
| 108 | - * this only affects what the admin sees. Guarded by an option flag. | |
| 109 | - * | |
| 110 | - * Runs on `admin_init`, which fires for every logged-in user who loads | |
| 111 | - * /wp-admin/ — including a Subscriber. Since this writes settings (and copies | |
| 112 | - * the legacy OpenAI key onto `ai_api_key_openai`), it is gated on the same | |
| 113 | - * `edit_docs_settings` capability used everywhere else for settings writes. | |
| 114 | - * The one-shot flag is deliberately NOT set when the capability check fails, | |
| 115 | - * otherwise the first low-privilege page load would permanently skip the | |
| 116 | - * migration for administrators too. | |
| 117 | - * | |
| 118 | - * @return void | |
| 119 | - */ | |
| 120 | - public function maybe_migrate_ai_platform_settings() { | |
| 121 | - if ( get_option( 'betterdocs_ai_platform_migrated' ) ) { | |
| 122 | - return; | |
| 123 | - } | |
| 124 | - | |
| 125 | - if ( ! current_user_can( 'edit_docs_settings' ) ) { | |
| 126 | - return; | |
| 127 | - } | |
| 128 | - | |
| 129 | - if ( '' === (string) $this->get_raw_field( 'ai_platform', '' ) ) { | |
| 130 | - $this->save( 'ai_platform', 'openai' ); | |
| 131 | - } | |
| 132 | - | |
| 133 | - $legacy_key = (string) $this->get( 'ai_autowrite_api_key', '' ); | |
| 134 | - if ( '' !== $legacy_key && '' === (string) $this->get_raw_field( 'ai_api_key_openai', '' ) ) { | |
| 135 | - $this->save( 'ai_api_key_openai', $legacy_key ); | |
| 136 | - } | |
| 137 | - | |
| 138 | - if ( '' === (string) $this->get_raw_field( 'ai_model', '' ) ) { | |
| 139 | - $legacy_model = (string) $this->get( 'write_with_ai_model', '' ); | |
| 140 | - if ( '' === $legacy_model ) { | |
| 141 | - $legacy_model = (string) $this->get( 'article_summary_model', 'gpt-4o-mini' ); | |
| 142 | - } | |
| 143 | - $this->save( 'ai_model', $legacy_model !== '' ? $legacy_model : 'gpt-4o-mini' ); | |
| 144 | - } | |
| 145 | - | |
| 146 | - update_option( 'betterdocs_ai_platform_migrated', 1 ); | |
| 147 | - } | |
| 148 | - | |
| 149 | - /** | |
| 150 | 106 | * This method is responsible for enqueueing scripts in settings panel |
| 151 | 107 | * |
| 152 | 108 | * @param string $hook |
| 153 | 109 | * |
| @@ -327,8 +283,12 @@ | ||
| 327 | 283 | 'enable_breadcrumb_category' => true, |
| 328 | 284 | 'enable_breadcrumb_title' => true, |
| 329 | 285 | 'enable_sidebar_cat_list' => true, |
| 330 | 286 | 'enable_print_icon' => true, |
| 287 | + 'print_enable_logo' => false, | |
| 288 | + 'print_logo' => array(), | |
| 289 | + 'print_enable_footer' => false, | |
| 290 | + 'print_footer_text' => '', | |
| 331 | 291 | 'enable_tags' => true, |
| 332 | 292 | 'email_feedback' => true, |
| 333 | 293 | 'feedback_link_text' => __( 'Still stuck? How can we help?', 'betterdocs' ), |
| 334 | 294 | 'reaction_feedback_text' => __( 'Thanks for your feedback', 'betterdocs' ), |
| @@ -367,13 +327,13 @@ | ||
| 367 | 327 | 'article_summary_model' => 'gpt-4o-mini', |
| 368 | 328 | 'article_summary_max_token' => 1500, |
| 369 | 329 | // Multi-platform AI (content suite). `ai_platform` selects the active |
| 370 | 330 | // provider; `ai_model` is the single global model; keys are stored |
| 371 | - // per platform so switching never loses a saved key. Legacy keys | |
| 372 | - // above are kept for back-compat and migrated on upgrade. | |
| 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. | |
| 373 | 334 | 'ai_platform' => 'openai', |
| 374 | 335 | 'ai_model' => 'gpt-4o-mini', |
| 375 | - 'ai_api_key_openai' => '', | |
| 376 | 336 | 'ai_api_key_gemini' => '', |
| 377 | 337 | 'ai_api_key_claude' => '', |
| 378 | 338 | 'ai_api_key_deepseek' => '', |
| 379 | 339 | 'ai_api_key_openrouter' => '', |
| @@ -387,9 +347,40 @@ | ||
| 387 | 347 | 'singular_estimated_reading_time_text' => __( 'min read', 'betterdocs' ), |
| 388 | 348 | 'betterdocs_access_control_repeater' => array(), |
| 389 | 349 | 'internal_knowledge_base_type' => 'basic', |
| 390 | 350 | 'betterdocs_access_control_repeater_kb' => array(), |
| 391 | - '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' => '' | |
| 392 | 383 | ); |
| 393 | 384 | |
| 394 | 385 | $_default = apply_filters( 'betterdocs_default_settings', $_default ); |
| 395 | 386 | // $_default = apply_filters_deprecated( |
| @@ -761,21 +752,38 @@ | ||
| 761 | 752 | betterdocs()->kbmigration->migrate(); |
| 762 | 753 | } |
| 763 | 754 | $_settings = wp_parse_args( $_normalized_settings, $_old_settings ); |
| 764 | 755 | |
| 765 | - // Check if there are actual changes before saving. | |
| 766 | - // update_option returns false when values serialize identically, which can happen | |
| 767 | - // due to object caching or type normalization even when user made changes. | |
| 768 | - $_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; | |
| 769 | 778 | |
| 770 | 779 | $_saved = $this->database->save( $this->base_key, $_settings ); |
| 771 | 780 | |
| 772 | 781 | do_action_ref_array( 'betterdocs::settings::saved', array( $_saved, $_settings, $_old_settings, &$this ) ); |
| 773 | 782 | |
| 774 | - // Return true if save succeeded OR if there were changes to attempt saving. | |
| 775 | - // This handles cases where update_option returns false due to identical serialization | |
| 776 | - // (e.g., object caching, type coercion during serialization). | |
| 777 | - 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; | |
| 778 | 786 | } |
| 779 | 787 | |
| 780 | 788 | public function views( $hook ) { |
| 781 | 789 | return betterdocs()->views->get( 'admin/settings' ); |
| @@ -1428,8 +1436,44 @@ | ||
| 1428 | 1436 | 'enable_disable_text_active' => true, |
| 1429 | 1437 | 'default' => 1, |
| 1430 | 1438 | 'priority' => 3 |
| 1431 | 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 | + ), | |
| 1432 | 1476 | 'enable_tags' => array( |
| 1433 | 1477 | 'name' => 'enable_tags', |
| 1434 | 1478 | 'type' => 'toggle', |
| 1435 | 1479 | 'label' => __( 'Tags', 'betterdocs' ), |
| @@ -1434,9 +1478,9 @@ | ||
| 1434 | 1478 | 'type' => 'toggle', |
| 1435 | 1479 | 'label' => __( 'Tags', 'betterdocs' ), |
| 1436 | 1480 | 'enable_disable_text_active' => true, |
| 1437 | 1481 | 'default' => 1, |
| 1438 | - 'priority' => 4 | |
| 1482 | + 'priority' => 8 | |
| 1439 | 1483 | ), |
| 1440 | 1484 | 'show_last_update_time' => array( |
| 1441 | 1485 | 'name' => 'show_last_update_time', |
| 1442 | 1486 | 'type' => 'toggle', |
| @@ -1442,9 +1486,9 @@ | ||
| 1442 | 1486 | 'type' => 'toggle', |
| 1443 | 1487 | 'label' => __( 'Last Update Time', 'betterdocs' ), |
| 1444 | 1488 | 'enable_disable_text_active' => true, |
| 1445 | 1489 | 'default' => 1, |
| 1446 | - 'priority' => 5 | |
| 1490 | + 'priority' => 9 | |
| 1447 | 1491 | ), |
| 1448 | 1492 | 'enable_navigation' => array( |
| 1449 | 1493 | 'name' => 'enable_navigation', |
| 1450 | 1494 | 'type' => 'toggle', |
| @@ -1450,9 +1494,9 @@ | ||
| 1450 | 1494 | 'type' => 'toggle', |
| 1451 | 1495 | 'label' => __( 'Navigation', 'betterdocs' ), |
| 1452 | 1496 | 'enable_disable_text_active' => true, |
| 1453 | 1497 | 'default' => 1, |
| 1454 | - 'priority' => 6 | |
| 1498 | + 'priority' => 10 | |
| 1455 | 1499 | ), |
| 1456 | 1500 | 'enable_comment' => array( |
| 1457 | 1501 | 'name' => 'enable_comment', |
| 1458 | 1502 | 'type' => 'toggle', |
| @@ -1458,9 +1502,9 @@ | ||
| 1458 | 1502 | 'type' => 'toggle', |
| 1459 | 1503 | 'label' => __( 'Comment', 'betterdocs' ), |
| 1460 | 1504 | 'enable_disable_text_active' => true, |
| 1461 | 1505 | 'default' => '', |
| 1462 | - 'priority' => 7 | |
| 1506 | + 'priority' => 11 | |
| 1463 | 1507 | ), |
| 1464 | 1508 | 'enable_credit' => array( |
| 1465 | 1509 | 'name' => 'enable_credit', |
| 1466 | 1510 | 'type' => 'toggle', |
| @@ -1466,9 +1510,9 @@ | ||
| 1466 | 1510 | 'type' => 'toggle', |
| 1467 | 1511 | 'label' => __( 'Show Powered by BetterDocs', 'betterdocs' ), |
| 1468 | 1512 | 'enable_disable_text_active' => true, |
| 1469 | 1513 | 'default' => '', |
| 1470 | - 'priority' => 8 | |
| 1514 | + 'priority' => 12 | |
| 1471 | 1515 | ), |
| 1472 | 1516 | 'reaction_feedback_text' => array( |
| 1473 | 1517 | 'name' => 'reaction_feedback_text', |
| 1474 | 1518 | 'type' => 'text', |
| @@ -1473,9 +1517,9 @@ | ||
| 1473 | 1517 | 'name' => 'reaction_feedback_text', |
| 1474 | 1518 | 'type' => 'text', |
| 1475 | 1519 | 'label' => __( 'Reaction Feedback Text', 'betterdocs' ), |
| 1476 | 1520 | 'default' => __( 'Thanks for your feedback.', 'betterdocs' ), |
| 1477 | - 'priority' => 9 | |
| 1521 | + 'priority' => 13 | |
| 1478 | 1522 | ), |
| 1479 | 1523 | 'enable_estimated_reading_time' => array( |
| 1480 | 1524 | 'name' => 'enable_estimated_reading_time', |
| 1481 | 1525 | 'type' => 'toggle', |
| @@ -1481,9 +1525,9 @@ | ||
| 1481 | 1525 | 'type' => 'toggle', |
| 1482 | 1526 | 'label' => __( 'Estimated Reading Time', 'betterdocs' ), |
| 1483 | 1527 | 'enable_disable_text_active' => true, |
| 1484 | 1528 | 'default' => 0, |
| 1485 | - 'priority' => 10 | |
| 1529 | + 'priority' => 14 | |
| 1486 | 1530 | ), |
| 1487 | 1531 | 'estimated_reading_time_title' => array( |
| 1488 | 1532 | 'name' => 'estimated_reading_time_title', |
| 1489 | 1533 | 'type' => 'text', |
| @@ -1488,9 +1532,9 @@ | ||
| 1488 | 1532 | 'name' => 'estimated_reading_time_title', |
| 1489 | 1533 | 'type' => 'text', |
| 1490 | 1534 | 'label' => __( 'Estimated Reading Time Title', 'betterdocs' ), |
| 1491 | 1535 | 'default' => '', |
| 1492 | - 'priority' => 11, | |
| 1536 | + 'priority' => 15, | |
| 1493 | 1537 | 'rules' => Rules::is( 'enable_estimated_reading_time', true ) |
| 1494 | 1538 | ), |
| 1495 | 1539 | 'estimated_reading_time_text' => array( |
| 1496 | 1540 | 'name' => 'estimated_reading_time_text', |
| @@ -1496,9 +1540,9 @@ | ||
| 1496 | 1540 | 'name' => 'estimated_reading_time_text', |
| 1497 | 1541 | 'type' => 'text', |
| 1498 | 1542 | 'label' => __( 'Estimated Reading Time Text', 'betterdocs' ), |
| 1499 | 1543 | 'default' => __( 'min read', 'betterdocs' ), |
| 1500 | - 'priority' => 12, | |
| 1544 | + 'priority' => 16, | |
| 1501 | 1545 | 'rules' => Rules::is( 'enable_estimated_reading_time', true ) |
| 1502 | 1546 | ), |
| 1503 | 1547 | 'singular_estimated_reading_time_text' => array( |
| 1504 | 1548 | 'name' => 'singular_estimated_reading_time_text', |
| @@ -1504,9 +1548,9 @@ | ||
| 1504 | 1548 | 'name' => 'singular_estimated_reading_time_text', |
| 1505 | 1549 | 'type' => 'text', |
| 1506 | 1550 | 'label' => __( 'Estimated Reading Time Text Singular', 'betterdocs' ), |
| 1507 | 1551 | 'default' => __( 'min read', 'betterdocs' ), |
| 1508 | - 'priority' => 13, | |
| 1552 | + 'priority' => 17, | |
| 1509 | 1553 | 'rules' => Rules::is( 'enable_estimated_reading_time', true ) |
| 1510 | 1554 | ) |
| 1511 | 1555 | ) |
| 1512 | 1556 | ), |
| @@ -1773,8 +1817,202 @@ | ||
| 1773 | 1817 | 'enable_disable_text_active' => true, |
| 1774 | 1818 | 'default' => false |
| 1775 | 1819 | ) |
| 1776 | 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 | + ) ) | |
| 1777 | 2015 | ) |
| 1778 | 2016 | ) |
| 1779 | 2017 | ) |
| 1780 | 2018 | ) |
| @@ -2400,16 +2638,18 @@ | ||
| 2400 | 2638 | 'default' => 'openai', |
| 2401 | 2639 | // DeepSeek and OpenRouter are temporarily hidden from the |
| 2402 | 2640 | // dropdown. Filter the options here, NOT ModelRegistry::platforms() — |
| 2403 | 2641 | // sensitive_api_key_fields() loops the registry to mask each |
| 2404 | - // ai_api_key_{platform}, so trimming the registry would silently | |
| 2642 | + // platform's key field, so trimming the registry would silently | |
| 2405 | 2643 | // un-mask those keys. Re-enable later by dropping the array_diff_key. |
| 2406 | 2644 | 'options' => GlobalFields::normalize_fields( |
| 2407 | 2645 | array_diff_key( ModelRegistry::platforms(), array_flip( array( 'deepseek', 'openrouter' ) ) ) |
| 2408 | 2646 | ) |
| 2409 | 2647 | ), |
| 2410 | - 'ai_api_key_openai' => array( | |
| 2411 | - 'name' => 'ai_api_key_openai', | |
| 2648 | + // OpenAI's key field keeps its original name so installs | |
| 2649 | + // that already saved a Write with AI key keep it. | |
| 2650 | + 'ai_autowrite_api_key' => array( | |
| 2651 | + 'name' => 'ai_autowrite_api_key', | |
| 2412 | 2652 | 'type' => 'text', |
| 2413 | 2653 | 'label' => __( 'OpenAI API Key', 'betterdocs' ), |
| 2414 | 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/' ) ), |
| 2415 | 2655 | 'default' => '', |
| @@ -2526,9 +2766,10 @@ | ||
| 2526 | 2766 | 'priority' => 6, |
| 2527 | 2767 | 'label' => __( 'Write Glossaries with AI', 'betterdocs' ), |
| 2528 | 2768 | 'label_subtitle' => __( 'Generate AI based Glossary definitions from the Glossaries admin page', 'betterdocs' ), |
| 2529 | 2769 | 'enable_disable_text_active' => true, |
| 2530 | - 'default' => true | |
| 2770 | + 'default' => true, | |
| 2771 | + 'is_pro' => true | |
| 2531 | 2772 | ), |
| 2532 | 2773 | 'enable_docs_ai_suite' => array( |
| 2533 | 2774 | 'name' => 'enable_docs_ai_suite', |
| 2534 | 2775 | 'type' => 'toggle', |