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:
+170
@@ -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 );
|
||||
};
|
||||
}
|
||||
Vendored
Executable
+1
@@ -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)}}
|
||||
Executable
+319
@@ -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;
|
||||
},
|
||||
};
|
||||
}
|
||||
Vendored
Executable
+19
@@ -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}}}
|
||||
Executable
+163
@@ -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;
|
||||
}
|
||||
Vendored
Executable
+8
@@ -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}
|
||||
Executable
+67
@@ -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;
|
||||
},
|
||||
};
|
||||
}
|
||||
Vendored
Executable
+5
@@ -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}}}
|
||||
Executable
+1398
File diff suppressed because it is too large
Load Diff
Vendored
Executable
+43
File diff suppressed because one or more lines are too long
Executable
+245
@@ -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 ) );
|
||||
Vendored
Executable
+3
@@ -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);
|
||||
Reference in New Issue
Block a user