Questions' );
return $links;
}
public static function menu() {
add_menu_page( 'Replyvane Chat Support', 'Replyvane', self::CAP, 'replyvane', array( __CLASS__, 'page_questions' ), 'dashicons-format-chat', 58 );
add_submenu_page( 'replyvane', 'Questions', 'Questions', self::CAP, 'replyvane', array( __CLASS__, 'page_questions' ) );
add_submenu_page( 'replyvane', 'Topics', 'Topics', self::CAP, 'replyvane-topics', array( __CLASS__, 'page_topics' ) );
add_submenu_page( 'replyvane', 'Import / Export', 'Import / Export', self::CAP, 'replyvane-import', array( __CLASS__, 'page_import' ) );
add_submenu_page( 'replyvane', 'Visitor activity', 'Visitor activity', self::CAP, 'replyvane-activity', array( __CLASS__, 'page_activity' ) );
add_submenu_page( 'replyvane', 'Settings', 'Settings', self::CAP, 'replyvane-settings', array( __CLASS__, 'page_settings' ) );
}
private static function url( $page, array $args = array() ) {
return add_query_arg( array_merge( array( 'page' => $page ), $args ), admin_url( 'admin.php' ) );
}
private static function notice( $message, $type = 'success' ) {
printf( '
', esc_attr( $type ), esc_html( $message ) );
}
public static function settings_assets( $hook ) {
if ( false === strpos( (string) $hook, 'replyvane-settings' ) ) {
return;
}
wp_enqueue_style( 'wp-color-picker' );
wp_enqueue_script( 'wp-color-picker' );
wp_enqueue_media();
// The real chat styles, so the preview matches the website.
wp_enqueue_style( 'replyvane-chat', REPLYVANE_URL . 'assets/chat.css', array(), REPLYVANE_VERSION );
wp_enqueue_script( 'replyvane-settings', REPLYVANE_URL . 'assets/admin-settings.js', array( 'jquery', 'wp-color-picker' ), REPLYVANE_VERSION, true );
$theme = Replyvane_Appearance::theme();
wp_localize_script(
'replyvane-settings',
'replyVaneSettings',
array(
'option' => Replyvane_Plugin::OPTION,
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'replyvane_ai_test' ),
'theme' => array(
'accent' => $theme['accent'] ? $theme['accent'] : Replyvane_Appearance::FALLBACK,
'font' => $theme['font'] ? $theme['font'] : 'inherit',
),
'i18n' => array(
'working' => __( 'Working…', 'replyvane-chat-support' ),
'failed' => __( 'That could not run. Please try again.', 'replyvane-chat-support' ),
'imageTitle' => __( 'Chat button image', 'replyvane-chat-support' ),
'imageButton' => __( 'Use this image', 'replyvane-chat-support' ),
'keptOut' => __( 'kept out', 'replyvane-chat-support' ),
'automatic' => __( 'automatically', 'replyvane-chat-support' ),
),
)
);
}
/** Reminder on the plugin's screens while the chat is hidden from visitors. */
public static function preview_notice() {
$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
if ( ! $screen || false === strpos( (string) $screen->id, 'replyvane' ) || ! Replyvane_Plugin::admins_only() ) {
return;
}
printf(
'',
esc_url( self::url( 'replyvane-settings' ) )
);
}
private static function messages() {
$map = array(
'saved' => 'Question saved.',
'deleted' => 'Question deleted.',
'topics' => 'Topics saved.',
'cleared' => 'Visitor activity cleared.',
'reset' => 'Open counts reset to zero.',
);
$msg = isset( $_GET['msg'] ) ? sanitize_key( $_GET['msg'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
if ( isset( $map[ $msg ] ) ) {
self::notice( $map[ $msg ] );
}
}
/* ================================================================== */
/* Questions */
/* ================================================================== */
public static function page_questions() {
$action = isset( $_GET['action'] ) ? sanitize_key( $_GET['action'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
if ( 'edit' === $action || 'new' === $action ) {
self::question_form();
return;
}
$cats = Replyvane_Store::categories();
$cat_names = wp_list_pluck( $cats, 'title', 'id' );
$filter = isset( $_GET['topic'] ) ? sanitize_title( wp_unslash( $_GET['topic'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
$search = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
$items = array_filter(
Replyvane_Store::items(),
function ( $row ) use ( $filter, $search ) {
if ( $filter && $row['category'] !== $filter ) {
return false;
}
if ( $search && false === stripos( $row['question'] . ' ' . $row['answer'] . ' ' . $row['keywords'], $search ) ) {
return false;
}
return true;
}
);
?>
Questions
Add question
These are the questions visitors can click in the chat window. Questions marked Popular show as soon as the chat opens; the rest are found by topic or by typing. To add many at once, use Import / Export .
Question Topic Popular Shown Order Opened
No questions yet. Import a JSON file or add one.
'edit', 'id' => $row['id'] ) );
$delete = wp_nonce_url( admin_url( 'admin-post.php?action=replyvane_delete_item&id=' . (int) $row['id'] ), 'replyvane_delete_item' );
?>
Edit |
Delete
| id:
| created from your website (editing it makes it yours)
'',
'category' => '',
'question' => isset( $_GET['question'] ) ? sanitize_text_field( wp_unslash( $_GET['question'] ) ) : '', // phpcs:ignore WordPress.Security.NonceVerification
'answer' => '',
'keywords' => '',
'links' => '[]',
'related' => '[]',
'featured' => 0,
'sort_order' => 0,
'active' => 1,
);
if ( $row ) {
$item = array_merge( $item, $row );
}
// Keep what was typed if saving failed.
$failed = get_transient( 'replyvane_form_' . get_current_user_id() );
if ( is_array( $failed ) ) {
delete_transient( 'replyvane_form_' . get_current_user_id() );
self::notice( $failed['error'], 'error' );
$item = array_merge( $item, $failed['item'] );
}
$links_text = '';
foreach ( Replyvane_Store::decode_list( $item['links'] ) as $link ) {
$links_text .= $link['label'] . ' | ' . $link['url'] . "\n";
}
$related = Replyvane_Store::decode_list( $item['related'] );
$others = array_filter(
Replyvane_Store::items(),
function ( $r ) use ( $id ) {
return (int) $r['id'] !== (int) $id;
}
);
?>
isset( $_POST['slug'] ) ? sanitize_title( wp_unslash( $_POST['slug'] ) ) : '',
'question' => isset( $_POST['question'] ) ? sanitize_text_field( wp_unslash( $_POST['question'] ) ) : '',
'answer' => isset( $_POST['answer'] ) ? sanitize_textarea_field( wp_unslash( $_POST['answer'] ) ) : '',
'category' => isset( $_POST['category'] ) ? sanitize_title( wp_unslash( $_POST['category'] ) ) : '',
'keywords' => isset( $_POST['keywords'] ) ? sanitize_text_field( wp_unslash( $_POST['keywords'] ) ) : '',
'links' => isset( $_POST['links'] ) ? wp_unslash( $_POST['links'] ) : '', // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- "Label | URL" lines: each label and URL is sanitized in Replyvane_Store::clean_links() (sanitize_text_field, esc_url_raw), which keeps encoded characters in URLs.
'related' => isset( $_POST['related'] ) ? array_map( 'sanitize_title', (array) wp_unslash( $_POST['related'] ) ) : array(),
'featured' => ! empty( $_POST['featured'] ),
'active' => ! empty( $_POST['active'] ),
'order' => isset( $_POST['order'] ) ? (int) $_POST['order'] : 0,
);
$row = Replyvane_Store::clean_item( $raw );
$result = is_wp_error( $row ) ? $row : Replyvane_Store::save( $row, $id );
if ( is_wp_error( $result ) ) {
set_transient(
'replyvane_form_' . get_current_user_id(),
array(
'error' => $result->get_error_message(),
'item' => array(
'slug' => sanitize_title( $raw['id'] ),
'question' => sanitize_text_field( $raw['question'] ),
'answer' => Replyvane_Store::clean_answer( $raw['answer'] ),
'category' => sanitize_title( $raw['category'] ),
'keywords' => sanitize_text_field( $raw['keywords'] ),
'links' => wp_json_encode( Replyvane_Store::clean_links( $raw['links'] ) ),
'related' => wp_json_encode( array_map( 'sanitize_title', $raw['related'] ) ),
'featured' => $raw['featured'] ? 1 : 0,
'active' => $raw['active'] ? 1 : 0,
'sort_order' => $raw['order'],
),
),
300
);
wp_safe_redirect( self::url( 'replyvane', array( 'action' => $id ? 'edit' : 'new', 'id' => $id ) ) );
exit;
}
wp_safe_redirect( self::url( 'replyvane', array( 'msg' => 'saved' ) ) );
exit;
}
public static function handle_delete_item() {
if ( ! current_user_can( self::CAP ) ) {
wp_die( esc_html__( 'You are not allowed to do this.', 'replyvane-chat-support' ) );
}
check_admin_referer( 'replyvane_delete_item' );
Replyvane_Store::delete( isset( $_GET['id'] ) ? absint( $_GET['id'] ) : 0 );
wp_safe_redirect( self::url( 'replyvane', array( 'msg' => 'deleted' ) ) );
exit;
}
/* ================================================================== */
/* Topics */
/* ================================================================== */
public static function page_topics() {
$cats = Replyvane_Store::categories();
$kept = get_transient( 'replyvane_topics_kept_' . get_current_user_id() );
?>
Topics
Topics group questions in the chat window ("Browse by topic"). A topic only appears in the chat when it has at least one question shown.
0 ) {
$kept[] = $title ? $title : $id;
} else {
continue;
}
}
$keep[] = array(
'id' => $id,
'title' => $title,
'order' => isset( $topic['order'] ) && '' !== $topic['order'] ? (int) $topic['order'] : count( $keep ) + 1,
);
}
Replyvane_Store::save_categories( $keep );
if ( $kept ) {
set_transient( 'replyvane_topics_kept_' . get_current_user_id(), $kept, 300 );
}
wp_safe_redirect( self::url( 'replyvane-topics', array( 'msg' => 'topics' ) ) );
exit;
}
/* ================================================================== */
/* Import / Export */
/* ================================================================== */
public static function page_import() {
$report = get_transient( 'replyvane_import_' . get_current_user_id() );
if ( $report ) {
delete_transient( 'replyvane_import_' . get_current_user_id() );
}
$count = count( Replyvane_Store::items() );
?>
Import / Export
Import finished:
Problems:
Notes:
Import questions from JSON
Choose a .json file:
Or paste the JSON here:
Add and update. New ids are added, matching ids are updated, and questions not in the file are kept.
Replace everything. Deletes all current questions and topics, then imports the file.
"if(document.querySelector('input[name=mode]:checked').value==='replace'){return confirm('This deletes every current question before importing. Continue?');}" ) ); ?>
Download
Download current questions ()
Download blank template
The easiest way to make changes in bulk: download the current questions, edit the file, and import it with "Add and update".
Questions from your website
To create starter questions from this website's own pages, or refresh them, go to Website knowledge .
JSON format
Field What it is
categoriesList of topics: { "id": "scans", "title": "Scans and prices", "order": 1 }
faqsList of questions, each with the fields below.
idUnique id using lowercase letters, numbers and hyphens, e.g. scan-prices. Importing the same id again updates that question. Optional: made from the question if left out.
categoryThe id of its topic.
questionRequired. What visitors click.
answerRequired. Use \n for a new line and \n\n for a new paragraph; lines starting - become bullets; **bold**; [text](https://…) for links.
keywordsOptional list of words visitors might type: ["price", "cost"]
linksOptional buttons under the answer: [{ "label": "Book a scan", "url": "https://…" }]
relatedOptional list of other question ids to suggest next.
featuredtrue to show it as soon as the chat opens.
orderNumber; lower shows first.
activefalse to hide it without deleting. Default true.
5 * MB_IN_BYTES ) {
$report = array( 'added' => 0, 'updated' => 0, 'skipped' => 0, 'errors' => array( 'The file is larger than 5 MB.' ), 'warnings' => array() );
} else {
$json = (string) file_get_contents( $_FILES['json_file']['tmp_name'] ); // phpcs:ignore WordPress.WP.AlternativeFunctions,WordPress.Security.ValidatedSanitizedInput
}
}
if ( '' === $json && ! empty( $_POST['json_text'] ) ) {
$json = (string) wp_unslash( $_POST['json_text'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Raw JSON: every field is sanitized after decoding, in Replyvane_Store::clean_item().
}
if ( ! isset( $report ) ) {
$report = '' === trim( $json )
? array( 'added' => 0, 'updated' => 0, 'skipped' => 0, 'errors' => array( 'Choose a JSON file or paste JSON first.' ), 'warnings' => array() )
: Replyvane_Store::import( $json, isset( $_POST['mode'] ) && 'replace' === $_POST['mode'] ? 'replace' : 'merge' );
}
set_transient( 'replyvane_import_' . get_current_user_id(), $report, 300 );
wp_safe_redirect( self::url( 'replyvane-import' ) );
exit;
}
public static function handle_export() {
if ( ! current_user_can( self::CAP ) ) {
wp_die( esc_html__( 'You are not allowed to do this.', 'replyvane-chat-support' ) );
}
check_admin_referer( 'replyvane_export' );
nocache_headers();
header( 'Content-Type: application/json; charset=utf-8' );
header( 'Content-Disposition: attachment; filename=replyvane-questions-' . gmdate( 'Y-m-d' ) . '.json' );
echo wp_json_encode( Replyvane_Store::export(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
exit;
}
/* ================================================================== */
/* Visitor activity */
/* ================================================================== */
public static function page_activity() {
global $wpdb;
$log = Replyvane_Plugin::log_table();
$since = gmdate( 'Y-m-d H:i:s', time() - 30 * DAY_IN_SECONDS );
$clicks = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM %i WHERE event = 'click' AND created_at >= %s", $log, $since ) );
$searches = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM %i WHERE event = 'search' AND created_at >= %s", $log, $since ) );
$unmatched = $wpdb->get_results( $wpdb->prepare( "SELECT query, COUNT(*) AS n, MAX(created_at) AS last FROM %i WHERE event = 'search' AND matched = 0 AND created_at >= %s GROUP BY query ORDER BY n DESC, last DESC LIMIT 40", $log, $since ), ARRAY_A );
$recent = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM %i ORDER BY id DESC LIMIT 100", $log ), ARRAY_A );
$top = Replyvane_Store::items();
usort(
$top,
function ( $a, $b ) {
return (int) $b['clicks'] <=> (int) $a['clicks'];
}
);
$top = array_slice( array_filter( $top, function ( $r ) { return $r['clicks'] > 0; } ), 0, 15 );
$titles = wp_list_pluck( Replyvane_Store::items(), 'question', 'slug' );
?>
Visitor activity
Recording is switched off in Settings, so new activity isn't being saved. Open counts still update.
No names, emails, IP addresses or accounts are recorded, only what was clicked or typed, when, and on which page.
Questions opened (last 30 days)
Typed searches (last 30 days)
Typed questions with no answer
What visitors asked that your list didn't cover. Click one to write an answer for it.
Most opened questions
All time. Reset counts
Question Opened
Nothing yet.
Latest activity
Download all activity as CSV
When What Question Page
Nothing yet.
“”
Delete all recorded activity
get_results( $wpdb->prepare( "SELECT * FROM %i ORDER BY id DESC LIMIT 1000 OFFSET %d", $table, $offset ), ARRAY_A );
foreach ( (array) $rows as $r ) {
fputcsv(
$out,
array_map(
$safe,
array(
get_date_from_gmt( $r['created_at'], 'Y-m-d H:i' ),
$r['session_id'],
$r['event'],
$r['query'],
$r['faq_slug'] && isset( $titles[ $r['faq_slug'] ] ) ? $titles[ $r['faq_slug'] ] : $r['faq_slug'],
$r['matched'] ? 'yes' : 'no',
$r['page_url'],
)
)
);
}
$offset += 1000;
} while ( ! empty( $rows ) && 1000 === count( $rows ) );
fclose( $out ); // phpcs:ignore WordPress.WP.AlternativeFunctions
exit;
}
public static function handle_clear_log() {
if ( ! current_user_can( self::CAP ) ) {
wp_die( esc_html__( 'You are not allowed to do this.', 'replyvane-chat-support' ) );
}
check_admin_referer( 'replyvane_clear_log' );
global $wpdb;
$wpdb->query( $wpdb->prepare( 'TRUNCATE TABLE %i', Replyvane_Plugin::log_table() ) );
wp_safe_redirect( self::url( 'replyvane-activity', array( 'msg' => 'cleared' ) ) );
exit;
}
public static function handle_reset_clicks() {
if ( ! current_user_can( self::CAP ) ) {
wp_die( esc_html__( 'You are not allowed to do this.', 'replyvane-chat-support' ) );
}
check_admin_referer( 'replyvane_reset_clicks' );
global $wpdb;
$wpdb->query( $wpdb->prepare( 'UPDATE %i SET clicks = 0', Replyvane_Plugin::items_table() ) );
wp_safe_redirect( self::url( 'replyvane-activity', array( 'msg' => 'reset' ) ) );
exit;
}
/* ================================================================== */
/* Settings */
/* ================================================================== */
public static function register_settings() {
register_setting(
'replyvane',
Replyvane_Plugin::OPTION,
array(
'type' => 'array',
'sanitize_callback' => array( __CLASS__, 'sanitize_settings' ),
)
);
}
public static function sanitize_settings( $in ) {
$d = Replyvane_Plugin::defaults();
$in = is_array( $in ) ? $in : array();
$text = function ( $key ) use ( $in, $d ) {
$v = isset( $in[ $key ] ) ? trim( sanitize_text_field( $in[ $key ] ) ) : '';
return '' !== $v ? $v : $d[ $key ];
};
$area = function ( $key ) use ( $in, $d ) {
$v = isset( $in[ $key ] ) ? trim( sanitize_textarea_field( $in[ $key ] ) ) : '';
return '' !== $v ? $v : $d[ $key ];
};
$color = sanitize_hex_color( isset( $in['color'] ) ? $in['color'] : '' );
return array(
'bot_name' => $text( 'bot_name' ),
'welcome' => $area( 'welcome' ),
'popular_title' => $text( 'popular_title' ),
'no_match' => $area( 'no_match' ),
'contact_url' => esc_url_raw( isset( $in['contact_url'] ) ? $in['contact_url'] : '' ),
'color' => $color ? $color : $d['color'],
'color_mode' => isset( $in['color_mode'] ) && 'custom' === $in['color_mode'] ? 'custom' : 'theme',
'font_mode' => isset( $in['font_mode'] ) && 'custom' === $in['font_mode'] ? 'custom' : 'theme',
'font_custom' => isset( $in['font_custom'] ) ? mb_substr( preg_replace( '/[^\w\s,"\'\-]/u', '', wp_unslash( $in['font_custom'] ) ), 0, 120 ) : '',
'question_bg' => (string) sanitize_hex_color( isset( $in['question_bg'] ) ? $in['question_bg'] : '' ),
'question_text' => (string) sanitize_hex_color( isset( $in['question_text'] ) ? $in['question_text'] : '' ),
'answer_bg' => (string) sanitize_hex_color( isset( $in['answer_bg'] ) ? $in['answer_bg'] : '' ),
'icon' => isset( $in['icon'] ) && ( 'custom' === $in['icon'] || isset( Replyvane_Appearance::icons()[ $in['icon'] ] ) ) ? $in['icon'] : 'chat',
'icon_url' => esc_url_raw( isset( $in['icon_url'] ) ? $in['icon_url'] : '' ),
'icon_fill' => empty( $in['icon_fill'] ) ? 0 : 1,
'notify_enabled' => empty( $in['notify_enabled'] ) ? 0 : 1,
'notify_sound' => empty( $in['notify_sound'] ) ? 0 : 1,
'agent_avatars' => empty( $in['agent_avatars'] ) ? 0 : 1,
'message_avatars' => empty( $in['message_avatars'] ) ? 0 : 1,
'engine' => isset( $in['engine'] ) && 'wp_ai' === $in['engine'] ? 'wp_ai' : 'builtin',
'ai_label' => empty( $in['ai_label'] ) ? 0 : 1,
'ai_model' => isset( $in['ai_model'] ) ? substr( preg_replace( '#[^a-zA-Z0-9._:/-]#', '', (string) $in['ai_model'] ), 0, 100 ) : '',
'position' => isset( $in['position'] ) && 'left' === $in['position'] ? 'left' : 'right',
'visibility' => isset( $in['visibility'] ) && 'admins' === $in['visibility'] ? 'admins' : 'everyone',
'show_search' => empty( $in['show_search'] ) ? 0 : 1,
'show_logged_in' => empty( $in['show_logged_in'] ) ? 0 : 1,
'log_enabled' => empty( $in['log_enabled'] ) ? 0 : 1,
'log_days' => absint( isset( $in['log_days'] ) ? $in['log_days'] : $d['log_days'] ),
'answer_from_site' => empty( $in['answer_from_site'] ) ? 0 : 1,
'exclude_ids' => array_values( array_unique( array_filter( array_map( 'absint', isset( $in['exclude_ids'] ) ? (array) $in['exclude_ids'] : array() ) ) ) ),
'sslverify' => empty( $in['sslverify'] ) ? 0 : 1,
'reply_from' => isset( $in['reply_from'] ) && 'agent' === $in['reply_from'] ? 'agent' : 'site',
'from_email' => is_email( isset( $in['from_email'] ) ? sanitize_email( $in['from_email'] ) : '' ) ? sanitize_email( $in['from_email'] ) : $d['from_email'],
// Live chat.
'live_enabled' => empty( $in['live_enabled'] ) ? 0 : 1,
'live_label' => $text( 'live_label' ),
'team_email' => implode( ', ', array_filter( array_map( 'sanitize_email', array_map( 'trim', explode( ',', isset( $in['team_email'] ) ? (string) $in['team_email'] : '' ) ) ), 'is_email' ) ),
'email_new_chat' => empty( $in['email_new_chat'] ) ? 0 : 1,
'escalate_minutes' => min( 120, absint( isset( $in['escalate_minutes'] ) ? $in['escalate_minutes'] : $d['escalate_minutes'] ) ),
'alert_include_text' => empty( $in['alert_include_text'] ) ? 0 : 1,
'require_email' => empty( $in['require_email'] ) ? 0 : 1,
'live_consent' => isset( $in['live_consent'] ) ? trim( sanitize_textarea_field( $in['live_consent'] ) ) : $d['live_consent'],
'hours_enabled' => empty( $in['hours_enabled'] ) ? 0 : 1,
'hours_days' => array_values( array_intersect( array( 1, 2, 3, 4, 5, 6, 7 ), array_map( 'intval', isset( $in['hours_days'] ) ? (array) $in['hours_days'] : array() ) ) ),
'hours_start' => isset( $in['hours_start'] ) && preg_match( '/^([01]\d|2[0-3]):[0-5]\d$/', $in['hours_start'] ) ? $in['hours_start'] : $d['hours_start'],
'hours_end' => isset( $in['hours_end'] ) && preg_match( '/^([01]\d|2[0-3]):[0-5]\d$/', $in['hours_end'] ) ? $in['hours_end'] : $d['hours_end'],
'live_days' => absint( isset( $in['live_days'] ) ? $in['live_days'] : $d['live_days'] ),
);
}
public static function page_settings() {
$s = Replyvane_Plugin::settings();
$o = Replyvane_Plugin::OPTION;
$f = function ( $key ) use ( $o ) {
return $o . '[' . $key . ']';
};
?>
Replyvane Chat Support settings
Who can see the chat
Chat window
Look and feel
' . esc_html( $hex ) . '';
};
?>
Answer engine
Answers from your website
Live chat with your team
Visitor activity
Preview
Updates as you change the settings. Click Save Changes to apply them to your site.
$v ) {
$pv_style .= $k . ':' . $v . ';';
}
$pv_icon_html = 'img' === $pv_icon['type'] ? '
' : $pv_icon['svg'];
$pv_bot_av = 'img' === $pv_icon['type'] ? '
' : $pv_icon['svg'];
$pv_person = Replyvane_Appearance::person_svg();
?>
What services do you offer? How can I contact you?
>
AI answer based on our website
>Example: a new reply preview appears here
>1