This session is being continued from a previous conversation that ran out of context. The summary below covers the earlier portion of the conversation.

Analysis:
Let me chronologically analyze the entire conversation:

**Session 1 (from previous conversation summary):**
1. User introduced the project at `/var/www/mssql.danmarcrm.com/dev1/extension2/` - email intelligence system
2. Created PROJECT_OVERVIEW.md
3. Synced Chrome extension from Mac to server
4. Built full AI chat system for web app: multi-thread context builder, 5 AI agents, 8 AI providers, chat session persistence, contact memory, token budget management
5. Fixed bugs: upsert() → insertOrUpdate(), contacts API using raw SQL → QueryAdapter, AI proxy model format
6. Created CLAUDE.md and updated MEMORY.md

**Session 2 (current conversation):**

**Message 1: User asks about which folder had changes**
"in which folder we had the changes done?"
I responded with the list of modified files in extension2.

**Message 2: User reveals the work should have been in extension2v1**
"I think we were supposed to do it in /var/www/mssql.danmarcrm.com/dev1/extension2v1/"
"Pls check the differences and apply to extension2v1 as well"

I then:
1. Compared all 4 key files between extension2 and extension2v1 using diff
2. Discovered extension2v1 is a DIFFERENT/NEWER version with extra features (Products tab, Business Ideas tab, ImportantContext class, thread_view.php redirect)
3. Ported changes carefully:

**ThreadAnalyzer.php** - Two changes:
- `$this->stripQuotedContent($body)` → `self::stripQuotedContent($body)` at line 404
- `private function stripQuotedContent` → `public static function stripQuotedContent` at line 1605

**api.php** - Three major additions:
- Replaced raw SQL contacts case with QueryAdapter::getContactsWithProfiles()
- Added entire `ai_chat` case (multi-thread, agents, token budget, sessions, memory) after `ask_ai` case
- Added 6 new endpoints before `default` case: ai_chat_sessions, ai_chat_session, ai_chat_session_delete, ai_contact_memories, ai_contact_memory_add, ai_contact_memory_delete

**app.js** - Appended entire AI chat panel JS block at end:
- Provider/model constants (8 providers)
- Agent presets (5 agents)
- Context builder functions
- Session management
- Contact memory
- Enhanced sendAiChat()
- Note: I converted arrow functions to regular functions and optional chaining to regular checks for Node v10 compatibility

**index.php** - Added before Loading Overlay:
- Full AI chat floating panel HTML
- FAB toggle button

All PHP syntax checks passed.

**Message 3: User asks to enhance thread_view.php**
"Ok, I just found we had something similar implemented..."
"But what we have now is better in terms as search for contact/thread"
"Pls implement our new features to existing thread view all page wide."

Requirements:
- Full page width, not wider
- Keep multi-model selection (send to multiple AI simultaneously)
- Add General/Quotes/Deception/Profiler/Composer agent buttons with sub-buttons
- Search by email/thread with + buttons to include any thread/email
- Estimated tokens, don't exceed max
- Save results (keep existing)
- Continue discussing after initial result
- Log everything sent to AI and response to file for debug

I entered plan mode and started exploring the codebase.

**Message 4: User adds more requirements**
The user repeated the thread_view.php request but added 3 MORE requirements:
- "the main page should be with threads, and not that many to load it faster. The contacts should display also less records to speed up"
- "to fix, in threads list view all emails are assigned to Etienne Turner .. or nearly all for sure girls bonuses not related to him"
- "in Recent Analysis there are marked with Unknown .. not sure if it makes sense all the AI Thread Analysis tab .. if you don't find it usefull hide it and make sure it doesn't take time to load numbers or counts"

I launched 3 parallel Explore agents to investigate:

**Agent 1 findings (thread list and contacts):**
- Threads loaded via QueryAdapter::getThreads() with limit 20
- Contact name bug: `MAX(c.name)` in SQL and `$max` on contact_id in MongoDB picks wrong contact
- AI Thread Analysis section runs heavy queries: COUNT(DISTINCT thread_id), N+1 queries for 5 recent threads, auto-refreshes every 30 seconds
- DOMContentLoaded runs: loadContacts(), loadFilters(), refreshAnalysisStatus()

**Agent 2 findings (AI proxy):**
- save_ai_response goes to MongoDB `ai_query_responses` collection
- api_opus/query.php handles multi-provider in one request
- Existing logging at logs/ai_analysis/, logs/prompt.log, etc.
- Models keyed by provider: `[$provider => $model]`

**Agent 3 findings (QueryAdapter):**
- getThreadsMongo() line 609: `'contact_id' => ['$max' => '$contact_id']` is the bug
- getThreadsSQL() line 549-550: `MAX(e.contact_id)` and `MAX(c.name)` is the bug
- getContactsWithProfiles() already properly fixed
- MongoDB version's `$arrayElemAt` for contact lookup is correct technique but fed wrong contact_id

**Plan agent designed detailed implementation for 4 tasks:**

TASK 1: Enhance thread_view.php (full-width, agents, context builder, tokens, logging)
- Keep existing multi-provider flow via api_opus proxy
- Add agent presets as JS constants with system prompts
- Context builder searching contacts/threads APIs
- Token estimation with model limits
- File logging via new log_ai_debug endpoint

TASK 2: Speed up main page
- Reduce thread/contact limits from 20 to 10

TASK 3: Fix thread contact name bug
- In getThreadsMongo(): Collect all contacts per thread, post-process to find most frequent external sender
- In getThreadsSQL(): Use subquery for most frequent external contact

TASK 4: Hide AI Thread Analysis
- Add display:none to section HTML
- Comment out refreshAnalysisStatus() and setInterval calls

**Current state:** I was in plan mode, had just finished reading QueryAdapter.php lines 569-650 and app.js lines 155-194 to verify findings before writing the final plan. The plan file exists at `/root/.claude/plans/scalable-doodling-peach.md` but hasn't been written to yet.

The todo list shows:
1. [pending] Add AI debug logging endpoint to api.php
2. [pending] Rewrite thread_view.php with agents, context builder, tokens, logging
3. [pending] Verify PHP syntax and test readiness

Summary:
1. Primary Request and Intent:

**Phase 1 (completed):** Port AI chat changes from `extension2/` to `extension2v1/`. The user discovered work was done in the wrong directory. All 4 files were successfully ported: ThreadAnalyzer.php, api.php, app.js, index.php.

**Phase 2 (in progress - planning):** The user found an existing `thread_view.php` page at `https://mssql.danmarcrm.com/dev1/extension2v1/public/thread_view.php?thread_id=19ae5c0cb75e381c` and wants it enhanced with all the new AI chat features, PLUS several fixes to the main dashboard. Specific requirements:

- **thread_view.php enhancements:**
  - Full page width layout (not wider than viewport)
  - Keep existing multi-model selection (send to multiple AI providers simultaneously)
  - Add General/Quotes/Deception/Profiler/Composer agent buttons with quick-prompt sub-buttons
  - Add context builder: search by email/thread with + buttons to include any thread/email
  - Show estimated tokens and enforce model max limits
  - Keep saving results (existing save_ai_response works)
  - Continue discussing after initial AI result (conversation history)
  - Log EVERYTHING sent to AI and AI response in a debug log file

- **Main page (index.php) speed fixes:**
  - Load fewer threads and contacts to speed up page load
  - Fix thread contact name bug: all threads showing "Etienne Turner" instead of actual contacts
  - Hide the "AI Thread Analysis" section (shows "Unknown", not useful, slow to load with heavy N+1 queries and 30-second auto-refresh)

2. Key Technical Concepts:
   - MongoDB as primary database (SQL is legacy/empty) — always use `QueryAdapter`
   - `MongoDatabase::insertOrUpdate()` for upserts (NOT `upsert()`)
   - AI proxy at `aiapi/api_codex51/api/index.php` — models keyed by provider: `[$provider => $model]`
   - Second AI proxy at `aiapi/api_opus/api/query.php` — supports multi-provider simultaneous requests (used by thread_view.php)
   - `ThreadAnalyzer::stripQuotedContent()` — public static method for removing quoted email content
   - Token budget: `strlen($contextBlock) / 3.5` for estimation, 60% of model limit for context
   - MongoDB aggregation pipelines with `$group`, `$lookup`, `$project` stages
   - The `$max` aggregator in MongoDB group stages picks arbitrary contact_id (bug)
   - Node.js on server is v10.19 — no arrow functions or optional chaining in server-checked JS
   - `ai_query_responses` MongoDB collection for saved thread_view AI responses
   - `ai_chat_sessions` MongoDB collection for floating panel chat sessions
   - `ai_contact_memory` MongoDB collection for auto-extracted contact facts

