  2300→                    'answer' => $response
  2301→                ]);
  2302→            } catch (Exception $e) {
  2303→                jsonError('AI request failed: ' . $e->getMessage());
  2304→            }
  2305→            break;
  2306→
  2307→        // ==========================================
  2308→        // AI Chat - Multi-model chat with full context
  2309→        // ==========================================
  2310→        case 'ai_chat':
  2311→            if ($_SERVER['REQUEST_METHOD'] !== 'POST') jsonError('POST required', 405);
  2312→
  2313→            $jsonData = json_decode(file_get_contents('php://input'), true) ?? [];
  2314→            $chatMessage  = trim($jsonData['message'] ?? '');
  2315→            $chatModel    = $jsonData['model'] ?? 'gpt-4o';
  2316→            $chatMode     = $jsonData['mode'] ?? 'general';   // thread|contact|quote|reply|general
  2317→            $chatThreadId = $jsonData['thread_id'] ?? null;
  2318→            $chatContactId= (int)($jsonData['contact_id'] ?? 0);
  2319→            $chatHistory  = $jsonData['history'] ?? [];       // [{role,content}]
  2320→            $chatEmailId  = (int)($jsonData['email_id'] ?? 0);
  2321→
  2322→            if (!$chatMessage) jsonError('message required');
  2323→            if ($chatThreadId) $chatThreadId = resolveThreadId($chatThreadId);
  2324→
  2325→            // --- Build context block ---
  2326→            $ctxParts = [];
  2327→            $ctxAdapter = QueryAdapter::getInstance();
  2328→            $ctxMongo = \EmailIntelligence\MongoDatabase::getInstance();
  2329→            $useMongoDB = $ctxAdapter->getActiveDatabase() === 'mongodb';
  2330→
  2331→            // Thread context - use MongoDB-aware queries
  2332→            if ($chatThreadId) {
  2333→                if ($useMongoDB) {
  2334→                    $ctxEmails = $ctxMongo->findAll('emails', ['thread_id' => $chatThreadId], [
  2335→                        'sort' => ['date' => 1]
  2336→                    ]);
  2337→                } else {
  2338→                    $ctxEmails = $db->fetchAll(
  2339→                        "SELECT direction, date, from_email, to_email, subject, body_text FROM emails WHERE thread_id = ? ORDER BY date ASC",
  2340→                        [$chatThreadId]
  2341→                    );
  2342→                }
  2343→                if ($ctxEmails) {
  2344→                    $ctxParts[] = "=== EMAIL THREAD (" . count($ctxEmails) . " emails) ===";
  2345→                    foreach ($ctxEmails as $i => $em) {
  2346→                        $dir = ($em['direction'] ?? '') === 'inbound' ? 'FROM THEM' : 'FROM US';
  2347→                        $body = substr($em['body_text'] ?? '', 0, 800);
  2348→                        $ctxParts[] = "Email #".($i+1)." {$dir} ({$em['date']})\nSubject: {$em['subject']}\n{$body}";
  2349→                    }
  2350→                    // Include existing analysis if available
  2351→                    if ($useMongoDB) {
  2352→                        $ctxAnalysis = $ctxMongo->findOne('thread_analysis', ['thread_id' => $chatThreadId, 'is_analyzed' => 1]);
  2353→                        if (!$ctxAnalysis) {
  2354→                            // Try without is_analyzed filter (MongoDB may store as bool or int)
  2355→                            $ctxAnalysis = $ctxMongo->findOne('thread_analysis', ['thread_id' => $chatThreadId]);
  2356→                        }
  2357→                    } else {
  2358→                        $ctxAnalysis = $db->fetchOne("SELECT summary, raw_analysis FROM thread_analysis WHERE thread_id = ? AND is_analyzed=1", [$chatThreadId]);
  2359→                    }
  2360→                    if ($ctxAnalysis && !empty($ctxAnalysis['summary'])) {
  2361→                        $ctxParts[] = "=== EXISTING AI ANALYSIS ===\nSummary: " . $ctxAnalysis['summary'];
  2362→                    }
  2363→                }
  2364→            }
  2365→
  2366→            // Contact context - use MongoDB-aware queries
  2367→            if ($chatContactId) {
  2368→                if ($useMongoDB) {
  2369→                    $ctxContact = $ctxMongo->findOne('contacts', ['id' => $chatContactId]);
  2370→                    $ctxProfile = $ctxMongo->findOne('contact_profiles', ['contact_id' => $chatContactId]);
  2371→                } else {
  2372→                    $ctxContact = $db->fetchOne("SELECT name, email, type, company FROM contacts WHERE id = ?", [$chatContactId]);
  2373→                    $ctxProfile = $db->fetchOne("SELECT * FROM contact_profiles WHERE contact_id = ?", [$chatContactId]);
  2374→                }
  2375→                if ($ctxContact) {
  2376→                    $ctxParts[] = "=== CONTACT: {$ctxContact['name']} ({$ctxContact['email']}) ===";
  2377→                    if ($ctxProfile) {
  2378→                        $ctxParts[] = "Trust score: " . ($ctxProfile['overall_trust_score'] ?? 'N/A') .
  2379→                            " | Communication style: " . ($ctxProfile['communication_style'] ?? 'N/A');
  2380→                    }
  2381→                }
  2382→            }
  2383→
  2384→            // Single email context (for reply mode) - use MongoDB-aware queries
  2385→            if ($chatEmailId) {
  2386→                if ($useMongoDB) {
  2387→                    $ctxEmail = $ctxMongo->findOne('emails', ['id' => $chatEmailId]);
  2388→                } else {
  2389→                    $ctxEmail = $db->fetchOne("SELECT * FROM emails WHERE id = ?", [$chatEmailId]);
  2390→                }
  2391→                if ($ctxEmail) {
  2392→                    $ctxParts[] = "=== SPECIFIC EMAIL TO REPLY TO ===\nFrom: {$ctxEmail['from_email']}\nSubject: {$ctxEmail['subject']}\n{$ctxEmail['body_text']}";
  2393→                }
  2394→            }
  2395→
  2396→            // Mode-specific system instructions
  2397→            $systemInstructions = [
  2398→                'thread'  => "You are a business intelligence assistant analyzing an email thread. Provide insights about the conversation, relationships, risks, and opportunities. Be direct and specific.",
  2399→                'contact' => "You are a CRM intelligence assistant analyzing a contact's profile and history. Help understand their behavior, communication patterns, and how best to work with them.",
  2400→                'quote'   => "You are a business consultant helping estimate project costs and create quotes. Based on the email context, suggest realistic pricing, scope, and approach for software/CRM projects.",
  2401→                'reply'   => "You are an email writing assistant. Compose professional, ready-to-send email replies based on the thread context. Write only the reply text with greeting and sign-off.",
  2402→                'general' => "You are a business intelligence assistant with access to email and contact data. Answer questions, provide analysis, and help with business decisions.",
  2403→            ];
  2404→            $sysMsg = $systemInstructions[$chatMode] ?? $systemInstructions['general'];
  2405→            $contextBlock = implode("\n\n", $ctxParts);
  2406→            if ($contextBlock) {
  2407→                $sysMsg .= "\n\nCONTEXT:\n" . $contextBlock;
  2408→            }
  2409→
  2410→            // Build messages array with history
  2411→            $messages = [];
  2412→            foreach ($chatHistory as $h) {
  2413→                if (isset($h['role'], $h['content'])) {
  2414→                    $messages[] = ['role' => $h['role'], 'content' => $h['content']];
  2415→                }
  2416→            }
  2417→            $messages[] = ['role' => 'user', 'content' => $chatMessage];
  2418→
  2419→            // Determine provider from model name
  2420→            $chatProvider = 'openai';
  2421→            if (strpos($chatModel, 'claude') !== false) $chatProvider = 'claude';
  2422→            elseif (strpos($chatModel, 'gemini') !== false) $chatProvider = 'gemini';
  2423→            elseif (strpos($chatModel, 'grok') !== false) $chatProvider = 'grok';
  2424→
  2425→            // Route through proxy for multi-model support
  2426→            $proxyUrl = 'https://mssql.danmarcrm.com/dev1/extension2/aiapi/api_codex51/api/index.php';
  2427→            $proxyPayload = json_encode([
  2428→                'action'       => 'ask',
  2429→                'user_content' => $chatMessage,
  2430→                'system'       => $sysMsg,
  2431→                'models'       => [$chatModel],
  2432→                'providers'    => [$chatProvider],
  2433→                'history'      => array_slice($chatHistory, -10), // last 10 turns
  2434→            ]);
  2435→
  2436→            $ch = curl_init($proxyUrl);
  2437→            curl_setopt_array($ch, [
  2438→                CURLOPT_RETURNTRANSFER => true,
  2439→                CURLOPT_POST => true,
  2440→                CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
  2441→                CURLOPT_POSTFIELDS => $proxyPayload,
  2442→                CURLOPT_TIMEOUT => 120,
  2443→            ]);
  2444→            $proxyResponse = curl_exec($ch);
  2445→            $curlError = curl_error($ch);
  2446→            curl_close($ch);
  2447→
  2448→            if ($curlError) {
  2449→                // Fallback to direct ThreadAnalyzer if proxy fails
  2450→                try {
  2451→                    $analyzer = new ThreadAnalyzer($chatProvider === 'openai' ? 'openai' : 'claude', $chatModel);
  2452→                    $fullPrompt = $sysMsg . "\n\nUser: " . $chatMessage;
  2453→                    $answer = $analyzer->askClaude($fullPrompt);
  2454→                    jsonResponse(['success' => true, 'answer' => $answer, 'model' => $chatModel, 'via' => 'direct']);
  2455→                } catch (Exception $e) {
  2456→                    jsonError('AI request failed: ' . $e->getMessage());
  2457→                }
  2458→                break;
  2459→            }
  2460→
  2461→            $proxyResult = json_decode($proxyResponse, true);
  2462→            if (!$proxyResult || !$proxyResult['success']) {
  2463→                jsonError('AI proxy error: ' . ($proxyResult['error'] ?? 'Unknown error'));
  2464→            }
  2465→
  2466→            // Extract answer from proxy response
  2467→            // Proxy returns: {"success":true,"responses":{"openai_response":{"response":"...","model":"..."}}}
  2468→            $answer = '';
  2469→            $usedModel = $chatModel;
  2470→            $responses = $proxyResult['responses'] ?? [];
  2471→            if (!empty($responses)) {
  2472→                $firstResponse = reset($responses);
  2473→                $answer = $firstResponse['response'] ?? $firstResponse['content'] ?? '';
  2474→                $usedModel = $firstResponse['model'] ?? $chatModel;
  2475→            }
  2476→            if (!$answer) {
  2477→                // Fallback: try top-level content
  2478→                $answer = $proxyResult['content'] ?? $proxyResult['response'] ?? '';
  2479→            }
  2480→            if (!$answer) {
  2481→                jsonError('AI proxy returned empty response. Raw: ' . substr($proxyResponse, 0, 200));
  2482→            }
  2483→
  2484→            jsonResponse([
  2485→                'success' => true,
  2486→                'answer'  => $answer,
  2487→                'model'   => $usedModel,
  2488→                'provider'=> $chatProvider,
  2489→                'via'     => 'proxy'
  2490→            ]);
  2491→            break;
  2492→
  2493→
  2494→
  2495→        // ==========================================
  2496→        case 'generate_reply':
  2497→            if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
  2498→                jsonError('POST required', 405);
  2499→            }

                    'answer' => $response
                ]);
            } catch (Exception $e) {
                jsonError('AI request failed: ' . $e->getMessage());
            }
            break;

        // ==========================================
        // AI Chat - Multi-model chat with full context
        // ==========================================
        case 'ai_chat':
            if ($_SERVER['REQUEST_METHOD'] !== 'POST') jsonError('POST required', 405);

            $jsonData = json_decode(file_get_contents('php://input'), true) ?? [];
            $chatMessage  = trim($jsonData['message'] ?? '');
            $chatModel    = $jsonData['model'] ?? 'gpt-4o';
            $chatMode     = $jsonData['mode'] ?? 'general';   // thread|contact|quote|reply|general
            $chatThreadId = $jsonData['thread_id'] ?? null;
            $chatContactId= (int)($jsonData['contact_id'] ?? 0);
            $chatHistory  = $jsonData['history'] ?? [];       // [{role,content}]
            $chatEmailId  = (int)($jsonData['email_id'] ?? 0);

            if (!$chatMessage) jsonError('message required');
            if ($chatThreadId) $chatThreadId = resolveThreadId($chatThreadId);

            // --- Build context block ---
            $ctxParts = [];
            $ctxAdapter = QueryAdapter::getInstance();
            $ctxMongo = \EmailIntelligence\MongoDatabase::getInstance();
            $useMongoDB = $ctxAdapter->getActiveDatabase() === 'mongodb';

            // Thread context - use MongoDB-aware queries
            if ($chatThreadId) {
                if ($useMongoDB) {
                    $ctxEmails = $ctxMongo->findAll('emails', ['thread_id' => $chatThreadId], [
                        'sort' => ['date' => 1]
                    ]);
                } else {
                    $ctxEmails = $db->fetchAll(
                        "SELECT direction, date, from_email, to_email, subject, body_text FROM emails WHERE thread_id = ? ORDER BY date ASC",
                        [$chatThreadId]
                    );
                }
                if ($ctxEmails) {
                    $ctxParts[] = "=== EMAIL THREAD (" . count($ctxEmails) . " emails) ===";
                    foreach ($ctxEmails as $i => $em) {
                        $dir = ($em['direction'] ?? '') === 'inbound' ? 'FROM THEM' : 'FROM US';
                        $body = substr($em['body_text'] ?? '', 0, 800);
                        $ctxParts[] = "Email #".($i+1)." {$dir} ({$em['date']})\nSubject: {$em['subject']}\n{$body}";
                    }
                    // Include existing analysis if available
                    if ($useMongoDB) {
                        $ctxAnalysis = $ctxMongo->findOne('thread_analysis', ['thread_id' => $chatThreadId, 'is_analyzed' => 1]);
                        if (!$ctxAnalysis) {
                            // Try without is_analyzed filter (MongoDB may store as bool or int)
                            $ctxAnalysis = $ctxMongo->findOne('thread_analysis', ['thread_id' => $chatThreadId]);
                        }
                    } else {
                        $ctxAnalysis = $db->fetchOne("SELECT summary, raw_analysis FROM thread_analysis WHERE thread_id = ? AND is_analyzed=1", [$chatThreadId]);
                    }
                    if ($ctxAnalysis && !empty($ctxAnalysis['summary'])) {
                        $ctxParts[] = "=== EXISTING AI ANALYSIS ===\nSummary: " . $ctxAnalysis['summary'];
                    }
                }
            }

            // Contact context - use MongoDB-aware queries
            if ($chatContactId) {
                if ($useMongoDB) {
                    $ctxContact = $ctxMongo->findOne('contacts', ['id' => $chatContactId]);
                    $ctxProfile = $ctxMongo->findOne('contact_profiles', ['contact_id' => $chatContactId]);
                } else {
                    $ctxContact = $db->fetchOne("SELECT name, email, type, company FROM contacts WHERE id = ?", [$chatContactId]);
                    $ctxProfile = $db->fetchOne("SELECT * FROM contact_profiles WHERE contact_id = ?", [$chatContactId]);
                }
                if ($ctxContact) {
                    $ctxParts[] = "=== CONTACT: {$ctxContact['name']} ({$ctxContact['email']}) ===";
                    if ($ctxProfile) {
                        $ctxParts[] = "Trust score: " . ($ctxProfile['overall_trust_score'] ?? 'N/A') .
                            " | Communication style: " . ($ctxProfile['communication_style'] ?? 'N/A');
                    }
                }
            }

            // Single email context (for reply mode) - use MongoDB-aware queries
            if ($chatEmailId) {
                if ($useMongoDB) {
                    $ctxEmail = $ctxMongo->findOne('emails', ['id' => $chatEmailId]);
                } else {
                    $ctxEmail = $db->fetchOne("SELECT * FROM emails WHERE id = ?", [$chatEmailId]);
                }
                if ($ctxEmail) {
                    $ctxParts[] = "=== SPECIFIC EMAIL TO REPLY TO ===\nFrom: {$ctxEmail['from_email']}\nSubject: {$ctxEmail['subject']}\n{$ctxEmail['body_text']}";
                }
            }

            // Mode-specific system instructions
            $systemInstructions = [
                'thread'  => "You are a business intelligence assistant analyzing an email thread. Provide insights about the conversation, relationships, risks, and opportunities. Be direct and specific.",
                'contact' => "You are a CRM intelligence assistant analyzing a contact's profile and history. Help understand their behavior, communication patterns, and how best to work with them.",
                'quote'   => "You are a business consultant helping estimate project costs and create quotes. Based on the email context, suggest realistic pricing, scope, and approach for software/CRM projects.",
                'reply'   => "You are an email writing assistant. Compose professional, ready-to-send email replies based on the thread context. Write only the reply text with greeting and sign-off.",
                'general' => "You are a business intelligence assistant with access to email and contact data. Answer questions, provide analysis, and help with business decisions.",
            ];
            $sysMsg = $systemInstructions[$chatMode] ?? $systemInstructions['general'];
            $contextBlock = implode("\n\n", $ctxParts);
            if ($contextBlock) {
                $sysMsg .= "\n\nCONTEXT:\n" . $contextBlock;
            }

            // Build messages array with history
            $messages = [];
            foreach ($chatHistory as $h) {
                if (isset($h['role'], $h['content'])) {
                    $messages[] = ['role' => $h['role'], 'content' => $h['content']];
                }
            }
            $messages[] = ['role' => 'user', 'content' => $chatMessage];

            // Determine provider from model name
            $chatProvider = 'openai';
            if (strpos($chatModel, 'claude') !== false) $chatProvider = 'claude';
            elseif (strpos($chatModel, 'gemini') !== false) $chatProvider = 'gemini';
            elseif (strpos($chatModel, 'grok') !== false) $chatProvider = 'grok';

            // Route through proxy for multi-model support
            $proxyUrl = 'https://mssql.danmarcrm.com/dev1/extension2/aiapi/api_codex51/api/index.php';
            $proxyPayload = json_encode([
                'action'       => 'ask',
                'user_content' => $chatMessage,
                'system'       => $sysMsg,
                'models'       => [$chatModel],
                'providers'    => [$chatProvider],
                'history'      => array_slice($chatHistory, -10), // last 10 turns
            ]);

            $ch = curl_init($proxyUrl);
            curl_setopt_array($ch, [
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_POST => true,
                CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
                CURLOPT_POSTFIELDS => $proxyPayload,
                CURLOPT_TIMEOUT => 120,
            ]);
            $proxyResponse = curl_exec($ch);
            $curlError = curl_error($ch);
            curl_close($ch);

            if ($curlError) {
                // Fallback to direct ThreadAnalyzer if proxy fails
                try {
                    $analyzer = new ThreadAnalyzer($chatProvider === 'openai' ? 'openai' : 'claude', $chatModel);
                    $fullPrompt = $sysMsg . "\n\nUser: " . $chatMessage;
                    $answer = $analyzer->askClaude($fullPrompt);
                    jsonResponse(['success' => true, 'answer' => $answer, 'model' => $chatModel, 'via' => 'direct']);
                } catch (Exception $e) {
                    jsonError('AI request failed: ' . $e->getMessage());
                }
                break;
            }

            $proxyResult = json_decode($proxyResponse, true);
            if (!$proxyResult || !$proxyResult['success']) {
                jsonError('AI proxy error: ' . ($proxyResult['error'] ?? 'Unknown error'));
            }

            // Extract answer from proxy response
            // Proxy returns: {"success":true,"responses":{"openai_response":{"response":"...","model":"..."}}}
            $answer = '';
            $usedModel = $chatModel;
            $responses = $proxyResult['responses'] ?? [];
            if (!empty($responses)) {
                $firstResponse = reset($responses);
                $answer = $firstResponse['response'] ?? $firstResponse['content'] ?? '';
                $usedModel = $firstResponse['model'] ?? $chatModel;
            }
            if (!$answer) {
                // Fallback: try top-level content
                $answer = $proxyResult['content'] ?? $proxyResult['response'] ?? '';
            }
            if (!$answer) {
                jsonError('AI proxy returned empty response. Raw: ' . substr($proxyResponse, 0, 200));
            }

            jsonResponse([
                'success' => true,
                'answer'  => $answer,
                'model'   => $usedModel,
                'provider'=> $chatProvider,
                'via'     => 'proxy'
            ]);
            break;

        // ==========================================
        case 'generate_reply':
            if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
                jsonError('POST required', 405);
            }


