Snapshot: MLS sync fixes, image refresh, plugin/theme updates

MLS plugin fixes from this session:
- Fix silent insert failures: location column NOT NULL was rejecting wpdb->insert calls,
  causing ~18k new properties since Dec 2025 to be lost. Inserts now build raw SQL
  with ST_PointFromText so the spatial column is populated atomically.
- Auto-refresh expired media URLs in MLS_Media_Handler::fetch_and_cache(), guarded by
  a property-level GET_LOCK so concurrent fetches share one API refresh.
- Normalize WP_Error to null in mls_get_property_image() so callers can rely on the
  documented string|null contract.
- Support comma-separated property_type filters in MLS_Query and MLS_Cluster so the
  homepage "View All Commercial" link (?property_type=Commercial+Sale,Land,Farm)
  actually filters correctly.
- Incremental sync now looks back 10 minutes past the latest modification timestamp
  as a safety margin against missed records.
- Smart sync exits silently (info-level, not warning) when a full sync is in progress.

Operational:
- New cron: weekly full sync Sundays at 3 AM (/usr/local/bin/mls-full-sync).
- New cron: hourly 2GB cap on mls-thumbnails/ and cache/transformed-images/
  (/usr/local/bin/mls-image-cache-cap).
- Logrotate config for wp-content/debug.log (2-day retention, daily rotation,
  delaycompress).

Repo policy:
- CLAUDE.md updated with explicit "commit everything except build artifacts" policy.
- .gitignore: untrack runtime image caches and debug.log rotations.

