PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 2.4.6
MxChat – AI Chatbot & Content Generation for WordPress v2.4.6
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
← All changes | includes/class-mxchat-word-handler.php +358 -411 3.2.212.4.6 View file →
@@ -1,412 +1,359 @@
1 -<?php
2 -/**
3 - * Word document handler and processor for MXChat
4 - * Can be directly bundled in WordPress plugins
5 - */
6 -class MXChat_Word_Handler {
7 - private $temp_dir;
8 - private $options;
9 -
10 - public function __construct($options) {
11 - $this->options = $options;
12 - $this->temp_dir = wp_upload_dir()['path'];
13 - }
14 -
15 - /**
16 - * Handle Word document upload and processing
17 - */
18 - public function mxchat_handle_word_upload() {
19 - // Match the PDF handler's nonce verification: the widget sends the chat-send
20 - // nonce (action 'mxchat_chat_send'), which the old check_ajax_referer('mxchat_chat_nonce')
21 - // rejected with -1. mxchat_verify_chat_send_nonce accepts both chat-send and chat nonces.
22 - if (!isset($_POST['nonce']) || !MxChat_Integrator::mxchat_verify_chat_send_nonce(wp_unslash((string) $_POST['nonce']))) {
23 - wp_send_json_error(array('message' => esc_html__('Invalid nonce.', 'mxchat')), 403);
24 - }
25 -
26 - if (!isset($_FILES['word_file']) || !isset($_POST['session_id'])) {
27 - wp_send_json_error(esc_html__('Missing required parameters.', 'mxchat'));
28 - return;
29 - }
30 -
31 - // SECURITY FIX: Check if Word uploads are enabled in settings
32 - $options = get_option('mxchat_options', array());
33 - $show_word_button = isset($options['show_word_upload_button']) ? $options['show_word_upload_button'] : 'on';
34 -
35 - if ($show_word_button !== 'on') {
36 - wp_send_json_error(esc_html__('Word document uploads are currently disabled.', 'mxchat'));
37 - return;
38 - }
39 -
40 - $file = $_FILES['word_file'];
41 - $session_id = MxChat_Utils::sanitize_session_id(wp_unslash($_POST['session_id']));
42 - $original_filename = sanitize_text_field($file['name']);
43 -
44 - // Update session owner if it changed (e.g. IP changed due to network switch)
45 - $current_user_identifier = MxChat_User::mxchat_get_user_identifier();
46 - $session_owner = MxChat_Session_Store::get($session_id, 'owner');
47 -
48 - if (!$session_owner || $session_owner !== $current_user_identifier) {
49 - MxChat_Session_Store::set($session_id, 'owner', $current_user_identifier);
50 - }
51 -
52 - // Check file type
53 - $allowed_types = array(
54 - 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
55 - );
56 - $file_type = wp_check_filetype($file['name'], $allowed_types);
57 -
58 - if (!$file_type['type']) {
59 - wp_send_json_error(esc_html__('Invalid file type. Only .docx files are allowed.', 'mxchat'));
60 - return;
61 - }
62 -
63 - // SECURITY FIX: Generate random filename without exposing session_id
64 - $random_string = wp_generate_password(20, false, false); // 20 char alphanumeric string
65 - $word_filename = 'mxchat_word_' . $random_string . '_' . time() . '.docx';
66 - $word_path = $this->temp_dir . '/' . $word_filename;
67 -
68 - if (!move_uploaded_file($file['tmp_name'], $word_path)) {
69 - wp_send_json_error(esc_html__('Failed to upload file.', 'mxchat'));
70 - return;
71 - }
72 -
73 - $this->mxchat_clear_word_transients($session_id);
74 -
75 - // Process the document
76 - $embeddings = $this->mxchat_process_word_document($word_path);
77 -
78 - if ($embeddings === false || empty($embeddings)) {
79 - unlink($word_path);
80 - $error_message = $this->options['word_intent_error_text'] ??
81 - esc_html__('The uploaded document appears to be empty or contains unsupported content.', 'mxchat');
82 - wp_send_json_error($error_message);
83 - return;
84 - }
85 -
86 - // Store the mapping between session and the random filename
87 - set_transient('mxchat_word_url_' . $session_id, $word_path, HOUR_IN_SECONDS);
88 - set_transient('mxchat_word_filename_' . $session_id, $original_filename, HOUR_IN_SECONDS);
89 - set_transient('mxchat_word_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
90 - set_transient('mxchat_include_word_in_context_' . $session_id, true, HOUR_IN_SECONDS);
91 -
92 - $success_message = $this->options['pdf_intent_success_text'] ??
93 - __("I've processed the document. What questions do you have about it?", 'mxchat');
94 -
95 - wp_send_json_success([
96 - 'message' => $success_message,
97 - 'filename' => $original_filename
98 - ]);
99 - }
100 -
101 - /**
102 - * Extract plain text from a .docx file. THE one .docx parser — the
103 - * visitor-facing toolbar upload (via mxchat_process_word_document) and the
104 - * admin knowledge importer (plan 0485e5) both call this; do not fork a
105 - * second copy. Returns the cleaned text, or false on unreadable/empty
106 - * files (not a Zip, no word/document.xml, no text content).
107 - */
108 - public static function extract_docx_text($file_path) {
109 - try {
110 - $zip = new ZipArchive();
111 - if ($zip->open($file_path) !== true) {
112 - return false;
113 - }
114 - $content = $zip->getFromName('word/document.xml');
115 - $zip->close();
116 - if ($content === false) {
117 - return false;
118 - }
119 - $text = self::clean_word_xml($content);
120 - return $text === '' ? false : $text;
121 - } catch (Exception $e) {
122 - return false;
123 - }
124 - }
125 -
126 - /**
127 - * Process Word document and generate embeddings
128 - */
129 -private function mxchat_process_word_document($file_path) {
130 - // Get the maximum number of pages allowed from admin settings
131 - $max_pages = isset($this->options['pdf_max_pages']) ? intval($this->options['pdf_max_pages']) : 69; // Use same setting as PDF
132 -
133 - try {
134 - $text = self::extract_docx_text($file_path);
135 - if ($text === false) {
136 - return false;
137 - }
138 -
139 - // Count pages (roughly estimate based on paragraphs)
140 - $paragraphs = explode("\n\n", $text);
141 - $estimated_pages = ceil(count($paragraphs) / 3); // Assume ~3 paragraphs per page
142 -
143 - if ($estimated_pages > $max_pages) {
144 - return esc_html__('too_many_pages', 'mxchat');
145 - }
146 -
147 - // Split into chunks and continue processing...
148 - $chunks = $this->mxchat_split_word_into_chunks($text, 1000);
149 -
150 - $embeddings = [];
151 - foreach ($chunks as $chunk_number => $chunk) {
152 - if (empty(trim($chunk))) {
153 - continue;
154 - }
155 -
156 - $embedding = $this->mxchat_generate_embedding_word(
157 - esc_html__('Chunk ', 'mxchat') . ($chunk_number + 1) . ': ' . $chunk,
158 - $this->options['api_key']
159 - );
160 -
161 - if ($embedding) {
162 - $embeddings[] = [
163 - 'chunk_number' => $chunk_number + 1,
164 - 'embedding' => $embedding,
165 - 'text' => $chunk,
166 - ];
167 - }
168 - }
169 -
170 - return $embeddings;
171 -
172 - } catch (Exception $e) {
173 - return false;
174 - }
175 -}
176 - /**
177 - * Clean Word XML content (word/document.xml) to plain text.
178 - * Static so both callers of extract_docx_text share one implementation.
179 - */
180 - public static function clean_word_xml($content) {
181 - // Remove XML namespaces
182 - $content = preg_replace('/xmlns[^=]*="[^"]*"/i', '', $content);
183 -
184 - // Convert Word XML elements to text
185 - $content = str_replace('</w:p>', "\n", $content);
186 - $content = str_replace('</w:tr>', "\n", $content);
187 -
188 - // Strip remaining XML tags
189 - $content = strip_tags($content);
190 -
191 - // Clean up whitespace
192 - $content = preg_replace('/\s+/', ' ', $content);
193 - $content = preg_replace('/\n\s*\n/', "\n\n", $content);
194 -
195 - return trim($content);
196 - }
197 -
198 - /**
199 - * Split text into manageable chunks
200 - */
201 - private function mxchat_split_word_into_chunks($text, $chunk_size) {
202 - $chunks = [];
203 - $paragraphs = explode("\n\n", $text);
204 -
205 - $current_chunk = '';
206 - foreach ($paragraphs as $paragraph) {
207 - if (strlen($current_chunk) + strlen($paragraph) > $chunk_size) {
208 - if (!empty($current_chunk)) {
209 - $chunks[] = trim($current_chunk);
210 - }
211 - $current_chunk = $paragraph;
212 - } else {
213 - $current_chunk .= (!empty($current_chunk) ? "\n\n" : '') . $paragraph;
214 - }
215 - }
216 -
217 - if (!empty($current_chunk)) {
218 - $chunks[] = trim($current_chunk);
219 - }
220 -
221 - return $chunks;
222 - }
223 -
224 - /**
225 - * Remove Word document and clean up transients
226 - */
227 -public function mxchat_handle_word_remove() {
228 - check_ajax_referer('mxchat_chat_nonce', 'nonce');
229 -
230 - if (empty($_POST['session_id'])) {
231 - wp_send_json_error(esc_html__('Session ID missing.', 'mxchat'));
232 - return;
233 - }
234 -
235 - $session_id = MxChat_Utils::sanitize_session_id(wp_unslash($_POST['session_id']));
236 - if ($session_id === '') {
237 - wp_send_json_error(esc_html__('Session ID missing.', 'mxchat'));
238 - return;
239 - }
240 -
241 - // Session-ownership bookkeeping — same rule as handle_pdf_remove() and the
242 - // history endpoint (plan-mxchat-20260731-d42bec). Possession of the session
243 - // id is the credential, so this records/refreshes the owner rather than
244 - // refusing a mismatch; see the fuller note in handle_pdf_remove().
245 - $current_user_identifier = MxChat_User::mxchat_get_user_identifier();
246 - $session_owner = MxChat_Session_Store::get($session_id, 'owner');
247 - if (!$session_owner || $session_owner !== $current_user_identifier) {
248 - MxChat_Session_Store::set($session_id, 'owner', $current_user_identifier);
249 - }
250 -
251 - $word_path = get_transient('mxchat_word_url_' . $session_id);
252 -
253 - if ($word_path && file_exists($word_path)) {
254 - unlink($word_path);
255 - }
256 -
257 - $this->mxchat_clear_word_transients($session_id);
258 -
259 - wp_send_json_success([
260 - 'message' => esc_html__('Document removed successfully.', 'mxchat')
261 - ]);
262 - }
263 -
264 - /**
265 - * Clear all Word-related transients
266 - */
267 - private function mxchat_clear_word_transients($session_id) {
268 - delete_transient('mxchat_word_url_' . $session_id);
269 - delete_transient('mxchat_word_filename_' . $session_id);
270 - delete_transient('mxchat_word_embeddings_' . $session_id);
271 - delete_transient('mxchat_include_word_in_context_' . $session_id);
272 - }
273 -
274 - /**
275 - * Find relevant chunks from the Word document
276 - */
277 - public function mxchat_find_relevant_word_chunks($query_embedding, $embeddings) {
278 - $most_relevant = null;
279 - $highest_similarity = -INF;
280 -
281 - foreach ($embeddings as $chunk_data) {
282 - $similarity = $this->mxchat_calculate_cosine_similarity_word($query_embedding, $chunk_data['embedding']);
283 -
284 - if ($similarity > $highest_similarity) {
285 - $highest_similarity = $similarity;
286 - $most_relevant = $chunk_data['chunk_number'];
287 - }
288 - }
289 -
290 - if (!is_null($most_relevant)) {
291 - $chunk_numbers = range(
292 - max(1, $most_relevant - 1),
293 - min(count($embeddings), $most_relevant + 1)
294 - );
295 - return array_filter($embeddings, function ($chunk) use ($chunk_numbers) {
296 - return in_array($chunk['chunk_number'], $chunk_numbers);
297 - });
298 - }
299 -
300 - return [];
301 - }
302 -
303 - /**
304 - * Handle Word document discussion similar to PDF discussion
305 - */
306 -public function mxchat_handle_word_discussion($message, $user_id, $session_id) {
307 - // Get stored embeddings for the session
308 - $embeddings = get_transient('mxchat_word_embeddings_' . $session_id);
309 - $word_path = get_transient('mxchat_word_url_' . $session_id);
310 -
311 - if (!$embeddings || !$word_path) {
312 - $trigger_text = $this->options['word_intent_trigger_text'] ??
313 - __("Please upload a Word document (.docx) that you'd like to discuss.", 'mxchat');
314 - set_transient('mxchat_waiting_for_word_' . $session_id, true, HOUR_IN_SECONDS);
315 - $this->fallbackResponse['text'] = $trigger_text;
316 - return;
317 - }
318 -
319 - // Set context flag for including Word content in conversation
320 - set_transient('mxchat_include_word_in_context_' . $session_id, true, HOUR_IN_SECONDS);
321 - $this->fallbackResponse['text'] = ''; // Proceed without additional message
322 - }
323 -
324 -
325 - private function mxchat_generate_embedding_word($text, $api_key) {
326 - $endpoint = 'https://api.openai.com/v1/embeddings';
327 -
328 - $body = wp_json_encode([
329 - 'input' => $text,
330 - 'model' => 'text-embedding-ada-002'
331 - ]);
332 -
333 - $args = [
334 - 'body' => $body,
335 - 'headers' => [
336 - 'Content-Type' => 'application/json',
337 - 'Authorization' => 'Bearer ' . $api_key,
338 - ],
339 - 'timeout' => 60,
340 - 'redirection' => 5,
341 - 'blocking' => true,
342 - 'httpversion' => '1.0',
343 - 'sslverify' => true,
344 - ];
345 -
346 - $response = wp_remote_post($endpoint, $args);
347 -
348 - if (is_wp_error($response)) {
349 - return null;
350 - }
351 -
352 - $response_body = json_decode(wp_remote_retrieve_body($response), true);
353 -
354 - if (isset($response_body['data'][0]['embedding']) && is_array($response_body['data'][0]['embedding'])) {
355 - return $response_body['data'][0]['embedding'];
356 - } else {
357 - return null;
358 - }
359 - }
360 -
361 -
362 - private function mxchat_calculate_cosine_similarity_word($vectorA, $vectorB) {
363 - if (!is_array($vectorA) || !is_array($vectorB) || empty($vectorA) || empty($vectorB)) {
364 - return 0;
365 - }
366 -
367 - $dotProduct = array_sum(array_map(function ($a, $b) {
368 - return $a * $b;
369 - }, $vectorA, $vectorB));
370 - $normA = sqrt(array_sum(array_map(function ($a) {
371 - return $a * $a;
372 - }, $vectorA)));
373 - $normB = sqrt(array_sum(array_map(function ($b) {
374 - return $b * $b;
375 - }, $vectorB)));
376 -
377 - if ($normA == 0 || $normB == 0) {
378 - return 0;
379 - }
380 -
381 - return $dotProduct / ($normA * $normB);
382 - }
383 -
384 - /**
385 - * Check the status of a Word document for the current session
386 - */
387 -public function mxchat_check_word_status() {
388 - check_ajax_referer('mxchat_chat_nonce', 'nonce');
389 -
390 - if (empty($_POST['session_id'])) {
391 - wp_send_json_error(esc_html__('Session ID missing.', 'mxchat'));
392 - return;
393 - }
394 -
395 - $session_id = MxChat_Utils::sanitize_session_id(wp_unslash($_POST['session_id']));
396 - $word_path = get_transient('mxchat_word_url_' . $session_id);
397 - $filename = get_transient('mxchat_word_filename_' . $session_id);
398 -
399 - if ($word_path && file_exists($word_path) && $filename) {
400 - wp_send_json_success([
401 - 'has_word' => true,
402 - 'filename' => $filename
403 - ]);
404 - } else {
405 - wp_send_json_success([
406 - 'has_word' => false
407 - ]);
408 - }
409 -}
410 -
411 -
1 +<?php
2 +/**
3 + * Word document handler and processor for MXChat
4 + * Can be directly bundled in WordPress plugins
5 + */
6 +class MXChat_Word_Handler {
7 + private $temp_dir;
8 + private $options;
9 +
10 + public function __construct($options) {
11 + $this->options = $options;
12 + $this->temp_dir = wp_upload_dir()['path'];
13 + }
14 +
15 + /**
16 + * Handle Word document upload and processing
17 + */
18 +public function mxchat_handle_word_upload() {
19 + check_ajax_referer('mxchat_chat_nonce', 'nonce');
20 +
21 + if (!isset($_FILES['word_file']) || !isset($_POST['session_id'])) {
22 + wp_send_json_error(esc_html__('Missing required parameters.', 'mxchat'));
23 + return;
24 + }
25 +
26 + $file = $_FILES['word_file'];
27 + $session_id = sanitize_text_field($_POST['session_id']);
28 + $original_filename = sanitize_text_field($file['name']);
29 +
30 + // Check file type
31 + $allowed_types = array(
32 + 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
33 + );
34 + $file_type = wp_check_filetype($file['name'], $allowed_types);
35 +
36 + if (!$file_type['type']) {
37 + wp_send_json_error(esc_html__('Invalid file type. Only .docx files are allowed.', 'mxchat'));
38 + return;
39 + }
40 +
41 + // Generate unique filename
42 + $word_filename = 'mxchat_word_' . $session_id . '_' . time() . '.docx';
43 + $word_path = $this->temp_dir . '/' . $word_filename;
44 +
45 + if (!move_uploaded_file($file['tmp_name'], $word_path)) {
46 + wp_send_json_error(esc_html__('Failed to upload file.', 'mxchat'));
47 + return;
48 + }
49 +
50 + $this->mxchat_clear_word_transients($session_id);
51 +
52 + // Process the document
53 + $embeddings = $this->mxchat_process_word_document($word_path);
54 +
55 + if ($embeddings === false || empty($embeddings)) {
56 + unlink($word_path);
57 + $error_message = $this->options['word_intent_error_text'] ??
58 + esc_html__('The uploaded document appears to be empty or contains unsupported content.', 'mxchat');
59 + wp_send_json_error($error_message);
60 + return;
61 + }
62 +
63 + // Store the embeddings and file information
64 + set_transient('mxchat_word_url_' . $session_id, $word_path, HOUR_IN_SECONDS);
65 + set_transient('mxchat_word_filename_' . $session_id, $original_filename, HOUR_IN_SECONDS);
66 + set_transient('mxchat_word_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
67 + set_transient('mxchat_include_word_in_context_' . $session_id, true, HOUR_IN_SECONDS);
68 +
69 + $success_message = $this->options['pdf_intent_success_text'] ??
70 + __("I've processed the document. What questions do you have about it?", 'mxchat');
71 +
72 + wp_send_json_success([
73 + 'message' => $success_message,
74 + 'filename' => $original_filename
75 + ]);
76 + }
77 +
78 + /**
79 + * Process Word document and generate embeddings
80 + */
81 +private function mxchat_process_word_document($file_path) {
82 + // Get the maximum number of pages allowed from admin settings
83 + $max_pages = isset($this->options['pdf_max_pages']) ? intval($this->options['pdf_max_pages']) : 69; // Use same setting as PDF
84 +
85 + try {
86 + $zip = new ZipArchive();
87 + if ($zip->open($file_path) !== true) {
88 + return false;
89 + }
90 +
91 + // Extract main document content
92 + $content = $zip->getFromName('word/document.xml');
93 + $zip->close();
94 +
95 + if ($content === false) {
96 + return false;
97 + }
98 +
99 + // Clean up the content
100 + $text = $this->mxchat_clean_word_content($content);
101 +
102 + // Count pages (roughly estimate based on paragraphs)
103 + $paragraphs = explode("\n\n", $text);
104 + $estimated_pages = ceil(count($paragraphs) / 3); // Assume ~3 paragraphs per page
105 +
106 + if ($estimated_pages > $max_pages) {
107 + return esc_html__('too_many_pages', 'mxchat');
108 + }
109 +
110 + // Split into chunks and continue processing...
111 + $chunks = $this->mxchat_split_word_into_chunks($text, 1000);
112 +
113 + $embeddings = [];
114 + foreach ($chunks as $chunk_number => $chunk) {
115 + if (empty(trim($chunk))) {
116 + continue;
117 + }
118 +
119 + $embedding = $this->mxchat_generate_embedding_word(
120 + esc_html__('Chunk ', 'mxchat') . ($chunk_number + 1) . ': ' . $chunk,
121 + $this->options['api_key']
122 + );
123 +
124 + if ($embedding) {
125 + $embeddings[] = [
126 + 'chunk_number' => $chunk_number + 1,
127 + 'embedding' => $embedding,
128 + 'text' => $chunk,
129 + ];
130 + }
131 + }
132 +
133 + return $embeddings;
134 +
135 + } catch (Exception $e) {
136 + return false;
137 + }
138 +}
139 + /**
140 + * Clean Word XML content
141 + */
142 + private function mxchat_clean_word_content($content) {
143 + // Remove XML namespaces
144 + $content = preg_replace('/xmlns[^=]*="[^"]*"/i', '', $content);
145 +
146 + // Convert Word XML elements to text
147 + $content = str_replace('</w:p>', "\n", $content);
148 + $content = str_replace('</w:tr>', "\n", $content);
149 +
150 + // Strip remaining XML tags
151 + $content = strip_tags($content);
152 +
153 + // Clean up whitespace
154 + $content = preg_replace('/\s+/', ' ', $content);
155 + $content = preg_replace('/\n\s*\n/', "\n\n", $content);
156 +
157 + return trim($content);
158 + }
159 +
160 + /**
161 + * Split text into manageable chunks
162 + */
163 + private function mxchat_split_word_into_chunks($text, $chunk_size) {
164 + $chunks = [];
165 + $paragraphs = explode("\n\n", $text);
166 +
167 + $current_chunk = '';
168 + foreach ($paragraphs as $paragraph) {
169 + if (strlen($current_chunk) + strlen($paragraph) > $chunk_size) {
170 + if (!empty($current_chunk)) {
171 + $chunks[] = trim($current_chunk);
172 + }
173 + $current_chunk = $paragraph;
174 + } else {
175 + $current_chunk .= (!empty($current_chunk) ? "\n\n" : '') . $paragraph;
176 + }
177 + }
178 +
179 + if (!empty($current_chunk)) {
180 + $chunks[] = trim($current_chunk);
181 + }
182 +
183 + return $chunks;
184 + }
185 +
186 + /**
187 + * Remove Word document and clean up transients
188 + */
189 +public function mxchat_handle_word_remove() {
190 + check_ajax_referer('mxchat_chat_nonce', 'nonce');
191 +
192 + if (empty($_POST['session_id'])) {
193 + wp_send_json_error(esc_html__('Session ID missing.', 'mxchat'));
194 + return;
195 + }
196 +
197 + $session_id = sanitize_text_field($_POST['session_id']);
198 + $word_path = get_transient('mxchat_word_url_' . $session_id);
199 +
200 + if ($word_path && file_exists($word_path)) {
201 + unlink($word_path);
202 + }
203 +
204 + $this->mxchat_clear_word_transients($session_id);
205 +
206 + wp_send_json_success([
207 + 'message' => esc_html__('Document removed successfully.', 'mxchat')
208 + ]);
209 + }
210 +
211 + /**
212 + * Clear all Word-related transients
213 + */
214 + private function mxchat_clear_word_transients($session_id) {
215 + delete_transient('mxchat_word_url_' . $session_id);
216 + delete_transient('mxchat_word_filename_' . $session_id);
217 + delete_transient('mxchat_word_embeddings_' . $session_id);
218 + delete_transient('mxchat_include_word_in_context_' . $session_id);
219 + }
220 +
221 + /**
222 + * Find relevant chunks from the Word document
223 + */
224 + public function mxchat_find_relevant_word_chunks($query_embedding, $embeddings) {
225 + $most_relevant = null;
226 + $highest_similarity = -INF;
227 +
228 + foreach ($embeddings as $chunk_data) {
229 + $similarity = $this->mxchat_calculate_cosine_similarity_word($query_embedding, $chunk_data['embedding']);
230 +
231 + if ($similarity > $highest_similarity) {
232 + $highest_similarity = $similarity;
233 + $most_relevant = $chunk_data['chunk_number'];
234 + }
235 + }
236 +
237 + if (!is_null($most_relevant)) {
238 + $chunk_numbers = range(
239 + max(1, $most_relevant - 1),
240 + min(count($embeddings), $most_relevant + 1)
241 + );
242 + return array_filter($embeddings, function ($chunk) use ($chunk_numbers) {
243 + return in_array($chunk['chunk_number'], $chunk_numbers);
244 + });
245 + }
246 +
247 + return [];
248 + }
249 +
250 + /**
251 + * Handle Word document discussion similar to PDF discussion
252 + */
253 +public function mxchat_handle_word_discussion($message, $user_id, $session_id) {
254 + // Get stored embeddings for the session
255 + $embeddings = get_transient('mxchat_word_embeddings_' . $session_id);
256 + $word_path = get_transient('mxchat_word_url_' . $session_id);
257 +
258 + if (!$embeddings || !$word_path) {
259 + $trigger_text = $this->options['word_intent_trigger_text'] ??
260 + __("Please upload a Word document (.docx) that you'd like to discuss.", 'mxchat');
261 + set_transient('mxchat_waiting_for_word_' . $session_id, true, HOUR_IN_SECONDS);
262 + $this->fallbackResponse['text'] = $trigger_text;
263 + return;
264 + }
265 +
266 + // Set context flag for including Word content in conversation
267 + set_transient('mxchat_include_word_in_context_' . $session_id, true, HOUR_IN_SECONDS);
268 + $this->fallbackResponse['text'] = ''; // Proceed without additional message
269 + }
270 +
271 +
272 + private function mxchat_generate_embedding_word($text, $api_key) {
273 + $endpoint = 'https://api.openai.com/v1/embeddings';
274 +
275 + $body = wp_json_encode([
276 + 'input' => $text,
277 + 'model' => 'text-embedding-ada-002'
278 + ]);
279 +
280 + $args = [
281 + 'body' => $body,
282 + 'headers' => [
283 + 'Content-Type' => 'application/json',
284 + 'Authorization' => 'Bearer ' . $api_key,
285 + ],
286 + 'timeout' => 60,
287 + 'redirection' => 5,
288 + 'blocking' => true,
289 + 'httpversion' => '1.0',
290 + 'sslverify' => true,
291 + ];
292 +
293 + $response = wp_remote_post($endpoint, $args);
294 +
295 + if (is_wp_error($response)) {
296 + return null;
297 + }
298 +
299 + $response_body = json_decode(wp_remote_retrieve_body($response), true);
300 +
301 + if (isset($response_body['data'][0]['embedding']) && is_array($response_body['data'][0]['embedding'])) {
302 + return $response_body['data'][0]['embedding'];
303 + } else {
304 + return null;
305 + }
306 + }
307 +
308 +
309 + private function mxchat_calculate_cosine_similarity_word($vectorA, $vectorB) {
310 + if (!is_array($vectorA) || !is_array($vectorB) || empty($vectorA) || empty($vectorB)) {
311 + return 0;
312 + }
313 +
314 + $dotProduct = array_sum(array_map(function ($a, $b) {
315 + return $a * $b;
316 + }, $vectorA, $vectorB));
317 + $normA = sqrt(array_sum(array_map(function ($a) {
318 + return $a * $a;
319 + }, $vectorA)));
320 + $normB = sqrt(array_sum(array_map(function ($b) {
321 + return $b * $b;
322 + }, $vectorB)));
323 +
324 + if ($normA == 0 || $normB == 0) {
325 + return 0;
326 + }
327 +
328 + return $dotProduct / ($normA * $normB);
329 + }
330 +
331 + /**
332 + * Check the status of a Word document for the current session
333 + */
334 +public function mxchat_check_word_status() {
335 + check_ajax_referer('mxchat_chat_nonce', 'nonce');
336 +
337 + if (empty($_POST['session_id'])) {
338 + wp_send_json_error(esc_html__('Session ID missing.', 'mxchat'));
339 + return;
340 + }
341 +
342 + $session_id = sanitize_text_field($_POST['session_id']);
343 + $word_path = get_transient('mxchat_word_url_' . $session_id);
344 + $filename = get_transient('mxchat_word_filename_' . $session_id);
345 +
346 + if ($word_path && file_exists($word_path) && $filename) {
347 + wp_send_json_success([
348 + 'has_word' => true,
349 + 'filename' => $filename
350 + ]);
351 + } else {
352 + wp_send_json_success([
353 + 'has_word' => false
354 + ]);
355 + }
356 +}
357 +
358 +
412 359 }