PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.0.1
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.0.1
3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.9 3.4.8 3.4.7 0.2.1 1.6.91 0.2.2 1.6.92 0.2.3 1.6.93 0.2.4 1.6.94 0.2.5 1.6.95 0.2.6 1.6.96 0.2.7 1.6.97 0.2.8 1.6.98 0.2.9 1.6.99 0.3.0 1.7.0 0.3.1 1.7.1 0.3.2 1.7.2 0.3.3 1.7.3 0.3.4 1.7.4 0.3.5 1.7.5 0.3.6 1.7.6 0.4.0 1.7.7 0.4.1 1.7.8 0.4.2 1.7.9 0.4.3 1.8.0 0.4.4 1.8.1 0.4.5 1.8.2 0.4.6 1.8.3 0.4.7 1.8.4 0.4.8 1.8.5 0.4.9 1.8.6 0.5.0 1.8.7 0.5.1 1.8.8 0.5.2 1.8.9 0.5.3 1.9.0 0.5.4 1.9.1 0.5.5 1.9.2 0.5.6 1.9.3 0.5.7 1.9.4 0.5.8 1.9.5 0.5.9 1.9.6 0.6.0 1.9.7 0.6.1 1.9.8 0.6.2 1.9.81 0.6.3 1.9.82 0.6.4 1.9.83 0.6.5 1.9.84 0.6.6 1.9.85 0.6.7 1.9.86 0.6.8 1.9.87 0.6.9 1.9.88 0.7.0 1.9.89 0.7.1 1.9.90 0.7.2 1.9.91 0.7.3 1.9.92 0.7.4 1.9.93 0.7.5 1.9.94 0.7.6 1.9.95 0.7.7 1.9.96 0.7.8 1.9.97 0.7.9 1.9.98 0.8.0 1.9.99 0.8.1 2.0.0 0.8.2 2.0.1 0.8.3 2.0.2 0.8.4 2.0.3 0.8.5 2.0.4 0.8.6 2.0.5 0.8.7 2.0.6 0.8.8 2.0.7 0.8.9 2.0.8 0.9.0 2.0.9 0.9.2 2.1.0 0.9.3 2.1.1 0.9.4 2.1.2 0.9.5 2.1.3 0.9.6 2.1.4 0.9.7 2.1.5 0.9.8 2.1.6 0.9.81 2.1.7 0.9.82 2.1.8 0.9.83 2.1.9 0.9.84 2.2.0 0.9.85 2.2.1 0.9.86 2.2.2 0.9.87 2.2.3 0.9.88 2.2.4 0.9.89 2.2.5 0.9.9 2.2.51 0.9.91 2.2.52 0.9.92 2.2.53 0.9.93 2.2.54 0.9.94 2.2.56 0.9.95 2.2.57 0.9.96 2.2.6 0.9.97 2.2.60 0.9.98 2.2.61 0.9.99 2.2.62 1.0.0 2.2.63 1.0.01 2.2.70 1.0.1 2.2.80 1.0.2 2.2.81 1.0.3 2.2.90 1.0.4 2.2.91 1.0.5 2.2.92 1.0.6 2.2.93 1.0.7 2.2.94 1.0.8 2.2.95 1.0.9 2.3.0 1.1.0 2.3.1 1.1.1 2.3.2 1.1.2 2.3.3 1.1.3 2.3.4 1.1.4 2.3.5 1.1.5 2.3.6 1.1.6 2.3.7 1.1.7 2.3.8 1.1.8 2.3.9 1.1.9 2.4.0 1.2.0 2.4.1 1.2.1 2.4.2 1.2.2 2.4.3 1.2.21 2.4.4 1.2.3 2.4.5 1.2.30 2.4.6 1.3.0 2.4.7 1.3.1 2.4.8 1.3.2 2.4.9 1.3.3 2.5.0 1.3.31 2.5.1 1.3.32 2.5.2 1.3.33 2.5.3 1.3.34 2.5.4 1.3.35 2.5.5 1.3.36 2.5.6 1.3.37 2.5.7 1.3.38 2.5.8 1.3.39 2.5.9 1.3.40 2.6.0 1.3.41 2.6.1 1.3.42 2.6.2 1.3.43 2.6.3 1.3.44 2.6.5 1.3.45 2.6.6 1.3.46 2.6.7 1.3.47 2.6.8 1.3.48 2.6.9 1.3.49 2.7.0 1.3.50 2.7.1 1.3.51 2.7.2 1.3.52 2.7.3 1.3.53 2.7.4 1.3.54 2.7.5 1.3.56 2.7.6 1.3.57 2.7.7 1.3.58 2.7.8 1.3.59 2.7.9 1.3.60 2.8.0 1.3.61 2.8.1 1.3.62 2.8.2 1.3.63 2.8.3 1.3.64 2.8.4 1.3.65 2.8.5 1.3.66 2.8.6 1.3.67 2.8.7 1.3.68 2.8.8 1.3.69 2.8.9 1.3.70 2.9.0 1.3.71 2.9.1 1.3.72 2.9.2 1.3.73 2.9.3 1.3.74 2.9.4 1.3.75 2.9.5 1.3.76 2.9.6 1.3.77 2.9.7 1.3.78 2.9.8 1.3.79 2.9.9 1.3.80 3.0.0 1.3.81 3.0.1 1.3.82 3.0.2 1.3.83 3.0.3 1.3.84 3.0.4 1.3.85 3.0.5 1.3.86 3.0.6 1.3.87 3.0.7 1.3.88 3.0.8 1.3.89 3.0.9 1.3.90 3.1.0 1.3.91 3.1.1 1.3.92 3.1.2 1.3.93 3.1.3 1.3.94 3.1.4 1.3.95 3.1.5 1.3.96 3.1.6 1.3.97 3.1.7 1.3.98 3.1.8 1.3.99 3.1.9 1.4.0 3.2.0 1.4.1 3.2.1 1.4.2 3.2.2 1.4.3 3.2.3 1.4.4 3.2.4 1.4.5 3.2.5 1.4.6 3.2.6 1.4.7 3.2.7 1.4.8 3.2.8 1.4.9 3.2.9 1.5.0 3.3.0 1.5.1 3.3.1 1.5.2 3.3.2 1.5.3 3.3.3 1.5.4 3.3.4 1.5.5 3.3.5 1.5.6 3.3.6 1.5.7 3.3.7 1.5.8 3.3.8 1.5.9 3.3.9 1.6.0 3.4.0 1.6.1 3.4.1 1.6.2 3.4.2 1.6.3 3.4.3 1.6.5 3.4.4 1.6.51 3.4.5 1.6.52 3.4.6 1.6.53 1.6.54 1.6.55 1.6.56 1.6.57 1.6.58 1.6.59 1.6.60 1.6.61 1.6.62 1.6.63 1.6.64 1.6.65 1.6.66 1.6.67 1.6.68 trunk 1.6.69 0.0.1 1.6.70 0.0.2 1.6.71 0.0.3 1.6.72 0.0.4 1.6.73 0.0.5 1.6.74 0.0.6 1.6.75 0.0.7 1.6.76 0.0.8 1.6.77 0.0.9 1.6.78 0.1.0 1.6.79 0.1.1 1.6.81 0.1.2 1.6.82 0.1.3 1.6.83 0.1.4 1.6.84 0.1.5 1.6.85 0.1.6 1.6.86 0.1.7 1.6.87 0.1.8 1.6.88 0.1.9 1.6.89 0.2.0 1.6.90
ai-engine / classes / modules / chatbot.php
ai-engine / classes / modules Last commit date
advisor.php 11 months ago chatbot.php 10 months ago discussions.php 11 months ago files.php 11 months ago gdpr.php 11 months ago search.php 11 months ago security.php 11 months ago tasks.php 11 months ago wand.php 11 months ago
chatbot.php
1182 lines
1 <?php
2
3 // Params for the chatbot (front and server)
4 define( 'MWAI_CHATBOT_FRONT_PARAMS', [ 'id', 'customId', 'aiName', 'userName', 'guestName', 'aiAvatar', 'userAvatar', 'guestAvatar', 'aiAvatarUrl', 'userAvatarUrl', 'guestAvatarUrl', 'textSend', 'textClear', 'imageUpload', 'fileUpload', 'multiUpload', 'fileSearch', 'mode', 'textInputPlaceholder', 'textInputMaxLength', 'textCompliance', 'startSentence', 'localMemory', 'themeId', 'window', 'icon', 'iconText', 'iconTextDelay', 'iconAlt', 'iconPosition', 'centerOpen', 'width', 'openDelay', 'iconBubble', 'windowAnimation', 'fullscreen', 'copyButton', 'headerSubtitle', 'containerType', 'headerType', 'messagesType', 'inputType', 'footerType' ] );
5
6 define( 'MWAI_CHATBOT_SERVER_PARAMS', [ 'id', 'envId', 'scope', 'mode', 'contentAware', 'context', 'startSentence', 'embeddingsEnvId', 'embeddingsIndex', 'embeddingsNamespace', 'assistantId', 'instructions', 'resolution', 'voice', 'model', 'temperature', 'maxTokens', 'contextMaxLength', 'maxResults', 'apiKey', 'functions', 'mcpServers', 'tools', 'historyStrategy', 'previousResponseId', 'parentBotId', 'crossSite' ] );
7
8 // Params for the discussions (front and server)
9 define( 'MWAI_DISCUSSIONS_FRONT_PARAMS', [ 'themeId', 'textNewChat' ] );
10 define( 'MWAI_DISCUSSIONS_SERVER_PARAMS', [ 'customId' ] );
11
12 class Meow_MWAI_Modules_Chatbot {
13 private $core = null;
14 private $namespace = 'mwai-ui/v1';
15 private $siteWideChatId = null;
16
17 public function __construct() {
18 global $mwai_core;
19 $this->core = $mwai_core;
20 $this->siteWideChatId = $this->core->get_option( 'botId' );
21
22 add_shortcode( 'mwai_chatbot', [ $this, 'chat_shortcode' ] );
23 add_action( 'rest_api_init', [ $this, 'rest_api_init' ] );
24 add_action( 'wp_enqueue_scripts', [ $this, 'register_scripts' ] );
25 add_action( 'admin_enqueue_scripts', [ $this, 'register_scripts' ] );
26 if ( $this->core->get_option( 'chatbot_discussions' ) ) {
27 add_shortcode( 'mwai_discussions', [ $this, 'chatbot_discussions' ] );
28 }
29 }
30
31 public function register_scripts() {
32 // Load JS
33 $physical_file = trailingslashit( MWAI_PATH ) . 'app/chatbot.js';
34 $cache_buster = file_exists( $physical_file ) ? filemtime( $physical_file ) : MWAI_VERSION;
35 wp_register_script( 'mwai_chatbot', trailingslashit( MWAI_URL )
36 . 'app/chatbot.js', [ 'wp-element' ], $cache_buster, false );
37
38 // Actual loading of the scripts
39 $hasSiteWideChat = $this->siteWideChatId && $this->siteWideChatId !== 'none';
40 if ( is_admin() || $hasSiteWideChat ) {
41 $themeId = null;
42 if ( $hasSiteWideChat ) {
43 $bot = $this->core->get_chatbot( $this->siteWideChatId );
44 if ( $bot && isset( $bot['themeId'] ) ) {
45 $themeId = $bot['themeId'];
46 }
47 }
48 $this->enqueue_scripts( is_admin() ? null : $themeId );
49 if ( $hasSiteWideChat ) {
50 // Chatbot Injection
51 add_action( 'wp_footer', [ $this, 'inject_chat' ] );
52 }
53 }
54 }
55
56 public function enqueue_scripts( $themeId = null ) {
57 wp_enqueue_script( 'mwai_chatbot' );
58 if ( $this->core->get_option( 'syntax_highlight' ) ) {
59 wp_enqueue_script( 'mwai_highlight' );
60 }
61 if ( $themeId ) {
62 $this->core->enqueue_theme( $themeId );
63 }
64 else {
65 $this->core->enqueue_themes();
66 }
67 }
68
69 /**
70 * Helper method to create REST responses with automatic token refresh
71 *
72 * @param array $data The response data
73 * @param int $status HTTP status code
74 * @return WP_REST_Response
75 */
76 protected function create_rest_response( $data, $status = 200 ) {
77 // Always check if we need to provide a new nonce
78 $current_nonce = $this->core->get_nonce( true );
79 $request_nonce = isset( $_SERVER['HTTP_X_WP_NONCE'] ) ? $_SERVER['HTTP_X_WP_NONCE'] : null;
80
81 // Check if nonce is approaching expiration (WordPress nonces last 12-24 hours)
82 // We'll refresh if the nonce is older than 10 hours to be safe
83 $should_refresh = false;
84
85 if ( $request_nonce ) {
86 // Try to determine the age of the nonce
87 // WordPress uses a tick system where each tick is 12 hours
88 // If we're in the second half of the nonce's life, refresh it
89 $time = time();
90 $nonce_tick = wp_nonce_tick();
91
92 // Verify if the nonce is still valid but getting old
93 $verify = wp_verify_nonce( $request_nonce, 'wp_rest' );
94 if ( $verify === 2 ) {
95 // Nonce is valid but was generated 12-24 hours ago
96 $should_refresh = true;
97 // Log will be written when token is included in response
98 }
99 }
100
101 // If the nonce has changed or should be refreshed, include the new one
102 if ( $should_refresh || ( $request_nonce && $current_nonce !== $request_nonce ) ) {
103 $data['new_token'] = $current_nonce;
104
105 // Log if server debug mode is enabled
106 if ( $this->core->get_option( 'server_debug_mode' ) ) {
107 error_log( '[AI Engine] Token refresh: Nonce refreshed (12-24 hours old)' );
108 }
109 }
110
111 return new WP_REST_Response( $data, $status );
112 }
113
114 public function rest_api_init() {
115 register_rest_route( $this->namespace, '/chats/submit', [
116 'methods' => 'POST',
117 'callback' => [ $this, 'rest_chat' ],
118 'permission_callback' => [ $this->core, 'check_rest_nonce' ]
119 ] );
120 }
121
122 public function basics_security_check( $botId, $customId, $newMessage, $newFileId ) {
123 if ( !$botId && !$customId ) {
124 Meow_MWAI_Logging::warn( 'The query was rejected - no botId nor id was specified.' );
125 return false;
126 }
127
128 if ( $newFileId ) {
129 return true;
130 }
131
132 $length = strlen( empty( $newMessage ) ? '' : $newMessage );
133 if ( $length < 1 ) {
134 Meow_MWAI_Logging::warn( 'The query was rejected - message was too short.' );
135 return false;
136 }
137 return true;
138 }
139
140 public function build_final_res( $botId, $newMessage, $newFileId, $params, $reply, $images, $actions, $usage, $responseId = null ) {
141 $filterParams = [
142 'step' => 'reply',
143 'botId' => $botId,
144 'reply' => $reply,
145 'images' => $images,
146 'newMessage' => $newMessage,
147 'newFileId' => $newFileId,
148 'params' => $params,
149 'usage' => $usage,
150 'messages' => $params['messages'] ?? [],
151 'isNewConversation' => empty( $params['messages'] ) || count( $params['messages'] ) <= 1,
152 ];
153 $actions = apply_filters( 'mwai_chatbot_actions', $actions, $filterParams );
154 $blocks = apply_filters( 'mwai_chatbot_blocks', [], $filterParams );
155 $shortcuts = apply_filters( 'mwai_chatbot_shortcuts', [], $filterParams );
156 $actions = $this->sanitize_actions( $actions );
157 $blocks = $this->sanitize_blocks( $blocks );
158 $shortcuts = $this->sanitize_shortcuts( $shortcuts );
159 $result = [
160 'success' => true,
161 'reply' => $reply,
162 'images' => $images,
163 'actions' => $actions,
164 'shortcuts' => $shortcuts,
165 'blocks' => $blocks,
166 'usage' => $usage
167 ];
168
169 // Add response ID if available
170 if ( !empty( $responseId ) ) {
171 $result['responseId'] = $responseId;
172 }
173
174 // Check if token needs refresh
175 $current_nonce = $this->core->get_nonce( true );
176 $request_nonce = isset( $_SERVER['HTTP_X_WP_NONCE'] ) ? $_SERVER['HTTP_X_WP_NONCE'] : null;
177
178 $should_refresh = false;
179 if ( $request_nonce ) {
180 $verify = wp_verify_nonce( $request_nonce, 'wp_rest' );
181 if ( $verify === 2 ) {
182 // Nonce is valid but was generated 12-24 hours ago
183 $should_refresh = true;
184 }
185 }
186
187 if ( $should_refresh || ( $request_nonce && $current_nonce !== $request_nonce ) ) {
188 $result['new_token'] = $current_nonce;
189 }
190
191 return $result;
192 }
193
194 public function rest_chat( $request ) {
195 $params = $request->get_json_params();
196 $botId = $params['botId'] ?? null;
197 $customId = $params['customId'] ?? null;
198 $stream = $params['stream'] ?? false;
199 $newMessage = trim( $params['newMessage'] ?? '' );
200 $newFileId = $params['newFileId'] ?? null;
201 $newFileIds = $params['newFileIds'] ?? [];
202 $crossSite = $params['crossSite'] ?? false;
203
204 if ( !$this->basics_security_check( $botId, $customId, $newMessage, $newFileId ) ) {
205 return $this->create_rest_response( [
206 'success' => false,
207 'message' => apply_filters( 'mwai_ai_exception', 'Sorry, your query has been rejected.' )
208 ], 403 );
209 }
210
211 try {
212 $data = $this->chat_submit( $botId, $newMessage, $newFileId, $params, $stream, $newFileIds );
213 $final_res = $this->build_final_res(
214 $botId,
215 $newMessage,
216 $newFileId,
217 $params,
218 $data['reply'],
219 $data['images'],
220 $data['actions'],
221 $data['usage'],
222 $data['responseId'] ?? null
223 );
224 return $this->create_rest_response( $final_res, 200 );
225 }
226 catch ( Exception $e ) {
227 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
228 return $this->create_rest_response( [
229 'success' => false,
230 'message' => $message
231 ], 500 );
232 }
233 }
234
235 private function sanitize_items( $items, $supported_types, $type_name ) {
236 if ( empty( $items ) ) {
237 return $items;
238 }
239 $sanitized_items = [];
240 foreach ( $items as $item ) {
241 if ( isset( $supported_types[$item['type']] ) ) {
242 $is_valid = true;
243 foreach ( $supported_types[$item['type']] as $param ) {
244 if ( !isset( $item['data'][$param] ) ) {
245 $is_valid = false;
246 Meow_MWAI_Logging::warn( "The query was rejected - missing required parameter '{$param}' for {$type_name} type: {$item['type']}." );
247 break;
248 }
249 }
250 if ( $is_valid ) {
251 $sanitized_items[] = $item;
252 }
253 }
254 else {
255 Meow_MWAI_Logging::warn( "The query was rejected - unsupported {$type_name} type: {$item['type']}." );
256 }
257 }
258 return $sanitized_items;
259 }
260
261 public function sanitize_actions( $actions ) {
262 $supported_action_types = [
263 'function' => ['name', 'args'],
264 'javascript' => ['snippet'],
265 ];
266 return $this->sanitize_items( $actions, $supported_action_types, 'action' );
267 }
268
269 public function sanitize_blocks( $blocks ) {
270 $supported_block_types = [
271 'content' => ['html'],
272 ];
273 return $this->sanitize_items( $blocks, $supported_block_types, 'block' );
274 }
275
276 public function sanitize_shortcuts( $shortcuts ) {
277 $supported_shortcut_types = [
278 'action' => ['label', 'message', 'action'],
279 'callback' => ['label', 'onClick'],
280 ];
281 return $this->sanitize_items( $shortcuts, $supported_shortcut_types, 'shortcut' );
282 }
283
284 #region Messages Integrity Check
285
286 public function messages_integrity_diff( $messages1, $messages2 ) {
287 // Ensure both parameters are arrays
288 if ( !is_array( $messages1 ) ) {
289 $messages1 = [];
290 }
291 if ( !is_array( $messages2 ) ) {
292 $messages2 = [];
293 }
294
295 // Collect messages with role not 'user' from messages1
296 $messagesList1 = [];
297 foreach ( $messages1 as $msg ) {
298 $role = isset( $msg->role ) ? $msg->role : ( isset( $msg['role'] ) ? $msg['role'] : null );
299 $content = isset( $msg->content ) ? $msg->content : ( isset( $msg['content'] ) ? $msg['content'] : null );
300 if ( $role && $role != 'user' ) {
301 $messageData = [ 'role' => $role, 'content' => $content ];
302 $messagesList1[] = $messageData;
303 }
304 }
305
306 // Collect messages with role not 'user' from messages2
307 $messagesList2 = [];
308 foreach ( $messages2 as $msg ) {
309 $role = isset( $msg->role ) ? $msg->role : ( isset( $msg['role'] ) ? $msg['role'] : null );
310 $content = isset( $msg->content ) ? $msg->content : ( isset( $msg['content'] ) ? $msg['content'] : null );
311 if ( $role && $role != 'user' ) {
312 $messageData = [ 'role' => $role, 'content' => $content ];
313 $messagesList2[] = $messageData;
314 }
315 }
316
317 // Count occurrences of each message in messagesList1
318 $counts1 = [];
319 foreach ( $messagesList1 as $msg ) {
320 $key = serialize( $msg );
321 if ( isset( $counts1[ $key ] ) ) {
322 $counts1[ $key ]++;
323 }
324 else {
325 $counts1[ $key ] = 1;
326 }
327 }
328
329 // Count occurrences of each message in messagesList2
330 $counts2 = [];
331 foreach ( $messagesList2 as $msg ) {
332 $key = serialize( $msg );
333 if ( isset( $counts2[ $key ] ) ) {
334 $counts2[ $key ]++;
335 }
336 else {
337 $counts2[ $key ] = 1;
338 }
339 }
340
341 // Compare counts to find unmatched messages
342 $all_keys = array_unique( array_merge( array_keys( $counts1 ), array_keys( $counts2 ) ) );
343
344 $diffs = [];
345 foreach ( $all_keys as $key ) {
346 $count1 = isset( $counts1[ $key ] ) ? $counts1[ $key ] : 0;
347 $count2 = isset( $counts2[ $key ] ) ? $counts2[ $key ] : 0;
348 if ( $count1 != $count2 ) {
349 $message = unserialize( $key );
350 $diffs[] = [
351 'message' => $message,
352 'count_in_messages1' => $count1,
353 'count_in_messages2' => $count2
354 ];
355 }
356 }
357
358 return $diffs;
359 }
360
361 private function calculate_messages_checksum( $messages ) {
362 $messages_to_hash = [];
363 foreach ( $messages as $msg ) {
364 $role = is_array( $msg ) ? ( $msg['role'] ?? '' ) : ( is_object( $msg ) ? ( $msg->role ?? '' ) : '' );
365 $content = is_array( $msg ) ? ( $msg['content'] ?? '' ) : ( is_object( $msg ) ? ( $msg->content ?? '' ) : '' );
366 if ( in_array( $role, ['assistant', 'system'] ) ) {
367 $messages_to_hash[] = [ 'role' => $role, 'content' => $content ];
368 }
369 }
370 return md5( json_encode( $messages_to_hash ) );
371 }
372
373 #endregion
374
375 public function chat_submit( $botId, $newMessage, $newFileId = null, $params = [], $stream = false, $newFileIds = [] ) {
376 try {
377 $chatbot = null;
378 $customId = $params['customId'] ?? null;
379
380 // Custom Chatbot
381 if ( $customId ) {
382 $chatbot = get_transient( 'mwai_custom_chatbot_' . $customId );
383 }
384 // Registered Chatbot
385 if ( !$chatbot && $botId ) {
386 $chatbot = $this->core->get_chatbot( $botId );
387 }
388
389 if ( !$chatbot ) {
390 Meow_MWAI_Logging::warn( 'The query was rejected - no chatbot was found.' );
391 throw new Exception( 'Sorry, your query has been rejected.' );
392 }
393
394 $textInputMaxLength = $chatbot['textInputMaxLength'] ?? null;
395 if ( $textInputMaxLength && $this->core->safe_strlen( $newMessage ) > (int) $textInputMaxLength ) {
396 Meow_MWAI_Logging::warn( 'The query was rejected - message was too long.' );
397 throw new Exception( 'Sorry, your query has been rejected.' );
398 }
399
400 // We need to check the integrity of the messages sent by the client.
401 // This is important to ensure that the messages are not tampered with.
402
403 // Messages Integrity Check with Checksums
404 $chatId = $params['chatId'] ?? 'default';
405 $checksum_key = 'mwai_chatbot_checksum_' . $chatId;
406 $stored_checksum = get_transient( $checksum_key );
407 $client_messages = $params['messages'] ?? [];
408 $client_checksum = $this->calculate_messages_checksum( $client_messages );
409 if ( $stored_checksum && $stored_checksum !== $client_checksum ) {
410 Meow_MWAI_Logging::warn( 'Integrity Check: Messages integrity check failed. Assistant or system messages sent by the client do not match stored messages. Please enable the Discussions module for better logs.' );
411 }
412
413 // Messages Integrity Check with Discussions
414 if ( $this->core->get_option( 'chatbot_discussions' ) && $this->core->discussions && isset( $params['chatId'] ) ) {
415 $discussion = $this->core->discussions->get_discussion( $botId ? $botId : $customId, $params['chatId'] );
416 if ( $discussion ) {
417 $messages = $discussion['messages'];
418 $clientMessages = isset( $params['messages'] ) ? $params['messages'] : [];
419 $diffs = $this->messages_integrity_diff( $messages, $clientMessages );
420 if ( count( $diffs ) > 0 ) {
421 Meow_MWAI_Logging::warn( "Integrity Check: It seems the messages in the discussion #{$discussion['id']} do not match the ones sent by the client." );
422 }
423
424 // Maintain conversation state for Responses API by loading previousResponseId
425 // This enables stateful conversations where only new messages are sent
426 if ( empty( $params['previousResponseId'] ) && !empty( $discussion['extra'] ) ) {
427 $extra = json_decode( $discussion['extra'], true );
428 if ( !empty( $extra['responseId'] ) ) {
429 // Response IDs expire after 30 days per OpenAI's policy
430 // Check if the stored response is still valid
431 $responseDate = !empty( $extra['responseDate'] ) ? strtotime( $extra['responseDate'] ) : 0;
432 $thirtyDaysAgo = time() - ( 30 * 24 * 60 * 60 );
433
434 if ( $responseDate > $thirtyDaysAgo ) {
435 // Use the stored response ID for stateful conversation
436 $params['previousResponseId'] = $extra['responseId'];
437 }
438 }
439 }
440 }
441 else {
442 // No discussion yet? We still need to check the startSentence.
443 $startSentence = isset( $chatbot['startSentence'] ) ? $chatbot['startSentence'] : null;
444 $messages = [];
445 if ( !empty( $startSentence ) ) {
446 $messages[] = [ 'role' => 'assistant', 'content' => $startSentence ];
447 }
448 $clientMessages = isset( $params['messages'] ) ? $params['messages'] : [];
449 $diffs = $this->messages_integrity_diff( $messages, $clientMessages );
450 if ( count( $diffs ) > 0 ) {
451 Meow_MWAI_Logging::warn( 'Integrity Check: It seems the messages in the discussion do not match the ones sent by the client: ' . json_encode( $diffs ) );
452 }
453 }
454 }
455
456 // Create QueryText
457 $context = null;
458 $streamCallback = null;
459 $mode = $chatbot['mode'] ?? 'chat';
460
461 if ( $mode === 'images' ) {
462 // Check for uploaded files
463 $fileForImage = null;
464 if ( !empty( $newFileIds ) && is_array( $newFileIds ) ) {
465 $fileForImage = $newFileIds[0];
466 } elseif ( !empty( $newFileId ) ) {
467 $fileForImage = $newFileId;
468 }
469
470 // If there's an uploaded file, use EditImage query instead
471 if ( !empty( $fileForImage ) ) {
472 $query = new Meow_MWAI_Query_EditImage( $newMessage );
473
474 // Handle the uploaded image
475 $url = $this->core->files->get_url( $fileForImage );
476 $mimeType = $this->core->files->get_mime_type( $fileForImage );
477 $isIMG = in_array( $mimeType, [ 'image/jpeg', 'image/png', 'image/gif', 'image/webp' ] );
478
479 if ( $isIMG ) {
480 $query->set_file( Meow_MWAI_Query_DroppedFile::from_url( $url, 'vision', $mimeType ) );
481 $fileId = $this->core->files->get_id_from_refId( $fileForImage );
482 $this->core->files->update_purpose( $fileId, 'vision' );
483 }
484 }
485 else {
486 $query = new Meow_MWAI_Query_Image( $newMessage );
487 }
488
489 // Handle Params
490 $newParams = [];
491 foreach ( $chatbot as $key => $value ) {
492 $newParams[$key] = $value;
493 }
494 if ( is_array( $params ) ) {
495 foreach ( $params as $key => $value ) {
496 $newParams[$key] = $value;
497 }
498 }
499
500 // Map 'environment' field to 'envId' for compatibility
501 if ( isset( $newParams['environment'] ) && !isset( $newParams['envId'] ) ) {
502 $newParams['envId'] = $newParams['environment'];
503 }
504
505 $params = apply_filters( 'mwai_chatbot_params', $newParams );
506 $params['scope'] = empty( $params['scope'] ) ? 'chatbot' : $params['scope'];
507
508
509 // Debug log for embeddings
510 if ( !empty( $params['embeddingsEnvId'] ) ) {
511 Meow_MWAI_Logging::log( 'Chatbot: Setting embeddingsEnvId on query: ' . $params['embeddingsEnvId'] );
512 } else {
513 // Log all params to debug
514 $paramKeys = array_keys( $params );
515 Meow_MWAI_Logging::log( 'Chatbot: No embeddingsEnvId found. Available params: ' . implode( ', ', $paramKeys ) );
516 }
517
518 $query->inject_params( $params );
519 }
520 else {
521 $query = $mode === 'assistant' ? new Meow_MWAI_Query_Assistant( $newMessage ) :
522 new Meow_MWAI_Query_Text( $newMessage, 1024 );
523
524 // Handle Params
525 $newParams = [];
526 foreach ( $chatbot as $key => $value ) {
527 $newParams[$key] = $value;
528 }
529 if ( is_array( $params ) ) {
530 foreach ( $params as $key => $value ) {
531 $newParams[$key] = $value;
532 }
533 }
534
535 // Map 'environment' field to 'envId' for compatibility
536 if ( isset( $newParams['environment'] ) && !isset( $newParams['envId'] ) ) {
537 $newParams['envId'] = $newParams['environment'];
538 }
539
540 $params = apply_filters( 'mwai_chatbot_params', $newParams );
541 $params['scope'] = empty( $params['scope'] ) ? 'chatbot' : $params['scope'];
542
543
544 // Debug log for embeddings
545 if ( !empty( $params['embeddingsEnvId'] ) ) {
546 Meow_MWAI_Logging::log( 'Chatbot: Setting embeddingsEnvId on query: ' . $params['embeddingsEnvId'] );
547 } else {
548 // Log all params to debug
549 $paramKeys = array_keys( $params );
550 Meow_MWAI_Logging::log( 'Chatbot: No embeddingsEnvId found. Available params: ' . implode( ', ', $paramKeys ) );
551 }
552
553 $query->inject_params( $params );
554
555 $storeId = null;
556 if ( $mode === 'assistant' ) {
557 $chatId = $params['chatId'] ?? null;
558 if ( !empty( $chatId ) && $this->core->discussions ) {
559 $discussion = $this->core->discussions->get_discussion( $query->botId, $chatId );
560 if ( isset( $discussion['storeId'] ) ) {
561 $storeId = $discussion['storeId'];
562 $query->setStoreId( $storeId );
563 }
564 }
565 }
566
567 // Support for Multiple Uploaded Files
568 $filesToProcess = [];
569 if ( !empty( $newFileIds ) && is_array( $newFileIds ) ) {
570 $filesToProcess = $newFileIds;
571 } elseif ( !empty( $newFileId ) ) {
572 $filesToProcess[] = $newFileId;
573 }
574
575 // Support for Uploaded Image/Files
576 if ( !empty( $filesToProcess ) ) {
577 // For now, we only process the first file to maintain backward compatibility
578 // TODO: In the future, we could support multiple files in the query
579 $fileToProcess = $filesToProcess[0];
580
581 // Get extension and mime type
582 $isImage = $this->core->files->is_image( $fileToProcess );
583
584 if ( $mode === 'assistant' && !$isImage ) {
585 $url = $this->core->files->get_path( $fileToProcess );
586 $data = $this->core->files->get_data( $fileToProcess );
587 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $query->envId );
588 $filename = basename( $url );
589
590 // Upload the file
591 $file = $openai->upload_file( $filename, $data, 'assistants' );
592
593 // Create a store
594 if ( empty( $storeId ) ) {
595 $chatbotName = 'mwai_' . strtolower( !empty( $chatbot['name'] ) ? $chatbot['name'] : 'default' );
596 if ( !empty( $query->chatId ) ) {
597 $chatbotName .= '_' . $query->chatId;
598 }
599 $metadata = [];
600 if ( !empty( $chatbot['assistantId'] ) ) {
601 $metadata['assistantId'] = $chatbot['assistantId'];
602 }
603 if ( !empty( $query->chatId ) ) {
604 $metadata['chatId'] = $query->chatId;
605 }
606 $expiry = $this->core->get_option( 'image_expires' );
607 $storeId = $openai->create_vector_store( $chatbotName, $expiry, $metadata );
608 $query->setStoreId( $storeId );
609 }
610
611 // Add the file to the store - wait a moment for store to be ready
612 sleep( 1 );
613 $storeFileId = $openai->add_vector_store_file( $storeId, $file['id'] );
614
615 if ( empty( $storeFileId ) ) {
616 throw new Exception( 'Failed to add file to vector store.' );
617 }
618
619 // Update the local file with the OpenAI RefId, StoreId and StoreFileId
620 $openAiRefId = $file['id'];
621 $internalFileId = $this->core->files->get_id_from_refId( $fileToProcess );
622 $this->core->files->update_refId( $internalFileId, $openAiRefId );
623 $this->core->files->update_envId( $internalFileId, $query->envId );
624 $this->core->files->update_purpose( $internalFileId, 'assistant-in' );
625 $this->core->files->add_metadata( $internalFileId, 'assistant_storeId', $storeId );
626 $this->core->files->add_metadata( $internalFileId, 'assistant_storeFileId', $storeFileId );
627 $fileToProcess = $openAiRefId;
628 $scope = $params['fileSearch'];
629 if ( $scope === 'discussion' || $scope === 'user' || $scope === 'assistant' ) {
630 $id = $this->core->files->get_id_from_refId( $fileToProcess );
631 $this->core->files->add_metadata( $id, 'assistant_scope', $scope );
632 }
633 }
634 else {
635 $url = $this->core->files->get_url( $fileToProcess );
636 $mimeType = $this->core->files->get_mime_type( $fileToProcess );
637 $isIMG = in_array( $mimeType, [ 'image/jpeg', 'image/png', 'image/gif', 'image/webp' ] );
638 $purposeType = $isIMG ? 'vision' : 'files';
639 $query->set_file( Meow_MWAI_Query_DroppedFile::from_url( $url, $purposeType, $mimeType ) );
640 $fileId = $this->core->files->get_id_from_refId( $fileToProcess );
641 $this->core->files->update_envId( $fileId, $query->envId );
642 $this->core->files->update_purpose( $fileId, $purposeType );
643 $this->core->files->add_metadata( $fileId, 'query_envId', $query->envId );
644 $this->core->files->add_metadata( $fileId, 'query_session', $query->session );
645 }
646 }
647
648 // Takeover
649 $takeoverAnswer = apply_filters( 'mwai_chatbot_takeover', null, $query, $params );
650 if ( !empty( $takeoverAnswer ) ) {
651 $rawText = apply_filters( 'mwai_chatbot_reply', $takeoverAnswer, $query, $params, [] );
652 return [
653 'reply' => $rawText,
654 'chatId' => $this->core->fix_chat_id( $query, $params ),
655 'images' => null,
656 'actions' => [],
657 'usage' => null
658 ];
659 }
660
661 // Moderation
662 $moderationEnabled = $this->core->get_option( 'module_moderation' ) &&
663 $this->core->get_option( 'shortcode_chat_moderation' );
664 if ( $moderationEnabled ) {
665 global $mwai;
666 $isFlagged = $mwai->moderationCheck( $query->get_message() );
667 if ( $isFlagged ) {
668 throw new Exception( 'Sorry, your message has been rejected by moderation.' );
669 }
670 }
671
672 // Setup streaming if enabled (before embeddings to capture those events)
673 $streamCallback = null;
674 $debugEvents = [];
675
676 if ( $stream ) {
677 $streamCallback = function ( $reply ) use ( $query ) {
678 // Support both legacy string data and new Event objects
679 if ( is_string( $reply ) ) {
680 $this->core->stream_push( [ 'type' => 'live', 'data' => $reply ], $query );
681 }
682 else {
683 $this->core->stream_push( $reply, $query );
684 }
685 };
686 if ( headers_sent( $filename, $linenum ) ) {
687 throw new Exception( "Headers already sent in $filename on line $linenum. Cannot start streaming." );
688 }
689 header( 'Cache-Control: no-cache' );
690 header( 'Content-Type: text/event-stream' );
691 // This is useful to disable buffering in nginx through headers.
692 header( 'X-Accel-Buffering: no' );
693 ob_implicit_flush( true );
694 if ( ob_get_level() > 0 ) {
695 ob_end_flush();
696 }
697 }
698 else if ( $this->core->get_option( 'module_devtools' ) && $this->core->get_option( 'debug_mode' ) ) {
699 // For non-streaming debug mode, collect events
700 $streamCallback = function ( $event ) use ( &$debugEvents ) {
701 if ( is_object( $event ) && method_exists( $event, 'toArray' ) ) {
702 $debugEvents[] = $event->toArray();
703 }
704 };
705 }
706
707 // Awareness & Embeddings
708 $context = $this->core->retrieve_context( $params, $query, $streamCallback );
709 if ( !empty( $context ) ) {
710 $query->set_context( $context['content'] );
711 }
712
713 // Function Aware
714 $query = apply_filters( 'mwai_chatbot_query', $query, $params );
715 }
716
717 // Process Query
718
719 $reply = $this->core->run_query( $query, $streamCallback, true );
720 $rawText = $reply->result;
721 $extra = [];
722 if ( $context ) {
723 $extra = [ 'embeddings' => isset( $context['embeddings'] ) ? $context['embeddings'] : null ];
724 }
725 // Store response ID for Responses API stateful conversations
726 // CRITICAL: Must store even when function calls are present
727 // This enables the feedback query to use previous_response_id
728 if ( !empty( $reply->id ) ) {
729 $extra['responseId'] = $reply->id;
730 $extra['responseDate'] = gmdate( 'Y-m-d H:i:s' ); // Track age for 30-day expiry
731 }
732 $rawText = apply_filters( 'mwai_chatbot_reply', $rawText, $query, $params, $extra );
733
734 // Integrity Check: We need to store the checksum of the messages sent by the client.
735 $stored_messages = $client_messages;
736 $stored_messages[] = [ 'role' => 'user', 'content' => $newMessage ];
737 $stored_messages[] = [ 'role' => 'assistant', 'content' => $rawText ];
738 $stored_checksum = $this->calculate_messages_checksum( $stored_messages );
739 set_transient( $checksum_key, $stored_checksum, 60 * 60 * 24 * 30 );
740
741 // Actions
742 $actions = [];
743 if ( $reply->needClientActions ) {
744 foreach ( $reply->needClientActions as $action ) {
745 $actions[] = [
746 'type' => 'function',
747 'data' => [
748 'name' => $action['function']->name,
749 'args' => $action['arguments']
750 ]
751 ];
752 }
753 }
754
755 $restRes = [
756 'reply' => $rawText,
757 'chatId' => $this->core->fix_chat_id( $query, $params ),
758 'images' => $reply->get_type() === 'images' ? $reply->results : null,
759 'actions' => $actions,
760 'usage' => $reply->usage
761 ];
762
763 // Add debug events if collected
764 if ( !empty( $debugEvents ) ) {
765 $restRes['debugEvents'] = $debugEvents;
766 }
767
768 // Add response ID if available (for Responses API)
769 if ( !empty( $reply->id ) ) {
770 $restRes['responseId'] = $reply->id;
771 }
772
773 // Process Reply
774 if ( $stream ) {
775 $final_res = $this->build_final_res(
776 $botId,
777 $newMessage,
778 $newFileId,
779 $params,
780 $restRes['reply'],
781 $restRes['images'],
782 $restRes['actions'],
783 $restRes['usage'],
784 $restRes['responseId'] ?? null
785 );
786 $this->core->stream_push( [ 'type' => 'end', 'data' => json_encode( $final_res ) ], $query );
787 die();
788 }
789 else {
790 return $restRes;
791 }
792
793 }
794 catch ( Exception $e ) {
795 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
796 if ( $stream ) {
797 $this->core->stream_push( [ 'type' => 'error', 'data' => $message ], $query );
798 die();
799 }
800 else {
801 throw $e;
802 }
803 }
804 }
805
806 public function inject_chat() {
807 $params = $this->core->get_chatbot( $this->siteWideChatId );
808 $clean_params = [];
809 if ( !empty( $params ) ) {
810 $clean_params['window'] = true;
811 $clean_params['id'] = $this->siteWideChatId;
812 echo $this->chat_shortcode( $clean_params );
813 }
814 return null;
815 }
816
817 public function build_front_params( $botId, $customId, $crossSite = false ) {
818 $frontSystem = [
819 'botId' => ( $customId && $customId !== '' ) ? null : sanitize_text_field( $botId ),
820 'customId' => ( $customId && $customId !== '' ) ? sanitize_text_field( $customId ) : null,
821 'userData' => $this->core->get_user_data(),
822 'sessionId' => $this->core->get_session_id(),
823 // IMPORTANT: REST nonce handling differs by user state:
824 // - Logged-in users: get_nonce() returns a user-specific nonce created in current session context
825 // - Logged-out users: get_nonce() returns null, they'll fetch via /start_session endpoint
826 // This prevents rest_cookie_invalid_nonce errors for logged-in users by ensuring the nonce
827 // matches their authentication context from the start.
828 'restNonce' => $crossSite ? null : $this->core->get_nonce(),
829 'contextId' => get_the_ID(),
830 'pluginUrl' => MWAI_URL,
831 'restUrl' => untrailingslashit( get_rest_url() ),
832 'stream' => $this->core->get_option( 'ai_streaming' ),
833 'debugMode' => $this->core->get_option( 'module_devtools' ) && $this->core->get_option( 'debug_mode' ),
834 'eventLogs' => $this->core->get_option( 'event_logs' ),
835 'speech_recognition' => $this->core->get_option( 'speech_recognition' ),
836 'speech_synthesis' => $this->core->get_option( 'speech_synthesis' ),
837 'typewriter' => $this->core->get_option( 'chatbot_typewriter' ),
838 'crossSite' => $crossSite
839 ];
840 return $frontSystem;
841 }
842
843 public function resolveBotInfo( &$atts ) {
844 $chatbot = null;
845 $botId = $atts['id'] ?? null;
846 $customId = $atts['custom_id'] ?? null;
847 $parentBotId = null;
848
849 if ( !$botId && !$customId ) {
850 $botId = 'default';
851 }
852 if ( $botId ) {
853 $chatbot = $this->core->get_chatbot( $botId );
854 if ( !$chatbot ) {
855 $botId = $botId ?: 'N/A';
856 $safe_botId = esc_html( $botId );
857 return [
858 'error' => "AI Engine: Chatbot '{$safe_botId}' not found. If you meant to set an ID for your custom chatbot, please use 'custom_id' instead of 'id'.",
859 ];
860 }
861 }
862 $chatbot = $chatbot ?: $this->core->get_chatbot( 'default' );
863
864
865 if ( !empty( $customId ) ) {
866 if ( $botId !== null ) {
867 $parentBotId = $botId;
868 $botId = null;
869 }
870 }
871
872 unset( $atts['id'] );
873 return [
874 'chatbot' => $chatbot,
875 'botId' => $botId,
876 'customId' => $customId,
877 'parentBotId' => $parentBotId
878 ];
879 }
880
881 public function chat_shortcode( $atts ) {
882 $atts = empty( $atts ) ? [] : $atts;
883
884 foreach ( $atts as $key => $value ) {
885 $atts[ $key ] = urldecode( $value );
886 }
887
888 // Let the user override the chatbot params
889 $atts = apply_filters( 'mwai_chatbot_params', $atts );
890
891 // Resolve the bot info
892 $resolvedBot = $this->resolveBotInfo( $atts );
893 if ( isset( $resolvedBot['error'] ) ) {
894 return $resolvedBot['error'];
895 }
896 $chatbot = $resolvedBot['chatbot'];
897 $botId = $resolvedBot['botId'];
898 $customId = $resolvedBot['customId'];
899 $parentBotId = $resolvedBot['parentBotId'];
900
901 // Rename the keys of the atts into camelCase to match the internal params system.
902 $atts = array_map( function ( $key, $value ) {
903 $key = str_replace( '_', ' ', $key );
904 $key = ucwords( $key );
905 $key = str_replace( ' ', '', $key );
906 $key = lcfirst( $key );
907 return [ $key => $value ];
908 }, array_keys( $atts ), $atts );
909 $atts = array_merge( ...$atts );
910
911 if ( !empty( $parentBotId ) ) {
912 $atts['parentBotId'] = $parentBotId;
913 }
914
915 $frontParams = [];
916 // Define text parameters that need sanitization (excluding those that support HTML)
917 $textParams = ['aiName', 'userName', 'guestName', 'textSend', 'textClear', 'textInputPlaceholder',
918 'startSentence', 'iconText', 'iconAlt', 'headerSubtitle'];
919 // Parameters that support HTML content
920 $htmlParams = ['textCompliance'];
921 // Boolean parameters that need special handling
922 $booleanParams = ['window', 'copyButton', 'fullscreen', 'localMemory', 'iconBubble', 'centerOpen',
923 'imageUpload', 'fileUpload', 'multiUpload', 'fileSearch'];
924
925
926 foreach ( MWAI_CHATBOT_FRONT_PARAMS as $param ) {
927 // Let's go through the overriden or custom params first (the ones passed in the shortcode)
928 if ( isset( $atts[$param] ) ) {
929 if ( $param === 'localMemory' ) {
930 $frontParams[$param] = $atts[$param] === 'true';
931 }
932 else if ( in_array( $param, $textParams ) ) {
933 // Sanitize text parameters to prevent XSS
934 $frontParams[$param] = sanitize_text_field( $atts[$param] );
935 }
936 else if ( in_array( $param, $htmlParams ) ) {
937 // For HTML parameters, use wp_kses_post to allow safe HTML
938 $frontParams[$param] = wp_kses_post( $atts[$param] );
939 }
940 else if ( in_array( $param, $booleanParams ) ) {
941 // Convert to proper boolean
942 $frontParams[$param] = !empty( $atts[$param] ) && $atts[$param] !== 'false';
943 }
944 else {
945 $frontParams[$param] = $atts[$param];
946 }
947 }
948 // If not, let's use the chatbot's default values
949 else if ( isset( $chatbot[$param] ) ) {
950 if ( in_array( $param, $booleanParams ) ) {
951 // Convert to proper boolean for chatbot defaults too
952 $frontParams[$param] = !empty( $chatbot[$param] ) && $chatbot[$param] !== 'false';
953 }
954 else {
955 $frontParams[$param] = $chatbot[$param];
956 }
957 }
958
959 // Apply the placeholders
960 if ( in_array( $param, ['startSentence', 'iconText'] ) ) {
961 $frontParams[$param] = $this->core->do_placeholders( $frontParams[$param] );
962 }
963 }
964
965 // Server Params
966 // NOTE: We don't need the server params for the chatbot if there are no overrides, it means
967 // we are using the default or a specific chatbot.
968 $isSiteWide = $this->siteWideChatId && $botId === $this->siteWideChatId;
969
970 // Parameters that are purely visual/UI and shouldn't trigger custom ID
971 $visualOnlyParams = [
972 // Bot selectors
973 'id', 'custom_id',
974 // System-added params
975 'crossSite',
976 // Visual/UI parameters that don't affect AI behavior
977 'aiName', 'userName', 'guestName', // Display names
978 'aiAvatar', 'userAvatar', 'guestAvatar', 'aiAvatarUrl', 'userAvatarUrl', 'guestAvatarUrl', // Avatars
979 'textSend', 'textClear', 'textInputPlaceholder', 'textCompliance', // UI text labels
980 'textInputMaxLength', // Input constraint (visual)
981 'themeId', // Theme selection
982 'window', 'icon', 'iconText', 'iconTextDelay', 'iconAlt', 'iconPosition', // Window/icon settings
983 'centerOpen', 'width', 'openDelay', 'iconBubble', 'windowAnimation', 'fullscreen', // Window behavior
984 'copyButton', 'headerSubtitle', // UI features
985 'containerType', 'headerType', 'messagesType', 'inputType', 'footerType' // UI style variants
986 ];
987
988 // Remove visual-only params from override detection
989 $attsForOverrideCheck = array_diff_key( $atts, array_flip( $visualOnlyParams ) );
990
991 // Only these front params affect behavior and should trigger custom ID:
992 // - mode: chat vs. prompt mode
993 // - startSentence: initial AI message
994 // - localMemory: affects data persistence
995 // - imageUpload, fileUpload, multiUpload, fileSearch: affect capabilities
996 $behavioralFrontParams = ['mode', 'startSentence', 'localMemory', 'imageUpload', 'fileUpload', 'multiUpload', 'fileSearch'];
997
998 $hasServerOverrides = count( array_intersect( array_keys( $attsForOverrideCheck ), MWAI_CHATBOT_SERVER_PARAMS ) ) > 0;
999 $hasBehavioralFrontOverrides = count( array_intersect( array_keys( $attsForOverrideCheck ), $behavioralFrontParams ) ) > 0;
1000 $hasOverrides = !$isSiteWide && ( $hasServerOverrides || $hasBehavioralFrontOverrides );
1001
1002 $serverParams = [];
1003 if ( $hasOverrides ) {
1004 // Server parameters don't need sanitization as they're processed server-side
1005 // and not rendered in HTML. They may contain code, HTML, etc. for AI context.
1006 foreach ( MWAI_CHATBOT_SERVER_PARAMS as $param ) {
1007 if ( isset( $atts[$param] ) ) {
1008 $serverParams[$param] = $atts[$param];
1009 }
1010 else {
1011 // For custom chatbots, don't inherit embeddingsEnvId from the default chatbot
1012 if ( $param === 'embeddingsEnvId' && !empty( $customId ) ) {
1013 $serverParams[$param] = '';
1014 }
1015 else {
1016 $serverParams[$param] = $chatbot[$param] ?? null;
1017 }
1018 }
1019 }
1020 }
1021
1022 // Front Params
1023 $frontSystem = $this->build_front_params( $botId, $customId );
1024
1025 // Clean Params
1026 $frontParams = $this->clean_params( $frontParams );
1027 $frontSystem = $this->clean_params( $frontSystem );
1028 $serverParams = $this->clean_params( $serverParams );
1029
1030 // Server-side: Keep the System Params
1031 if ( $hasOverrides ) {
1032 if ( empty( $customId ) ) {
1033 $customId = md5( json_encode( $serverParams ) );
1034 $frontSystem['customId'] = $customId;
1035 }
1036 set_transient( 'mwai_custom_chatbot_' . $customId, $serverParams, 60 * 60 * 24 );
1037 }
1038
1039 // Retrieve the actions, shortcuts, and blocks we want to inject at the beginning
1040 $filterParams = [
1041 'step' => 'init',
1042 'botId' => $botId,
1043 'params' => array_merge( $frontParams, $frontSystem, $serverParams )
1044 ];
1045 $actions = apply_filters( 'mwai_chatbot_actions', [], $filterParams );
1046 $blocks = apply_filters( 'mwai_chatbot_blocks', [], $filterParams );
1047 $shortcuts = apply_filters( 'mwai_chatbot_shortcuts', [], $filterParams );
1048 $frontSystem['actions'] = $this->sanitize_actions( $actions );
1049 $frontSystem['blocks'] = $this->sanitize_blocks( $blocks );
1050 $frontSystem['shortcuts'] = $this->sanitize_shortcuts( $shortcuts );
1051
1052 // Client-side: Prepare JSON for Front Params and System Params
1053 $theme = isset( $frontParams['themeId'] ) ? $this->core->get_theme( $frontParams['themeId'] ) : null;
1054 $jsonFrontParams = htmlspecialchars( json_encode( $frontParams ), ENT_QUOTES, 'UTF-8' );
1055 $jsonFrontSystem = htmlspecialchars( json_encode( $frontSystem ), ENT_QUOTES, 'UTF-8' );
1056 $jsonFrontTheme = htmlspecialchars( json_encode( $theme ), ENT_QUOTES, 'UTF-8' );
1057 //$jsonAttributes = htmlspecialchars(json_encode($atts), ENT_QUOTES, 'UTF-8');
1058
1059 $this->enqueue_scripts( $frontParams['themeId'] ?? null );
1060
1061 return "<div class='mwai-chatbot-container' data-params='{$jsonFrontParams}' data-system='{$jsonFrontSystem}' data-theme='{$jsonFrontTheme}'></div>";
1062 }
1063
1064 public function chatbot_discussions( $atts ) {
1065 $atts = empty( $atts ) ? [] : $atts;
1066
1067 // Resolve the bot info
1068 $resolvedBot = $this->resolveBotInfo( $atts );
1069 if ( isset( $resolvedBot['error'] ) ) {
1070 return $resolvedBot['error'];
1071 }
1072 $chatbot = $resolvedBot['chatbot'];
1073 $botId = $resolvedBot['botId'];
1074 $customId = $resolvedBot['customId'];
1075
1076 // Rename the keys of the atts into camelCase to match the internal params system.
1077 $atts = array_map( function ( $key, $value ) {
1078 $key = str_replace( '_', ' ', $key );
1079 $key = ucwords( $key );
1080 $key = str_replace( ' ', '', $key );
1081 $key = lcfirst( $key );
1082 return [ $key => $value ];
1083 }, array_keys( $atts ), $atts );
1084 $atts = array_merge( ...$atts );
1085
1086 // Front Params
1087 $frontParams = [];
1088 // All discussion params are text params that need sanitization
1089 $textParams = ['textNewChat'];
1090
1091 foreach ( MWAI_DISCUSSIONS_FRONT_PARAMS as $param ) {
1092 if ( isset( $atts[$param] ) ) {
1093 // Sanitize text parameters
1094 $frontParams[$param] = in_array( $param, $textParams ) ? sanitize_text_field( $atts[$param] ) : $atts[$param];
1095 }
1096 else if ( isset( $chatbot[$param] ) ) {
1097 $frontParams[$param] = $chatbot[$param];
1098 }
1099 }
1100
1101 // Server Params
1102 $serverParams = [];
1103 foreach ( MWAI_DISCUSSIONS_SERVER_PARAMS as $param ) {
1104 if ( isset( $atts[$param] ) ) {
1105 $serverParams[$param] = $atts[$param];
1106 }
1107 }
1108
1109 // Front System
1110 $frontSystem = $this->build_front_params( $botId, $customId );
1111 // Get refresh interval from settings
1112 $refresh_interval = $this->core->get_option( 'chatbot_discussions_refresh_interval' );
1113 if ( $refresh_interval === 'Never' ) {
1114 $frontSystem['refreshInterval'] = 0;
1115 }
1116 elseif ( $refresh_interval === 'Manual' ) {
1117 $frontSystem['refreshInterval'] = -1;
1118 }
1119 elseif ( is_numeric( $refresh_interval ) ) {
1120 $frontSystem['refreshInterval'] = intval( $refresh_interval ) * 1000; // Convert to milliseconds
1121 }
1122 else {
1123 $frontSystem['refreshInterval'] = 5000; // Default to 5 seconds
1124 }
1125 $frontSystem['refreshInterval'] = apply_filters( 'mwai_discussions_refresh_interval', $frontSystem['refreshInterval'] );
1126
1127 // Get paging setting
1128 $paging_option = $this->core->get_option( 'chatbot_discussions_paging' );
1129 if ( $paging_option === 'None' ) {
1130 $frontSystem['paging'] = 0; // No pagination
1131 }
1132 else {
1133 $frontSystem['paging'] = is_numeric( $paging_option ) ? intval( $paging_option ) : 10; // Default to 10
1134 }
1135
1136 // Get metadata settings
1137 $frontSystem['metadata'] = [
1138 'enabled' => $this->core->get_option( 'chatbot_discussions_metadata_enabled' ),
1139 'startDate' => $this->core->get_option( 'chatbot_discussions_metadata_start_date' ),
1140 'lastUpdate' => $this->core->get_option( 'chatbot_discussions_metadata_last_update' ),
1141 'messageCount' => $this->core->get_option( 'chatbot_discussions_metadata_message_count' )
1142 ];
1143
1144 // Clean Params
1145 $frontParams = $this->clean_params( $frontParams );
1146 $frontSystem = $this->clean_params( $frontSystem );
1147 $serverParams = $this->clean_params( $serverParams );
1148
1149 $theme = isset( $frontParams['themeId'] ) ? $this->core->get_theme( $frontParams['themeId'] ) : null;
1150 $jsonFrontParams = htmlspecialchars( json_encode( $frontParams ), ENT_QUOTES, 'UTF-8' );
1151 $jsonFrontSystem = htmlspecialchars( json_encode( $frontSystem ), ENT_QUOTES, 'UTF-8' );
1152 $jsonFrontTheme = htmlspecialchars( json_encode( $theme ), ENT_QUOTES, 'UTF-8' );
1153
1154 return "<div class='mwai-discussions-container' data-params='{$jsonFrontParams}' data-system='{$jsonFrontSystem}' data-theme='{$jsonFrontTheme}'></div>";
1155 }
1156
1157 public function clean_params( &$params ) {
1158 foreach ( $params as $param => $value ) {
1159 if ( $param === 'restNonce' ) {
1160 continue;
1161 }
1162 // Skip only if value is null or an array - but not if it's false or 0
1163 if ( is_null( $value ) || is_array( $value ) ) {
1164 continue;
1165 }
1166 // Handle empty strings
1167 if ( $value === '' ) {
1168 continue;
1169 }
1170 $lowerCaseValue = is_string( $value ) ? strtolower( $value ) : '';
1171 if ( $lowerCaseValue === 'true' || $lowerCaseValue === 'false' || is_bool( $value ) ) {
1172 $params[$param] = filter_var( $value, FILTER_VALIDATE_BOOLEAN );
1173 }
1174 else if ( is_numeric( $value ) ) {
1175 $params[$param] = filter_var( $value, FILTER_VALIDATE_FLOAT );
1176 }
1177 }
1178 return $params;
1179 }
1180
1181 }
1182