Now I have a thorough understanding of the codebase. Let me now design the plan using a Plan agent, then write the final plan.

Key findings:
1. **Current web AI chat** already supports modes: general, thread, contact, quote, reply - but only for ONE thread or ONE contact at a time
2. **Chrome extension** has a more advanced "Ask AI" with multi-provider model selection and context input
3. **Backend** has QueryAdapter with `getThreads()` that can filter by contactId and search
4. The user wants: ability to select MULTIPLE threads, or ALL threads for a contact, and send as context to AI

5. The core feature needed is a context builder that lets users search for and select multiple threads, then pass them all to the AI chat along with their question and chosen model.

Let me start designing the implementation plan.


Now I have a thorough understanding of both the web app and chrome extension. Let me design the plan.


Task

Design AI context builder plan

Prompt:
Design an implementation plan for adding a "context builder + AI chat" feature to the Email Intelligence web app. Here's all the context:

## What the user wants:
The user wants to replicate the Chrome extension's "Ask AI" functionality in the web app, with ENHANCED capabilities:
1. Search for contacts by email/name
2. Select a single thread, multiple threads, or ALL threads for a contact as AI context
3. Choose an AI model from a dropdown (multi-provider: Claude, GPT, Gemini, Grok, DeepSeek, etc.)
4. Ask questions about the selected context - e.g. "how much should I quote?", "what's the deception level?"

Example use cases:
- "Grab all emails for stephanie.k***@asendia.com and check how much we quoted, her profile, and ask how much to quote for task X"
- "Grab multiple threads of david.zolanz@digitalj2.com and ask the level of deception from david"

## Current web app AI chat (what already exists):
- File: `/var/www/mssql.danmarcrm.com/dev1/extension2/public/assets/app.js` (lines 2672-2882)
- Has modes: general, thread (single), contact, quote, reply
- Sends to `api.php?action=ai_chat` which builds context from ONE thread_id or ONE contact_id
- Has model dropdown but only basic models
- Draggable panel with chat history, quick prompts

## Current backend:
- `api.php?action=ai_chat` (lines 2310-2491) - builds context from single thread or contact
- `api.php?action=threads` - lists threads, filterable by contact_id and search
- `QueryAdapter::getThreads($contactId, $search, $limit, $offset)` - fetches threads
- MongoDB + SQL dual support throughout
- Thread emails stored with thread_id grouping
- Contact profiles with trust scores, communication style

## Chrome extension (what to draw inspiration from):
- Has provider dropdown + model dropdown with ALL providers (Claude, OpenAI, Gemini, Grok, DeepSeek, Qwen, Mistral, Cohere)
- Model list defined in JS (lines 18-56 of contentScript.js)
- `fetchEmailContext()` can fetch by thread ID or by email (returns contact + recent threads)
- Conversation history (last 10 exchanges)
- Quick question buttons

## The enhancement needed:
The current web chat can only load ONE thread or ONE contact as context. We need:

