PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / trunk
MxChat – AI Chatbot & Content Generation for WordPress vtrunk
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 +411 -381 3.2.11trunk View file →
@@ -1,382 +1,412 @@
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 = sanitize_text_field($_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 = sanitize_text_field($_POST['session_id']);
221 - $word_path = get_transient('mxchat_word_url_' . $session_id);
222 -
223 - if ($word_path && file_exists($word_path)) {
224 - unlink($word_path);
225 - }
226 -
227 - $this->mxchat_clear_word_transients($session_id);
228 -
229 - wp_send_json_success([
230 - 'message' => esc_html__('Document removed successfully.', 'mxchat')
231 - ]);
232 - }
233 -
234 - /**
235 - * Clear all Word-related transients
236 - */
237 - private function mxchat_clear_word_transients($session_id) {
238 - delete_transient('mxchat_word_url_' . $session_id);
239 - delete_transient('mxchat_word_filename_' . $session_id);
240 - delete_transient('mxchat_word_embeddings_' . $session_id);
241 - delete_transient('mxchat_include_word_in_context_' . $session_id);
242 - }
243 -
244 - /**
245 - * Find relevant chunks from the Word document
246 - */
247 - public function mxchat_find_relevant_word_chunks($query_embedding, $embeddings) {
248 - $most_relevant = null;
249 - $highest_similarity = -INF;
250 -
251 - foreach ($embeddings as $chunk_data) {
252 - $similarity = $this->mxchat_calculate_cosine_similarity_word($query_embedding, $chunk_data['embedding']);
253 -
254 - if ($similarity > $highest_similarity) {
255 - $highest_similarity = $similarity;
256 - $most_relevant = $chunk_data['chunk_number'];
257 - }
258 - }
259 -
260 - if (!is_null($most_relevant)) {
261 - $chunk_numbers = range(
262 - max(1, $most_relevant - 1),
263 - min(count($embeddings), $most_relevant + 1)
264 - );
265 - return array_filter($embeddings, function ($chunk) use ($chunk_numbers) {
266 - return in_array($chunk['chunk_number'], $chunk_numbers);
267 - });
268 - }
269 -
270 - return [];
271 - }
272 -
273 - /**
274 - * Handle Word document discussion similar to PDF discussion
275 - */
276 -public function mxchat_handle_word_discussion($message, $user_id, $session_id) {
277 - // Get stored embeddings for the session
278 - $embeddings = get_transient('mxchat_word_embeddings_' . $session_id);
279 - $word_path = get_transient('mxchat_word_url_' . $session_id);
280 -
281 - if (!$embeddings || !$word_path) {
282 - $trigger_text = $this->options['word_intent_trigger_text'] ??
283 - __("Please upload a Word document (.docx) that you'd like to discuss.", 'mxchat');
284 - set_transient('mxchat_waiting_for_word_' . $session_id, true, HOUR_IN_SECONDS);
285 - $this->fallbackResponse['text'] = $trigger_text;
286 - return;
287 - }
288 -
289 - // Set context flag for including Word content in conversation
290 - set_transient('mxchat_include_word_in_context_' . $session_id, true, HOUR_IN_SECONDS);
291 - $this->fallbackResponse['text'] = ''; // Proceed without additional message
292 - }
293 -
294 -
295 - private function mxchat_generate_embedding_word($text, $api_key) {
296 - $endpoint = 'https://api.openai.com/v1/embeddings';
297 -
298 - $body = wp_json_encode([
299 - 'input' => $text,
300 - 'model' => 'text-embedding-ada-002'
301 - ]);
302 -
303 - $args = [
304 - 'body' => $body,
305 - 'headers' => [
306 - 'Content-Type' => 'application/json',
307 - 'Authorization' => 'Bearer ' . $api_key,
308 - ],
309 - 'timeout' => 60,
310 - 'redirection' => 5,
311 - 'blocking' => true,
312 - 'httpversion' => '1.0',
313 - 'sslverify' => true,
314 - ];
315 -
316 - $response = wp_remote_post($endpoint, $args);
317 -
318 - if (is_wp_error($response)) {
319 - return null;
320 - }
321 -
322 - $response_body = json_decode(wp_remote_retrieve_body($response), true);
323 -
324 - if (isset($response_body['data'][0]['embedding']) && is_array($response_body['data'][0]['embedding'])) {
325 - return $response_body['data'][0]['embedding'];
326 - } else {
327 - return null;
328 - }
329 - }
330 -
331 -
332 - private function mxchat_calculate_cosine_similarity_word($vectorA, $vectorB) {
333 - if (!is_array($vectorA) || !is_array($vectorB) || empty($vectorA) || empty($vectorB)) {
334 - return 0;
335 - }
336 -
337 - $dotProduct = array_sum(array_map(function ($a, $b) {
338 - return $a * $b;
339 - }, $vectorA, $vectorB));
340 - $normA = sqrt(array_sum(array_map(function ($a) {
341 - return $a * $a;
342 - }, $vectorA)));
343 - $normB = sqrt(array_sum(array_map(function ($b) {
344 - return $b * $b;
345 - }, $vectorB)));
346 -
347 - if ($normA == 0 || $normB == 0) {
348 - return 0;
349 - }
350 -
351 - return $dotProduct / ($normA * $normB);
352 - }
353 -
354 - /**
355 - * Check the status of a Word document for the current session
356 - */
357 -public function mxchat_check_word_status() {
358 - check_ajax_referer('mxchat_chat_nonce', 'nonce');
359 -
360 - if (empty($_POST['session_id'])) {
361 - wp_send_json_error(esc_html__('Session ID missing.', 'mxchat'));
362 - return;
363 - }
364 -
365 - $session_id = sanitize_text_field($_POST['session_id']);
366 - $word_path = get_transient('mxchat_word_url_' . $session_id);
367 - $filename = get_transient('mxchat_word_filename_' . $session_id);
368 -
369 - if ($word_path && file_exists($word_path) && $filename) {
370 - wp_send_json_success([
371 - 'has_word' => true,
372 - 'filename' => $filename
373 - ]);
374 - } else {
375 - wp_send_json_success([
376 - 'has_word' => false
377 - ]);
378 - }
379 -}
380 -
381 -
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 +
382 412 }