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