1. A **contact/thread search** UI in the AI chat panel (or a new dedicated panel)
2. Ability to **add multiple threads to context** - like a "shopping cart" for context
3. Ability to **"Add all threads for contact X"** with one click
4. The **multi-provider model dropdown** from the chrome extension
5. A **visible context summary** showing what's loaded (e.g. "3 threads, 15 emails from stephanie@...")
6. Backend API changes to accept multiple thread_ids and build combined context

## Key files to modify:
- `/var/www/mssql.danmarcrm.com/dev1/extension2/public/assets/app.js` - Frontend JS
- `/var/www/mssql.danmarcrm.com/dev1/extension2/public/api.php` - Backend API
- `/var/www/mssql.danmarcrm.com/dev1/extension2/public/index.php` - HTML for new UI elements

## Constraints:
- Keep using the existing draggable AI chat panel (enhance it, don't replace it)
- Must work with both MongoDB and SQL backends
- Keep the existing modes (thread, contact, quote, reply, general) but enhance them
- Reuse existing QueryAdapter and MongoDatabase classes
- Use the same AI proxy at `aiapi/api_codex51/api/index.php`

Design a detailed, step-by-step implementation plan.


Design AI context builder plan


Now I have a comprehensive understanding of the entire system. Let me synthesize the implementation plan.

---

# Implementation Plan: Context Builder + AI Chat Feature

## Architecture Overview

After thoroughly exploring the codebase, here is the current state and the precise changes needed across three files (frontend JS, backend API, and HTML template), plus one optional enhancement to QueryAdapter.

---

## Part 1: Understanding the Current System

**Current AI Chat Flow:**
1. Frontend (`app.js` lines 2672-2882): The `aiChat` object stores a single `threadId`, `contactId`, and `emailId`. The `sendAiChat()` function sends one `thread_id` and/or one `contact_id` to the backend.
2. Backend (`api.php` lines 2310-2491): The `ai_chat` action builds a `$ctxParts` array from a single thread's emails and a single contact's profile. It assembles a system prompt, then routes through the AI proxy at `aiapi/api_codex51/api/index.php`.
3. AI Proxy (`AIOrchestrator`): Supports 5 providers (OpenAI, Claude, Gemini, Grok, DeepSeek) with provider-based model selection.

**Key Discovery -- Existing Bug:**
The current `api.php` sends `'models' => [$chatModel]` (numerically indexed array like `['gpt-4o']`), but the AIOrchestrator expects `$modelOverrides` keyed by provider code (like `['openai' => 'gpt-4o']`). This means model overrides are silently ignored and the proxy always uses default models. This plan fixes this.

**Chrome Extension Reference:**
The Chrome extension (`contentScript.js` lines 18-56) defines `PROVIDER_MODELS` with 8 providers (Claude, OpenAI, Gemini, Grok, DeepSeek, Qwen, Mistral, Cohere). It has a `savedAIContext` feature using Chrome storage that allows building up context incrementally.

---

## Part 2: Detailed Implementation Steps

### Step 1: Extend the Frontend `aiChat` State Object

**File:** `/var/www/mssql.danmarcrm.com/dev1/extension2/public/assets/app.js`
**Location:** Lines 2672-2684

Replace the `aiChat` object to support multi-thread context:

```javascript
const aiChat = {
    isOpen: false,
    mode: 'general',
    threadId: null,          // Keep for backward compat (single thread mode)
    contactId: null,
    emailId: null,
    threadSubject: null,
    contactName: null,
    history: [],
    isDragging: false,
    dragOffsetX: 0,
    dragOffsetY: 0,
    // NEW: Context builder state
    contextThreads: [],      // Array of {thread_id, subject, email_count, contact_name}
    contextContact: null,    // {id, name, email} - the contact whose threads are loaded
    contextSearchVisible: false,
    contextProvider: 'openai',  // Selected provider code
};
```

The `contextThreads` array is the "shopping cart" -- each entry represents a thread added to the AI context. This is the core data structure for multi-thread support.

### Step 2: Add the PROVIDER_MODELS Constant to Frontend

**File:** `/var/www/mssql.danmarcrm.com/dev1/extension2/public/assets/app.js`
**Location:** Insert right after the `aiChat` object (after line 2684), before `AI_QUICK_PROMPTS`

```javascript
const AI_PROVIDER_MODELS = {
    openai: [
        { id: 'gpt-4o', name: 'GPT-4o (Latest)' },
        { id: 'gpt-4o-mini', name: 'GPT-4o Mini (Fast)' },
        { id: 'o1', name: 'o1 (Reasoning)' },
        { id: 'o1-mini', name: 'o1-mini' },
    ],
    claude: [
        { id: 'claude-3-5-sonnet-20241022', name: 'Claude 3.5 Sonnet' },
        { id: 'claude-3-5-haiku-20241022', name: 'Claude 3.5 Haiku (Fast)' },
        { id: 'claude-3-opus-20240229', name: 'Claude 3 Opus' },
    ],
    gemini: [
        { id: 'gemini-2.0-flash-exp', name: 'Gemini 2.0 Flash' },
        { id: 'gemini-1.5-pro', name: 'Gemini 1.5 Pro' },
        { id: 'gemini-1.5-flash', name: 'Gemini 1.5 Flash' },
    ],
    grok: [
        { id: 'grok-2-latest', name: 'Grok 2 (Latest)' },
    ],
    deepseek: [
        { id: 'deepseek-chat', name: 'DeepSeek V3 Chat' },
        { id: 'deepseek-reasoner', name: 'DeepSeek R1 (Reasoning)' },
    ],
};

const AI_PROVIDER_NAMES = {
    openai: 'OpenAI',
    claude: 'Anthropic Claude',
    gemini: 'Google Gemini',
    grok: 'xAI',
    deepseek: 'DeepSeek',
};
```

These match the providers actually supported by `AIOrchestrator` (5 providers with registered classes in `Providers.php`). Qwen, Mistral, and Cohere exist as provider classes in other API modules (`api_composer`, `api_grok`) but NOT in `api_codex51` which is the proxy used by the web app. Adding them would require also registering those providers in `AIOrchestrator.registerProviders()`. That should be a separate follow-up.

### Step 3: Replace the Static Model Dropdown with Dynamic Provider+Model Selector

**File:** `/var/www/mssql.danmarcrm.com/dev1/extension2/public/index.php`
**Location:** Lines 538-561 (the Model + Mode selectors section)

Replace the static `<select>` with a two-part selector: a provider dropdown and a model dropdown that updates dynamically.

```html
<div style="display:flex; gap:8px; align-items:center; margin-bottom:8px;">
    <label style="color:#94a3b8; font-size:0.72em; white-space:nowrap;">PROVIDER</label>
    <select id="ai-chat-provider" onchange="updateAiModelDropdown()" style="flex:1; background:#1e1e2e; color:#e2e8f0; border:1px solid #3d3d5c; border-radius:6px; padding:4px 8px; font-size:0.78em; cursor:pointer;">
        <!-- Populated by JS -->
    </select>
</div>
<div style="display:flex; gap:8px; align-items:center; margin-bottom:8px;">
    <label style="color:#94a3b8; font-size:0.72em; white-space:nowrap;">MODEL</label>
    <select id="ai-chat-model" style="flex:1; background:#1e1e2e; color:#e2e8f0; border:1px solid #3d3d5c; border-radius:6px; padding:4px 8px; font-size:0.78em; cursor:pointer;">
        <!-- Populated dynamically by updateAiModelDropdown() -->
    </select>
</div>
```

**New JS functions in `app.js`:**

```javascript
function initAiProviderDropdown() {
    const providerSelect = document.getElementById('ai-chat-provider');
    if (!providerSelect) return;
    providerSelect.innerHTML = '';
    Object.keys(AI_PROVIDER_MODELS).forEach(code => {
        const opt = document.createElement('option');
        opt.value = code;
        opt.textContent = AI_PROVIDER_NAMES[code] || code;
        providerSelect.appendChild(opt);
    });
    providerSelect.value = aiChat.contextProvider || 'openai';
    updateAiModelDropdown();
}

function updateAiModelDropdown() {
    const providerSelect = document.getElementById('ai-chat-provider');
    const modelSelect = document.getElementById('ai-chat-model');
    if (!providerSelect || !modelSelect) return;

    const provider = providerSelect.value;
    aiChat.contextProvider = provider;
    const models = AI_PROVIDER_MODELS[provider] || [];

    modelSelect.innerHTML = '';
    models.forEach(m => {
        const opt = document.createElement('option');
        opt.value = m.id;
        opt.textContent = m.name;
        modelSelect.appendChild(opt);
    });
}
```

Call `initAiProviderDropdown()` from the `DOMContentLoaded` listener (line 2862).

### Step 4: Add the Context Builder UI to the Chat Panel

**File:** `/var/www/mssql.danmarcrm.com/dev1/extension2/public/index.php`
**Location:** Insert between the Mode tabs (line 570) and the Messages div (line 574)

Add a collapsible "Context Builder" section:

```html
<!-- Context Builder Section -->
<div id="ai-context-builder" style="background:#1a1a2e; border-bottom:1px solid #2d2d4e; flex-shrink:0; max-height:220px; overflow:hidden; transition:max-height 0.3s;">
    <!-- Context Summary Bar (always visible) -->
    <div id="ai-context-summary" onclick="toggleContextBuilder()" style="padding:8px 12px; cursor:pointer; display:flex; justify-content:space-between; align-items:center;">
        <span id="ai-context-summary-text" style="color:#94a3b8; font-size:0.72em;">No context loaded. Click to search contacts/threads.</span>
        <span id="ai-context-toggle-icon" style="color:#6366f1; font-size:0.9em;">+</span>
    </div>

    <!-- Expandable Search Area -->
    <div id="ai-context-search-area" style="display:none; padding:0 12px 10px;">
        <!-- Contact/Email Search -->
        <div style="display:flex; gap:6px; margin-bottom:8px;">
            <input id="ai-context-search-input" type="text" placeholder="Search by email or name..."
                   style="flex:1; background:#252540; color:#e2e8f0; border:1px solid #3d3d5c; border-radius:6px; padding:5px 8px; font-size:0.75em; outline:none;"
                   onkeydown="if(event.key==='Enter')searchContextContacts()">
            <button onclick="searchContextContacts()" style="background:#6366f1; color:#fff; border:none; border-radius:6px; padding:5px 10px; font-size:0.72em; cursor:pointer;">Search</button>
        </div>

        <!-- Search Results -->
        <div id="ai-context-search-results" style="max-height:80px; overflow-y:auto; margin-bottom:6px;"></div>

        <!-- Thread Picker (shows after contact selected) -->
        <div id="ai-context-thread-picker" style="display:none; max-height:80px; overflow-y:auto; margin-bottom:6px;"></div>

        <!-- Selected Context Items (the "cart") -->
        <div id="ai-context-cart" style="display:none;"></div>
    </div>
</div>
```

### Step 5: Implement the Context Builder JavaScript Functions

**File:** `/var/www/mssql.danmarcrm.com/dev1/extension2/public/assets/app.js`
**Location:** Insert after the `clearAiChat()` function (after line 2859)

**5a. Toggle visibility:**
```javascript
function toggleContextBuilder() {
    const area = document.getElementById('ai-context-search-area');
    const icon = document.getElementById('ai-context-toggle-icon');
    const isVisible = area.style.display !== 'none';
    area.style.display = isVisible ? 'none' : 'block';
    icon.textContent = isVisible ? '+' : '-';
}
```

**5b. Search for contacts:**
```javascript
async function searchContextContacts() {
    const query = document.getElementById('ai-context-search-input').value.trim();
    if (!query) return;

    const resultsDiv = document.getElementById('ai-context-search-results');
    resultsDiv.innerHTML = '<div style="color:#6366f1;font-size:0.72em;padding:4px;">Searching...</div>';

    try {
        const data = await apiCall('contacts', { search: query, limit: 5 });
        if (!data.contacts || data.contacts.length === 0) {
            resultsDiv.innerHTML = '<div style="color:#94a3b8;font-size:0.72em;padding:4px;">No contacts found</div>';
            return;
        }

        resultsDiv.innerHTML = data.contacts.map(c => `
            <div onclick="selectContextContact(${c.id}, '${escapeHtml(c.name || c.email)}', '${escapeHtml(c.email)}')"
                 style="padding:5px 8px; cursor:pointer; border-radius:4px; font-size:0.72em; color:#e2e8f0; display:flex; justify-content:space-between; align-items:center;"
                 onmouseover="this.style.background='#2d2d4e'" onmouseout="this.style.background='transparent'">
                <span><strong>${escapeHtml(c.name || c.email)}</strong> <span style="color:#6b7280;">${escapeHtml(c.email)}</span></span>
                <span style="color:#6b7280;font-size:0.85em;">${c.total_emails || 0} emails</span>
            </div>
        `).join('');
    } catch (err) {
        resultsDiv.innerHTML = '<div style="color:#fca5a5;font-size:0.72em;padding:4px;">Error: ' + escapeHtml(err.message) + '</div>';
    }
}
```

**5c. Select a contact and load their threads:**
```javascript
async function selectContextContact(contactId, name, email) {
    aiChat.contextContact = { id: contactId, name, email };
    aiChat.contactId = contactId;
    aiChat.contactName = name;

    // Clear search results
    document.getElementById('ai-context-search-results').innerHTML = '';

    const picker = document.getElementById('ai-context-thread-picker');
    picker.style.display = 'block';
    picker.innerHTML = '<div style="color:#6366f1;font-size:0.72em;padding:4px;">Loading threads...</div>';

    try {
        const data = await apiCall('threads', { contact: contactId, limit: 50 });

        if (!data.threads || data.threads.length === 0) {
            picker.innerHTML = '<div style="color:#94a3b8;font-size:0.72em;padding:4px;">No threads for this contact</div>';
            return;
        }

        let html = `
            <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:4px;">
                <span style="color:#a5b4fc; font-size:0.72em; font-weight:600;">${escapeHtml(name)} - ${data.threads.length} threads</span>
                <button onclick="addAllContactThreads()" style="background:#10b981; color:#fff; border:none; border-radius:4px; padding:2px 8px; font-size:0.68em; cursor:pointer;">+ Add All</button>
            </div>
        `;

        // Store threads for "add all"
        aiChat._availableThreads = data.threads;

        html += data.threads.map(t => {
            const isAdded = aiChat.contextThreads.some(ct => ct.thread_id === t.thread_id);
            return `
                <div style="display:flex; justify-content:space-between; align-items:center; padding:3px 6px; border-radius:4px; font-size:0.68em;"
                     onmouseover="this.style.background='#2d2d4e'" onmouseout="this.style.background='transparent'">
                    <span style="color:#e2e8f0; flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="${escapeHtml(t.subject || '(No Subject)')}">
                        ${escapeHtml((t.subject || '(No Subject)').substring(0, 40))}
                        <span style="color:#6b7280;">(${t.email_count} emails)</span>
                    </span>
                    <button onclick="addThreadToContext('${escapeHtml(t.thread_id)}', '${escapeHtml((t.subject || '').replace(/'/g, "\\\\'"))}', ${t.email_count}, '${escapeHtml(name)}')"
                            style="background:${isAdded ? '#4b5563' : '#6366f1'}; color:#fff; border:none; border-radius:3px; padding:1px 6px; font-size:0.95em; cursor:pointer; margin-left:4px;"
                            ${isAdded ? 'disabled' : ''}>
                        ${isAdded ? '...' : '+'}
                    </button>
                </div>
            `;
        }).join('');

        picker.innerHTML = html;
    } catch (err) {
        picker.innerHTML = '<div style="color:#fca5a5;font-size:0.72em;padding:4px;">Error: ' + escapeHtml(err.message) + '</div>';
    }
}
```

