Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
99.15% covered (success)
99.15%
116 / 117
n/a
0 / 0
CRAP
n/a
0 / 0
1<?php
2/**
3 * AI Answer block render.
4 *
5 * Renders the panel scaffold that the Interactivity store hydrates with the
6 * streaming AI answer. Inserting the block is its own opt-in switch — the
7 * `jetpack_search_ai_answers_enabled` option governs the overlay, not the block.
8 *
9 * The gates below (paid Search plan, host AI opt-out, Jetpack AI master
10 * switch) emit nothing when they fail, matching how WordAds / Premium
11 * Content hide on a failed plan check.
12 *
13 * @package automattic/jetpack-search
14 */
15
16namespace Automattic\Jetpack\Search;
17
18if ( ! defined( 'ABSPATH' ) ) {
19    exit( 0 );
20}
21
22// Paid-plan gate. AI Answer requires Jetpack Search's paid plan; on a free
23// or no-plan site the block contributes nothing to the page (no panel
24// scaffold, no `data-wp-interactive` div, no Interactivity hydration). The
25// editor surface shows an upgrade prompt instead — see `edit.js`.
26if ( ! Search_Blocks::supports_paid_search() ) {
27    return;
28}
29
30// Host-owner AI opt-out (core's wp_supports_ai() / WP_AI_SUPPORT). The overlay
31// already honors it through the plugin's filter gate; the block must agree.
32if ( ! AI_Answers::host_allows_ai() ) {
33    return;
34}
35
36if ( ! AI_Answers::is_master_enabled() ) {
37    return;
38}
39
40// $attributes is injected by WordPress at block-render time via the
41// `render_callback` include scope; static analysis can't see the binding,
42// so both phpcs and Phan need a one-line suppression on the next statement.
43// @phan-suppress-next-line PhanUndeclaredGlobalVariable
44$attrs           = (array) $attributes; // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
45$heading_raw     = trim( (string) ( $attrs['heading'] ?? '' ) );
46$heading         = '' === $heading_raw ? __( 'AI answer', 'jetpack-search-pkg' ) : $heading_raw;
47$show_citations  = ! isset( $attrs['showCitations'] ) || $attrs['showCitations'];
48$enable_extended = ! isset( $attrs['enableShowMore'] ) || $attrs['enableShowMore'];
49$wrapper_attrs   = get_block_wrapper_attributes(
50    array( 'class' => 'jp-search-answers-panel' )
51);
52?>
53<div
54    <?php echo wp_kses_data( $wrapper_attrs ); ?>
55    data-wp-interactive="jetpack-search"
56    data-wp-init="callbacks.initializeAiAnswer"
57    data-wp-bind--hidden="state.aiPanelHidden"
58    aria-live="polite"
59    hidden
60>
61    <h2 class="jp-search-answers-panel__heading"><?php echo esc_html( $heading ); ?></h2>
62
63    <div
64        class="jp-search-answers-panel__loading"
65        data-wp-bind--hidden="!state.aiIsLoading"
66        hidden
67    >
68        <?php esc_html_e( 'Finding an answer', 'jetpack-search-pkg' ); ?>
69        <span class="jp-search-animated-ellipsis" aria-hidden="true">
70            <span></span><span></span><span></span>
71        </span>
72    </div>
73
74    <div
75        class="jp-search-answers-panel__error"
76        role="alert"
77        data-wp-bind--hidden="!state.aiIsError"
78        hidden
79    >
80        <p
81            class="jp-search-answers-panel__error-message"
82            data-wp-text="state.aiErrorPrimary"
83        ></p>
84        <p
85            class="jp-search-answers-panel__error-detail"
86            data-wp-bind--hidden="!state.aiHasErrorDetail"
87        >
88            <span data-wp-text="state.aiErrorDetail"></span>
89            <span data-wp-bind--hidden="!state.aiHasErrorCode">
90                <br />
91                <span data-wp-text="state.aiErrorCodeText"></span>
92            </span>
93        </p>
94    </div>
95
96    <div
97        class="jp-search-answers-panel__content"
98        data-wp-bind--hidden="!state.aiHasContent"
99        hidden
100    >
101        <div
102            class="jp-search-answers-panel__text"
103            data-wp-watch="callbacks.renderAiAnswerHtml"
104        ></div>
105
106        <?php if ( $show_citations ) : ?>
107        <ul
108            class="jp-search-answers-panel__citations"
109            data-wp-bind--hidden="!state.aiHasCitations"
110            hidden
111        >
112            <template
113                data-wp-each--citation="state.aiVisibleCitations"
114                data-wp-each-key="context.citation.key"
115            >
116                <li>
117                    <a
118                        data-wp-bind--href="context.citation.href"
119                        target="_blank"
120                        rel="noopener noreferrer"
121                    >
122                        <span data-wp-text="context.citation.title"></span>
123                        <svg
124                            width="10"
125                            height="10"
126                            viewBox="0 0 10 10"
127                            fill="none"
128                            xmlns="http://www.w3.org/2000/svg"
129                            aria-hidden="true"
130                            class="jp-search-answers-panel__citation-icon"
131                        >
132                            <path d="M1 9L9 1M9 1H5M9 1V5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
133                        </svg>
134                    </a>
135                </li>
136            </template>
137        </ul>
138        <?php endif; ?>
139    </div>
140
141    <p
142        class="jp-search-answers-panel__loading-hint"
143        data-wp-bind--hidden="!state.aiExtendedLoadingHintShown"
144        hidden
145    >
146        <span data-wp-text="state.aiExtendedLoadingText"></span>
147        <span class="jp-search-animated-ellipsis" aria-hidden="true">
148            <span></span><span></span><span></span>
149        </span>
150    </p>
151
152    <?php if ( $enable_extended ) : ?>
153    <button
154        type="button"
155        class="jp-search-answers-panel__toggle"
156        data-wp-bind--hidden="!state.aiShowExtendedButton"
157        data-wp-on--click="actions.showExtendedAiAnswer"
158        hidden
159    >
160        <?php esc_html_e( 'Show more', 'jetpack-search-pkg' ); ?>
161        <span class="jp-search-answers-panel__toggle-icon" aria-hidden="true"></span>
162    </button>
163    <?php endif; ?>
164</div>