Plugin Directory

Changeset 3446370


Ignore:
Timestamp:
01/25/2026 02:15:03 AM (4 weeks ago)
Author:
mxchat
Message:

3.0.4 - January 23, 2026

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.

Location:
mxchat-basic
Files:
127 added
10 edited

Legend:

Unmodified
Added
Removed
  • mxchat-basic/trunk/admin/class-ajax-handler.php

    r3444379 r3446370  
    9090                //error_log('MXChat Save: Checking against whitelist');
    9191                 $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',
    9293                            'gemini-2.0-flash', 'gemini-2.0-flash-lite', 'gemini-1.5-pro', 'gemini-1.5-flash',
    9394                            '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  
    949949    padding: 8px 12px;
    950950    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;
    9511026}
    9521027
     
    16041679}
    16051680
     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
    16061847/* ==========================================================================
    16071848   Responsive Adjustments
  • mxchat-basic/trunk/includes/admin-transcripts-page.php

    r3439963 r3446370  
    478478                                    </button>
    479479                                </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>
    480514                            </div>
    481515
     
    635669        <div class="mxch-modal-content">
    636670            <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>
    638672                <button type="button" class="mxch-modal-close">&times;</button>
    639673            </div>
    640674            <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
    641689                <div class="mxch-rag-loading" style="display: none;">
    642690                    <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>
    647706                </div>
    648707            </div>
  • mxchat-basic/trunk/includes/class-mxchat-admin.php

    r3444379 r3446370  
    8484        // Slack test connection
    8585        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'));
    8690    }
    8791
     
    13801384    global $wpdb;
    13811385    $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
    1382    
     1386    $translations_table = $wpdb->prefix . 'mxchat_transcript_translations';
     1387
    13831388    // Calculate the cutoff timestamp
    13841389    $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
    13861399    // Delete transcripts older than the cutoff date
    13871400    $deleted = $wpdb->query(
    13881401        $wpdb->prepare(
    1389             "DELETE FROM {$table_name} WHERE created_at < %s",
     1402            "DELETE FROM {$table_name} WHERE timestamp < %s",
    13901403            $cutoff_date
    13911404        )
    13921405    );
    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
    13941418    // Log the cleanup action
    13951419    if ($deleted !== false && $deleted > 0) {
     
    14581482}
    14591483
     1484/**
     1485 * Handle translation of chat messages via AJAX
     1486 */
     1487public 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 */
     1637private 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 */
     1664public 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 */
     1709private 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 */
     1731private 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 */
     1768private 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 */
     1806private 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 */
     1843private 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 */
     1880private 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 */
     1922private 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}
    14601957
    14611958public function mxchat_fetch_chat_history() {
     
    22022699    if (isset($_POST['delete_session_ids']) && is_array($_POST['delete_session_ids'])) {
    22032700        $deleted_count = 0;
     2701        $translations_table = $wpdb->prefix . 'mxchat_transcript_translations';
    22042702
    22052703        foreach ($_POST['delete_session_ids'] as $session_id) {
     
    22122710            // Perform the deletion from the database table
    22132711            $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            }
    22142717
    22152718            // Delete the corresponding option entry from wp_options table
     
    50375540        esc_textarea($pre_chat_message)
    50385541    );
    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>';
    50405542}
    50415543
     
    50495551        $instructions
    50505552    );
    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
    50535554    echo '<div class="mxchat-instructions-container">';
    50545555    echo '<button type="button" class="mxchat-instructions-btn" id="mxchatViewSampleBtn">';
     
    51335634        ),
    51345635        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'),
    51455645        ),
    51465646        esc_html__('X.AI Models', 'mxchat') => array(
     
    52055705    echo '</select>';
    52065706
    5207     echo '<p class="description">' . esc_html__('Select the AI model your chatbot will use for chatting.', 'mxchat') . '</p>';
    5208 
    52095707    // Add a note for OpenRouter
    52105708    echo '<p class="description" id="openrouter-model-note" style="display:none; color: #d63638; font-weight: 500;">';
     
    52965794    echo '<span class="slider"></span>';
    52975795    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>';
    53015796
    53025797    // Test button with better styling
     
    53425837    echo '<span class="slider"></span>';
    53435838    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>';
    53475839
    53485840    echo '</div>';
     
    54265918    }
    54275919    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>';
    54295920
    54305921    // API Key Status Messages for Embedding Models
     
    54685959    // Render the input field
    54695960    echo '<input type="text" id="top_bar_title" name="top_bar_title" value="' . $top_bar_title . '" />';
    5470 
    5471     // Add a description
    5472     echo '<p class="description">' . esc_html__('Enter the title text that will appear on the top bar of the chatbot.', 'mxchat') . '</p>';
    54735961}
    54745962public function mxchat_ai_agent_text_callback() {
     
    54775965    // Render the input field
    54785966    echo '<input type="text" id="ai_agent_text" name="ai_agent_text" value="' . $ai_agent_text . '" />';
    5479     // Add a description
    5480     echo '<p class="description">' . esc_html__('Enter the text that will appear for AI agents in the status indicator. Default: "AI Agent"', 'mxchat') . '</p>';
    54815967}
    54825968
     
    54995985    echo '<span class="slider"></span>';
    55005986    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>';
    55025987}
    55035988
     
    55196004            data-setting="email_blocker_header_content"
    55206005          >' . esc_textarea($content) . '</textarea>';
    5521 
    5522     echo '<p class="description">';
    5523     echo esc_html__('You may enter HTML here, such as &lt;h2&gt;Welcome&lt;/h2&gt; or &lt;p&gt;Let\'s get started&lt;/p&gt;.', 'mxchat');
    5524     echo '</p>';
    55256006}
    55266007
     
    55366017    // Use esc_attr to safely render the existing text
    55376018    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>';
    55426019}
    55436020
     
    55576034    echo '<span class="slider"></span>';
    55586035    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>';
    55606036}
    55616037//Name field placeholder callback
     
    55676043
    55686044    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>';
    55726045}
    55736046
     
    55836056    ?>
    55846057    <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>
    55886058    <?php
    55896059}
     
    56036073        esc_attr__('How can I assist?', 'mxchat')
    56046074    );
    5605 
    5606     // Output the description
    5607     echo '<p class="description">' . esc_html__('This is the placeholder text for the chat input field.', 'mxchat') . '</p>';
    56086075}
    56096076
     
    60636530    echo '<span class="slider"></span>';
    60646531    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>';
    60686532
    60696533    // Post Type Visibility Options (only visible when auto-display is ON)
     
    60736537    echo '<div class="mxchat-post-type-visibility-header">';
    60746538    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>';
    60766539    echo '</div>';
    60776540
     
    61586621    echo '<span class="slider"></span>';
    61596622    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>';
    61646623}
    61656624
     
    61856644    echo '<span class="slider"></span>';
    61866645    echo '</label>';
    6187     echo '<p class="description">' . esc_html__('Enable this option to display a privacy notice below the chat widget.', 'mxchat') . '</p>';
    61886646
    61896647    // Output the custom text input field
     
    61926650        esc_textarea($privacy_text)
    61936651    );
    6194     echo '<p class="description">' . esc_html__('Enter the privacy policy text. You can include HTML links.', 'mxchat') . '</p>';
    61956652}
    61966653
     
    62126669    echo '<span class="slider"></span>';
    62136670    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>';
    62166671}
    62176672
     
    62326687    echo '<span class="slider"></span>';
    62336688    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>';
    62356689}
    62366690
     
    62516705    echo '<span class="slider"></span>';
    62526706    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>';
    62556707}
    62566708
     
    62686720        esc_attr__('Enter Quick Question 1', 'mxchat')
    62696721    );
    6270 
    6271     // Add a description for the field
    6272     echo '<p class="description">' . esc_html__('This will be the first Quick Question in the chatbot, displayed above the input field.', 'mxchat') . '</p>';
    62736722}
    62746723
     
    62876736        esc_attr__('Enter Quick Question 2', 'mxchat')
    62886737    );
    6289 
    6290     // Add a description for the field
    6291     echo '<p class="description">' . esc_html__('This will be the second Quick Question in the chatbot.', 'mxchat') . '</p>';
    62926738}
    62936739
     
    63066752       esc_attr(__('Enter Quick Question 3', 'mxchat'))
    63076753   );
    6308 
    6309    // Add a description for the field
    6310    echo '<p class="description">' . esc_html__('This will be the third Quick Question in the chatbot.', 'mxchat') . '</p>';
    63116754}
    63126755
     
    63596802        esc_html__('Add Question', 'mxchat')
    63606803    );
    6361     echo '<p class="description">' . esc_html__('Add as many Quick Questions as you need.', 'mxchat') . '</p>';
    63626804}
    63636805
     
    69667408                'export_nonce' => wp_create_nonce('mxchat_export_transcripts'),
    69677409                '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')
    69697412            ));
    69707413
     
    72337676    } else {
    72347677        $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',
    72357683            'gemini-2.0-flash',
    72367684            'gemini-2.0-flash-lite',
  • mxchat-basic/trunk/includes/class-mxchat-integrator.php

    r3444379 r3446370  
    18211821        // Prepare RAG context data for storage (only include documents used for context)
    18221822        $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            }
    18311842        }
    18321843
     
    26472658 */
    26482659private function interpret_query_with_gemini($user_query, $system_prompt, $api_key, $model) {
    2649     // Strip "gemini-" prefix for the API
    2650     $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);
    26532664   
    26542665    $args = [
     
    64106421            // Prepare RAG context for streaming response
    64116422            $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                }
    64206440            }
    64216441            $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
     
    66676687            // Prepare RAG context for streaming response
    66686688            $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                }
    66776706            }
    66786707            $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
     
    72827311            // Prepare RAG context for streaming response
    72837312            $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                }
    72927330            }
    72937331            $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
     
    75257563            // Prepare RAG context for streaming response
    75267564            $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                }
    75357582            }
    75367583            $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
     
    77677814            // Prepare RAG context for streaming response
    77687815            $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                }
    77777833            }
    77787834            $this->mxchat_save_chat_message($session_id, 'bot', $full_response, null, $rag_context_for_storage);
     
    86628718   
    86638719    // 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;
    86658723   
    86668724    // Set up the API request
  • mxchat-basic/trunk/js/chat-script.js

    r3444430 r3446370  
    26362636
    26372637// ====================================
    2638 //  EMAIL COLLECTION SETUP - FIXED VERSION
     2638//  EMAIL COLLECTION SETUP - MULTI-INSTANCE VERSION
    26392639// ====================================
    26402640// Only run email collection setup if it's enabled
    26412641if (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);
    27092811                }
    27102812            } 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);
    27232897                }
    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);
    28622965                }
    2863                 return response.json();
    2864             })
    2865             .then((data) => {
    2866                 setSubmissionState(false);
    2867 
    2868                 if (data.success) {
    2869                     // Show chat immediately
    2870                     showChatContainer();
    2871 
    2872                     // Handle bot response if provided
    2873                     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 prevention
    2891         }
    2892 
    2893         // Real-time email validation
    2894         const emailInput = document.getElementById('user-email');
    2895         if (emailInput) {
    2896             let validationTimeout;
    2897            
    2898             emailInput.addEventListener('input', function() {
    2899                 // Clear previous validation timeout
    2900                 if (validationTimeout) {
    2901                     clearTimeout(validationTimeout);
    2902                 }
    2903                
    2904                 // Debounce validation
    2905                 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 key
    2916             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 validation
    2925         const nameInput = document.getElementById('user-name');
    2926         if (nameInput) {
    2927             let nameValidationTimeout;
    2928            
    2929             nameInput.addEventListener('input', function() {
    2930                 // Clear previous validation timeout
    2931                 if (nameValidationTimeout) {
    2932                     clearTimeout(nameValidationTimeout);
    2933                 }
    2934                
    2935                 // Debounce validation
    2936                 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 key
    2947             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 check
    2956         if (mxchatChat.skip_email_check && mxchatChat.initial_email_state) {
    2957             const emailState = mxchatChat.initial_email_state;
    2958             if (emailState.show_email_form) {
    2959                 showEmailForm();
    29602966            } 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    });
    30102973}
    30112974
  • mxchat-basic/trunk/js/mxchat-admin.js

    r3444379 r3446370  
    414414
    415415        // 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() {
    417473                    const $field = $(this);
    418474                    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                    }
    419496                   
    420497                    // Skip saving for API key fields that haven't been interacted with and are empty
     
    919996            ],
    920997            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' },
    9251007            ],
    9261008            openai: [
  • mxchat-basic/trunk/js/mxchat_transcripts.js

    r3439963 r3446370  
    428428        currentSessionId = sessionId;
    429429
     430        // Reset translation state when selecting new chat
     431        if (typeof resetTranslationState === 'function') {
     432            resetTranslationState();
     433        }
     434
    430435        // Show loading in conversation panel
    431436        $('#mxch-conversation-empty').hide();
     
    443448                if (response.success) {
    444449                    renderConversation(response);
     450                    // Load saved translation after rendering
     451                    if (typeof loadSavedTranslation === 'function') {
     452                        setTimeout(function() {
     453                            loadSavedTranslation(sessionId);
     454                        }, 100);
     455                    }
    445456                } else {
    446457                    $('#mxch-messages-area').html('<div class="mxch-messages-error">Failed to load conversation</div>');
     
    632643
    633644    // ==========================================================================
    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)
    635810    // ==========================================================================
    636811
     
    638813        const $modal = $('#mxch-rag-modal');
    639814        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');
    641826
    642827        $modal.fadeIn(200);
    643828        $loading.show();
    644         $content.html('');
     829        $sourcesContent.html('');
     830        $actionsContent.html('');
    645831
    646832        $.ajax({
     
    655841
    656842                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                    }
    658859                } 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>');
    660862                }
    661863            },
    662864            error: function() {
    663865                $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    });
    668885
    669886    function renderRagContext(data, $container) {
    670887        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        }
    671895
    672896        html += '<div class="mxch-rag-summary">';
     
    676900        html += '</div>';
    677901
    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
    706928            });
    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 ? '&#10003;' : '&#10007;';
    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 ? '&#10003;' : '&#10007;';
     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>';
    741952            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 = '&#10007;';
     999            let statusLabel = 'Below Threshold';
     1000
     1001            if (action.triggered) {
     1002                cardClass = 'mxch-action-triggered';
     1003                statusIcon = '&#9889;';
     1004                statusLabel = 'Triggered';
     1005            } else if (action.above_threshold) {
     1006                cardClass = 'mxch-rag-match-used';
     1007                statusIcon = '&#10003;';
     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>';
    7461035        $container.html(html);
    7471036    }
  • mxchat-basic/trunk/mxchat-basic.php

    r3444430 r3446370  
    44 * Plugin URI: https://mxchat.ai/
    55 * Description: AI chatbot for WordPress with OpenAI, Claude, xAI, DeepSeek, live agent, PDF uploads, WooCommerce, and training on website data.
    6  * Version: 3.0.3
     6 * Version: 3.0.4
    77 * Author: MxChat
    88 * Author URI: https://mxchat.ai
     
    431431
    432432/**
     433 * Create transcript translations table for persisting translations
     434 */
     435function 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/**
    433457 * 2.5.2: Fix URL column size to support long URLs (especially with UTF-8 encoding)
    434458 * This fixes "url, source_url. The supplied values may be too long" errors
     
    542566    mxchat_create_queue_tables();
    543567
     568    // Create transcript translations table
     569    mxchat_create_translations_table();
     570
    544571    // Ensure additional columns in system prompt table
    545572    $existing_system_columns = $wpdb->get_results("SHOW COLUMNS FROM $system_prompt_table");
     
    10051032    mxchat_migrate_pinecone_roles_add_bot_id();
    10061033    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 */
     1041function 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');
    10071054}
    10081055
  • mxchat-basic/trunk/readme.txt

    r3444430 r3446370  
    66Tested up to: 6.9
    77Requires PHP: 7.2
    8 Stable tag: 3.0.3
     8Stable tag: 3.0.4
    99License: GPLv2 or later
    1010License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    3838👉 [Visit our website to view all add-ons](https://mxchat.ai)
    3939
    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
    5355
    5456## Core Features That Set MxChat Apart
     
    7678**X.AI**: 
    7779Grok-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 Flash
     80**Google Gemini**:
     81Gemini 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
    8082**DeepSeek**: 
    8183DeepSeek V3
     
    267269
    268270== 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
    269277
    270278= 3.0.3 - January 21, 2026 =
     
    778786== Upgrade Notice ==
    779787
    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 =
     789New 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.
    782790
    783791== License & Warranty ==
Note: See TracChangeset for help on using the changeset viewer.