     1→"use strict";
     2→
     3→/**
     4→ * Email Intelligence Content Script
     5→ *
     6→ * Integrates with Gmail to provide AI-powered email analysis
     7→ * Pattern detection matches existing extension: polls every 2s for expanded emails
     8→ */
     9→
    10→(function() {
    11→  console.log("[Email Intelligence] Content script loaded");
    12→
    13→  // Version info
    14→  const EXTENSION_VERSION = "2.1.1";
    15→  const BUILD_DATE = "2024.12.30.2";
    16→
    17→  // Provider/Model Configuration
    18→  const PROVIDER_MODELS = {
    19→    claude: [
    20→      { id: 'claude-sonnet-4-5-20250929', name: 'Sonnet 4.5' },
    21→      { id: 'claude-opus-4-5-20251101', name: 'Opus 4.5 (Best)' },
    22→      { id: 'claude-haiku-4-5-20251001', name: 'Haiku 4.5 (Fast)' }
    23→    ],
    24→    openai: [
    25→      { id: 'gpt-5.2', name: 'GPT-5.2 (Latest)' },
    26→      { id: 'gpt-5.2-pro', name: 'GPT-5.2 Pro (Best)' },
    27→      { id: 'gpt-5-mini', name: 'GPT-5 Mini' },
    28→      { id: 'gpt-5-nano', name: 'GPT-5 Nano (Fast)' }
    29→    ],
    30→    gemini: [
    31→      { id: 'gemini-2.5-flash', name: 'Gemini 2.5 Flash' },
    32→      { id: 'gemini-3-pro', name: 'Gemini 3 Pro' },
    33→      { id: 'gemini-2.5-flash-lite', name: 'Flash-Lite (Fast)' }
    34→    ],
    35→    grok: [
    36→      { id: 'grok-4', name: 'Grok 4 (Latest)' },
    37→      { id: 'grok-3', name: 'Grok 3' },
    38→      { id: 'grok-3-mini', name: 'Grok 3 Mini (Fast)' }
    39→    ],
    40→    deepseek: [
    41→      { id: 'deepseek-chat', name: 'DeepSeek V3.2' },
    42→      { id: 'deepseek-reasoner', name: 'Reasoner (Thinking)' }
    43→    ],
    44→    qwen: [
    45→      { id: 'qwen-max-latest', name: 'Qwen Max' },
    46→      { id: 'qwen-turbo-latest', name: 'Qwen Turbo (Fast)' }
    47→    ],
    48→    mistral: [
    49→      { id: 'mistral-large-latest', name: 'Mistral Large' },
    50→      { id: 'mistral-small-latest', name: 'Mistral Small (Fast)' }
    51→    ],
    52→    cohere: [
    53→      { id: 'command-a-03-2025', name: 'Command A' },
    54→      { id: 'command-r7b-12-2024', name: 'Command R7B (Fast)' }
    55→    ]
    56→  };
    57→
    58→  const PROVIDER_NAMES = {
    59→    claude: 'Claude',
    60→    openai: 'OpenAI',
    61→    gemini: 'Gemini',
    62→    grok: 'Grok',
    63→    deepseek: 'DeepSeek',
    64→    qwen: 'Qwen',
    65→    mistral: 'Mistral',
    66→    cohere: 'Cohere'
    67→  };
    68→
    69→  // Current AI selection (persisted to storage)
    70→  let selectedProvider = 'claude';
    71→  let selectedModel = 'claude-sonnet-4-5-20250929';
    72→
    73→  // Conversation memory (ChatGPT-like)
    74→  let aiConversationHistory = [];
    75→  const MAX_AI_CONVERSATION_HISTORY = 10;
    76→
    77→  // Saved context (always included in AI requests)
    78→  let savedAIContext = [];
    79→
    80→  // Last question/answer for save functionality
    81→  let lastAIQuestion = '';
    82→  let lastAIAnswer = '';
    83→
    84→  // State management
    85→  let extensionContainer = null;
    86→  let shadowRoot = null;
    87→  let sidebar = null;
    88→  let showButton = null;
    89→  let isSidebarOpen = false;
    90→  let currentThreadId = null;
    91→  let currentUrlThreadId = null;  // The ID from URL (FMfcgz... style)
    92→  let currentMessageId = null;    // Gmail message ID from DOM (for resolving real thread ID)
    93→  let currentSubject = null;
    94→  let currentContactEmail = null;
    95→  let currentContactId = null;
    96→  let processedThreads = new Set();
    97→  let threadIdResolutionCache = new Map();  // Cache URL ID -> real thread ID (only from API resolution, not DOM)
    98→  let pauseAutoLoad = false; // Auto-load existing analysis by default
    99→
   100→  /**
   101→   * Load thread ID resolution cache from Chrome storage
   102→   */
   103→  async function loadThreadIdCache() {
   104→    try {
   105→      const storage = await chrome.storage.local.get('threadIdCache');
   106→      if (storage.threadIdCache && typeof storage.threadIdCache === 'object') {
   107→        // Convert plain object back to Map
   108→        Object.entries(storage.threadIdCache).forEach(([urlId, realId]) => {
   109→          threadIdResolutionCache.set(urlId, realId);
   110→        });
   111→        console.log("[Email Intelligence] Loaded", threadIdResolutionCache.size, "cached thread ID resolutions from storage");
   112→      }
   113→    } catch (e) {
   114→      console.log("[Email Intelligence] Could not load thread ID cache:", e);
   115→    }
   116→  }
   117→
   118→  /**
   119→   * Save thread ID resolution cache to Chrome storage
   120→   */
   121→  async function saveThreadIdCache() {
   122→    try {
   123→      // Convert Map to plain object for storage
   124→      const cacheObj = {};
   125→      threadIdResolutionCache.forEach((realId, urlId) => {
   126→        cacheObj[urlId] = realId;
   127→      });
   128→
   129→      // Limit cache size to prevent storage bloat (keep last 500 entries)
   130→      const entries = Object.entries(cacheObj);
   131→      if (entries.length > 500) {
   132→        const trimmed = entries.slice(-500);
   133→        const trimmedObj = Object.fromEntries(trimmed);
   134→        await chrome.storage.local.set({ threadIdCache: trimmedObj });
   135→      } else {
   136→        await chrome.storage.local.set({ threadIdCache: cacheObj });
   137→      }
   138→
   139→      console.log("[Email Intelligence] Saved", Object.keys(cacheObj).length, "thread ID resolutions to storage");
   140→    } catch (e) {
   141→      console.log("[Email Intelligence] Could not save thread ID cache:", e);
   142→    }
   143→  }
   144→
   145→  /**
   146→   * Add a thread ID resolution to cache and persist it
   147→   */
   148→  function cacheThreadIdResolution(urlId, realId) {
   149→    if (!urlId || !realId) return;
   150→
   151→    threadIdResolutionCache.set(urlId, realId);
   152→    console.log("[Email Intelligence] Cached thread ID resolution:", urlId, "->", realId);
   153→
   154→    // Save to Chrome storage asynchronously (fire and forget)
   155→    saveThreadIdCache();
   156→  }
   157→
   158→  /**
   159→   * Load AI selection from Chrome storage
   160→   */
   161→  async function loadAISelection() {
   162→    try {
   163→      const storage = await chrome.storage.local.get(['aiProvider', 'aiModel']);
   164→      if (storage.aiProvider && PROVIDER_MODELS[storage.aiProvider]) {
   165→        selectedProvider = storage.aiProvider;
   166→        selectedModel = storage.aiModel || PROVIDER_MODELS[storage.aiProvider][0].id;
   167→        console.log("[Email Intelligence] Loaded AI selection:", selectedProvider, selectedModel);
   168→      }
   169→    } catch (e) {
   170→      console.log("[Email Intelligence] Could not load AI selection:", e);
   171→    }
   172→  }
   173→
   174→  /**
   175→   * Save AI selection to Chrome storage
   176→   */
   177→  async function saveAISelection() {
   178→    try {
   179→      await chrome.storage.local.set({ aiProvider: selectedProvider, aiModel: selectedModel });
   180→      console.log("[Email Intelligence] Saved AI selection:", selectedProvider, selectedModel);
   181→    } catch (e) {
   182→      console.log("[Email Intelligence] Could not save AI selection:", e);
   183→    }
   184→  }
   185→
   186→  /**
   187→   * Update model dropdown when provider changes
   188→   */
   189→  function updateModelDropdownInSidebar() {
   190→    if (!shadowRoot) return;
   191→    const modelSelect = shadowRoot.querySelector('#ai-model-select');
   192→    if (!modelSelect) return;
   193→
   194→    const models = PROVIDER_MODELS[selectedProvider] || [];
   195→    modelSelect.innerHTML = models.map(m =>
   196→      `<option value="${m.id}" ${m.id === selectedModel ? 'selected' : ''}>${m.name}</option>`
   197→    ).join('');
   198→
   199→    // If current model not in new provider, select first
   200→    if (!models.find(m => m.id === selectedModel)) {

Prompt:
Explore the Chrome extension's "Ask AI" tab functionality thoroughly. Look at:

1. `/var/www/mssql.danmarcrm.com/dev1/extension2/chrome-extensionCopyMac20260220/chrome-extension/` - explore the full directory structure
2. Look at popup.html and popup.js for the "Ask AI" tab UI
3. Look at any JS files that handle AI chat, model selection dropdown, thread context sending
4. Look at background.js for API calls to the server
5. Look at contentScript.js for how it grabs email thread data from Gmail

I need to understand: how does the chrome extension's "Ask AI" tab work? What models are available in the dropdown? How does it send thread context to the AI? What's the UI layout?


     1→<?php
     2→/**
     3→ * Contact Profiler - Aggregated Intelligence Builder
     4→ *
     5→ * Builds comprehensive profiles for contacts by aggregating
     6→ * analysis data from all their emails.
     7→ *
     8→ * CALLING MAP:
     9→ * - Called by: cli/analyze_batch.php (after email analysis)
    10→ * - Called by: cli/build_profiles.php (dedicated profile builder)
    11→ * - Called by: public/api.php (on-demand profile generation)
    12→ * - Uses: Database.php for data access
    13→ * - Uses: AIAnalyzer.php for Claude API calls
    14→ * - Reads from: emails, email_analysis, contacts, quotes tables
    15→ * - Writes to: contact_profiles table
    16→ *
    17→ * PROFILE BUILDING FLOW:
    18→ * 1. Gather all analyzed emails for a contact
    19→ * 2. Calculate aggregate metrics (response times, sentiment trends)
    20→ * 3. Compile quote history and pricing patterns
    21→ * 4. Send aggregated data to Claude for psychological profiling
    22→ * 5. Store comprehensive profile in contact_profiles table
    23→ *
    24→ * @package EmailIntelligence
    25→ */
    26→
    27→namespace EmailIntelligence;
    28→
    29→use Exception;
    30→use GuzzleHttp\Client as HttpClient;
    31→use GuzzleHttp\HandlerStack;
    32→use GuzzleHttp\Handler\CurlHandler;
    33→
    34→class ContactProfiler
    35→{
    36→    /**
    37→     * Database instance
    38→     * @var Database
    39→     */
    40→    private Database $db;
    41→
    42→    /**
    43→     * HTTP client for API
    44→     * @var HttpClient
    45→     */
    46→    private HttpClient $http;
    47→
    48→    /**
    49→     * API configuration
    50→     * @var array
    51→     */
    52→    private array $config;
    53→
    54→    /**
    55→     * Statistics for current run
    56→     * @var array
    57→     */
    58→    private array $stats = [
    59→        'profiles_built' => 0,
    60→        'profiles_updated' => 0,
    61→        'failed' => 0,
    62→        'start_time' => null
    63→    ];
    64→
    65→    /**
    66→     * Constructor
    67→     */
    68→    public function __construct()
    69→    {
    70→        $this->db = Database::getInstance();
    71→
    72→        $this->config = [
    73→            'api_key' => defined('CLAUDE_API_KEY') ? CLAUDE_API_KEY : '',
    74→            'model' => defined('CLAUDE_MODEL') ? CLAUDE_MODEL : 'claude-3-sonnet-20240229',
    75→            'max_tokens' => defined('CLAUDE_MAX_TOKENS') ? CLAUDE_MAX_TOKENS : 4096,
    76→            'endpoint' => 'https://api.anthropic.com/v1/messages'
    77→        ];
    78→
    79→        // Create clean handler stack to avoid New Relic Guzzle infinite loop
    80→        $stack = HandlerStack::create(new CurlHandler());
    81→
    82→        $this->http = new HttpClient([
    83→            'timeout' => 180,
    84→            'handler' => $stack,
    85→            'headers' => [
    86→                'Content-Type' => 'application/json',
    87→                'x-api-key' => $this->config['api_key'],
    88→                'anthropic-version' => '2023-06-01'
    89→            ]
    90→        ]);
    91→    }
    92→
    93→    /**
    94→     * Build or update profile for a specific contact
    95→     *
    96→     * @param int $contactId Contact ID
    97→     * @param bool $forceRebuild Force rebuild even if recent profile exists
    98→     * @return array Profile data
    99→     * @throws Exception On error
   100→     */
   101→    public function buildProfile(int $contactId, bool $forceRebuild = false): array
   102→    {
   103→        // Get contact
   104→        $contact = $this->db->fetchOne(
   105→            "SELECT * FROM contacts WHERE id = ?",
   106→            [$contactId]
   107→        );
   108→
   109→        if (!$contact) {
   110→            throw new Exception("Contact not found: {$contactId}");
   111→        }
   112→
   113→        // Check if recent profile exists
   114→        if (!$forceRebuild) {
   115→            $existingProfile = $this->db->fetchOne(
   116→                "SELECT * FROM contact_profiles WHERE contact_id = ? AND last_updated > DATE_SUB(NOW(), INTERVAL 7 DAY)",
   117→                [$contactId]
   118→            );
   119→
   120→            if ($existingProfile) {
   121→                $this->logMessage("Recent profile exists for contact {$contactId}, skipping (use force to rebuild)");
   122→                return $existingProfile;
   123→            }
   124→        }
   125→
   126→        $this->logMessage("Building profile for: {$contact['email']}");
   127→
   128→        // Gather all data
   129→        $emailStats = $this->calculateEmailStats($contactId);
   130→        $analysisAggregates = $this->aggregateAnalysis($contactId);
   131→        $quoteHistory = $this->getQuoteHistory($contactId);
   132→        $emailSamples = $this->getEmailSamples($contactId);
   133→
   134→        // If not enough data, create basic profile
   135→        if ($emailStats['total_analyzed'] < 3) {
   136→            return $this->createBasicProfile($contactId, $contact, $emailStats);
   137→        }
   138→
   139→        // Build comprehensive profile using AI
   140→        $aiProfile = $this->generateAIProfile($contact, $emailStats, $analysisAggregates, $quoteHistory, $emailSamples);
   141→
   142→        // Combine calculated and AI-generated data
   143→        $profile = $this->combineProfileData($contactId, $emailStats, $analysisAggregates, $quoteHistory, $aiProfile);
   144→
   145→        // Store profile
   146→        $this->storeProfile($contactId, $profile);
   147→
   148→        $this->stats['profiles_built']++;
   149→        return $profile;
   150→    }
   151→
   152→    /**
   153→     * Calculate email statistics for contact
   154→     *
   155→     * @param int $contactId
   156→     * @return array Statistics
   157→     */
   158→    private function calculateEmailStats(int $contactId): array
   159→    {
   160→        $stats = $this->db->fetchOne(
   161→            "SELECT
   162→                COUNT(*) as total_emails,
   163→                SUM(CASE WHEN direction = 'inbound' THEN 1 ELSE 0 END) as inbound_count,
   164→                SUM(CASE WHEN direction = 'outbound' THEN 1 ELSE 0 END) as outbound_count,
   165→                SUM(CASE WHEN is_analyzed = 1 THEN 1 ELSE 0 END) as total_analyzed,
   166→                MIN(date) as first_email,
   167→                MAX(date) as last_email
   168→            FROM emails WHERE contact_id = ?",
   169→            [$contactId]
   170→        );
   171→
   172→        // Calculate response times
   173→        $responseTimes = $this->calculateResponseTimes($contactId);
   174→
   175→        return array_merge($stats, [
   176→            'avg_response_time_hours' => $responseTimes['avg'],
   177→            'median_response_time_hours' => $responseTimes['median'],
   178→            'response_rate' => $responseTimes['response_rate']
   179→        ]);
   180→    }
   181→
   182→    /**
   183→     * Calculate average response times
   184→     *
   185→     * @param int $contactId
   186→     * @return array Response time statistics
   187→     */
   188→    private function calculateResponseTimes(int $contactId): array
   189→    {
   190→        // Get email pairs to calculate response times
   191→        $emails = $this->db->fetchAll(
   192→            "SELECT id, direction, date, thread_id FROM emails
   193→             WHERE contact_id = ? ORDER BY thread_id, date",
   194→            [$contactId]
   195→        );
   196→
   197→        $responseTimes = [];
   198→        $ourEmailsWithReply = 0;
   199→        $ourTotalEmails = 0;
   200→
   201→        $threadEmails = [];
   202→        foreach ($emails as $email) {
   203→            $threadEmails[$email['thread_id']][] = $email;
   204→        }
   205→
   206→        foreach ($threadEmails as $threadId => $thread) {
   207→            for ($i = 0; $i < count($thread) - 1; $i++) {
   208→                $current = $thread[$i];
   209→                $next = $thread[$i + 1];
   210→
   211→                // If we sent and they replied
   212→                if ($current['direction'] === 'outbound' && $next['direction'] === 'inbound') {
   213→                    $ourTotalEmails++;
   214→                    $ourEmailsWithReply++;
   215→                    $timeDiff = strtotime($next['date']) - strtotime($current['date']);
   216→                    if ($timeDiff > 0 && $timeDiff < 604800) { // Less than 1 week
   217→                        $responseTimes[] = $timeDiff / 3600; // Convert to hours
   218→                    }
   219→                }
   220→                // Count our emails that didn't get reply
   221→                if ($current['direction'] === 'outbound' && $next['direction'] === 'outbound') {
   222→                    $ourTotalEmails++;
   223→                }
   224→            }
   225→        }
   226→
   227→        if (empty($responseTimes)) {
   228→            return ['avg' => null, 'median' => null, 'response_rate' => null];
   229→        }
   230→
   231→        sort($responseTimes);
   232→        $count = count($responseTimes);
   233→        $median = $count % 2 === 0
   234→            ? ($responseTimes[$count / 2 - 1] + $responseTimes[$count / 2]) / 2
   235→            : $responseTimes[floor($count / 2)];
   236→
   237→        $responseRate = $ourTotalEmails > 0 ? ($ourEmailsWithReply / $ourTotalEmails) * 100 : null;
   238→
   239→        return [
   240→            'avg' => round(array_sum($responseTimes) / $count, 2),
   241→            'median' => round($median, 2),
   242→            'response_rate' => round($responseRate, 1)
   243→        ];
   244→    }
   245→
   246→    /**
   247→     * Aggregate analysis data across all emails
   248→     *
   249→     * @param int $contactId
   250→     * @return array Aggregated analysis
   251→     */
   252→    private function aggregateAnalysis(int $contactId): array
   253→    {
   254→        // Sentiment aggregation
   255→        $sentimentStats = $this->db->fetchOne(
   256→            "SELECT
   257→                AVG(ea.sentiment_score) as avg_sentiment,
   258→                MIN(ea.sentiment_score) as min_sentiment,
   259→                MAX(ea.sentiment_score) as max_sentiment,
   260→                AVG(ea.urgency_level) as avg_urgency,
   261→                AVG(ea.formality_level) as avg_formality,
   262→                SUM(CASE WHEN ea.deception_score > 0.5 THEN 1 ELSE 0 END) as high_deception_count,
   263→                AVG(ea.deception_score) as avg_deception_score
   264→            FROM email_analysis ea
   265→            JOIN emails e ON ea.email_id = e.id
   266→            WHERE e.contact_id = ?",
   267→            [$contactId]
   268→        );
   269→
   270→        // Tone distribution
   271→        $toneDistribution = $this->db->fetchAll(
   272→            "SELECT ea.tone, COUNT(*) as count
   273→             FROM email_analysis ea
   274→             JOIN emails e ON ea.email_id = e.id
   275→             WHERE e.contact_id = ? AND ea.tone IS NOT NULL
   276→             GROUP BY ea.tone ORDER BY count DESC",
   277→            [$contactId]
   278→        );
   279→
   280→        // Sentiment trend (last 10 emails vs first 10)
   281→        $sentimentTrend = $this->calculateSentimentTrend($contactId);
   282→
   283→        // Collect all deception flags
   284→        $deceptionFlags = $this->db->fetchAll(
   285→            "SELECT ea.deception_flags, e.subject, e.date
   286→             FROM email_analysis ea
   287→             JOIN emails e ON ea.email_id = e.id
   288→             WHERE e.contact_id = ? AND ea.deception_flags IS NOT NULL AND ea.deception_flags != '[]'
   289→             ORDER BY e.date DESC LIMIT 20",
   290→            [$contactId]
   291→        );
   292→
   293→        return [
   294→            'sentiment' => $sentimentStats,
   295→            'tone_distribution' => $toneDistribution,
   296→            'sentiment_trend' => $sentimentTrend,
   297→            'deception_flags' => $deceptionFlags
   298→        ];
   299→    }
   300→
   301→    /**
   302→     * Calculate sentiment trend over time
   303→     *
   304→     * @param int $contactId
   305→     * @return string improving, declining, or stable
   306→     */
   307→    private function calculateSentimentTrend(int $contactId): string
   308→    {
   309→        $recentSentiment = $this->db->fetchColumn(
   310→            "SELECT AVG(ea.sentiment_score)
   311→             FROM email_analysis ea
   312→             JOIN emails e ON ea.email_id = e.id
   313→             WHERE e.contact_id = ?
   314→             ORDER BY e.date DESC LIMIT 5",
   315→            [$contactId]
   316→        );
   317→
   318→        $olderSentiment = $this->db->fetchColumn(
   319→            "SELECT AVG(ea.sentiment_score)
   320→             FROM (
   321→                SELECT ea2.sentiment_score
   322→                FROM email_analysis ea2
   323→                JOIN emails e2 ON ea2.email_id = e2.id
   324→                WHERE e2.contact_id = ?
   325→                ORDER BY e2.date ASC LIMIT 5
   326→             ) as older",
   327→            [$contactId]
   328→        );
   329→
   330→        if ($recentSentiment === null || $olderSentiment === null) {
   331→            return 'stable';
   332→        }
   333→
   334→        $diff = $recentSentiment - $olderSentiment;
   335→        if ($diff > 0.2) return 'improving';
   336→        if ($diff < -0.2) return 'declining';
   337→        return 'stable';
   338→    }
   339→
   340→    /**
   341→     * Get quote history for contact
   342→     *
   343→     * @param int $contactId
   344→     * @return array Quote statistics
   345→     */
   346→    private function getQuoteHistory(int $contactId): array
   347→    {
   348→        $quotes = $this->db->fetchAll(
   349→            "SELECT * FROM quotes WHERE contact_id = ? ORDER BY quote_date DESC",
   350→            [$contactId]
   351→        );
   352→
   353→        if (empty($quotes)) {
   354→            return ['quotes' => [], 'stats' => null];
   355→        }
   356→
   357→        // Calculate statistics
   358→        $sentQuotes = array_filter($quotes, fn($q) => $q['direction'] === 'sent');
   359→        $acceptedQuotes = array_filter($sentQuotes, fn($q) => $q['outcome'] === 'accepted');
   360→        $rejectedQuotes = array_filter($sentQuotes, fn($q) => $q['outcome'] === 'rejected');
   361→        $negotiatedQuotes = array_filter($sentQuotes, fn($q) => $q['outcome'] === 'negotiated');
   362→
   363→        $avgDiscount = 0;
   364→        if (!empty($negotiatedQuotes)) {
   365→            $discounts = [];
   366→            foreach ($negotiatedQuotes as $q) {
   367→                if ($q['amount'] > 0 && $q['final_amount'] > 0) {
   368→                    $discounts[] = (($q['amount'] - $q['final_amount']) / $q['amount']) * 100;
   369→                }
   370→            }
   371→            $avgDiscount = !empty($discounts) ? array_sum($discounts) / count($discounts) : 0;
   372→        }
   373→
   374→        return [
   375→            'quotes' => $quotes,
   376→            'stats' => [
   377→                'total_quotes_sent' => count($sentQuotes),
   378→                'accepted' => count($acceptedQuotes),
   379→                'rejected' => count($rejectedQuotes),
   380→                'negotiated' => count($negotiatedQuotes),
   381→                'acceptance_rate' => count($sentQuotes) > 0
   382→                    ? round((count($acceptedQuotes) / count($sentQuotes)) * 100, 1)
   383→                    : null,
   384→                'avg_discount_percent' => round($avgDiscount, 1)
   385→            ]
   386→        ];
   387→    }
   388→
   389→    /**
   390→     * Get sample emails for AI analysis
   391→     *
   392→     * @param int $contactId
   393→     * @param int $limit
   394→     * @return array Email samples with summaries
   395→     */
   396→    private function getEmailSamples(int $contactId, int $limit = 15): array
   397→    {
   398→        return $this->db->fetchAll(
   399→            "SELECT e.subject, e.date, e.direction, ea.summary, ea.tone,
   400→                    ea.sentiment_label, ea.key_points, ea.deception_flags
   401→             FROM emails e
   402→             LEFT JOIN email_analysis ea ON e.id = ea.email_id
   403→             WHERE e.contact_id = ? AND e.is_analyzed = 1
   404→             ORDER BY e.date DESC
   405→             LIMIT ?",
   406→            [$contactId, $limit]
   407→        );
   408→    }
   409→
   410→    /**
   411→     * Generate AI-powered psychological profile
   412→     *
   413→     * @param array $contact Contact data
   414→     * @param array $emailStats Email statistics
   415→     * @param array $analysisAggregates Aggregated analysis
   416→     * @param array $quoteHistory Quote history
   417→     * @param array $emailSamples Sample emails
   418→     * @return array AI-generated profile elements
   419→     */
   420→    private function generateAIProfile(
   421→        array $contact,
   422→        array $emailStats,
   423→        array $analysisAggregates,
   424→        array $quoteHistory,
   425→        array $emailSamples
   426→    ): array {
   427→        if (empty($this->config['api_key'])) {
   428→            return $this->getDefaultAIProfile();
   429→        }
   430→
   431→        $prompt = $this->buildProfilePrompt($contact, $emailStats, $analysisAggregates, $quoteHistory, $emailSamples);
   432→
   433→        try {
   434→            // Clean UTF-8 in prompt to avoid json_encode errors
   435→            $cleanPrompt = $this->cleanUtf8($prompt);
   436→
   437→            // Manually encode JSON to use safe encoding with UTF-8 substitute
   438→            $requestBody = [
   439→                'model' => $this->config['model'],
   440→                'max_tokens' => $this->config['max_tokens'],
   441→                'messages' => [['role' => 'user', 'content' => $cleanPrompt]]
   442→            ];
   443→            $jsonBody = $this->safeJsonEncode($requestBody);
   444→
   445→            $response = $this->http->post($this->config['endpoint'], [
   446→                'headers' => [
   447→                    'Content-Type' => 'application/json'
   448→                ],
   449→                'body' => $jsonBody
   450→            ]);
   451→
   452→            $body = json_decode($response->getBody()->getContents(), true);
   453→            $content = $body['content'][0]['text'] ?? '';
   454→
   455→            // Parse JSON response
   456→            $jsonStart = strpos($content, '{');
   457→            $jsonEnd = strrpos($content, '}');
   458→            if ($jsonStart !== false && $jsonEnd !== false) {
   459→                $json = substr($content, $jsonStart, $jsonEnd - $jsonStart + 1);
   460→                return json_decode($json, true) ?? $this->getDefaultAIProfile();
   461→            }
   462→
   463→        } catch (Exception $e) {
   464→            $this->logMessage("AI profile generation failed: " . $e->getMessage(), 'error');
   465→        }
   466→
   467→        return $this->getDefaultAIProfile();
   468→    }
   469→
   470→    /**
   471→     * Build prompt for profile generation
   472→     *
   473→     * @param array $contact
   474→     * @param array $emailStats
   475→     * @param array $analysisAggregates
   476→     * @param array $quoteHistory
   477→     * @param array $emailSamples
   478→     * @return string
   479→     */
   480→    private function buildProfilePrompt(
   481→        array $contact,
   482→        array $emailStats,
   483→        array $analysisAggregates,
   484→        array $quoteHistory,
   485→        array $emailSamples
   486→    ): string {
   487→        $contactInfo = "Email: {$contact['email']}";
   488→        if ($contact['name']) $contactInfo .= ", Name: {$contact['name']}";
   489→        if ($contact['company']) $contactInfo .= ", Company: {$contact['company']}";
   490→
   491→        $statsInfo = "Total emails: {$emailStats['total_emails']}, Inbound: {$emailStats['inbound_count']}, Outbound: {$emailStats['outbound_count']}";
   492→        if ($emailStats['avg_response_time_hours']) {
   493→            $statsInfo .= ", Avg response time: {$emailStats['avg_response_time_hours']} hours";
   494→        }
   495→
   496→        $sentimentInfo = "";
   497→        if ($analysisAggregates['sentiment']['avg_sentiment'] !== null) {
   498→            $avgSent = round($analysisAggregates['sentiment']['avg_sentiment'], 2);
   499→            $sentimentInfo = "Average sentiment: {$avgSent}, Trend: {$analysisAggregates['sentiment_trend']}";
   500→        }
   501→
   502→        $quoteInfo = "";
   503→        if ($quoteHistory['stats']) {
   504→            $qs = $quoteHistory['stats'];
   505→            $quoteInfo = "Quotes sent: {$qs['total_quotes_sent']}, Accepted: {$qs['accepted']}, Rejected: {$qs['rejected']}, Avg discount: {$qs['avg_discount_percent']}%";
   506→        }
   507→
   508→        // Format email samples
   509→        $sampleText = "";
   510→        foreach (array_slice($emailSamples, 0, 10) as $sample) {
   511→            $sampleText .= "- [{$sample['date']}] ({$sample['direction']}) {$sample['subject']}\n";
   512→            if ($sample['summary']) {
   513→                $sampleText .= "  Summary: {$sample['summary']}\n";
   514→            }
   515→        }
   516→
   517→        // Format deception flags
   518→        $deceptionText = "";
   519→        $allFlags = [];
   520→        foreach ($analysisAggregates['deception_flags'] as $flagData) {
   521→            $flags = json_decode($flagData['deception_flags'], true) ?? [];
   522→            foreach ($flags as $flag) {
   523→                $allFlags[] = $flag['flag'] ?? $flag;
   524→            }
   525→        }
   526→        if (!empty($allFlags)) {
   527→            $deceptionText = "Detected concerns: " . implode("; ", array_slice(array_unique($allFlags), 0, 5));
   528→        }
   529→
   530→        return <<<PROMPT
   531→Create a comprehensive business profile for this contact based on email communication analysis.
   532→
   533→CONTACT INFO:
   534→{$contactInfo}
   535→
   536→COMMUNICATION STATISTICS:
   537→{$statsInfo}
   538→{$sentimentInfo}
   539→
   540→PRICING/QUOTES:
   541→{$quoteInfo}
   542→
   543→CONCERNS DETECTED:
   544→{$deceptionText}
   545→
   546→RECENT EMAIL SAMPLES:
   547→{$sampleText}
   548→
   549→Based on this data, provide a JSON profile with these fields:
   550→
   551→{
   552→  "psychological_profile": "<2-3 paragraphs describing personality, communication preferences, decision-making style>",
   553→  "communication_style": "<brief description: formal/casual, responsive/slow, direct/indirect>",
   554→  "personality_traits": ["<trait1>", "<trait2>", "<trait3>"],
   555→  "negotiation_patterns": "<aggressive|collaborative|avoidant|accommodating|competitive> with explanation",
   556→  "decision_making_speed": "<fast|moderate|slow>",
   557→  "pricing_sensitivity": "<very_high|high|medium|low|very_low>",
   558→  "risk_factors": ["<risk1>", "<risk2>"],
   559→  "relationship_health": "<excellent|strong|stable|at-risk|poor|new>",
   560→  "ai_recommendations": "<specific actionable advice for dealing with this contact>",
   561→  "best_approach": {
   562→    "communication": "<how to communicate with them>",
   563→    "negotiation": "<how to negotiate with them>",
   564→    "timing": "<best times/frequency to contact>"
   565→  },
   566→  "topics_to_avoid": ["<topic1>", "<topic2>"]
   567→}
   568→
   569→Be objective and base conclusions on the data provided. If data is insufficient for a field, provide your best assessment or null.
   570→PROMPT;
   571→    }
   572→
   573→    /**
   574→     * Get default AI profile when API unavailable
   575→     *
   576→     * @return array
   577→     */
   578→    private function getDefaultAIProfile(): array
   579→    {
   580→        return [
   581→            'psychological_profile' => null,
   582→            'communication_style' => null,
   583→            'personality_traits' => [],
   584→            'negotiation_patterns' => null,
   585→            'decision_making_speed' => null,
   586→            'pricing_sensitivity' => null,
   587→            'risk_factors' => [],
   588→            'relationship_health' => 'new',
   589→            'ai_recommendations' => null,
   590→            'best_approach' => null,
   591→            'topics_to_avoid' => []
   592→        ];
   593→    }
   594→
   595→    /**
   596→     * Combine calculated data with AI profile
   597→     *
   598→     * @param int $contactId
   599→     * @param array $emailStats
   600→     * @param array $analysisAggregates
   601→     * @param array $quoteHistory
   602→     * @param array $aiProfile
   603→     * @return array Combined profile
   604→     */
   605→    private function combineProfileData(
   606→        int $contactId,
   607→        array $emailStats,
   608→        array $analysisAggregates,
   609→        array $quoteHistory,
   610→        array $aiProfile
   611→    ): array {
   612→        // Determine pricing sensitivity from quote data if not from AI
   613→        $pricingSensitivity = $aiProfile['pricing_sensitivity'] ?? null;
   614→        if (!$pricingSensitivity && $quoteHistory['stats']) {
   615→            $avgDiscount = $quoteHistory['stats']['avg_discount_percent'] ?? 0;
   616→            if ($avgDiscount > 20) $pricingSensitivity = 'very_high';
   617→            elseif ($avgDiscount > 10) $pricingSensitivity = 'high';
   618→            elseif ($avgDiscount > 5) $pricingSensitivity = 'medium';
   619→            else $pricingSensitivity = 'low';
   620→        }
   621→
   622→        // Calculate reliability score
   623→        $reliabilityScore = $this->calculateReliabilityScore($emailStats, $analysisAggregates);
   624→
   625→        // Determine risk level
   626→        $riskLevel = 'low';
   627→        $avgDeception = $analysisAggregates['sentiment']['avg_deception_score'] ?? 0;
   628→        if ($avgDeception > 0.5 || count($aiProfile['risk_factors'] ?? []) > 2) {
   629→            $riskLevel = 'high';
   630→        } elseif ($avgDeception > 0.3 || count($aiProfile['risk_factors'] ?? []) > 0) {
   631→            $riskLevel = 'medium';
   632→        }
   633→
   634→        // Compile inconsistencies
   635→        $inconsistencies = [];
   636→        foreach ($analysisAggregates['deception_flags'] as $flagData) {
   637→            $flags = json_decode($flagData['deception_flags'], true) ?? [];
   638→            foreach ($flags as $flag) {
   639→                $inconsistencies[] = [
   640→                    'flag' => is_array($flag) ? ($flag['flag'] ?? '') : $flag,
   641→                    'date' => $flagData['date'],
   642→                    'subject' => $flagData['subject']
   643→                ];
   644→            }
   645→        }
   646→
   647→        return [
   648→            'contact_id' => $contactId,
   649→            'psychological_profile' => $aiProfile['psychological_profile'],
   650→            'communication_style' => $aiProfile['communication_style'],
   651→            'personality_traits' => json_encode($aiProfile['personality_traits'] ?? []),
   652→            'negotiation_patterns' => $aiProfile['negotiation_patterns'],
   653→            'decision_making_speed' => $aiProfile['decision_making_speed'],
   654→            'reliability_score' => $reliabilityScore,
   655→            'commitment_follow_through' => null, // Requires more data tracking
   656→            'response_reliability' => $emailStats['response_rate'],
   657→            'pricing_sensitivity' => $pricingSensitivity,
   658→            'typical_discount_request' => $quoteHistory['stats']['avg_discount_percent'] ?? null,
   659→            'price_objection_frequency' => null, // Could be calculated from analysis
   660→            'avg_response_time_hours' => $emailStats['avg_response_time_hours'],
   661→            'response_rate' => $emailStats['response_rate'],
   662→            'detected_inconsistencies' => json_encode(array_slice($inconsistencies, 0, 20)),
   663→            'inconsistency_count' => count($inconsistencies),
   664→            'risk_level' => $riskLevel,
   665→            'risk_factors' => json_encode($aiProfile['risk_factors'] ?? []),
   666→            'relationship_health' => $aiProfile['relationship_health'] ?? $this->determineRelationshipHealth($emailStats, $analysisAggregates),
   667→            'relationship_trend' => $analysisAggregates['sentiment_trend'],
   668→            'ai_recommendations' => $aiProfile['ai_recommendations'],
   669→            'best_approach' => json_encode($aiProfile['best_approach'] ?? []),
   670→            'topics_to_avoid' => json_encode($aiProfile['topics_to_avoid'] ?? []),
   671→            'emails_analyzed' => $emailStats['total_analyzed'],
   672→            'profile_confidence' => min(1, $emailStats['total_analyzed'] / 20), // Max confidence at 20+ emails
   673→            'quote_history' => json_encode($quoteHistory['stats'])
   674→        ];
   675→    }
   676→
   677→    /**
   678→     * Calculate reliability score
   679→     *
   680→     * @param array $emailStats
   681→     * @param array $analysisAggregates
   682→     * @return float Score 0-100
   683→     */
   684→    private function calculateReliabilityScore(array $emailStats, array $analysisAggregates): float
   685→    {
   686→        $score = 50; // Base score
   687→
   688→        // Response rate factor
   689→        if ($emailStats['response_rate'] !== null) {
   690→            $score += ($emailStats['response_rate'] - 50) * 0.3;
   691→        }
   692→
   693→        // Sentiment factor
   694→        $avgSentiment = $analysisAggregates['sentiment']['avg_sentiment'] ?? 0;
   695→        $score += $avgSentiment * 20;
   696→
   697→        // Deception factor
   698→        $avgDeception = $analysisAggregates['sentiment']['avg_deception_score'] ?? 0;
   699→        $score -= $avgDeception * 30;
   700→
   701→        // Sentiment trend factor
   702→        if ($analysisAggregates['sentiment_trend'] === 'improving') {
   703→            $score += 10;
   704→        } elseif ($analysisAggregates['sentiment_trend'] === 'declining') {
   705→            $score -= 10;
   706→        }
   707→
   708→        return max(0, min(100, round($score, 2)));
   709→    }
   710→
   711→    /**
   712→     * Determine relationship health from data
   713→     *
   714→     * @param array $emailStats
   715→     * @param array $analysisAggregates
   716→     * @return string
   717→     */
   718→    private function determineRelationshipHealth(array $emailStats, array $analysisAggregates): string
   719→    {
   720→        if ($emailStats['total_emails'] < 5) return 'new';
   721→
   722→        $avgSentiment = $analysisAggregates['sentiment']['avg_sentiment'] ?? 0;
   723→        $trend = $analysisAggregates['sentiment_trend'];
   724→
   725→        if ($avgSentiment > 0.5 && $trend !== 'declining') return 'excellent';
   726→        if ($avgSentiment > 0.2) return 'strong';
   727→        if ($avgSentiment > -0.2) return 'stable';
   728→        if ($avgSentiment > -0.5 || $trend === 'declining') return 'at-risk';
   729→        return 'poor';
   730→    }
   731→
   732→    /**
   733→     * Create basic profile when insufficient data
   734→     *
   735→     * @param int $contactId
   736→     * @param array $contact
   737→     * @param array $emailStats
   738→     * @return array
   739→     */
   740→    private function createBasicProfile(int $contactId, array $contact, array $emailStats): array
   741→    {
   742→        $profile = [
   743→            'contact_id' => $contactId,
   744→            'psychological_profile' => 'Insufficient data for comprehensive profile. Need more analyzed emails.',
   745→            'communication_style' => null,
   746→            'relationship_health' => 'new',
   747→            'emails_analyzed' => $emailStats['total_analyzed'],
   748→            'profile_confidence' => $emailStats['total_analyzed'] / 20,
   749→            'avg_response_time_hours' => $emailStats['avg_response_time_hours'],
   750→            'response_rate' => $emailStats['response_rate']
   751→        ];
   752→
   753→        $this->storeProfile($contactId, $profile);
   754→        return $profile;
   755→    }
   756→
   757→    /**
   758→     * Store profile in database
   759→     *
   760→     * @param int $contactId
   761→     * @param array $profile
   762→     */
   763→    private function storeProfile(int $contactId, array $profile): void
   764→    {
   765→        // Ensure contact_id is set
   766→        $profile['contact_id'] = $contactId;
   767→
   768→        $this->db->insertOrUpdate('contact_profiles', $profile, array_keys($profile));
   769→    }
   770→
   771→    /**
   772→     * Build profiles for all contacts that need them
   773→     *
   774→     * @param int $batchSize
   775→     * @return array Statistics
   776→     */
   777→    public function buildAllProfiles(int $batchSize = 10): array
   778→    {
   779→        $this->stats = [
   780→            'profiles_built' => 0,
   781→            'profiles_updated' => 0,
   782→            'failed' => 0,
   783→            'start_time' => microtime(true)
   784→        ];
   785→
   786→        // Get contacts needing profile update
   787→        $contacts = $this->db->fetchAll(
   788→            "SELECT c.id, c.email, c.total_emails,
   789→                    (SELECT COUNT(*) FROM emails e WHERE e.contact_id = c.id AND e.is_analyzed = 1) as analyzed_count
   790→             FROM contacts c
   791→             LEFT JOIN contact_profiles cp ON c.id = cp.contact_id
   792→             WHERE c.is_active = 1
   793→               AND (cp.id IS NULL OR cp.last_updated < DATE_SUB(NOW(), INTERVAL 7 DAY))
   794→               AND c.total_emails >= 3
   795→             ORDER BY c.last_contact DESC
   796→             LIMIT ?",
   797→            [$batchSize]
   798→        );
   799→
   800→        $this->logMessage("Found " . count($contacts) . " contacts needing profile updates");
   801→
   802→        foreach ($contacts as $contact) {
   803→            try {
   804→                $this->buildProfile($contact['id']);
   805→            } catch (Exception $e) {
   806→                $this->stats['failed']++;
   807→                $this->logMessage("Failed to build profile for {$contact['email']}: " . $e->getMessage(), 'error');
   808→            }
   809→        }
   810→
   811→        $duration = round(microtime(true) - $this->stats['start_time'], 2);
   812→        $this->logMessage("Profile building complete in {$duration}s. Built: {$this->stats['profiles_built']}, Failed: {$this->stats['failed']}");
   813→
   814→        return $this->stats;
   815→    }
   816→
   817→    /**
   818→     * Get profile for a contact
   819→     *
   820→     * @param int $contactId
   821→     * @return array|null
   822→     */
   823→    public function getProfile(int $contactId): ?array
   824→    {
   825→        return $this->db->fetchOne(
   826→            "SELECT cp.*, c.email, c.name, c.company, c.type, c.total_emails
   827→             FROM contact_profiles cp
   828→             JOIN contacts c ON cp.contact_id = c.id
   829→             WHERE cp.contact_id = ?",
   830→            [$contactId]
   831→        );
   832→    }
   833→
   834→    /**
   835→     * Get statistics
   836→     *
   837→     * @return array
   838→     */
   839→    public function getStats(): array
   840→    {
   841→        return $this->stats;
   842→    }
   843→
   844→    /**
   845→     * Log message
   846→     *
   847→     * @param string $message
   848→     * @param string $level
   849→     */
   850→    private function logMessage(string $message, string $level = 'info'): void
   851→    {
   852→        $logFile = defined('LOG_PATH') ? LOG_PATH . 'profiler.log' : __DIR__ . '/../logs/profiler.log';
   853→        $timestamp = date('Y-m-d H:i:s');
   854→        $levelUpper = strtoupper($level);
   855→        file_put_contents($logFile, "[{$timestamp}] [{$levelUpper}] {$message}\n", FILE_APPEND);
   856→
   857→        if (php_sapi_name() === 'cli') {
   858→            echo "[{$levelUpper}] {$message}\n";
   859→        }
   860→    }
   861→
   862→    /**
   863→     * Safe JSON encode with UTF-8 handling
   864→     */
   865→    private function safeJsonEncode($data): string
   866→    {
   867→        $cleaned = $this->cleanUtf8($data);
   868→        $json = json_encode($cleaned, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
   869→        if ($json === false) {
   870→            $this->logMessage("json_encode error: " . json_last_error_msg(), 'error');
   871→            return '{}';
   872→        }
   873→        return $json;
   874→    }
   875→
   876→    /**
   877→     * Clean UTF-8 characters recursively
   878→     */
   879→    private function cleanUtf8($data)
   880→    {
   881→        if (is_string($data)) {
   882→            $data = mb_convert_encoding($data, 'UTF-8', 'UTF-8');
   883→            $data = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $data);
   884→            return $data;
   885→        }
   886→        if (is_array($data)) {
   887→            return array_map([$this, 'cleanUtf8'], $data);
   888→        }
   889→        return $data;
   890→    }
   891→}
   892→

Prompt:
Explore how contacts and threads are searched/queried in the backend. Look at:

1. `/var/www/mssql.danmarcrm.com/dev1/extension2/src/QueryAdapter.php` - how threads are queried, filtered by contact
2. `/var/www/mssql.danmarcrm.com/dev1/extension2/src/ThreadAnalyzer.php` - how thread analysis works
3. `/var/www/mssql.danmarcrm.com/dev1/extension2/src/ContactProfiler.php` - how contact profiles are built
4. `/var/www/mssql.danmarcrm.com/dev1/extension2/src/MongoDatabase.php` and `MongoRepository.php` - how email threads are stored/queried in MongoDB
5. `/var/www/mssql.danmarcrm.com/dev1/extension2/public/api.php` - search for endpoints related to search, contacts, threads, filtering

I need to understand: how can we query all threads for a specific contact email? How are threads stored? What data structure do they have? How does the existing search work?