Changeset 3446370
- Timestamp:
- 01/25/2026 02:15:03 AM (4 weeks ago)
- Location:
- mxchat-basic
- Files:
-
- 127 added
- 10 edited
-
tags/3.0.4 (added)
-
tags/3.0.4/admin (added)
-
tags/3.0.4/admin/class-ajax-handler.php (added)
-
tags/3.0.4/admin/class-knowledge-manager.php (added)
-
tags/3.0.4/admin/class-pinecone-manager.php (added)
-
tags/3.0.4/css (added)
-
tags/3.0.4/css/actions.css (added)
-
tags/3.0.4/css/admin-add-ons.css (added)
-
tags/3.0.4/css/admin-pro.css (added)
-
tags/3.0.4/css/admin-sidebar.css (added)
-
tags/3.0.4/css/admin-style.css (added)
-
tags/3.0.4/css/chat-style.css (added)
-
tags/3.0.4/css/chat-transcripts.css (added)
-
tags/3.0.4/css/content-selector.css (added)
-
tags/3.0.4/css/intent-style.css (added)
-
tags/3.0.4/css/knowledge-style.css (added)
-
tags/3.0.4/css/test-panel.css (added)
-
tags/3.0.4/images (added)
-
tags/3.0.4/images/Icon-01.svg (added)
-
tags/3.0.4/images/Icon-02.svg (added)
-
tags/3.0.4/images/Icon-03.svg (added)
-
tags/3.0.4/images/Icon-04.svg (added)
-
tags/3.0.4/images/addons (added)
-
tags/3.0.4/images/addons/mxchat-admin-assistant.png (added)
-
tags/3.0.4/images/addons/mxchat-assistant-api.png (added)
-
tags/3.0.4/images/addons/mxchat-forms.png (added)
-
tags/3.0.4/images/addons/mxchat-image-analysis.png (added)
-
tags/3.0.4/images/addons/mxchat-moderation.png (added)
-
tags/3.0.4/images/addons/mxchat-multi-bot.png (added)
-
tags/3.0.4/images/addons/mxchat-perplexity.png (added)
-
tags/3.0.4/images/addons/mxchat-theme.png (added)
-
tags/3.0.4/images/addons/mxchat-trigger.png (added)
-
tags/3.0.4/images/addons/mxchat-veo.png (added)
-
tags/3.0.4/images/addons/mxchat-woo.png (added)
-
tags/3.0.4/images/icon-128x128.png (added)
-
tags/3.0.4/images/pro-only-dark.png (added)
-
tags/3.0.4/includes (added)
-
tags/3.0.4/includes/admin-actions-page.php (added)
-
tags/3.0.4/includes/admin-knowledge-page.php (added)
-
tags/3.0.4/includes/admin-pro-page.php (added)
-
tags/3.0.4/includes/admin-settings-page.php (added)
-
tags/3.0.4/includes/admin-transcripts-page.php (added)
-
tags/3.0.4/includes/class-mxchat-addons.php (added)
-
tags/3.0.4/includes/class-mxchat-admin.php (added)
-
tags/3.0.4/includes/class-mxchat-chunker.php (added)
-
tags/3.0.4/includes/class-mxchat-integrator.php (added)
-
tags/3.0.4/includes/class-mxchat-meta-box.php (added)
-
tags/3.0.4/includes/class-mxchat-public.php (added)
-
tags/3.0.4/includes/class-mxchat-user.php (added)
-
tags/3.0.4/includes/class-mxchat-utils.php (added)
-
tags/3.0.4/includes/class-mxchat-woocommerce.php (added)
-
tags/3.0.4/includes/class-mxchat-word-handler.php (added)
-
tags/3.0.4/includes/pdf-parser (added)
-
tags/3.0.4/includes/pdf-parser/alt_autoload.php (added)
-
tags/3.0.4/includes/pdf-parser/src (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/Config.php (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/Document.php (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/Element (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/Element.php (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/Element/ElementArray.php (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/Element/ElementBoolean.php (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/Element/ElementDate.php (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/Element/ElementHexa.php (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/Element/ElementMissing.php (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/Element/ElementName.php (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/Element/ElementNull.php (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/Element/ElementNumeric.php (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/Element/ElementString.php (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/Element/ElementStruct.php (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/Element/ElementXRef.php (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/Encoding (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/Encoding.php (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/Encoding/AbstractEncoding.php (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/Encoding/EncodingLocator.php (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/Encoding/ISOLatin1Encoding.php (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/Encoding/ISOLatin9Encoding.php (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/Encoding/MacRomanEncoding.php (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/Encoding/PDFDocEncoding.php (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/Encoding/PostScriptGlyphs.php (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/Encoding/StandardEncoding.php (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/Encoding/WinAnsiEncoding.php (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/Exception (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/Exception/EmptyPdfException.php (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/Exception/EncodingNotFoundException.php (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/Exception/MissingPdfHeaderException.php (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/Exception/NotImplementedException.php (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/Font (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/Font.php (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/Font/FontCIDFontType0.php (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/Font/FontCIDFontType2.php (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/Font/FontTrueType.php (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/Font/FontType0.php (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/Font/FontType1.php (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/Font/FontType3.php (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/Header.php (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/PDFObject.php (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/Page.php (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/Pages.php (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/Parser.php (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/RawData (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/RawData/FilterHelper.php (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/RawData/RawDataParser.php (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/XObject (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/XObject/Form.php (added)
-
tags/3.0.4/includes/pdf-parser/src/Smalot/PdfParser/XObject/Image.php (added)
-
tags/3.0.4/js (added)
-
tags/3.0.4/js/activation-script.js (added)
-
tags/3.0.4/js/admin-status.js (added)
-
tags/3.0.4/js/chat-script.js (added)
-
tags/3.0.4/js/content-selector.js (added)
-
tags/3.0.4/js/embedding-check.js (added)
-
tags/3.0.4/js/floating-script.js (added)
-
tags/3.0.4/js/knowledge-processing.js (added)
-
tags/3.0.4/js/meta-box.js (added)
-
tags/3.0.4/js/mxchat-admin.js (added)
-
tags/3.0.4/js/mxchat-test-streaming.js (added)
-
tags/3.0.4/js/mxchat_actions.js (added)
-
tags/3.0.4/js/mxchat_pro.js (added)
-
tags/3.0.4/js/mxchat_transcripts.js (added)
-
tags/3.0.4/js/my-color-picker.js (added)
-
tags/3.0.4/js/test-panel.js (added)
-
tags/3.0.4/languages (added)
-
tags/3.0.4/languages/mxchat.pot (added)
-
tags/3.0.4/mxchat-basic.php (added)
-
tags/3.0.4/readme.txt (added)
-
trunk/admin/class-ajax-handler.php (modified) (1 diff)
-
trunk/css/chat-transcripts.css (modified) (2 diffs)
-
trunk/includes/admin-transcripts-page.php (modified) (2 diffs)
-
trunk/includes/class-mxchat-admin.php (modified) (35 diffs)
-
trunk/includes/class-mxchat-integrator.php (modified) (8 diffs)
-
trunk/js/chat-script.js (modified) (1 diff)
-
trunk/js/mxchat-admin.js (modified) (2 diffs)
-
trunk/js/mxchat_transcripts.js (modified) (6 diffs)
-
trunk/mxchat-basic.php (modified) (4 diffs)
-
trunk/readme.txt (modified) (5 diffs)
Legend:
- Unmodified
- Added
- Removed
-
mxchat-basic/trunk/admin/class-ajax-handler.php
r3444379 r3446370 90 90 //error_log('MXChat Save: Checking against whitelist'); 91 91 $allowed_models = array( 92 'gemini-3-pro-preview', 'gemini-3-flash-preview', 'gemini-2.5-pro', 'gemini-2.5-flash', 'gemini-2.5-flash-lite', 92 93 'gemini-2.0-flash', 'gemini-2.0-flash-lite', 'gemini-1.5-pro', 'gemini-1.5-flash', 93 94 'grok-4-0709', 'grok-4-1-fast-reasoning', 'grok-4-1-fast-non-reasoning', 'grok-3-beta', 'grok-3-fast-beta', 'grok-3-mini-beta', -
mxchat-basic/trunk/css/chat-transcripts.css
r3439963 r3446370 949 949 padding: 8px 12px; 950 950 font-size: 12px; 951 } 952 953 /* Translate Controls */ 954 .mxch-translate-section { 955 margin-top: 12px; 956 padding-top: 12px; 957 border-top: 1px solid #f3f4f6; 958 } 959 960 .mxch-translate-controls { 961 display: flex; 962 align-items: center; 963 gap: 8px; 964 } 965 966 .mxch-translate-select { 967 padding: 6px 10px; 968 font-size: 12px; 969 border: 1px solid #e5e7eb; 970 border-radius: 6px; 971 background: #fff; 972 color: #374151; 973 cursor: pointer; 974 min-width: 100px; 975 } 976 977 .mxch-translate-select:focus { 978 outline: none; 979 border-color: #3b82f6; 980 box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1); 981 } 982 983 #mxch-translate-btn, 984 #mxch-show-original-btn { 985 display: inline-flex; 986 align-items: center; 987 gap: 6px; 988 } 989 990 #mxch-translate-btn svg, 991 #mxch-show-original-btn svg { 992 width: 14px; 993 height: 14px; 994 flex-shrink: 0; 995 } 996 997 #mxch-translate-btn:disabled { 998 opacity: 0.6; 999 cursor: not-allowed; 1000 } 1001 1002 .mxch-translate-spinner { 1003 animation: spin 1s linear infinite; 1004 } 1005 1006 @keyframes mxch-translate-spin { 1007 from { transform: rotate(0deg); } 1008 to { transform: rotate(360deg); } 1009 } 1010 1011 /* Translated message indicator */ 1012 .mxch-message-bubble.translated { 1013 position: relative; 1014 } 1015 1016 .mxch-message-bubble.translated::after { 1017 content: ''; 1018 position: absolute; 1019 top: 6px; 1020 right: 6px; 1021 width: 6px; 1022 height: 6px; 1023 background: #3b82f6; 1024 border-radius: 50%; 1025 opacity: 0.6; 951 1026 } 952 1027 … … 1604 1679 } 1605 1680 1681 /* Context Modal Tabs */ 1682 .mxch-context-tabs { 1683 display: flex; 1684 gap: 4px; 1685 padding: 4px; 1686 background: #f1f5f9; 1687 border-radius: var(--mxch-radius-md); 1688 margin-bottom: var(--mxch-spacing-md); 1689 } 1690 1691 .mxch-context-tab { 1692 flex: 1; 1693 display: flex; 1694 align-items: center; 1695 justify-content: center; 1696 gap: 8px; 1697 padding: 10px 16px; 1698 border: none; 1699 background: transparent; 1700 border-radius: var(--mxch-radius-sm); 1701 font-size: 13px; 1702 font-weight: 500; 1703 color: var(--mxch-text-secondary); 1704 cursor: pointer; 1705 transition: all var(--mxch-transition-fast); 1706 } 1707 1708 .mxch-context-tab:hover { 1709 color: var(--mxch-text-primary); 1710 background: rgba(255, 255, 255, 0.5); 1711 } 1712 1713 .mxch-context-tab.active { 1714 background: white; 1715 color: var(--mxch-primary); 1716 box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 1717 } 1718 1719 .mxch-context-tab svg { 1720 width: 16px; 1721 height: 16px; 1722 flex-shrink: 0; 1723 } 1724 1725 .mxch-tab-badge { 1726 display: inline-flex; 1727 align-items: center; 1728 justify-content: center; 1729 min-width: 20px; 1730 height: 20px; 1731 padding: 0 6px; 1732 background: rgba(120, 115, 245, 0.1); 1733 color: var(--mxch-primary); 1734 font-size: 11px; 1735 font-weight: 600; 1736 border-radius: 10px; 1737 } 1738 1739 .mxch-context-tab.active .mxch-tab-badge { 1740 background: var(--mxch-primary); 1741 color: white; 1742 } 1743 1744 .mxch-tab-content { 1745 min-height: 200px; 1746 } 1747 1748 /* Action Specific Styles */ 1749 .mxch-action-triggered { 1750 border-color: rgba(245, 158, 11, 0.4); 1751 background: rgba(245, 158, 11, 0.08); 1752 } 1753 1754 .mxch-action-threshold-badge { 1755 font-size: 11px; 1756 padding: 2px 8px; 1757 background: rgba(100, 116, 139, 0.1); 1758 color: var(--mxch-text-secondary); 1759 border-radius: 10px; 1760 } 1761 1762 .mxch-rag-match-status.status-triggered { 1763 color: #f59e0b; 1764 font-weight: 600; 1765 } 1766 1767 .mxch-action-details { 1768 margin-top: var(--mxch-spacing-sm); 1769 } 1770 1771 .mxch-action-label { 1772 font-size: 14px; 1773 font-weight: 600; 1774 color: var(--mxch-text-primary); 1775 margin-bottom: 4px; 1776 } 1777 1778 .mxch-action-callback { 1779 font-size: 12px; 1780 color: var(--mxch-text-muted); 1781 font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace; 1782 } 1783 1784 .mxch-action-callback-label { 1785 color: var(--mxch-text-secondary); 1786 font-family: inherit; 1787 } 1788 1789 /* Action Score Bar Visualization */ 1790 .mxch-action-score-bar { 1791 position: relative; 1792 height: 6px; 1793 background: #e2e8f0; 1794 border-radius: 3px; 1795 margin-top: var(--mxch-spacing-sm); 1796 overflow: visible; 1797 } 1798 1799 .mxch-action-score-fill { 1800 position: absolute; 1801 left: 0; 1802 top: 0; 1803 height: 100%; 1804 background: linear-gradient(90deg, #7873f5, #a5a1fa); 1805 border-radius: 3px; 1806 transition: width 0.3s ease; 1807 } 1808 1809 .mxch-action-triggered .mxch-action-score-fill { 1810 background: linear-gradient(90deg, #f59e0b, #fbbf24); 1811 } 1812 1813 .mxch-action-threshold-marker { 1814 position: absolute; 1815 top: -4px; 1816 width: 2px; 1817 height: 14px; 1818 background: #ef4444; 1819 border-radius: 1px; 1820 transform: translateX(-50%); 1821 } 1822 1823 .mxch-action-threshold-marker::after { 1824 content: ''; 1825 position: absolute; 1826 top: -3px; 1827 left: 50%; 1828 transform: translateX(-50%); 1829 width: 0; 1830 height: 0; 1831 border-left: 4px solid transparent; 1832 border-right: 4px solid transparent; 1833 border-top: 4px solid #ef4444; 1834 } 1835 1836 /* No Results State */ 1837 .mxch-no-results { 1838 text-align: center; 1839 padding: var(--mxch-spacing-xl); 1840 color: var(--mxch-text-secondary); 1841 } 1842 1843 .mxch-no-results p { 1844 margin: 0; 1845 } 1846 1606 1847 /* ========================================================================== 1607 1848 Responsive Adjustments -
mxchat-basic/trunk/includes/admin-transcripts-page.php
r3439963 r3446370 478 478 </button> 479 479 </div> 480 <div class="mxch-translate-section"> 481 <div class="mxch-translate-controls"> 482 <select id="mxch-translate-lang" class="mxch-translate-select" title="<?php esc_attr_e('Target language', 'mxchat'); ?>"> 483 <option value="en">English</option> 484 <option value="es">Español</option> 485 <option value="fr">Français</option> 486 <option value="de">Deutsch</option> 487 <option value="it">Italiano</option> 488 <option value="pt">Português</option> 489 <option value="nl">Nederlands</option> 490 <option value="ru">Русский</option> 491 <option value="zh">中文</option> 492 <option value="ja">日本語</option> 493 <option value="ko">한국어</option> 494 <option value="ar">العربية</option> 495 <option value="hi">हिन्दी</option> 496 <option value="tr">Türkçe</option> 497 <option value="pl">Polski</option> 498 <option value="vi">Tiếng Việt</option> 499 <option value="th">ไทย</option> 500 <option value="id">Bahasa Indonesia</option> 501 <option value="sv">Svenska</option> 502 <option value="da">Dansk</option> 503 </select> 504 <button type="button" id="mxch-translate-btn" class="mxch-btn mxch-btn-primary mxch-btn-sm"> 505 <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"><path d="m5 8 6 6"/><path d="m4 14 6-6 2-3"/><path d="M2 5h12"/><path d="M7 2h1"/><path d="m22 22-5-10-5 10"/><path d="M14 18h6"/></svg> 506 <span class="mxch-translate-text"><?php esc_html_e('Translate', 'mxchat'); ?></span> 507 </button> 508 <button type="button" id="mxch-show-original-btn" class="mxch-btn mxch-btn-secondary mxch-btn-sm" style="display: none;"> 509 <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"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg> 510 <?php esc_html_e('Original', 'mxchat'); ?> 511 </button> 512 </div> 513 </div> 480 514 </div> 481 515 … … 635 669 <div class="mxch-modal-content"> 636 670 <div class="mxch-modal-header"> 637 <h2><?php esc_html_e(' Retrieved Documents', 'mxchat'); ?></h2>671 <h2><?php esc_html_e('Message Context', 'mxchat'); ?></h2> 638 672 <button type="button" class="mxch-modal-close">×</button> 639 673 </div> 640 674 <div class="mxch-modal-body"> 675 <!-- Tabs --> 676 <div class="mxch-context-tabs"> 677 <button type="button" class="mxch-context-tab active" data-tab="sources"> 678 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg> 679 <?php esc_html_e('Sources', 'mxchat'); ?> 680 <span class="mxch-tab-badge" id="mxch-sources-count" style="display: none;">0</span> 681 </button> 682 <button type="button" class="mxch-context-tab" data-tab="actions"> 683 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg> 684 <?php esc_html_e('Actions', 'mxchat'); ?> 685 <span class="mxch-tab-badge" id="mxch-actions-count" style="display: none;">0</span> 686 </button> 687 </div> 688 641 689 <div class="mxch-rag-loading" style="display: none;"> 642 690 <span class="spinner is-active"></span> 643 <?php esc_html_e('Loading document context...', 'mxchat'); ?> 644 </div> 645 <div class="mxch-rag-content"> 646 <!-- RAG context will be populated via JavaScript --> 691 <?php esc_html_e('Loading context...', 'mxchat'); ?> 692 </div> 693 694 <!-- Sources Tab Content --> 695 <div class="mxch-tab-content" id="mxch-tab-sources"> 696 <div class="mxch-rag-content"> 697 <!-- RAG context will be populated via JavaScript --> 698 </div> 699 </div> 700 701 <!-- Actions Tab Content --> 702 <div class="mxch-tab-content" id="mxch-tab-actions" style="display: none;"> 703 <div class="mxch-actions-content"> 704 <!-- Action scores will be populated via JavaScript --> 705 </div> 647 706 </div> 648 707 </div> -
mxchat-basic/trunk/includes/class-mxchat-admin.php
r3444379 r3446370 84 84 // Slack test connection 85 85 add_action('wp_ajax_mxchat_test_slack_connection', array($this, 'mxchat_test_slack_connection')); 86 87 // Translation handlers 88 add_action('wp_ajax_mxchat_translate_messages', array($this, 'mxchat_translate_messages')); 89 add_action('wp_ajax_mxchat_get_transcript_translation', array($this, 'mxchat_get_transcript_translation')); 86 90 } 87 91 … … 1380 1384 global $wpdb; 1381 1385 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts'; 1382 1386 $translations_table = $wpdb->prefix . 'mxchat_transcript_translations'; 1387 1383 1388 // Calculate the cutoff timestamp 1384 1389 $cutoff_date = date('Y-m-d H:i:s', strtotime("-{$days} days")); 1385 1390 1391 // First, get the session IDs that will be deleted (to clean up translations too) 1392 $sessions_to_delete = $wpdb->get_col( 1393 $wpdb->prepare( 1394 "SELECT DISTINCT session_id FROM {$table_name} WHERE timestamp < %s", 1395 $cutoff_date 1396 ) 1397 ); 1398 1386 1399 // Delete transcripts older than the cutoff date 1387 1400 $deleted = $wpdb->query( 1388 1401 $wpdb->prepare( 1389 "DELETE FROM {$table_name} WHERE created_at< %s",1402 "DELETE FROM {$table_name} WHERE timestamp < %s", 1390 1403 $cutoff_date 1391 1404 ) 1392 1405 ); 1393 1406 1407 // Also delete any translations for the deleted sessions 1408 if (!empty($sessions_to_delete) && $wpdb->get_var("SHOW TABLES LIKE '$translations_table'") === $translations_table) { 1409 $placeholders = implode(',', array_fill(0, count($sessions_to_delete), '%s')); 1410 $wpdb->query( 1411 $wpdb->prepare( 1412 "DELETE FROM {$translations_table} WHERE session_id IN ($placeholders)", 1413 $sessions_to_delete 1414 ) 1415 ); 1416 } 1417 1394 1418 // Log the cleanup action 1395 1419 if ($deleted !== false && $deleted > 0) { … … 1458 1482 } 1459 1483 1484 /** 1485 * Handle translation of chat messages via AJAX 1486 */ 1487 public function mxchat_translate_messages() { 1488 if (!current_user_can('manage_options')) { 1489 wp_send_json_error(['error' => 'Insufficient permissions']); 1490 wp_die(); 1491 } 1492 1493 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : ''; 1494 $target_lang = isset($_POST['target_lang']) ? sanitize_text_field($_POST['target_lang']) : 'en'; 1495 $messages_json = isset($_POST['messages']) ? wp_unslash($_POST['messages']) : '[]'; 1496 $messages = json_decode($messages_json, true); 1497 1498 if (empty($session_id)) { 1499 wp_send_json_error(['error' => 'No session ID provided']); 1500 wp_die(); 1501 } 1502 1503 if (empty($messages) || !is_array($messages)) { 1504 wp_send_json_error(['error' => 'No messages to translate']); 1505 wp_die(); 1506 } 1507 1508 // Language names for prompting 1509 $languages = [ 1510 'en' => 'English', 1511 'es' => 'Spanish', 1512 'fr' => 'French', 1513 'de' => 'German', 1514 'it' => 'Italian', 1515 'pt' => 'Portuguese', 1516 'nl' => 'Dutch', 1517 'ru' => 'Russian', 1518 'zh' => 'Chinese', 1519 'ja' => 'Japanese', 1520 'ko' => 'Korean', 1521 'ar' => 'Arabic', 1522 'hi' => 'Hindi', 1523 'tr' => 'Turkish', 1524 'pl' => 'Polish', 1525 'vi' => 'Vietnamese', 1526 'th' => 'Thai', 1527 'id' => 'Indonesian', 1528 'sv' => 'Swedish', 1529 'da' => 'Danish' 1530 ]; 1531 1532 $target_lang_name = isset($languages[$target_lang]) ? $languages[$target_lang] : 'English'; 1533 1534 // Build combined text for translation (numbered for parsing) 1535 $numbered_messages = []; 1536 foreach ($messages as $i => $msg) { 1537 $content = isset($msg['content']) ? trim($msg['content']) : ''; 1538 if (!empty($content)) { 1539 $numbered_messages[] = "[MSG" . $i . "]" . $content . "[/MSG" . $i . "]"; 1540 } 1541 } 1542 1543 if (empty($numbered_messages)) { 1544 wp_send_json_error(['error' => 'No valid messages to translate']); 1545 wp_die(); 1546 } 1547 1548 $combined_text = implode("\n\n", $numbered_messages); 1549 1550 // Prepare the translation prompt 1551 $system_prompt = "You are a translator. Translate the following messages to {$target_lang_name}. Keep the [MSG#] and [/MSG#] tags exactly as they are - only translate the content between them. Maintain the original formatting, line breaks, and any HTML tags. Return ONLY the translated messages with the tags, no explanations."; 1552 1553 // Get user's selected model and determine provider 1554 $options = get_option('mxchat_options', []); 1555 $selected_model = $options['model'] ?? 'gpt-4o'; 1556 1557 // Check if using OpenRouter 1558 if ($selected_model === 'openrouter') { 1559 $provider = 'openrouter'; 1560 $selected_model = $options['openrouter_selected_model'] ?? ''; 1561 $api_key = $options['openrouter_api_key'] ?? ''; 1562 1563 if (empty($selected_model)) { 1564 wp_send_json_error(['error' => 'No OpenRouter model selected']); 1565 wp_die(); 1566 } 1567 } else { 1568 // Determine provider from model name 1569 $model_parts = explode('-', $selected_model); 1570 $provider = strtolower($model_parts[0]); 1571 1572 // Get the appropriate API key based on provider 1573 $api_key = ''; 1574 switch ($provider) { 1575 case 'gpt': 1576 case 'o1': 1577 $api_key = $options['api_key'] ?? ''; 1578 break; 1579 case 'claude': 1580 $api_key = $options['claude_api_key'] ?? ''; 1581 break; 1582 case 'grok': 1583 $api_key = $options['xai_api_key'] ?? ''; 1584 break; 1585 case 'deepseek': 1586 $api_key = $options['deepseek_api_key'] ?? ''; 1587 break; 1588 case 'gemini': 1589 $api_key = $options['gemini_api_key'] ?? ''; 1590 break; 1591 default: 1592 // Default to OpenAI 1593 $api_key = $options['api_key'] ?? ''; 1594 $provider = 'gpt'; 1595 break; 1596 } 1597 } 1598 1599 if (empty($api_key)) { 1600 wp_send_json_error(['error' => 'No API key configured for ' . $provider]); 1601 wp_die(); 1602 } 1603 1604 // Make API request based on provider 1605 $response = $this->translate_with_provider($provider, $selected_model, $api_key, $system_prompt, $combined_text); 1606 1607 if (is_wp_error($response)) { 1608 wp_send_json_error(['error' => $response->get_error_message()]); 1609 wp_die(); 1610 } 1611 1612 // Parse the response to extract translated messages 1613 $translations = []; 1614 foreach ($messages as $msg) { 1615 $index = $msg['index']; 1616 $pattern = '/\[MSG' . $index . '\](.*?)\[\/MSG' . $index . '\]/s'; 1617 if (preg_match($pattern, $response, $matches)) { 1618 $translations[] = [ 1619 'index' => $index, 1620 'translated' => trim($matches[1]) 1621 ]; 1622 } 1623 } 1624 1625 // Save translations to database 1626 if (!empty($translations)) { 1627 $this->save_transcript_translation($session_id, $target_lang, $translations); 1628 } 1629 1630 wp_send_json(['success' => true, 'translations' => $translations, 'language' => $target_lang]); 1631 wp_die(); 1632 } 1633 1634 /** 1635 * Save transcript translation to database 1636 */ 1637 private function save_transcript_translation($session_id, $language_code, $translations) { 1638 global $wpdb; 1639 $table_name = $wpdb->prefix . 'mxchat_transcript_translations'; 1640 1641 // Check if table exists, create if not 1642 if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") !== $table_name) { 1643 mxchat_create_translations_table(); 1644 } 1645 1646 $now = current_time('mysql'); 1647 $translations_json = wp_json_encode($translations); 1648 1649 // Use REPLACE to insert or update 1650 $wpdb->query($wpdb->prepare( 1651 "REPLACE INTO $table_name (session_id, language_code, translations, created_at, updated_at) 1652 VALUES (%s, %s, %s, %s, %s)", 1653 $session_id, 1654 $language_code, 1655 $translations_json, 1656 $now, 1657 $now 1658 )); 1659 } 1660 1661 /** 1662 * Get saved translation for a session 1663 */ 1664 public function mxchat_get_transcript_translation() { 1665 if (!current_user_can('manage_options')) { 1666 wp_send_json_error(['error' => 'Insufficient permissions']); 1667 wp_die(); 1668 } 1669 1670 $session_id = isset($_POST['session_id']) ? sanitize_text_field($_POST['session_id']) : ''; 1671 1672 if (empty($session_id)) { 1673 wp_send_json_error(['error' => 'No session ID provided']); 1674 wp_die(); 1675 } 1676 1677 global $wpdb; 1678 $table_name = $wpdb->prefix . 'mxchat_transcript_translations'; 1679 1680 // Check if table exists 1681 if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") !== $table_name) { 1682 wp_send_json(['success' => true, 'has_translation' => false]); 1683 wp_die(); 1684 } 1685 1686 // Get the most recent translation for this session 1687 $result = $wpdb->get_row($wpdb->prepare( 1688 "SELECT language_code, translations FROM $table_name WHERE session_id = %s ORDER BY updated_at DESC LIMIT 1", 1689 $session_id 1690 )); 1691 1692 if ($result) { 1693 $translations = json_decode($result->translations, true); 1694 wp_send_json([ 1695 'success' => true, 1696 'has_translation' => true, 1697 'language' => $result->language_code, 1698 'translations' => $translations 1699 ]); 1700 } else { 1701 wp_send_json(['success' => true, 'has_translation' => false]); 1702 } 1703 wp_die(); 1704 } 1705 1706 /** 1707 * Translate text using the user's selected provider and model 1708 */ 1709 private function translate_with_provider($provider, $model, $api_key, $system_prompt, $text) { 1710 switch ($provider) { 1711 case 'claude': 1712 return $this->translate_with_claude($api_key, $model, $system_prompt, $text); 1713 case 'grok': 1714 return $this->translate_with_xai($api_key, $model, $system_prompt, $text); 1715 case 'deepseek': 1716 return $this->translate_with_deepseek($api_key, $model, $system_prompt, $text); 1717 case 'gemini': 1718 return $this->translate_with_gemini($api_key, $model, $system_prompt, $text); 1719 case 'openrouter': 1720 return $this->translate_with_openrouter($api_key, $model, $system_prompt, $text); 1721 case 'gpt': 1722 case 'o1': 1723 default: 1724 return $this->translate_with_openai($api_key, $model, $system_prompt, $text); 1725 } 1726 } 1727 1728 /** 1729 * Translate text using OpenAI API 1730 */ 1731 private function translate_with_openai($api_key, $model, $system_prompt, $text) { 1732 $response = wp_remote_post('https://api.openai.com/v1/chat/completions', [ 1733 'timeout' => 60, 1734 'headers' => [ 1735 'Authorization' => 'Bearer ' . $api_key, 1736 'Content-Type' => 'application/json' 1737 ], 1738 'body' => wp_json_encode([ 1739 'model' => $model, 1740 'messages' => [ 1741 ['role' => 'system', 'content' => $system_prompt], 1742 ['role' => 'user', 'content' => $text] 1743 ], 1744 'temperature' => 0.3 1745 ]) 1746 ]); 1747 1748 if (is_wp_error($response)) { 1749 return $response; 1750 } 1751 1752 $body = json_decode(wp_remote_retrieve_body($response), true); 1753 1754 if (isset($body['error'])) { 1755 return new WP_Error('api_error', $body['error']['message']); 1756 } 1757 1758 if (isset($body['choices'][0]['message']['content'])) { 1759 return $body['choices'][0]['message']['content']; 1760 } 1761 1762 return new WP_Error('api_error', 'Invalid API response'); 1763 } 1764 1765 /** 1766 * Translate text using Claude API 1767 */ 1768 private function translate_with_claude($api_key, $model, $system_prompt, $text) { 1769 $response = wp_remote_post('https://api.anthropic.com/v1/messages', [ 1770 'timeout' => 60, 1771 'headers' => [ 1772 'x-api-key' => $api_key, 1773 'anthropic-version' => '2023-06-01', 1774 'Content-Type' => 'application/json' 1775 ], 1776 'body' => wp_json_encode([ 1777 'model' => $model, 1778 'max_tokens' => 4096, 1779 'system' => $system_prompt, 1780 'messages' => [ 1781 ['role' => 'user', 'content' => $text] 1782 ] 1783 ]) 1784 ]); 1785 1786 if (is_wp_error($response)) { 1787 return $response; 1788 } 1789 1790 $body = json_decode(wp_remote_retrieve_body($response), true); 1791 1792 if (isset($body['error'])) { 1793 return new WP_Error('api_error', $body['error']['message']); 1794 } 1795 1796 if (isset($body['content'][0]['text'])) { 1797 return $body['content'][0]['text']; 1798 } 1799 1800 return new WP_Error('api_error', 'Invalid API response'); 1801 } 1802 1803 /** 1804 * Translate text using xAI (Grok) API 1805 */ 1806 private function translate_with_xai($api_key, $model, $system_prompt, $text) { 1807 $response = wp_remote_post('https://api.x.ai/v1/chat/completions', [ 1808 'timeout' => 60, 1809 'headers' => [ 1810 'Authorization' => 'Bearer ' . $api_key, 1811 'Content-Type' => 'application/json' 1812 ], 1813 'body' => wp_json_encode([ 1814 'model' => $model, 1815 'messages' => [ 1816 ['role' => 'system', 'content' => $system_prompt], 1817 ['role' => 'user', 'content' => $text] 1818 ], 1819 'temperature' => 0.3 1820 ]) 1821 ]); 1822 1823 if (is_wp_error($response)) { 1824 return $response; 1825 } 1826 1827 $body = json_decode(wp_remote_retrieve_body($response), true); 1828 1829 if (isset($body['error'])) { 1830 return new WP_Error('api_error', $body['error']['message']); 1831 } 1832 1833 if (isset($body['choices'][0]['message']['content'])) { 1834 return $body['choices'][0]['message']['content']; 1835 } 1836 1837 return new WP_Error('api_error', 'Invalid API response'); 1838 } 1839 1840 /** 1841 * Translate text using DeepSeek API 1842 */ 1843 private function translate_with_deepseek($api_key, $model, $system_prompt, $text) { 1844 $response = wp_remote_post('https://api.deepseek.com/v1/chat/completions', [ 1845 'timeout' => 60, 1846 'headers' => [ 1847 'Authorization' => 'Bearer ' . $api_key, 1848 'Content-Type' => 'application/json' 1849 ], 1850 'body' => wp_json_encode([ 1851 'model' => $model, 1852 'messages' => [ 1853 ['role' => 'system', 'content' => $system_prompt], 1854 ['role' => 'user', 'content' => $text] 1855 ], 1856 'temperature' => 0.3 1857 ]) 1858 ]); 1859 1860 if (is_wp_error($response)) { 1861 return $response; 1862 } 1863 1864 $body = json_decode(wp_remote_retrieve_body($response), true); 1865 1866 if (isset($body['error'])) { 1867 return new WP_Error('api_error', $body['error']['message']); 1868 } 1869 1870 if (isset($body['choices'][0]['message']['content'])) { 1871 return $body['choices'][0]['message']['content']; 1872 } 1873 1874 return new WP_Error('api_error', 'Invalid API response'); 1875 } 1876 1877 /** 1878 * Translate text using Google Gemini API 1879 */ 1880 private function translate_with_gemini($api_key, $model, $system_prompt, $text) { 1881 $url = 'https://generativelanguage.googleapis.com/v1beta/models/' . $model . ':generateContent?key=' . $api_key; 1882 1883 $response = wp_remote_post($url, [ 1884 'timeout' => 60, 1885 'headers' => [ 1886 'Content-Type' => 'application/json' 1887 ], 1888 'body' => wp_json_encode([ 1889 'contents' => [ 1890 [ 1891 'parts' => [ 1892 ['text' => $system_prompt . "\n\n" . $text] 1893 ] 1894 ] 1895 ], 1896 'generationConfig' => [ 1897 'temperature' => 0.3 1898 ] 1899 ]) 1900 ]); 1901 1902 if (is_wp_error($response)) { 1903 return $response; 1904 } 1905 1906 $body = json_decode(wp_remote_retrieve_body($response), true); 1907 1908 if (isset($body['error'])) { 1909 return new WP_Error('api_error', $body['error']['message']); 1910 } 1911 1912 if (isset($body['candidates'][0]['content']['parts'][0]['text'])) { 1913 return $body['candidates'][0]['content']['parts'][0]['text']; 1914 } 1915 1916 return new WP_Error('api_error', 'Invalid API response'); 1917 } 1918 1919 /** 1920 * Translate text using OpenRouter API 1921 */ 1922 private function translate_with_openrouter($api_key, $model, $system_prompt, $text) { 1923 $response = wp_remote_post('https://openrouter.ai/api/v1/chat/completions', [ 1924 'timeout' => 60, 1925 'headers' => [ 1926 'Authorization' => 'Bearer ' . $api_key, 1927 'Content-Type' => 'application/json', 1928 'HTTP-Referer' => home_url(), 1929 'X-Title' => 'MxChat Translation' 1930 ], 1931 'body' => wp_json_encode([ 1932 'model' => $model, 1933 'messages' => [ 1934 ['role' => 'system', 'content' => $system_prompt], 1935 ['role' => 'user', 'content' => $text] 1936 ], 1937 'temperature' => 0.3 1938 ]) 1939 ]); 1940 1941 if (is_wp_error($response)) { 1942 return $response; 1943 } 1944 1945 $body = json_decode(wp_remote_retrieve_body($response), true); 1946 1947 if (isset($body['error'])) { 1948 return new WP_Error('api_error', $body['error']['message']); 1949 } 1950 1951 if (isset($body['choices'][0]['message']['content'])) { 1952 return $body['choices'][0]['message']['content']; 1953 } 1954 1955 return new WP_Error('api_error', 'Invalid API response'); 1956 } 1460 1957 1461 1958 public function mxchat_fetch_chat_history() { … … 2202 2699 if (isset($_POST['delete_session_ids']) && is_array($_POST['delete_session_ids'])) { 2203 2700 $deleted_count = 0; 2701 $translations_table = $wpdb->prefix . 'mxchat_transcript_translations'; 2204 2702 2205 2703 foreach ($_POST['delete_session_ids'] as $session_id) { … … 2212 2710 // Perform the deletion from the database table 2213 2711 $wpdb->delete($table_name, ['session_id' => $session_id_sanitized]); 2712 2713 // Delete any saved translations for this session 2714 if ($wpdb->get_var("SHOW TABLES LIKE '$translations_table'") === $translations_table) { 2715 $wpdb->delete($translations_table, ['session_id' => $session_id_sanitized]); 2716 } 2214 2717 2215 2718 // Delete the corresponding option entry from wp_options table … … 5037 5540 esc_textarea($pre_chat_message) 5038 5541 ); 5039 echo '<p class="description">' . esc_html__('Set the message displayed to users before they start a chat. Use this to provide a friendly greeting or instructions.', 'mxchat') . '</p>';5040 5542 } 5041 5543 … … 5049 5551 $instructions 5050 5552 ); 5051 // Provide a helpful description with sample instructions button 5052 echo '<p class="description">' . esc_html__('Provide system-level instructions for the AI to guide its behavior. Be clear and concise for better results.', 'mxchat') . '</p>'; 5553 // Sample instructions button 5053 5554 echo '<div class="mxchat-instructions-container">'; 5054 5555 echo '<button type="button" class="mxchat-instructions-btn" id="mxchatViewSampleBtn">'; … … 5133 5634 ), 5134 5635 esc_html__('Google Gemini Models', 'mxchat') => array( 5135 'gemini-2.0-flash' => esc_html__('Gemini 2.0 Flash (Next-Gen Features)', 'mxchat'), 5136 'gemini-2.0-flash-lite' => esc_html__('Gemini 2.0 Flash-Lite (Cost-Efficient)', 'mxchat'), 5137 'gemini-1.5-pro' => esc_html__('Gemini 1.5 Pro (Complex Reasoning)', 'mxchat'), 5138 'gemini-1.5-flash' => esc_html__('Gemini 1.5 Flash (Fast & Versatile)', 'mxchat'), 5139 ), 5140 esc_html__('Google Gemini Models', 'mxchat') => array( 5141 'gemini-2.0-flash' => esc_html__('Gemini 2.0 Flash (Next-Gen Features)', 'mxchat'), 5142 'gemini-2.0-flash-lite' => esc_html__('Gemini 2.0 Flash-Lite (Cost-Efficient)', 'mxchat'), 5143 'gemini-1.5-pro' => esc_html__('Gemini 1.5 Pro (Complex Reasoning)', 'mxchat'), 5144 'gemini-1.5-flash' => esc_html__('Gemini 1.5 Flash (Fast & Versatile)', 'mxchat'), 5636 'gemini-3-pro-preview' => esc_html__('Gemini 3 Pro (Most Intelligent, Multimodal)', 'mxchat'), 5637 'gemini-3-flash-preview' => esc_html__('Gemini 3 Flash (Balanced Speed & Scale)', 'mxchat'), 5638 'gemini-2.5-pro' => esc_html__('Gemini 2.5 Pro (Advanced Thinking)', 'mxchat'), 5639 'gemini-2.5-flash' => esc_html__('Gemini 2.5 Flash (Best Price-Performance)', 'mxchat'), 5640 'gemini-2.5-flash-lite' => esc_html__('Gemini 2.5 Flash-Lite (Ultra Fast)', 'mxchat'), 5641 'gemini-2.0-flash' => esc_html__('Gemini 2.0 Flash (Deprecated Mar 2026)', 'mxchat'), 5642 'gemini-2.0-flash-lite' => esc_html__('Gemini 2.0 Flash-Lite (Deprecated Mar 2026)', 'mxchat'), 5643 'gemini-1.5-pro' => esc_html__('Gemini 1.5 Pro (Deprecated Sep 2025)', 'mxchat'), 5644 'gemini-1.5-flash' => esc_html__('Gemini 1.5 Flash (Deprecated Sep 2025)', 'mxchat'), 5145 5645 ), 5146 5646 esc_html__('X.AI Models', 'mxchat') => array( … … 5205 5705 echo '</select>'; 5206 5706 5207 echo '<p class="description">' . esc_html__('Select the AI model your chatbot will use for chatting.', 'mxchat') . '</p>';5208 5209 5707 // Add a note for OpenRouter 5210 5708 echo '<p class="description" id="openrouter-model-note" style="display:none; color: #d63638; font-weight: 500;">'; … … 5296 5794 echo '<span class="slider"></span>'; 5297 5795 echo '</label>'; 5298 echo '<p class="description">' .5299 esc_html__('Enable real-time streaming responses for supported models (OpenAI, Claude, DeepSeek, and Grok). When disabled, responses will load all at once.', 'mxchat') .5300 '</p>';5301 5796 5302 5797 // Test button with better styling … … 5342 5837 echo '<span class="slider"></span>'; 5343 5838 echo '</label>'; 5344 echo '<p class="description">' .5345 esc_html__('Allow the chatbot to search the web for up-to-date information when answering questions. Uses OpenAI\'s web search tool via the Responses API.', 'mxchat') .5346 '</p>';5347 5839 5348 5840 echo '</div>'; … … 5426 5918 } 5427 5919 echo '</select>'; 5428 echo '<p class="description"><span class="red-warning">IMPORTANT:</span> A vector embedding model is required for MxChat to function. Changing models is not recommended; if you do, you must delete all existing knowledge & intent data and reconfigure them.</p>';5429 5920 5430 5921 // API Key Status Messages for Embedding Models … … 5468 5959 // Render the input field 5469 5960 echo '<input type="text" id="top_bar_title" name="top_bar_title" value="' . $top_bar_title . '" />'; 5470 5471 // Add a description5472 echo '<p class="description">' . esc_html__('Enter the title text that will appear on the top bar of the chatbot.', 'mxchat') . '</p>';5473 5961 } 5474 5962 public function mxchat_ai_agent_text_callback() { … … 5477 5965 // Render the input field 5478 5966 echo '<input type="text" id="ai_agent_text" name="ai_agent_text" value="' . $ai_agent_text . '" />'; 5479 // Add a description5480 echo '<p class="description">' . esc_html__('Enter the text that will appear for AI agents in the status indicator. Default: "AI Agent"', 'mxchat') . '</p>';5481 5967 } 5482 5968 … … 5499 5985 echo '<span class="slider"></span>'; 5500 5986 echo '</label>'; 5501 echo '<p class="description">' . esc_html__('Their email will appear at the top of the transcript. Email form will show for users who are not logged in or have not provided an email within 24h.', 'mxchat') . '</p>';5502 5987 } 5503 5988 … … 5519 6004 data-setting="email_blocker_header_content" 5520 6005 >' . esc_textarea($content) . '</textarea>'; 5521 5522 echo '<p class="description">';5523 echo esc_html__('You may enter HTML here, such as <h2>Welcome</h2> or <p>Let\'s get started</p>.', 'mxchat');5524 echo '</p>';5525 6006 } 5526 6007 … … 5536 6017 // Use esc_attr to safely render the existing text 5537 6018 echo '<input type="text" id="email_blocker_button_text" name="email_blocker_button_text" value="' . esc_attr($button_text) . '" style="width: 300px;" />'; 5538 5539 echo '<p class="description">';5540 echo esc_html__('Enter the text you want on the submit button, e.g. "Start Chat".', 'mxchat');5541 echo '</p>';5542 6019 } 5543 6020 … … 5557 6034 echo '<span class="slider"></span>'; 5558 6035 echo '</label>'; 5559 echo '<p class="description">' . esc_html__('Enable this to also require users to enter their name along with their email before chatting.', 'mxchat') . '</p>';5560 6036 } 5561 6037 //Name field placeholder callback … … 5567 6043 5568 6044 echo '<input type="text" id="name_field_placeholder" name="name_field_placeholder" value="' . esc_attr($placeholder) . '" style="width: 300px;" />'; 5569 echo '<p class="description">';5570 echo esc_html__('Placeholder text for the name field.', 'mxchat');5571 echo '</p>';5572 6045 } 5573 6046 … … 5583 6056 ?> 5584 6057 <textarea id="intro_message" name="intro_message" rows="5" cols="50"><?php echo $saved_message; ?></textarea> 5585 <p class="description">5586 <?php esc_html_e('Enter your message. HTML tags and line breaks will be preserved.', 'mxchat'); ?>5587 </p>5588 6058 <?php 5589 6059 } … … 5603 6073 esc_attr__('How can I assist?', 'mxchat') 5604 6074 ); 5605 5606 // Output the description5607 echo '<p class="description">' . esc_html__('This is the placeholder text for the chat input field.', 'mxchat') . '</p>';5608 6075 } 5609 6076 … … 6063 6530 echo '<span class="slider"></span>'; 6064 6531 echo '</label>'; 6065 echo '<p class="description">' .6066 esc_html__('Show chatbot automatically on all pages. When disabled, you can place the chatbot manually using shortcode [mxchat_chatbot floating="yes"].', 'mxchat') .6067 '</p>';6068 6532 6069 6533 // Post Type Visibility Options (only visible when auto-display is ON) … … 6073 6537 echo '<div class="mxchat-post-type-visibility-header">'; 6074 6538 echo '<h4>' . esc_html__('Post Type Visibility', 'mxchat') . '</h4>'; 6075 echo '<p class="description">' . esc_html__('Control which post types display the chatbot.', 'mxchat') . '</p>';6076 6539 echo '</div>'; 6077 6540 … … 6158 6621 echo '<span class="slider"></span>'; 6159 6622 echo '</label>'; 6160 // Warning message - displayed via callback since wrapper uses esc_html()6161 echo '<p class="description" style="margin-top: 8px; color: #d63638; font-weight: 500;">' .6162 esc_html__('⚠️ Important: If disabled, you must also remove any instructions about including links or citations from your AI Behavior/System Prompt settings. Otherwise, the AI may fabricate URLs.', 'mxchat') .6163 '</p>';6164 6623 } 6165 6624 … … 6185 6644 echo '<span class="slider"></span>'; 6186 6645 echo '</label>'; 6187 echo '<p class="description">' . esc_html__('Enable this option to display a privacy notice below the chat widget.', 'mxchat') . '</p>';6188 6646 6189 6647 // Output the custom text input field … … 6192 6650 esc_textarea($privacy_text) 6193 6651 ); 6194 echo '<p class="description">' . esc_html__('Enter the privacy policy text. You can include HTML links.', 'mxchat') . '</p>';6195 6652 } 6196 6653 … … 6212 6669 echo '<span class="slider"></span>'; 6213 6670 echo '</label>'; 6214 6215 echo '<p class="description">' . esc_html__('Enable this option to apply Complianz consent logic to the chatbot (must have Complianz Plugin).', 'mxchat') . '</p>';6216 6671 } 6217 6672 … … 6232 6687 echo '<span class="slider"></span>'; 6233 6688 echo '</label>'; 6234 echo '<p class="description">' . esc_html__('Enable to open links in a new tab (default is to open in the same tab).', 'mxchat') . '</p>';6235 6689 } 6236 6690 … … 6251 6705 echo '<span class="slider"></span>'; 6252 6706 echo '</label>'; 6253 6254 echo '<p class="description">' . esc_html__('Enable to keep chat history when users navigate tabs or return to the site within 24 hours.', 'mxchat') . '</p>';6255 6707 } 6256 6708 … … 6268 6720 esc_attr__('Enter Quick Question 1', 'mxchat') 6269 6721 ); 6270 6271 // Add a description for the field6272 echo '<p class="description">' . esc_html__('This will be the first Quick Question in the chatbot, displayed above the input field.', 'mxchat') . '</p>';6273 6722 } 6274 6723 … … 6287 6736 esc_attr__('Enter Quick Question 2', 'mxchat') 6288 6737 ); 6289 6290 // Add a description for the field6291 echo '<p class="description">' . esc_html__('This will be the second Quick Question in the chatbot.', 'mxchat') . '</p>';6292 6738 } 6293 6739 … … 6306 6752 esc_attr(__('Enter Quick Question 3', 'mxchat')) 6307 6753 ); 6308 6309 // Add a description for the field6310 echo '<p class="description">' . esc_html__('This will be the third Quick Question in the chatbot.', 'mxchat') . '</p>';6311 6754 } 6312 6755 … … 6359 6802 esc_html__('Add Question', 'mxchat') 6360 6803 ); 6361 echo '<p class="description">' . esc_html__('Add as many Quick Questions as you need.', 'mxchat') . '</p>';6362 6804 } 6363 6805 … … 6966 7408 'export_nonce' => wp_create_nonce('mxchat_export_transcripts'), 6967 7409 'delete_nonce' => wp_create_nonce('mxchat_delete_chat_history'), 6968 'setting_nonce' => wp_create_nonce('mxchat_save_setting_nonce') 7410 'setting_nonce' => wp_create_nonce('mxchat_save_setting_nonce'), 7411 'translate_nonce' => wp_create_nonce('mxchat_translate_messages') 6969 7412 )); 6970 7413 … … 7233 7676 } else { 7234 7677 $allowed_models = array( 7678 'gemini-3-pro-preview', 7679 'gemini-3-flash-preview', 7680 'gemini-2.5-pro', 7681 'gemini-2.5-flash', 7682 'gemini-2.5-flash-lite', 7235 7683 'gemini-2.0-flash', 7236 7684 'gemini-2.0-flash-lite', -
mxchat-basic/trunk/includes/class-mxchat-integrator.php
r3444379 r3446370 1821 1821 // Prepare RAG context data for storage (only include documents used for context) 1822 1822 $rag_context_for_storage = null; 1823 if ($this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches'])) { 1824 $rag_context_for_storage = [ 1825 'top_matches' => $this->last_similarity_analysis['top_matches'], 1826 'approved_urls' => $this->current_valid_urls ?? [], 1827 'similarity_threshold' => $this->last_similarity_analysis['threshold_used'] ?? 0.35, 1828 'knowledge_base_type' => $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database', 1829 'total_documents_checked' => $this->last_similarity_analysis['total_checked'] ?? 0 1830 ]; 1823 $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']); 1824 $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis); 1825 1826 if ($has_rag_data || $has_action_data) { 1827 $rag_context_for_storage = []; 1828 1829 // Add RAG/source data if available 1830 if ($has_rag_data) { 1831 $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches']; 1832 $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? []; 1833 $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35; 1834 $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database'; 1835 $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0; 1836 } 1837 1838 // Add action analysis data if available 1839 if ($has_action_data) { 1840 $rag_context_for_storage['action_analysis'] = $this->last_action_analysis; 1841 } 1831 1842 } 1832 1843 … … 2647 2658 */ 2648 2659 private function interpret_query_with_gemini($user_query, $system_prompt, $api_key, $model) { 2649 // Strip "gemini-" prefix for the API2650 $ model_version = str_replace('gemini-', '', $model);2651 2652 $url = "https://generativelanguage.googleapis.com/ v1/models/$model_version:generateContent?key=" . urlencode($api_key);2660 // Use v1beta for preview models, v1 for stable models 2661 $api_version = (strpos($model, 'preview') !== false || strpos($model, 'exp') !== false) ? 'v1beta' : 'v1'; 2662 2663 $url = "https://generativelanguage.googleapis.com/{$api_version}/models/{$model}:generateContent?key=" . urlencode($api_key); 2653 2664 2654 2665 $args = [ … … 6410 6421 // Prepare RAG context for streaming response 6411 6422 $rag_context_for_storage = null; 6412 if ($this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches'])) { 6413 $rag_context_for_storage = [ 6414 'top_matches' => $this->last_similarity_analysis['top_matches'], 6415 'approved_urls' => $this->current_valid_urls ?? [], 6416 'similarity_threshold' => $this->last_similarity_analysis['threshold_used'] ?? 0.35, 6417 'knowledge_base_type' => $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database', 6418 'total_documents_checked' => $this->last_similarity_analysis['total_checked'] ?? 0 6419 ]; 6423 $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']); 6424 $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis); 6425 6426 if ($has_rag_data || $has_action_data) { 6427 $rag_context_for_storage = []; 6428 6429 if ($has_rag_data) { 6430 $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches']; 6431 $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? []; 6432 $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35; 6433 $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database'; 6434 $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0; 6435 } 6436 6437 if ($has_action_data) { 6438 $rag_context_for_storage['action_analysis'] = $this->last_action_analysis; 6439 } 6420 6440 } 6421 6441 $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage); … … 6667 6687 // Prepare RAG context for streaming response 6668 6688 $rag_context_for_storage = null; 6669 if ($this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches'])) { 6670 $rag_context_for_storage = [ 6671 'top_matches' => $this->last_similarity_analysis['top_matches'], 6672 'approved_urls' => $this->current_valid_urls ?? [], 6673 'similarity_threshold' => $this->last_similarity_analysis['threshold_used'] ?? 0.35, 6674 'knowledge_base_type' => $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database', 6675 'total_documents_checked' => $this->last_similarity_analysis['total_checked'] ?? 0 6676 ]; 6689 $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']); 6690 $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis); 6691 6692 if ($has_rag_data || $has_action_data) { 6693 $rag_context_for_storage = []; 6694 6695 if ($has_rag_data) { 6696 $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches']; 6697 $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? []; 6698 $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35; 6699 $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database'; 6700 $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0; 6701 } 6702 6703 if ($has_action_data) { 6704 $rag_context_for_storage['action_analysis'] = $this->last_action_analysis; 6705 } 6677 6706 } 6678 6707 $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage); … … 7282 7311 // Prepare RAG context for streaming response 7283 7312 $rag_context_for_storage = null; 7284 if ($this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches'])) { 7285 $rag_context_for_storage = [ 7286 'top_matches' => $this->last_similarity_analysis['top_matches'], 7287 'approved_urls' => $this->current_valid_urls ?? [], 7288 'similarity_threshold' => $this->last_similarity_analysis['threshold_used'] ?? 0.35, 7289 'knowledge_base_type' => $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database', 7290 'total_documents_checked' => $this->last_similarity_analysis['total_checked'] ?? 0 7291 ]; 7313 $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']); 7314 $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis); 7315 7316 if ($has_rag_data || $has_action_data) { 7317 $rag_context_for_storage = []; 7318 7319 if ($has_rag_data) { 7320 $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches']; 7321 $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? []; 7322 $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35; 7323 $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database'; 7324 $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0; 7325 } 7326 7327 if ($has_action_data) { 7328 $rag_context_for_storage['action_analysis'] = $this->last_action_analysis; 7329 } 7292 7330 } 7293 7331 $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage); … … 7525 7563 // Prepare RAG context for streaming response 7526 7564 $rag_context_for_storage = null; 7527 if ($this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches'])) { 7528 $rag_context_for_storage = [ 7529 'top_matches' => $this->last_similarity_analysis['top_matches'], 7530 'approved_urls' => $this->current_valid_urls ?? [], 7531 'similarity_threshold' => $this->last_similarity_analysis['threshold_used'] ?? 0.35, 7532 'knowledge_base_type' => $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database', 7533 'total_documents_checked' => $this->last_similarity_analysis['total_checked'] ?? 0 7534 ]; 7565 $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']); 7566 $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis); 7567 7568 if ($has_rag_data || $has_action_data) { 7569 $rag_context_for_storage = []; 7570 7571 if ($has_rag_data) { 7572 $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches']; 7573 $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? []; 7574 $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35; 7575 $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database'; 7576 $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0; 7577 } 7578 7579 if ($has_action_data) { 7580 $rag_context_for_storage['action_analysis'] = $this->last_action_analysis; 7581 } 7535 7582 } 7536 7583 $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage); … … 7767 7814 // Prepare RAG context for streaming response 7768 7815 $rag_context_for_storage = null; 7769 if ($this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches'])) { 7770 $rag_context_for_storage = [ 7771 'top_matches' => $this->last_similarity_analysis['top_matches'], 7772 'approved_urls' => $this->current_valid_urls ?? [], 7773 'similarity_threshold' => $this->last_similarity_analysis['threshold_used'] ?? 0.35, 7774 'knowledge_base_type' => $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database', 7775 'total_documents_checked' => $this->last_similarity_analysis['total_checked'] ?? 0 7776 ]; 7816 $has_rag_data = $this->last_similarity_analysis !== null && !empty($this->last_similarity_analysis['top_matches']); 7817 $has_action_data = isset($this->last_action_analysis) && !empty($this->last_action_analysis); 7818 7819 if ($has_rag_data || $has_action_data) { 7820 $rag_context_for_storage = []; 7821 7822 if ($has_rag_data) { 7823 $rag_context_for_storage['top_matches'] = $this->last_similarity_analysis['top_matches']; 7824 $rag_context_for_storage['approved_urls'] = $this->current_valid_urls ?? []; 7825 $rag_context_for_storage['similarity_threshold'] = $this->last_similarity_analysis['threshold_used'] ?? 0.35; 7826 $rag_context_for_storage['knowledge_base_type'] = $this->last_similarity_analysis['knowledge_base_type'] ?? 'WordPress Database'; 7827 $rag_context_for_storage['total_documents_checked'] = $this->last_similarity_analysis['total_checked'] ?? 0; 7828 } 7829 7830 if ($has_action_data) { 7831 $rag_context_for_storage['action_analysis'] = $this->last_action_analysis; 7832 } 7777 7833 } 7778 7834 $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage); … … 8662 8718 8663 8719 // Prepare the API endpoint 8664 $api_endpoint = 'https://generativelanguage.googleapis.com/v1/models/' . $selected_model . ':generateContent?key=' . $gemini_api_key; 8720 // Use v1beta for preview models (Gemini 3, experimental), v1 for stable models 8721 $api_version = (strpos($selected_model, 'preview') !== false || strpos($selected_model, 'exp') !== false) ? 'v1beta' : 'v1'; 8722 $api_endpoint = 'https://generativelanguage.googleapis.com/' . $api_version . '/models/' . $selected_model . ':generateContent?key=' . $gemini_api_key; 8665 8723 8666 8724 // Set up the API request -
mxchat-basic/trunk/js/chat-script.js
r3444430 r3446370 2636 2636 2637 2637 // ==================================== 2638 // EMAIL COLLECTION SETUP - FIXEDVERSION2638 // EMAIL COLLECTION SETUP - MULTI-INSTANCE VERSION 2639 2639 // ==================================== 2640 2640 // Only run email collection setup if it's enabled 2641 2641 if (mxchatChat && mxchatChat.email_collection_enabled === 'on') { 2642 // Email collection form setup and handlers 2643 const emailForm = document.getElementById('email-collection-form'); 2644 const emailBlocker = document.getElementById('email-blocker'); 2645 const chatbotWrapper = document.getElementById('chat-container'); 2646 2647 if (emailForm && emailBlocker && chatbotWrapper) { 2648 2649 // Add loading state management 2650 let isSubmitting = false; 2651 2652 // Optimized UI transition functions 2653 function showEmailForm() { 2654 emailBlocker.style.display = 'flex'; 2655 chatbotWrapper.style.display = 'none'; 2656 } 2657 2658 function showChatContainer() { 2659 // Show chat immediately without delay 2660 emailBlocker.style.display = 'none'; 2661 chatbotWrapper.style.display = 'flex'; 2662 2663 // Load chat history only after showing chat container 2664 if (typeof loadChatHistory === 'function') { 2665 loadChatHistory(); 2666 } 2667 } 2668 2669 // Enhanced email validation 2670 function isValidEmail(email) { 2671 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 2672 return emailRegex.test(email.trim()) && email.length <= 254; // RFC 5321 limit 2673 } 2674 2675 // Enhanced name validation 2676 function isValidName(name) { 2677 return name && name.trim().length >= 2 && name.trim().length <= 100; 2678 } 2679 2680 // Show loading state with spinner 2681 function setSubmissionState(loading) { 2682 const submitButton = document.getElementById('email-submit-button'); 2683 const emailInput = document.getElementById('user-email'); 2684 const nameInput = document.getElementById('user-name'); 2685 2686 if (loading) { 2687 isSubmitting = true; 2688 if (submitButton) submitButton.disabled = true; 2689 if (emailInput) emailInput.disabled = true; 2690 if (nameInput) nameInput.disabled = true; 2691 2692 // Store original content and add spinner 2693 if (submitButton && !submitButton.getAttribute('data-original-html')) { 2694 submitButton.setAttribute('data-original-html', submitButton.innerHTML); 2695 2696 // Add loading spinner while keeping original text 2697 const originalText = submitButton.textContent; 2698 submitButton.innerHTML = ` 2699 <svg class="email-spinner" style="width: 16px; height: 16px; margin-right: 8px; animation: spin 1s linear infinite;" viewBox="0 0 24 24"> 2700 <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none" stroke-dasharray="31.416" stroke-dashoffset="31.416"> 2701 <animate attributeName="stroke-dasharray" dur="2s" values="0 31.416;15.708 15.708;0 31.416" repeatCount="indefinite"/> 2702 <animate attributeName="stroke-dashoffset" dur="2s" values="0;-15.708;-31.416" repeatCount="indefinite"/> 2703 </circle> 2704 </svg> 2705 ${originalText} 2706 `; 2707 2708 submitButton.style.opacity = '0.8'; 2642 2643 // Track submitting state per bot 2644 const emailSubmittingState = {}; 2645 2646 // Add CSS animations for email form (once globally) 2647 if (!document.getElementById('email-error-styles')) { 2648 const style = document.createElement('style'); 2649 style.id = 'email-error-styles'; 2650 style.textContent = ` 2651 @keyframes fadeInError { 2652 from { opacity: 0; transform: translateY(-5px); } 2653 to { opacity: 1; transform: translateY(0); } 2654 } 2655 .email-input-shake { 2656 animation: shake 0.5s ease-in-out; 2657 } 2658 @keyframes shake { 2659 0%, 100% { transform: translateX(0); } 2660 25% { transform: translateX(-5px); } 2661 75% { transform: translateX(5px); } 2662 } 2663 @keyframes spin { 2664 from { transform: rotate(0deg); } 2665 to { transform: rotate(360deg); } 2666 } 2667 .email-spinner { 2668 display: inline-block; 2669 vertical-align: middle; 2670 } 2671 `; 2672 document.head.appendChild(style); 2673 } 2674 2675 // Helper functions for email collection (multi-instance aware) 2676 function showEmailFormForBot(botId) { 2677 var emailBlocker = getElementDOM(botId, 'email-blocker'); 2678 var chatContainer = getElementDOM(botId, 'chat-container'); 2679 if (emailBlocker) emailBlocker.style.display = 'flex'; 2680 if (chatContainer) chatContainer.style.display = 'none'; 2681 } 2682 2683 function showChatContainerForBot(botId) { 2684 var emailBlocker = getElementDOM(botId, 'email-blocker'); 2685 var chatContainer = getElementDOM(botId, 'chat-container'); 2686 if (emailBlocker) emailBlocker.style.display = 'none'; 2687 if (chatContainer) chatContainer.style.display = 'flex'; 2688 2689 // Load chat history for this bot 2690 if (typeof loadChatHistory === 'function') { 2691 loadChatHistory(botId); 2692 } 2693 } 2694 2695 function isValidEmailAddress(email) { 2696 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 2697 return emailRegex.test(email.trim()) && email.length <= 254; 2698 } 2699 2700 function isValidNameInput(name) { 2701 return name && name.trim().length >= 2 && name.trim().length <= 100; 2702 } 2703 2704 function setEmailSubmissionState(botId, loading) { 2705 var submitButton = getElementDOM(botId, 'email-submit-button'); 2706 var emailInput = getElementDOM(botId, 'user-email'); 2707 var nameInput = getElementDOM(botId, 'user-name'); 2708 2709 if (loading) { 2710 emailSubmittingState[botId] = true; 2711 if (submitButton) submitButton.disabled = true; 2712 if (emailInput) emailInput.disabled = true; 2713 if (nameInput) nameInput.disabled = true; 2714 2715 if (submitButton && !submitButton.getAttribute('data-original-html')) { 2716 submitButton.setAttribute('data-original-html', submitButton.innerHTML); 2717 const originalText = submitButton.textContent; 2718 submitButton.innerHTML = ` 2719 <svg class="email-spinner" style="width: 16px; height: 16px; margin-right: 8px; animation: spin 1s linear infinite;" viewBox="0 0 24 24"> 2720 <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none" stroke-dasharray="31.416" stroke-dashoffset="31.416"> 2721 <animate attributeName="stroke-dasharray" dur="2s" values="0 31.416;15.708 15.708;0 31.416" repeatCount="indefinite"/> 2722 <animate attributeName="stroke-dashoffset" dur="2s" values="0;-15.708;-31.416" repeatCount="indefinite"/> 2723 </circle> 2724 </svg> 2725 ${originalText} 2726 `; 2727 submitButton.style.opacity = '0.8'; 2728 } 2729 } else { 2730 emailSubmittingState[botId] = false; 2731 if (submitButton) submitButton.disabled = false; 2732 if (emailInput) emailInput.disabled = false; 2733 if (nameInput) nameInput.disabled = false; 2734 2735 if (submitButton) { 2736 const originalHtml = submitButton.getAttribute('data-original-html'); 2737 if (originalHtml) { 2738 submitButton.innerHTML = originalHtml; 2739 } 2740 submitButton.style.opacity = '1'; 2741 } 2742 } 2743 } 2744 2745 function showEmailError(botId, message) { 2746 clearEmailError(botId); 2747 2748 var emailForm = getElementDOM(botId, 'email-collection-form'); 2749 if (!emailForm) return; 2750 2751 const errorDiv = document.createElement('div'); 2752 errorDiv.className = 'email-error'; 2753 errorDiv.style.cssText = ` 2754 color: #e74c3c; 2755 font-size: 12px; 2756 margin-top: 8px; 2757 padding: 4px 0; 2758 animation: fadeInError 0.3s ease; 2759 `; 2760 errorDiv.textContent = message; 2761 emailForm.appendChild(errorDiv); 2762 2763 // Add shake animation to inputs 2764 var emailInput = getElementDOM(botId, 'user-email'); 2765 var nameInput = getElementDOM(botId, 'user-name'); 2766 2767 if (emailInput) { 2768 emailInput.classList.add('email-input-shake'); 2769 setTimeout(() => emailInput.classList.remove('email-input-shake'), 500); 2770 } 2771 if (nameInput) { 2772 nameInput.classList.add('email-input-shake'); 2773 setTimeout(() => nameInput.classList.remove('email-input-shake'), 500); 2774 } 2775 } 2776 2777 function clearEmailError(botId) { 2778 var emailForm = getElementDOM(botId, 'email-collection-form'); 2779 if (emailForm) { 2780 const existingErrors = emailForm.querySelectorAll('.email-error'); 2781 existingErrors.forEach(error => error.remove()); 2782 } 2783 } 2784 2785 function checkSessionAndEmailForBot(botId) { 2786 const sessionId = getChatSession(botId); 2787 2788 fetch(mxchatChat.ajax_url, { 2789 method: 'POST', 2790 headers: { 2791 'Content-Type': 'application/x-www-form-urlencoded', 2792 }, 2793 body: new URLSearchParams({ 2794 action: 'mxchat_check_email_provided', 2795 session_id: sessionId, 2796 nonce: mxchatChat.nonce, 2797 }) 2798 }) 2799 .then((response) => { 2800 if (!response.ok) { 2801 throw new Error(`HTTP error! status: ${response.status}`); 2802 } 2803 return response.json(); 2804 }) 2805 .then((data) => { 2806 if (data.success) { 2807 if (data.data.logged_in || data.data.email) { 2808 showChatContainerForBot(botId); 2809 } else { 2810 showEmailFormForBot(botId); 2709 2811 } 2710 2812 } else { 2711 isSubmitting = false; 2712 if (submitButton) submitButton.disabled = false; 2713 if (emailInput) emailInput.disabled = false; 2714 if (nameInput) nameInput.disabled = false; 2715 2716 // Restore original content 2717 if (submitButton) { 2718 const originalHtml = submitButton.getAttribute('data-original-html'); 2719 if (originalHtml) { 2720 submitButton.innerHTML = originalHtml; 2721 } 2722 submitButton.style.opacity = '1'; 2813 showEmailFormForBot(botId); 2814 } 2815 }) 2816 .catch((error) => { 2817 showEmailFormForBot(botId); 2818 }); 2819 } 2820 2821 // Event delegation for email form submission 2822 $(document).on('submit', '.email-collection-form', function(e) { 2823 e.preventDefault(); 2824 e.stopPropagation(); 2825 2826 var botId = getBotIdFromElement(this); 2827 2828 // Prevent double submission 2829 if (emailSubmittingState[botId]) { 2830 return false; 2831 } 2832 2833 var emailInput = getElementDOM(botId, 'user-email'); 2834 var nameInput = getElementDOM(botId, 'user-name'); 2835 var userEmail = emailInput ? emailInput.value.trim() : ''; 2836 var userName = nameInput ? nameInput.value.trim() : ''; 2837 var sessionId = getChatSession(botId); 2838 2839 // Validate email 2840 if (!userEmail) { 2841 showEmailError(botId, 'Please enter your email address.'); 2842 return false; 2843 } 2844 2845 if (!isValidEmailAddress(userEmail)) { 2846 showEmailError(botId, 'Please enter a valid email address.'); 2847 return false; 2848 } 2849 2850 // Validate name if field exists and has content 2851 if (nameInput && userName && !isValidNameInput(userName)) { 2852 showEmailError(botId, 'Please enter a valid name (2-100 characters).'); 2853 return false; 2854 } 2855 2856 clearEmailError(botId); 2857 setEmailSubmissionState(botId, true); 2858 2859 // Prepare form data 2860 const formData = new URLSearchParams({ 2861 action: 'mxchat_handle_save_email_and_response', 2862 email: userEmail, 2863 session_id: sessionId, 2864 nonce: mxchatChat.nonce, 2865 }); 2866 2867 if (userName) { 2868 formData.append('name', userName); 2869 } 2870 2871 fetch(mxchatChat.ajax_url, { 2872 method: 'POST', 2873 headers: { 2874 'Content-Type': 'application/x-www-form-urlencoded', 2875 }, 2876 body: formData 2877 }) 2878 .then((response) => { 2879 if (!response.ok) { 2880 throw new Error(`HTTP error! status: ${response.status}`); 2881 } 2882 return response.json(); 2883 }) 2884 .then((data) => { 2885 setEmailSubmissionState(botId, false); 2886 2887 if (data.success) { 2888 showChatContainerForBot(botId); 2889 2890 if (data.message && typeof appendMessage === 'function') { 2891 setTimeout(() => { 2892 appendMessage('bot', data.message, '', [], false, botId); 2893 if (typeof scrollToBottom === 'function') { 2894 scrollToBottom(botId); 2895 } 2896 }, 100); 2723 2897 } 2724 } 2725 } 2726 2727 // Error display functions 2728 function showEmailError(message) { 2729 clearEmailError(); 2730 2731 const errorDiv = document.createElement('div'); 2732 errorDiv.className = 'email-error'; 2733 errorDiv.style.cssText = ` 2734 color: #e74c3c; 2735 font-size: 12px; 2736 margin-top: 8px; 2737 padding: 4px 0; 2738 animation: fadeInError 0.3s ease; 2739 `; 2740 errorDiv.textContent = message; 2741 2742 // Add CSS animation if not already present 2743 if (!document.getElementById('email-error-styles')) { 2744 const style = document.createElement('style'); 2745 style.id = 'email-error-styles'; 2746 style.textContent = ` 2747 @keyframes fadeInError { 2748 from { opacity: 0; transform: translateY(-5px); } 2749 to { opacity: 1; transform: translateY(0); } 2750 } 2751 .email-input-shake { 2752 animation: shake 0.5s ease-in-out; 2753 } 2754 @keyframes shake { 2755 0%, 100% { transform: translateX(0); } 2756 25% { transform: translateX(-5px); } 2757 75% { transform: translateX(5px); } 2758 } 2759 @keyframes spin { 2760 from { transform: rotate(0deg); } 2761 to { transform: rotate(360deg); } 2762 } 2763 .email-spinner { 2764 display: inline-block; 2765 vertical-align: middle; 2766 } 2767 `; 2768 document.head.appendChild(style); 2769 } 2770 2771 emailForm.appendChild(errorDiv); 2772 2773 // Add shake animation to inputs 2774 const emailInput = document.getElementById('user-email'); 2775 const nameInput = document.getElementById('user-name'); 2776 2777 if (emailInput) { 2778 emailInput.classList.add('email-input-shake'); 2779 setTimeout(() => { 2780 emailInput.classList.remove('email-input-shake'); 2781 }, 500); 2782 } 2783 2784 if (nameInput) { 2785 nameInput.classList.add('email-input-shake'); 2786 setTimeout(() => { 2787 nameInput.classList.remove('email-input-shake'); 2788 }, 500); 2789 } 2790 } 2791 2792 function clearEmailError() { 2793 const existingErrors = emailForm.querySelectorAll('.email-error'); 2794 existingErrors.forEach(error => error.remove()); 2795 } 2796 2797 // MAIN FORM SUBMIT HANDLER 2798 // Remove any existing event listeners first 2799 emailForm.removeEventListener('submit', handleFormSubmit); 2800 2801 // Add the form submit handler 2802 emailForm.addEventListener('submit', handleFormSubmit); 2803 2804 function handleFormSubmit(event) { 2805 event.preventDefault(); 2806 event.stopPropagation(); 2807 2808 // Prevent double submission 2809 if (isSubmitting) { 2810 return false; 2811 } 2812 2813 const userEmail = document.getElementById('user-email').value.trim(); 2814 const nameInput = document.getElementById('user-name'); 2815 const userName = nameInput ? nameInput.value.trim() : ''; 2816 const sessionId = getChatSession(); 2817 2818 // Validate email before submission 2819 if (!userEmail) { 2820 showEmailError('Please enter your email address.'); 2821 return false; 2822 } 2823 2824 if (!isValidEmail(userEmail)) { 2825 showEmailError('Please enter a valid email address.'); 2826 return false; 2827 } 2828 2829 // Validate name if field exists 2830 if (nameInput && !isValidName(userName)) { 2831 showEmailError('Please enter a valid name (2-100 characters).'); 2832 return false; 2833 } 2834 2835 // Clear any existing errors 2836 clearEmailError(); 2837 setSubmissionState(true); 2838 2839 // Prepare form data with optional name 2840 const formData = new URLSearchParams({ 2841 action: 'mxchat_handle_save_email_and_response', 2842 email: userEmail, 2843 session_id: sessionId, 2844 nonce: mxchatChat.nonce, 2845 }); 2846 2847 // Add name to form data if provided 2848 if (userName) { 2849 formData.append('name', userName); 2850 } 2851 2852 fetch(mxchatChat.ajax_url, { 2853 method: 'POST', 2854 headers: { 2855 'Content-Type': 'application/x-www-form-urlencoded', 2856 }, 2857 body: formData 2858 }) 2859 .then((response) => { 2860 if (!response.ok) { 2861 throw new Error(`HTTP error! status: ${response.status}`); 2898 } else { 2899 showEmailError(botId, data.message || 'Failed to save email. Please try again.'); 2900 } 2901 }) 2902 .catch((error) => { 2903 setEmailSubmissionState(botId, false); 2904 showEmailError(botId, 'An error occurred. Please try again.'); 2905 }); 2906 2907 return false; 2908 }); 2909 2910 // Real-time email validation using event delegation 2911 $(document).on('input', '.mxchat-email-input', function() { 2912 var botId = getBotIdFromElement(this); 2913 var $input = $(this); 2914 2915 // Clear previous timeout 2916 clearTimeout($input.data('validationTimeout')); 2917 2918 // Debounce validation 2919 var timeout = setTimeout(() => { 2920 var email = this.value.trim(); 2921 clearEmailError(botId); 2922 2923 if (email && !isValidEmailAddress(email)) { 2924 showEmailError(botId, 'Please enter a valid email address.'); 2925 } 2926 }, 500); 2927 2928 $input.data('validationTimeout', timeout); 2929 }); 2930 2931 // Handle Enter key in email input 2932 $(document).on('keypress', '.mxchat-email-input', function(e) { 2933 if (e.key === 'Enter') { 2934 e.preventDefault(); 2935 var botId = getBotIdFromElement(this); 2936 if (!emailSubmittingState[botId]) { 2937 $(this).closest('.email-collection-form').submit(); 2938 } 2939 } 2940 }); 2941 2942 // Handle Enter key in name input 2943 $(document).on('keypress', '.mxchat-name-input', function(e) { 2944 if (e.key === 'Enter') { 2945 e.preventDefault(); 2946 var botId = getBotIdFromElement(this); 2947 if (!emailSubmittingState[botId]) { 2948 $(this).closest('.email-collection-form').submit(); 2949 } 2950 } 2951 }); 2952 2953 // Initialize email check for all bot instances 2954 $('.mxchat-chatbot-wrapper').each(function() { 2955 var botId = $(this).data('bot-id') || 'default'; 2956 var emailBlocker = getElementDOM(botId, 'email-blocker'); 2957 2958 // Only check if email blocker exists for this bot 2959 if (emailBlocker) { 2960 if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) { 2961 if (mxchatChat.initial_email_state.show_email_form) { 2962 showEmailFormForBot(botId); 2963 } else { 2964 showChatContainerForBot(botId); 2862 2965 } 2863 return response.json();2864 })2865 .then((data) => {2866 setSubmissionState(false);2867 2868 if (data.success) {2869 // Show chat immediately2870 showChatContainer();2871 2872 // Handle bot response if provided2873 if (data.message && typeof appendMessage === 'function') {2874 setTimeout(() => {2875 appendMessage('bot', data.message);2876 if (typeof scrollToBottom === 'function') {2877 scrollToBottom();2878 }2879 }, 100);2880 }2881 } else {2882 showEmailError(data.message || 'Failed to save email. Please try again.');2883 }2884 })2885 .catch((error) => {2886 setSubmissionState(false);2887 showEmailError('An error occurred. Please try again.');2888 });2889 2890 return false; // Extra prevention2891 }2892 2893 // Real-time email validation2894 const emailInput = document.getElementById('user-email');2895 if (emailInput) {2896 let validationTimeout;2897 2898 emailInput.addEventListener('input', function() {2899 // Clear previous validation timeout2900 if (validationTimeout) {2901 clearTimeout(validationTimeout);2902 }2903 2904 // Debounce validation2905 validationTimeout = setTimeout(() => {2906 const email = this.value.trim();2907 clearEmailError();2908 2909 if (email && !isValidEmail(email)) {2910 showEmailError('Please enter a valid email address.');2911 }2912 }, 500);2913 });2914 2915 // Handle Enter key2916 emailInput.addEventListener('keypress', function(e) {2917 if (e.key === 'Enter' && !isSubmitting) {2918 e.preventDefault();2919 emailForm.dispatchEvent(new Event('submit'));2920 }2921 });2922 }2923 2924 // Real-time name validation2925 const nameInput = document.getElementById('user-name');2926 if (nameInput) {2927 let nameValidationTimeout;2928 2929 nameInput.addEventListener('input', function() {2930 // Clear previous validation timeout2931 if (nameValidationTimeout) {2932 clearTimeout(nameValidationTimeout);2933 }2934 2935 // Debounce validation2936 nameValidationTimeout = setTimeout(() => {2937 const name = this.value.trim();2938 clearEmailError();2939 2940 if (name && !isValidName(name)) {2941 showEmailError('Name must be between 2 and 100 characters.');2942 }2943 }, 500);2944 });2945 2946 // Handle Enter key2947 nameInput.addEventListener('keypress', function(e) {2948 if (e.key === 'Enter' && !isSubmitting) {2949 e.preventDefault();2950 emailForm.dispatchEvent(new Event('submit'));2951 }2952 });2953 }2954 2955 // Initial state check2956 if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) {2957 const emailState = mxchatChat.initial_email_state;2958 if (emailState.show_email_form) {2959 showEmailForm();2960 2966 } else { 2961 showChatContainer(); 2962 } 2963 } else { 2964 // Check email status via AJAX 2965 setTimeout(checkSessionAndEmail, 100); 2966 } 2967 2968 // Check if email exists for the current session 2969 function checkSessionAndEmail() { 2970 const sessionId = getChatSession(); 2971 2972 fetch(mxchatChat.ajax_url, { 2973 method: 'POST', 2974 headers: { 2975 'Content-Type': 'application/x-www-form-urlencoded', 2976 }, 2977 body: new URLSearchParams({ 2978 action: 'mxchat_check_email_provided', 2979 session_id: sessionId, 2980 nonce: mxchatChat.nonce, 2981 }) 2982 }) 2983 .then((response) => { 2984 if (!response.ok) { 2985 throw new Error(`HTTP error! status: ${response.status}`); 2986 } 2987 return response.json(); 2988 }) 2989 .then((data) => { 2990 if (data.success) { 2991 if (data.data.logged_in || data.data.email) { 2992 showChatContainer(); 2993 } else { 2994 showEmailForm(); 2995 } 2996 } else { 2997 // On error, default to showing email form 2998 showEmailForm(); 2999 } 3000 }) 3001 .catch((error) => { 3002 // Email check failed - default to email form 3003 showEmailForm(); 3004 }); 3005 } 3006 3007 } else { 3008 // Email collection is enabled but essential elements are missing - silently continue 3009 } 2967 setTimeout(function() { 2968 checkSessionAndEmailForBot(botId); 2969 }, 100); 2970 } 2971 } 2972 }); 3010 2973 } 3011 2974 -
mxchat-basic/trunk/js/mxchat-admin.js
r3444379 r3446370 414 414 415 415 // Handle all input changes (including range slider) 416 $autosaveSections.find('input, textarea, select').not('#model, #openrouter_selected_model').on('change', function() { 416 // Store pending AJAX requests and debounce timers per field to prevent freezing 417 const pendingRequests = {}; 418 const fieldDebounceTimers = {}; 419 420 // Helper function to save rate limit fields with request tracking 421 function triggerFieldSave($field, name, pendingRequests) { 422 const value = $field.val(); 423 424 // Remove any existing feedback containers for this field 425 $field.siblings('.feedback-container').remove(); 426 $field.parent().find('.feedback-container').remove(); 427 428 // Create feedback container 429 const feedbackContainer = $('<div class="feedback-container"></div>'); 430 const spinner = $('<div class="saving-spinner"></div>'); 431 const successIcon = $('<div class="success-icon">✔</div>'); 432 $field.after(feedbackContainer); 433 feedbackContainer.append(spinner); 434 435 // Rate limits use the main settings action 436 const ajaxAction = 'mxchat_save_setting'; 437 const nonce = mxchatAdmin.setting_nonce; 438 439 // Store the AJAX request so it can be aborted if needed 440 pendingRequests[name] = $.ajax({ 441 url: mxchatAdmin.ajax_url, 442 type: 'POST', 443 data: { 444 action: ajaxAction, 445 name: name, 446 value: value, 447 _ajax_nonce: nonce 448 }, 449 success: function(response) { 450 delete pendingRequests[name]; 451 if (response.success) { 452 spinner.fadeOut(200, function() { 453 feedbackContainer.append(successIcon); 454 successIcon.fadeIn(200).delay(800).fadeOut(200, function() { 455 feedbackContainer.remove(); 456 }); 457 }); 458 } else { 459 feedbackContainer.remove(); 460 } 461 }, 462 error: function(xhr, textStatus, error) { 463 delete pendingRequests[name]; 464 // Don't show error for aborted requests 465 if (textStatus !== 'abort') { 466 feedbackContainer.remove(); 467 } 468 } 469 }); 470 } 471 472 $autosaveSections.find('input, textarea, select').not('#model, #openrouter_selected_model').on('change', function() { 417 473 const $field = $(this); 418 474 const name = $field.attr('name'); 475 476 // Debounce rate limit fields to prevent UI freezing from rapid changes 477 if (name && name.indexOf('rate_limits') !== -1) { 478 // Clear any pending timer for this field 479 if (fieldDebounceTimers[name]) { 480 clearTimeout(fieldDebounceTimers[name]); 481 } 482 // Abort any pending AJAX request for this field 483 if (pendingRequests[name]) { 484 pendingRequests[name].abort(); 485 // Remove any existing feedback containers for this field 486 $field.siblings('.feedback-container').remove(); 487 $field.parent().find('.feedback-container').remove(); 488 } 489 // Debounce the save operation 490 const fieldRef = $field; 491 fieldDebounceTimers[name] = setTimeout(function() { 492 triggerFieldSave(fieldRef, name, pendingRequests); 493 }, 300); 494 return; 495 } 419 496 420 497 // Skip saving for API key fields that haven't been interacted with and are empty … … 919 996 ], 920 997 gemini: [ 921 { value: 'gemini-2.0-flash', label: 'Gemini 2.0 Flash', description: 'Next-Gen features, speed & multimodal generation' }, 922 { value: 'gemini-2.0-flash-lite', label: 'Gemini 2.0 Flash-Lite', description: 'Cost-efficient with low latency' }, 923 { value: 'gemini-1.5-pro', label: 'Gemini 1.5 Pro', description: 'Complex reasoning tasks requiring more intelligence' }, 924 { value: 'gemini-1.5-flash', label: 'Gemini 1.5 Flash', description: 'Fast and versatile performance' }, 998 { value: 'gemini-3-pro-preview', label: 'Gemini 3 Pro', description: 'Most intelligent model - multimodal understanding & agentic' }, 999 { value: 'gemini-3-flash-preview', label: 'Gemini 3 Flash', description: 'Most balanced - speed, scale & frontier intelligence' }, 1000 { value: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro', description: 'Advanced thinking - code, math, STEM & long context' }, 1001 { value: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash', description: 'Best price-performance with thinking capabilities' }, 1002 { value: 'gemini-2.5-flash-lite', label: 'Gemini 2.5 Flash-Lite', description: 'Ultra fast - optimized for cost & high throughput' }, 1003 { value: 'gemini-2.0-flash', label: 'Gemini 2.0 Flash', description: 'Deprecated March 2026' }, 1004 { value: 'gemini-2.0-flash-lite', label: 'Gemini 2.0 Flash-Lite', description: 'Deprecated March 2026' }, 1005 { value: 'gemini-1.5-pro', label: 'Gemini 1.5 Pro', description: 'Deprecated September 2025' }, 1006 { value: 'gemini-1.5-flash', label: 'Gemini 1.5 Flash', description: 'Deprecated September 2025' }, 925 1007 ], 926 1008 openai: [ -
mxchat-basic/trunk/js/mxchat_transcripts.js
r3439963 r3446370 428 428 currentSessionId = sessionId; 429 429 430 // Reset translation state when selecting new chat 431 if (typeof resetTranslationState === 'function') { 432 resetTranslationState(); 433 } 434 430 435 // Show loading in conversation panel 431 436 $('#mxch-conversation-empty').hide(); … … 443 448 if (response.success) { 444 449 renderConversation(response); 450 // Load saved translation after rendering 451 if (typeof loadSavedTranslation === 'function') { 452 setTimeout(function() { 453 loadSavedTranslation(sessionId); 454 }, 100); 455 } 445 456 } else { 446 457 $('#mxch-messages-area').html('<div class="mxch-messages-error">Failed to load conversation</div>'); … … 632 643 633 644 // ========================================================================== 634 // RAG Context Modal 645 // Translation Functionality 646 // ========================================================================== 647 648 // Store original messages for reverting 649 let originalMessages = null; 650 let isTranslated = false; 651 let currentTranslationLang = null; 652 653 // Load saved language preference from localStorage 654 const savedLang = localStorage.getItem('mxch_translate_lang'); 655 if (savedLang) { 656 $('#mxch-translate-lang').val(savedLang); 657 } 658 659 // Save language preference when changed 660 $('#mxch-translate-lang').on('change', function() { 661 localStorage.setItem('mxch_translate_lang', $(this).val()); 662 }); 663 664 // Apply translations to messages 665 function applyTranslations(translations) { 666 // Store original messages if not already stored 667 if (!originalMessages) { 668 originalMessages = []; 669 $('#mxch-messages-area .mxch-message-bubble').each(function() { 670 originalMessages.push($(this).html()); 671 }); 672 } 673 674 // Apply translations 675 translations.forEach(function(item) { 676 const $bubble = $('#mxch-messages-area .mxch-message-bubble').eq(item.index); 677 if ($bubble.length) { 678 $bubble.html(item.translated); 679 $bubble.addClass('translated'); 680 } 681 }); 682 683 isTranslated = true; 684 $('#mxch-show-original-btn').show(); 685 } 686 687 // Load saved translation for current session 688 function loadSavedTranslation(sessionId) { 689 $.ajax({ 690 url: ajaxurl, 691 type: 'POST', 692 data: { 693 action: 'mxchat_get_transcript_translation', 694 session_id: sessionId 695 }, 696 success: function(response) { 697 if (response.success && response.has_translation) { 698 currentTranslationLang = response.language; 699 applyTranslations(response.translations); 700 // Update language selector to show saved language 701 $('#mxch-translate-lang').val(response.language); 702 } 703 } 704 }); 705 } 706 707 // Translate button click handler 708 $('#mxch-translate-btn').on('click', function() { 709 if (!currentSessionId) return; 710 711 const $btn = $(this); 712 const targetLang = $('#mxch-translate-lang').val(); 713 714 // Disable button and show loading state 715 $btn.prop('disabled', true); 716 $btn.find('.mxch-translate-text').text('Translating...'); 717 $btn.find('svg').addClass('mxch-translate-spinner'); 718 719 // Store original messages before translation 720 if (!originalMessages) { 721 originalMessages = []; 722 $('#mxch-messages-area .mxch-message-bubble').each(function() { 723 originalMessages.push($(this).html()); 724 }); 725 } 726 727 // If already translated, restore originals first before re-translating 728 if (isTranslated) { 729 $('#mxch-messages-area .mxch-message-bubble').each(function(index) { 730 if (originalMessages[index]) { 731 $(this).html(originalMessages[index]); 732 $(this).removeClass('translated'); 733 } 734 }); 735 } 736 737 // Collect all message content (from originals) 738 const messages = []; 739 originalMessages.forEach(function(html, index) { 740 // Create temp element to get text content 741 const $temp = $('<div>').html(html); 742 messages.push({ 743 index: index, 744 content: $temp.text().trim() 745 }); 746 }); 747 748 // Send translation request 749 $.ajax({ 750 url: ajaxurl, 751 type: 'POST', 752 data: { 753 action: 'mxchat_translate_messages', 754 session_id: currentSessionId, 755 target_lang: targetLang, 756 messages: JSON.stringify(messages), 757 security: mxchatAdmin.translate_nonce || '' 758 }, 759 success: function(response) { 760 if (response.success && response.translations) { 761 currentTranslationLang = response.language; 762 applyTranslations(response.translations); 763 $btn.find('.mxch-translate-text').text('Translate'); 764 } else { 765 alert(response.error || 'Translation failed. Please try again.'); 766 $btn.find('.mxch-translate-text').text('Translate'); 767 } 768 }, 769 error: function() { 770 alert('Translation request failed. Please try again.'); 771 $btn.find('.mxch-translate-text').text('Translate'); 772 }, 773 complete: function() { 774 $btn.prop('disabled', false); 775 $btn.find('svg').removeClass('mxch-translate-spinner'); 776 } 777 }); 778 }); 779 780 // Show original button click handler 781 $('#mxch-show-original-btn').on('click', function() { 782 if (!originalMessages) return; 783 784 // Restore original messages 785 $('#mxch-messages-area .mxch-message-bubble').each(function(index) { 786 if (originalMessages[index]) { 787 $(this).html(originalMessages[index]); 788 $(this).removeClass('translated'); 789 } 790 }); 791 792 isTranslated = false; 793 $(this).hide(); 794 }); 795 796 // Reset translation state (called when selecting new chat) 797 function resetTranslationState() { 798 originalMessages = null; 799 isTranslated = false; 800 currentTranslationLang = null; 801 $('#mxch-show-original-btn').hide(); 802 } 803 804 // Make functions available to selectChat 805 window.resetTranslationState = resetTranslationState; 806 window.loadSavedTranslation = loadSavedTranslation; 807 808 // ========================================================================== 809 // RAG Context Modal (Sources & Actions Tabs) 635 810 // ========================================================================== 636 811 … … 638 813 const $modal = $('#mxch-rag-modal'); 639 814 const $loading = $modal.find('.mxch-rag-loading'); 640 const $content = $modal.find('.mxch-rag-content'); 815 const $sourcesContent = $modal.find('.mxch-rag-content'); 816 const $actionsContent = $modal.find('.mxch-actions-content'); 817 818 // Reset to Sources tab 819 $modal.find('.mxch-context-tab').removeClass('active'); 820 $modal.find('.mxch-context-tab[data-tab="sources"]').addClass('active'); 821 $('#mxch-tab-sources').show(); 822 $('#mxch-tab-actions').hide(); 823 824 // Reset badge counts 825 $('#mxch-sources-count, #mxch-actions-count').hide().text('0'); 641 826 642 827 $modal.fadeIn(200); 643 828 $loading.show(); 644 $content.html(''); 829 $sourcesContent.html(''); 830 $actionsContent.html(''); 645 831 646 832 $.ajax({ … … 655 841 656 842 if (response.success && response.data) { 657 renderRagContext(response.data, $content); 843 // Render sources tab 844 renderRagContext(response.data, $sourcesContent); 845 846 // Render actions tab 847 renderActionsContext(response.data, $actionsContent); 848 849 // Update badge counts 850 const sourcesCount = response.data.top_matches ? response.data.top_matches.length : 0; 851 const actionsCount = response.data.action_analysis ? response.data.action_analysis.length : 0; 852 853 if (sourcesCount > 0) { 854 $('#mxch-sources-count').text(sourcesCount).show(); 855 } 856 if (actionsCount > 0) { 857 $('#mxch-actions-count').text(actionsCount).show(); 858 } 658 859 } else { 659 $content.html('<div class="mxch-rag-error">Unable to load document context.</div>'); 860 $sourcesContent.html('<div class="mxch-rag-error">Unable to load document context.</div>'); 861 $actionsContent.html('<div class="mxch-rag-error">No action data available.</div>'); 660 862 } 661 863 }, 662 864 error: function() { 663 865 $loading.hide(); 664 $content.html('<div class="mxch-rag-error">Error loading document context. Please try again.</div>'); 665 } 666 }); 667 } 866 $sourcesContent.html('<div class="mxch-rag-error">Error loading context. Please try again.</div>'); 867 $actionsContent.html('<div class="mxch-rag-error">Error loading context. Please try again.</div>'); 868 } 869 }); 870 } 871 872 // Tab switching 873 $(document).on('click', '.mxch-context-tab', function() { 874 const $tab = $(this); 875 const tabName = $tab.data('tab'); 876 877 // Update active tab 878 $('.mxch-context-tab').removeClass('active'); 879 $tab.addClass('active'); 880 881 // Show/hide content 882 $('.mxch-tab-content').hide(); 883 $('#mxch-tab-' + tabName).show(); 884 }); 668 885 669 886 function renderRagContext(data, $container) { 670 887 let html = ''; 888 889 // Check if we have any source data 890 if (!data.top_matches || data.top_matches.length === 0) { 891 html += '<div class="mxch-no-results"><p>No document matches found for this response.</p></div>'; 892 $container.html(html); 893 return; 894 } 671 895 672 896 html += '<div class="mxch-rag-summary">'; … … 676 900 html += '</div>'; 677 901 678 if (data.top_matches && data.top_matches.length > 0) { 679 const groupedByUrl = {}; 680 681 data.top_matches.forEach(function(match) { 682 const url = match.source_display || 'Unknown'; 683 if (!groupedByUrl[url]) { 684 groupedByUrl[url] = { 685 url: url, 686 isUrl: url.startsWith('http'), 687 bestScore: 0, 688 usedForContext: false, 689 matchedChunks: [] 690 }; 691 } 692 693 if (match.similarity_percentage > groupedByUrl[url].bestScore) { 694 groupedByUrl[url].bestScore = match.similarity_percentage; 695 } 696 697 if (match.used_for_context) { 698 groupedByUrl[url].usedForContext = true; 699 } 700 701 groupedByUrl[url].matchedChunks.push({ 702 chunkIndex: match.chunk_index, 703 score: match.similarity_percentage, 704 usedForContext: match.used_for_context 705 }); 902 const groupedByUrl = {}; 903 904 data.top_matches.forEach(function(match) { 905 const url = match.source_display || 'Unknown'; 906 if (!groupedByUrl[url]) { 907 groupedByUrl[url] = { 908 url: url, 909 isUrl: url.startsWith('http'), 910 bestScore: 0, 911 usedForContext: false, 912 matchedChunks: [] 913 }; 914 } 915 916 if (match.similarity_percentage > groupedByUrl[url].bestScore) { 917 groupedByUrl[url].bestScore = match.similarity_percentage; 918 } 919 920 if (match.used_for_context) { 921 groupedByUrl[url].usedForContext = true; 922 } 923 924 groupedByUrl[url].matchedChunks.push({ 925 chunkIndex: match.chunk_index, 926 score: match.similarity_percentage, 927 usedForContext: match.used_for_context 706 928 }); 707 708 const urlGroups = Object.values(groupedByUrl).sort((a, b) => b.bestScore - a.bestScore); 709 const usedUrlCount = urlGroups.filter(g => g.usedForContext).length; 710 711 html += '<div class="mxch-rag-matches">'; 712 html += '<h3>Retrieved Documents</h3>'; 713 html += '<p style="color: var(--mxch-text-secondary); font-size: 13px; margin-bottom: 16px;">' + usedUrlCount + ' entr' + (usedUrlCount === 1 ? 'y' : 'ies') + ' used for response</p>'; 714 715 urlGroups.forEach(function(group) { 716 const cardClass = group.usedForContext ? 'mxch-rag-match-used' : 'mxch-rag-match-below'; 717 const statusIcon = group.usedForContext ? '✓' : '✗'; 718 const statusLabel = group.usedForContext ? 'Used' : 'Not Used'; 719 720 html += '<div class="mxch-rag-match-card ' + cardClass + '">'; 721 html += '<div class="mxch-rag-match-header">'; 722 html += '<span class="mxch-rag-match-score">' + group.bestScore + '%</span>'; 723 724 if (group.matchedChunks.length > 1) { 725 html += '<span class="mxch-rag-chunk-badge">' + group.matchedChunks.length + ' chunks</span>'; 726 } 727 728 html += '<span class="mxch-rag-match-status ' + (group.usedForContext ? 'status-used' : 'status-below') + '">' + statusIcon + ' ' + statusLabel + '</span>'; 729 html += '</div>'; 730 731 html += '<div class="mxch-rag-match-source">'; 732 if (group.isUrl) { 733 html += '<a href="' + escapeHtml(group.url) + '" target="_blank">' + escapeHtml(group.url) + '</a>'; 734 } else { 735 html += escapeHtml(group.url); 736 } 737 html += '</div>'; 738 html += '</div>'; 739 }); 740 929 }); 930 931 const urlGroups = Object.values(groupedByUrl).sort((a, b) => b.bestScore - a.bestScore); 932 const usedUrlCount = urlGroups.filter(g => g.usedForContext).length; 933 934 html += '<div class="mxch-rag-matches">'; 935 html += '<h3>Retrieved Documents</h3>'; 936 html += '<p style="color: var(--mxch-text-secondary); font-size: 13px; margin-bottom: 16px;">' + usedUrlCount + ' entr' + (usedUrlCount === 1 ? 'y' : 'ies') + ' used for response</p>'; 937 938 urlGroups.forEach(function(group) { 939 const cardClass = group.usedForContext ? 'mxch-rag-match-used' : 'mxch-rag-match-below'; 940 const statusIcon = group.usedForContext ? '✓' : '✗'; 941 const statusLabel = group.usedForContext ? 'Used' : 'Not Used'; 942 943 html += '<div class="mxch-rag-match-card ' + cardClass + '">'; 944 html += '<div class="mxch-rag-match-header">'; 945 html += '<span class="mxch-rag-match-score">' + group.bestScore + '%</span>'; 946 947 if (group.matchedChunks.length > 1) { 948 html += '<span class="mxch-rag-chunk-badge">' + group.matchedChunks.length + ' chunks</span>'; 949 } 950 951 html += '<span class="mxch-rag-match-status ' + (group.usedForContext ? 'status-used' : 'status-below') + '">' + statusIcon + ' ' + statusLabel + '</span>'; 741 952 html += '</div>'; 742 } else { 743 html += '<div class="mxch-no-results"><p>No document matches found for this response.</p></div>'; 744 } 745 953 954 html += '<div class="mxch-rag-match-source">'; 955 if (group.isUrl) { 956 html += '<a href="' + escapeHtml(group.url) + '" target="_blank">' + escapeHtml(group.url) + '</a>'; 957 } else { 958 html += escapeHtml(group.url); 959 } 960 html += '</div>'; 961 html += '</div>'; 962 }); 963 964 html += '</div>'; 965 $container.html(html); 966 } 967 968 function renderActionsContext(data, $container) { 969 let html = ''; 970 971 // Check if we have action analysis data 972 if (!data.action_analysis || data.action_analysis.length === 0) { 973 html += '<div class="mxch-no-results"><p>No action analysis available for this message.</p><p style="color: var(--mxch-text-secondary); font-size: 13px; margin-top: 8px;">Actions are only evaluated when enabled in your bot configuration.</p></div>'; 974 $container.html(html); 975 return; 976 } 977 978 const actions = data.action_analysis; 979 const triggeredAction = actions.find(a => a.triggered); 980 const actionsAboveThreshold = actions.filter(a => a.above_threshold).length; 981 982 // Summary section 983 html += '<div class="mxch-rag-summary">'; 984 html += '<div class="mxch-rag-summary-item"><span class="mxch-rag-label">Actions Evaluated:</span> <span class="mxch-rag-value">' + actions.length + '</span></div>'; 985 html += '<div class="mxch-rag-summary-item"><span class="mxch-rag-label">Above Threshold:</span> <span class="mxch-rag-value">' + actionsAboveThreshold + '</span></div>'; 986 if (triggeredAction) { 987 html += '<div class="mxch-rag-summary-item"><span class="mxch-rag-label">Triggered:</span> <span class="mxch-rag-value" style="color: #10b981; font-weight: 600;">' + escapeHtml(triggeredAction.intent_label) + '</span></div>'; 988 } 989 html += '</div>'; 990 991 // Actions list 992 html += '<div class="mxch-rag-matches">'; 993 html += '<h3>Action Scores</h3>'; 994 html += '<p style="color: var(--mxch-text-secondary); font-size: 13px; margin-bottom: 16px;">Showing all evaluated actions sorted by similarity score</p>'; 995 996 actions.forEach(function(action) { 997 let cardClass = 'mxch-rag-match-below'; 998 let statusIcon = '✗'; 999 let statusLabel = 'Below Threshold'; 1000 1001 if (action.triggered) { 1002 cardClass = 'mxch-action-triggered'; 1003 statusIcon = '⚡'; 1004 statusLabel = 'Triggered'; 1005 } else if (action.above_threshold) { 1006 cardClass = 'mxch-rag-match-used'; 1007 statusIcon = '✓'; 1008 statusLabel = 'Above Threshold'; 1009 } 1010 1011 html += '<div class="mxch-rag-match-card ' + cardClass + '">'; 1012 html += '<div class="mxch-rag-match-header">'; 1013 html += '<span class="mxch-rag-match-score">' + action.similarity_percentage + '%</span>'; 1014 html += '<span class="mxch-action-threshold-badge">Threshold: ' + action.threshold_percentage + '%</span>'; 1015 html += '<span class="mxch-rag-match-status ' + (action.triggered ? 'status-triggered' : (action.above_threshold ? 'status-used' : 'status-below')) + '">' + statusIcon + ' ' + statusLabel + '</span>'; 1016 html += '</div>'; 1017 1018 html += '<div class="mxch-action-details">'; 1019 html += '<div class="mxch-action-label">' + escapeHtml(action.intent_label) + '</div>'; 1020 html += '<div class="mxch-action-callback"><span class="mxch-action-callback-label">Callback:</span> ' + escapeHtml(action.callback_function) + '</div>'; 1021 html += '</div>'; 1022 1023 // Score bar visualization 1024 const scoreBarWidth = Math.min(action.similarity_percentage, 100); 1025 const thresholdPos = Math.min(action.threshold_percentage, 100); 1026 html += '<div class="mxch-action-score-bar">'; 1027 html += '<div class="mxch-action-score-fill" style="width: ' + scoreBarWidth + '%;"></div>'; 1028 html += '<div class="mxch-action-threshold-marker" style="left: ' + thresholdPos + '%;"></div>'; 1029 html += '</div>'; 1030 1031 html += '</div>'; 1032 }); 1033 1034 html += '</div>'; 746 1035 $container.html(html); 747 1036 } -
mxchat-basic/trunk/mxchat-basic.php
r3444430 r3446370 4 4 * Plugin URI: https://mxchat.ai/ 5 5 * Description: AI chatbot for WordPress with OpenAI, Claude, xAI, DeepSeek, live agent, PDF uploads, WooCommerce, and training on website data. 6 * Version: 3.0. 36 * Version: 3.0.4 7 7 * Author: MxChat 8 8 * Author URI: https://mxchat.ai … … 431 431 432 432 /** 433 * Create transcript translations table for persisting translations 434 */ 435 function mxchat_create_translations_table() { 436 global $wpdb; 437 $charset_collate = $wpdb->get_charset_collate(); 438 439 $table_name = $wpdb->prefix . 'mxchat_transcript_translations'; 440 $sql = "CREATE TABLE $table_name ( 441 id bigint(20) unsigned NOT NULL AUTO_INCREMENT, 442 session_id varchar(255) NOT NULL, 443 language_code varchar(10) NOT NULL, 444 translations longtext NOT NULL, 445 created_at datetime NOT NULL, 446 updated_at datetime NOT NULL, 447 PRIMARY KEY (id), 448 UNIQUE KEY session_lang (session_id, language_code), 449 KEY session_id (session_id) 450 ) $charset_collate;"; 451 452 require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); 453 dbDelta($sql); 454 } 455 456 /** 433 457 * 2.5.2: Fix URL column size to support long URLs (especially with UTF-8 encoding) 434 458 * This fixes "url, source_url. The supplied values may be too long" errors … … 542 566 mxchat_create_queue_tables(); 543 567 568 // Create transcript translations table 569 mxchat_create_translations_table(); 570 544 571 // Ensure additional columns in system prompt table 545 572 $existing_system_columns = $wpdb->get_results("SHOW COLUMNS FROM $system_prompt_table"); … … 1005 1032 mxchat_migrate_pinecone_roles_add_bot_id(); 1006 1033 mxchat_migrate_add_content_type_column(); 1034 mxchat_migrate_add_translations_table(); 1035 } 1036 1037 /** 1038 * Migration: Create transcript translations table (v3.0.4) 1039 * For users upgrading from versions before 3.0.4 1040 */ 1041 function mxchat_migrate_add_translations_table() { 1042 $migration_key = 'mxchat_translations_table_created'; 1043 1044 // Check if migration already ran 1045 if (get_option($migration_key)) { 1046 return; 1047 } 1048 1049 // Create the translations table 1050 mxchat_create_translations_table(); 1051 1052 // Mark migration as complete 1053 update_option($migration_key, '3.0.4'); 1007 1054 } 1008 1055 -
mxchat-basic/trunk/readme.txt
r3444430 r3446370 6 6 Tested up to: 6.9 7 7 Requires PHP: 7.2 8 Stable tag: 3.0. 38 Stable tag: 3.0.4 9 9 License: GPLv2 or later 10 10 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 38 38 👉 [Visit our website to view all add-ons](https://mxchat.ai) 39 39 40 ## 🔥 What's New in Version 3.0.2 41 42 📱 **Telegram Live Agent Integration** 43 - New: Telegram support for live agent handoff (Settings > Integrations > Telegram) 44 - Creates forum topics in Telegram supergroups for each chat session 45 - Agents reply directly in Telegram - messages sync to chatbot in real-time 46 - Closure commands (#close, #end, #disconnect, #done) to end sessions 47 - Renamed "Live Agent" settings tab to "Slack" for clarity 48 49 🔗 **Citation Links Toggle** 50 - New: Option to enable or disable citation links in AI responses (Chatbot > Behavior) 51 - When disabled, the AI will not include source URLs in responses 52 - Useful for users who prefer cleaner responses without hyperlinks 40 ## 🔥 What's New in Version 3.0.4 41 42 🤖 **New Google Gemini Models** 43 - New: Added latest Google Gemini models (3 Pro Preview, 3 Flash Preview, 2.5 Pro, 2.5 Flash, 2.5 Flash-Lite) 44 45 🌍 **Transcript Translation** 46 - New: Auto-translate chat transcripts to any language 47 48 📊 **Action Scores in Transcripts** 49 - New: View action similarity scores in the chat transcripts sources modal 50 - See which documents ranked highest for each response 51 - Track which actions triggered or almost triggered during conversations 52 53 🐛 **Bug Fixes** 54 - Fixed: Lead capture forms not working with multi-bot class IDs 53 55 54 56 ## Core Features That Set MxChat Apart … … 76 78 **X.AI**: 77 79 Grok-4, Grok-3, Grok-3 Fast, Grok-3 Mini, Grok-3 Mini Fast, Grok-2, **Grok 4.1 Fast (Reasoning)**, **Grok 4.1 Fast (Non-Reasoning)** 78 **Google Gemini**: 79 Gemini 2.0 Flash, Gemini 2.0 Flash-Lite, Gemini 1.5 Pro, Gemini 1.5 Flash80 **Google Gemini**: 81 Gemini 3 Pro Preview, Gemini 3 Flash Preview, Gemini 2.5 Pro, Gemini 2.5 Flash, Gemini 2.5 Flash-Lite, Gemini 2.0 Flash, Gemini 2.0 Flash-Lite, Gemini 1.5 Pro, Gemini 1.5 Flash 80 82 **DeepSeek**: 81 83 DeepSeek V3 … … 267 269 268 270 == Changelog == 271 272 = 3.0.4 - January 24, 2026 = 273 - New: Added latest Google Gemini models (3 Pro Preview, 3 Flash Preview, 2.5 Pro, 2.5 Flash, 2.5 Flash-Lite) 274 - New: Transcript translation feature - auto-translate chat transcripts to any language 275 - New: Action scores visible in transcript sources modal - see document rankings and triggered actions 276 - Fixed: Lead capture forms not working due to multi-bot class ID changes 269 277 270 278 = 3.0.3 - January 21, 2026 = … … 778 786 == Upgrade Notice == 779 787 780 = 3.0. 3 - January 21, 2026 =781 - Fixed: Bug when web search was enabled the chat input was not enabled again.788 = 3.0.4 - January 23, 2026 = 789 New Gemini models (3 Pro, 3 Flash, 2.5 Pro, 2.5 Flash, 2.5 Flash-Lite), transcript translation, action scores in transcript sources modal, and lead capture fix for multi-bot setups. 782 790 783 791 == License & Warranty ==
Note: See TracChangeset
for help on using the changeset viewer.