**5d. Add/remove threads from context cart:**
```javascript
function addThreadToContext(threadId, subject, emailCount, contactName) {
    if (aiChat.contextThreads.some(t => t.thread_id === threadId)) return;

    aiChat.contextThreads.push({ thread_id: threadId, subject, email_count: emailCount, contact_name: contactName });
    updateContextCart();
    updateAiChatContextLabel();

    // Re-render thread picker to update button states
    if (aiChat.contextContact) {
        selectContextContact(aiChat.contextContact.id, aiChat.contextContact.name, aiChat.contextContact.email);
    }
}

function removeThreadFromContext(threadId) {
    aiChat.contextThreads = aiChat.contextThreads.filter(t => t.thread_id !== threadId);
    updateContextCart();
    updateAiChatContextLabel();
}

function addAllContactThreads() {
    const threads = aiChat._availableThreads || [];
    threads.forEach(t => {
        if (!aiChat.contextThreads.some(ct => ct.thread_id === t.thread_id)) {
            aiChat.contextThreads.push({
                thread_id: t.thread_id,
                subject: t.subject || '(No Subject)',
                email_count: t.email_count,
                contact_name: aiChat.contextContact?.name || 'Unknown'
            });
        }
    });
    updateContextCart();
    updateAiChatContextLabel();
    // Re-render picker
    if (aiChat.contextContact) {
        selectContextContact(aiChat.contextContact.id, aiChat.contextContact.name, aiChat.contextContact.email);
    }
}

function clearContextCart() {
    aiChat.contextThreads = [];
    aiChat.contextContact = null;
    updateContextCart();
    updateAiChatContextLabel();
}
```

**5e. Render the context cart:**
```javascript
function updateContextCart() {
    const cartDiv = document.getElementById('ai-context-cart');
    if (!cartDiv) return;

    if (aiChat.contextThreads.length === 0) {
        cartDiv.style.display = 'none';
        return;
    }

    cartDiv.style.display = 'block';
    const totalEmails = aiChat.contextThreads.reduce((sum, t) => sum + (t.email_count || 0), 0);

    cartDiv.innerHTML = `
        <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:4px;">
            <span style="color:#10b981; font-size:0.72em; font-weight:600;">
                Context: ${aiChat.contextThreads.length} thread${aiChat.contextThreads.length > 1 ? 's' : ''}, ~${totalEmails} emails
            </span>
            <button onclick="clearContextCart()" style="background:#7f1d1d; color:#fca5a5; border:none; border-radius:3px; padding:1px 6px; font-size:0.65em; cursor:pointer;">Clear All</button>
        </div>
        <div style="max-height:50px; overflow-y:auto;">
            ${aiChat.contextThreads.map(t => `
                <div style="display:flex; justify-content:space-between; align-items:center; padding:1px 4px; font-size:0.65em; color:#94a3b8;">
                    <span style="overflow:hidden; text-overflow:ellipsis; white-space:nowrap; flex:1;" title="${escapeHtml(t.subject)}">${escapeHtml(t.subject.substring(0, 35))}</span>
                    <button onclick="removeThreadFromContext('${escapeHtml(t.thread_id)}')" style="background:none; border:none; color:#f87171; cursor:pointer; font-size:1.1em; padding:0 2px;">x</button>
                </div>
            `).join('')}
        </div>
    `;
}
```

### Step 6: Update the Context Label to Show Multi-Thread Info

**File:** `/var/www/mssql.danmarcrm.com/dev1/extension2/public/assets/app.js`
**Location:** Replace `updateAiChatContextLabel()` function (lines 2728-2736)

```javascript
function updateAiChatContextLabel() {
    const label = document.getElementById('ai-chat-context-label');
    if (!label) return;

    // If context builder has threads loaded, show that info
    if (aiChat.contextThreads.length > 0) {
        const totalEmails = aiChat.contextThreads.reduce((sum, t) => sum + (t.email_count || 0), 0);
        const contactLabel = aiChat.contextContact ? aiChat.contextContact.name : '';
        label.textContent = `${aiChat.contextThreads.length} threads, ~${totalEmails} emails` + (contactLabel ? ` from ${contactLabel}` : '');
        label.style.color = '#10b981';
        return;
    }

    // Fallback to original behavior
    const modeLabels = { general: 'General Chat', thread: 'Thread Analysis', contact: 'Contact Intelligence', quote: 'Quote Assistant', reply: 'Reply Composer' };
    let ctx = modeLabels[aiChat.mode] || 'General Chat';
    if (aiChat.mode === 'thread' && aiChat.threadSubject) ctx += ' . ' + aiChat.threadSubject.substring(0, 30);
    if (aiChat.mode === 'contact' && aiChat.contactName) ctx += ' . ' + aiChat.contactName;
    label.textContent = ctx;
    label.style.color = 'rgba(255,255,255,0.7)';
}
```

### Step 7: Modify `sendAiChat()` to Send Multi-Thread Context

**File:** `/var/www/mssql.danmarcrm.com/dev1/extension2/public/assets/app.js`
**Location:** Modify the `sendAiChat()` function (lines 2756-2793)

The key change is in the payload construction (around line 2770-2773):

```javascript
async function sendAiChat() {
    const input = document.getElementById('ai-chat-input');
    const message = input.value.trim();
    if (!message) return;
    const model = document.getElementById('ai-chat-model').value;
    const providerSelect = document.getElementById('ai-chat-provider');
    const provider = providerSelect ? providerSelect.value : 'openai';
    const sendBtn = document.getElementById('ai-chat-send-btn');

    appendAiChatMessage('user', message);
    input.value = '';
    sendBtn.disabled = true; sendBtn.style.opacity = '0.5';
    aiChat.history.push({ role: 'user', content: message });
    const typingId = appendAiChatTyping();

    try {
        const payload = {
            message,
            model,
            provider,   // NEW: explicit provider
            mode: aiChat.mode,
            history: aiChat.history.slice(-10)
        };

        // Multi-thread context (NEW)
        if (aiChat.contextThreads.length > 0) {
            payload.thread_ids = aiChat.contextThreads.map(t => t.thread_id);
        }

        // Single thread fallback (existing behavior)
        if (aiChat.threadId && aiChat.contextThreads.length === 0) {
            payload.thread_id = aiChat.threadId;
        }

        if (aiChat.contactId) payload.contact_id = aiChat.contactId;
        if (aiChat.emailId) payload.email_id = aiChat.emailId;

        const resp = await fetch('api.php?action=ai_chat', {
            method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload)
        });
        const data = await resp.json();
        removeAiChatTyping(typingId);

        if (data.success && data.answer) {
            aiChat.history.push({ role: 'assistant', content: data.answer });
            appendAiChatMessage('assistant', data.answer, data.model);
        } else {
            appendAiChatMessage('error', 'Warning: ' + (data.message || data.error || 'Unknown error'));
        }
    } catch (err) {
        removeAiChatTyping(typingId);
        appendAiChatMessage('error', 'Network error: ' + err.message);
    } finally {
        sendBtn.disabled = false; sendBtn.style.opacity = '1';
    }
}
```

### Step 8: Backend -- Modify `ai_chat` Action to Support Multiple Thread IDs

**File:** `/var/www/mssql.danmarcrm.com/dev1/extension2/public/api.php`
**Location:** Lines 2310-2491 (the `case 'ai_chat':` block)

Key changes:

**8a. Parse the new `thread_ids` array and `provider` field (around line 2317):**

