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( '

%s

', 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( '

Preview mode: only administrators can see the chat on the site. Make it live for everyone in Settings.

', 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.

'edit', 'id' => $row['id'] ) ); $delete = wp_nonce_url( admin_url( 'admin-post.php?action=replyvane_delete_item&id=' . (int) $row['id'] ), 'replyvane_delete_item' ); ?>
QuestionTopicPopularShownOrderOpened
No questions yet. Import a JSON file or add one.
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; } ); ?>

← All questions

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.

'', 'title' => '', 'order' => '' ) ) ); foreach ( $rows as $i => $cat ) : $count = '' !== $cat['id'] ? Replyvane_Store::items_in_category( $cat['id'] ) : 0; ?>
Name shown to visitorsIDOrderQuestionsDelete
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



"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

FieldWhat 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.

TypedTimes
Nothing yet.

Most opened questions

All time. Reset counts

QuestionOpened
Nothing yet.

Latest activity

Download all activity as CSV

WhenWhatQuestionPage
Nothing yet.
“”
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