PluginProbe
WPBot – AI ChatBot for Live Support, Lead Generation, WordPress Automation, AI Services / 8.8.2
WPBot – AI ChatBot for Live Support, Lead Generation, WordPress Automation, AI Services v8.8.2
8.8.2 8.8.1 8.8.0 8.7.9 8.7.8 8.7.7 8.7.6 8.7.5 8.7.4 8.7.3 8.7.2 8.7.1 8.7.0 8.6.9 8.6.8 8.6.7 8.6.6 8.6.5 8.6.4 8.6.2 8.6.1 8.6.0 8.5.9 8.5.8 8.5.7 All 538 releases
← All changes | includes/integration/openai/qcld-bot-openai.php +259 -41 8.6.0 → 8.8.2 View file →
@@ -375,9 +375,8 @@
375 375 public function get_prompt($keyword){
376 376 $openai_include_keyword = get_option( 'openai_include_keyword');
377 377 $openai_exclude_keyword = get_option( 'openai_exclude_keyword');
378 378 $qcld_openai_prompt = get_option('qcld_openai_prompt',true);
379 -
380 379 }
381 380 public function include_exclude_prompt($keyword){
382 381 $openai_include_keyword = strtolower(get_option('openai_include_keyword'));
383 382 $openai_exclude_keyword = strtolower(get_option('openai_exclude_keyword'));
@@ -434,9 +433,9 @@
434 433 @ini_set( 'implicit_flush', 1 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
435 434 while ( ob_get_level() ) {
436 435 ob_end_clean();
437 436 }
438 - ob_implicit_flush( 1 );
437 + ob_implicit_flush( true );
439 438
440 439 header( 'Content-Type: text/event-stream' );
441 440 header( 'Cache-Control: no-cache' );
442 441 header( 'Connection: keep-alive' );
@@ -449,10 +448,58 @@
449 448 flush();
450 449 wp_die();
451 450 }
452 451
453 - $system_content = get_option( 'qcld_openai_system_content', 'You are a helpful assistant.' );
452 + $is_playground = isset( $_POST['is_ai_actions_playground'] ) && intval( $_POST['is_ai_actions_playground'] ) === 1;
453 + $active_ai_action = isset( $_POST['active_ai_action'] ) ? sanitize_text_field( wp_unslash( $_POST['active_ai_action'] ) ) : '';
454 + $raw_ai_history = isset( $_POST['ai_history'] ) ? json_decode( stripslashes( $_POST['ai_history'] ), true ) : array();
455 + $is_ai_action = $is_playground || ! empty( $active_ai_action ) || ( class_exists( 'Qcld_WPBot_Common_Functions' ) && Qcld_WPBot_Common_Functions::is_ai_action_in_progress( $raw_ai_history, $keyword ) );
454 456
457 + $system_content = $is_playground ? 'You are a helpful AI assistant. You MUST strictly follow the interactive form instructions if the user asks for them.' : get_option( 'qcld_openai_system_content' );
458 + if ( empty( $system_content ) ) {
459 + $system_content = 'You are a helpful assistant.';
460 + }
461 +
462 + // AI Interactive Form
463 + $saved_ai_forms = get_option( 'wpbot_ai_forms', array() );
464 + $active_interactive_forms = array();
465 + if ( get_option( 'enable_ai_interactive_form' ) == '1' && ! empty( $saved_ai_forms ) && is_array( $saved_ai_forms ) ) {
466 + foreach ( $saved_ai_forms as $form ) {
467 + if ( ! isset( $form['interactive'] ) || $form['interactive'] == 1 ) {
468 + $active_interactive_forms[] = $form;
469 + }
470 + }
471 + }
472 +
473 + if ( ! empty( $active_interactive_forms ) ) {
474 + $system_content .= "\n\nYou must handle the following interactive forms when the user asks for them:\n";
475 + foreach ( $active_interactive_forms as $form ) {
476 + $system_content .= "\nForm Title: " . $form['title'] . "\nInstructions: " . $form['prompt'] . "\n";
477 + }
478 + $system_content .= "\n\nCRITICAL INSTRUCTIONS FOR INTERACTIVE FORMS:\n";
479 + $system_content .= "When a user triggers an interactive form, you must act as a step-by-step data collection agent.\n";
480 + $system_content .= "1. DO NOT ask all questions at once. Ask exactly ONE question at a time.\n";
481 + $system_content .= "2. Wait for the user's response before asking the next question.\n";
482 + $system_content .= "3. Once all necessary information is collected for the form, you MUST output a final JSON block summarizing the collected data. The keys inside the \"data\" object MUST be dynamically named based on the specific questions you asked during the form collection (e.g., \"Full Name\", \"Company Size\", \"Email\", etc.). The final JSON block must be wrapped EXACTLY in these delimiters:\n";
483 + $system_content .= "__AI_FORM_DATA__{ \"form_title\": \"<Form Title>\", \"data\": { \"<Generated Key 1>\": \"Answer 1\", \"<Generated Key 2>\": \"Answer 2\" } }__AI_FORM_DATA_END__\n";
484 + $system_content .= "Do not include any other text after this JSON block once the form is complete.\n";
485 + $system_content .= "4. If the user provides an invalid, irrelevant, or nonsensical answer to your question, DO NOT apologize or state that you lack information. Instead, politely inform the user in the language of the conversation (the language the user is speaking in, e.g. German if communicating in German) that their answer is invalid or cannot be assigned, and ask the exact same question again in the user's language. NEVER output the English phrase 'Invalid answer found' unless the user is communicating in English.\n";
486 + $system_content .= "5. MULTI-LANGUAGE SUPPORT: Always communicate, ask questions, validate answers, and reply strictly in the user's language (matching the language used by the user, e.g. German, French, Spanish, etc.). Understand and accept valid answers in the user's language.";
487 + }
488 +
489 + if ( $is_ai_action ) {
490 + $system_content .= "\n\nCRITICAL ACTIVE FORM COLLECTION INSTRUCTION:\n" .
491 + "You are currently conducting an interactive step-by-step form data collection.\n" .
492 + "RULES FOR ACTIVE FORM COLLECTION:\n" .
493 + "1. The user's message is an answer to your last question (e.g. name, email, dates, guests, room type, phone number, etc.).\n" .
494 + "2. Review the conversation history carefully. Notice which questions from the form have ALREADY been asked and answered.\n" .
495 + "3. NEVER repeat questions that have already been answered earlier in the conversation.\n" .
496 + "4. Ask the NEXT missing question in the form sequence, exactly ONE question at a time.\n" .
497 + "5. Once ALL questions for this form have been answered, DO NOT ask any more questions or restart the form. Immediately output the final summary JSON block wrapped in __AI_FORM_DATA__{ \"form_title\": \"...\", \"data\": { ... } }__AI_FORM_DATA_END__.\n" .
498 + "6. DO NOT apologize or state that you lack information in documentation. Just proceed with the form collection.\n" .
499 + "7. LANGUAGE CONSISTENCY: Always respond, ask questions, validate answers, and provide feedback in the user's language (the language the user is speaking, e.g. German, French, Spanish, etc.). Do not switch to English. If an answer cannot be assigned or is invalid, explain this politely in the user's language and re-ask the question in that language. Always accept valid answers in the user's language.";
500 + }
501 +
455 502 // RAG integration
456 503 if ( get_option( 'is_page_rag_enabled' ) == '1' ) {
457 504 if ( class_exists( 'Qcld_Bot_Rag' ) ) {
458 505 $rag_context_text = Qcld_Bot_Rag::instance()->run_rag_search( $keyword );
@@ -490,12 +537,30 @@
490 537 $model = get_option( 'openai_engines', 'gpt-4o' );
491 538 }
492 539
493 540 $messages = [
494 - [ 'role' => 'system', 'content' => $system_content ],
495 - [ 'role' => 'user', 'content' => $keyword ],
541 + [ 'role' => 'system', 'content' => $system_content ]
496 542 ];
497 543
544 + $history_added = false;
545 + if (isset($_POST['ai_history'])) {
546 + $ai_history = json_decode(stripslashes($_POST['ai_history']), true);
547 + if (is_array($ai_history) && !empty($ai_history)) {
548 + foreach ($ai_history as $hist_msg) {
549 + if (isset($hist_msg['role']) && isset($hist_msg['content'])) {
550 + $messages[] = [
551 + 'role' => sanitize_text_field($hist_msg['role']),
552 + 'content' => sanitize_text_field($hist_msg['content'])
553 + ];
554 + }
555 + }
556 + $history_added = true;
557 + }
558 + }
559 + if (!$history_added) {
560 + $messages[] = [ 'role' => 'user', 'content' => $keyword ];
561 + }
562 +
498 563 $headers = [
499 564 'Content-Type: application/json',
500 565 'Authorization: Bearer ' . $api_key,
501 566 ];
@@ -505,8 +570,9 @@
505 570 'messages' => $messages,
506 571 'stream' => true,
507 572 ] );
508 573
574 + // phpcs:disable WordPress.WP.AlternativeFunctions.curl_curl_init, WordPress.WP.AlternativeFunctions.curl_curl_setopt, WordPress.WP.AlternativeFunctions.curl_curl_exec, WordPress.WP.AlternativeFunctions.curl_curl_errno, WordPress.WP.AlternativeFunctions.curl_curl_error, WordPress.WP.AlternativeFunctions.curl_curl_close -- SSE streaming requires cURL write callback.
509 575 $ch = curl_init( 'https://api.openai.com/v1/chat/completions' );
510 576 curl_setopt( $ch, CURLOPT_POST, true );
511 577 curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers );
512 578 curl_setopt( $ch, CURLOPT_POSTFIELDS, $post_data );
@@ -521,8 +587,9 @@
521 587 echo 'data: [ERROR] ' . esc_html( curl_error( $ch ) ) . "\n\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- curl_error is already escaped above
522 588 flush();
523 589 }
524 590 curl_close( $ch );
591 + // phpcs:enable WordPress.WP.AlternativeFunctions.curl_curl_init, WordPress.WP.AlternativeFunctions.curl_curl_setopt, WordPress.WP.AlternativeFunctions.curl_curl_exec, WordPress.WP.AlternativeFunctions.curl_curl_errno, WordPress.WP.AlternativeFunctions.curl_curl_error, WordPress.WP.AlternativeFunctions.curl_curl_close
525 592 do_action( 'qcld_openai_user_rate_cal', 1 );
526 593 exit;
527 594 }
528 595
@@ -543,13 +610,61 @@
543 610 $gptkeyword = [];
544 611 $keyword = sanitize_text_field(wp_unslash($_POST['keyword']));
545 612 $relevant_pagelink = $this->relevant_pagelink($keyword);
546 613
614 + // Check if from AI Actions Playground
615 + $is_playground = isset($_POST['is_ai_actions_playground']) && intval($_POST['is_ai_actions_playground']) === 1;
616 + $active_ai_action = isset($_POST['active_ai_action']) ? sanitize_text_field(wp_unslash($_POST['active_ai_action'])) : '';
617 +
618 + $raw_ai_history = isset($_POST['ai_history']) ? json_decode(stripslashes($_POST['ai_history']), true) : array();
619 + $is_ai_action = $is_playground || !empty($active_ai_action) || Qcld_WPBot_Common_Functions::is_ai_action_in_progress($raw_ai_history, $keyword);
620 +
547 621 // Build context-aware system instructions
548 - $system_content = get_option('qcld_openai_system_content');
622 + $system_content = $is_playground ? 'You are a helpful AI assistant. You MUST strictly follow the interactive form instructions if the user asks for them.' : get_option('qcld_openai_system_content');
549 623
624 + // AI Interactive Form
625 + $saved_ai_forms = get_option('wpbot_ai_forms', array());
626 + $active_interactive_forms = array();
627 +
628 + if (!empty($saved_ai_forms) && is_array($saved_ai_forms)) {
629 + foreach ($saved_ai_forms as $form) {
630 + if (!isset($form['interactive']) || $form['interactive'] == 1) {
631 + $active_interactive_forms[] = $form;
632 + }
633 + }
634 + }
635 +
636 + if (!empty($active_interactive_forms)) {
637 + $system_content .= "\n\nYou must handle the following interactive forms when the user asks for them:\n";
638 + foreach ($active_interactive_forms as $form) {
639 + $system_content .= "\nForm Title: " . $form['title'] . "\nInstructions: " . $form['prompt'] . "\n";
640 + }
641 + $system_content .= "\n\nCRITICAL INSTRUCTIONS FOR INTERACTIVE FORMS:\n";
642 + $system_content .= "When a user triggers an interactive form, you must act as a step-by-step data collection agent.\n";
643 + $system_content .= "1. DO NOT ask all questions at once. Ask exactly ONE question at a time.\n";
644 + $system_content .= "2. Wait for the user's response before asking the next question.\n";
645 + $system_content .= "3. Once all necessary information is collected for the form, you MUST output a final JSON block summarizing the collected data. The keys inside the \"data\" object MUST be dynamically named based on the specific questions you asked during the form collection (e.g., \"Full Name\", \"Company Size\", \"Email\", etc.). The final JSON block must be wrapped EXACTLY in these delimiters:\n";
646 + $system_content .= "__AI_FORM_DATA__{ \"form_title\": \"<Form Title>\", \"data\": { \"<Generated Key 1>\": \"Answer 1\", \"<Generated Key 2>\": \"Answer 2\" } }__AI_FORM_DATA_END__\n";
647 + $system_content .= "Do not include any other text after this JSON block once the form is complete.\n";
648 + $system_content .= "4. If the user provides an invalid, irrelevant, or nonsensical answer to your question, DO NOT apologize or state that you lack information. Instead, politely inform the user in the language of the conversation (the language the user is speaking in, e.g. German if communicating in German) that their answer is invalid or cannot be assigned, and ask the exact same question again in the user's language. NEVER output the English phrase 'Invalid answer found' unless the user is communicating in English.\n";
649 + $system_content .= "5. MULTI-LANGUAGE SUPPORT: Always communicate, ask questions, validate answers, and reply strictly in the user's language (matching the language used by the user, e.g. German, French, Spanish, etc.). Understand and accept valid answers in the user's language.";
650 + }
651 +
652 + if ($is_ai_action) {
653 + $system_content .= "\n\nCRITICAL ACTIVE FORM COLLECTION INSTRUCTION:\n" .
654 + "You are currently conducting an interactive step-by-step form data collection.\n" .
655 + "RULES FOR ACTIVE FORM COLLECTION:\n" .
656 + "1. The user's message is an answer to your last question (e.g. name, email, dates, guests, room type, phone number, etc.).\n" .
657 + "2. Review the conversation history carefully. Notice which questions from the form have ALREADY been asked and answered.\n" .
658 + "3. NEVER repeat questions that have already been answered earlier in the conversation.\n" .
659 + "4. Ask the NEXT missing question in the form sequence, exactly ONE question at a time.\n" .
660 + "5. Once ALL questions for this form have been answered, DO NOT ask any more questions or restart the form. Immediately output the final summary JSON block wrapped in __AI_FORM_DATA__{ \"form_title\": \"...\", \"data\": { ... } }__AI_FORM_DATA_END__.\n" .
661 + "6. DO NOT apologize or state that you lack information in documentation. Just proceed with the form collection.\n" .
662 + "7. LANGUAGE CONSISTENCY: Always respond, ask questions, validate answers, and provide feedback in the user's language (the language the user is speaking, e.g. German, French, Spanish, etc.). Do not switch to English. If an answer cannot be assigned or is invalid, explain this politely in the user's language and re-ask the question in that language. Always accept valid answers in the user's language.";
663 + }
664 +
550 665 // RAG Integration
551 - if (get_option('is_page_rag_enabled') == '1') {
666 + if (!$is_playground && !$is_ai_action && get_option('is_page_rag_enabled') == '1') {
552 667 $rag_context_text = Qcld_Bot_Rag::instance()->run_rag_search($keyword);
553 668 if (!empty($rag_context_text) && $rag_context_text != "No knowledge base found.") {
554 669 $rag_context = "Relevant Knowledge Base Information:\n";
555 670 $rag_context .= $rag_context_text;
@@ -557,9 +672,9 @@
557 672 $system_content .= "\n\n" . $rag_context;
558 673 }
559 674 }
560 675
561 - if ( get_option('context_awareness_enabled') == '1' ) {
676 + if ( !$is_playground && get_option('context_awareness_enabled') == '1' ) {
562 677 $site_name = get_bloginfo('name');
563 678 $site_desc = get_bloginfo('description');
564 679
565 680 // Get current page URL and title more reliably
@@ -640,9 +755,9 @@
640 755 }
641 756
642 757 $relevant_pagelink = array_slice($relevant_pagelink, 0, 5, true);
643 758
644 - if( (get_option('page_suggestion_enabled') == '1') && count($relevant_pagelink) > 0 ){
759 + if( !$is_ai_action && (get_option('page_suggestion_enabled') == '1') && count($relevant_pagelink) > 0 ){
645 760
646 761 $relevant_post_link = get_option('qlcd_wp_chatbot_relevant_post_link_openai');
647 762
648 763 if(is_array($relevant_post_link )){
@@ -655,36 +770,39 @@
655 770 $relevant_pagelinks = '';
656 771 }
657 772
658 773
659 - array_push( $gptkeyword, array(
774 + $gptkeyword[] = array(
660 775 "role" => "system",
661 - "content" => $system_content
662 - ));
663 - array_push($gptkeyword, array(
664 - "role" => "user",
665 - "content" => $keyword
666 - ));
667 - if(((get_option('openai_include_keyword') != '') || (get_option('openai_exclude_keyword') != '')) && (get_option('qcld_openai_relevant_enabled') == '1') ){
668 - $prompts = $this->include_exclude_prompt($keyword);
776 + "content" => $system_content
777 + );
669 778
670 - $gptkeyword = [];
671 - array_push($gptkeyword, array(
779 + $history_added = false;
780 + if (isset($_POST['ai_history'])) {
781 + $ai_history = json_decode(stripslashes($_POST['ai_history']), true);
782 + if (is_array($ai_history) && !empty($ai_history)) {
783 + foreach ($ai_history as $hist_msg) {
784 + if (isset($hist_msg['role']) && isset($hist_msg['content'])) {
785 + $gptkeyword[] = array(
786 + "role" => sanitize_text_field($hist_msg['role']),
787 + "content" => sanitize_text_field($hist_msg['content'])
788 + );
789 + }
790 + }
791 + $history_added = true;
792 + }
793 + }
794 + if (!$history_added) {
795 + $gptkeyword[] = array(
672 796 "role" => "user",
673 - "content" => $prompts,
674 - ));
675 - }else if(((get_option('openai_include_keyword') != '') || (get_option('openai_exclude_keyword') != '')) && (get_option('qcld_openai_relevant_enabled') == '0')){
797 + "content" => $keyword
798 + );
799 + }
800 + if(((get_option('openai_include_keyword') != '') || (get_option('openai_exclude_keyword') != '')) && (get_option('qcld_openai_relevant_enabled') == '0')){
676 801 if($this->qcld_include_keyword_exist($keyword) == false){
677 -
678 802 $response['message'] = 'Sorry, No result found!';
679 803 wp_send_json( $response );
680 - }else{
681 - array_push($gptkeyword, array(
682 - "role" => "user",
683 - "content" => $keyword
684 - ));
685 804 }
686 -
687 805 }
688 806 $res = $OpenAI->gptcomplete(
689 807 $gptkeyword
690 808 );
@@ -689,25 +807,23 @@
689 807 $gptkeyword
690 808 );
691 809 $mess = json_decode($res);
692 810 $Qcld_Parsedown = new Qcld_Parsedown();
693 - $msg = $mess->output[0]->content[0]->text;
694 - if( $msg == null || empty($msg) ){
811 + $msg = isset($mess->output[0]->content[0]->text) ? $mess->output[0]->content[0]->text : '';
812 + if( empty($msg) && isset($mess->output[1]->content[0]->text) ){
695 813 $msg = $mess->output[1]->content[0]->text;
696 814 }
697 815 $msg = $Qcld_Parsedown->text($msg);
698 816
699 - $response['message'] = $msg ;
700 - if(($response['message'] == 'DUH.') || ($response['message'] == 'DUH')){
817 + if(($msg == 'DUH.') || ($msg == 'DUH')){
701 818 $response['message'] = 'Sorry, No result found!';
702 819 }else{
703 - $Qcld_Parsedown = new Qcld_Parsedown();
704 - $msg = $mess->output[0]->content[0]->text;
705 - if( $msg == null || empty($msg) ){
706 - $msg = $mess->output[1]->content[0]->text;
820 + if ($is_ai_action || strpos($msg, 'AI_FORM_DATA') !== false) {
821 + $msg = $this->format_and_save_ai_form_response($msg);
822 + $response['message'] = $msg;
823 + } else {
824 + $response['message'] = $msg . $relevant_pagelinks;
707 825 }
708 - $msg = $Qcld_Parsedown->text($msg);
709 - $response['message'] = $msg . $relevant_pagelinks;
710 826 }
711 827 do_action('qcld_openai_user_rate_cal', 1);
712 828 wp_send_json( $response );
713 829 //}
@@ -761,8 +877,9 @@
761 877
762 878 $conversation_continuity = sanitize_text_field(wp_unslash($_POST['conversation_continuity']));
763 879 $qcld_openai_system_content = sanitize_textarea_field(wp_unslash($_POST['qcld_openai_system_content']));
764 880 $qcld_openai_append_content = sanitize_text_field(wp_unslash($_POST['qcld_openai_append_content']));
881 + $email_addresses = isset($_POST['email_addresses']) ? array_map(function($email){ return sanitize_textarea_field(wp_unslash($email)); }, (array)$_POST['email_addresses']) : [];
765 882
766 883 /* Customized by Kadir on 05-12-2023 : To set empty value for API field */
767 884 $disable_ss = isset( $_POST['disable_ss'] ) ? sanitize_text_field(wp_unslash($_POST['disable_ss'])) : '';
768 885
@@ -808,8 +925,10 @@
808 925 if( $ai_enabled == 1 ){
809 926 update_option('qcld_openrouter_enabled',0);
810 927 update_option('qcld_grok_enabled',0);
811 928 update_option('qcld_gemini_enabled',0);
929 + update_option('qcld_claude_enabled',0);
930 + update_option('disable_wp_chatbot_site_search',1);
812 931 }
813 932
814 933 update_option('qcld_openai_relevant_enabled',$is_relevant_enabled);
815 934 update_option('page_suggestion_enabled',$suggestion_enabled);
@@ -912,9 +1031,8 @@
912 1031 update_option('rag_embed_meta', $rag_embed_meta);
913 1032 update_option('rag_embed_meta_keys', $rag_embed_meta_keys);
914 1033 update_option('rag_auto_sync_enabled', $rag_auto_sync_enabled);
915 1034 update_option('rag_embed_cpts', $rag_embed_cpts);
916 -
917 1035 wp_send_json( array('status' => 'success') );
918 1036 }else{
919 1037 if( !get_option('open_ai_api_key') || !get_option('qcld_gemini_api_key') ){
920 1038 wp_send_json_success(array('status' => 'error', 'message' => esc_html__('RAG cannot be enabled without an API key.', 'chatbot')));
@@ -1201,8 +1319,108 @@
1201 1319 }
1202 1320 wp_send_json_error($msg);
1203 1321 }
1204 1322 wp_die();
1323 + }
1324 +
1325 + public function format_and_save_ai_form_response($msg) {
1326 + if (empty($msg) || (strpos($msg, 'AI_FORM_DATA') === false)) {
1327 + return $msg;
1328 + }
1329 +
1330 + $pattern = '/(?:<[^>]+>)*\s*(?:__|<strong>|<b>)?AI_FORM_DATA(?:__|<\/strong>|<\/b>)?[\s\S]*?(?:__|<strong>|<b>)?AI_FORM_DATA_END(?:__|<\/strong>|<\/b>)?\s*(?:<\/[^>]+>)*/i';
1331 +
1332 + if (preg_match($pattern, $msg, $matches)) {
1333 + $block = $matches[0];
1334 + if (preg_match('/\{[\s\S]*\}/', $block, $json_matches)) {
1335 + $json_str = trim($json_matches[0]);
1336 + $data = json_decode($json_str, true);
1337 +
1338 + if ($data && isset($data['form_title'])) {
1339 + $post_title = sanitize_text_field($data['form_title']) . ' - ' . current_time('mysql');
1340 + $post_id = wp_insert_post(array(
1341 + 'post_title' => $post_title,
1342 + 'post_type' => 'wpbot_form_entry',
1343 + 'post_status' => 'publish'
1344 + ));
1345 +
1346 + if ($post_id && isset($data['data']) && is_array($data['data'])) {
1347 + $email_body = "<h2>" . esc_html__('New AI Chat Submission', 'chatbot') . "</h2>";
1348 + $email_body .= "<p><strong>" . esc_html__('Chat', 'chatbot') . ":</strong> " . sanitize_text_field($data['form_title']) . "</p>";
1349 + $email_body .= "<table border='1' cellpadding='10' cellspacing='0' style='border-collapse: collapse; width: 100%; max-width: 600px; font-family: sans-serif;'>";
1350 +
1351 + // Extract user's email for Reply-To (not From, to avoid SMTP rejection)
1352 + $reply_to = '';
1353 +
1354 + foreach ($data['data'] as $key => $value) {
1355 + update_post_meta($post_id, sanitize_text_field($key), sanitize_text_field($value));
1356 + $clean_key = ucwords(str_replace(array('-', '_'), ' ', sanitize_text_field($key)));
1357 + $email_body .= "<tr><td style='background: #f4f4f4; width: 40%;'><strong>" . esc_html($clean_key) . "</strong></td><td>" . esc_html(sanitize_text_field($value)) . "</td></tr>";
1358 +
1359 + // Extract email from value — handles plain, [bracketed], and combined answers
1360 + if (preg_match('/\[?([a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,})\]?/', $value, $email_match)) {
1361 + $candidate = sanitize_email(trim($email_match[1]));
1362 + if (is_email($candidate)) {
1363 + $reply_to = $candidate;
1364 + }
1365 + }
1366 + }
1367 + $email_body .= "</table>";
1368 +
1369 + $email_enabled = true;
1370 + $per_action_emails = '';
1371 + $saved_forms = get_option('wpbot_ai_forms', array());
1372 + $ai_form_title = strtolower( trim( sanitize_text_field( $data['form_title'] ) ) );
1373 + if (is_array($saved_forms)) {
1374 + foreach ($saved_forms as $form) {
1375 + if ( strtolower( trim( $form['title'] ) ) === $ai_form_title ) {
1376 + if (isset($form['email'])) {
1377 + $email_enabled = $form['email'] == 1;
1378 + }
1379 + $per_action_emails = isset($form['email_addresses']) ? trim($form['email_addresses']) : '';
1380 + //error_log('[WPBot AI] Matched form: "' . $form['title'] . '" | email_addresses stored: "' . $per_action_emails . '"');
1381 + break;
1382 + }
1383 + }
1384 + }
1385 +
1386 + if ($email_enabled) {
1387 + // Per-action email_addresses → fallback to qlcd_wp_chatbot_admin_email → fallback to WP admin_email
1388 + if (!empty($per_action_emails)) {
1389 + $to = array_map('trim', explode(',', $per_action_emails));
1390 + } else {
1391 + $default = get_option('qlcd_wp_chatbot_admin_email', '');
1392 + $to = !empty($default) ? array_map('trim', explode(',', $default)) : get_option('admin_email');
1393 + }
1394 + $subject = sanitize_text_field($data['form_title']) . " - " . esc_html__('New AI Chat Submission', 'chatbot');
1395 + $headers = array('Content-Type: text/html; charset=UTF-8');
1396 + if (!empty($reply_to)) {
1397 + $headers[] = 'Reply-To: ' . $reply_to;
1398 + }
1399 + wp_mail($to, $subject, $email_body, $headers);
1400 + }
1401 + }
1402 +
1403 + $summary_html = '<div class="ai-form-summary-card" style="background: #f4f6f9; border-left: 4px solid #0073aa; padding: 12px 14px; margin: 10px 0; border-radius: 4px; font-size: 13px; line-height: 1.5; color: #333;">';
1404 + if (!empty($data['form_title'])) {
1405 + $summary_html .= '<div style="font-weight: 600; color: #0073aa; margin-bottom: 8px; text-transform: capitalize; font-size: 14px;">' . esc_html($data['form_title']) . '</div>';
1406 + }
1407 + if (!empty($data['data']) && is_array($data['data'])) {
1408 + $summary_html .= '<table style="width: 100%; border-collapse: collapse; margin-top: 4px;">';
1409 + foreach ($data['data'] as $k => $v) {
1410 + $clean_k = ucwords(str_replace(array('-', '_'), ' ', sanitize_text_field($k)));
1411 + $summary_html .= '<tr><td style="padding: 3px 6px 3px 0; color: #555; font-weight: 600; width: 42%; vertical-align: top;">' . esc_html($clean_k) . ':</td><td style="padding: 3px 0; color: #222; vertical-align: top;">' . esc_html($v) . '</td></tr>';
1412 + }
1413 + $summary_html .= '</table>';
1414 + }
1415 + $summary_html .= '</div>';
1416 +
1417 + $msg = preg_replace($pattern, $summary_html, $msg);
1418 + }
1419 + }
1420 + }
1421 +
1422 + return $msg;
1205 1423 }
1206 1424 }
1207 1425
1208 1426 /**