```php
case 'ai_chat':
    if ($_SERVER['REQUEST_METHOD'] !== 'POST') jsonError('POST required', 405);

    $jsonData = json_decode(file_get_contents('php://input'), true) ?? [];
    $chatMessage  = trim($jsonData['message'] ?? '');
    $chatModel    = $jsonData['model'] ?? 'gpt-4o';
    $chatProvider = $jsonData['provider'] ?? null;  // NEW: explicit provider from frontend
    $chatMode     = $jsonData['mode'] ?? 'general';
    $chatThreadId = $jsonData['thread_id'] ?? null;     // Single thread (backward compat)
    $chatThreadIds = $jsonData['thread_ids'] ?? [];      // NEW: multiple threads
    $chatContactId= (int)($jsonData['contact_id'] ?? 0);
    $chatHistory  = $jsonData['history'] ?? [];
    $chatEmailId  = (int)($jsonData['email_id'] ?? 0);

    if (!$chatMessage) jsonError('message required');

    // Resolve thread IDs
    if ($chatThreadId) $chatThreadId = resolveThreadId($chatThreadId);
    $resolvedThreadIds = [];
    foreach ($chatThreadIds as $tid) {
        $resolved = resolveThreadId($tid);
        if ($resolved) $resolvedThreadIds[] = $resolved;
    }
    // If single thread_id provided and no multi-thread, add it to the array
    if ($chatThreadId && empty($resolvedThreadIds)) {
        $resolvedThreadIds = [$chatThreadId];
    }
```

**8b. Build context from MULTIPLE threads (replace lines 2331-2363):**

```php
    // --- Build context block ---
    $ctxParts = [];
    $ctxAdapter = QueryAdapter::getInstance();
    $ctxMongo = \EmailIntelligence\MongoDatabase::getInstance();
    $useMongoDB = $ctxAdapter->getActiveDatabase() === 'mongodb';

    // Multi-thread context
    $totalEmailCount = 0;
    foreach ($resolvedThreadIds as $idx => $tid) {
        if ($useMongoDB) {
            $ctxEmails = $ctxMongo->findAll('emails', ['thread_id' => $tid], [
                'sort' => ['date' => 1]
            ]);
        } else {
            $ctxEmails = $db->fetchAll(
                "SELECT direction, date, from_email, to_email, subject, body_text FROM emails WHERE thread_id = ? ORDER BY date ASC",
                [$tid]
            );
        }
        if ($ctxEmails) {
            $threadLabel = count($resolvedThreadIds) > 1 ? "THREAD " . ($idx + 1) . " OF " . count($resolvedThreadIds) : "EMAIL THREAD";
            $ctxParts[] = "=== {$threadLabel} (" . count($ctxEmails) . " emails) ===";
            foreach ($ctxEmails as $i => $em) {
                $dir = ($em['direction'] ?? '') === 'inbound' ? 'FROM THEM' : 'FROM US';
                // Allow more body text for multi-thread to give full context
                $maxBody = count($resolvedThreadIds) > 3 ? 500 : 800;
                $body = substr($em['body_text'] ?? '', 0, $maxBody);
                $ctxParts[] = "Email #".($i+1)." {$dir} ({$em['date']})\nSubject: {$em['subject']}\n{$body}";
            }
            $totalEmailCount += count($ctxEmails);

            // Include thread analysis if available
            if ($useMongoDB) {
                $ctxAnalysis = $ctxMongo->findOne('thread_analysis', ['thread_id' => $tid]);
            } else {
                $ctxAnalysis = $db->fetchOne("SELECT summary, raw_analysis FROM thread_analysis WHERE thread_id = ? AND is_analyzed=1", [$tid]);
            }
            if ($ctxAnalysis && !empty($ctxAnalysis['summary'])) {
                $ctxParts[] = "Analysis: " . $ctxAnalysis['summary'];
            }
        }
    }

    // Add total context summary if multi-thread
    if (count($resolvedThreadIds) > 1) {
        array_unshift($ctxParts, "=== MULTI-THREAD CONTEXT: {$totalEmailCount} total emails across " . count($resolvedThreadIds) . " threads ===");
    }
```

The contact context block (lines 2366-2382) and email context block (lines 2384-2394) remain unchanged.

**8c. Fix the provider detection and proxy payload (replace lines 2419-2434):**

```php
    // Determine provider from explicit field or model name
    if (!$chatProvider) {
        $chatProvider = 'openai';
        if (strpos($chatModel, 'claude') !== false) $chatProvider = 'claude';
        elseif (strpos($chatModel, 'gemini') !== false) $chatProvider = 'gemini';
        elseif (strpos($chatModel, 'grok') !== false) $chatProvider = 'grok';
        elseif (strpos($chatModel, 'deepseek') !== false) $chatProvider = 'deepseek';
    }

    // Route through proxy for multi-model support
    $proxyUrl = 'https://mssql.danmarcrm.com/dev1/extension2/aiapi/api_codex51/api/index.php';
    $proxyPayload = json_encode([
        'action'       => 'ask',
        'user_content' => $chatMessage,
        'system'       => $sysMsg,
        'models'       => [$chatProvider => $chatModel],   // FIX: keyed by provider code
        'providers'    => [$chatProvider],
        'history'      => array_slice($chatHistory, -10),
    ]);
```

Note the critical fix: `'models' => [$chatProvider => $chatModel]` instead of the old `'models' => [$chatModel]`. This ensures the AIOrchestrator correctly uses the selected model (line 72 of AIOrchestrator: `$model = $modelOverrides[$code] ?? ...`).

### Step 9: Add Context-Aware Quick Prompts

**File:** `/var/www/mssql.danmarcrm.com/dev1/extension2/public/assets/app.js`
**Location:** Modify the `AI_QUICK_PROMPTS` object (line 2686)

Add a new mode for multi-thread context:

```javascript
const AI_QUICK_PROMPTS = {
    general: ['What are my most at-risk clients?', 'Which threads need urgent attention?', 'Summarize recent activity'],
    thread: ['Summarize this thread', 'What are the key risks?', 'What action should I take next?', 'Is this client trustworthy?', 'What are they really asking for?'],
    contact: ['What is their communication style?', 'Are there any red flags?', 'How should I approach them?', 'What opportunities exist with them?'],
    quote: ['How much should I quote for this project?', 'What is the scope of work?', 'What are the risks?', 'Suggest a project timeline'],
    reply: ['Write a professional reply', 'Write a firm but polite response', 'Write a follow-up email', 'Write a brief acknowledgment'],
    // NEW: prompts for multi-thread context
    multithread: ['Summarize all these conversations', 'What is the overall sentiment?', 'How much have we quoted across these threads?', 'What is the deception level?', 'What patterns emerge across these threads?', 'Build a complete profile of this person'],
};
```

Then modify `renderQuickPrompts()` to detect multi-thread mode:

```javascript
function renderQuickPrompts() {
    const container = document.getElementById('ai-quick-prompts');
    if (!container) return;
    // Use multithread prompts when context builder has threads loaded
    const effectiveMode = aiChat.contextThreads.length > 0 ? 'multithread' : aiChat.mode;
    const prompts = AI_QUICK_PROMPTS[effectiveMode] || AI_QUICK_PROMPTS.general;
    container.innerHTML = prompts.map(p =>
        `<button onclick="sendAiChatPrompt(${JSON.stringify(p)})" style="background:#2d2d4e;color:#94a3b8;border:1px solid #3d3d5c;border-radius:12px;padding:3px 10px;font-size:0.68em;cursor:pointer;white-space:nowrap;" onmouseover="this.style.background='#6366f1';this.style.color='#fff'" onmouseout="this.style.background='#2d2d4e';this.style.color='#94a3b8'">${escapeHtml(p)}</button>`
    ).join('');
}
```

### Step 10: Wire Up Initialization and Backward Compatibility

**File:** `/var/www/mssql.danmarcrm.com/dev1/extension2/public/assets/app.js`
**Location:** Within the DOMContentLoaded listener (lines 2862-2881)

Add initialization call:

```javascript
document.addEventListener('DOMContentLoaded', () => {
    // Initialize provider/model dropdowns
    initAiProviderDropdown();

    // Existing draggable panel code...
    const header = document.getElementById('ai-chat-header');
    const panel = document.getElementById('ai-chat-panel');
    // ... (rest unchanged)
});
```

Ensure backward compatibility: the existing `openAiChatForThread()` and `openAiChatForContact()` functions continue to work. They set `aiChat.threadId` and `aiChat.contactId` which are still sent when `contextThreads` is empty. No changes needed to those functions -- they represent the "single thread from thread view" use case, while the context builder is the "search and add multiple" use case.

---

## Part 3: Token/Context Budget Management

When multiple threads are loaded, the total context can be very large. The system prompt with context is already sent as the `system` field to the AI proxy. Some guardrails should be added:

**In `api.php`, after building the context block, add a size check:**

```php
// Truncate context if too large (approximate token limit ~100K chars for most models)
$maxContextChars = 80000;
if (strlen($contextBlock) > $maxContextChars) {
    // Trim older threads first, keep most recent
    $contextBlock = substr($contextBlock, -$maxContextChars);
    $contextBlock = "... [Context truncated due to size. Showing most recent portion.]\n\n" . $contextBlock;
}
```

Alternatively, reduce per-email body size dynamically based on the number of threads:

```php
$maxBody = min(800, max(200, intval(60000 / max(1, $totalEmailCount))));
```

This ensures the total context stays roughly under 60K characters regardless of how many threads are selected.

---

## Part 4: Implementation Sequencing and Dependencies

**Phase 1 -- Backend API changes (api.php):**
1. Add `thread_ids` array parsing
2. Build multi-thread context loop
3. Fix provider/model mapping in proxy payload
4. Add context budget management

**Phase 2 -- Frontend state and functions (app.js):**
1. Extend `aiChat` state object
2. Add `AI_PROVIDER_MODELS` and `AI_PROVIDER_NAMES` constants
3. Add provider dropdown functions
4. Add context builder functions (search, select, add, remove, cart)
5. Modify `sendAiChat()` to send `thread_ids` and `provider`
6. Update `updateAiChatContextLabel()` for multi-thread display
7. Add multithread quick prompts
8. Wire up initialization

**Phase 3 -- HTML template (index.php):**
1. Replace static model dropdown with provider + model dropdowns
2. Insert context builder section between mode tabs and messages

**Phase 4 -- Testing:**
1. Test single-thread mode still works (backward compat)
2. Test searching for a contact by email
3. Test adding individual threads to context
4. Test "Add All" for a contact
5. Test switching providers and models
6. Test asking questions with multi-thread context
7. Test context truncation with large numbers of threads
8. Test with both MongoDB and SQL backends

---

## Part 5: Potential Challenges and Mitigations

1. **Context size limits:** With "all threads for a contact" there could be hundreds of emails. Mitigation: dynamic body truncation based on total email count, and a hard character limit with truncation notice.

2. **Provider model mismatch:** The Qwen, Mistral, Cohere providers exist in the Chrome extension's UI but are NOT registered in `api_codex51/src/AIOrchestrator.php`. Only OpenAI, Claude, Gemini, Grok, and DeepSeek are registered. Mitigation: Only show these 5 providers in the web app dropdown. Adding more providers requires registering them in `AIOrchestrator.registerProviders()` and adding their PHP provider classes to `api_codex51/src/Providers.php`.

3. **Thread ID resolution:** The `resolveThreadId()` function maps URL-style Gmail thread IDs (FMfcgz...) to hex IDs via `ThreadIdCache`. Each thread_id in the multi-thread array needs this resolution. Already handled in the plan.

4. **Performance with many threads:** Fetching 50 threads with emails sequentially could be slow. Mitigation: The context is built server-side in a single API call, so no N+1 HTTP requests. The MongoDB aggregation pipeline already supports batch lookups.

5. **Existing `contacts` API action uses SQL directly (lines 290-351):** It does NOT go through QueryAdapter. For MongoDB compatibility in the contact search, this would ideally use `QueryAdapter::getContactsWithProfiles()` instead. However, since the search works in the current app already, this is not a blocker for this feature.

---

### Critical Files for Implementation