3. Files and Code Sections:

   - **`/var/www/mssql.danmarcrm.com/dev1/extension2v1/src/ThreadAnalyzer.php`**
     - Changed `stripQuotedContent()` to public static so it can be called from api.php
     - Line 404: `$this->stripQuotedContent($body)` → `self::stripQuotedContent($body)`
     - Line 1605: `private function stripQuotedContent` → `public static function stripQuotedContent`

   - **`/var/www/mssql.danmarcrm.com/dev1/extension2v1/public/api.php`** (5546 lines after edits)
     - **contacts case (~line 290):** Replaced raw SQL with QueryAdapter::getContactsWithProfiles(), strips wildcard chars
     - **ai_chat case (~line 2257):** Full rewrite with multi-thread support, 5 agent presets, token budget management, contact memory, session persistence. Routes through codex51 proxy.
     - **6 new endpoints** added before default case (~line 5408): ai_chat_sessions, ai_chat_session, ai_chat_session_delete, ai_contact_memories, ai_contact_memory_add, ai_contact_memory_delete
     - **save_ai_response case** (~line 4749): Already exists, saves to MongoDB `ai_query_responses` collection
     - **analysis_status case** (~line 990): Heavy endpoint with N+1 queries, COUNT DISTINCT, runs every 30s — to be disabled
     - **Needs new `log_ai_debug` endpoint** for file-based debug logging

   - **`/var/www/mssql.danmarcrm.com/dev1/extension2v1/public/assets/app.js`** (3958 lines after edits)
     - AI chat panel JS appended at end (from line ~3196): provider/model constants, agent presets, context builder, session management, contact memory functions
     - `loadContacts()` at line 163: currently `limit: 20` → needs to be `limit: 10`
     - `loadEmails()` at ~line 650: currently `limit: 20` → needs to be `limit: 10`
     - `refreshAnalysisStatus()` called on DOMContentLoaded and `setInterval(refreshAnalysisStatus, 30000)` — both need to be disabled

   - **`/var/www/mssql.danmarcrm.com/dev1/extension2v1/public/index.php`** (854 lines after edits)
     - AI chat floating panel HTML added before Loading Overlay (~line 733)
     - FAB toggle button added
     - Analysis status section (~line 136) needs `display:none`

   - **`/var/www/mssql.danmarcrm.com/dev1/extension2v1/public/thread_view.php`** (2033 lines, NOT YET MODIFIED)
     - Existing architecture: 2-column grid (left: email timeline + responses, right: AI form 400px)
     - Currently has: max-width 1600px page container, multi-provider checkboxes, content processing mode (full/stripped/smart), question/context textareas, conversation history in sessionStorage, saved responses from MongoDB `ai_query_responses`, quick view modal
     - Sends to `../aiapi/api_opus/api/query.php` for multi-provider support
     - Saves via `api.php?action=save_ai_response`
     - **Needs:** full-width layout, agent buttons + quick prompts, context builder (search + add threads), token estimation, debug file logging, system prompt injection per agent

   - **`/var/www/mssql.danmarcrm.com/dev1/extension2v1/src/QueryAdapter.php`**
     - **getThreadsMongo()** (lines 569-650): Bug at line 609 — `'contact_id' => ['$max' => '$contact_id']` picks highest contact_id, not most frequent sender
     - **getThreadsSQL()** (lines 514-567): Bug at lines 549-550 — `MAX(e.contact_id)` and `MAX(c.name)` picks wrong contact
     - Fix: Collect all contacts per thread, post-process to find most frequent external sender (excluding user's own email from GMAIL_USER_EMAIL constant)

   - **Logging infrastructure:**
     - Existing logs at `/var/www/mssql.danmarcrm.com/dev1/extension2/logs/` — ai_analysis/, prompt.log, mongodb.log, exceptions.log
     - New log file needed: `/var/www/mssql.danmarcrm.com/dev1/extension2v1/logs/ai_debug.log`

4. Errors and fixes:
   - **`upsert()` method doesn't exist on MongoDatabase** (from previous session): Fixed by replacing with `insertOrUpdate()` — two occurrences in api.php
   - **Node.js v10 syntax failure**: `node -c app.js` failed on optional chaining `?.`. Pre-existing code issue, not from our changes. Modern browsers handle it fine. Noted to never use Node for JS syntax checks.
   - **Contacts search returning empty**: Root cause was `contacts` API case using raw SQL (`$db->fetchAll`) when data is in MongoDB. Fixed with `QueryAdapter::getContactsWithProfiles()`
   - **AI proxy model override ignored**: `'models' => [$chatModel]` (numerically indexed) was wrong. Fixed to `'models' => [$chatProvider => $chatModel]` (keyed by provider)
   - **rsync skipping directory**: User's rsync command missing `-r` flag
   - **Thread contact name bug (identified, not yet fixed)**: `MAX(contact_id)` / `MAX(c.name)` in QueryAdapter getThreads methods picks wrong contact. All threads show "Etienne Turner". Need to find most frequent external sender per thread.
   - **AI Thread Analysis section slow (identified, not yet fixed)**: N+1 queries, COUNT DISTINCT on all emails, 30-second auto-refresh interval. Shows "Unknown" for contact names. User wants it hidden.

5. Problem Solving:
   - Successfully identified that extension2v1 is a different/newer version with extra features (Products, Business Ideas, ImportantContext, thread_view.php) — ported changes without overwriting v1-specific features
   - Converted all arrow functions and template literals to regular functions/string concatenation in app.js for broader compatibility
   - Identified thread contact name bug root cause via QueryAdapter exploration: `$max` aggregator and `MAX()` SQL function don't find the most relevant contact
   - Identified main page performance issues: heavy analysis_status endpoint with N+1 queries running on 30-second interval
   - Plan designed for 4 tasks with specific implementation order: Task 4 (hide analysis) → Task 2 (reduce limits) → Task 3 (fix contact bug) → Task 1 (enhance thread_view.php)

6. All user messages:
   - "in which folder we had the changes done?"
   - "I think we were supposed to do it in /var/www/mssql.danmarcrm.com/dev1/extension2v1/ Pls check the differences and appy to /var/www/mssql.danmarcrm.com/dev1/extension2v1/ as well"
   - "Ok, I just found we had something similar implemented ex: https://mssql.danmarcrm.com/dev1/extension2v1/public/thread_view.php?thread_id=19ae5c0cb75e381c But what we have now is better in terms as search for contact/thread Pls implement our new features to existing thread view all page wide. Make sure the total widh will be the whole page and not go wider - keep the selection model so from the thread view so we can send to multiple AI at the same time - but implement the General/Quotes/Deception/Profiler/Composer buttons with subbuttons - as stated before use the new search by email/thread and + buttons so we can include any thread email etc - of course add the estimated tokens and make sure we don't go over the max accepted - as before results should be saved no changes here unless there must be - we should be able to keep discussing after an initial result - pls save in a log file everything we are sending to AI and the AI response for easy debug"
   - (Repeated the above with 3 additional requirements): "- the main page should be with threads, and not that many to load it faster. The contacts should display also less records to speed up - to fix, in threads list view all emails are assigned to Etienne Turner .. or nearly all for sure girls bonuses not related to him - in Recent Analysis there are marked with Unknown .. not sure if it makes sense all the AI Thread Analysis tab .. if you don't find it usefull hide it and make sure it doesn't take time to load numbers or counts"

7. Pending Tasks:
   - **TASK 1:** Enhance thread_view.php — full-width layout, agent buttons with quick prompts, context builder (search + add threads), token estimation, debug file logging, system prompt injection per agent
   - **TASK 2:** Speed up main page — reduce thread/contact load limits from 20 to 10
   - **TASK 3:** Fix thread contact name bug in QueryAdapter.php — getThreadsMongo() and getThreadsSQL() need to find most frequent external sender instead of MAX(contact_id)
   - **TASK 4:** Hide AI Thread Analysis section — add display:none, disable refreshAnalysisStatus() and setInterval auto-refresh
   - **TASK 1E sub-task:** Add `log_ai_debug` endpoint to api.php for file-based debug logging

8. Current Work:
   I am in **plan mode** (entered via EnterPlanMode). I have completed Phase 1 (exploration with 3 parallel Explore agents) and Phase 2 (Plan agent designed the implementation). I was in Phase 3 (review) — had just read QueryAdapter.php lines 569-650 and app.js lines 155-194 to verify exploration findings. The plan file at `/root/.claude/plans/scalable-doodling-peach.md` exists but has NOT been written to yet. I need to write the final plan and call ExitPlanMode.

9. Optional Next Step:
   Write the final implementation plan to `/root/.claude/plans/scalable-doodling-peach.md` covering all 4 tasks with specific file changes, then call ExitPlanMode for user approval. The implementation order should be: Task 4 (hide analysis, 5 min) → Task 2 (reduce limits, 5 min) → Task 3 (fix contact bug, 30 min) → Task 1 (enhance thread_view.php, 2-3 hours).

   Key quote from user's most recent request: "Pls implement our new features to existing thread view all page wide. Make sure the total widh will be the whole page and not go wider - keep the selection model so from the thread view so we can send to multiple AI at the same time - but implement the General/Quotes/Deception/Profiler/Composer buttons with subbuttons - as stated before use the new search by email/thread and + buttons so we can include any thread email etc - of course add the estimated tokens and make sure we don't go over the max accepted - as before results should be saved no changes here unless there must be - we should be able to keep discussing after an initial result - pls save in a log file everything we are sending to AI and the AI response for easy debug - the main page should be with threads, and not that many to load it faster. The contacts should display also less records to speed up - to fix, in threads list view all emails are assigned to Etienne Turner .. or nearly all for sure girls bonuses not related to him - in Recent Analysis there are marked with Unknown .. not sure if it makes sense all the AI Thread Analysis tab .. if you don't find it usefull hide it and make sure it doesn't take time to load numbers or counts"

If you need specific details from before compaction (like exact code snippets, error messages, or content you generated), read the full transcript at: /root/.claude/projects/-var-www-mssql-danmarcrm-com-dev1-extension2/75ef92ac-556a-4223-9d55-17c4e535294d.jsonl
Please continue the conversation from where we left off without asking the user any further questions. Continue with the last task that you were asked to work on.


I need to continue from where I left off. I was in plan mode and needed to write the final implementation plan to the plan file, then call ExitPlanMode. Let me first read the current plan file to see what's there, then write the comprehensive final plan.

Let me read the current plan file and some key files I need to reference for the plan.


I need to finalize the plan file and call ExitPlanMode. Let me first read the current plan file and verify a few key file sections.


     1→# Plan: AI Context Builder with Agents & Memory
     2→
     3→## Context
     4→
     5→The web app's AI chat panel currently only supports asking AI about **one thread** or **one contact** at a time, with no persistence and no deduplication. We're building a full-featured AI chat system with:
     6→
     7→- **Multi-thread context**: Search contacts, select multiple threads as AI context
     8→- **Named AI agents**: Specialized presets (Quote Analyst, Deception Detector, Profiler, Composer)
     9→- **Contact memory**: Persistent facts about contacts that accumulate over time
    10→- **Chat session persistence**: Save/reload/continue conversations
    11→- **Smart token management**: Strip quoted email content, enforce context budgets
    12→
    13→## Files to Modify
    14→
    15→| File | Changes |
    16→|------|---------|
    17→| [public/api.php](public/api.php) | Multi-thread context, quoted content stripping, token budget, session CRUD, agent presets, contact memory endpoints |
    18→| [public/assets/app.js](public/assets/app.js) | Context builder, provider/model selector, agent selector, session management, memory UI |
    19→| [public/index.php](public/index.php) | Provider+model dropdowns, context builder HTML, agent selector, session list, memory panel |
    20→| [src/ThreadAnalyzer.php](src/ThreadAnalyzer.php) | Make `stripQuotedContent()` public static (line 1616) |
    21→
    22→## Data Structures (MongoDB collections)
    23→
    24→### `ai_chat_sessions`
    25→```json
    26→{
    27→  "session_id": "uuid",
    28→  "created_at": "datetime",
    29→  "updated_at": "datetime",
    30→  "title": "auto from first question",
    31→  "agent": "general|quote_analyst|deception_detector|contact_profiler|email_composer",
    32→  "provider": "claude",
    33→  "model": "claude-sonnet-4-5-20250929",
    34→  "context_thread_ids": ["tid1", "tid2"],
    35→  "contact_id": 123,
    36→  "contact_name": "Stephanie K",
    37→  "messages": [
    38→    {"role": "user", "content": "...", "timestamp": "datetime"},
    39→    {"role": "assistant", "content": "...", "timestamp": "datetime", "model": "claude-sonnet-4-5"}
    40→  ]
    41→}
    42→```
    43→
    44→### `ai_contact_memory`
    45→```json
    46→{
    47→  "contact_id": 123,
    48→  "contact_email": "stephanie@asendia.com",
    49→  "memories": [
    50→    {
    51→      "id": "uuid",
    52→      "fact": "Always negotiates 20-30% below initial quote",
    53→      "source": "auto|manual",
    54→      "session_id": "uuid (which session discovered this)",
    55→      "created_at": "datetime"
    56→    }
    57→  ],
    58→  "updated_at": "datetime"
    59→}
    60→```
    61→
    62→## Agent Presets
    63→
    64→Stored as constants in `app.js` and mirrored in `api.php`:
    65→
    66→| Agent | System Prompt Focus | Preferred Model | Auto-Memory |
    67→|-------|-------------------|-----------------|-------------|
    68→| **General Assistant** | Open-ended business intelligence | User's choice | No |
    69→| **Quote Analyst** | Pricing patterns, quote history, scope analysis, negotiation leverage | Claude Sonnet | Yes: extracts pricing facts |
    70→| **Deception Detector** | Inconsistencies, contradictions, tone shifts, pressure tactics, red flags | Claude Opus (best reasoning) | Yes: flags contradictions |
    71→| **Contact Profiler** | Communication style, psychological patterns, loyalty, relationship health | Claude Sonnet | Yes: saves profile traits |
    72→| **Email Composer** | Professional replies, negotiation-aware, tone-matching | GPT-4o | No |
    73→
    74→Each agent = `{id, name, icon, systemPrompt, preferredProvider, preferredModel, autoMemory: bool, quickPrompts: []}`
    75→
    76→## Implementation Steps
    77→
    78→### Step 1: Make `stripQuotedContent()` reusable — ThreadAnalyzer.php
    79→
    80→Change line 1616 from `private function` to `public static function`. The method is pure/stateless (no `$this` references), so this is safe.
    81→
    82→### Step 2: Backend — api.php
    83→
    84→**2a. Modify `ai_chat` action (lines 2310-2491):**
    85→
    86→Parse new fields:
    87→- `thread_ids` (array) alongside existing `thread_id`
    88→- `provider` (explicit provider code)
    89→- `session_id` (optional – continue existing session)
    90→- `agent` (agent preset ID)
    91→
    92→**2b. Strip quoted content from email bodies:**
    93→- Before `substr($body, 0, 800)` on line 2347, call `ThreadAnalyzer::stripQuotedContent($body)`
    94→- This removes `> quoted` text, "On X wrote:" patterns, "Original Message" dividers
    95→- Reduces a 5-email thread's context by 60-80% (no more duplicate content per reply)
    96→
    97→**2c. Multi-thread context loop:**
    98→- If `thread_ids` array provided, loop and fetch emails per thread
    99→- Label "THREAD 1 OF N", "THREAD 2 OF N"
   100→- Include per-thread analysis summaries if available
   101→- Single `thread_id` keeps existing behavior
   102→
   103→**2d. Token budget management:**
   104→- Model context limits: `{'gpt-4o': 128000, 'claude-sonnet-4-5': 200000, 'grok-4': 131000, ...}`
   105→- Estimate tokens: `strlen($contextBlock) / 3.5`
   106→- Reserve 30% for response + history
   107→- If over budget: reduce per-email body proportionally, then omit oldest emails with note
   108→- Return `context_stats` in response: `{threads, emails, estimated_tokens, truncated}`
   109→
   110→**2e. Inject contact memory into context:**
   111→- If `contact_id` provided, fetch from `ai_contact_memory` collection
   112→- Append as `=== KNOWN FACTS ABOUT [CONTACT] ===` section in system prompt
   113→- This means AI already "knows" key facts without re-reading all emails
   114→
   115→**2f. Agent system prompt:**
   116→- Map `agent` ID to full system prompt (defined in PHP array)
   117→- Override mode-based prompts when agent is specified
   118→
   119→**2g. Auto-extract memories from AI response:**
   120→- If agent has `autoMemory: true`, append instruction to system prompt:
   121→  `"If you discover important new facts about this contact (pricing patterns, contradictions, preferences), include them in a [MEMORY] tag at the end of your response."`
   122→- Parse AI response for `[MEMORY]...[/MEMORY]` blocks
   123→- Save extracted facts to `ai_contact_memory` automatically
   124→- Strip the `[MEMORY]` tags from the response shown to user
   125→
   126→**2h. Fix provider/model proxy format:**
   127→- Change `'models' => [$chatModel]` to `'models' => [$chatProvider => $chatModel]` (line 2431)
   128→- Use explicit `provider` field when available
   129→
   130→**2i. New API actions:**
   131→- `ai_chat_sessions` — GET — list recent sessions (limit 20)
   132→- `ai_chat_session` — GET — load specific session by `session_id`
   133→- `ai_chat_session_delete` — POST — delete a session
   134→- `ai_contact_memories` — GET — get memories for a contact
   135→- `ai_contact_memory_add` — POST — manually add a memory fact
   136→- `ai_contact_memory_delete` — POST — delete a memory fact
   137→
   138→**2j. Auto-save session:**
   139→- After successful AI response, upsert to `ai_chat_sessions`
   140→- Create new `session_id` if not provided, return it in response
   141→- Title auto-generated from first user message (first 60 chars)
   142→
   143→### Step 3: Frontend JS — app.js
   144→
   145→**3a. Extend `aiChat` state (line 2672):**
   146→```
   147→contextThreads: [], contextContact: null,
   148→sessionId: null, agent: 'general',
   149→contactMemories: []
   150→```
   151→
   152→**3b. Add constants (after line 2684):**
   153→- `PROVIDER_MODELS` / `PROVIDER_NAMES` — synced with Chrome extension (`contentScript.js` lines 18-67): 8 providers (Claude, OpenAI, Gemini, Grok, DeepSeek, Qwen, Mistral, Cohere)
   154→- `AI_AGENTS` — agent presets with `{id, name, icon, preferredProvider, preferredModel, quickPrompts[]}`
   155→
   156→**3c. Provider/model dropdown functions:**
   157→- `initAiProviderDropdown()` — populates provider select
   158→- `updateAiModelDropdown()` — populates model based on provider
   159→
   160→**3d. Agent selector:**
   161→- `selectAgent(agentId)` — sets agent, updates provider/model to agent's preferred defaults, updates quick prompts, updates context label
   162→- Agent buttons replace or augment the mode tabs
   163→
   164→**3e. Context builder functions:**
   165→- `toggleContextBuilder()` — show/hide search area
   166→- `searchContextContacts()` — search contacts API, render results
   167→- `selectContextContact(id, name, email)` — load threads, show picker with "+Add" and "Add All"
   168→- `addThreadToContext()` / `removeThreadFromContext()` / `addAllContactThreads()` / `clearContextCart()`
   169→- `updateContextCart()` — render selected threads summary
   170→
   171→**3f. Modify `sendAiChat()` (line 2756):**
   172→- Send `thread_ids`, `provider`, `session_id`, `agent` in payload
   173→- On response: store `session_id`, display `context_stats`, parse any auto-extracted memories
   174→- If `context_stats.truncated`, show subtle note: "Context was trimmed to fit model limits"
   175→
   176→**3g. Session management:**
   177→- `loadChatSessions()` — fetch + render recent sessions list
   178→- `loadChatSession(id)` — restore messages, context, agent, model
   179→- `startNewSession()` — clear chat, reset session_id
   180→- `deleteChatSession(id)` — delete from server
   181→
   182→**3h. Contact memory UI:**
   183→- When a contact is selected in context builder, load their memories
   184→- Display as small badges/pills below the context cart: "Always negotiates 20-30% below" ×
   185→- Manual "Add Memory" button for user-defined facts
   186→- Delete button on each memory
   187→
   188→**3i. Quick prompts:**
   189→- Each agent has its own quick prompts (replaces the mode-based `AI_QUICK_PROMPTS`)
   190→- When `contextThreads.length > 0` and agent is selected, show agent-specific prompts
   191→
   192→### Step 4: HTML — index.php
   193→
   194→**4a. Replace static model dropdown (lines 538-561):**
   195→- Row 1: Provider dropdown + Model dropdown (populated by JS)
   196→- Row 2: Agent selector buttons (icons: 💬 General, 💰 Quote, 🔍 Deception, 👤 Profiler, ✍ Composer)
   197→
   198→**4b. Chat header additions (line 531 area):**
   199→- "New Chat" button (resets session)
   200→- "History" button (toggles session list overlay)
   201→
   202→**4c. Insert context builder section (between line 570 and 574):**
   203→- Collapsible context summary bar → expands to show search + thread picker + cart + contact memories
   204→
   205→**4d. Session list overlay:**
   206→- Shown when "History" clicked
   207→- Lists recent sessions with title, agent icon, date, message count
   208→- Click to load, swipe/button to delete
   209→
   210→## Execution Order
   211→
   212→Since this is a large feature, implement in this order to have something testable at each step:
   213→
   214→1. **`stripQuotedContent()` → public static** (1 line change, instant win)
   215→2. **Backend multi-thread + token budget + stripped content** (api.php modifications to `ai_chat`)
   216→3. **Frontend context builder + provider/model selector** (search, add threads, send multi-thread)
   217→4. **Agent presets** (frontend selector + backend system prompts)
   218→5. **Chat session persistence** (backend CRUD + frontend save/load)
   219→6. **Contact memory** (backend extraction/storage + frontend display)
   220→
   221→## Verification
   222→
   223→1. Open web app → AI chat → verify provider/model dropdowns work
   224→2. Select "Quote Analyst" agent → verify model switches to Claude Sonnet, prompts change
   225→3. Search "stephanie" → click contact → see threads → "Add All" → verify green context label
   226→4. Ask "how much have we quoted?" → verify AI sees all thread content without duplicate quoted text
   227→5. Check response includes `context_stats` → verify token count is reasonable
   228→6. Ask follow-up "what should we quote for next project?" → verify conversation continues
   229→7. Refresh page → click History → verify session appears → click to reload → messages restored
   230→8. Select "Deception Detector" agent → add threads for david → ask about deception level
   231→9. Check `ai_contact_memory` collection → verify auto-extracted facts are saved
   232→10. Load a different session about same contact → verify memories appear as context
   233→11. Test with 30+ threads for one contact → verify token budget truncation works gracefully
   234→12. Test backward compat: open single thread → "Ask AI About This Thread" → still works
   235→

# Plan: AI Context Builder with Agents & Memory

## Context

The web app's AI chat panel currently only supports asking AI about **one thread** or **one contact** at a time, with no persistence and no deduplication. We're building a full-featured AI chat system with:

- **Multi-thread context**: Search contacts, select multiple threads as AI context
- **Named AI agents**: Specialized presets (Quote Analyst, Deception Detector, Profiler, Composer)
- **Contact memory**: Persistent facts about contacts that accumulate over time
- **Chat session persistence**: Save/reload/continue conversations
- **Smart token management**: Strip quoted email content, enforce context budgets

## Files to Modify

| File | Changes |
|------|---------|
| [public/api.php](public/api.php) | Multi-thread context, quoted content stripping, token budget, session CRUD, agent presets, contact memory endpoints |
| [public/assets/app.js](public/assets/app.js) | Context builder, provider/model selector, agent selector, session management, memory UI |
| [public/index.php](public/index.php) | Provider+model dropdowns, context builder HTML, agent selector, session list, memory panel |
| [src/ThreadAnalyzer.php](src/ThreadAnalyzer.php) | Make `stripQuotedContent()` public static (line 1616) |

## Data Structures (MongoDB collections)

### `ai_chat_sessions`
```json
{
  "session_id": "uuid",
  "created_at": "datetime",
  "updated_at": "datetime",
  "title": "auto from first question",
  "agent": "general|quote_analyst|deception_detector|contact_profiler|email_composer",
  "provider": "claude",
  "model": "claude-sonnet-4-5-20250929",
  "context_thread_ids": ["tid1", "tid2"],
  "contact_id": 123,
  "contact_name": "Stephanie K",
  "messages": [
    {"role": "user", "content": "...", "timestamp": "datetime"},
    {"role": "assistant", "content": "...", "timestamp": "datetime", "model": "claude-sonnet-4-5"}
  ]
}
```

### `ai_contact_memory`
```json
{
  "contact_id": 123,
  "contact_email": "stephanie@asendia.com",
  "memories": [
    {
      "id": "uuid",
      "fact": "Always negotiates 20-30% below initial quote",
      "source": "auto|manual",
      "session_id": "uuid (which session discovered this)",
      "created_at": "datetime"
    }
  ],
  "updated_at": "datetime"
}
```

## Agent Presets

Stored as constants in `app.js` and mirrored in `api.php`:

| Agent | System Prompt Focus | Preferred Model | Auto-Memory |
|-------|-------------------|-----------------|-------------|
| **General Assistant** | Open-ended business intelligence | User's choice | No |
| **Quote Analyst** | Pricing patterns, quote history, scope analysis, negotiation leverage | Claude Sonnet | Yes: extracts pricing facts |
| **Deception Detector** | Inconsistencies, contradictions, tone shifts, pressure tactics, red flags | Claude Opus (best reasoning) | Yes: flags contradictions |
| **Contact Profiler** | Communication style, psychological patterns, loyalty, relationship health | Claude Sonnet | Yes: saves profile traits |
| **Email Composer** | Professional replies, negotiation-aware, tone-matching | GPT-4o | No |

Each agent = `{id, name, icon, systemPrompt, preferredProvider, preferredModel, autoMemory: bool, quickPrompts: []}`

## Implementation Steps

### Step 1: Make `stripQuotedContent()` reusable — ThreadAnalyzer.php

Change line 1616 from `private function` to `public static function`. The method is pure/stateless (no `$this` references), so this is safe.

### Step 2: Backend — api.php

**2a. Modify `ai_chat` action (lines 2310-2491):**

Parse new fields:
- `thread_ids` (array) alongside existing `thread_id`
- `provider` (explicit provider code)
- `session_id` (optional – continue existing session)
- `agent` (agent preset ID)

**2b. Strip quoted content from email bodies:**
- Before `substr($body, 0, 800)` on line 2347, call `ThreadAnalyzer::stripQuotedContent($body)`
- This removes `> quoted` text, "On X wrote:" patterns, "Original Message" dividers
- Reduces a 5-email thread's context by 60-80% (no more duplicate content per reply)

**2c. Multi-thread context loop:**
- If `thread_ids` array provided, loop and fetch emails per thread
- Label "THREAD 1 OF N", "THREAD 2 OF N"
- Include per-thread analysis summaries if available
- Single `thread_id` keeps existing behavior

**2d. Token budget management:**
- Model context limits: `{'gpt-4o': 128000, 'claude-sonnet-4-5': 200000, 'grok-4': 131000, ...}`
- Estimate tokens: `strlen($contextBlock) / 3.5`
- Reserve 30% for response + history
- If over budget: reduce per-email body proportionally, then omit oldest emails with note
- Return `context_stats` in response: `{threads, emails, estimated_tokens, truncated}`

**2e. Inject contact memory into context:**
- If `contact_id` provided, fetch from `ai_contact_memory` collection
- Append as `=== KNOWN FACTS ABOUT [CONTACT] ===` section in system prompt
- This means AI already "knows" key facts without re-reading all emails

**2f. Agent system prompt:**
- Map `agent` ID to full system prompt (defined in PHP array)
- Override mode-based prompts when agent is specified

**2g. Auto-extract memories from AI response:**
- If agent has `autoMemory: true`, append instruction to system prompt:
  `"If you discover important new facts about this contact (pricing patterns, contradictions, preferences), include them in a [MEMORY] tag at the end of your response."`
- Parse AI response for `[MEMORY]...[/MEMORY]` blocks
- Save extracted facts to `ai_contact_memory` automatically
- Strip the `[MEMORY]` tags from the response shown to user

**2h. Fix provider/model proxy format:**
- Change `'models' => [$chatModel]` to `'models' => [$chatProvider => $chatModel]` (line 2431)
- Use explicit `provider` field when available

**2i. New API actions:**
- `ai_chat_sessions` — GET — list recent sessions (limit 20)
- `ai_chat_session` — GET — load specific session by `session_id`
- `ai_chat_session_delete` — POST — delete a session
- `ai_contact_memories` — GET — get memories for a contact
- `ai_contact_memory_add` — POST — manually add a memory fact
- `ai_contact_memory_delete` — POST — delete a memory fact

**2j. Auto-save session:**
- After successful AI response, upsert to `ai_chat_sessions`
- Create new `session_id` if not provided, return it in response
- Title auto-generated from first user message (first 60 chars)

### Step 3: Frontend JS — app.js

**3a. Extend `aiChat` state (line 2672):**
```
contextThreads: [], contextContact: null,
sessionId: null, agent: 'general',
contactMemories: []
```

**3b. Add constants (after line 2684):**
- `PROVIDER_MODELS` / `PROVIDER_NAMES` — synced with Chrome extension (`contentScript.js` lines 18-67): 8 providers (Claude, OpenAI, Gemini, Grok, DeepSeek, Qwen, Mistral, Cohere)
- `AI_AGENTS` — agent presets with `{id, name, icon, preferredProvider, preferredModel, quickPrompts[]}`

**3c. Provider/model dropdown functions:**
- `initAiProviderDropdown()` — populates provider select
- `updateAiModelDropdown()` — populates model based on provider

**3d. Agent selector:**
- `selectAgent(agentId)` — sets agent, updates provider/model to agent's preferred defaults, updates quick prompts, updates context label
- Agent buttons replace or augment the mode tabs

**3e. Context builder functions:**
- `toggleContextBuilder()` — show/hide search area
- `searchContextContacts()` — search contacts API, render results
- `selectContextContact(id, name, email)` — load threads, show picker with "+Add" and "Add All"
- `addThreadToContext()` / `removeThreadFromContext()` / `addAllContactThreads()` / `clearContextCart()`
- `updateContextCart()` — render selected threads summary

**3f. Modify `sendAiChat()` (line 2756):**
- Send `thread_ids`, `provider`, `session_id`, `agent` in payload
- On response: store `session_id`, display `context_stats`, parse any auto-extracted memories
- If `context_stats.truncated`, show subtle note: "Context was trimmed to fit model limits"

**3g. Session management:**
- `loadChatSessions()` — fetch + render recent sessions list
- `loadChatSession(id)` — restore messages, context, agent, model
- `startNewSession()` — clear chat, reset session_id
- `deleteChatSession(id)` — delete from server

**3h. Contact memory UI:**
- When a contact is selected in context builder, load their memories
- Display as small badges/pills below the context cart: "Always negotiates 20-30% below" ×
- Manual "Add Memory" button for user-defined facts
- Delete button on each memory

**3i. Quick prompts:**
- Each agent has its own quick prompts (replaces the mode-based `AI_QUICK_PROMPTS`)
- When `contextThreads.length > 0` and agent is selected, show agent-specific prompts

### Step 4: HTML — index.php

**4a. Replace static model dropdown (lines 538-561):**
- Row 1: Provider dropdown + Model dropdown (populated by JS)
- Row 2: Agent selector buttons (icons: 💬 General, 💰 Quote, 🔍 Deception, 👤 Profiler, ✍ Composer)

**4b. Chat header additions (line 531 area):**
- "New Chat" button (resets session)
- "History" button (toggles session list overlay)

**4c. Insert context builder section (between line 570 and 574):**
- Collapsible context summary bar → expands to show search + thread picker + cart + contact memories

**4d. Session list overlay:**
- Shown when "History" clicked
- Lists recent sessions with title, agent icon, date, message count
- Click to load, swipe/button to delete

## Execution Order

Since this is a large feature, implement in this order to have something testable at each step:

1. **`stripQuotedContent()` → public static** (1 line change, instant win)
2. **Backend multi-thread + token budget + stripped content** (api.php modifications to `ai_chat`)
3. **Frontend context builder + provider/model selector** (search, add threads, send multi-thread)
4. **Agent presets** (frontend selector + backend system prompts)
5. **Chat session persistence** (backend CRUD + frontend save/load)
6. **Contact memory** (backend extraction/storage + frontend display)

## Verification

1. Open web app → AI chat → verify provider/model dropdowns work
2. Select "Quote Analyst" agent → verify model switches to Claude Sonnet, prompts change
3. Search "stephanie" → click contact → see threads → "Add All" → verify green context label
4. Ask "how much have we quoted?" → verify AI sees all thread content without duplicate quoted text
5. Check response includes `context_stats` → verify token count is reasonable
6. Ask follow-up "what should we quote for next project?" → verify conversation continues
7. Refresh page → click History → verify session appears → click to reload → messages restored
8. Select "Deception Detector" agent → add threads for david → ask about deception level
9. Check `ai_contact_memory` collection → verify auto-extracted facts are saved
10. Load a different session about same contact → verify memories appear as context
11. Test with 30+ threads for one contact → verify token budget truncation works gracefully
12. Test backward compat: open single thread → "Ask AI About This Thread" → still works


     1→<?php
     2→/**
     3→ * Thread View Page
     4→ *
     5→ * Dedicated page for viewing email threads and sending to AI API.
     6→ * Features:
     7→ * - View thread emails with actual sender names
     8→ * - Select additional threads from the same contact
     9→ * - Send to AI API with provider/model selection
    10→ * - Add custom questions and context
    11→ * - Save and load AI responses
    12→ *
    13→ * @package EmailIntelligence
    14→ */
    15→
    16→require_once dirname(__DIR__) . '/config/config.php';
    17→require_once dirname(__DIR__) . '/vendor/autoload.php';
    18→
    19→use EmailIntelligence\MongoDatabase;
    20→use EmailIntelligence\QueryAdapter;
    21→
    22→session_start();
    23→
    24→// Get thread ID from URL
    25→$threadId = $_GET['thread_id'] ?? '';
    26→if (empty($threadId)) {
    27→    header('Location: index.php');
    28→    exit;
    29→}
    30→
    31→// Get data via QueryAdapter (which uses MongoDB)
    32→$adapter = QueryAdapter::getInstance();
    33→$mongo = MongoDatabase::getInstance();
    34→
    35→// Get thread details
    36→$threadData = $adapter->getThreadDetails($threadId);
    37→$emails = $threadData['emails'] ?? [];
    38→$analysis = $threadData['analysis'] ?? null;
    39→
    40→if (empty($emails)) {
    41→    header('Location: index.php?error=thread_not_found');
    42→    exit;
    43→}
    44→
    45→// Get the primary contact from this thread
    46→// Priority: 1) Most frequent external contact, 2) First inbound email sender, 3) Any contact
    47→$primaryContactId = null;
    48→$primaryContactEmail = null;
    49→$primaryContactName = null;
    50→$userEmail = defined('GMAIL_USER_EMAIL') ? strtolower(GMAIL_USER_EMAIL) : '';
    51→
    52→// Count contacts to find the most frequent one (excluding user)
    53→$contactCounts = [];
    54→$contactInfo = [];
    55→
    56→foreach ($emails as $email) {
    57→    if (!empty($email['contact_id'])) {
    58→        $contactId = $email['contact_id'];
    59→        $contactEmail = strtolower($email['contact_email'] ?? $email['from_email'] ?? '');
    60→
    61→        // Skip if this is the user's own email
    62→        if ($contactEmail === $userEmail) {
    63→            continue;
    64→        }
    65→
    66→        if (!isset($contactCounts[$contactId])) {
    67→            $contactCounts[$contactId] = 0;
    68→            $contactInfo[$contactId] = [
    69→                'email' => $email['contact_email'] ?? $email['from_email'],
    70→                'name' => $email['contact_name'] ?? ''
    71→            ];
    72→        }
    73→        $contactCounts[$contactId]++;
    74→    }
    75→}
    76→
    77→// Select the most frequent contact
    78→if (!empty($contactCounts)) {
    79→    arsort($contactCounts);
    80→    $primaryContactId = array_key_first($contactCounts);
    81→    $primaryContactEmail = $contactInfo[$primaryContactId]['email'];
    82→    $primaryContactName = $contactInfo[$primaryContactId]['name'];
    83→} else {
    84→    // Fallback: try to find any inbound email sender
    85→    foreach ($emails as $email) {
    86→        if (($email['direction'] ?? '') === 'inbound' && !empty($email['from_email'])) {
    87→            $senderEmail = strtolower($email['from_email']);
    88→            if ($senderEmail !== $userEmail) {
    89→                $primaryContactId = $email['contact_id'] ?? null;
    90→                $primaryContactEmail = $email['from_email'];
    91→                $primaryContactName = $email['from_name'] ?? $email['contact_name'] ?? '';
    92→                break;
    93→            }
    94→        }
    95→    }
    96→}
    97→
    98→// Get other threads from the same contact for selection
    99→$otherThreads = [];
   100→if ($primaryContactId) {

<?php
/**
 * Thread View Page
 *
 * Dedicated page for viewing email threads and sending to AI API.
 * Features:
 * - View thread emails with actual sender names
 * - Select additional threads from the same contact
 * - Send to AI API with provider/model selection
 * - Add custom questions and context
 * - Save and load AI responses
 *
 * @package EmailIntelligence
 */

require_once dirname(__DIR__) . '/config/config.php';
require_once dirname(__DIR__) . '/vendor/autoload.php';

use EmailIntelligence\MongoDatabase;
use EmailIntelligence\QueryAdapter;

session_start();

// Get thread ID from URL
$threadId = $_GET['thread_id'] ?? '';
if (empty($threadId)) {
    header('Location: index.php');
    exit;
}

// Get data via QueryAdapter (which uses MongoDB)
$adapter = QueryAdapter::getInstance();
$mongo = MongoDatabase::getInstance();

// Get thread details
$threadData = $adapter->getThreadDetails($threadId);
$emails = $threadData['emails'] ?? [];
$analysis = $threadData['analysis'] ?? null;

if (empty($emails)) {
    header('Location: index.php?error=thread_not_found');
    exit;
}

// Get the primary contact from this thread
// Priority: 1) Most frequent external contact, 2) First inbound email sender, 3) Any contact
$primaryContactId = null;
$primaryContactEmail = null;
$primaryContactName = null;
$userEmail = defined('GMAIL_USER_EMAIL') ? strtolower(GMAIL_USER_EMAIL) : '';

// Count contacts to find the most frequent one (excluding user)
$contactCounts = [];
$contactInfo = [];

foreach ($emails as $email) {
    if (!empty($email['contact_id'])) {
        $contactId = $email['contact_id'];
        $contactEmail = strtolower($email['contact_email'] ?? $email['from_email'] ?? '');

        // Skip if this is the user's own email
        if ($contactEmail === $userEmail) {
            continue;
        }

        if (!isset($contactCounts[$contactId])) {
            $contactCounts[$contactId] = 0;
            $contactInfo[$contactId] = [
                'email' => $email['contact_email'] ?? $email['from_email'],
                'name' => $email['contact_name'] ?? ''
            ];
        }
        $contactCounts[$contactId]++;
    }
}

// Select the most frequent contact
if (!empty($contactCounts)) {
    arsort($contactCounts);
    $primaryContactId = array_key_first($contactCounts);
    $primaryContactEmail = $contactInfo[$primaryContactId]['email'];
    $primaryContactName = $contactInfo[$primaryContactId]['name'];
} else {
    // Fallback: try to find any inbound email sender
    foreach ($emails as $email) {
        if (($email['direction'] ?? '') === 'inbound' && !empty($email['from_email'])) {
            $senderEmail = strtolower($email['from_email']);
            if ($senderEmail !== $userEmail) {
                $primaryContactId = $email['contact_id'] ?? null;
                $primaryContactEmail = $email['from_email'];
                $primaryContactName = $email['from_name'] ?? $email['contact_name'] ?? '';
                break;
            }
        }
    }
}

// Get other threads from the same contact for selection
$otherThreads = [];
if ($primaryContactId) {


     1→<?php
     2→/**
     3→ * Email Intelligence Dashboard
     4→ *
     5→ * Main web interface for viewing contacts, emails, and analysis results.
     6→ *
     7→ * CALLING MAP:
     8→ * - Entry point: Web browser
     9→ * - Uses: api.php for data (via AJAX)
    10→ * - Uses: oauth_callback.php for Gmail connection
    11→ *
    12→ * @package EmailIntelligence
    13→ */
    14→
    15→// Load configuration
    16→require_once dirname(__DIR__) . '/config/config.php';
    17→require_once dirname(__DIR__) . '/vendor/autoload.php';
    18→
    19→use EmailIntelligence\Database;
    20→use EmailIntelligence\GmailClient;
    21→
    22→// Start session for flash messages
    23→session_start();
    24→
    25→// If contact parameter is present, redirect to contact view
    26→if (isset($_GET['contact']) && is_numeric($_GET['contact'])) {
    27→    header('Location: contact_view.php?contact=' . (int)$_GET['contact']);
    28→    exit;
    29→}
    30→
    31→// Get flash message if any
    32→$flashMessage = $_SESSION['flash_message'] ?? null;
    33→unset($_SESSION['flash_message']);
    34→
    35→// Get initial status
    36→try {
    37→    $db = Database::getInstance();
    38→    $syncStatus = $db->fetchOne("SELECT * FROM sync_status WHERE id = 1");
    39→    $stats = [
    40→        'emails' => $db->fetchColumn("SELECT COUNT(*) FROM emails"),
    41→        'contacts' => $db->fetchColumn("SELECT COUNT(*) FROM contacts"),
    42→        'analyzed' => $db->fetchColumn("SELECT COUNT(*) FROM emails WHERE is_analyzed = 1"),
    43→        'pending' => $db->fetchColumn("SELECT COUNT(*) FROM emails WHERE is_analyzed = 0"),
    44→    ];
    45→
    46→    $gmail = new GmailClient();
    47→    $gmailStatus = $gmail->getAuthStatus();
    48→    $authUrl = $gmail->hasCredentials() ? $gmail->getAuthUrl() : null;
    49→
    50→} catch (Exception $e) {

<?php
/**
 * Email Intelligence Dashboard
 *
 * Main web interface for viewing contacts, emails, and analysis results.
 *
 * CALLING MAP:
 * - Entry point: Web browser
 * - Uses: api.php for data (via AJAX)
 * - Uses: oauth_callback.php for Gmail connection
 *
 * @package EmailIntelligence
 */

// Load configuration
require_once dirname(__DIR__) . '/config/config.php';
require_once dirname(__DIR__) . '/vendor/autoload.php';

use EmailIntelligence\Database;
use EmailIntelligence\GmailClient;

// Start session for flash messages
session_start();

// If contact parameter is present, redirect to contact view
if (isset($_GET['contact']) && is_numeric($_GET['contact'])) {
    header('Location: contact_view.php?contact=' . (int)$_GET['contact']);
    exit;
}

// Get flash message if any
$flashMessage = $_SESSION['flash_message'] ?? null;
unset($_SESSION['flash_message']);

// Get initial status
try {
    $db = Database::getInstance();
    $syncStatus = $db->fetchOne("SELECT * FROM sync_status WHERE id = 1");
    $stats = [
        'emails' => $db->fetchColumn("SELECT COUNT(*) FROM emails"),
        'contacts' => $db->fetchColumn("SELECT COUNT(*) FROM contacts"),
        'analyzed' => $db->fetchColumn("SELECT COUNT(*) FROM emails WHERE is_analyzed = 1"),
        'pending' => $db->fetchColumn("SELECT COUNT(*) FROM emails WHERE is_analyzed = 0"),
    ];

    $gmail = new GmailClient();
    $gmailStatus = $gmail->getAuthStatus();
    $authUrl = $gmail->hasCredentials() ? $gmail->getAuthUrl() : null;

} catch (Exception $e) {


Now let me read more of the thread_view.php to understand its full structure, and also check the key sections of index.php and app.js that need changes.


Let me read more of the key files to finalize the plan.


   100→if ($primaryContactId) {
   101→    $contactThreads = $adapter->getThreads($primaryContactId, '', 50, 0);
   102→    foreach ($contactThreads['threads'] as $t) {
   103→        if ($t['thread_id'] !== $threadId) {
   104→            $otherThreads[] = $t;
   105→        }
   106→    }
   107→}
   108→
   109→// Get saved AI responses for this thread from MongoDB
   110→$savedResponsesLimit = 5; // Initial load limit
   111→$savedResponses = $mongo->findAll('ai_query_responses',
   112→    ['thread_ids' => $threadId],
   113→    ['sort' => ['created_at' => -1], 'limit' => $savedResponsesLimit]
   114→);
   115→
   116→// Get total count of saved responses for this thread
   117→$totalSavedResponses = $mongo->count('ai_query_responses', ['thread_ids' => $threadId]);
   118→
   119→// Get first email's subject for page title
   120→$threadSubject = $emails[0]['subject'] ?? '(No Subject)';
   121→
   122→// Build participant map for displaying real names
   123→$participantMap = [];
   124→// $userEmail already defined above
   125→
   126→foreach ($emails as $email) {
   127→    $fromEmail = strtolower($email['from_email'] ?? '');
   128→    if ($fromEmail && !isset($participantMap[$fromEmail])) {
   129→        $name = '';
   130→        if ($fromEmail === $userEmail) {
   131→            $name = 'You';
   132→        } elseif (!empty($email['from_name'])) {
   133→            $name = $email['from_name'];
   134→        } elseif (!empty($email['contact_name'])) {
   135→            $name = $email['contact_name'];
   136→        } else {
   137→            // Extract name from email
   138→            $parts = explode('@', $fromEmail);
   139→            $nameParts = explode('.', $parts[0]);
   140→            $name = ucfirst($nameParts[0]);
   141→        }
   142→        $participantMap[$fromEmail] = $name;
   143→    }
   144→}
   145→?>
   146→<!DOCTYPE html>
   147→<html lang="en">
   148→<head>
   149→    <meta charset="UTF-8">
   150→    <meta name="viewport" content="width=device-width, initial-scale=1.0">
   151→    <title>Thread: <?= htmlspecialchars($threadSubject) ?> - Email Intelligence</title>
   152→    <link rel="stylesheet" href="assets/style.css">
   153→    <style>
   154→        :root {
   155→            --bg-primary: #f5f7fa;
   156→            --bg-secondary: #ffffff;
   157→            --accent-primary: #667eea;
   158→            --accent-secondary: #764ba2;
   159→            --text-primary: #333;
   160→            --text-secondary: #666;
   161→            --border-color: #e0e0e0;
   162→            --success: #10b981;
   163→            --warning: #f59e0b;
   164→            --error: #ef4444;
   165→        }
   166→
   167→        body {
   168→            background: var(--bg-primary);
   169→            margin: 0;
   170→            padding: 0;
   171→        }
   172→
   173→        .page-container {
   174→            max-width: 1600px;
   175→            margin: 0 auto;
   176→            padding: 20px;
   177→        }
   178→
   179→        .page-header {
   180→            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
   181→            color: white;
   182→            padding: 20px 30px;
   183→            border-radius: 12px;
   184→            margin-bottom: 20px;
   185→            display: flex;
   186→            justify-content: space-between;
   187→            align-items: center;
   188→        }
   189→
   190→        .page-header h1 {
   191→            margin: 0 0 5px 0;
   192→            font-size: 1.5em;
   193→        }
   194→
   195→        .page-header .subtitle {
   196→            opacity: 0.9;
   197→            font-size: 0.9em;
   198→        }
   199→
   200→        .back-link {
   201→            color: white;
   202→            text-decoration: none;
   203→            padding: 10px 20px;
   204→            background: rgba(255,255,255,0.2);
   205→            border-radius: 8px;
   206→            transition: background 0.2s;
   207→        }
   208→
   209→        .back-link:hover {
   210→            background: rgba(255,255,255,0.3);
   211→        }
   212→
   213→        .main-grid {
   214→            display: grid;
   215→            grid-template-columns: 1fr 400px;
   216→            gap: 20px;
   217→        }
   218→
   219→        @media (max-width: 1200px) {
   220→            .main-grid {
   221→                grid-template-columns: 1fr;
   222→            }
   223→        }
   224→
   225→        .card {
   226→            background: var(--bg-secondary);
   227→            border-radius: 12px;
   228→            padding: 20px;
   229→            box-shadow: 0 2px 10px rgba(0,0,0,0.08);
   230→            margin-bottom: 20px;
   231→        }
   232→
   233→        .card-header {
   234→            display: flex;
   235→            justify-content: space-between;
   236→            align-items: center;
   237→            margin-bottom: 15px;
   238→            padding-bottom: 10px;
   239→            border-bottom: 1px solid var(--border-color);
   240→        }
   241→
   242→        .card-header h2 {
   243→            margin: 0;
   244→            font-size: 1.2em;
   245→            color: var(--text-primary);
   246→        }
   247→
   248→        /* Email Timeline */
   249→        .email-timeline {
   250→            max-height: 600px;
   251→            overflow-y: auto;
   252→        }
   253→
   254→        .email-item {
   255→            border-left: 4px solid #3b82f6;
   256→            padding: 15px;
   257→            margin-bottom: 15px;
   258→            background: #f9fafb;
   259→            border-radius: 0 8px 8px 0;
   260→        }
   261→
   262→        .email-item.inbound {
   263→            border-left-color: #10b981;
   264→        }
   265→
   266→        .email-item.outbound {
   267→            border-left-color: #3b82f6;
   268→        }
   269→
   270→        .email-meta {
   271→            display: flex;
   272→            justify-content: space-between;
   273→            align-items: center;
   274→            margin-bottom: 10px;
   275→        }
   276→
   277→        .sender-name {
   278→            font-weight: 600;
   279→            color: var(--text-primary);
   280→        }
   281→
   282→        .sender-email {
   283→            color: var(--text-secondary);
   284→            font-size: 0.85em;
   285→        }
   286→
   287→        .email-date {
   288→            color: var(--text-secondary);
   289→            font-size: 0.85em;
   290→        }
   291→
   292→        .email-body {
   293→            font-size: 0.95em;
   294→            line-height: 1.6;
   295→            color: var(--text-primary);
   296→            white-space: pre-wrap;
   297→            word-wrap: break-word;
   298→            max-height: 200px;
   299→            overflow-y: auto;

if ($primaryContactId) {
    $contactThreads = $adapter->getThreads($primaryContactId, '', 50, 0);
    foreach ($contactThreads['threads'] as $t) {
        if ($t['thread_id'] !== $threadId) {
            $otherThreads[] = $t;
        }
    }
}

// Get saved AI responses for this thread from MongoDB
$savedResponsesLimit = 5; // Initial load limit
$savedResponses = $mongo->findAll('ai_query_responses',
    ['thread_ids' => $threadId],
    ['sort' => ['created_at' => -1], 'limit' => $savedResponsesLimit]
);

// Get total count of saved responses for this thread
$totalSavedResponses = $mongo->count('ai_query_responses', ['thread_ids' => $threadId]);

// Get first email's subject for page title
$threadSubject = $emails[0]['subject'] ?? '(No Subject)';

// Build participant map for displaying real names
$participantMap = [];
// $userEmail already defined above

foreach ($emails as $email) {
    $fromEmail = strtolower($email['from_email'] ?? '');
    if ($fromEmail && !isset($participantMap[$fromEmail])) {
        $name = '';
        if ($fromEmail === $userEmail) {
            $name = 'You';
        } elseif (!empty($email['from_name'])) {
            $name = $email['from_name'];
        } elseif (!empty($email['contact_name'])) {
            $name = $email['contact_name'];
        } else {
            // Extract name from email
            $parts = explode('@', $fromEmail);
            $nameParts = explode('.', $parts[0]);
            $name = ucfirst($nameParts[0]);
        }
        $participantMap[$fromEmail] = $name;
    }
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Thread: <?= htmlspecialchars($threadSubject) ?> - Email Intelligence</title>
    <link rel="stylesheet" href="assets/style.css">
    <style>
        :root {
            --bg-primary: #f5f7fa;
            --bg-secondary: #ffffff;
            --accent-primary: #667eea;
            --accent-secondary: #764ba2;
            --text-primary: #333;
            --text-secondary: #666;
            --border-color: #e0e0e0;
            --success: #10b981;
            --warning: #f59e0b;
            --error: #ef4444;
        }

        body {
            background: var(--bg-primary);
            margin: 0;
            padding: 0;
        }

        .page-container {
            max-width: 1600px;
            margin: 0 auto;
            padding: 20px;
        }

        .page-header {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            padding: 20px 30px;
            border-radius: 12px;
            margin-bottom: 20px;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }

        .page-header h1 {
            margin: 0 0 5px 0;
            font-size: 1.5em;
        }

        .page-header .subtitle {
            opacity: 0.9;
            font-size: 0.9em;
        }

        .back-link {
            color: white;
            text-decoration: none;
            padding: 10px 20px;
            background: rgba(255,255,255,0.2);
            border-radius: 8px;
            transition: background 0.2s;
        }

        .back-link:hover {
            background: rgba(255,255,255,0.3);
        }

        .main-grid {
            display: grid;
            grid-template-columns: 1fr 400px;
            gap: 20px;
        }

        @media (max-width: 1200px) {
            .main-grid {
                grid-template-columns: 1fr;
            }
        }

        .card {
            background: var(--bg-secondary);
            border-radius: 12px;
            padding: 20px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.08);
            margin-bottom: 20px;
        }

        .card-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 15px;
            padding-bottom: 10px;
            border-bottom: 1px solid var(--border-color);
        }

        .card-header h2 {
            margin: 0;
            font-size: 1.2em;
            color: var(--text-primary);
        }

        /* Email Timeline */
        .email-timeline {
            max-height: 600px;
            overflow-y: auto;
        }

        .email-item {
            border-left: 4px solid #3b82f6;
            padding: 15px;
            margin-bottom: 15px;
            background: #f9fafb;
            border-radius: 0 8px 8px 0;
        }

        .email-item.inbound {
            border-left-color: #10b981;
        }

        .email-item.outbound {
            border-left-color: #3b82f6;
        }

        .email-meta {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 10px;
        }

        .sender-name {
            font-weight: 600;
            color: var(--text-primary);
        }

        .sender-email {
            color: var(--text-secondary);
            font-size: 0.85em;
        }

        .email-date {
            color: var(--text-secondary);
            font-size: 0.85em;
        }

        .email-body {
            font-size: 0.95em;
            line-height: 1.6;
            color: var(--text-primary);
            white-space: pre-wrap;
            word-wrap: break-word;
            max-height: 200px;
            overflow-y: auto;


120→
<?= number_format($stats['emails']) ?>

   121→

   122→                    <?= number_format($stats['analyzed']) ?> analyzed,
   123→                    <?= number_format($stats['pending']) ?> pending
   124→

   125→

   126→
   127→
   128→

   129→
Contacts

   130→
<?= number_format($stats['contacts']) ?>

   131→
Clients & Suppliers

   132→

   133→

   134→
   135→
   136→

   137→
AI Thread Analysis

   138→

   139→

   140→                    Threads
   141→                    -
   142→

   143→

   144→                    Analyzed
   145→                    -
   146→

   147→

   148→                    Pending
   149→                    -
   150→

   151→

   152→                    Progress
   153→                    -
   154→

   155→

   156→

   157→                <button class="btn btn-primary" onclick="triggerAnalysis()" id="btn-analyze">Analyze Threads</button>
   158→                <button class="btn btn-small" onclick="refreshAnalysisStatus()">Refresh Status</button>
   159→

   160→

   161→

   162→
   163→
   164→

   165→            <button class="tab-btn active" data-tab="contacts">Contacts</button>
   166→            <button class="tab-btn" data-tab="emails">Threads</button>
   167→            <button class="tab-btn" data-tab="products">🛒 Products</button>
   168→            <button class="tab-btn" data-tab="gmail-accounts">📧 Gmail Accounts</button>
   169→            <button class="tab-btn" data-tab="enrichment">🔬 Enrichment</button>

<?= number_format($stats['emails']) ?>

                    <?= number_format($stats['analyzed']) ?> analyzed,
                    <?= number_format($stats['pending']) ?> pending

Contacts

<?= number_format($stats['contacts']) ?>

Clients & Suppliers

AI Thread Analysis

                    Threads
                    -

                    Analyzed
                    -

                    Pending
                    -

                    Progress
                    -

                <button class="btn btn-primary" onclick="triggerAnalysis()" id="btn-analyze">Analyze Threads</button>
                <button class="btn btn-small" onclick="refreshAnalysisStatus()">Refresh Status</button>

            <button class="tab-btn active" data-tab="contacts">Contacts</button>
            <button class="tab-btn" data-tab="emails">Threads</button>
            <button class="tab-btn" data-tab="products">🛒 Products</button>
            <button class="tab-btn" data-tab="gmail-accounts">📧 Gmail Accounts</button>
            <button class="tab-btn" data-tab="enrichment">🔬 Enrichment</button>


155→
   156→function hideLoading() {
   157→    document.getElementById('loading-overlay').style.display = 'none';
   158→}
   159→
   160→// ============================================
   161→// Contacts
   162→// ============================================
   163→async function loadContacts(page = 1) {
   164→    const container = document.getElementById('contacts-list');
   165→    container.innerHTML = '
Loading contacts...
';
   166→
   167→    try {
   168→        state.contacts.page = page;
   169→        state.contacts.search = document.getElementById('contact-search')?.value || '';
   170→        state.contacts.type = document.getElementById('contact-type-filter')?.value || '';
   171→
   172→        const data = await apiCall('contacts', {
   173→            page: page,
   174→            search: state.contacts.search,
   175→            type: state.contacts.type,
   176→            limit: 20
   177→        });
   178→
   179→        if (data.contacts.length === 0) {
   180→            container.innerHTML = '
No contacts found
';
   181→            return;
   182→        }
   183→
   184→        let html = `
   185→

   186→

   187→

   188→                        Contact
   189→                        Type
   190→                        Emails
   191→                        Relationship
   192→                        Risk
   193→                        Last Contact
   194→                        Actions
   195→

   196→

   197→

   198→        `;
   199→
   200→        data.contacts.forEach(contact => {
   201→            const riskClass = contact.risk_level ? 'badge-' + contact.risk_level : 'badge-low';
   202→            const relClass = contact.relationship_health ? 'badge-' + contact.relationship_health.replace('_', '-') : 'badge-new';
   203→            const enrichedBadge = contact.enrichment_status === 'complete' ? '✓' : '';
   204→

function hideLoading() {
    document.getElementById('loading-overlay').style.display = 'none';
}

// ============================================
// Contacts
// ============================================
async function loadContacts(page = 1) {
    const container = document.getElementById('contacts-list');
    container.innerHTML = '
Loading contacts...
';

    try {
        state.contacts.page = page;
        state.contacts.search = document.getElementById('contact-search')?.value || '';
        state.contacts.type = document.getElementById('contact-type-filter')?.value || '';

        const data = await apiCall('contacts', {
            page: page,
            search: state.contacts.search,
            type: state.contacts.type,
            limit: 20
        });

        if (data.contacts.length === 0) {
            container.innerHTML = '
No contacts found
';
            return;
        }

        let html = `

                        Contact
                        Type
                        Emails
                        Relationship
                        Risk
                        Last Contact
                        Actions

        `;

        data.contacts.forEach(contact => {
            const riskClass = contact.risk_level ? 'badge-' + contact.risk_level : 'badge-low';
            const relClass = contact.relationship_health ? 'badge-' + contact.relationship_health.replace('_', '-') : 'badge-new';
            const enrichedBadge = contact.enrichment_status === 'complete' ? '✓' : '';


Let me read more of thread_view.php to understand the full AI form and JS sections.


Let me read the AI form section and JS in thread_view.php.


700→        /* Loading */
   701→        .loading-spinner {
   702→            display: inline-block;
   703→            width: 20px;
   704→            height: 20px;
   705→            border: 3px solid rgba(255,255,255,0.3);
   706→            border-radius: 50%;
   707→            border-top-color: white;
   708→            animation: spin 1s linear infinite;
   709→        }
   710→
   711→        @keyframes spin {
   712→            to { transform: rotate(360deg); }
   713→        }
   714→
   715→        .loading-overlay {
   716→            position: fixed;
   717→            top: 0;
   718→            left: 0;
   719→            right: 0;
   720→            bottom: 0;
   721→            background: rgba(0,0,0,0.5);
   722→            display: flex;
   723→            justify-content: center;
   724→            align-items: center;
   725→            z-index: 1000;
   726→            display: none;
   727→        }
   728→
   729→        .loading-overlay.active {
   730→            display: flex;
   731→        }
   732→
   733→        .loading-box {
   734→            background: white;
   735→            padding: 30px 50px;
   736→            border-radius: 12px;
   737→            text-align: center;
   738→        }
   739→
   740→        .loading-box .spinner {
   741→            width: 40px;
   742→            height: 40px;
   743→            border: 4px solid #e5e7eb;
   744→            border-top-color: var(--accent-primary);
   745→            border-radius: 50%;
   746→            animation: spin 1s linear infinite;
   747→            margin: 0 auto 15px;
   748→        }
   749→
   750→        /* Direction badge */
   751→        .badge-direction {
   752→            display: inline-block;
   753→            padding: 2px 8px;
   754→            border-radius: 4px;
   755→            font-size: 0.75em;
   756→            font-weight: 600;
   757→            text-transform: uppercase;
   758→        }
   759→
   760→        .badge-inbound {
   761→            background: #d1fae5;
   762→            color: #065f46;
   763→        }
   764→
   765→        .badge-outbound {
   766→            background: #dbeafe;
   767→            color: #1e40af;
   768→        }
   769→
   770→        /* Tabs for responses */
   771→        .response-tabs {
   772→            display: flex;
   773→            gap: 10px;
   774→            margin-bottom: 15px;
   775→            border-bottom: 2px solid var(--border-color);
   776→            padding-bottom: 10px;
   777→        }
   778→
   779→        .response-tab {
   780→            padding: 8px 16px;
   781→            background: none;
   782→            border: none;
   783→            cursor: pointer;
   784→            font-size: 0.9em;
   785→            color: var(--text-secondary);
   786→            border-bottom: 2px solid transparent;
   787→            margin-bottom: -12px;
   788→        }
   789→
   790→        .response-tab.active {
   791→            color: var(--accent-primary);
   792→            border-bottom-color: var(--accent-primary);
   793→            font-weight: 600;
   794→        }
   795→
   796→        .response-content {
   797→            display: none;
   798→        }
   799→
   800→        .response-content.active {
   801→            display: block;
   802→        }
   803→    </style>
   804→
   805→
   806→

   807→
   808→

   809→

   810→
<?= htmlspecialchars($threadSubject) ?>

   811→

   812→                    <?= count($emails) ?> emails
   813→                    <?php if ($primaryContactName): ?>
   814→                        with <?= htmlspecialchars($primaryContactName) ?>
   815→                    <?php endif; ?>
   816→                    | Thread ID: <?= htmlspecialchars($threadId) ?>
   817→

   818→

   819→            Back to Dashboard
   820→

   821→
   822→

   823→
   824→

   825→
   826→

   827→

   828→
Email Conversation

   829→                        <?= count($emails) ?> emails
   830→

   831→

   832→                        <?php foreach ($emails as $index => $email):
   833→                            $senderEmail = strtolower($email['from_email'] ?? '');
   834→                            $senderName = $participantMap[$senderEmail] ?? 'Unknown';
   835→                            $isInbound = ($email['direction'] ?? '') === 'inbound';
   836→                            $body = $email['body_text'] ?? $email['body_html'] ?? '(No content)';
   837→                            // Truncate for display
   838→                            $displayBody = strlen($body) > 1500 ? substr($body, 0, 1500) . '...' : $body;
   839→                        ?>
   840→                        <div class="email-item <?= $isInbound ? 'inbound' : 'outbound' ?>">
   841→

   842→

   843→                                    <?= htmlspecialchars($senderName) ?>
   844→                                    <span class="badge-direction <?= $isInbound ? 'badge-inbound' : 'badge-outbound' ?>">
   845→                                        <?= $isInbound ? 'Received' : 'Sent' ?>
   846→
   847→

   848→                                    <?= htmlspecialchars($email['from_email'] ?? '') ?>
   849→

   850→                                <?= date('M j, Y g:i A', strtotime($email['date'])) ?>
   851→

   852→
<?= htmlspecialchars($displayBody) ?>

   853→

   854→                        <?php endforeach; ?>
   855→

   856→

   857→
   858→
   859→

   860→

   861→
AI Response

   862→                        <button class="btn btn-secondary" onclick="closeResponse()">Close</button>
   863→

   864→

   865→

   866→

   867→
   868→
   869→                <?php if (!empty($savedResponses)): ?>
   870→

   871→

   872→
Previous AI Analyses (<?= $totalSavedResponses ?> total)

   873→

   874→

   875→                        <?php foreach ($savedResponses as $saved):
   876→                            // Determine what to display as the "question"
   877→                            $displayQuestion = $saved['question'] ?? 'General Analysis';
   878→                            $hasContext = !empty($saved['context']);
   879→                            $isDefaultQuestion = strpos($displayQuestion, 'Please analyze this email conversation') === 0;
   880→
   881→                            // If question is the default and user provided context, show context instead
   882→                            if ($isDefaultQuestion && $hasContext) {
   883→                                $displayQuestion = '[Context] ' . $saved['context'];
   884→                            } elseif ($hasContext && !$isDefaultQuestion) {
   885→                                // If both custom question and context exist, show question + indicator
   886→                                $displayQuestion = $displayQuestion . ' (+context)';
   887→                            }
   888→
   889→                            // Truncate to 200 chars instead of 100
   890→                            $displayQuestion = strlen($displayQuestion) > 200
   891→                                ? substr($displayQuestion, 0, 200) . '...'
   892→                                : $displayQuestion;
   893→
   894→                            // Get response excerpt for easier identification
   895→                            $responseText = $saved['response']['response'] ?? '';
   896→                            $responseExcerpt = strlen($responseText) > 120
   897→                                ? substr($responseText, 0, 120) . '...'
   898→                                : $responseText;
   899→                        ?>
   900→                        <div class="saved-response" id="saved-response-<?= $saved['_id'] ?>" data-id="<?= $saved['_id'] ?>">
   901→

   902→
   903→                                    <?= htmlspecialchars($displayQuestion) ?>
   904→
   905→
   906→                                    <?= date('M j, Y g:i A', strtotime($saved['created_at'] ?? 'now')) ?>
   907→
   908→

   909→

   910→                                <?= htmlspecialchars($saved['provider'] ?? 'Unknown') ?> /
   911→                                <?= htmlspecialchars($saved['model'] ?? 'Unknown') ?>
   912→

   913→                            <?php if ($responseExcerpt): ?>
   914→

   915→                                <?= htmlspecialchars($responseExcerpt) ?>
   916→

   917→                            <?php endif; ?>
   918→

   919→                                <button class="btn-view" onclick="event.stopPropagation(); quickViewResponse('<?= $saved['_id'] ?>')">
   920→                                    👁 Quick View
   921→                                </button>
   922→                                <button class="btn-expand" onclick="event.stopPropagation(); loadSavedResponse('<?= $saved['_id'] ?>')">
   923→                                    📄 Full View
   924→                                </button>
   925→

   926→

   927→                        <?php endforeach; ?>
   928→

   929→                    <?php if ($totalSavedResponses > $savedResponsesLimit): ?>
   930→

   931→                        <button class="btn btn-secondary" id="load-more-btn" onclick="loadMoreResponses()"
   932→                                data-offset="<?= $savedResponsesLimit ?>"
   933→                                data-total="<?= $totalSavedResponses ?>">
   934→                            Load More (<?= $totalSavedResponses - $savedResponsesLimit ?> remaining)
   935→                        </button>
   936→

   937→                    <?php endif; ?>
   938→

   939→                <?php endif; ?>
   940→

   941→
   942→
   943→

   944→
   945→                <?php if (!empty($otherThreads)): ?>
   946→

   947→

   948→
Include Other Threads

   949→

   950→

   951→                        Select additional threads from <?= htmlspecialchars($primaryContactName ?: 'this contact') ?> to include in the AI analysis:
   952→

   953→

   954→                        <?php foreach (array_slice($otherThreads, 0, 10) as $thread): ?>
   955→
   956→                            <input type="checkbox" name="additional_threads[]" value="<?= htmlspecialchars($thread['thread_id']) ?>">
   957→

   958→
<?= htmlspecialchars($thread['subject'] ?? '(No Subject)') ?>

   959→

   960→                                    <?= $thread['email_count'] ?? 0 ?> emails |
   961→                                    <?= date('M j, Y', strtotime($thread['last_date'] ?? 'now')) ?>
   962→

   963→

   964→
   965→                        <?php endforeach; ?>
   966→

   967→

   968→                <?php endif; ?>
   969→
   970→
   971→

   972→

   973→
Ask AI

   974→

   975→
   976→
   977→

   978→                        Select AI Provider
   979→

   980→
   981→
Loading providers...

   982→

   983→

   984→
   985→
   986→

   987→                        Email Content Processing
   988→                        <select id="content-mode" style="width: 100%;">
   989→                            <option value="full">Full Content (include all quoted replies)</option>
   990→                            <option value="stripped" selected>Stripped (remove quoted replies - saves tokens)</option>
   991→                            <option value="smart">Smart Deduplication (quotes only in last email)</option>
   992→                        </select>
   993→

   994→                            Stripped: Removes ">" quoted lines and "On [date] wrote:" sections.

   995→                            Smart: Keeps full context only in the last email to avoid redundancy.
   996→

   997→

   998→
   999→

/* Loading */
        .loading-spinner {
            display: inline-block;
            width: 20px;
            height: 20px;
            border: 3px solid rgba(255,255,255,0.3);
            border-radius: 50%;
            border-top-color: white;
            animation: spin 1s linear infinite;
        }

        @keyframes spin {
            to { transform: rotate(360deg); }
        }

        .loading-overlay {
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: rgba(0,0,0,0.5);
            display: flex;
            justify-content: center;
            align-items: center;
            z-index: 1000;
            display: none;
        }

        .loading-overlay.active {
            display: flex;
        }

        .loading-box {
            background: white;
            padding: 30px 50px;
            border-radius: 12px;
            text-align: center;
        }

        .loading-box .spinner {
            width: 40px;
            height: 40px;
            border: 4px solid #e5e7eb;
            border-top-color: var(--accent-primary);
            border-radius: 50%;
            animation: spin 1s linear infinite;
            margin: 0 auto 15px;
        }

        /* Direction badge */
        .badge-direction {
            display: inline-block;
            padding: 2px 8px;
            border-radius: 4px;
            font-size: 0.75em;
            font-weight: 600;
            text-transform: uppercase;
        }

        .badge-inbound {
            background: #d1fae5;
            color: #065f46;
        }

        .badge-outbound {
            background: #dbeafe;
            color: #1e40af;
        }

        /* Tabs for responses */
        .response-tabs {
            display: flex;
            gap: 10px;
            margin-bottom: 15px;
            border-bottom: 2px solid var(--border-color);
            padding-bottom: 10px;
        }

        .response-tab {
            padding: 8px 16px;
            background: none;
            border: none;
            cursor: pointer;
            font-size: 0.9em;
            color: var(--text-secondary);
            border-bottom: 2px solid transparent;
            margin-bottom: -12px;
        }

        .response-tab.active {
            color: var(--accent-primary);
            border-bottom-color: var(--accent-primary);
            font-weight: 600;
        }

        .response-content {
            display: none;
        }

        .response-content.active {
            display: block;
        }
    </style>

<?= htmlspecialchars($threadSubject) ?>

                    <?= count($emails) ?> emails
                    <?php if ($primaryContactName): ?>
                        with <?= htmlspecialchars($primaryContactName) ?>
                    <?php endif; ?>
                    | Thread ID: <?= htmlspecialchars($threadId) ?>

            Back to Dashboard

Email Conversation

                        <?= count($emails) ?> emails

                        <?php foreach ($emails as $index => $email):
                            $senderEmail = strtolower($email['from_email'] ?? '');
                            $senderName = $participantMap[$senderEmail] ?? 'Unknown';
                            $isInbound = ($email['direction'] ?? '') === 'inbound';
                            $body = $email['body_text'] ?? $email['body_html'] ?? '(No content)';
                            // Truncate for display
                            $displayBody = strlen($body) > 1500 ? substr($body, 0, 1500) . '...' : $body;
                        ?>
                        <div class="email-item <?= $isInbound ? 'inbound' : 'outbound' ?>">

                                    <?= htmlspecialchars($senderName) ?>
                                    <span class="badge-direction <?= $isInbound ? 'badge-inbound' : 'badge-outbound' ?>">
                                        <?= $isInbound ? 'Received' : 'Sent' ?>

                                    <?= htmlspecialchars($email['from_email'] ?? '') ?>

                                <?= date('M j, Y g:i A', strtotime($email['date'])) ?>

<?= htmlspecialchars($displayBody) ?>

                        <?php endforeach; ?>

AI Response

                        <button class="btn btn-secondary" onclick="closeResponse()">Close</button>

                <?php if (!empty($savedResponses)): ?>

Previous AI Analyses (<?= $totalSavedResponses ?> total)

                        <?php foreach ($savedResponses as $saved):
                            // Determine what to display as the "question"
                            $displayQuestion = $saved['question'] ?? 'General Analysis';
                            $hasContext = !empty($saved['context']);
                            $isDefaultQuestion = strpos($displayQuestion, 'Please analyze this email conversation') === 0;

                            // If question is the default and user provided context, show context instead
                            if ($isDefaultQuestion && $hasContext) {
                                $displayQuestion = '[Context] ' . $saved['context'];
                            } elseif ($hasContext && !$isDefaultQuestion) {
                                // If both custom question and context exist, show question + indicator
                                $displayQuestion = $displayQuestion . ' (+context)';
                            }

                            // Truncate to 200 chars instead of 100
                            $displayQuestion = strlen($displayQuestion) > 200
                                ? substr($displayQuestion, 0, 200) . '...'
                                : $displayQuestion;

                            // Get response excerpt for easier identification
                            $responseText = $saved['response']['response'] ?? '';
                            $responseExcerpt = strlen($responseText) > 120
                                ? substr($responseText, 0, 120) . '...'
                                : $responseText;
                        ?>
                        <div class="saved-response" id="saved-response-<?= $saved['_id'] ?>" data-id="<?= $saved['_id'] ?>">

                                    <?= htmlspecialchars($displayQuestion) ?>

                                    <?= date('M j, Y g:i A', strtotime($saved['created_at'] ?? 'now')) ?>

                                <?= htmlspecialchars($saved['provider'] ?? 'Unknown') ?> /
                                <?= htmlspecialchars($saved['model'] ?? 'Unknown') ?>

                            <?php if ($responseExcerpt): ?>

                                <?= htmlspecialchars($responseExcerpt) ?>

                            <?php endif; ?>

                                <button class="btn-view" onclick="event.stopPropagation(); quickViewResponse('<?= $saved['_id'] ?>')">
                                    👁 Quick View
                                </button>
                                <button class="btn-expand" onclick="event.stopPropagation(); loadSavedResponse('<?= $saved['_id'] ?>')">
                                    📄 Full View
                                </button>

                        <?php endforeach; ?>

                    <?php if ($totalSavedResponses > $savedResponsesLimit): ?>

                        <button class="btn btn-secondary" id="load-more-btn" onclick="loadMoreResponses()"
                                data-offset="<?= $savedResponsesLimit ?>"
                                data-total="<?= $totalSavedResponses ?>">
                            Load More (<?= $totalSavedResponses - $savedResponsesLimit ?> remaining)
                        </button>

                    <?php endif; ?>

                <?php endif; ?>

                <?php if (!empty($otherThreads)): ?>

Include Other Threads

                        Select additional threads from <?= htmlspecialchars($primaryContactName ?: 'this contact') ?> to include in the AI analysis:

                        <?php foreach (array_slice($otherThreads, 0, 10) as $thread): ?>

                            <input type="checkbox" name="additional_threads[]" value="<?= htmlspecialchars($thread['thread_id']) ?>">

<?= htmlspecialchars($thread['subject'] ?? '(No Subject)') ?>

                                    <?= $thread['email_count'] ?? 0 ?> emails |
                                    <?= date('M j, Y', strtotime($thread['last_date'] ?? 'now')) ?>

                        <?php endforeach; ?>

                <?php endif; ?>

Ask AI

                        Select AI Provider

Loading providers...

                        Email Content Processing
                        <select id="content-mode" style="width: 100%;">
                            <option value="full">Full Content (include all quoted replies)</option>
                            <option value="stripped" selected>Stripped (remove quoted replies - saves tokens)</option>
                            <option value="smart">Smart Deduplication (quotes only in last email)</option>
                        </select>

                            Stripped: Removes ">" quoted lines and "On [date] wrote:" sections.

                            Smart: Keeps full context only in the last email to avoid redundancy.


1000→

  1001→                        Your Question
  1002→                        <textarea id="ai-question" placeholder="What would you like to know about this conversation?

Examples:
- What is the client's main concern?
- Summarize the key points
- Is there any deception detected?
- What should be my next steps?"></textarea>
  1003→

  1004→
  1005→
  1006→

  1007→                        Additional Context (Optional)
  1008→                        <textarea id="ai-context" placeholder="Add any additional context the AI should know...

e.g., business background, relationship history, specific concerns..."></textarea>
  1009→

  1010→
  1011→
  1012→                    <button class="btn btn-primary" id="submit-ai-btn" onclick="submitToAI()" style="width: 100%;">
  1013→                        Send to AI
  1014→                    </button>
  1015→

  1016→

  1017→

  1018→

  1019→
  1020→
  1021→

  1022→

  1023→

  1024→
Analyzing with AI...

  1025→

  1026→

  1027→
  1028→
  1029→

  1030→

  1031→

  1032→
AI Response

  1033→                <button class="quick-view-close" onclick="closeQuickView()">✕ Close (Esc)</button>
  1034→

  1035→

  1036→

  1037→

  1038→

  1039→

  1040→

  1041→

  1042→
  1043→

                        Your Question
                        <textarea id="ai-question" placeholder="What would you like to know about this conversation?

Examples:
- What is the client's main concern?
- Summarize the key points
- Is there any deception detected?
- What should be my next steps?"></textarea>

                        Additional Context (Optional)
                        <textarea id="ai-context" placeholder="Add any additional context the AI should know...

e.g., business background, relationship history, specific concerns..."></textarea>

                    <button class="btn btn-primary" id="submit-ai-btn" onclick="submitToAI()" style="width: 100%;">
                        Send to AI
                    </button>

Analyzing with AI...

AI Response

                <button class="quick-view-close" onclick="closeQuickView()">✕ Close (Esc)</button>