get_charset_collate();
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
// Table 1: Chat Logs
$table_logs = $wpdb->prefix . 'soft_ai_chat_logs';
$sql_logs = "CREATE TABLE $table_logs (
id mediumint(9) NOT NULL AUTO_INCREMENT,
time datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
user_ip varchar(100) DEFAULT '' NOT NULL,
provider varchar(50) DEFAULT '' NOT NULL,
model varchar(100) DEFAULT '' NOT NULL,
question text NOT NULL,
answer longtext NOT NULL,
source varchar(50) DEFAULT 'widget' NOT NULL,
is_read tinyint(1) DEFAULT 0 NOT NULL,
current_url varchar(500) DEFAULT '' NOT NULL,
referrer_url varchar(500) DEFAULT '' NOT NULL,
device varchar(50) DEFAULT '' NOT NULL,
country varchar(50) DEFAULT '' NOT NULL,
PRIMARY KEY (id),
KEY time (time),
KEY user_ip (user_ip)
) $charset_collate;";
dbDelta($sql_logs);
// Table 2: Canned Messages
$table_canned = $wpdb->prefix . 'soft_ai_canned_msgs';
$sql_canned = "CREATE TABLE $table_canned (
id mediumint(9) NOT NULL AUTO_INCREMENT,
shortcut varchar(50) NOT NULL,
content text NOT NULL,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) $charset_collate;";
dbDelta($sql_canned);
// Table 3: Chat Stats
$table_stats = $wpdb->prefix . 'soft_ai_chat_stats';
$sql_stats = "CREATE TABLE $table_stats (
id mediumint(9) NOT NULL AUTO_INCREMENT,
date_day date NOT NULL,
user_ip varchar(100) NOT NULL,
is_taken_over tinyint(1) DEFAULT 0 NOT NULL,
ai_msgs int(11) DEFAULT 0 NOT NULL,
admin_msgs int(11) DEFAULT 0 NOT NULL,
ai_orders int(11) DEFAULT 0 NOT NULL,
first_user_time datetime DEFAULT NULL,
first_admin_time datetime DEFAULT NULL,
last_admin_time datetime DEFAULT NULL,
PRIMARY KEY (id),
UNIQUE KEY ip_date (date_day, user_ip)
) $charset_collate;";
dbDelta($sql_stats);
// Table 4: RAG Knowledge Base
$table_rag = $wpdb->prefix . 'soft_ai_rag_docs';
$sql_rag = "CREATE TABLE $table_rag (
id mediumint(9) NOT NULL AUTO_INCREMENT,
title varchar(255) NOT NULL,
source_type varchar(50) NOT NULL,
source_url text NOT NULL,
content longtext NOT NULL,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) $charset_collate;";
dbDelta($sql_rag);
}
// ---------------------------------------------------------
// 0.5. STATS HELPER
// ---------------------------------------------------------
function soft_ai_update_stats_table($ip, $event_type) {
global $wpdb;
$table = $wpdb->prefix . 'soft_ai_chat_stats';
$date = current_time('Y-m-d');
$now = current_time('mysql');
$wpdb->query($wpdb->prepare("INSERT IGNORE INTO $table (date_day, user_ip) VALUES (%s, %s)", $date, $ip));
if ($event_type === 'user_msg') {
$wpdb->query($wpdb->prepare("UPDATE $table SET first_user_time = IFNULL(first_user_time, %s) WHERE date_day = %s AND user_ip = %s", $now, $date, $ip));
} elseif ($event_type === 'ai_reply') {
$wpdb->query($wpdb->prepare("UPDATE $table SET ai_msgs = ai_msgs + 1 WHERE date_day = %s AND user_ip = %s", $date, $ip));
} elseif ($event_type === 'admin_reply') {
$wpdb->query($wpdb->prepare("UPDATE $table SET admin_msgs = admin_msgs + 1, first_admin_time = IFNULL(first_admin_time, %s), last_admin_time = %s WHERE date_day = %s AND user_ip = %s", $now, $now, $date, $ip));
} elseif ($event_type === 'takeover') {
$wpdb->query($wpdb->prepare("UPDATE $table SET is_taken_over = 1 WHERE date_day = %s AND user_ip = %s", $date, $ip));
} elseif ($event_type === 'ai_order') {
$wpdb->query($wpdb->prepare("UPDATE $table SET ai_orders = ai_orders + 1 WHERE date_day = %s AND user_ip = %s", $date, $ip));
}
}
// ---------------------------------------------------------
// 0.6. DEVICE & COUNTRY HELPER
// ---------------------------------------------------------
function soft_ai_get_device() {
$ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
if (preg_match('/Mobile|Android|iP(hone|od)|IEMobile|BlackBerry|Kindle|Silk-Accelerated|(hpw|web)OS|Opera M(obi|ini)/i', $ua)) {
return 'Mobile';
}
if (preg_match('/Tablet|iPad|Playbook|Silk/i', $ua)) {
return 'Tablet';
}
return 'Desktop';
}
function soft_ai_get_country() {
if (!empty($_SERVER['HTTP_CF_IPCOUNTRY'])) return $_SERVER['HTTP_CF_IPCOUNTRY'];
if (!empty($_SERVER['HTTP_X_COUNTRY_CODE'])) return $_SERVER['HTTP_X_COUNTRY_CODE'];
return '';
}
// ---------------------------------------------------------
// 1. SETTINGS & ADMIN MENU
// ---------------------------------------------------------
add_action('admin_menu', 'soft_ai_chat_add_admin_menu');
add_action('admin_init', 'soft_ai_chat_settings_init');
add_action('admin_enqueue_scripts', 'soft_ai_chat_admin_enqueue');
function soft_ai_chat_add_admin_menu() {
add_menu_page('AI Chat', 'AI Chat', 'manage_options', 'soft-ai-chat', 'soft_ai_chat_options_page', 'dashicons-format-chat', 80);
add_submenu_page('soft-ai-chat', 'Live Chat (Support)', '🟢 Livechat', 'manage_options', 'soft-ai-live-chat', 'soft_ai_live_chat_page');
add_submenu_page('soft-ai-chat', 'Dữ liệu RAG', '📚 Dữ liệu RAG', 'manage_options', 'soft-ai-rag', 'soft_ai_rag_page');
add_submenu_page('soft-ai-chat', 'Chat History', 'Lịch sử chat', 'manage_options', 'soft-ai-chat-history', 'soft_ai_chat_history_page');
add_submenu_page('soft-ai-chat', 'Canned Responses', 'Câu trả lời mẫu', 'manage_options', 'soft-ai-canned-responses', 'soft_ai_canned_responses_page');
add_submenu_page('soft-ai-chat', 'Thống kê', '📊 Thống kê', 'manage_options', 'soft-ai-chat-stats', 'soft_ai_chat_stats_page');
add_submenu_page('soft-ai-chat', 'Settings', 'Settings', 'manage_options', 'soft-ai-chat', 'soft_ai_chat_options_page');
}
function soft_ai_chat_admin_enqueue($hook_suffix) {
if ($hook_suffix === 'toplevel_page_soft-ai-chat') {
wp_enqueue_style('wp-color-picker');
wp_enqueue_script('wp-color-picker');
wp_add_inline_script('jquery', "
jQuery(document).ready(function($){
function toggleFields() {
var provider = $('#soft_ai_provider_select').val();
$('.api-key-row').closest('tr').hide();
$('.row-' + provider).closest('tr').show();
}
$('#soft_ai_provider_select').change(toggleFields);
toggleFields();
$('.soft-ai-color-field').wpColorPicker();
});
");
}
}
function soft_ai_chat_settings_init() {
register_setting('softAiChat', 'soft_ai_chat_settings');
add_settings_section('soft_ai_chat_main', __('General & AI Configuration', 'soft-ai-chat'), null, 'softAiChat');
add_settings_field('save_history', __('Save Chat History', 'soft-ai-chat'), 'soft_ai_render_checkbox', 'softAiChat', 'soft_ai_chat_main', ['field' => 'save_history']);
add_settings_field('admin_email_notify', __('Admin Notification Email', 'soft-ai-chat'), 'soft_ai_render_text', 'softAiChat', 'soft_ai_chat_main', ['field' => 'admin_email_notify', 'desc' => 'Địa chỉ email nhận thông báo khi có tin nhắn mới từ khách.']);
add_settings_field('provider', __('Select AI Provider', 'soft-ai-chat'), 'soft_ai_chat_provider_render', 'softAiChat', 'soft_ai_chat_main');
add_settings_field('groq_api_key', __('Groq API Key', 'soft-ai-chat'), 'soft_ai_render_password', 'softAiChat', 'soft_ai_chat_main', ['field' => 'groq_api_key', 'class' => 'row-groq']);
add_settings_field('openai_api_key', __('OpenAI API Key', 'soft-ai-chat'), 'soft_ai_render_password', 'softAiChat', 'soft_ai_chat_main', ['field' => 'openai_api_key', 'class' => 'row-openai']);
add_settings_field('gemini_api_key', __('Google Gemini API Key', 'soft-ai-chat'), 'soft_ai_render_password', 'softAiChat', 'soft_ai_chat_main', ['field' => 'gemini_api_key', 'class' => 'row-gemini']);
add_settings_field('model', __('AI Model Name', 'soft-ai-chat'), 'soft_ai_render_text', 'softAiChat', 'soft_ai_chat_main', ['field' => 'model', 'default' => 'openai/gpt-oss-120b','desc' => 'Recommended: openai/gpt-oss-120b (Groq), gpt-4o-mini (OpenAI), gemini-3-flash-preview (Gemini)']);
add_settings_field('temperature', __('Creativity', 'soft-ai-chat'), 'soft_ai_render_number', 'softAiChat', 'soft_ai_chat_main', ['field' => 'temperature', 'default' => 0.5, 'step' => 0.1, 'max' => 1]);
add_settings_field('max_tokens', __('Max Tokens', 'soft-ai-chat'), 'soft_ai_render_number', 'softAiChat', 'soft_ai_chat_main', ['field' => 'max_tokens', 'default' => 4096]);
add_settings_field('system_prompt', __('Custom Persona', 'soft-ai-chat'), 'soft_ai_render_textarea', 'softAiChat', 'soft_ai_chat_main', ['field' => 'system_prompt']);
add_settings_section('soft_ai_chat_payment', __('Payment Integration (Chat Only)', 'soft-ai-chat'), null, 'softAiChat');
add_settings_field('vietqr_bank', __('VietQR Bank Code', 'soft-ai-chat'), 'soft_ai_render_text', 'softAiChat', 'soft_ai_chat_payment', ['field' => 'vietqr_bank']);
add_settings_field('vietqr_acc', __('VietQR Account No', 'soft-ai-chat'), 'soft_ai_render_text', 'softAiChat', 'soft_ai_chat_payment', ['field' => 'vietqr_acc']);
add_settings_field('vietqr_name', __('Account Name (Optional)', 'soft-ai-chat'), 'soft_ai_render_text', 'softAiChat', 'soft_ai_chat_payment', ['field' => 'vietqr_name']);
add_settings_field('paypal_me', __('PayPal.me Username', 'soft-ai-chat'), 'soft_ai_render_text', 'softAiChat', 'soft_ai_chat_payment', ['field' => 'paypal_me']);
add_settings_section('soft_ai_chat_ui', __('User Interface', 'soft-ai-chat'), null, 'softAiChat');
add_settings_field('chat_title', __('Chat Window Title', 'soft-ai-chat'), 'soft_ai_render_text', 'softAiChat', 'soft_ai_chat_ui', ['field' => 'chat_title', 'default' => 'Trợ lý AI']);
add_settings_field('welcome_msg', __('Welcome Message', 'soft-ai-chat'), 'soft_ai_render_text', 'softAiChat', 'soft_ai_chat_ui', ['field' => 'welcome_msg', 'default' => 'Xin chào! Bạn cần tìm gì ạ?', 'width' => '100%']);
add_settings_field('theme_color', __('Widget Color', 'soft-ai-chat'), 'soft_ai_chat_themecolor_render', 'softAiChat', 'soft_ai_chat_ui');
add_settings_section('soft_ai_chat_social', __('Social Media Integration', 'soft-ai-chat'), 'soft_ai_chat_social_desc', 'softAiChat');
add_settings_field('fb_sep', '--- Facebook Messenger --- ', 'soft_ai_render_sep', 'softAiChat', 'soft_ai_chat_social');
add_settings_field('enable_fb_widget', __('Show FB Chat Bubble', 'soft-ai-chat'), 'soft_ai_render_checkbox', 'softAiChat', 'soft_ai_chat_social', ['field' => 'enable_fb_widget']);
add_settings_field('fb_page_id', __('Facebook Page ID', 'soft-ai-chat'), 'soft_ai_render_text', 'softAiChat', 'soft_ai_chat_social', ['field' => 'fb_page_id', 'desc' => 'Required for Chatbox Widget.']);
add_settings_field('fb_app_access_token', __('Facebook App Access Token', 'soft-ai-chat'), 'soft_ai_render_token', 'softAiChat', 'soft_ai_chat_social', ['field' => 'fb_app_access_token', 'desc' => 'Required for extended API features (Optional).']);
add_settings_field('fb_page_token', __('Facebook Page Access Token', 'soft-ai-chat'), 'soft_ai_render_token', 'softAiChat', 'soft_ai_chat_social', ['field' => 'fb_page_token', 'desc' => 'Required for AI Auto-Reply.']);
add_settings_field('fb_verify_token', __('Facebook Verify Token', 'soft-ai-chat'), 'soft_ai_render_text', 'softAiChat', 'soft_ai_chat_social', ['field' => 'fb_verify_token', 'default' => 'soft_ai_verify']);
add_settings_field('zalo_sep', '--- Zalo OA --- ', 'soft_ai_render_sep', 'softAiChat', 'soft_ai_chat_social');
add_settings_field('enable_zalo_widget', __('Show Zalo Widget', 'soft-ai-chat'), 'soft_ai_render_checkbox', 'softAiChat', 'soft_ai_chat_social', ['field' => 'enable_zalo_widget']);
add_settings_field('zalo_oa_id', __('Zalo OA ID', 'soft-ai-chat'), 'soft_ai_render_text', 'softAiChat', 'soft_ai_chat_social', ['field' => 'zalo_oa_id', 'desc' => 'Required for Chat Widget.']);
add_settings_field('zalo_access_token', __('Zalo OA Access Token', 'soft-ai-chat'), 'soft_ai_render_token', 'softAiChat', 'soft_ai_chat_social', ['field' => 'zalo_access_token', 'desc' => 'Required for AI Auto-Reply.']);
}
function soft_ai_render_sep() { echo ''; }
function soft_ai_render_text($args) {
$options = get_option('soft_ai_chat_settings');
$val = $options[$args['field']] ?? ($args['default'] ?? '');
$width = $args['width'] ?? '400px';
echo " ";
if(isset($args['desc'])) echo "
{$args['desc']}
";
}
function soft_ai_render_textarea($args) {
$options = get_option('soft_ai_chat_settings');
$val = $options[$args['field']] ?? '';
echo "";
if(isset($args['desc'])) echo "{$args['desc']}
";
}
function soft_ai_render_password($args) {
$options = get_option('soft_ai_chat_settings');
$val = $options[$args['field']] ?? '';
$cls = $args['class'] ?? '';
echo "";
if(isset($args['desc'])) echo "
{$args['desc']}
";
echo "
";
}
function soft_ai_render_token($args) {
$options = get_option('soft_ai_chat_settings');
$val = $options[$args['field']] ?? '';
$cls = $args['class'] ?? '';
echo "";
if(isset($args['desc'])) echo "
{$args['desc']}
";
echo "
";
}
function soft_ai_render_number($args) {
$options = get_option('soft_ai_chat_settings');
$val = $options[$args['field']] ?? ($args['default'] ?? 0);
$step = $args['step'] ?? 1;
$max = $args['max'] ?? 99999;
echo " ";
}
function soft_ai_render_checkbox($args) {
$options = get_option('soft_ai_chat_settings');
$val = isset($options[$args['field']]) ? $options[$args['field']] : '0';
echo ' Enable ';
}
function soft_ai_chat_provider_render() {
$options = get_option('soft_ai_chat_settings');
$val = $options['provider'] ?? 'groq';
?>
>Groq (Llama 3/Mixtral)
>OpenAI (GPT-4o/Turbo)
>Google Gemini
';
}
function soft_ai_chat_social_desc() {
echo 'Configure webhooks to connect AI to social platforms. Use the "Page ID" and "OA ID" to display chat bubbles on your site.
';
echo 'Webhooks URL: FB: ' . rest_url('soft-ai-chat/v1/webhook/facebook') . ' Zalo: ' . rest_url('soft-ai-chat/v1/webhook/zalo') . '
';
}
function soft_ai_chat_options_page() {
if (!current_user_can('manage_options')) return;
?>
AI Chat Configuration
open($tmp_name) === TRUE) {
for ($i = 0; $i < $zip->numFiles; $i++) {
$name = $zip->getNameIndex($i);
// DOCX
if ($ext == 'docx' && $name == 'word/document.xml') {
$content .= strip_tags(str_replace(['', ' '], [" ", "\n"], $zip->getFromIndex($i)));
}
// XLSX
if ($ext == 'xlsx' && $name == 'xl/sharedStrings.xml') {
$content .= strip_tags(str_replace(['', ' '], [" ", " "], $zip->getFromIndex($i)));
}
// PPTX
if ($ext == 'pptx' && strpos($name, 'ppt/slides/slide') !== false) {
$content .= strip_tags(str_replace(['', ' '], [" ", "\n"], $zip->getFromIndex($i))) . "\n";
}
}
$zip->close();
}
}
} else {
// Fallback cho PDF, DOC, XLS (Thô, bóc tách ký tự ASCII cơ bản)
$raw = file_get_contents($tmp_name);
$content = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+/', ' ', $raw);
$content = strip_tags($content);
}
return mb_substr(trim($content), 0, 100000); // Giới hạn 100,000 ký tự
}
function soft_ai_extract_text_from_google($url) {
$content = '';
if (preg_match('/docs\.google\.com\/(document|spreadsheets|presentation)\/d\/([a-zA-Z0-9-_]+)/', $url, $matches)) {
$type = $matches[1];
$id = $matches[2];
$export_url = '';
if ($type == 'document') {
$export_url = "https://docs.google.com/document/d/{$id}/export?format=txt";
} elseif ($type == 'spreadsheets') {
$export_url = "https://docs.google.com/spreadsheets/d/{$id}/export?format=csv";
} elseif ($type == 'presentation') {
$export_url = "https://docs.google.com/presentation/d/{$id}/export/txt";
}
if ($export_url) {
$response = wp_remote_get($export_url, ['timeout' => 30]);
if (!is_wp_error($response)) {
$content = wp_remote_retrieve_body($response);
if ($type == 'spreadsheets' || $type == 'presentation') {
$content = strip_tags($content);
}
}
}
}
return mb_substr(trim($content), 0, 100000);
}
function soft_ai_rag_page() {
if (!current_user_can('manage_options')) return;
global $wpdb;
$table_rag = $wpdb->prefix . 'soft_ai_rag_docs';
// Xóa file
if (isset($_GET['action']) && $_GET['action'] == 'delete_rag' && isset($_GET['id'])) {
$del_id = intval($_GET['id']);
$wpdb->delete($table_rag, ['id' => $del_id]);
echo 'Đã xóa dữ liệu thành công.
';
}
// Xử lý Upload/URL
if (isset($_POST['sac_save_rag']) && check_admin_referer('sac_save_rag_nonce')) {
$title = sanitize_text_field($_POST['title']);
$gdrive_url = esc_url_raw($_POST['gdrive_url']);
$extracted_content = '';
$type = '';
$source_url = '';
// Xử lý Google Drive Link
if (!empty($gdrive_url)) {
$extracted_content = soft_ai_extract_text_from_google($gdrive_url);
$type = 'google_drive';
$source_url = $gdrive_url;
if (!$title) $title = "Google Drive Document";
}
// Xử lý File Upload
elseif (!empty($_FILES['rag_file']['name'])) {
require_once( ABSPATH . 'wp-admin/includes/file.php' );
$uploadedfile = $_FILES['rag_file'];
$ext = pathinfo($uploadedfile['name'], PATHINFO_EXTENSION);
if ($uploadedfile['error'] == 0) {
$extracted_content = soft_ai_extract_text_from_file($uploadedfile['tmp_name'], $ext);
$type = 'uploaded_file';
$source_url = $uploadedfile['name'];
if (!$title) $title = $uploadedfile['name'];
}
}
if (!empty($extracted_content)) {
$wpdb->insert($table_rag, [
'title' => $title,
'source_type' => $type,
'source_url' => $source_url,
'content' => $extracted_content
]);
echo 'Đã thêm dữ liệu vào Knowledge Base thành công! (Dung lượng: '.strlen($extracted_content).' bytes)
';
} else {
echo 'Không thể trích xuất văn bản từ nguồn này hoặc file rỗng.
';
}
}
$docs = $wpdb->get_results("SELECT * FROM $table_rag ORDER BY id DESC");
?>
📚 Dữ liệu RAG (Knowledge Base)
Thêm dữ liệu từ các tài liệu bên ngoài (Google Docs/Sheets/Slides hoặc Upload File) để AI có thể đọc và trả lời khách hàng.
Khuyến nghị: Định dạng .txt, .csv, .docx mang lại kết quả bóc tách chính xác nhất. Google Drive cần được set quyền "Anyone with the link".
ID
Tiêu đề / Tên File
Loại Nguồn
Độ dài (Ký tự)
Hành động
id; ?>
title); ?>
source_url); ?>
source_type); ?>
content)); ?>
Xóa
Chưa có dữ liệu nào. '; endif; ?>
prefix . 'soft_ai_canned_msgs';
if (isset($_POST['sac_save_canned']) && check_admin_referer('sac_save_canned_nonce')) {
$shortcut = sanitize_text_field($_POST['shortcut']);
$content = sanitize_textarea_field($_POST['content']);
$edit_id = intval($_POST['edit_id']);
if ($shortcut && $content) {
if ($edit_id > 0) {
$wpdb->update($table, ['shortcut' => $shortcut, 'content' => $content], ['id' => $edit_id]);
echo '';
} else {
$wpdb->insert($table, ['shortcut' => $shortcut, 'content' => $content]);
echo '';
}
}
}
if (isset($_GET['action']) && $_GET['action'] == 'delete' && isset($_GET['id'])) {
$del_id = intval($_GET['id']);
$wpdb->delete($table, ['id' => $del_id]);
echo '';
}
$edit_data = null;
if (isset($_GET['action']) && $_GET['action'] == 'edit' && isset($_GET['id'])) {
$edit_data = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table WHERE id = %d", intval($_GET['id'])));
}
$responses = $wpdb->get_results("SELECT * FROM $table ORDER BY id DESC");
?>
Quản lý Câu trả lời mẫu (Canned Responses)
Tạo các câu trả lời nhanh để sử dụng trong Live Chat.
Shortcut
Nội dung
Hành động
shortcut); ?>
content)); ?>
Sửa
Xóa
Chưa có câu mẫu nào. '; endif; ?>
';
?>
🟢 Livechat
Select a user from the left to start chatting.
prefix . 'soft_ai_chat_stats';
$filter_date = isset($_GET['filter_date']) ? sanitize_text_field($_GET['filter_date']) : '';
$date_cond = $filter_date ? $wpdb->prepare(" AND date_day = %s", $filter_date) : "";
$total_conversations = (int) $wpdb->get_var("SELECT COUNT(DISTINCT user_ip) FROM $table WHERE 1=1 $date_cond");
$human_takeover = (int) $wpdb->get_var("SELECT COUNT(DISTINCT user_ip) FROM $table WHERE is_taken_over = 1 $date_cond");
$total_ai_msgs = (int) $wpdb->get_var("SELECT SUM(ai_msgs) FROM $table WHERE 1=1 $date_cond");
$total_admin_msgs = (int) $wpdb->get_var("SELECT SUM(admin_msgs) FROM $table WHERE 1=1 $date_cond");
$total_sent_msgs = $total_admin_msgs + $total_ai_msgs;
$total_ai_orders = (int) $wpdb->get_var("SELECT SUM(ai_orders) FROM $table WHERE 1=1 $date_cond");
$threads = $wpdb->get_results("SELECT first_user_time, first_admin_time, last_admin_time FROM $table WHERE 1=1 $date_cond");
$total_response_time = 0;
$response_count = 0;
$total_handle_time = 0;
$handle_count = 0;
foreach($threads as $row) {
if ($row->first_user_time && $row->first_admin_time) {
$t_user = strtotime($row->first_user_time);
$t_admin = strtotime($row->first_admin_time);
if ($t_admin >= $t_user) {
$total_response_time += ($t_admin - $t_user);
$response_count++;
}
}
if ($row->first_admin_time && $row->last_admin_time) {
$total_handle_time += (strtotime($row->last_admin_time) - strtotime($row->first_admin_time));
$handle_count++;
}
}
$avg_response_time = $response_count > 0 ? round($total_response_time / $response_count) : 0;
$avg_handle_time = $handle_count > 0 ? round($total_handle_time / $handle_count) : 0;
$first_msg_day = $wpdb->get_var("SELECT MIN(first_admin_time) FROM $table WHERE 1=1 $date_cond");
$last_msg_day = $wpdb->get_var("SELECT MAX(last_admin_time) FROM $table WHERE 1=1 $date_cond");
$online_time = 0;
if ($first_msg_day && $last_msg_day) {
$online_time = strtotime($last_msg_day) - strtotime($first_msg_day);
}
$online_rate = ($online_time > 0) ? round(($online_time / 86400) * 100, 2) : 0;
if ($online_rate > 100) $online_rate = 100;
function soft_ai_format_seconds($seconds) {
if ($seconds == 0) return '0s';
$h = floor($seconds / 3600);
$m = floor(($seconds % 3600) / 60);
$s = $seconds % 60;
$res = '';
if ($h > 0) $res .= $h . 'h ';
if ($m > 0) $res .= $m . 'm ';
$res .= $s . 's';
return trim($res);
}
?>
📊 Thống kê hiệu suất AI & Chat
👥 Tổng hội thoại tiếp nhận
Số lượng người dùng đã tương tác (IP/User riêng biệt).
🔄 Hội thoại tiếp quản từ AI
Số lượng khách hàng được nhân viên vào hỗ trợ trực tiếp.
💬 Tổng số tin nhắn đã gửi
AI tự động trả lời:
Nhân viên phản hồi:
🛒 Số đơn hàng do AI tạo
Số lượng đơn hàng được chốt thành công qua Chatbot AI.
⏱️ Thời gian phản hồi trung bình
Thời gian kể từ lúc khách nhắn đến khi nhân viên phản hồi đầu tiên (First Response Time).
⚙️ Thời gian xử lý trung bình
Thời gian trung bình để giải quyết một cuộc trò chuyện do nhân viên đảm nhận.
🟢 Thời gian trực chat (Ước tính)
Tỷ lệ online: % (Tính toán dựa trên khoảng cách giữa tin nhắn đầu và cuối của admin trong kỳ).
prefix . 'soft_ai_chat_logs';
$date = isset($_GET['date']) ? sanitize_text_field($_GET['date']) : '';
$traffic = isset($_GET['traffic_source']) ? sanitize_text_field($_GET['traffic_source']) : '';
$device = isset($_GET['device']) ? sanitize_text_field($_GET['device']) : '';
$country = isset($_GET['country']) ? sanitize_text_field($_GET['country']) : '';
$where = "WHERE 1=1";
if ($date) {
$where .= $wpdb->prepare(" AND DATE(time) = %s", $date);
} else {
$where .= " AND time > NOW() - INTERVAL 24 HOUR";
}
$having_clauses = [];
if ($traffic === 'direct') {
$having_clauses[] = " MAX(referrer_url) = '' AND MAX(source) = 'widget' ";
} elseif ($traffic === 'organic') {
$having_clauses[] = " MAX(referrer_url) REGEXP 'google|bing|yahoo|coccoc|duckduckgo' ";
} elseif ($traffic === 'social') {
$having_clauses[] = " (MAX(source) IN ('facebook', 'zalo') OR MAX(referrer_url) REGEXP 'facebook|fb\\.com|instagram|tiktok|zalo|twitter|t\\.co') ";
} elseif ($traffic === 'referring') {
$having_clauses[] = " MAX(referrer_url) != '' AND MAX(source) = 'widget' AND MAX(referrer_url) NOT REGEXP 'google|bing|yahoo|coccoc|duckduckgo|facebook|fb\\.com|instagram|tiktok|zalo|twitter|t\\.co' ";
}
if ($device) {
$having_clauses[] = $wpdb->prepare(" MAX(device) = %s ", $device);
}
if ($country) {
$having_clauses[] = $wpdb->prepare(" MAX(country) = %s ", strtoupper($country));
}
$having = "";
if (!empty($having_clauses)) {
$having = " HAVING " . implode(" AND ", $having_clauses);
}
$query = "
SELECT user_ip as ip, MAX(time) as latest_time, MAX(source) as source, MAX(referrer_url) as ref_url, MAX(device) as device_val, MAX(country) as country_val,
SUM(CASE WHEN is_read = 0 AND provider != 'live_admin' THEN 1 ELSE 0 END) as unread
FROM $table
$where
GROUP BY user_ip
$having
ORDER BY latest_time DESC
";
$results = $wpdb->get_results($query);
$data = [];
foreach($results as $r) {
$source_icon = '🌐';
if($r->source === 'facebook') $source_icon = '🔵';
if($r->source === 'zalo') $source_icon = '🟡';
$traffic_label = '';
if ($r->source === 'widget') {
if (empty($r->ref_url)) $traffic_label = ' [Direct]';
elseif (preg_match('/google|bing|yahoo|coccoc|duckduckgo/i', $r->ref_url)) $traffic_label = ' [Organic]';
elseif (preg_match('/facebook|fb\.com|instagram|tiktok|zalo|twitter|t\.co/i', $r->ref_url)) $traffic_label = ' [Social]';
else $traffic_label = ' [Ref]';
}
$device_icon = '';
if ($r->device_val === 'Mobile') $device_icon = '📱';
elseif ($r->device_val === 'Tablet') $device_icon = '💊';
elseif ($r->device_val === 'Desktop') $device_icon = '💻';
$country_flag = $r->country_val ? ' [' . strtoupper($r->country_val) . ']' : '';
// Tích hợp tên khách hàng Facebook
$display_ip = $r->ip;
if ($r->source === 'facebook') {
$fb_name = get_transient('soft_ai_fb_name_' . $r->ip);
if ($fb_name && $fb_name !== 'Unknown FB User') {
$display_ip = $fb_name . ' (' . $r->ip . ')';
}
}
$data[] = [
'ip' => $r->ip,
'display_name' => $display_ip,
'time' => date('H:i, d/m/Y', strtotime($r->latest_time)),
'unread' => $r->unread,
'source_label' => $source_icon . ' ' . ucfirst($r->source) . $traffic_label . ' ' . $device_icon . $country_flag
];
}
wp_send_json_success($data);
}
function soft_ai_ajax_get_messages() {
global $wpdb;
$ip = sanitize_text_field($_GET['ip']);
$date = isset($_GET['date']) ? sanitize_text_field($_GET['date']) : '';
$table = $wpdb->prefix . 'soft_ai_chat_logs';
$wpdb->update($table, ['is_read' => 1], ['user_ip' => $ip]);
$context = new Soft_AI_Context($ip, 'widget');
$is_live = $context->get('live_chat_mode');
if ($date) {
$logs = $wpdb->get_results($wpdb->prepare("SELECT * FROM $table WHERE user_ip = %s AND DATE(time) = %s ORDER BY time ASC LIMIT 100", $ip, $date));
} else {
$logs = $wpdb->get_results($wpdb->prepare("SELECT * FROM $table WHERE user_ip = %s ORDER BY time ASC LIMIT 100", $ip));
}
$messages = [];
foreach($logs as $log) {
$is_admin = ($log->provider === 'live_admin');
$content = $is_admin ? $log->answer : $log->question;
$admin_name = '';
if ($log->provider == 'live_user') {
$content = $log->question;
$is_admin = false;
} elseif ($log->provider == 'live_admin') {
$content = $log->answer;
$is_admin = true;
$admin_name = (!empty($log->model) && $log->model !== 'human') ? $log->model : 'Admin';
} elseif (!empty($log->answer) && !empty($log->question)) {
$messages[] = [
'content' => $log->question,
'is_admin' => false,
'time' => date('H:i, d/m/Y', strtotime($log->time)),
'current_url' => isset($log->current_url) ? $log->current_url : '',
'referrer_url' => isset($log->referrer_url) ? $log->referrer_url : ''
];
$messages[] = [
'content' => $log->answer,
'is_admin' => true,
'admin_name' => 'AI Bot',
'time' => date('H:i, d/m/Y', strtotime($log->time))
];
continue;
}
if ($is_admin && empty($admin_name)) {
$admin_name = 'AI Bot';
}
$messages[] = [
'content' => $content,
'is_admin' => $is_admin,
'admin_name' => $admin_name,
'time' => date('H:i, d/m/Y', strtotime($log->time)),
'current_url' => isset($log->current_url) ? $log->current_url : '',
'referrer_url' => isset($log->referrer_url) ? $log->referrer_url : ''
];
}
wp_send_json_success(['messages' => $messages, 'is_live' => (bool)$is_live]);
}
function soft_ai_ajax_toggle_mode() {
$ip = sanitize_text_field($_POST['ip']);
$mode = sanitize_text_field($_POST['mode']);
if (!$ip) wp_send_json_error();
$context = new Soft_AI_Context($ip, 'widget');
$context->set('live_chat_mode', ($mode === 'live'));
if ($mode === 'live') {
soft_ai_update_stats_table($ip, 'takeover');
}
wp_send_json_success();
}
function soft_ai_ajax_delete_conversation() {
if (!current_user_can('manage_options')) wp_send_json_error('Unauthorized');
global $wpdb;
$ip = sanitize_text_field($_POST['ip']);
$table = $wpdb->prefix . 'soft_ai_chat_logs';
$wpdb->delete($table, ['user_ip' => $ip]);
wp_send_json_success();
}
function soft_ai_ajax_send_reply() {
global $wpdb;
$ip = sanitize_text_field($_POST['ip']);
$msg = sanitize_textarea_field(wp_unslash($_POST['message']));
if (!$ip || !$msg) wp_send_json_error();
$table = $wpdb->prefix . 'soft_ai_chat_logs';
$last_source = $wpdb->get_var($wpdb->prepare("SELECT source FROM $table WHERE user_ip = %s ORDER BY id DESC LIMIT 1", $ip)) ?: 'widget';
$current_user = wp_get_current_user();
$admin_name = !empty($current_user->display_name) ? $current_user->display_name : 'Admin';
$wpdb->insert($table, [
'time' => current_time('mysql'),
'user_ip' => $ip,
'provider' => 'live_admin',
'model' => $admin_name,
'question' => '',
'answer' => $msg,
'source' => $last_source,
'is_read' => 1,
'device' => soft_ai_get_device(),
'country' => soft_ai_get_country()
]);
soft_ai_update_stats_table($ip, 'admin_reply');
if ($last_source === 'facebook') {
$options = get_option('soft_ai_chat_settings');
soft_ai_send_fb_message($ip, $msg, $options['fb_page_token']);
} elseif ($last_source === 'zalo') {
$token = get_option('soft_ai_chat_settings')['zalo_access_token'] ?? '';
if ($token) {
wp_remote_post("https://openapi.zalo.me/v3.0/oa/message/cs", [
'headers' => ['access_token' => $token, 'Content-Type' => 'application/json'],
'body' => wp_json_encode(['recipient' => ['user_id' => (string)$ip], 'message' => ['text' => $msg]])
]);
}
}
$context = new Soft_AI_Context($ip, $last_source);
$context->set('live_chat_mode', true);
wp_send_json_success();
}
function soft_ai_ajax_get_canned_msgs() {
global $wpdb;
$table = $wpdb->prefix . 'soft_ai_canned_msgs';
$results = $wpdb->get_results("SELECT id, shortcut, content FROM $table ORDER BY shortcut ASC");
wp_send_json_success($results);
}
// ---------------------------------------------------------
// 1.7. HISTORY PAGE
// ---------------------------------------------------------
function soft_ai_chat_history_page() {
global $wpdb;
$table_name = $wpdb->prefix . 'soft_ai_chat_logs';
if (isset($_POST['clear_all_logs']) && check_admin_referer('clear_all_logs')) {
$wpdb->query("TRUNCATE TABLE $table_name");
echo '';
}
if (isset($_POST['delete_thread']) && isset($_POST['thread_ip']) && check_admin_referer('delete_thread_' . $_POST['thread_ip'])) {
$del_ip = sanitize_text_field($_POST['thread_ip']);
$wpdb->delete($table_name, ['user_ip' => $del_ip]);
echo '';
}
$view_ip = isset($_GET['view_ip']) ? sanitize_text_field($_GET['view_ip']) : null;
?>
Chat History (Logs)
Clear All History
← Back to List
get_results($wpdb->prepare("SELECT * FROM $table_name WHERE user_ip = %s AND DATE(time) = %s ORDER BY time ASC", $view_ip, $view_date));
} else {
$logs = $wpdb->get_results($wpdb->prepare("SELECT * FROM $table_name WHERE user_ip = %s ORDER BY time ASC", $view_ip));
}
echo '';
$chat_title = esc_html($view_ip);
if ($logs && $logs[0]->source === 'facebook') {
$fb_name = get_transient('soft_ai_fb_name_' . $view_ip);
if ($fb_name && $fb_name !== 'Unknown FB User') {
$chat_title = esc_html($fb_name) . ' (' . esc_html($view_ip) . ')';
}
}
?>
provider === 'live_admin');
if (!empty($log->question) && $log->provider !== 'live_admin') {
$qid = 'q-' . $log->id;
$url_info = '';
if (!empty($log->current_url)) {
$url_info .= "
";
}
echo '
' . $url_info . '
' . date('H:i, d/m/Y', strtotime($log->time)) . '
'.esc_textarea($log->question).'
';
}
if (!empty($log->answer)) {
$cls = $is_admin_reply ? 'admin' : 'bot';
$aid = 'a-' . $log->id;
$raw_answer = $log->answer;
$admin_name_html = '';
if ($is_admin_reply) {
$name = (!empty($log->model) && $log->model !== 'human') ? esc_html($log->model) : 'Admin';
$admin_name_html = "
🗣️ {$name}
";
} else {
$admin_name_html = "
🤖 AI Bot
";
}
echo '
' . $admin_name_html . '
' . date('H:i, d/m/Y', strtotime($log->time)) . '
'.esc_textarea($raw_answer).'
';
}
endforeach; else: echo '
No messages found.
'; endif; ?>
prepare(" AND DATE(time) = %s", $filter_date);
}
$base_query .= " GROUP BY user_ip";
$having_clauses = [];
if ($traffic_filter === 'direct') {
$having_clauses[] = " MAX(referrer_url) = '' AND MAX(source) = 'widget' ";
} elseif ($traffic_filter === 'organic') {
$having_clauses[] = " MAX(referrer_url) REGEXP 'google|bing|yahoo|coccoc|duckduckgo' ";
} elseif ($traffic_filter === 'social') {
$having_clauses[] = " (MAX(source) IN ('facebook', 'zalo') OR MAX(referrer_url) REGEXP 'facebook|fb\\.com|instagram|tiktok|zalo|twitter|t\\.co') ";
} elseif ($traffic_filter === 'referring') {
$having_clauses[] = " MAX(referrer_url) != '' AND MAX(source) = 'widget' AND MAX(referrer_url) NOT REGEXP 'google|bing|yahoo|coccoc|duckduckgo|facebook|fb\\.com|instagram|tiktok|zalo|twitter|t\\.co' ";
}
if ($filter_device) {
$having_clauses[] = $wpdb->prepare(" MAX(device) = %s ", $filter_device);
}
if ($filter_country) {
$having_clauses[] = $wpdb->prepare(" MAX(country) = %s ", strtoupper($filter_country));
}
$having = "";
if (!empty($having_clauses)) {
$having = " HAVING " . implode(" AND ", $having_clauses);
}
$count_query = "SELECT COUNT(*) FROM ($base_query $having) as temp_table";
$total_threads = $wpdb->get_var($count_query);
$total_pages = ceil($total_threads / $per_page);
$fetch_query = "$base_query $having ORDER BY last_time DESC LIMIT %d OFFSET %d";
$threads = $wpdb->get_results($wpdb->prepare($fetch_query, $per_page, $offset));
?>
user_ip, 0, 1));
$is_numeric_ip = filter_var($th->user_ip, FILTER_VALIDATE_IP);
$display_name = $is_numeric_ip ? "Visitor (" . $th->user_ip . ")" : $th->user_ip;
$time_ago = human_time_diff(strtotime($th->last_time), current_time('timestamp')) . ' ago';
$traffic_label = '';
if ($th->source === 'widget') {
if (empty($th->ref_url)) $traffic_label = ' [Direct]';
elseif (preg_match('/google|bing|yahoo|coccoc|duckduckgo/i', $th->ref_url)) $traffic_label = ' [Organic]';
elseif (preg_match('/facebook|fb\.com|instagram|tiktok|zalo|twitter|t\.co/i', $th->ref_url)) $traffic_label = ' [Social]';
else $traffic_label = ' [Ref: ' . parse_url($th->ref_url, PHP_URL_HOST) . ']';
}
// Tích hợp tên khách hàng Facebook
if ($th->source === 'facebook') {
$fb_name = get_transient('soft_ai_fb_name_' . $th->user_ip);
if ($fb_name && $fb_name !== 'Unknown FB User') {
$display_name = $fb_name . ' (' . $th->user_ip . ')';
}
}
?>
source === 'facebook') echo '🔵';
elseif($th->source === 'zalo') echo '🟡';
else echo '🌐';
?>
No chat history found.
1): ?>
user_id = $user_id;
$this->source = $source;
}
public function get($key) {
if ($this->source === 'widget') {
return (function_exists('WC') && WC()->session) ? WC()->session->get('soft_ai_' . $key) : null;
} else {
$data = get_transient('soft_ai_sess_' . $this->user_id);
return isset($data[$key]) ? $data[$key] : null;
}
}
public function set($key, $val) {
if ($this->source === 'widget') {
if (function_exists('WC') && WC()->session) WC()->session->set('soft_ai_' . $key, $val);
} else {
$data = get_transient('soft_ai_sess_' . $this->user_id) ?: [];
$data[$key] = $val;
set_transient('soft_ai_sess_' . $this->user_id, $data, 24 * HOUR_IN_SECONDS);
}
}
public function add_to_cart($product_id, $qty = 1, $variation_id = 0, $variation = []) {
if ($this->source === 'widget' && function_exists('WC')) {
WC()->cart->add_to_cart($product_id, $qty, $variation_id, $variation);
} else {
$cart = $this->get('cart') ?: [];
$cart_key = $variation_id ? $variation_id : $product_id;
if (isset($cart[$cart_key])) {
$cart[$cart_key]['qty'] += $qty;
} else {
$cart[$cart_key] = [
'qty' => $qty,
'product_id' => $product_id,
'variation_id' => $variation_id,
'variation' => $variation
];
}
$this->set('cart', $cart);
}
}
public function empty_cart() {
if ($this->source === 'widget' && function_exists('WC')) WC()->cart->empty_cart();
else {
$this->set('cart', []);
$this->set('coupons', []);
}
}
public function get_cart_count() {
if ($this->source === 'widget' && function_exists('WC')) return WC()->cart->get_cart_contents_count();
else {
$c = 0; $cart = $this->get('cart') ?: [];
foreach($cart as $i) $c += $i['qty'];
return $c;
}
}
public function get_cart_total_string() {
if ($this->source === 'widget' && function_exists('WC')) return WC()->cart->get_cart_total();
else {
$total = 0; $cart = $this->get('cart') ?: [];
foreach($cart as $pid => $item) {
$p = function_exists('wc_get_product') ? wc_get_product($pid) : null;
if($p) $total += ($p->get_price() * $item['qty']);
}
return function_exists('wc_price') ? wc_price($total) : number_format($total) . 'đ';
}
}
}
// ---------------------------------------------------------
// 3. CORE LOGIC
// ---------------------------------------------------------
function soft_ai_notify_admin_by_email($question, $platform, $user_id) {
$options = get_option('soft_ai_chat_settings');
$admin_email_setting = $options['admin_email_notify'] ?? get_option('admin_email');
if (empty($admin_email_setting)) return;
$admin_emails = array_map('trim', explode(',', $admin_email_setting));
$admin_emails = array_filter($admin_emails, 'is_email');
if (empty($admin_emails)) return;
$subject = "[Soft AI Chat] Tin nhắn mới từ " . strtoupper($platform);
$body = "Bạn có tin nhắn mới từ khách hàng trên website. \n\n";
$body .= "------------------------------------------ \n";
$body .= "Nền tảng: " . $platform . " \n";
$body .= "User ID/IP: " . $user_id . " \n";
$body .= "Nội dung: " . $question . " \n";
$body .= "------------------------------------------ \n\n";
$body .= "Đi tới Live Chat để trả lời: " . admin_url('admin.php?page=soft-ai-live-chat');
$headers = array('Content-Type: text/html; charset=UTF-8');
wp_mail($admin_emails, $subject, $body, $headers);
}
function soft_ai_clean_content($content) {
if (!is_string($content)) return '';
$content = strip_shortcodes($content);
$content = preg_replace('/\[\/?et_pb_[^\]]+\]/', '', $content);
$content = wp_strip_all_tags($content);
$content = preg_replace('/\s+/', ' ', $content);
return mb_substr(trim($content), 0, 1500);
}
function soft_ai_chat_get_context($question) {
global $wpdb;
// 1. Lấy Context từ RAG (Knowledge Base)
$rag_context = "";
$table_rag = $wpdb->prefix . 'soft_ai_rag_docs';
// Tách từ khóa để tìm kiếm (Bỏ qua từ quá ngắn)
$search_terms = array_filter(explode(' ', mb_strtolower(trim($question))), function($w) {
return mb_strlen($w) > 2;
});
if (!empty($search_terms)) {
$likes = [];
$params = [];
foreach ($search_terms as $term) {
$likes[] = "content LIKE %s";
$params[] = '%' . $wpdb->esc_like($term) . '%';
}
$where = implode(' OR ', $likes);
// Tìm 3 tài liệu khớp nhất
$rag_docs = $wpdb->get_results($wpdb->prepare("SELECT title, content FROM $table_rag WHERE $where LIMIT 3", $params));
foreach ($rag_docs as $doc) {
// Lấy 1 snippet nhỏ chứa từ khóa để giảm token
$content_lower = mb_strtolower($doc->content);
$pos = 0;
foreach ($search_terms as $term) {
$p = mb_strpos($content_lower, $term);
if ($p !== false) {
$pos = $p;
break;
}
}
$start = max(0, $pos - 200); // Lùi lại 200 ký tự để lấy ngữ cảnh
$snippet = mb_substr($doc->content, $start, 800); // Trích xuất đoạn 800 ký tự
$rag_context .= "--- Source (RAG Knowledge Base): {$doc->title} ---\nContent: ...{$snippet}...\n\n";
}
}
// 2. Lấy Context từ bài viết/sản phẩm trên Website (Hành vi cũ)
$args = ['post_type' => ['post', 'page', 'product'], 'post_status' => 'publish', 'posts_per_page' => 4, 's' => $question, 'orderby' => 'relevance'];
$posts = get_posts($args);
$wp_context = "";
if ($posts) {
foreach ($posts as $post) {
$info = "";
if ($post->post_type === 'product' && function_exists('wc_get_product')) {
$p = wc_get_product($post->ID);
if ($p) $info = " | Price: " . $p->get_price_html() . " | Status: " . $p->get_stock_status();
}
$clean_body = soft_ai_clean_content($post->post_content);
$wp_context .= "--- Source (Website): {$post->post_title} ---\nLink: " . get_permalink($post->ID) . $info . "\nContent: $clean_body\n\n";
}
}
$final_context = $rag_context . $wp_context;
return $final_context ?: "No specific website content or Knowledge Base found for this query.";
}
function soft_ai_log_chat($question, $answer, $source = 'widget', $provider_override = '', $model_override = '', $current_url = '', $referrer_url = '', $user_id_override = '') {
global $wpdb;
$opt = get_option('soft_ai_chat_settings');
$ip = !empty($user_id_override) ? $user_id_override : ($_SERVER['REMOTE_ADDR'] ?? '0.0.0.0');
if (!empty($question)) {
soft_ai_update_stats_table($ip, 'user_msg');
}
if ($provider_override === 'system_switch' || $provider_override === 'live_user') {
soft_ai_update_stats_table($ip, 'takeover');
} elseif (!empty($answer)) {
soft_ai_update_stats_table($ip, 'ai_reply');
}
$is_live = (strpos($provider_override, 'live_') !== false);
if (empty($opt['save_history']) && !$is_live) return;
$wpdb->insert($wpdb->prefix . 'soft_ai_chat_logs', [
'time' => current_time('mysql'),
'user_ip' => $ip,
'provider' => $provider_override ?: ($opt['provider'] ?? 'unknown'),
'model' => $model_override ?: ($opt['model'] ?? 'unknown'),
'question' => $question,
'answer' => $answer,
'source' => $source,
'current_url' => $current_url,
'referrer_url' => $referrer_url,
'device' => soft_ai_get_device(),
'country' => soft_ai_get_country()
]);
}
function soft_ai_clean_text_for_social($content) {
$content = html_entity_decode($content, ENT_QUOTES | ENT_HTML5, 'UTF-8');
$content = preg_replace('/^\|?\s*[-:]+\s*(\|\s*[-:]+\s*)+\|?\s*$/m', '', $content);
$content = preg_replace('/^\|\s*/m', '', $content);
$content = preg_replace('/\s*\|$/m', '', $content);
$content = str_replace('|', ' - ', $content);
$content = preg_replace('/!\[([^\]]*)\]\(([^)]+)\)/', '$2', $content);
$content = preg_replace('/\[([^\]]+)\]\(([^)]+)\)/', '$1: $2', $content);
$content = str_replace(['**', '__', '`'], '', $content);
$content = preg_replace('/^#+\s*/m', '', $content);
return trim(wp_strip_all_tags($content));
}
function soft_ai_generate_answer($question, $platform = 'widget', $user_id = '', $current_url = '', $referrer_url = '') {
global $wpdb;
if (empty($user_id)) $user_id = get_current_user_id() ?: md5($_SERVER['REMOTE_ADDR']);
$context = new Soft_AI_Context($user_id, $platform);
$human_keywords = ['human', 'người thật', 'nhân viên', 'tư vấn viên', 'gặp người', 'chat với người', 'support', 'live chat'];
$exit_keywords = ['thoát', 'exit', 'bye', 'bot', 'gặp bot'];
$clean_q = mb_strtolower(trim($question));
if (in_array($clean_q, $human_keywords)) {
$context->set('live_chat_mode', true);
$msg = "Đã chuyển sang chế độ Chat với Nhân Viên 🔴.\nVui lòng nhắn tin, nhân viên sẽ trả lời bạn sớm nhất có thể.";
soft_ai_log_chat($question, $msg, $platform, 'system_switch', '', $current_url, $referrer_url, $user_id);
return $msg;
}
if (in_array($clean_q, $exit_keywords)) {
$context->set('live_chat_mode', false);
return "Đã quay lại chế độ AI Bot 🤖. Bạn cần giúp gì không?";
}
if ($context->get('live_chat_mode')) {
soft_ai_log_chat($question, '', $platform, 'live_user', 'human', $current_url, $referrer_url, $user_id);
return "[WAIT_FOR_HUMAN]";
}
$current_step = $context->get('bot_collecting_info_step');
$cancel_keywords = ['huỷ', 'hủy', 'cancel', 'thôi', 'stop', 'thoát'];
if (in_array($clean_q, $cancel_keywords)) {
$context->set('bot_collecting_info_step', null);
return "Đã hủy thao tác hiện tại. Mình có thể giúp gì khác không?";
}
if ($current_step && class_exists('WooCommerce')) {
$response = soft_ai_handle_ordering_steps($question, $current_step, $context);
if ($platform === 'facebook' || $platform === 'zalo') {
return soft_ai_clean_text_for_social($response);
}
return $response;
}
$checkout_triggers = ['thanh toán', 'thanh toan', 'xác nhận', 'xac nhan', 'chốt đơn', 'chot don', 'đặt hàng', 'dat hang', 'mua ngay', 'pay'];
$is_checkout_intent = false;
foreach ($checkout_triggers as $trigger) {
if (strpos($clean_q, $trigger) !== false) {
$is_checkout_intent = true;
break;
}
}
if ($is_checkout_intent && class_exists('WooCommerce')) {
$response = soft_ai_process_order_logic(['action' => 'checkout'], $context);
if ($platform === 'facebook' || $platform === 'zalo') return soft_ai_clean_text_for_social($response);
return $response;
}
$options = get_option('soft_ai_chat_settings');
$provider = $options['provider'] ?? 'groq';
$model = $options['model'] ?? 'openai/gpt-oss-120b';
$site_context = soft_ai_chat_get_context($question);
$user_instruction = $options['system_prompt'] ?? '';
$system_prompt = "You are a helpful AI assistant for this website.\n" .
($user_instruction ? "Additional Persona: $user_instruction\n" : "") .
"Website & RAG Knowledge Base Context:\n" . $site_context . "\n\n" .
"CRITICAL INSTRUCTIONS:\n" .
"1. If user wants to BUY/ORDER/FIND products, return STRICT JSON only (no markdown):\n" .
" {\"action\": \"find_product\", \"query\": \"product name\"}\n" .
" {\"action\": \"list_products\"}\n" .
" {\"action\": \"check_cart\"}\n" .
" {\"action\": \"checkout\"}\n" .
" {\"action\": \"list_coupons\"}\n" .
" {\"action\": \"apply_coupon\", \"code\": \"CODE_HERE\"}\n" .
"2. If user asks general discovery questions like 'bán gì', 'có gì', 'sản phẩm gì', 'menu', use action 'list_products'.\n" .
"3. If user asks about 'mã giảm giá', 'coupon', 'voucher', 'khuyến mãi', 'ưu đãi', 'discount', use action 'list_coupons'.\n" .
"4. If user wants to apply a code (e.g. 'dùng mã ABC', 'nhập mã XYZ', 'áp dụng mã...'), use action 'apply_coupon'.\n" .
"5. For general chat or questions related to the RAG context, answer normally in Vietnamese based strictly on the context provided.\n" .
"6. If you use tables, use standard Markdown table format. Keep tables simple with 2-3 columns maximum for mobile view." .
"7. If unknown, admit it politely.";
$ai_response = soft_ai_chat_call_api($provider, $model, $system_prompt, $question, $options);
if (is_wp_error($ai_response)) return "Lỗi hệ thống: " . $ai_response->get_error_message();
$clean_response = trim($ai_response);
if (preg_match('/```json\s*(.*?)\s*```/s', $clean_response, $matches)) {
$clean_response = $matches[1];
} elseif (preg_match('/```\s*(.*?)\s*```/s', $clean_response, $matches)) {
$clean_response = $matches[1];
}
$intent = json_decode($clean_response, true);
if (json_last_error() === JSON_ERROR_NONE && isset($intent['action']) && class_exists('WooCommerce')) {
$response = soft_ai_process_order_logic($intent, $context);
if ($platform === 'facebook' || $platform === 'zalo') return soft_ai_clean_text_for_social($response);
return $response;
}
if ($platform === 'facebook' || $platform === 'zalo') {
return soft_ai_clean_text_for_social($clean_response);
}
return $clean_response;
}
function soft_ai_process_order_logic($intent, $context) {
$action = $intent['action'];
$source = $context->source;
switch ($action) {
case 'list_coupons':
$args = ['post_type' => 'shop_coupon', 'post_status' => 'publish', 'posts_per_page' => 5];
$coupons = get_posts($args);
if (empty($coupons)) return "Hiện tại không có mã giảm giá nào ạ.";
$msg = "Dạ, shop đang có các mã ưu đãi này:\n";
foreach ($coupons as $c) {
$code = $c->post_title;
$wc_coupon = new WC_Coupon($code);
if ($wc_coupon->get_usage_limit() > 0 && $wc_coupon->get_usage_count() >= $wc_coupon->get_usage_limit()) continue;
if ($wc_coupon->get_date_expires() && $wc_coupon->get_date_expires()->getTimestamp() < time()) continue;
$desc = $c->post_excerpt ? "({$c->post_excerpt})" : "";
$amount = strip_tags(wc_price($wc_coupon->get_amount()));
if($wc_coupon->get_discount_type() == 'percent') $amount = $wc_coupon->get_amount() . '%';
$msg .= "- **$code** $desc: Giảm $amount\n";
}
return $msg . "\nBạn muốn dùng mã nào cứ nhắn 'Dùng mã [CODE]' nhé!";
case 'apply_coupon':
$code = strtoupper(sanitize_text_field($intent['code'] ?? ''));
if (!$code) return "Bạn chưa nhập mã. Vui lòng nhập mã cụ thể.";
if ($source === 'widget' && function_exists('WC')) {
if (!WC()->cart->has_discount($code)) {
$res = WC()->cart->apply_coupon($code);
if ($res === true) {
return "✅ Đã áp dụng mã **$code** thành công! Tổng đơn hiện tại: " . WC()->cart->get_cart_total();
} else {
return "❌ Mã này không hợp lệ hoặc không áp dụng được cho đơn này ạ.";
}
} else {
return "Mã này đã được áp dụng rồi ạ.";
}
} else {
$saved_coupons = $context->get('coupons') ?: [];
if (!in_array($code, $saved_coupons)) {
$saved_coupons[] = $code;
$context->set('coupons', $saved_coupons);
return "✅ Đã lưu mã **$code**. Sẽ áp dụng khi tạo đơn.";
}
return "Mã này đã lưu rồi ạ.";
}
case 'list_products':
$args = ['limit' => 12, 'status' => 'publish', 'orderby' => 'date', 'order' => 'DESC'];
$products = wc_get_products($args);
if (empty($products)) return "Dạ hiện tại shop chưa cập nhật sản phẩm lên web ạ.";
$msg = "Dạ, bên em đang có những sản phẩm nổi bật này ạ: ";
if ($source !== 'widget') $msg = "Dạ, bên em đang có những sản phẩm nổi bật này ạ:\n";
foreach ($products as $p) {
$price = $p->get_price_html();
$name = $p->get_name();
if ($source === 'widget') {
$img_id = $p->get_image_id();
$img_url = $img_id ? wp_get_attachment_image_url($img_id, 'thumbnail') : wc_placeholder_img_src();
$msg .= "
";
} else {
$plain_price = strip_tags(wc_price($p->get_price()));
$msg .= "- {$name} ({$plain_price})\n";
}
}
$suffix = ($source === 'widget') ? " Bạn quan tâm món nào nhắn tên để em tư vấn nhé!" : "\nBạn quan tâm món nào nhắn tên để em tư vấn nhé!";
return $msg . $suffix;
case 'find_product':
$query = sanitize_text_field($intent['query'] ?? '');
$products = wc_get_products(['status' => 'publish', 'limit' => 1, 's' => $query]);
if (!empty($products)) {
$p = $products[0];
if (!$p->is_in_stock()) return "Sản phẩm " . $p->get_name() . " hiện đang hết hàng ạ.";
$context->set('pending_product_id', $p->get_id());
$attributes = $p->get_attributes();
$attr_keys = array_keys($attributes);
if (!empty($attr_keys) && $p->is_type('variable')) {
$group_attr_key = null;
$group_terms = [];
$keywords = ['người lớn', 'trẻ em', 'em bé', 'nguoi lon', 'tre em', 'em be', 'adult', 'child', 'infant', 'người', 'khách'];
foreach ($attr_keys as $attr_key) {
$terms = wc_get_product_terms($p->get_id(), $attr_key, array('fields' => 'names'));
if (!is_wp_error($terms) && !empty($terms)) {
foreach ($terms as $term) {
$lower_term = mb_strtolower($term);
foreach ($keywords as $kw) {
if (strpos($lower_term, $kw) !== false) {
$group_attr_key = $attr_key;
$group_terms = $terms;
break 3;
}
}
}
}
}
$normal_attrs = [];
if ($group_attr_key) {
foreach ($attr_keys as $k) {
if ($k !== $group_attr_key) $normal_attrs[] = $k;
}
$context->set('group_attr_key', $group_attr_key);
$context->set('group_qty_queue', $group_terms);
$context->set('group_qty_answers', []);
} else {
$normal_attrs = $attr_keys;
$context->set('group_attr_key', null);
}
$context->set('attr_queue', $normal_attrs);
$context->set('attr_answers', []);
$context->set('bot_collecting_info_step', 'process_attribute_loop');
$question = soft_ai_ask_next_attribute($context, $p);
return ($source == 'widget') ? "Tìm thấy: " . $p->get_name() . " . " . $question : "Tìm thấy: " . $p->get_name() . ".\n" . $question;
} else {
$context->set('bot_collecting_info_step', 'ask_quantity');
return "Đã tìm thấy " . $p->get_name() . ". Bạn muốn lấy số lượng bao nhiêu?";
}
}
return "Xin lỗi, mình không tìm thấy sản phẩm nào khớp với '$query'.";
case 'check_cart':
$count = $context->get_cart_count();
return $count > 0
? "Giỏ hàng có $count sản phẩm (" . $context->get_cart_total_string() . "). Gõ 'Thanh toán' để đặt hàng nhé."
: "Giỏ hàng của bạn đang trống.";
case 'checkout':
if ($context->get_cart_count() == 0) return "Giỏ hàng trống. Hãy chọn sản phẩm trước nhé!";
$has_info = false;
$name = '';
if ($source === 'widget' && WC()->customer->get_billing_first_name() && WC()->customer->get_billing_email()) {
$has_info = true;
$name = WC()->customer->get_billing_first_name();
} else {
$saved = $context->get('user_info');
if (!empty($saved['name']) && !empty($saved['email'])) {
$has_info = true;
$name = $saved['name'];
}
}
if ($has_info) {
return soft_ai_present_payment_gateways($context, "Chào $name! Bạn muốn thanh toán qua đâu?");
} else {
$context->set('bot_collecting_info_step', 'fullname');
return "Để đặt hàng, cho em xin Họ và Tên của bạn ạ?";
}
break;
}
return "Tôi chưa hiểu yêu cầu này. Bạn có thể nói rõ hơn không?";
}
function soft_ai_handle_ordering_steps($message, $step, $context) {
$clean_message = trim($message);
$source = $context->source;
switch ($step) {
case 'process_attribute_loop':
$current_slug = $context->get('current_asking_attr');
$valid_options = $context->get('valid_options_for_' . $current_slug);
$is_valid = false;
if (empty($valid_options)) {
$is_valid = true;
} else {
foreach ($valid_options as $opt) {
if (mb_strtolower(trim($opt)) === mb_strtolower($clean_message)) {
$is_valid = true;
$clean_message = $opt;
break;
}
}
}
if (!$is_valid) {
$label = wc_attribute_label($current_slug);
$list_str = implode(', ', $valid_options);
return "⚠️ Dạ shop không có $label '{$message}' ạ.\nVui lòng chỉ chọn một trong các loại sau: **$list_str**";
}
$answers = $context->get('attr_answers') ?: [];
$answers[$current_slug] = $clean_message;
$context->set('attr_answers', $answers);
$context->set('valid_options_for_' . $current_slug, null);
$p = wc_get_product($context->get('pending_product_id'));
return soft_ai_ask_next_attribute($context, $p);
case 'process_group_qty_loop':
$current_term = $context->get('current_asking_group_term');
$qty = intval($clean_message);
if ($qty < 0) return "Số lượng không hợp lệ. Vui lòng nhập lại số lượng cho **$current_term** (nhập 0 nếu không có):";
$answers = $context->get('group_qty_answers') ?: [];
if ($qty > 0) {
$answers[$current_term] = $qty;
}
$context->set('group_qty_answers', $answers);
$pid = $context->get('pending_product_id');
$p = wc_get_product($pid);
if (!$p) return "Có lỗi xảy ra, không tìm thấy sản phẩm.";
return soft_ai_ask_next_group_qty($context, $p);
case 'ask_quantity':
$qty = intval($clean_message);
if ($qty <= 0) return "Số lượng phải lớn hơn 0. Vui lòng nhập lại:";
$pid = $context->get('pending_product_id');
$p = wc_get_product($pid);
if ($p) {
$var_id = 0; $var_data = [];
if ($p->is_type('variable')) {
$collected = $context->get('attr_answers') ?: [];
$var_data = [];
foreach ($collected as $attr_key => $user_val_name) {
$slug_val = $user_val_name;
if (taxonomy_exists($attr_key)) {
$term = get_term_by('name', $user_val_name, $attr_key);
if ($term) $slug_val = $term->slug;
} else {
$slug_val = sanitize_title($user_val_name);
}
$var_data['attribute_' . $attr_key] = $slug_val;
}
$data_store = new WC_Product_Data_Store_CPT();
$var_id = $data_store->find_matching_product_variation($p, $var_data);
if (!$var_id) return "Xin lỗi, phiên bản bạn chọn hiện không tồn tại hoặc đã hết hàng. Vui lòng chọn lại.";
}
$context->add_to_cart($pid, $qty, $var_id, $var_data);
$context->set('bot_collecting_info_step', null);
$total = $context->get_cart_total_string();
return "✅ Đã thêm vào giỏ ($qty cái). Tổng tạm tính: $total.\nGõ 'Thanh toán' để chốt đơn hoặc hỏi mua tiếp.";
}
return "Có lỗi xảy ra với sản phẩm. Vui lòng tìm lại.";
case 'fullname':
$context->set('temp_name', $clean_message);
if ($source === 'widget') WC()->customer->set_billing_first_name($clean_message);
$context->set('bot_collecting_info_step', 'phone');
return "Chào $clean_message, cho em xin Số điện thoại liên hệ?";
case 'phone':
if (!preg_match('/^[0-9]{9,12}$/', $clean_message)) return "Số điện thoại không hợp lệ. Vui lòng nhập lại:";
$context->set('temp_phone', $clean_message);
if ($source === 'widget') WC()->customer->set_billing_phone($clean_message);
$context->set('bot_collecting_info_step', 'email');
return "Dạ, cho em xin địa chỉ Email để gửi thông tin đơn hàng và thanh toán ạ?";
case 'email':
if (!is_email($clean_message)) return "Email không hợp lệ. Vui lòng nhập lại (ví dụ: ten@gmail.com):";
$context->set('temp_email', $clean_message);
if ($source === 'widget') WC()->customer->set_billing_email($clean_message);
$context->set('bot_collecting_info_step', 'address');
return "Cuối cùng, cho em xin Địa chỉ giao hàng cụ thể ạ?";
case 'address':
$context->set('temp_address', $clean_message);
if ($source === 'widget') {
WC()->customer->set_billing_address_1($clean_message);
WC()->customer->save();
} else {
$context->set('user_info', [
'name' => $context->get('temp_name'),
'phone' => $context->get('temp_phone'),
'email' => $context->get('temp_email'),
'address' => $clean_message
]);
}
return soft_ai_present_payment_gateways($context, "Đã lưu địa chỉ. Bạn chọn hình thức thanh toán nào?");
case 'payment_method':
$method_key = mb_strtolower($clean_message);
if (strpos($method_key, 'vietqr') !== false || strpos($method_key, 'chuyển khoản') !== false || strpos($method_key, 'qr') !== false) {
return soft_ai_finalize_order($context, 'vietqr_custom');
}
if (strpos($method_key, 'paypal') !== false) {
return soft_ai_finalize_order($context, 'paypal_custom');
}
$gateways = WC()->payment_gateways->get_available_payment_gateways();
$selected = null;
foreach ($gateways as $g) {
if (stripos($g->title, $clean_message) !== false || stripos($g->id, $clean_message) !== false) {
$selected = $g; break;
}
}
if (!$selected && (stripos($clean_message, 'cod') !== false || stripos($clean_message, 'mặt') !== false)) {
$selected = $gateways['cod'] ?? null;
}
if (!$selected) return "Phương thức chưa đúng. Vui lòng nhập lại (ví dụ: VietQR, PayPal, COD).";
return soft_ai_finalize_order($context, $selected);
}
return "";
}
function soft_ai_ask_next_attribute($context, $product) {
$queue = $context->get('attr_queue');
if (empty($queue)) {
if ($context->get('group_attr_key')) {
$context->set('bot_collecting_info_step', 'process_group_qty_loop');
return soft_ai_ask_next_group_qty($context, $product);
}
$context->set('bot_collecting_info_step', 'ask_quantity');
return "Dạ bạn đã chọn đủ thông tin. Bạn muốn lấy số lượng bao nhiêu ạ?";
}
$current_slug = array_shift($queue);
$context->set('attr_queue', $queue);
$context->set('current_asking_attr', $current_slug);
$terms = wc_get_product_terms($product->get_id(), $current_slug, array('fields' => 'names'));
$options_text = "";
if (!empty($terms) && !is_wp_error($terms)) {
$context->set('valid_options_for_' . $current_slug, $terms);
$options_text = "\n(" . implode(', ', $terms) . ")";
} else {
$context->set('valid_options_for_' . $current_slug, []);
}
$label = wc_attribute_label($current_slug);
return "Bạn chọn **$label** loại nào?$options_text";
}
function soft_ai_ask_next_group_qty($context, $product) {
$queue = $context->get('group_qty_queue');
if (empty($queue)) {
return soft_ai_add_group_variations_to_cart($context, $product);
}
$current_term = array_shift($queue);
$context->set('group_qty_queue', $queue);
$context->set('current_asking_group_term', $current_term);
return "Bạn muốn lấy số lượng cho nhóm **" . $current_term . "** là bao nhiêu? (Nhập 0 nếu không có)";
}
function soft_ai_add_group_variations_to_cart($context, $product) {
$group_answers = $context->get('group_qty_answers');
$normal_answers = $context->get('attr_answers') ?: [];
$group_attr_key = $context->get('group_attr_key');
$pid = $product->get_id();
if (empty($group_answers)) {
$context->set('bot_collecting_info_step', null);
return "Bạn đã không chọn số lượng cho bất kỳ nhóm nào. Đã hủy thêm vào giỏ. Bạn cần giúp gì khác không?";
}
$data_store = new WC_Product_Data_Store_CPT();
$added_count = 0;
$total_qty = 0;
foreach ($group_answers as $term_name => $qty) {
if ($qty <= 0) continue;
$var_data = [];
foreach ($normal_answers as $attr_key => $user_val_name) {
$slug_val = $user_val_name;
if (taxonomy_exists($attr_key)) {
$term = get_term_by('name', $user_val_name, $attr_key);
if ($term) $slug_val = $term->slug;
} else {
$slug_val = sanitize_title($user_val_name);
}
$var_data['attribute_' . $attr_key] = $slug_val;
}
$group_slug_val = $term_name;
if (taxonomy_exists($group_attr_key)) {
$term = get_term_by('name', $term_name, $group_attr_key);
if ($term) $group_slug_val = $term->slug;
} else {
$group_slug_val = sanitize_title($term_name);
}
$var_data['attribute_' . $group_attr_key] = $group_slug_val;
$var_id = $data_store->find_matching_product_variation($product, $var_data);
if ($var_id) {
$context->add_to_cart($pid, $qty, $var_id, $var_data);
$added_count++;
$total_qty += $qty;
}
}
$context->set('bot_collecting_info_step', null);
if ($added_count > 0) {
$total = $context->get_cart_total_string();
return "✅ Đã thêm thành công vào giỏ ($total_qty vé/sản phẩm). Tổng tạm tính: $total.\nGõ 'Thanh toán' để chốt đơn hoặc hỏi mua tiếp.";
} else {
return "Xin lỗi, phiên bản bạn chọn hiện không tồn tại hoặc đã hết hàng. Vui lòng tìm và chọn lại.";
}
}
function soft_ai_present_payment_gateways($context, $msg) {
$gateways = WC()->payment_gateways->get_available_payment_gateways();
$opts = get_option('soft_ai_chat_settings');
$list = "";
$prefix = ($context->source == 'widget') ? " • " : "\n- ";
foreach ($gateways as $g) $list .= $prefix . $g->get_title();
if (!empty($opts['vietqr_bank']) && !empty($opts['vietqr_acc'])) $list .= $prefix . "VietQR (Chuyển khoản nhanh)";
if (!empty($opts['paypal_me'])) $list .= $prefix . "PayPal";
$context->set('bot_collecting_info_step', 'payment_method');
return $msg . $list;
}
function soft_ai_finalize_order($context, $gateway_or_code) {
try {
$order = wc_create_order();
$opts = get_option('soft_ai_chat_settings');
if ($context->source === 'widget' && function_exists('WC')) {
foreach (WC()->cart->get_cart() as $values) $order->add_product($values['data'], $values['quantity']);
} else {
$cart = $context->get('cart') ?: [];
foreach ($cart as $key => $item) {
$pid = isset($item['product_id']) ? $item['product_id'] : $key;
$vid = isset($item['variation_id']) ? $item['variation_id'] : 0;
$p = wc_get_product($vid ? $vid : $pid);
if ($p) $order->add_product($p, $item['qty']);
}
$stored_coupons = $context->get('coupons') ?: [];
foreach($stored_coupons as $code) {
$order->apply_coupon($code);
}
}
$name = $context->get('temp_name');
$phone = $context->get('temp_phone');
$email = $context->get('temp_email');
$address = $context->get('temp_address');
if ($context->source === 'widget' && function_exists('WC') && WC()->customer) {
if (empty($name)) $name = WC()->customer->get_billing_first_name();
if (empty($phone)) $phone = WC()->customer->get_billing_phone();
if (empty($email)) $email = WC()->customer->get_billing_email();
if (empty($address)) $address = WC()->customer->get_billing_address_1();
}
$parts = explode(' ', trim($name));
$last_name = (count($parts) > 1) ? array_pop($parts) : '';
$first_name = implode(' ', $parts);
if (empty($first_name)) $first_name = $name;
$billing_info = [
'first_name' => $first_name, 'last_name' => $last_name, 'phone' => $phone,
'email' => $email ?: 'no-email@example.com', 'address_1' => $address, 'country' => 'VN',
];
$order->set_address($billing_info, 'billing');
$order->set_address($billing_info, 'shipping');
$extra_msg = "";
if ($gateway_or_code === 'vietqr_custom') {
$order->set_payment_method('bacs');
$order->set_payment_method_title('VietQR (Chat)');
$order->calculate_totals();
$bacs_accounts = get_option('woocommerce_bacs_accounts');
$bank = ''; $acc = ''; $name_acc = '';
if (!empty($bacs_accounts) && is_array($bacs_accounts)) {
$account = $bacs_accounts[0];
$bank = str_replace(' ', '', $account['bank_name']);
$acc = str_replace(' ', '', $account['account_number']);
$name_acc = str_replace(' ', '%20', $account['account_name']);
} else {
$bank = str_replace(' ', '', $opts['vietqr_bank'] ?? '');
$acc = str_replace(' ', '', $opts['vietqr_acc'] ?? '');
$name_acc = str_replace(' ', '%20', $opts['vietqr_name'] ?? '');
}
$amt = intval($order->get_total());
$desc = "DH" . $order->get_id();
if ($bank && $acc) {
$qr_url = "https://img.vietqr.io/image/{$bank}-{$acc}-compact.jpg?amount={$amt}&addInfo={$desc}&accountName={$name_acc}";
$extra_msg = "\n\n⬇️ **Quét mã để thanh toán:**\n";
if ($context->source == 'widget') $extra_msg = "Quét mã để thanh toán: ";
}
} elseif ($gateway_or_code === 'paypal_custom') {
$order->set_payment_method('paypal');
$order->set_payment_method_title('PayPal (Chat Link)');
$order->calculate_totals();
$raw_user = $opts['paypal_me'] ?? '';
$raw_user = str_replace(['https://', 'http://', 'paypal.me/', '/'], '', $raw_user);
$currency = get_woocommerce_currency();
$amt = $order->get_total();
$pp_link = "https://paypal.me/{$raw_user}/{$amt}{$currency}";
$extra_msg = "\n\n👉 [Nhấn để thanh toán PayPal]($pp_link)";
if ($context->source == 'widget') $extra_msg = "Thanh toán ngay với PayPal ";
} else {
$order->set_payment_method($gateway_or_code);
$order->calculate_totals();
}
$order->update_status('on-hold', "Order created via Soft AI Chat. IP: " . ($_SERVER['REMOTE_ADDR'] ?? 'Unknown'));
$context->empty_cart();
$context->set('bot_collecting_info_step', null);
$ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
soft_ai_update_stats_table($ip, 'ai_order');
return "🎉 ĐẶT HÀNG THÀNH CÔNG!\nMã đơn: #" . $order->get_id() . "\nEmail xác nhận đã gửi tới " . $billing_info['email'] . "." . $extra_msg;
} catch (Exception $e) {
return "Lỗi khi tạo đơn: " . $e->getMessage();
}
}
// ---------------------------------------------------------
// 4. API CALLER
// ---------------------------------------------------------
function soft_ai_chat_call_api($provider, $model, $sys, $user, $opts) {
$api_key = $opts[$provider . '_api_key'] ?? '';
if (!$api_key) return new WP_Error('missing_key', 'API Key Missing');
$url = '';
$headers = ['Content-Type' => 'application/json'];
$body = [];
switch ($provider) {
case 'groq':
$url = 'https://api.groq.com/openai/v1/chat/completions';
$headers['Authorization'] = 'Bearer ' . $api_key;
$body = [
'model' => $model,
'messages' => [['role' => 'system', 'content' => $sys], ['role' => 'user', 'content' => $user]],
'temperature' => (float)$opts['temperature'],
'max_tokens' => (int)$opts['max_tokens']
];
break;
case 'openai':
$url = 'https://api.openai.com/v1/chat/completions';
$headers['Authorization'] = 'Bearer ' . $api_key;
$body = [
'model' => $model,
'messages' => [['role' => 'system', 'content' => $sys], ['role' => 'user', 'content' => $user]],
'temperature' => (float)$opts['temperature'],
'max_tokens' => (int)$opts['max_tokens']
];
break;
case 'gemini':
$url = "https://generativelanguage.googleapis.com/v1beta/models/{$model}:generateContent?key={$api_key}";
$body = [
'system_instruction' => ['parts' => [['text' => $sys]]],
'contents' => [['role' => 'user', 'parts' => [['text' => $user]]]],
'generationConfig' => ['temperature' => (float)$opts['temperature'], 'maxOutputTokens' => (int)$opts['max_tokens']]
];
break;
}
$response = wp_remote_post($url, [
'headers' => $headers,
'body' => json_encode($body),
'timeout' => 60
]);
if (is_wp_error($response)) return $response;
$data = json_decode(wp_remote_retrieve_body($response), true);
if (isset($data['choices'][0]['message']['content'])) return $data['choices'][0]['message']['content'];
if (isset($data['candidates'][0]['content']['parts'][0]['text'])) return $data['candidates'][0]['content']['parts'][0]['text'];
return "API Error: " . wp_remote_retrieve_body($response);
}
// ---------------------------------------------------------
// 5. REST API & WEBHOOKS
// ---------------------------------------------------------
add_action('rest_api_init', function () {
register_rest_route('soft-ai-chat/v1', '/ask', [
'methods' => 'POST',
'callback' => 'soft_ai_chat_handle_widget_request',
'permission_callback' => '__return_true',
]);
register_rest_route('soft-ai-chat/v1', '/poll', [
'methods' => 'POST',
'callback' => 'soft_ai_chat_poll_messages',
'permission_callback' => '__return_true',
]);
register_rest_route('soft-ai-chat/v1', '/webhook/facebook', [
'methods' => ['GET', 'POST'],
'callback' => 'soft_ai_chat_webhook_facebook',
'permission_callback' => '__return_true',
]);
register_rest_route('soft-ai-chat/v1', '/webhook/zalo', [
'methods' => 'POST',
'callback' => 'soft_ai_chat_webhook_zalo',
'permission_callback' => '__return_true',
]);
});
function soft_ai_get_facebook_user_profile($psid, $token) {
if (!$token) return 'Unknown FB User';
$url = "https://graph.facebook.com/v19.0/{$psid}?fields=first_name,last_name,profile_pic&access_token={$token}";
$response = wp_remote_get($url, ['timeout' => 5]);
if (is_wp_error($response)) return 'Unknown FB User';
$body = json_decode(wp_remote_retrieve_body($response), true);
if (isset($body['first_name'])) {
return trim($body['first_name'] . ' ' . ($body['last_name'] ?? ''));
}
return 'Unknown FB User';
}
function soft_ai_chat_handle_widget_request($request) {
if (function_exists('WC') && !WC()->session) {
$session_class = apply_filters('woocommerce_session_handler', 'WC_Session_Handler');
WC()->session = new $session_class();
WC()->session->init();
if (!WC()->cart) { WC()->cart = new WC_Cart(); WC()->cart->get_cart(); }
if (!WC()->customer) { WC()->customer = new WC_Customer(get_current_user_id()); }
}
$params = $request->get_json_params();
$question = sanitize_text_field($params['question'] ?? '');
$current_url = esc_url_raw($params['current_url'] ?? '');
$referrer_url = esc_url_raw($params['referrer_url'] ?? '');
if (!$question) return new WP_Error('no_input', 'Empty Question', ['status' => 400]);
soft_ai_notify_admin_by_email($question, 'Website Widget', $_SERVER['REMOTE_ADDR']);
$answer = soft_ai_generate_answer($question, 'widget', '', $current_url, $referrer_url);
if ($answer === '[WAIT_FOR_HUMAN]') {
return rest_ensure_response(['answer' => '', 'live_mode' => true]);
}
soft_ai_log_chat($question, $answer, 'widget', '', '', $current_url, $referrer_url);
return rest_ensure_response(['answer' => $answer]);
}
function soft_ai_chat_poll_messages($request) {
global $wpdb;
$ip = $_SERVER['REMOTE_ADDR'];
$table = $wpdb->prefix . 'soft_ai_chat_logs';
$last_id = (int) ($request->get_json_params()['last_id'] ?? 0);
$new_msgs = $wpdb->get_results($wpdb->prepare("SELECT id, answer, time, model FROM $table WHERE user_ip = %s AND provider = 'live_admin' AND id > %d ORDER BY time ASC", $ip, $last_id));
$data = [];
foreach($new_msgs as $m) {
$admin_name = (!empty($m->model) && $m->model !== 'human') ? $m->model : 'Nhân viên';
$data[] = ['id' => $m->id, 'text' => $m->answer, 'admin_name' => $admin_name];
}
return rest_ensure_response(['messages' => $data]);
}
function soft_ai_chat_webhook_facebook($request) {
$options = get_option('soft_ai_chat_settings');
$verify_token = $options['fb_verify_token'] ?? 'soft_ai_verify';
if ($request->get_method() === 'GET') {
$params = $request->get_query_params();
if (isset($params['hub_verify_token']) && $params['hub_verify_token'] === $verify_token) {
echo $params['hub_challenge']; exit;
}
return new WP_Error('forbidden', 'Invalid Token', ['status' => 403]);
}
$body = $request->get_json_params();
if (isset($body['object']) && $body['object'] === 'page') {
foreach ($body['entry'] as $entry) {
foreach ($entry['messaging'] as $event) {
if (isset($event['message']['text']) && !isset($event['message']['is_echo'])) {
$sender = $event['sender']['id'];
$user_msg = $event['message']['text'];
// Fetch & Cache Facebook Profile Name
$fb_name = get_transient('soft_ai_fb_name_' . $sender);
if ($fb_name === false) {
$fb_name = soft_ai_get_facebook_user_profile($sender, $options['fb_page_token'] ?? '');
set_transient('soft_ai_fb_name_' . $sender, $fb_name, 30 * DAY_IN_SECONDS);
}
$email_sender_id = ($fb_name && $fb_name !== 'Unknown FB User') ? "$fb_name (ID: $sender)" : $sender;
soft_ai_notify_admin_by_email($user_msg, 'Facebook', $email_sender_id);
$reply = soft_ai_generate_answer($user_msg, 'facebook', $sender);
if ($reply !== '[WAIT_FOR_HUMAN]') {
soft_ai_send_fb_message($sender, $reply, $options['fb_page_token']);
soft_ai_log_chat($event['message']['text'], $reply, 'facebook', '', '', '', '', $sender);
}
}
}
}
return rest_ensure_response(['status' => 'EVENT_RECEIVED']);
}
return new WP_Error('bad_req', 'Invalid FB Data', ['status' => 404]);
}
function soft_ai_send_fb_message($recipient, $text, $token) {
if (!$token) return;
$chunks = function_exists('mb_str_split') ? mb_str_split($text, 1900) : str_split($text, 1900);
foreach ($chunks as $chunk) {
wp_remote_post("https://graph.facebook.com/v21.0/me/messages?access_token=$token", [
'headers' => ['Content-Type' => 'application/json'],
'body' => wp_json_encode([
'messaging_type' => 'RESPONSE',
'recipient' => ['id' => (string)$recipient],
'message' => ['text' => $chunk]
])
]);
}
}
function soft_ai_chat_webhook_zalo($request) {
$body = $request->get_json_params();
if (isset($body['event_name']) && $body['event_name'] === 'user_send_text') {
$sender = $body['sender']['id'];
$user_msg = $body['message']['text'];
soft_ai_notify_admin_by_email($user_msg, 'Zalo', $sender);
$reply = soft_ai_generate_answer($user_msg, 'zalo', $sender);
if ($reply !== '[WAIT_FOR_HUMAN]') {
$token = get_option('soft_ai_chat_settings')['zalo_access_token'] ?? '';
if ($token) {
wp_remote_post("https://openapi.zalo.me/v3.0/oa/message/cs", [
'headers' => ['access_token' => $token, 'Content-Type' => 'application/json'],
'body' => wp_json_encode(['recipient' => ['user_id' => (string)$sender], 'message' => ['text' => $reply]])
]);
}
soft_ai_log_chat($body['message']['text'], $reply, 'zalo', '', '', '', '', $sender);
}
return rest_ensure_response(['status' => 'success']);
}
return rest_ensure_response(['status' => 'ignored']);
}
// ---------------------------------------------------------
// 6. FRONTEND WIDGETS
// ---------------------------------------------------------
add_action('wp_footer', 'soft_ai_chat_inject_widget');
add_action('wp_footer', 'soft_ai_social_widgets_render');
function soft_ai_social_widgets_render() {
$options = get_option('soft_ai_chat_settings');
// Zalo
if (!empty($options['enable_zalo_widget']) && !empty($options['zalo_oa_id'])) {
$zalo_id = esc_attr($options['zalo_oa_id']);
$welcome = esc_attr($options['welcome_msg'] ?? 'Xin chào!');
echo <<
HTML;
}
// FB
if (!empty($options['enable_fb_widget']) && !empty($options['fb_page_id'])) {
$fb_id = esc_attr($options['fb_page_id']);
echo <<
HTML;
}
}
function soft_ai_chat_inject_widget() {
global $wpdb;
$options = get_option('soft_ai_chat_settings');
if (is_admin() || empty($options['provider'])) return;
$color = $options['theme_color'] ?? '#027DDD';
$welcome = $options['welcome_msg'] ?? 'Xin chào! Bạn cần tìm gì ạ?';
$chat_title = $options['chat_title'] ?? 'Trợ lý AI';
$shop_name = get_bloginfo('name');
$ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
$table = $wpdb->prefix . 'soft_ai_chat_logs';
$max_id = (int) $wpdb->get_var("SELECT MAX(id) FROM $table");
?>
💬