- `/var/www/mssql.danmarcrm.com/dev1/extension2/public/assets/app.js` - Core frontend: extend aiChat state, add context builder functions, modify sendAiChat(), add provider/model selector logic (~200 lines of new code, ~30 lines modified)
- `/var/www/mssql.danmarcrm.com/dev1/extension2/public/api.php` - Backend API: modify ai_chat action to accept thread_ids array, build multi-thread context, fix provider/model mapping (~50 lines modified in the ai_chat case block, lines 2310-2491)
- `/var/www/mssql.danmarcrm.com/dev1/extension2/public/index.php` - HTML template: replace static model dropdown, add context builder section between lines 570-574 (~40 lines of new HTML)
- `/var/www/mssql.danmarcrm.com/dev1/extension2/chrome-extensionCopyMac20260220/chrome-extension/contentScript.js` - Reference file: PROVIDER_MODELS definition (lines 18-56) and savedAIContext pattern (lines 340-490) to draw from
- `/var/www/mssql.danmarcrm.com/dev1/extension2/aiapi/api_codex51/src/AIOrchestrator.php` - AI proxy orchestrator: understand how models/providers are resolved (line 72) to ensure correct model override format

agentId: af30ed7 (for resuming to continue this agent's work if needed)
<usage>total_tokens: 79511
tool_uses: 58
duration_ms: 362042</usage>

Prompt:
Design an implementation plan for adding a "context builder + AI chat" feature to the Email Intelligence web app. Here's all the context:

## What the user wants:
The user wants to replicate the Chrome extension's "Ask AI" functionality in the web app, with ENHANCED capabilities:
1. Search for contacts by email/name
2. Select a single thread, multiple threads, or ALL threads for a contact as AI context
3. Choose an AI model from a dropdown (multi-provider: Claude, GPT, Gemini, Grok, DeepSeek, etc.)
4. Ask questions about the selected context - e.g. "how much should I quote?", "what's the deception level?"

Example use cases:
- "Grab all emails for stephanie.k***@asendia.com and check how much we quoted, her profile, and ask how much to quote for task X"
- "Grab multiple threads of david.zolanz@digitalj2.com and ask the level of deception from david"

## Current web app AI chat (what already exists):
- File: `/var/www/mssql.danmarcrm.com/dev1/extension2/public/assets/app.js` (lines 2672-2882)
- Has modes: general, thread (single), contact, quote, reply
- Sends to `api.php?action=ai_chat` which builds context from ONE thread_id or ONE contact_id
- Has model dropdown but only basic models
- Draggable panel with chat history, quick prompts

## Current backend:
- `api.php?action=ai_chat` (lines 2310-2491) - builds context from single thread or contact
- `api.php?action=threads` - lists threads, filterable by contact_id and search
- `QueryAdapter::getThreads($contactId, $search, $limit, $offset)` - fetches threads
- MongoDB + SQL dual support throughout
- Thread emails stored with thread_id grouping
- Contact profiles with trust scores, communication style

## Chrome extension (what to draw inspiration from):
- Has provider dropdown + model dropdown with ALL providers (Claude, OpenAI, Gemini, Grok, DeepSeek, Qwen, Mistral, Cohere)
- Model list defined in JS (lines 18-56 of contentScript.js)
- `fetchEmailContext()` can fetch by thread ID or by email (returns contact + recent threads)
- Conversation history (last 10 exchanges)
- Quick question buttons

## The enhancement needed:
The current web chat can only load ONE thread or ONE contact as context. We need:

1. A **contact/thread search** UI in the AI chat panel (or a new dedicated panel)
2. Ability to **add multiple threads to context** - like a "shopping cart" for context
3. Ability to **"Add all threads for contact X"** with one click
4. The **multi-provider model dropdown** from the chrome extension
5. A **visible context summary** showing what's loaded (e.g. "3 threads, 15 emails from stephanie@...")
6. Backend API changes to accept multiple thread_ids and build combined context

## Key files to modify:
- `/var/www/mssql.danmarcrm.com/dev1/extension2/public/assets/app.js` - Frontend JS
- `/var/www/mssql.danmarcrm.com/dev1/extension2/public/api.php` - Backend API
- `/var/www/mssql.danmarcrm.com/dev1/extension2/public/index.php` - HTML for new UI elements

## Constraints:
- Keep using the existing draggable AI chat panel (enhance it, don't replace it)
- Must work with both MongoDB and SQL backends
- Keep the existing modes (thread, contact, quote, reply, general) but enhance them
- Reuse existing QueryAdapter and MongoDatabase classes
- Use the same AI proxy at `aiapi/api_codex51/api/index.php`

Design a detailed, step-by-step implementation plan.


Now I have a comprehensive plan. Let me also check what models the chrome extension actually has (since the Plan agent referenced some model names that might be outdated), and then check the index.php HTML to understand the current AI chat panel layout.


Let me read the exact HTML structure of the current AI chat panel and the chrome extension's model list to finalize the plan.


525→                    🤖
   526→

   527→
AI Assistant

   528→
General Chat

   529→

   530→

   531→

   532→                    <button onclick="clearAiChat()" title="Clear chat" style="background:rgba(255,255,255,0.15); border:none; color:#fff; border-radius:6px; padding:4px 8px; cursor:pointer; font-size:0.75em;">🗑 Clear</button>
   533→                    <button onclick="toggleAiChat()" style="background:rgba(255,255,255,0.15); border:none; color:#fff; border-radius:6px; padding:4px 8px; cursor:pointer; font-size:1em;">✕</button>
   534→

   535→

   536→
   537→
   538→

   539→

   540→                    MODEL
   541→                    <select id="ai-chat-model" style="flex:1; background:#1e1e2e; color:#e2e8f0; border:1px solid #3d3d5c; border-radius:6px; padding:4px 8px; font-size:0.78em; cursor:pointer;">
   542→                        <optgroup label="OpenAI">
   543→                            <option value="gpt-4o">GPT-4o (Latest)</option>
   544→                            <option value="gpt-4o-mini">GPT-4o Mini (Fast)</option>
   545→                            <option value="o1">o1 (Reasoning)</option>
   546→                            <option value="o1-mini">o1-mini</option>
   547→                        </optgroup>
   548→                        <optgroup label="Anthropic Claude">
   549→                            <option value="claude-3-5-sonnet-20241022">Claude 3.5 Sonnet</option>
   550→                            <option value="claude-3-5-haiku-20241022">Claude 3.5 Haiku (Fast)</option>
   551→                            <option value="claude-3-opus-20240229">Claude 3 Opus</option>
   552→                        </optgroup>
   553→                        <optgroup label="Google Gemini">
   554→                            <option value="gemini-2.0-flash-exp">Gemini 2.0 Flash</option>
   555→                            <option value="gemini-1.5-pro">Gemini 1.5 Pro</option>
   556→                            <option value="gemini-1.5-flash">Gemini 1.5 Flash</option>
   557→                        </optgroup>
   558→                        <optgroup label="xAI">
   559→                            <option value="grok-2-latest">Grok 2</option>
   560→                        </optgroup>
   561→                    </select>
   562→

   563→
   564→

   565→                    <button class="ai-mode-btn active" data-mode="general" onclick="setAiChatMode('general',this)" style="background:#6366f1; color:#fff; border:none; border-radius:5px; padding:3px 9px; font-size:0.7em; cursor:pointer;">💬 General</button>
   566→                    <button class="ai-mode-btn" data-mode="thread" onclick="setAiChatMode('thread',this)" style="background:#2d2d4e; color:#94a3b8; border:none; border-radius:5px; padding:3px 9px; font-size:0.7em; cursor:pointer;">📧 Thread</button>
   567→                    <button class="ai-mode-btn" data-mode="contact" onclick="setAiChatMode('contact',this)" style="background:#2d2d4e; color:#94a3b8; border:none; border-radius:5px; padding:3px 9px; font-size:0.7em; cursor:pointer;">👤 Contact</button>
   568→                    <button class="ai-mode-btn" data-mode="quote" onclick="setAiChatMode('quote',this)" style="background:#2d2d4e; color:#94a3b8; border:none; border-radius:5px; padding:3px 9px; font-size:0.7em; cursor:pointer;">💰 Quote</button>
   569→                    <button class="ai-mode-btn" data-mode="reply" onclick="setAiChatMode('reply',this)" style="background:#2d2d4e; color:#94a3b8; border:none; border-radius:5px; padding:3px 9px; font-size:0.7em; cursor:pointer;">✍ Reply</button>
   570→

   571→

   572→
   573→
   574→

   575→

   576→
🤖

   577→
Ask me anything about your threads, contacts, quotes, or compose replies.

   578→
Open a thread or contact first to use context modes.

   579→

   580→

   581→
   582→
   583→

   584→
   585→
   586→

   587→

   588→                    <textarea id="ai-chat-input" placeholder="Ask anything... (Enter to send, Shift+Enter for newline)" style="flex:1; background:#1e1e2e; color:#e2e8f0; border:1px solid #3d3d5c; border-radius:8px; padding:8px 10px; font-size:0.82em; resize:none; height:60px; font-family:inherit; outline:none;" onkeydown="handleAiChatKey(event)"></textarea>
   589→                    <button onclick="sendAiChat()" id="ai-chat-send-btn" style="background:linear-gradient(135deg,#6366f1,#8b5cf6); color:#fff; border:none; border-radius:8px; padding:8px 14px; cursor:pointer; font-size:1.1em; height:60px; min-width:44px; transition:opacity 0.2s;">➤</button>
   590→

   591→

   592→

   593→
   594→
   595→        <button id="ai-chat-fab" onclick="toggleAiChat()" title="AI Assistant" style="position:fixed; bottom:20px; right:20px; width:56px; height:56px; background:linear-gradient(135deg,#6366f1,#8b5cf6); color:#fff; border:none; border-radius:50%; font-size:1.5em; cursor:pointer; box-shadow:0 4px 20px rgba(99,102,241,0.5); z-index:9998; transition:transform 0.2s; display:flex; align-items:center; justify-content:center;" onmouseover="this.style.transform='scale(1.1)'" onmouseout="this.style.transform='scale(1)'">🤖</button>
   596→
   597→
   598→

   599→

   600→
Processing...

   601→

   602→

   603→
   604→

🤖

AI Assistant

General Chat

                    <button onclick="clearAiChat()" title="Clear chat" style="background:rgba(255,255,255,0.15); border:none; color:#fff; border-radius:6px; padding:4px 8px; cursor:pointer; font-size:0.75em;">🗑 Clear</button>
                    <button onclick="toggleAiChat()" style="background:rgba(255,255,255,0.15); border:none; color:#fff; border-radius:6px; padding:4px 8px; cursor:pointer; font-size:1em;">✕</button>

                    MODEL
                    <select id="ai-chat-model" style="flex:1; background:#1e1e2e; color:#e2e8f0; border:1px solid #3d3d5c; border-radius:6px; padding:4px 8px; font-size:0.78em; cursor:pointer;">
                        <optgroup label="OpenAI">
                            <option value="gpt-4o">GPT-4o (Latest)</option>
                            <option value="gpt-4o-mini">GPT-4o Mini (Fast)</option>
                            <option value="o1">o1 (Reasoning)</option>
                            <option value="o1-mini">o1-mini</option>
                        </optgroup>
                        <optgroup label="Anthropic Claude">
                            <option value="claude-3-5-sonnet-20241022">Claude 3.5 Sonnet</option>
                            <option value="claude-3-5-haiku-20241022">Claude 3.5 Haiku (Fast)</option>
                            <option value="claude-3-opus-20240229">Claude 3 Opus</option>
                        </optgroup>
                        <optgroup label="Google Gemini">
                            <option value="gemini-2.0-flash-exp">Gemini 2.0 Flash</option>
                            <option value="gemini-1.5-pro">Gemini 1.5 Pro</option>
                            <option value="gemini-1.5-flash">Gemini 1.5 Flash</option>
                        </optgroup>
                        <optgroup label="xAI">
                            <option value="grok-2-latest">Grok 2</option>
                        </optgroup>
                    </select>

                    <button class="ai-mode-btn active" data-mode="general" onclick="setAiChatMode('general',this)" style="background:#6366f1; color:#fff; border:none; border-radius:5px; padding:3px 9px; font-size:0.7em; cursor:pointer;">💬 General</button>
                    <button class="ai-mode-btn" data-mode="thread" onclick="setAiChatMode('thread',this)" style="background:#2d2d4e; color:#94a3b8; border:none; border-radius:5px; padding:3px 9px; font-size:0.7em; cursor:pointer;">📧 Thread</button>
                    <button class="ai-mode-btn" data-mode="contact" onclick="setAiChatMode('contact',this)" style="background:#2d2d4e; color:#94a3b8; border:none; border-radius:5px; padding:3px 9px; font-size:0.7em; cursor:pointer;">👤 Contact</button>
                    <button class="ai-mode-btn" data-mode="quote" onclick="setAiChatMode('quote',this)" style="background:#2d2d4e; color:#94a3b8; border:none; border-radius:5px; padding:3px 9px; font-size:0.7em; cursor:pointer;">💰 Quote</button>
                    <button class="ai-mode-btn" data-mode="reply" onclick="setAiChatMode('reply',this)" style="background:#2d2d4e; color:#94a3b8; border:none; border-radius:5px; padding:3px 9px; font-size:0.7em; cursor:pointer;">✍ Reply</button>

🤖

Ask me anything about your threads, contacts, quotes, or compose replies.

Open a thread or contact first to use context modes.

                    <textarea id="ai-chat-input" placeholder="Ask anything... (Enter to send, Shift+Enter for newline)" style="flex:1; background:#1e1e2e; color:#e2e8f0; border:1px solid #3d3d5c; border-radius:8px; padding:8px 10px; font-size:0.82em; resize:none; height:60px; font-family:inherit; outline:none;" onkeydown="handleAiChatKey(event)"></textarea>
                    <button onclick="sendAiChat()" id="ai-chat-send-btn" style="background:linear-gradient(135deg,#6366f1,#8b5cf6); color:#fff; border:none; border-radius:8px; padding:8px 14px; cursor:pointer; font-size:1.1em; height:60px; min-width:44px; transition:opacity 0.2s;">➤</button>

        <button id="ai-chat-fab" onclick="toggleAiChat()" title="AI Assistant" style="position:fixed; bottom:20px; right:20px; width:56px; height:56px; background:linear-gradient(135deg,#6366f1,#8b5cf6); color:#fff; border:none; border-radius:50%; font-size:1.5em; cursor:pointer; box-shadow:0 4px 20px rgba(99,102,241,0.5); z-index:9998; transition:transform 0.2s; display:flex; align-items:center; justify-content:center;" onmouseover="this.style.transform='scale(1.1)'" onmouseout="this.style.transform='scale(1)'">🤖</button>

Processing...


     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';

"use strict";

/**
 * Email Intelligence Content Script
 *
 * Integrates with Gmail to provide AI-powered email analysis
 * Pattern detection matches existing extension: polls every 2s for expanded emails
 */

(function() {
  console.log("[Email Intelligence] Content script loaded");

  // Version info
  const EXTENSION_VERSION = "2.1.1";
  const BUILD_DATE = "2024.12.30.2";

  // Provider/Model Configuration
  const PROVIDER_MODELS = {
    claude: [
      { id: 'claude-sonnet-4-5-20250929', name: 'Sonnet 4.5' },
      { id: 'claude-opus-4-5-20251101', name: 'Opus 4.5 (Best)' },
      { id: 'claude-haiku-4-5-20251001', name: 'Haiku 4.5 (Fast)' }
    ],
    openai: [
      { id: 'gpt-5.2', name: 'GPT-5.2 (Latest)' },
      { id: 'gpt-5.2-pro', name: 'GPT-5.2 Pro (Best)' },
      { id: 'gpt-5-mini', name: 'GPT-5 Mini' },
      { id: 'gpt-5-nano', name: 'GPT-5 Nano (Fast)' }
    ],
    gemini: [
      { id: 'gemini-2.5-flash', name: 'Gemini 2.5 Flash' },
      { id: 'gemini-3-pro', name: 'Gemini 3 Pro' },
      { id: 'gemini-2.5-flash-lite', name: 'Flash-Lite (Fast)' }
    ],
    grok: [
      { id: 'grok-4', name: 'Grok 4 (Latest)' },
      { id: 'grok-3', name: 'Grok 3' },
      { id: 'grok-3-mini', name: 'Grok 3 Mini (Fast)' }
    ],
    deepseek: [
      { id: 'deepseek-chat', name: 'DeepSeek V3.2' },
      { id: 'deepseek-reasoner', name: 'Reasoner (Thinking)' }
    ],
    qwen: [
      { id: 'qwen-max-latest', name: 'Qwen Max' },
      { id: 'qwen-turbo-latest', name: 'Qwen Turbo (Fast)' }
    ],
    mistral: [
      { id: 'mistral-large-latest', name: 'Mistral Large' },
      { id: 'mistral-small-latest', name: 'Mistral Small (Fast)' }
    ],
    cohere: [
      { id: 'command-a-03-2025', name: 'Command A' },
      { id: 'command-r7b-12-2024', name: 'Command R7B (Fast)' }
    ]
  };

  const PROVIDER_NAMES = {
    claude: 'Claude',
    openai: 'OpenAI',
    gemini: 'Gemini',
    grok: 'Grok',
    deepseek: 'DeepSeek',
    qwen: 'Qwen',
    mistral: 'Mistral',
    cohere: 'Cohere'
  };

  // Current AI selection (persisted to storage)
  let selectedProvider = 'claude';


Now I have all the information I need. Let me write the final plan. The key thing is to sync the model list with the chrome extension's list (which is newer and has different models than what's currently in the web app's index.php).

Let me write the plan now.


Now I have everything. Let me write the final plan.


Write

# Plan: AI Context Builder with Multi-Thread Support

## Context

The web app's AI chat panel currently only supports asking AI about **one thread** or **one contact** at a time. The Chrome extension already has a richer "Ask AI" with multi-provider model selection and optional context input. The user wants the web app to support:

- **Searching** for contacts by email/name
- **Selecting multiple threads** (or all threads for a contact) as AI context
- **Choosing AI provider + model** from a dropdown (synced with Chrome extension's list)
- **Asking questions** about the combined context (e.g. "how much to quote?", "deception level?")

## Files to Modify

| File | Changes |
|------|---------|
| [public/index.php](public/index.php) | Replace static model dropdown with provider+model selectors; add context builder HTML section |
| [public/assets/app.js](public/assets/app.js) | Extend `aiChat` state; add `PROVIDER_MODELS`; add context builder functions; modify `sendAiChat()` for multi-thread; update `renderQuickPrompts()` |
| [public/api.php](public/api.php) | Accept `thread_ids[]` + `provider`; build multi-thread context; fix model override format for proxy |

## Implementation Steps

### Step 1: Backend – api.php `ai_chat` action (lines 2310-2491)

**1a.** Parse new fields from JSON input:
- `thread_ids` (array of thread IDs) alongside existing `thread_id`
- `provider` (explicit provider code like "claude", "openai")

**1b.** Replace single-thread context build (lines 2331-2363) with a loop over all `thread_ids`:
- If `thread_ids` is provided, loop and fetch emails for each thread
- Label each as "THREAD 1 OF N", "THREAD 2 OF N" etc.
- Dynamically reduce per-email body length based on total email count: `min(800, max(200, 60000 / totalEmailCount))`
- Prepend summary line: "MULTI-THREAD CONTEXT: X total emails across Y threads"
- Include per-thread analysis summaries if available
- If only `thread_id` (single) is sent, keep existing behavior

**1c.** Fix provider detection (lines 2419-2424):
- Use explicit `provider` field from frontend when available
- Add `deepseek` detection for model name fallback

**1d.** Fix proxy payload model format (line 2431):
- Change `'models' => [$chatModel]` to `'models' => [$chatProvider => $chatModel]`
- This fixes a bug where the AI proxy ignores the selected model

### Step 2: Frontend JS – app.js

**2a.** Extend `aiChat` state (line 2672):
- Add `contextThreads: []` – array of `{thread_id, subject, email_count, contact_name}`
- Add `contextContact: null` – `{id, name, email}`

**2b.** Add `PROVIDER_MODELS` and `PROVIDER_NAMES` constants (after line 2684), synced with Chrome extension's list from `contentScript.js` lines 18-67:
- 8 providers: Claude, OpenAI, Gemini, Grok, DeepSeek, Qwen, Mistral, Cohere
- Same model IDs as the Chrome extension

**2c.** Add provider dropdown functions:
- `initAiProviderDropdown()` – populates provider select, calls `updateAiModelDropdown()`
- `updateAiModelDropdown()` – populates model select based on selected provider
- Call `initAiProviderDropdown()` from DOMContentLoaded

**2d.** Add context builder functions:
- `toggleContextBuilder()` – show/hide the search area
- `searchContextContacts()` – calls `apiCall('contacts', {search, limit:5})`, renders clickable results
- `selectContextContact(id, name, email)` – calls `apiCall('threads', {contact:id, limit:50})`, shows thread picker with "+ Add" buttons and an "Add All" button
- `addThreadToContext(threadId, subject, emailCount, contactName)` – pushes to `contextThreads[]`
- `removeThreadFromContext(threadId)` – removes from `contextThreads[]`
- `addAllContactThreads()` – adds all available threads at once
- `clearContextCart()` – clears all context
- `updateContextCart()` – renders the "cart" showing selected threads count and list with remove buttons

**2e.** Update `updateAiChatContextLabel()` (line 2728):
- When `contextThreads.length > 0`, show green text: "3 threads, ~15 emails from Stephanie"
- Otherwise keep existing mode-based label

**2f.** Add `multithread` quick prompts to `AI_QUICK_PROMPTS` (line 2686):
- "Summarize all conversations", "How much have we quoted?", "Deception level?", "Build a complete profile", "What patterns emerge?"
- Update `renderQuickPrompts()` to use `multithread` prompts when context builder has threads

**2g.** Modify `sendAiChat()` (line 2756):
- Read provider from `#ai-chat-provider` select
- If `contextThreads.length > 0`, send `thread_ids: [...]` array
- Otherwise send single `thread_id` as before (backward compat)
- Add `provider` to payload

### Step 3: HTML – index.php

**3a.** Replace static model dropdown (lines 538-561) with two selects:
- Provider dropdown (`#ai-chat-provider`) with `onchange="updateAiModelDropdown()"`
- Model dropdown (`#ai-chat-model`) – populated dynamically by JS

**3b.** Insert context builder section between mode tabs (line 570) and messages div (line 574):
- Context summary bar (always visible, clickable to expand) showing "No context loaded" or "3 threads, ~15 emails"
- Collapsible search area with:
  - Contact/email search input + search button
  - Search results container
  - Thread picker container (shows after contact selected)
  - Context cart (selected threads with remove buttons and "Clear All")

## Verification

1. Open https://mssql.danmarcrm.com/dev1/extension2/public/
2. Click the AI chat FAB button (bottom-right)
3. Verify provider dropdown shows all 8 providers, model updates on provider change
4. Click the context summary bar → search area expands
5. Search for a contact email (e.g. "stephanie" or "david.zolanz")
6. Click a contact → threads load with "+ Add" and "Add All" buttons
7. Add individual threads or click "Add All"
8. Verify context label turns green showing thread/email count
9. Verify quick prompts switch to multi-thread prompts
10. Ask a question → verify the AI response has context from ALL selected threads
11. Test backward compat: open a single thread view, click "Ask AI About This Thread" → still works with single thread mode
12. Test different providers/models work correctly


File created successfully at: /root/.claude/plans/scalable-doodling-peach.md

# Plan: AI Context Builder with Multi-Thread Support

## Context

The web app's AI chat panel currently only supports asking AI about **one thread** or **one contact** at a time. The Chrome extension already has a richer "Ask AI" with multi-provider model selection and optional context input. The user wants the web app to support:

- **Searching** for contacts by email/name
- **Selecting multiple threads** (or all threads for a contact) as AI context
- **Choosing AI provider + model** from a dropdown (synced with Chrome extension's list)
- **Asking questions** about the combined context (e.g. "how much to quote?", "deception level?")

## Files to Modify

| File | Changes |
|------|---------|
| [public/index.php](public/index.php) | Replace static model dropdown with provider+model selectors; add context builder HTML section |
| [public/assets/app.js](public/assets/app.js) | Extend `aiChat` state; add `PROVIDER_MODELS`; add context builder functions; modify `sendAiChat()` for multi-thread; update `renderQuickPrompts()` |
| [public/api.php](public/api.php) | Accept `thread_ids[]` + `provider`; build multi-thread context; fix model override format for proxy |

## Implementation Steps

### Step 1: Backend – api.php `ai_chat` action (lines 2310-2491)

**1a.** Parse new fields from JSON input:
- `thread_ids` (array of thread IDs) alongside existing `thread_id`
- `provider` (explicit provider code like "claude", "openai")

**1b.** Replace single-thread context build (lines 2331-2363) with a loop over all `thread_ids`:
- If `thread_ids` is provided, loop and fetch emails for each thread
- Label each as "THREAD 1 OF N", "THREAD 2 OF N" etc.
- Dynamically reduce per-email body length based on total email count: `min(800, max(200, 60000 / totalEmailCount))`
- Prepend summary line: "MULTI-THREAD CONTEXT: X total emails across Y threads"
- Include per-thread analysis summaries if available
- If only `thread_id` (single) is sent, keep existing behavior

**1c.** Fix provider detection (lines 2419-2424):
- Use explicit `provider` field from frontend when available
- Add `deepseek` detection for model name fallback

**1d.** Fix proxy payload model format (line 2431):
- Change `'models' => [$chatModel]` to `'models' => [$chatProvider => $chatModel]`
- This fixes a bug where the AI proxy ignores the selected model

### Step 2: Frontend JS – app.js

**2a.** Extend `aiChat` state (line 2672):
- Add `contextThreads: []` – array of `{thread_id, subject, email_count, contact_name}`
- Add `contextContact: null` – `{id, name, email}`

**2b.** Add `PROVIDER_MODELS` and `PROVIDER_NAMES` constants (after line 2684), synced with Chrome extension's list from `contentScript.js` lines 18-67:
- 8 providers: Claude, OpenAI, Gemini, Grok, DeepSeek, Qwen, Mistral, Cohere
- Same model IDs as the Chrome extension

**2c.** Add provider dropdown functions:
- `initAiProviderDropdown()` – populates provider select, calls `updateAiModelDropdown()`
- `updateAiModelDropdown()` – populates model select based on selected provider
- Call `initAiProviderDropdown()` from DOMContentLoaded

**2d.** Add context builder functions:
- `toggleContextBuilder()` – show/hide the search area
- `searchContextContacts()` – calls `apiCall('contacts', {search, limit:5})`, renders clickable results
- `selectContextContact(id, name, email)` – calls `apiCall('threads', {contact:id, limit:50})`, shows thread picker with "+ Add" buttons and an "Add All" button
- `addThreadToContext(threadId, subject, emailCount, contactName)` – pushes to `contextThreads[]`
- `removeThreadFromContext(threadId)` – removes from `contextThreads[]`
- `addAllContactThreads()` – adds all available threads at once
- `clearContextCart()` – clears all context
- `updateContextCart()` – renders the "cart" showing selected threads count and list with remove buttons

**2e.** Update `updateAiChatContextLabel()` (line 2728):
- When `contextThreads.length > 0`, show green text: "3 threads, ~15 emails from Stephanie"
- Otherwise keep existing mode-based label

**2f.** Add `multithread` quick prompts to `AI_QUICK_PROMPTS` (line 2686):
- "Summarize all conversations", "How much have we quoted?", "Deception level?", "Build a complete profile", "What patterns emerge?"
- Update `renderQuickPrompts()` to use `multithread` prompts when context builder has threads

**2g.** Modify `sendAiChat()` (line 2756):
- Read provider from `#ai-chat-provider` select
- If `contextThreads.length > 0`, send `thread_ids: [...]` array
- Otherwise send single `thread_id` as before (backward compat)
- Add `provider` to payload

### Step 3: HTML – index.php

**3a.** Replace static model dropdown (lines 538-561) with two selects:
- Provider dropdown (`#ai-chat-provider`) with `onchange="updateAiModelDropdown()"`
- Model dropdown (`#ai-chat-model`) – populated dynamically by JS

**3b.** Insert context builder section between mode tabs (line 570) and messages div (line 574):
- Context summary bar (always visible, clickable to expand) showing "No context loaded" or "3 threads, ~15 emails"
- Collapsible search area with:
  - Contact/email search input + search button
  - Search results container
  - Thread picker container (shows after contact selected)
  - Context cart (selected threads with remove buttons and "Clear All")

## Verification

1. Open https://mssql.danmarcrm.com/dev1/extension2/public/
2. Click the AI chat FAB button (bottom-right)
3. Verify provider dropdown shows all 8 providers, model updates on provider change
4. Click the context summary bar → search area expands
5. Search for a contact email (e.g. "stephanie" or "david.zolanz")
6. Click a contact → threads load with "+ Add" and "Add All" buttons
7. Add individual threads or click "Add All"
8. Verify context label turns green showing thread/email count
9. Verify quick prompts switch to multi-thread prompts
10. Ask a question → verify the AI response has context from ALL selected threads
11. Test backward compat: open a single thread view, click "Ask AI About This Thread" → still works with single thread mode
12. Test different providers/models work correctly


ExitPlanMode

# Plan: AI Context Builder with Multi-Thread Support

## Context

The web app's AI chat panel currently only supports asking AI about **one thread** or **one contact** at a time. The Chrome extension already has a richer "Ask AI" with multi-provider model selection and optional context input. The user wants the web app to support:

- **Searching** for contacts by email/name
- **Selecting multiple threads** (or all threads for a contact) as AI context
- **Choosing AI provider + model** from a dropdown (synced with Chrome extension's list)
- **Asking questions** about the combined context (e.g. "how much to quote?", "deception level?")

## Files to Modify

| File | Changes |
|------|---------|
| [public/index.php](public/index.php) | Replace static model dropdown with provider+model selectors; add context builder HTML section |
| [public/assets/app.js](public/assets/app.js) | Extend `aiChat` state; add `PROVIDER_MODELS`; add context builder functions; modify `sendAiChat()` for multi-thread; update `renderQuickPrompts()` |
| [public/api.php](public/api.php) | Accept `thread_ids[]` + `provider`; build multi-thread context; fix model override format for proxy |

## Implementation Steps

### Step 1: Backend – api.php `ai_chat` action (lines 2310-2491)

**1a.** Parse new fields from JSON input:
- `thread_ids` (array of thread IDs) alongside existing `thread_id`
- `provider` (explicit provider code like "claude", "openai")

**1b.** Replace single-thread context build (lines 2331-2363) with a loop over all `thread_ids`:
- If `thread_ids` is provided, loop and fetch emails for each thread
- Label each as "THREAD 1 OF N", "THREAD 2 OF N" etc.
- Dynamically reduce per-email body length based on total email count: `min(800, max(200, 60000 / totalEmailCount))`
- Prepend summary line: "MULTI-THREAD CONTEXT: X total emails across Y threads"
- Include per-thread analysis summaries if available
- If only `thread_id` (single) is sent, keep existing behavior

**1c.** Fix provider detection (lines 2419-2424):
- Use explicit `provider` field from frontend when available
- Add `deepseek` detection for model name fallback

**1d.** Fix proxy payload model format (line 2431):
- Change `'models' => [$chatModel]` to `'models' => [$chatProvider => $chatModel]`
- This fixes a bug where the AI proxy ignores the selected model

### Step 2: Frontend JS – app.js

**2a.** Extend `aiChat` state (line 2672):
- Add `contextThreads: []` – array of `{thread_id, subject, email_count, contact_name}`
- Add `contextContact: null` – `{id, name, email}`

**2b.** Add `PROVIDER_MODELS` and `PROVIDER_NAMES` constants (after line 2684), synced with Chrome extension's list from `contentScript.js` lines 18-67:
- 8 providers: Claude, OpenAI, Gemini, Grok, DeepSeek, Qwen, Mistral, Cohere
- Same model IDs as the Chrome extension

**2c.** Add provider dropdown functions:
- `initAiProviderDropdown()` – populates provider select, calls `updateAiModelDropdown()`
- `updateAiModelDropdown()` – populates model select based on selected provider
- Call `initAiProviderDropdown()` from DOMContentLoaded

**2d.** Add context builder functions:
- `toggleContextBuilder()` – show/hide the search area
- `searchContextContacts()` – calls `apiCall('contacts', {search, limit:5})`, renders clickable results
- `selectContextContact(id, name, email)` – calls `apiCall('threads', {contact:id, limit:50})`, shows thread picker with "+ Add" buttons and an "Add All" button
- `addThreadToContext(threadId, subject, emailCount, contactName)` – pushes to `contextThreads[]`
- `removeThreadFromContext(threadId)` – removes from `contextThreads[]`
- `addAllContactThreads()` – adds all available threads at once
- `clearContextCart()` – clears all context
- `updateContextCart()` – renders the "cart" showing selected threads count and list with remove buttons

**2e.** Update `updateAiChatContextLabel()` (line 2728):
- When `contextThreads.length > 0`, show green text: "3 threads, ~15 emails from Stephanie"
- Otherwise keep existing mode-based label

**2f.** Add `multithread` quick prompts to `AI_QUICK_PROMPTS` (line 2686):
- "Summarize all conversations", "How much have we quoted?", "Deception level?", "Build a complete profile", "What patterns emerge?"
- Update `renderQuickPrompts()` to use `multithread` prompts when context builder has threads

**2g.** Modify `sendAiChat()` (line 2756):
- Read provider from `#ai-chat-provider` select
- If `contextThreads.length > 0`, send `thread_ids: [...]` array
- Otherwise send single `thread_id` as before (backward compat)
- Add `provider` to payload

### Step 3: HTML – index.php

**3a.** Replace static model dropdown (lines 538-561) with two selects:
- Provider dropdown (`#ai-chat-provider`) with `onchange="updateAiModelDropdown()"`
- Model dropdown (`#ai-chat-model`) – populated dynamically by JS

**3b.** Insert context builder section between mode tabs (line 570) and messages div (line 574):
- Context summary bar (always visible, clickable to expand) showing "No context loaded" or "3 threads, ~15 emails"
- Collapsible search area with:
  - Contact/email search input + search button
  - Search results container
  - Thread picker container (shows after contact selected)
  - Context cart (selected threads with remove buttons and "Clear All")

## Verification

1. Open https://mssql.danmarcrm.com/dev1/extension2/public/
2. Click the AI chat FAB button (bottom-right)
3. Verify provider dropdown shows all 8 providers, model updates on provider change
4. Click the context summary bar → search area expands
5. Search for a contact email (e.g. "stephanie" or "david.zolanz")
6. Click a contact → threads load with "+ Add" and "Add All" buttons
7. Add individual threads or click "Add All"
8. Verify context label turns green showing thread/email count
9. Verify quick prompts switch to multi-thread prompts
10. Ask a question → verify the AI response has context from ALL selected threads
11. Test backward compat: open a single thread view, click "Ask AI About This Thread" → still works with single thread mode
12. Test different providers/models work correctly


The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.


[Request interrupted by user for tool use]