Other modifications in this snapshot are pre-existing in-flight theme/plugin/db_content_updates work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
root
2026-04-29 15:32:23 +00:00
parent 57b752f54e
commit b6df4dbb92
5385 changed files with 838580 additions and 2416 deletions
@@ -0,0 +1,170 @@
/* global wpforms_ai_chat_element */
/**
* @param wpforms_ai_chat_element.ajaxurl
* @param wpforms_ai_chat_element.errors.network
* @param wpforms_ai_chat_element.errors.default
*/
/**
* The WPForms AI API wrapper.
*
* @since 1.9.1
*
* @return {Function} The app cloning function.
*/
export default function() { // eslint-disable-line no-unused-vars, max-lines-per-function
/**
* Public functions and properties.
*
* @since 1.9.1
*
* @type {Object}
*/
const app = {
/**
* AI chat mode.
*
* @since 1.9.1
*
* @type {string}
*/
mode: '',
/**
* AI AJAX actions.
*
* @since 1.9.1
*
* @type {Object}
*/
actions: {
rate: 'wpforms_rate_ai_response',
choices: 'wpforms_get_ai_choices',
forms: 'wpforms_get_ai_form',
},
/**
* AJAX request.
*
* @param {Object} data Data to send.
*
* @return {Promise} The fetch result data promise.
*/
// eslint-disable-next-line complexity
async ajax( data ) {
if ( ! data.nonce ) {
data.nonce = wpforms_ai_chat_element.nonce;
}
const options = {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams( data ).toString(),
};
const response = await fetch( wpforms_ai_chat_element.ajaxurl, options )
.catch( ( error ) => {
if ( error.message === 'Failed to fetch' ) {
throw new Error( wpforms_ai_chat_element.errors.network );
} else {
throw new Error( error.message );
}
} );
if ( ! response.ok ) {
throw new Error( wpforms_ai_chat_element.errors.network );
}
const result = await response.json();
if ( ! result.success || result.data?.error ) {
throw new Error(
result.data?.error ?? wpforms_ai_chat_element.errors.default,
{
cause: result.data?.code ?? 400,
} );
}
return result.data;
},
/**
* Prompt.
*
* @param {string} prompt The question to ask.
* @param {string} sessionId Session ID.
*
* @return {Promise} The response data in promise.
*/
async prompt( prompt, sessionId ) {
const data = {
action: app.actions[ this.mode ] ?? app.actions.choices,
prompt,
};
if ( sessionId ) {
data.session_id = sessionId; // eslint-disable-line camelcase
}
return app.ajax( data );
},
/**
* Rate.
*
* @param {boolean} helpful Whether the response was helpful or not.
* @param {string} responseId Response ID.
*
* @return {Promise} The response data in promise.
*/
async rate( helpful, responseId ) {
const data = {
action: app.actions.rate,
helpful,
response_id: responseId, // eslint-disable-line camelcase
};
return app.ajax( data );
},
setUp() {
app.actions = {
...app.actions,
...wpforms_ai_chat_element.actions,
};
return this;
},
/**
* Set the AI chat mode.
*
* @since 1.9.1
*
* @param {string} mode The mode to set.
*
* @return {Object} The app object.
*/
setMode( mode ) {
this.mode = mode;
return this;
},
};
/**
* Return a clone of an app object.
*
* @since 1.9.1
*
* @param {string} mode The AI prompt mode.
*
* @return {Object} Cloned app object.
*/
return function( mode ) {
const obj = { ...app };
return obj.setUp().setMode( mode );
};
}
@@ -0,0 +1 @@
export default function(){let t={mode:"",actions:{rate:"wpforms_rate_ai_response",choices:"wpforms_get_ai_choices",forms:"wpforms_get_ai_form"},async ajax(e){e.nonce||(e.nonce=wpforms_ai_chat_element.nonce);e={method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams(e).toString()},e=await fetch(wpforms_ai_chat_element.ajaxurl,e).catch(e=>{throw"Failed to fetch"===e.message?new Error(wpforms_ai_chat_element.errors.network):new Error(e.message)});if(!e.ok)throw new Error(wpforms_ai_chat_element.errors.network);e=await e.json();if(!e.success||e.data?.error)throw new Error(e.data?.error??wpforms_ai_chat_element.errors.default,{cause:e.data?.code??400});return e.data},async prompt(e,r){e={action:t.actions[this.mode]??t.actions.choices,prompt:e};return r&&(e.session_id=r),t.ajax(e)},async rate(e,r){e={action:t.actions.rate,helpful:e,response_id:r};return t.ajax(e)},setUp(){return t.actions={...t.actions,...wpforms_ai_chat_element.actions},this},setMode(e){return this.mode=e,this}};return function(e){return{...t}.setUp().setMode(e)}}
@@ -0,0 +1,319 @@
/* global WPFormsAIChatHTMLElement, WPFormsBuilder, wpf, wpforms_builder */
/**
* The WPForms AI chat element.
*
* Choices helpers module.
*
* @since 1.9.1
*
* @param {WPFormsAIChatHTMLElement} chat The chat element.
*
* @return {Object} The choices' helpers object.
*/
export default function( chat ) { // eslint-disable-line max-lines-per-function
/**
* The `choices` mode helpers object.
*
* @since 1.9.1
*/
return {
/**
* Get the `choices` answer based on AI response data.
*
* @since 1.9.1
*
* @param {Object} response The response data.
*
* @return {string} Answer HTML markup.
*/
getAnswer( response ) {
if ( response.choices?.length < 1 ) {
return '';
}
const li = [];
for ( const i in response.choices ) {
li.push( `
<li class="wpforms-ai-chat-choices-item">
${ chat.htmlSpecialChars( response.choices[ i ] ) }
</li>
` );
}
let answerHtml = `
<h4>${ chat.htmlSpecialChars( response.heading ?? '' ) }</h4>
<ol>
${ li.join( '' ) }
</ol>
`;
// Add footer to the first answer only.
if ( ! chat.sessionId ) {
answerHtml += `<span>${ chat.modeStrings.footer }</span>`;
}
return answerHtml;
},
/**
* Get the answer pre-buttons HTML markup.
*
* @since 1.9.1
*
* @return {string} The answer pre-buttons HTML markup.
*/
getAnswerButtonsPre() {
return `
<button type="button" class="wpforms-ai-chat-choices-insert wpforms-ai-chat-answer-action wpforms-btn-sm wpforms-btn-orange" >
<span>${ chat.modeStrings.insert }</span>
</button>
`;
},
/**
* Get the warning message HTML markup.
*
* @since 1.9.1
*
* @return {string} The warning message HTML markup.
*/
getWarningMessage() {
// Trigger event before warning message insert.
chat.triggerEvent( 'wpformsAIModalBeforeWarningMessageInsert', { fieldId: chat.fieldId } );
return `<div class="wpforms-ai-chat-divider"></div>
<div class="wpforms-chat-item-notice">
<div class="wpforms-chat-item-notice-content">
<span>${ chat.modeStrings.warning }</span>
</div>
</div>`;
},
/**
* If the field has default choices, the welcome screen is active.
*
* @since 1.9.1
*
* @return {boolean} True if the field has default choices, false otherwise.
*/
isWelcomeScreen() {
const items = document.getElementById( `wpforms-field-option-row-${ chat.fieldId }-choices` )
.querySelectorAll( 'li input.label' );
if ( items.length === 1 && ! items[ 0 ].value.trim() ) {
return true;
}
if ( items.length > 3 ) {
return false;
}
const defaults = Object.values( chat.modeStrings.defaults );
for ( let i = 0; i < items.length; i++ ) {
if ( ! defaults.includes( items[ i ].value ) ) {
return false;
}
}
return true;
},
/**
* Add the `choices` answer.
*
* @since 1.9.1
*
* @param {HTMLElement} element The answer element.
*/
addedAnswer( element ) {
const button = element.querySelector( '.wpforms-ai-chat-choices-insert' );
// Listen to the button click event.
button?.addEventListener( 'click', this.insertButtonClick.bind( this ) );
},
/**
* Sanitize response.
*
* @since 1.9.2
*
* @param {Object} response The response data to sanitize.
*
* @return {Object} The sanitized response.
*/
sanitizeResponse( response ) {
if ( ! Array.isArray( response?.choices ) ) {
return response;
}
let choices = response.choices;
// Sanitize choices.
choices = choices.map( ( choice ) => {
return wpf.sanitizeHTML( choice, wpforms_builder.allowed_label_html_tags );
} );
// Remove empty choices.
response.choices = choices.filter( ( choice ) => {
return choice.trim() !== '';
} );
return response;
},
/**
* Check if the response has a prohibited code.
*
* @since 1.9.2
*
* @param {Object} response The response data.
* @param {Array} sanitizedResponse The sanitized response data.
*
* @return {boolean} Whether the answer has a prohibited code.
*/
hasProhibitedCode( response, sanitizedResponse ) {
// If the number of choices has changed after sanitization, it means that the answer contains prohibited code.
return sanitizedResponse?.choices?.length !== response?.choices?.length;
},
/**
* Click on the Use Choices button.
*
* @since 1.9.1
*
* @param {Event} e The event object.
*/
insertButtonClick( e ) {
const button = e.target;
const answer = button.closest( '.wpforms-chat-item.wpforms-chat-item-choices' );
const responseId = answer?.getAttribute( 'data-response-id' );
const choicesList = answer?.querySelector( 'ol' );
const items = choicesList.querySelectorAll( '.wpforms-ai-chat-choices-item' );
const choiceItems = [];
// Get choices data.
for ( const i in items ) {
if ( ! items.hasOwnProperty( i ) || ! items[ i ].textContent ) {
continue;
}
choiceItems.push( items[ i ].textContent.trim() );
}
// Rate the response.
chat.wpformsAiApi.rate( true, responseId );
// Replace field choices.
this.replaceChoices( choiceItems );
// Toggle to the field.
jQuery( `#wpforms-field-${ chat.fieldId }` ).click().promise().done( function() {
jQuery( `#wpforms-field-option-basic-${ chat.fieldId } a.wpforms-field-option-group-toggle` ).click();
} );
},
/**
* Replace field choices.
*
* @since 1.9.1
*
* @param {Array} choices Choices array.
*/
replaceChoices( choices ) {
const choicesOptionRow = document.getElementById( `wpforms-field-option-row-${ chat.fieldId }-choices` );
const choicesList = choicesOptionRow.querySelector( 'ul.choices-list' );
const choiceRow = choicesList.querySelector( 'li:first-child' ).cloneNode( true );
choiceRow.innerHTML = choiceRow.innerHTML.replace( /\[choices\]\[\d+\]/g, `[choices][{{key}}]` );
// Clear existing choices.
choicesList.innerHTML = '';
// Add new choices.
for ( const i in choices ) {
const key = ( Number( i ) + 1 ).toString();
const choice = choices[ i ];
// Clone choice item element.
let li = choiceRow.cloneNode( true );
// Get updated single choice item.
li = this.getUpdatedSingleChoiceItem( li, key, choice );
// Add new choice item.
choicesList.appendChild( li );
}
// Update data-next-id attribute for choices list.
choicesList.setAttribute( 'data-next-id', choices.length + 1 );
// Update field preview.
const fieldOptions = document.getElementById( `wpforms-field-option-${ chat.fieldId }` );
const fieldType = fieldOptions.querySelector( 'input.wpforms-field-option-hidden-type' )?.value;
WPFormsBuilder.fieldChoiceUpdate( fieldType, chat.fieldId, choices.length );
WPFormsBuilder.triggerBuilderEvent( 'wpformsFieldChoiceAdd' );
// Trigger event after choices insert.
chat.triggerEvent( 'wpformsAIModalAfterChoicesInsert', { fieldId: chat.fieldId } );
},
/**
* Get updated single choice item.
*
* @since 1.9.1
*
* @param {HTMLElement} li Choice item element.
* @param {string} key Choice key.
* @param {string} choice Choice value.
*
* @return {HTMLElement} The updated choice item.
*/
getUpdatedSingleChoiceItem( li, key, choice ) {
li.setAttribute( 'data-key', key.toString() );
// Update choice item inputs name attributes.
li.innerHTML = li.innerHTML.replaceAll( '{{key}}', key );
// Sanitize choice before set.
choice = wpf.sanitizeHTML( choice );
const inputDefault = li.querySelector( 'input.default' );
inputDefault.removeAttribute( 'checked' );
// Set label
const inputLabel = li.querySelector( 'input.label' );
inputLabel.value = choice;
inputLabel.setAttribute( 'value', choice );
// Set value.
const inputValue = li.querySelector( 'input.value' );
inputValue.value = choice;
inputValue.setAttribute( 'value', choice );
// Reset image upload.
const imageUpload = li.querySelector( '.wpforms-image-upload' );
const inputImage = imageUpload.querySelector( 'input.source' );
inputImage.value = '';
inputImage.setAttribute( 'value', '' );
imageUpload.querySelector( '.preview' ).innerHTML = '';
imageUpload.querySelector( '.wpforms-image-upload-add' ).style.display = 'block';
// Reset icon choice.
const iconSelect = li.querySelector( '.wpforms-icon-select' );
iconSelect.querySelector( '.ic-fa-preview' ).setAttribute( 'class', 'ic-fa-preview ic-fa-regular ic-fa-face-smile' );
iconSelect.querySelector( 'input.source-icon' ).value = 'face-smile';
iconSelect.querySelector( 'input.source-icon-style' ).value = 'regular';
return li;
},
};
}
@@ -0,0 +1,19 @@
export default function(c){return{getAnswer(e){if(e.choices?.length<1)return"";var t,r=[];for(t in e.choices)r.push(`
<li class="wpforms-ai-chat-choices-item">
${c.htmlSpecialChars(e.choices[t])}
</li>
`);let i=`
<h4>${c.htmlSpecialChars(e.heading??"")}</h4>
<ol>
${r.join("")}
</ol>
`;return c.sessionId||(i+=`<span>${c.modeStrings.footer}</span>`),i},getAnswerButtonsPre(){return`
<button type="button" class="wpforms-ai-chat-choices-insert wpforms-ai-chat-answer-action wpforms-btn-sm wpforms-btn-orange" >
<span>${c.modeStrings.insert}</span>
</button>
`},getWarningMessage(){return c.triggerEvent("wpformsAIModalBeforeWarningMessageInsert",{fieldId:c.fieldId}),`<div class="wpforms-ai-chat-divider"></div>
<div class="wpforms-chat-item-notice">
<div class="wpforms-chat-item-notice-content">
<span>${c.modeStrings.warning}</span>
</div>
</div>`},isWelcomeScreen(){var t=document.getElementById(`wpforms-field-option-row-${c.fieldId}-choices`).querySelectorAll("li input.label");if(1!==t.length||t[0].value.trim()){if(3<t.length)return!1;var r=Object.values(c.modeStrings.defaults);for(let e=0;e<t.length;e++)if(!r.includes(t[e].value))return!1}return!0},addedAnswer(e){e.querySelector(".wpforms-ai-chat-choices-insert")?.addEventListener("click",this.insertButtonClick.bind(this))},sanitizeResponse(t){if(Array.isArray(t?.choices)){let e=t.choices;e=e.map(e=>wpf.sanitizeHTML(e,wpforms_builder.allowed_label_html_tags)),t.choices=e.filter(e=>""!==e.trim())}return t},hasProhibitedCode(e,t){return t?.choices?.length!==e?.choices?.length},insertButtonClick(e){var t,e=e.target.closest(".wpforms-chat-item.wpforms-chat-item-choices"),r=e?.getAttribute("data-response-id"),i=(e?.querySelector("ol")).querySelectorAll(".wpforms-ai-chat-choices-item"),o=[];for(t in i)i.hasOwnProperty(t)&&i[t].textContent&&o.push(i[t].textContent.trim());c.wpformsAiApi.rate(!0,r),this.replaceChoices(o),jQuery("#wpforms-field-"+c.fieldId).click().promise().done(function(){jQuery(`#wpforms-field-option-basic-${c.fieldId} a.wpforms-field-option-group-toggle`).click()})},replaceChoices(e){var t,r=document.getElementById(`wpforms-field-option-row-${c.fieldId}-choices`).querySelector("ul.choices-list"),i=r.querySelector("li:first-child").cloneNode(!0);for(t in i.innerHTML=i.innerHTML.replace(/\[choices\]\[\d+\]/g,"[choices][{{key}}]"),r.innerHTML="",e){var o=(Number(t)+1).toString(),l=e[t],s=i.cloneNode(!0),s=this.getUpdatedSingleChoiceItem(s,o,l);r.appendChild(s)}r.setAttribute("data-next-id",e.length+1);var n=document.getElementById("wpforms-field-option-"+c.fieldId).querySelector("input.wpforms-field-option-hidden-type")?.value;WPFormsBuilder.fieldChoiceUpdate(n,c.fieldId,e.length),WPFormsBuilder.triggerBuilderEvent("wpformsFieldChoiceAdd"),c.triggerEvent("wpformsAIModalAfterChoicesInsert",{fieldId:c.fieldId})},getUpdatedSingleChoiceItem(e,t,r){e.setAttribute("data-key",t.toString()),e.innerHTML=e.innerHTML.replaceAll("{{key}}",t),r=wpf.sanitizeHTML(r);e.querySelector("input.default").removeAttribute("checked");t=e.querySelector("input.label"),t.value=r,t.setAttribute("value",r),t=e.querySelector("input.value"),t.value=r,t.setAttribute("value",r),t=e.querySelector(".wpforms-image-upload"),r=t.querySelector("input.source"),r.value="",r.setAttribute("value",""),t.querySelector(".preview").innerHTML="",t.querySelector(".wpforms-image-upload-add").style.display="block",r=e.querySelector(".wpforms-icon-select");return r.querySelector(".ic-fa-preview").setAttribute("class","ic-fa-preview ic-fa-regular ic-fa-face-smile"),r.querySelector("input.source-icon").value="face-smile",r.querySelector("input.source-icon-style").value="regular",e}}}
@@ -0,0 +1,163 @@
/* global WPFormsAIChatHTMLElement, WPFormsAIFormGenerator, wpf, wpforms_builder */
/**
* @param chat.modeStrings.footerFirst
* @param chat.modeStrings.inactiveAnswerTitle
* @param chat.preventResizeInput
* @param response.form_title
* @param wpforms_builder.allowed_label_html_tags
*/
/**
* The WPForms AI chat element.
*
* Forms mode helpers module.
*
* @since 1.9.2
*
* @param {WPFormsAIChatHTMLElement} chat The chat element.
*
* @return {Object} Forms helpers object.
*/
export default function( chat ) { // eslint-disable-line no-unused-vars, max-lines-per-function
/**
* The default `forms` mode helpers object.
*
* @since 1.9.2
*/
const forms = {
/**
* Init `forms` mode.
*
* @since 1.9.2
*/
init() {
// Set the initial form generator state.
if ( chat.sessionId ) {
WPFormsAIFormGenerator.state.chatStart = true;
// Remove the selected state from the current template card.
WPFormsAIFormGenerator.main.el.$templateCard
.next( '.selected' ).removeClass( 'selected' );
}
},
/**
* Reset the message input field.
*
* @since 1.9.2
*/
resetInput() {
chat.resizeInput();
},
/**
* Get the answer based on AI response data.
*
* @since 1.9.2
*
* @param {Object} response The AI response data.
*
* @return {string} HTML markup.
*/
getAnswer( response ) {
if ( ! response ) {
return '';
}
const rnd = Math.floor( Math.random() * chat.modeStrings.footer.length );
const footer = chat.modeStrings.footer[ rnd ];
const answer = response.explanation || ( response.form_title ?? '' );
return `
<h4>${ answer }</h4>
<span>${ footer }</span>
`;
},
/**
* Get the answer pre-buttons HTML markup.
*
* @since 1.9.2
*
* @return {string} The answer pre-buttons HTML markup.
*/
getAnswerButtonsPre() {
return `
<button type="button" class="wpforms-ai-chat-use-form wpforms-ai-chat-answer-action wpforms-btn-sm wpforms-btn-orange" >
<span>${ chat.modeStrings.useForm }</span>
</button>
`;
},
/**
* The answer was added.
*
* @since 1.9.2
*
* @param {HTMLElement} element The answer element.
*/
addedAnswer( element ) { // eslint-disable-line no-unused-vars
forms.updateInactiveAnswers();
},
/**
* Set active answer.
*
* @since 1.9.2
*
* @param {HTMLElement} element The answer element.
*/
setActiveAnswer( element ) {
forms.updateInactiveAnswers();
element.querySelector( '.wpforms-chat-item-content' ).setAttribute( 'title', '' );
},
/**
* Update inactive answers.
*
* @since 1.9.2
*/
updateInactiveAnswers() {
chat.messageList.querySelectorAll( '.wpforms-chat-item-answer:not(.active) .wpforms-chat-item-content' )
.forEach( ( el ) => {
// Set title attribute for inactive answers.
el.setAttribute( 'title', chat.modeStrings.inactiveAnswerTitle );
} );
},
/**
* Determine whether the Welcome Screen should be displayed.
*
* @since 1.9.2
*
* @return {boolean} Display the Welcome Screen or not.
*/
isWelcomeScreen() {
return true;
},
/**
* Sanitize response.
*
* @since 1.9.2
*
* @param {Object} response The response data to sanitize.
*
* @return {Object} The sanitized response.
*/
sanitizeResponse( response ) {
if ( ! response.explanation ) {
return response;
}
// Sanitize explanation string.
response.explanation = wpf.sanitizeHTML( response.explanation, wpforms_builder.allowed_label_html_tags );
return response;
},
};
return forms;
}
@@ -0,0 +1,8 @@
export default function(r){let t={init(){r.sessionId&&(WPFormsAIFormGenerator.state.chatStart=!0,WPFormsAIFormGenerator.main.el.$templateCard.next(".selected").removeClass("selected"))},resetInput(){r.resizeInput()},getAnswer(e){var t;return e?(t=Math.floor(Math.random()*r.modeStrings.footer.length),t=r.modeStrings.footer[t],`
<h4>${e.explanation||(e.form_title??"")}</h4>
<span>${t}</span>
`):""},getAnswerButtonsPre(){return`
<button type="button" class="wpforms-ai-chat-use-form wpforms-ai-chat-answer-action wpforms-btn-sm wpforms-btn-orange" >
<span>${r.modeStrings.useForm}</span>
</button>
`},addedAnswer(e){t.updateInactiveAnswers()},setActiveAnswer(e){t.updateInactiveAnswers(),e.querySelector(".wpforms-chat-item-content").setAttribute("title","")},updateInactiveAnswers(){r.messageList.querySelectorAll(".wpforms-chat-item-answer:not(.active) .wpforms-chat-item-content").forEach(e=>{e.setAttribute("title",r.modeStrings.inactiveAnswerTitle)})},isWelcomeScreen(){return!0},sanitizeResponse(e){return e.explanation&&(e.explanation=wpf.sanitizeHTML(e.explanation,wpforms_builder.allowed_label_html_tags)),e}};return t}
@@ -0,0 +1,67 @@
/* global WPFormsAIChatHTMLElement */
/**
* The WPForms AI chat element.
*
* Choices helpers module.
*
* @since 1.9.1
*
* @param {WPFormsAIChatHTMLElement} chat The chat element.
*
* @return {Function} The app cloning function.
*/
export default function( chat ) { // eslint-disable-line no-unused-vars
/**
* The default `text` mode helpers object.
*
* @since 1.9.1
*/
return {
/**
* Get the `text` answer based on AI response data.
*
* @since 1.9.1
*
* @param {Object} response The AI response data.
*
* @return {string} HTML markup.
*/
getAnswer( response ) {
return `
<h4>${ response?.heading ?? '' }</h4>
<p>${ response?.text ?? '' }</p>
<span>${ response?.footer ?? '' }</span>
`;
},
/**
* Get the answer pre-buttons HTML markup.
*
* @since 1.9.1
*
* @return {string} The answer pre-buttons HTML markup.
*/
getAnswerButtonsPre() {
return '';
},
/**
* Added answer callback.
*
* @since 1.9.1
*/
addedAnswer() {},
/**
* Determine whether the Welcome Screen should be displayed.
*
* @since 1.9.2
*
* @return {boolean} Display the Welcome Screen or not.
*/
isWelcomeScreen() {
return true;
},
};
}
@@ -0,0 +1,5 @@
export default function(e){return{getAnswer(e){return`
<h4>${e?.heading??""}</h4>
<p>${e?.text??""}</p>
<span>${e?.footer??""}</span>
`},getAnswerButtonsPre(){return""},addedAnswer(){},isWelcomeScreen(){return!0}}}
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
@@ -0,0 +1,245 @@
/* global wpforms_ai_chat_element */
/**
* @param wpforms_ai_chat_element.pinChat
* @param wpforms_ai_chat_element.unpinChat
* @param wpforms_ai_chat_element.close
*/
/**
* Dock JS module.
*
* @since 1.9.5
*
* @param {jQuery} $ jQuery object.
*
* @return {Object} Dock object.
*/
// eslint-disable-next-line no-unused-vars
const wpFormsAIDock = ( function( $ ) {
/**
* Pin modal.
*
* @since 1.9.5
*
* @param {jQuery} $modal Modal element.
*/
function pinModal( $modal ) {
localStorage.setItem( 'wpforms-ai-chat-prefers-pinned', '1' );
$modal.find( '.wpforms-ai-modal-pin' ).attr( 'title', wpforms_ai_chat_element.unpinChat );
const $toolbar = $( '#wpforms-builder-form .wpforms-toolbar' );
// Get the distance from the top of the screen to the bottom border of the toolbar.
const toolbarHeight = $toolbar.offset().top + $toolbar.outerHeight();
$modal.addClass( 'pinned' );
if ( $( '#wpadminbar' ).is( ':visible' ) ) {
$modal.addClass( 'with-wpadminbar' );
}
$modal.insertAfter( $toolbar ).promise().done( function() {
$modal.css( {
top: toolbarHeight,
} );
} );
}
/**
* Unpin modal.
*
* @since 1.9.5
*
* @param {jQuery} $modal Modal element.
*/
function unPinModal( $modal ) {
localStorage.setItem( 'wpforms-ai-chat-prefers-pinned', '0' );
$modal.find( '.wpforms-ai-modal-pin' ).attr( 'title', wpforms_ai_chat_element.pinChat );
$modal.removeClass( 'pinned' );
$modal.removeClass( 'with-wpadminbar' );
$modal.appendTo( $( 'body' ) ).promise().done( function() {
$modal.css( {
top: 0,
} );
} );
$modal.find( '.wpforms-ai-modal-top-bar' ).removeClass( 'scrolled' );
}
/**
* Handle click on the pin button.
*
* @since 1.9.5
*/
function onPinIconClick() {
if ( $( this ).hasClass( 'not-allowed' ) ) {
return;
}
const $modal = $( this ).closest( '.jconfirm.jconfirm-wpforms-ai-modal' );
if ( $modal.hasClass( 'pinned' ) ) {
unPinModal( $modal );
} else {
pinModal( $modal );
}
// Re-apply this action also for other modals but hide them.
const $otherModals = $( '.jconfirm.jconfirm-wpforms-ai-modal' ).not( $modal );
$otherModals.each( function() {
const $otherModal = $( this );
if ( $otherModal.hasClass( 'pinned' ) ) {
unPinModal( $otherModal );
} else {
pinModal( $otherModal );
}
$otherModal.hide();
} );
}
/**
* Handle click on the panel toggle button.
*
* Hide pinned chats if this is not the fields panel.
*
* @since 1.9.5
*
* @param {Object} clicked Clicked a button object.
*/
function onPanelToggleClick( clicked ) {
const $this = $( clicked.target ),
dataPanel = $this.closest( 'button' ).data( 'panel' );
if ( dataPanel === 'fields' ) {
return;
}
$( '.jconfirm.jconfirm-wpforms-ai-modal.pinned' ).each( function() {
$( this ).hide();
} );
}
/**
* Bind events.
*
* @since 1.9.5
*/
function bindEvents() {
const AIModalPin = $( '.wpforms-ai-modal-pin' );
$( document )
.off( 'click', '.wpforms-ai-modal-pin' )
.on( 'click', '.wpforms-ai-modal-pin', onPinIconClick )
// Hide pin icon for the time of response generation.
.on( 'wpformsAIChatBeforeSendMessage', () => AIModalPin.addClass( 'not-allowed' ) )
.on( 'wpformsAIChatBeforeError wpformsAIChatAfterTypeText', () => AIModalPin.removeClass( 'not-allowed' ) );
$( '#wpforms-panels-toggle button' )
.off( 'click', onPanelToggleClick )
.on( 'click', onPanelToggleClick );
}
/**
* Get the modal element.
*
* @since 1.9.5
*
* @param {number} fieldId Field ID.
*
* @return {jQuery} Modal element.
*/
function getModal( fieldId ) {
const $chatElement = $( 'wpforms-ai-chat[field-id="' + fieldId + '"]' );
return $chatElement.closest( '.jconfirm.jconfirm-wpforms-ai-modal' ).last();
}
/**
* Prepare dock by adding pin button.
*
* @since 1.9.5
*
* @param {number} fieldId Field ID.
*/
function prepareDock( fieldId ) {
const $jconfirmAIModal = getModal( fieldId ),
dockAlreadyPrepared = $jconfirmAIModal.find( '.wpforms-ai-modal-pin' ).length;
if ( dockAlreadyPrepared ) {
return;
}
const $closeIcon = $jconfirmAIModal.find( '.jconfirm-closeIcon' );
// Append new bar after close icon and move close icon inside this bar.
$closeIcon.after(
`<div class="wpforms-ai-modal-top-bar">
<div class="wpforms-ai-modal-pin" title="${ wpforms_ai_chat_element.pinChat }"></div>
</div>`
).promise().done( function() {
const $topBar = $jconfirmAIModal.find( '.wpforms-ai-modal-top-bar' );
$closeIcon.appendTo( $topBar );
} );
$closeIcon.attr( 'title', wpforms_ai_chat_element.close );
const $topBar = $jconfirmAIModal.find( '.wpforms-ai-modal-top-bar' ),
$messageList = $jconfirmAIModal.find( '.wpforms-ai-chat-message-list' );
$messageList.off( 'scroll' );
$messageList.on( 'scroll', function() {
if ( $messageList.scrollTop() > 0 ) {
$topBar.addClass( 'scrolled' );
return;
}
$topBar.removeClass( 'scrolled' );
} );
$jconfirmAIModal.on( 'remove', function() {
$messageList.off( 'scroll' );
} );
}
/**
* Maybe pin modal on open.
*
* @since 1.9.5
*
* @param {string} fieldId Field ID.
*/
function onOpen( fieldId ) {
const savedState = localStorage.getItem( 'wpforms-ai-chat-prefers-pinned' ) || '0';
if ( savedState === '0' ) {
return;
}
pinModal( getModal( fieldId ) );
}
/**
* Initialize the dock.
*
* @since 1.9.5
*
* @param {number} fieldId Field ID.
*/
function init( fieldId ) {
prepareDock( fieldId );
bindEvents();
onOpen( fieldId );
}
return { init };
}( jQuery ) );
@@ -0,0 +1,3 @@
let wpFormsAIDock=(e=>{function s(o){localStorage.setItem("wpforms-ai-chat-prefers-pinned","1"),o.find(".wpforms-ai-modal-pin").attr("title",wpforms_ai_chat_element.unpinChat);var a=e("#wpforms-builder-form .wpforms-toolbar");let i=a.offset().top+a.outerHeight();o.addClass("pinned"),e("#wpadminbar").is(":visible")&&o.addClass("with-wpadminbar"),o.insertAfter(a).promise().done(function(){o.css({top:i})})}function a(o){localStorage.setItem("wpforms-ai-chat-prefers-pinned","0"),o.find(".wpforms-ai-modal-pin").attr("title",wpforms_ai_chat_element.pinChat),o.removeClass("pinned"),o.removeClass("with-wpadminbar"),o.appendTo(e("body")).promise().done(function(){o.css({top:0})}),o.find(".wpforms-ai-modal-top-bar").removeClass("scrolled")}function n(){var o;e(this).hasClass("not-allowed")||(((o=e(this).closest(".jconfirm.jconfirm-wpforms-ai-modal")).hasClass("pinned")?a:s)(o),e(".jconfirm.jconfirm-wpforms-ai-modal").not(o).each(function(){var o=e(this);(o.hasClass("pinned")?a:s)(o),o.hide()}))}function t(o){"fields"!==e(o.target).closest("button").data("panel")&&e(".jconfirm.jconfirm-wpforms-ai-modal.pinned").each(function(){e(this).hide()})}function r(o){return e('wpforms-ai-chat[field-id="'+o+'"]').closest(".jconfirm.jconfirm-wpforms-ai-modal").last()}return{init:function(a){{var i=a;let e=r(i),o=e.find(".wpforms-ai-modal-pin").length;if(!o){let a=e.find(".jconfirm-closeIcon"),o=(a.after(`<div class="wpforms-ai-modal-top-bar">
<div class="wpforms-ai-modal-pin" title="${wpforms_ai_chat_element.pinChat}"></div>
</div>`).promise().done(function(){var o=e.find(".wpforms-ai-modal-top-bar");a.appendTo(o)}),a.attr("title",wpforms_ai_chat_element.close),e.find(".wpforms-ai-modal-top-bar")),i=e.find(".wpforms-ai-chat-message-list");i.off("scroll"),i.on("scroll",function(){0<i.scrollTop()?o.addClass("scrolled"):o.removeClass("scrolled")}),e.on("remove",function(){i.off("scroll")})}}{let o=e(".wpforms-ai-modal-pin");e(document).off("click",".wpforms-ai-modal-pin").on("click",".wpforms-ai-modal-pin",n).on("wpformsAIChatBeforeSendMessage",()=>o.addClass("not-allowed")).on("wpformsAIChatBeforeError wpformsAIChatAfterTypeText",()=>o.removeClass("not-allowed")),e("#wpforms-panels-toggle button").off("click",t).on("click",t)}i=a,"0"!==(localStorage.getItem("wpforms-ai-chat-prefers-pinned")||"0")&&s(r(i))}}})(jQuery);