PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / 2.4.0
Fluent Support – Helpdesk & Customer Support Ticket System v2.4.0
2.4.0 2.3.2 2.3.1 2.3.0 2.2.1 2.2.0 trunk 1.10.0 1.10.1 1.10.2 1.10.3 1.10.4 1.10.5 1.4.0 1.4.1 1.4.2 1.4.5 1.4.6 1.4.7 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 All 68 releases
← All changes | app/Http/Controllers/FluentBotController.php +173 -28 2.2.02.4.0 View file →
@@ -76,11 +76,11 @@
76 76 'configuredProductIds' => array_values(array_unique($configuredProductIds)),
77 77 ];
78 78 }
79 79
80 - public function generateResponse(Request $request)
80 + public function generateResponse(Request $request, $id)
81 81 {
82 - $ticketId = $request->getSafe('id', 'intval');
82 + $ticketId = intval($id);
83 83 $productId = $request->getSafe('product_id', 'intval');
84 84 $prompt = $request->getSafe('content', 'sanitize_text_field');
85 85 $conversationId = $request->getSafe('chat_id', 'sanitize_text_field', '');
86 86 $selectedText = $request->getSafe('selectedText', 'sanitize_text_field', '');
@@ -108,11 +108,11 @@
108 108 }
109 109
110 110
111 111
112 - public function generateStreamResponse(Request $request)
112 + public function generateStreamResponse(Request $request, $id)
113 113 {
114 - $ticketId = $request->getSafe('id', 'intval');
114 + $ticketId = intval($id);
115 115 $productId = $request->getSafe('product_id', 'intval');
116 116 $prompt = $request->getSafe('content', 'sanitize_text_field');
117 117 $selectedText = $request->getSafe('selectedText', 'sanitize_text_field', '');
118 118 $type = $request->getSafe('type', 'sanitize_text_field', 'response');
@@ -125,8 +125,10 @@
125 125 0, self::MAX_SELECTED_CONVERSATIONS
126 126 )
127 127 : null;
128 128 $includeTicketContent = filter_var($request->get('include_ticket_content', true), FILTER_VALIDATE_BOOLEAN);
129 + $webSearch = filter_var($request->get('web_search', false), FILTER_VALIDATE_BOOLEAN);
130 + $temperature = max(0, min(2, floatval($request->get('temperature', 0))));
129 131
130 132 // Cap and sanitize seed messages: only allowed roles, bounded content length, count capped.
131 133 $seedMessagesRaw = array_slice((array) $request->get('conversation_history', []), -self::MAX_SEED_MESSAGES);
132 134 $seedMessages = [];
@@ -208,9 +210,9 @@
208 210 echo "data: Connection established\n\n";
209 211 flush();
210 212
211 213 // Start streaming response
212 - $customAI->generateStreamResponse($prompt, $ticket, $productId, $conversationId ?: null, $selectedConversations, $includeTicketContent, $seedMessages ?: null);
214 + $customAI->generateStreamResponse($prompt, $ticket, $productId, $conversationId ?: null, $selectedConversations, $includeTicketContent, $seedMessages ?: null, $webSearch, $temperature);
213 215
214 216 // Send end event
215 217 echo "event: end\n";
216 218 echo "data: Stream completed\n\n";
@@ -229,12 +231,12 @@
229 231 exit;
230 232 }
231 233 }
232 234
233 - public function getTicketSummary(Request $request)
235 + public function getTicketSummary(Request $request, $id)
234 236 {
235 237 try {
236 - $ticketId = $request->getSafe('id', 'intval');
238 + $ticketId = intval($id);
237 239 $ticket = Ticket::with('responses')->findOrFail($ticketId);
238 240 $this->ensureCanAccessTicket($ticket);
239 241
240 242 return (new FluentBotService())->getTicketSummary($ticket);
@@ -244,12 +246,12 @@
244 246 ]);
245 247 }
246 248 }
247 249
248 - public function getTicketTone(Request $request)
250 + public function getTicketTone(Request $request, $id)
249 251 {
250 252 try {
251 - $ticketId = $request->getSafe('id', 'intval');
253 + $ticketId = intval($id);
252 254 $ticket = Ticket::with('responses')->findOrFail($ticketId);
253 255 $this->ensureCanAccessTicket($ticket);
254 256
255 257 return (new FluentBotService())->getTicketTone($ticket);
@@ -266,11 +268,11 @@
266 268 $this->ensureCanAccessTicket($ticket);
267 269 return $ticket;
268 270 }
269 271
270 - public function getChatId(Request $request)
272 + public function getChatId(Request $request, $id)
271 273 {
272 - $ticketId = $request->getSafe('id', 'intval');
274 + $ticketId = intval($id);
273 275 $this->authorizeTicketAccess($ticketId);
274 276
275 277 $meta = $this->getTicketMeta($ticketId, '_fluent_bot_chat_id');
276 278 $productMeta = $this->getTicketMeta($ticketId, '_fluent_bot_chat_product');
@@ -280,12 +282,59 @@
280 282 'product_id' => $productMeta ? (int) $productMeta->value : null
281 283 ];
282 284 }
283 285
284 - public function getChatMessages(Request $request)
286 + /**
287 + * Reconnect to the ticket's in-flight FluentBot turn and stream its buffered SSE
288 + * to the browser. Used after a refresh or a conversation switch so a turn that
289 + * is still running upstream renders live instead of appearing as an empty panel.
290 + *
291 + * Emits `idle` (via the upstream) when nothing is buffered, so the client can
292 + * stop without special-casing.
293 + */
294 + public function resumeChatStream(Request $request, $id)
285 295 {
286 - $ticketId = $request->getSafe('id', 'intval');
296 + $ticketId = intval($id);
287 297 $this->authorizeTicketAccess($ticketId);
298 +
299 + $meta = $this->getTicketMeta($ticketId, '_fluent_bot_chat_id');
300 + $chatId = $meta ? (string) $meta->value : '';
301 +
302 + // Only ever resume a chat this ticket owns, and only a well-formed id —
303 + // the id is interpolated into the upstream URL.
304 + if (!$chatId || !preg_match(self::CHAT_ID_PATTERN, $chatId)) {
305 + return $this->sendError(['message' => __('No conversation to resume.', 'fluent-support')], 404);
306 + }
307 +
308 + $productMeta = $this->getTicketMeta($ticketId, '_fluent_bot_chat_product');
309 + $productId = $productMeta ? (int) $productMeta->value : $request->getSafe('product_id', 'intval');
310 +
311 + // Prevent WordPress and PHP from flushing/compressing buffers on shutdown
312 + remove_action('shutdown', 'wp_ob_end_flush_all', 1);
313 + @ini_set('zlib.output_compression', 'Off');
314 + @ini_set('output_buffering', 'Off');
315 + @ini_set('output_handler', '');
316 +
317 + // Clear all output buffers for raw SSE streaming
318 + $maxLevels = 10;
319 + while (ob_get_level() && $maxLevels-- > 0) {
320 + @ob_end_clean();
321 + }
322 +
323 + header('Content-Type: text/event-stream');
324 + header('Cache-Control: no-cache');
325 + header('Connection: keep-alive');
326 + header('X-Accel-Buffering: no');
327 +
328 + (new FluentBotService())->resumeChatStream($chatId, $productId);
329 +
330 + exit;
331 + }
332 +
333 + public function getChatMessages(Request $request, $id)
334 + {
335 + $ticketId = intval($id);
336 + $this->authorizeTicketAccess($ticketId);
288 337 $cursor = $request->getSafe('cursor', 'sanitize_text_field', '');
289 338
290 339 $meta = $this->getTicketMeta($ticketId, '_fluent_bot_chat_id');
291 340
@@ -306,14 +355,15 @@
306 355
307 356 return $result;
308 357 }
309 358
310 - public function saveChatId(Request $request)
359 + public function saveChatId(Request $request, $id)
311 360 {
312 - $ticketId = $request->getSafe('id', 'intval');
361 + $ticketId = intval($id);
313 362 $this->authorizeTicketAccess($ticketId);
314 363 $chatId = $request->getSafe('chat_id', 'sanitize_text_field');
315 364 $productId = $request->getSafe('product_id', 'intval', 0);
365 + $title = $request->getSafe('title', 'sanitize_text_field', '');
316 366
317 367 // Validate UUID format
318 368 if (!$chatId || !preg_match(self::CHAT_ID_PATTERN, $chatId)) {
319 369 return $this->sendError(['message' => __('Invalid chat_id format', 'fluent-support')], 422);
@@ -349,8 +399,12 @@
349 399 if ($result) {
350 400 return $this->sendError(['message' => $result['error']], 409);
351 401 }
352 402
403 + // Track the conversation so it appears in the ticket's "Past conversations"
404 + // list. Idempotent per chat_id — repeated saves of the same id are no-ops.
405 + $this->appendChatHistory($ticketId, $chatId, $productId, $title);
406 +
353 407 return [
354 408 'success' => true,
355 409 'chat_id' => $chatId
356 410 ];
@@ -395,11 +449,11 @@
395 449 'key' => $key,
396 450 ])->delete();
397 451 }
398 452
399 - public function deleteChatId(Request $request)
453 + public function deleteChatId(Request $request, $id)
400 454 {
401 - $ticketId = $request->getSafe('id', 'intval');
455 + $ticketId = intval($id);
402 456 $this->authorizeTicketAccess($ticketId);
403 457
404 458 Meta::where('object_type', 'ticket_meta')
405 459 ->where('object_id', $ticketId)
@@ -414,13 +468,102 @@
414 468 'success' => true
415 469 ];
416 470 }
417 471
418 - public function getContextSelection(Request $request)
472 + /**
473 + * List the ticket's past FluentBot conversations (newest first) plus the
474 + * currently-active chat_id, for the "Past conversations" switcher.
475 + */
476 + public function getConversations(Request $request, $id)
419 477 {
420 - $ticketId = $request->getSafe('id', 'intval');
478 + $ticketId = intval($id);
421 479 $this->authorizeTicketAccess($ticketId);
422 480
481 + $active = $this->getTicketMeta($ticketId, '_fluent_bot_chat_id');
482 +
483 + return [
484 + 'conversations' => $this->readChatHistory($ticketId),
485 + 'active_chat_id' => $active ? $active->value : null,
486 + ];
487 + }
488 +
489 + /**
490 + * Make a past conversation the active one so the next message continues it.
491 + * Only a chat_id already recorded in THIS ticket's history may be selected —
492 + * the stream endpoint trusts the stored chat_id, so this is the ownership gate.
493 + */
494 + public function switchConversation(Request $request, $id)
495 + {
496 + $ticketId = intval($id);
497 + $this->authorizeTicketAccess($ticketId);
498 + $chatId = $request->getSafe('chat_id', 'sanitize_text_field');
499 +
500 + if (!$chatId || !preg_match(self::CHAT_ID_PATTERN, $chatId)) {
501 + return $this->sendError(['message' => __('Invalid chat_id format', 'fluent-support')], 422);
502 + }
503 +
504 + $entry = null;
505 + foreach ($this->readChatHistory($ticketId) as $h) {
506 + if (isset($h['chat_id']) && $h['chat_id'] === $chatId) {
507 + $entry = $h;
508 + break;
509 + }
510 + }
511 +
512 + if (!$entry) {
513 + return $this->sendError(['message' => __('Conversation not found for this ticket.', 'fluent-support')], 404);
514 + }
515 +
516 + $productId = (int) ($entry['product_id'] ?? 0);
517 + $this->upsertTicketMeta($ticketId, '_fluent_bot_chat_id', $chatId);
518 + $this->upsertTicketMeta($ticketId, '_fluent_bot_chat_product', $productId);
519 +
520 + return [
521 + 'success' => true,
522 + 'chat_id' => $chatId,
523 + 'product_id' => $productId,
524 + ];
525 + }
526 +
527 + private function readChatHistory($ticketId): array
528 + {
529 + $meta = $this->getTicketMeta($ticketId, '_fluent_bot_chat_history');
530 + $history = ($meta && $meta->value) ? json_decode($meta->value, true) : [];
531 +
532 + return is_array($history) ? array_values($history) : [];
533 + }
534 +
535 + /**
536 + * Prepend a conversation to the ticket's history. Idempotent per chat_id;
537 + * capped so ticket meta cannot grow unbounded.
538 + */
539 + private function appendChatHistory($ticketId, $chatId, $productId, $title = '')
540 + {
541 + $history = $this->readChatHistory($ticketId);
542 +
543 + foreach ($history as $entry) {
544 + if (isset($entry['chat_id']) && $entry['chat_id'] === $chatId) {
545 + return;
546 + }
547 + }
548 +
549 + array_unshift($history, [
550 + 'chat_id' => $chatId,
551 + 'product_id' => (int) $productId,
552 + 'title' => $title !== '' ? $title : __('Conversation', 'fluent-support'),
553 + 'created_at' => time(),
554 + ]);
555 +
556 + $history = array_slice($history, 0, 20);
557 +
558 + $this->upsertTicketMeta($ticketId, '_fluent_bot_chat_history', wp_json_encode($history));
559 + }
560 +
561 + public function getContextSelection(Request $request, $id)
562 + {
563 + $ticketId = intval($id);
564 + $this->authorizeTicketAccess($ticketId);
565 +
423 566 $meta = $this->getTicketMeta($ticketId, '_fluent_bot_context_selection');
424 567
425 568 if (!$meta) {
426 569 return ['data' => null];
@@ -428,11 +571,11 @@
428 571
429 572 return ['data' => Helper::safeUnserialize($meta->value)];
430 573 }
431 574
432 - public function saveContextSelection(Request $request)
575 + public function saveContextSelection(Request $request, $id)
433 576 {
434 - $ticketId = $request->getSafe('id', 'intval');
577 + $ticketId = intval($id);
435 578 $this->authorizeTicketAccess($ticketId);
436 579 $selectedIds = (array) $request->get('selected_ids', []);
437 580 $knownIds = (array) $request->get('known_ids', []);
438 581 $includeTicketContent = filter_var($request->get('include_ticket_content', true), FILTER_VALIDATE_BOOLEAN);
@@ -464,18 +607,19 @@
464 607 'product_id' => $productMeta ? (int) $productMeta->value : 0,
465 608 ];
466 609 }
467 610
468 - public function createFeedback(Request $request)
611 + public function createFeedback(Request $request, $id)
469 612 {
470 - $ticketId = $request->getSafe('id', 'intval');
613 + $ticketId = intval($id);
471 614 $this->authorizeTicketAccess($ticketId);
472 615
473 - $messageId = $request->getSafe('message_id', 'intval');
616 + // fluent-bot Message PKs are UUIDs — sanitize as text, not intval.
617 + $messageId = $request->getSafe('message_id', 'sanitize_text_field');
474 618 $reaction = $request->getSafe('reaction', 'sanitize_text_field');
475 619 $comment = $request->getSafe('comments', 'sanitize_textarea_field', '');
476 620
477 - if (!$messageId || $messageId < 1) {
621 + if (!$messageId || !wp_is_uuid($messageId)) {
478 622 return $this->sendError(['message' => __('Invalid message_id', 'fluent-support')], 422);
479 623 }
480 624
481 625 if (!$reaction || !in_array($reaction, ['positive', 'negative'], true)) {
@@ -496,14 +640,15 @@
496 640
497 641 return $result;
498 642 }
499 643
500 - public function deleteFeedback(Request $request)
644 + public function deleteFeedback(Request $request, $id, $feedback_id)
501 645 {
502 - $ticketId = $request->getSafe('id', 'intval');
646 + $ticketId = intval($id);
503 647 $this->authorizeTicketAccess($ticketId);
504 648
505 - $feedbackId = $request->getSafe('feedback_id', 'intval');
649 + // fluent-bot Feedback PKs are integers (unlike message ids, which are UUIDs).
650 + $feedbackId = intval($feedback_id);
506 651
507 652 if (!$feedbackId || $feedbackId < 1) {
508 653 return $this->sendError(['message' => __('Invalid feedback_id', 'fluent-support')], 422);
509 654 }