Changeset 3751971 for mxchat-basic/trunk/js/mxchat_transcripts.js
- Timestamp:
- 10/17/2026 01:47:11 PM (11 days ago)
- File:
-
- 1 edited
-
mxchat-basic/trunk/js/mxchat_transcripts.js (modified) (10 diffs)
Legend:
- Unmodified
- Added
- Removed
-
mxchat-basic/trunk/js/mxchat_transcripts.js
r3704282 r3751971 4 4 */ 5 5 jQuery(document).ready(function($) { 6 // Read-only transcripts role (plan 93999a): the server sends can_manage '0' 7 // and the delete controls this script draws (chat-list selection boxes, the 8 // per-lead Delete button) are left out. Administrators get no flag and keep 9 // every control. 10 const canManage = !(typeof mxchatAdmin !== 'undefined' && mxchatAdmin && mxchatAdmin.can_manage === '0'); 11 6 12 // ========================================================================== 7 13 // Sidebar Navigation … … 259 265 // Render a rating badge for a session row. Uses inline SVG so the glyph 260 266 // renders the same across OSes (emoji fonts vary). Tooltip surfaces the 261 // optional feedback text. 267 // optional feedback text. The badge is built with DOM APIs so visitor 268 // feedback only ever reaches the page through setAttribute(), never through 269 // an HTML string (CVE-2026-110662). 270 const RATING_ICON_UP = '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 10v12"/><path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H7"/><path d="M3 10h4"/></svg>'; 271 const RATING_ICON_DOWN = '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 14V2"/><path d="M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H17"/><path d="M21 14h-4"/></svg>'; 272 262 273 function renderRatingBadge(session) { 263 274 const value = (session && typeof session.rating_value === 'number') ? session.rating_value : null; 264 const feedback = (session && session.rating_feedback) ? session.rating_feedback : ''; 275 const feedback = (session && session.rating_feedback) ? String(session.rating_feedback) : ''; 276 const badge = document.createElement('span'); 277 let title; 265 278 if (value === 1) { 266 const title = feedback ? ('Visitor: ' + feedback) : 'Visitor rated this chat positively'; 267 return '<span class="mxch-chat-rating mxch-chat-rating-up" title="' + escapeHtml(title) + '" aria-label="' + escapeHtml(title) + '"><svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 10v12"/><path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H7"/><path d="M3 10h4"/></svg></span>'; 268 } 269 if (value === -1) { 270 const title = feedback ? ('Visitor: ' + feedback) : 'Visitor rated this chat negatively'; 271 return '<span class="mxch-chat-rating mxch-chat-rating-down" title="' + escapeHtml(title) + '" aria-label="' + escapeHtml(title) + '"><svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 14V2"/><path d="M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H17"/><path d="M21 14h-4"/></svg></span>'; 272 } 273 return '<span class="mxch-chat-rating mxch-chat-rating-empty" title="No rating yet" aria-label="No rating yet">—</span>'; 279 badge.className = 'mxch-chat-rating mxch-chat-rating-up'; 280 title = feedback ? ('Visitor: ' + feedback) : 'Visitor rated this chat positively'; 281 badge.innerHTML = RATING_ICON_UP; 282 } else if (value === -1) { 283 badge.className = 'mxch-chat-rating mxch-chat-rating-down'; 284 title = feedback ? ('Visitor: ' + feedback) : 'Visitor rated this chat negatively'; 285 badge.innerHTML = RATING_ICON_DOWN; 286 } else { 287 badge.className = 'mxch-chat-rating mxch-chat-rating-empty'; 288 title = 'No rating yet'; 289 badge.textContent = '—'; 290 } 291 badge.setAttribute('title', title); 292 badge.setAttribute('aria-label', title); 293 return badge.outerHTML; 274 294 } 275 295 … … 408 428 html += ` 409 429 <div class="mxch-chat-item${isActive}${isSelected}" data-session-id="${escapeHtml(session.session_id)}"> 410 <input type="checkbox" class="mxch-chat-checkbox"${isChecked}>430 ${canManage ? `<input type="checkbox" class="mxch-chat-checkbox"${isChecked}>` : ''} 411 431 <div class="mxch-chat-avatar"> 412 432 <span>${escapeHtml(session.initials)}</span> … … 565 585 } 566 586 587 // IP row — the server sends user.ip only when the guest's identifier is a 588 // valid address; a logged-in visitor (identified by login) keeps it hidden. 589 if (data.user && data.user.ip) { 590 $('#mxch-detail-ip').text(data.user.ip); 591 $('#mxch-detail-ip-row').show(); 592 } else { 593 $('#mxch-detail-ip').text('-'); 594 $('#mxch-detail-ip-row').hide(); 595 } 596 567 597 if (data.user.email) { 568 598 $('#mxch-detail-email').text(data.user.email); … … 592 622 } else { 593 623 $('#mxch-clicked-section').hide(); 624 } 625 626 // Chat token total (plan 705b6c): text APIs only; hidden when no reply 627 // recorded usage (every chat saved before 3.2.26). 628 const usageTotal = (data && data.usage_total && data.usage_total.label) ? data.usage_total : null; 629 if (usageTotal) { 630 $('#mxch-conversation-usage-value').text(String(usageTotal.label)); 631 $('#mxch-conversation-usage').attr('title', String(usageTotal.title || '')).prop('hidden', false); 632 } else { 633 $('#mxch-conversation-usage-value').text(''); 634 $('#mxch-conversation-usage').removeAttr('title').prop('hidden', true); 594 635 } 595 636 … … 616 657 ? '<span class="mxch-agent-label">Live Agent</span>' 617 658 : '<span class="mxch-bot-label">AI Assistant</span>'; 659 // Model and tokens of this reply (plan 705b6c), beside the time. 660 // Replies without recorded usage keep the time line alone. 661 const usage = (!isAgent && msg.usage && msg.usage.label) ? msg.usage : null; 662 const timeLine = `<div class="mxch-message-time">${escapeHtml(msg.timestamp)}</div>`; 663 const footer = usage 664 ? `<div class="mxch-message-foot"> 665 ${timeLine} 666 <div class="mxch-message-usage">${usage.model ? `<span class="mxch-message-usage-model">${escapeHtml(usage.model)}</span> ` : ''}<span class="mxch-message-usage-tokens">${escapeHtml(usage.label)}</span></div> 667 </div>` 668 : timeLine; 618 669 messagesHtml += ` 619 670 <div class="mxch-message mxch-message-bot${isAgent ? ' mxch-message-agent' : ''}" data-message-id="${msg.id}"> … … 627 678 </div> 628 679 </div> 629 <div class="mxch-message-time">${escapeHtml(msg.timestamp)}</div>680 ${footer} 630 681 </div> 631 682 `; … … 772 823 const $bubble = $('#mxch-messages-area .mxch-message-bubble').eq(item.index); 773 824 if ($bubble.length) { 774 $bubble.html(item.translated); 825 // A translation is model output from the messages' plain text 826 // (visitor-written), so it is shown as text; line breaks kept. 827 $bubble.html(escapeHtml(item.translated).replace(/\r?\n/g, '<br>')); 775 828 $bubble.addClass('translated'); 776 829 } … … 1250 1303 } 1251 1304 1305 // Escapes for both text and attribute contexts (quotes included), so a value 1306 // can never break out of title="", href="" or data-*="" (CVE-2026-110662). 1252 1307 function escapeHtml(text) { 1253 if (!text) return ''; 1254 const div = document.createElement('div'); 1255 div.textContent = text; 1256 return div.innerHTML; 1308 if (!text && text !== 0) return ''; 1309 return String(text) 1310 .replace(/&/g, '&') 1311 .replace(/</g, '<') 1312 .replace(/>/g, '>') 1313 .replace(/"/g, '"') 1314 .replace(/'/g, '''); 1257 1315 } 1258 1316 … … 1726 1784 </button>` 1727 1785 : ''; 1728 const deleteBtn = `<button type="button" class="mxch-btn mxch-btn-ghost mxch-btn-sm mxch-btn-danger-ghost mxch-leads-delete-row" data-email="${escapeHtmlLeads(emailKey)}" title="Delete lead">1786 const deleteBtn = !canManage ? '' : `<button type="button" class="mxch-btn mxch-btn-ghost mxch-btn-sm mxch-btn-danger-ghost mxch-leads-delete-row" data-email="${escapeHtmlLeads(emailKey)}" title="Delete lead"> 1729 1787 <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg> 1730 1788 </button>`;
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)