PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.2.16
MxChat – AI Chatbot & Content Generation for WordPress v3.2.16
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
mxchat-basic / js / admin-testing-tab.js

admin-testing-tab.js in MxChat – AI Chatbot & Content Generation for WordPress 3.2.16, at js/admin-testing-tab.js

689 lines 30.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * MxChat Admin Testing Tab
3 * Handles the in-admin testing interface with debug data display.
4 * Ported from test-panel.js but adapted for inline admin use.
5 */
6
7 (function($) {
8 'use strict';
9
10 var AdminTestingPanel = {
11 initialized: false,
12 lastQueryData: null,
13
14 init: function() {
15 if (this.initialized) return;
16
17 // Only initialize when the testing section becomes visible
18 this.bindTabActivation();
19 this.bindActionButtons();
20 this.interceptChatResponses();
21
22 this.initialized = true;
23 this.log('Admin testing panel initialized');
24 },
25
26 // =====================================================
27 // Tab activation - load system info when testing tab is shown
28 // =====================================================
29
30 bindTabActivation: function() {
31 var self = this;
32
33 // Watch for the testing section becoming active
34 var observer = new MutationObserver(function(mutations) {
35 mutations.forEach(function(mutation) {
36 if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
37 var testingSection = document.getElementById('testing');
38 if (testingSection && testingSection.classList.contains('active')) {
39 self.loadSystemInfo();
40 }
41 }
42 });
43 });
44
45 var testingSection = document.getElementById('testing');
46 if (testingSection) {
47 observer.observe(testingSection, { attributes: true });
48
49 // If already active (e.g. direct URL hash)
50 if (testingSection.classList.contains('active')) {
51 self.loadSystemInfo();
52 }
53 }
54 },
55
56 // =====================================================
57 // Action buttons
58 // =====================================================
59
60 bindActionButtons: function() {
61 var self = this;
62
63 $('#mxch-testing-clear-session').on('click', function() {
64 self.clearChatSession();
65 });
66
67 $('#mxch-testing-clear-debug').on('click', function() {
68 self.clearDebugConsole();
69 });
70 },
71
72 // =====================================================
73 // AJAX Interception - capture testing_data from responses
74 // =====================================================
75
76 interceptChatResponses: function() {
77 var self = this;
78
79 // Store reference globally so interception can find us
80 window.mxchatTestPanelInstance = self;
81
82 // Intercept jQuery AJAX calls
83 if (window.jQuery) {
84 var originalAjax = jQuery.ajax;
85
86 jQuery.ajax = function(options) {
87 var originalSuccess = options.success;
88
89 options.success = function(data, textStatus, jqXHR) {
90 // Check if this is a chat request
91 if (options.data &&
92 (options.data.action === 'mxchat_handle_chat_request' ||
93 options.data.action === 'mxchat_stream_chat')) {
94
95 if (self && data && data.testing_data) {
96 self.handleTestingData(data.testing_data);
97 } else if (self && data && data.data && data.data.testing_data) {
98 self.handleTestingData(data.data.testing_data);
99 }
100 }
101
102 if (originalSuccess) {
103 originalSuccess.call(this, data, textStatus, jqXHR);
104 }
105 };
106
107 return originalAjax.call(this, options);
108 };
109 }
110
111 // Intercept fetch API calls (for both JSON and SSE streaming)
112 var originalFetch = window.fetch;
113
114 window.fetch = function() {
115 var args = arguments;
116 return originalFetch.apply(this, args).then(function(response) {
117 if (args[0] && typeof args[0] === 'string' &&
118 (args[0].includes('admin-ajax.php') || args[0].includes('mxchat'))) {
119 var contentType = response.headers.get('content-type') || '';
120
121 if (contentType.includes('application/json')) {
122 // Non-streaming: parse the full JSON response
123 response.clone().json().then(function(data) {
124 if (self && data && data.testing_data) {
125 self.handleTestingData(data.testing_data);
126 } else if (self && data && data.data && data.data.testing_data) {
127 self.handleTestingData(data.data.testing_data);
128 }
129 }).catch(function() {});
130 } else if (contentType.includes('text/event-stream') || contentType.includes('text/plain')) {
131 // Streaming (SSE): read just enough to find the testing_data event
132 // Using clone() so the original stream is unaffected for chat-script.js
133 try {
134 var clonedResponse = response.clone();
135 var reader = clonedResponse.body.getReader();
136 var decoder = new TextDecoder();
137 var sseBuffer = '';
138 var found = false;
139
140 function readChunk() {
141 reader.read().then(function(result) {
142 if (result.done || found) {
143 reader.cancel().catch(function() {});
144 return;
145 }
146
147 sseBuffer += decoder.decode(result.value, { stream: true });
148 var sseLines = sseBuffer.split('\n');
149
150 for (var i = 0; i < sseLines.length; i++) {
151 var sseLine = sseLines[i];
152 if (sseLine.indexOf('data: ') === 0) {
153 var payload = sseLine.substring(6);
154 try {
155 var parsed = JSON.parse(payload);
156 if (parsed && parsed.testing_data) {
157 self.handleTestingData(parsed.testing_data);
158 found = true;
159 reader.cancel().catch(function() {});
160 return;
161 }
162 } catch (e) {}
163 }
164 }
165
166 // Keep reading until we find testing_data or hit a limit
167 if (sseBuffer.length < 50000) {
168 readChunk();
169 } else {
170 reader.cancel().catch(function() {});
171 }
172 }).catch(function() {
173 // Stream read error — safe to ignore
174 });
175 }
176
177 readChunk();
178 } catch (e) {}
179 }
180 }
181 return response;
182 });
183 };
184
185 this.log('Chat interception active');
186 },
187
188 // =====================================================
189 // Handle incoming testing data
190 // =====================================================
191
192 handleTestingData: function(testingData) {
193 this.log('Chat data captured');
194
195 // Update query
196 this.updateLastQuery(testingData.query || 'No query');
197
198 // Update approved URLs
199 this.updateApprovedUrls(testingData.approved_urls || []);
200
201 // Update document matches
202 this.updateTopMatches(
203 testingData.top_matches || [],
204 testingData.similarity_threshold || 0.75,
205 testingData.sources_used || 0,
206 testingData.total_chunks_used || 0
207 );
208
209 // Update action matches
210 this.updateActionMatches(testingData.action_matches || []);
211
212 // Log summary
213 if (testingData.knowledge_base_type) {
214 this.log('Knowledge Base: ' + testingData.knowledge_base_type);
215 }
216 if (testingData.similarity_threshold) {
217 this.log('Similarity Threshold: ' + (testingData.similarity_threshold * 100) + '%');
218 }
219 if (testingData.top_matches && testingData.top_matches.length > 0) {
220 var aboveThreshold = testingData.top_matches.filter(function(m) { return m.above_threshold; }).length;
221 this.log(aboveThreshold + ' above threshold, ' + (testingData.top_matches.length - aboveThreshold) + ' below');
222 this.log('Highest similarity: ' + testingData.top_matches[0].similarity_percentage + '%');
223 } else {
224 this.log('No document matches found');
225 }
226 if (testingData.action_matches && testingData.action_matches.length > 0) {
227 var triggered = testingData.action_matches.find(function(a) { return a.triggered; });
228 if (triggered) {
229 this.log('Action Triggered: ' + triggered.intent_label + ' (' + triggered.similarity_percentage + '%)');
230 } else {
231 this.log('No actions triggered - Highest: ' + testingData.action_matches[0].intent_label + ' (' + testingData.action_matches[0].similarity_percentage + '%)');
232 }
233 }
234 },
235
236 // =====================================================
237 // Update UI sections
238 // =====================================================
239
240 updateLastQuery: function(query) {
241 $('#mxch-testing-last-query').text(query);
242 this.lastQueryData = { query: query, timestamp: new Date() };
243 },
244
245 updateApprovedUrls: function(approvedUrls) {
246 var el = $('#mxch-testing-approved-urls');
247
248 if (!approvedUrls || !Array.isArray(approvedUrls) || approvedUrls.length === 0) {
249 el.html('<div class="mxch-testing-no-data">No approved URLs (AI cannot cite links)</div>');
250 return;
251 }
252
253 var html = '<div class="urls-header"><strong>' + approvedUrls.length + ' URL' + (approvedUrls.length !== 1 ? 's' : '') + ' approved for AI citations</strong></div>';
254
255 approvedUrls.forEach(function(url) {
256 var displayUrl = url;
257 try {
258 var urlObj = new URL(url);
259 displayUrl = urlObj.hostname + urlObj.pathname;
260 } catch (e) {}
261
262 html += '<div class="url-card"><div class="url-line">' +
263 '<span class="url-icon">&#128279;</span>' +
264 '<a href="' + url + '" target="_blank" rel="noopener noreferrer" class="url-link" title="' + url + '">' + displayUrl + '</a>' +
265 '</div></div>';
266 });
267
268 html += '<div class="urls-note">The AI can only cite these URLs. Others are automatically removed from responses.</div>';
269 el.html(html);
270 },
271
272 updateActionMatches: function(actionMatches) {
273 var el = $('#mxch-testing-action-scores');
274
275 if (!actionMatches || actionMatches.length === 0) {
276 el.html('<div class="mxch-testing-no-data">No actions checked</div>');
277 return;
278 }
279
280 var html = '<div class="actions-header"><strong>Top ' + actionMatches.length + ' actions checked</strong></div>';
281
282 actionMatches.forEach(function(action) {
283 var isTriggered = action.triggered;
284 var isAboveThreshold = action.above_threshold;
285 var statusIcon = isTriggered ? '&#127919;' : (isAboveThreshold ? '&#9888;&#65039;' : '&#10060;');
286
287 var statusLabel;
288 if (isTriggered) {
289 statusLabel = 'TRIGGERED';
290 } else if (isAboveThreshold) {
291 statusLabel = 'Above threshold';
292 } else {
293 statusLabel = 'Below threshold';
294 }
295
296 var cardClass = isTriggered ? 'action-triggered' : (isAboveThreshold ? 'action-above-threshold' : 'action-below-threshold');
297
298 html += '<div class="action-card ' + cardClass + '">' +
299 '<div class="action-line">' +
300 '<span class="action-icon">' + statusIcon + '</span>' +
301 '<span class="action-name">' + action.intent_label + '</span>' +
302 '<span class="action-score">' + action.similarity_percentage + '%</span>' +
303 '</div>' +
304 '<div class="action-details">' +
305 '<span class="action-status">' + statusLabel + '</span>' +
306 '<span class="action-threshold">Threshold: ' + action.threshold_percentage + '%</span>' +
307 '</div>' +
308 '</div>';
309 });
310
311 el.html(html);
312 },
313
314 updateTopMatches: function(topMatches, threshold, sourcesUsed, totalChunksUsed) {
315 var el = $('#mxch-testing-similarity-scores');
316
317 if (!topMatches || topMatches.length === 0) {
318 el.html('<div class="mxch-testing-no-data">No similarity data available</div>');
319 return;
320 }
321
322 // Hybrid keyword boost (38ffa1): rows carry matched_via + fused_rank
323 // when hybrid retrieval was on for this test message.
324 var hybridOn = topMatches.some(function(m) { return m.matched_via; });
325
326 // Group matches by source URL
327 var groupedByUrl = {};
328 topMatches.forEach(function(match) {
329 var url = match.source_display || 'Unknown';
330 if (!groupedByUrl[url]) {
331 groupedByUrl[url] = {
332 url: url,
333 isUrl: url.indexOf('http') === 0,
334 bestScore: 0,
335 usedForContext: false,
336 totalChunks: match.total_chunks || 1,
337 matchedChunks: [],
338 isChunked: match.is_chunk || false,
339 bestFusedRank: Infinity,
340 viaSet: {}
341 };
342 }
343
344 if (match.similarity_percentage > groupedByUrl[url].bestScore) {
345 groupedByUrl[url].bestScore = match.similarity_percentage;
346 }
347
348 if (match.used_for_context) {
349 groupedByUrl[url].usedForContext = true;
350 }
351
352 if (match.fused_rank && match.fused_rank < groupedByUrl[url].bestFusedRank) {
353 groupedByUrl[url].bestFusedRank = match.fused_rank;
354 }
355 if (match.matched_via) {
356 groupedByUrl[url].viaSet[match.matched_via] = true;
357 }
358
359 groupedByUrl[url].matchedChunks.push({
360 chunkIndex: match.chunk_index,
361 score: match.similarity_percentage,
362 usedForContext: match.used_for_context,
363 aboveThreshold: match.above_threshold
364 });
365 });
366
367 var urlGroups = Object.values(groupedByUrl).sort(function(a, b) {
368 if (hybridOn && a.bestFusedRank !== b.bestFusedRank) {
369 return a.bestFusedRank - b.bestFusedRank;
370 }
371 return b.bestScore - a.bestScore;
372 });
373 var usedUrlCount = sourcesUsed > 0 ? sourcesUsed : urlGroups.filter(function(g) { return g.usedForContext; }).length;
374 var chunksInfo = totalChunksUsed > 0 ? totalChunksUsed + ' chunks sent to AI' : topMatches.length + ' chunk matches';
375
376 var html = '<div class="matches-header">' +
377 '<strong>' + usedUrlCount + ' source' + (usedUrlCount === 1 ? '' : 's') + ' used for AI context</strong>' +
378 '<span class="matches-subheader">(' + chunksInfo + ')</span>' +
379 '</div>';
380
381 var self = this;
382
383 urlGroups.forEach(function(group, groupIndex) {
384 var cardClass = group.usedForContext ? 'above-threshold' : 'below-threshold';
385 var statusIcon = group.usedForContext ? '&#10003;' : '&#10007;';
386 var contextLabel = group.usedForContext ? 'Used for AI context' : 'Not used';
387
388 var chunkSummary = '';
389 if (group.isChunked && group.totalChunks > 1) {
390 var usedChunkCount = group.matchedChunks.filter(function(c) { return c.usedForContext; }).length;
391 chunkSummary = '<span class="chunk-summary">' + usedChunkCount + '/' + group.totalChunks + ' chunks matched</span>';
392 }
393
394 var hasMultipleChunks = group.matchedChunks.length > 1;
395 var expandToggle = hasMultipleChunks
396 ? '<span class="chunk-expand-toggle" data-group="' + groupIndex + '">&#9654; Show chunks</span>'
397 : '';
398
399 var viaChip = '';
400 if (hybridOn) {
401 var vias = Object.keys(group.viaSet);
402 if (vias.length) {
403 var viaLabel = (vias.length > 1 || vias[0] === 'both') ? 'Both'
404 : (vias[0] === 'keyword' ? 'Keyword' : 'Vector');
405 viaChip = '<span class="mxch-rag-via-chip mxch-rag-via-' + viaLabel.toLowerCase() + '">' + viaLabel + '</span>';
406 }
407 }
408
409 html += '<div class="match-card ' + cardClass + '">' +
410 '<div class="match-header">' +
411 '<div class="match-title">' +
412 '<span class="status-icon">' + statusIcon + '</span>' +
413 '<span class="similarity-score">' + group.bestScore + '%</span>' +
414 viaChip +
415 chunkSummary +
416 '</div>' +
417 '<span class="context-label">' + contextLabel + '</span>' +
418 '</div>' +
419 '<div class="match-source">' +
420 (group.isUrl ?
421 '<span class="source-icon link-icon">&#128279;</span> ' + group.url :
422 '<span class="source-icon doc-icon">&#128196;</span> ' + group.url
423 ) +
424 '</div>' +
425 expandToggle +
426 (hasMultipleChunks ? self.renderChunkDetails(group.matchedChunks, groupIndex) : '') +
427 '</div>';
428 });
429
430 el.html(html);
431
432 // Bind expand toggles
433 el.find('.chunk-expand-toggle').on('click', function() {
434 var groupId = $(this).data('group');
435 var details = el.find('.chunk-details[data-group="' + groupId + '"]');
436 var isExpanded = details.toggleClass('expanded').hasClass('expanded');
437 $(this).html(isExpanded ? '&#9660; Hide chunks' : '&#9654; Show chunks');
438 });
439 },
440
441 renderChunkDetails: function(chunks, groupIndex) {
442 var sortedChunks = chunks.slice().sort(function(a, b) { return (a.chunkIndex || 0) - (b.chunkIndex || 0); });
443
444 var html = '<div class="chunk-details" data-group="' + groupIndex + '">';
445
446 sortedChunks.forEach(function(chunk) {
447 var chunkNum = (chunk.chunkIndex !== null && chunk.chunkIndex !== undefined) ? chunk.chunkIndex + 1 : '?';
448 var statusClass = chunk.usedForContext ? 'chunk-used' : 'chunk-not-used';
449 var statusIcon = chunk.usedForContext ? '&#10003;' : '&#9675;';
450
451 html += '<div class="chunk-detail-row ' + statusClass + '">' +
452 '<span class="chunk-detail-icon">' + statusIcon + '</span>' +
453 '<span class="chunk-detail-num">Chunk ' + chunkNum + '</span>' +
454 '<span class="chunk-detail-score">' + chunk.score + '%</span>' +
455 '</div>';
456 });
457
458 html += '</div>';
459 return html;
460 },
461
462 // =====================================================
463 // System info loading
464 // =====================================================
465
466 loadSystemInfo: function() {
467 this.updateSimilarityThreshold();
468 this.updateSystemPrompt();
469 this.updateKnowledgeBaseStatus();
470 },
471
472 updateSimilarityThreshold: function() {
473 $.post(mxchatAdminTestData.ajaxUrl, {
474 action: 'mxchat_get_similarity_threshold',
475 nonce: mxchatAdminTestData.nonce
476 }).done(function(data) {
477 if (data.success) {
478 $('#mxch-testing-threshold').html('<code>' + data.data.threshold_percentage + '</code>');
479 } else {
480 $('#mxch-testing-threshold').html('<span class="error-text">Error loading threshold</span>');
481 }
482 }).fail(function() {
483 $('#mxch-testing-threshold').html('<span class="error-text">Connection error</span>');
484 });
485 },
486
487 updateSystemPrompt: function() {
488 var self = this;
489 var el = $('#mxch-testing-system-prompt');
490 el.text('Loading system prompt...');
491
492 $.post(mxchatAdminTestData.ajaxUrl, {
493 action: 'mxchat_get_system_info',
494 nonce: mxchatAdminTestData.nonce
495 }).done(function(data) {
496 if (data.success) {
497 el.text(data.data.system_prompt || 'No system prompt configured');
498
499 if (data.data.is_openrouter) {
500 self.log('Model: OpenRouter - ' + data.data.openrouter_model);
501 } else {
502 self.log('Model: ' + data.data.selected_model);
503 }
504
505 var apiStatus = data.data.api_status;
506 var configuredApis = Object.keys(apiStatus).filter(function(key) { return apiStatus[key]; });
507 if (configuredApis.length > 0) {
508 self.log('Configured APIs: ' + configuredApis.join(', '));
509 }
510 } else {
511 el.text('Error loading system prompt');
512 }
513 }).fail(function() {
514 el.text('Connection error');
515 });
516 },
517
518 updateKnowledgeBaseStatus: function() {
519 var el = $('#mxch-testing-kb-status');
520 el.html('Checking...');
521
522 $.post(mxchatAdminTestData.ajaxUrl, {
523 action: 'mxchat_get_kb_status',
524 nonce: mxchatAdminTestData.nonce
525 }).done(function(data) {
526 if (data.success) {
527 var kbData = data.data;
528 el.html('<span class="success-text">&#10003; ' + kbData.status + '</span> (' + kbData.type + ' - ' + kbData.documents + ')');
529 } else {
530 el.html('<span class="error-text">Error loading KB status</span>');
531 }
532 }).fail(function() {
533 el.html('<span class="error-text">Connection error</span>');
534 });
535 },
536
537 // =====================================================
538 // Clear session
539 // =====================================================
540
541 clearChatSession: function() {
542 var self = this;
543
544 // Determine the bot ID for the testing chatbot
545 var botId = 'testing';
546
547 // Get current session ID
548 var cookieName = 'mxchat_session_id_' + botId;
549 var sessionId = this.getCookie(cookieName) || this.getCurrentSessionId(botId);
550
551 if (!sessionId) {
552 this.log('No active session found');
553 return;
554 }
555
556 this.log('Clearing session: ' + sessionId);
557
558 // Use MxChatInstances to properly reset
559 if (typeof MxChatInstances !== 'undefined' && typeof MxChatInstances.resetChatSession === 'function') {
560 MxChatInstances.resetChatSession(botId);
561 }
562
563 // Get new session ID
564 var newSessionId = '';
565 if (typeof MxChatInstances !== 'undefined' && typeof MxChatInstances.getChatSession === 'function') {
566 newSessionId = MxChatInstances.getChatSession(botId);
567 }
568
569 if (!newSessionId) {
570 newSessionId = 'mxchat_chat_' + Math.random().toString(36).substr(2, 9);
571 document.cookie = cookieName + '=' + newSessionId + '; path=/; max-age=86400; SameSite=Lax';
572 }
573
574 // Call backend to clear old session
575 $.post(mxchatAdminTestData.ajaxUrl, {
576 action: 'mxchat_start_fresh_session',
577 nonce: mxchatAdminTestData.nonce,
578 old_session_id: sessionId,
579 new_session_id: newSessionId
580 }).done(function(data) {
581 if (data.success) {
582 self.log('Session cleared successfully');
583
584 // Clear the chat UI for the testing bot
585 var chatBox = document.getElementById('chat-box-testing');
586 if (chatBox) {
587 var messages = chatBox.querySelectorAll('.bot-message, .user-message');
588 messages.forEach(function(msg, index) {
589 // Keep first welcome message
590 if (index === 0 && msg.classList.contains('bot-message')) return;
591 msg.remove();
592 });
593 }
594
595 // Clear input
596 var chatInput = document.getElementById('chat-input-testing');
597 if (chatInput) {
598 chatInput.value = '';
599 }
600
601 // Show popular questions again
602 var pq = document.getElementById('mxchat-popular-questions-testing');
603 if (pq) {
604 pq.style.display = 'block';
605 }
606
607 // Clear debug displays
608 self.updateLastQuery('New session started');
609 self.updateTopMatches([], 0, 0, 0);
610 self.updateApprovedUrls([]);
611 self.updateActionMatches([]);
612
613 self.log('Fresh session started: ' + newSessionId);
614 } else {
615 self.log('Error: ' + (data.data && data.data.message ? data.data.message : 'Unknown error'));
616 }
617 }).fail(function() {
618 self.log('Connection error when clearing session');
619 });
620 },
621
622 // =====================================================
623 // Debug console
624 // =====================================================
625
626 log: function(message) {
627 var consoleEl = document.getElementById('mxch-testing-debug-console');
628 if (!consoleEl) return;
629
630 var timestamp = new Date().toLocaleTimeString();
631 var entry = document.createElement('div');
632 entry.className = 'mxch-testing-debug-entry';
633 entry.innerHTML = '<span class="mxch-testing-debug-timestamp">[' + timestamp + ']</span> ' + message;
634 consoleEl.appendChild(entry);
635 consoleEl.scrollTop = consoleEl.scrollHeight;
636
637 // Keep only last 50 entries
638 var entries = consoleEl.querySelectorAll('.mxch-testing-debug-entry');
639 if (entries.length > 50) {
640 entries[0].remove();
641 }
642 },
643
644 clearDebugConsole: function() {
645 var consoleEl = document.getElementById('mxch-testing-debug-console');
646 if (consoleEl) {
647 consoleEl.innerHTML = '<div class="mxch-testing-debug-entry">Debug console cleared...</div>';
648 }
649 },
650
651 // =====================================================
652 // Helpers
653 // =====================================================
654
655 getCookie: function(name) {
656 var value = '; ' + document.cookie;
657 var parts = value.split('; ' + name + '=');
658 if (parts.length === 2) return parts.pop().split(';').shift();
659 return '';
660 },
661
662 getCurrentSessionId: function(botId) {
663 if (typeof MxChatInstances !== 'undefined' && typeof MxChatInstances.getChatSession === 'function') {
664 var session = MxChatInstances.getChatSession(botId);
665 if (session) return session;
666 }
667
668 var cookieId = this.getCookie('mxchat_session_id_' + botId);
669 if (cookieId) return cookieId;
670
671 var chatInput = document.getElementById('chat-input-' + botId);
672 if (chatInput && chatInput.dataset.sessionId) {
673 return chatInput.dataset.sessionId;
674 }
675
676 return '';
677 }
678 };
679
680 // Initialize when DOM is ready
681 $(document).ready(function() {
682 // Only init if we're on the settings page and the testing section exists
683 if (document.getElementById('testing')) {
684 AdminTestingPanel.init();
685 }
686 });
687
688 })(jQuery);
689