AskUserQuestion

Question: How would you like to approach agents and memory for this feature?

Choices:
Named agent presets (Quote Analyst, Deception Detector, Profiler, Composer) with per-contact memory that persists across sessions. Most powerful but more work upfront.
Add the agent presets now (specialized system prompts + model preferences), add persistent memory in a follow-up phase.
Keep the existing mode tabs but add contact memory and session memory. Simpler to implement, still very useful.
Ship the multi-thread context builder + chat persistence first (the current plan), then add agents and memory as a second phase.


User has answered your questions: "How would you like to approach agents and memory for this feature?"="Full agents + memory (Recommended)". You can now continue with the user's answers in mind.

Question: How would you like to approach agents and memory for this feature?

Choices:
Named agent presets (Quote Analyst, Deception Detector, Profiler, Composer) with per-contact memory that persists across sessions. Most powerful but more work upfront.
Add the agent presets now (specialized system prompts + model preferences), add persistent memory in a follow-up phase.
Keep the existing mode tabs but add contact memory and session memory. Simpler to implement, still very useful.
Ship the multi-thread context builder + chat persistence first (the current plan), then add agents and memory as a second phase.


The user wants the full agents + memory approach. Let me update the plan to include:

1. Named agent presets (Quote Analyst, Deception Detector, Contact Profiler, Email Composer)
2. Per-contact memory that persists across sessions
3. Session memory (saved context items)

Let me update the plan file with these additions. This is a significant expansion, so I need to restructure the plan.


Full agents + memory it is. Let me update the plan with this expanded architecture.


Write

# 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


The file /root/.claude/plans/scalable-doodling-peach.md has been updated successfully.

# 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

-# Plan: AI Context Builder with Multi-Thread Support

+# Plan: AI Context Builder with Agents & Memory

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

+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:

-- **Searching** for contacts by email/name

-- **Selecting multiple threads** (or all threads for a contact) as AI context

-- **Choosing AI provider + model** from a dropdown (synced with Chrome extension's list)

-- **Asking questions** about the combined context (e.g. "how much to quote?", "deception level?")

+- **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

-- **Token budget management** – don't exceed model context limits

-- **Strip quoted/duplicate content** from emails (reuse existing `stripQuotedContent()`)

-- **Persist chat sessions** server-side so conversations can be reloaded and continued

-| [public/api.php](public/api.php) | Accept `thread_ids[]` + `provider` + `session_id`; strip quoted content; token budget; save/load chat sessions |

-| [public/assets/app.js](public/assets/app.js) | Extend `aiChat` state; add `PROVIDER_MODELS`; context builder functions; modify `sendAiChat()`; session persistence |

-| [public/index.php](public/index.php) | Replace static model dropdown with provider+model selectors; add context builder HTML; add session list UI |

-| [src/ThreadAnalyzer.php](src/ThreadAnalyzer.php) | Make `stripQuotedContent()` public static so it can be reused |

+| [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)

+  "agent": "general|quote_analyst|deception_detector|contact_profiler|email_composer",

+  "context_thread_ids": ["tid1", "tid2"],

+    {"role": "user", "content": "...", "timestamp": "datetime"},

+    {"role": "assistant", "content": "...", "timestamp": "datetime", "model": "claude-sonnet-4-5"}

+  "contact_email": "stephanie@asendia.com",

+      "fact": "Always negotiates 20-30% below initial quote",

+      "session_id": "uuid (which session discovered this)",

+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: []}`

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

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

-**1a.** Parse new fields from JSON input:

-- `thread_ids` (array of thread IDs) alongside existing `thread_id`

-- `provider` (explicit provider code like "claude", "openai")

-- `session_id` (optional – to continue an existing chat session)

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

-**1b.** Strip quoted content from email bodies using `ThreadAnalyzer::stripQuotedContent()`:

-- Currently the `ai_chat` sends raw `body_text` truncated to 800 chars (line 2347)

-- Each email reply typically contains the entire previous conversation quoted below

-- A 5-email thread sends ~5x duplicate content because email #5 quotes emails #1-4

-- **Fix**: call `stripQuotedContent($body)` before truncating, to keep only the new content in each email

-- This alone can reduce context size by 60-80% for long threads

-**1c.** Replace single-thread context build (lines 2331-2363) with a multi-thread loop:

-- If `thread_ids` array provided, loop and fetch emails for each thread

-- Label each as "THREAD 1 OF N", "THREAD 2 OF N"

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

+- `thread_ids` (array) alongside existing `thread_id`

+- `session_id` (optional – continue existing session)

+**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)

+- 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

-- If only `thread_id` (single) is sent, keep existing behavior (backward compat)

+- Single `thread_id` keeps existing behavior

-**1d.** Add token budget management after building context:

-- Define model context limits map: `{'gpt-4o': 128000, 'claude-sonnet-4-5': 200000, ...}`

-- Estimate tokens: `strlen($contextBlock) / 3.5` (rough chars-to-tokens ratio)

-- Reserve ~30% for AI response + conversation history

-- If context exceeds budget: truncate oldest emails first (keep newest), add "[X earlier emails omitted]" note

+- Estimate tokens: `strlen($contextBlock) / 3.5`

+- If over budget: reduce per-email body proportionally, then omit oldest emails with note

- Return `context_stats` in response: `{threads, emails, estimated_tokens, truncated}`

-**1e.** Fix provider detection (lines 2419-2424):

-- Use explicit `provider` field from frontend when available

-- Add `deepseek` detection for model name fallback

+**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

-**1f.** Fix proxy payload model format (line 2431):

-- Change `'models' => [$chatModel]` to `'models' => [$chatProvider => $chatModel]`

-- Fixes bug where the AI proxy ignores the selected model

+- Map `agent` ID to full system prompt (defined in PHP array)

+- Override mode-based prompts when agent is specified

-### Step 2: Chat Session Persistence (new)

+**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

-**2a.** Create `ai_chat_sessions` collection/table:

-  title: string (auto-generated from first question),

-    { role: 'user'|'assistant', content: string, timestamp: datetime, model: string|null }

+**2h. Fix provider/model proxy format:**

+- Change `'models' => [$chatModel]` to `'models' => [$chatProvider => $chatModel]` (line 2431)

+- Use explicit `provider` field when available

-- `ai_chat_save` – saves/updates a session (called after each AI response)

-- `ai_chat_sessions` – lists recent sessions (for the session list UI)

-- `ai_chat_session` – loads a specific session by `session_id`

-- `ai_chat_session_delete` – deletes a session

+- `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

-**2c.** Modify existing `ai_chat` action:

-- After successful AI response, auto-save the message pair to the session

-- Return `session_id` in response (create new if not provided)

+- 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)

-**3a.** Extend `aiChat` state (line 2672):

-- Add `contextThreads: []` – array of `{thread_id, subject, email_count, contact_name}`

-- Add `contextContact: null` – `{id, name, email}`

-- Add `sessionId: null` – current chat session ID

-- Add `sessions: []` – list of saved sessions

+**3a. Extend `aiChat` state (line 2672):**

+contextThreads: [], contextContact: null,

-**3b.** Add `PROVIDER_MODELS` and `PROVIDER_NAMES` constants (after line 2684), synced with Chrome extension (`contentScript.js` lines 18-67):

-- 8 providers: Claude, OpenAI, Gemini, Grok, DeepSeek, Qwen, Mistral, Cohere

-- Same model IDs as the Chrome extension

+**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.** Add provider dropdown functions:

-- `initAiProviderDropdown()` – populates provider select, calls `updateAiModelDropdown()`

-- `updateAiModelDropdown()` – populates model select based on selected provider

-- Call `initAiProviderDropdown()` from DOMContentLoaded

+**3c. Provider/model dropdown functions:**

+- `initAiProviderDropdown()` — populates provider select

+- `updateAiModelDropdown()` — populates model based on provider

-- `toggleContextBuilder()` – show/hide the search area

-- `searchContextContacts()` – calls `apiCall('contacts', {search, limit:5})`, renders clickable results

-- `selectContextContact(id, name, email)` – calls `apiCall('threads', {contact:id, limit:50})`, shows thread picker with "+ Add" buttons and an "Add All" button

-- `addThreadToContext(threadId, subject, emailCount, contactName)` – pushes to `contextThreads[]`

-- `removeThreadFromContext(threadId)` – removes from `contextThreads[]`

-- `addAllContactThreads()` – adds all available threads at once

-- `clearContextCart()` – clears all context

-- `updateContextCart()` – renders the "cart" showing selected threads count and list with remove buttons

+- `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.** Update `updateAiChatContextLabel()` (line 2728):

-- When `contextThreads.length > 0`, show green text: "3 threads, ~15 emails from Stephanie"

-- Otherwise keep existing mode-based label

+- `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.** Add `multithread` quick prompts to `AI_QUICK_PROMPTS` (line 2686):

-- "Summarize all conversations", "How much have we quoted?", "Deception level?", "Build a complete profile", "What patterns emerge?"

-- Update `renderQuickPrompts()` to use `multithread` prompts when context builder has threads

+**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.** Modify `sendAiChat()` (line 2756):

-- Read provider from `#ai-chat-provider` select

-- If `contextThreads.length > 0`, send `thread_ids: [...]` array

-- Otherwise send single `thread_id` as before (backward compat)

-- Add `provider` and `session_id` to payload

-- On response: store returned `session_id`, display `context_stats` if truncation happened

+- `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.** Add session management functions:

-- `loadChatSessions()` – fetches recent sessions from API, renders in a dropdown/list

-- `loadChatSession(sessionId)` – loads a session, restores messages + context + model selection

-- `startNewSession()` – clears chat and starts fresh (generates new session_id on first message)

-- `deleteChatSession(sessionId)` – deletes from server

+- 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

+- 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

-**4a.** Replace static model dropdown (lines 538-561) with two selects:

-- Provider dropdown (`#ai-chat-provider`) with `onchange="updateAiModelDropdown()"`

-- Model dropdown (`#ai-chat-model`) – populated dynamically by JS

-**4b.** Add session controls to the chat header (line 531 area):

-- "History" button that toggles a session list overlay

+**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)

-**4c.** Insert context builder section between mode tabs (line 570) and messages div (line 574):

-- Context summary bar (always visible, clickable to expand) showing "No context loaded" or "3 threads, ~15 emails"

-  - Contact/email search input + search button

-  - Thread picker container (shows after contact selected)

-  - Context cart (selected threads with remove buttons and "Clear All")

+**4b. Chat header additions (line 531 area):**

+- "History" button (toggles session list overlay)

-### Step 5: Make `stripQuotedContent()` reusable – ThreadAnalyzer.php

+**4c. Insert context builder section (between line 570 and 574):**

+- Collapsible context summary bar → expands to show search + thread picker + cart + contact memories

-**5a.** Change `private function stripQuotedContent(string $body): string` (line 1616) to `public static function stripQuotedContent(string $body): string`

-- No other changes needed – the method is pure/stateless, no `$this` references

+- Lists recent sessions with title, agent icon, date, message count

+- Click to load, swipe/button to delete

-Each email in a thread typically quotes all previous emails in the body. By calling `stripQuotedContent()` we keep only the **new content** per email. Since we send emails in chronological order, AI sees the full conversation without duplication. For a 10-email thread this can reduce context from ~40KB to ~8KB.

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

-Rather than hard-failing, we use **smart truncation**:

-1. Strip quoted content first (biggest win)

-3. If over budget: reduce per-email body length proportionally

-4. If still over: omit oldest emails per thread, noting "[X earlier emails omitted for context budget]"

-5. Never truncate the user's question or conversation history

+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)

-- Sessions are saved server-side (MongoDB `ai_chat_sessions` collection)

-- Auto-saved after each AI response (not manually)

-- Session title auto-generated from first user message

-- Frontend sends `session_id` with each request; server appends to message history

-- Loading a session restores: messages, context thread IDs, provider/model, mode

-- This replaces the Chrome extension's `localStorage` approach with a proper server-side solution

-2. Click the AI chat FAB button (bottom-right)

-3. Verify provider dropdown shows all 8 providers, model updates on provider change

-4. Click the context summary bar → search area expands

-5. Search for a contact email (e.g. "stephanie" or "david.zolanz")

-6. Click a contact → threads load with "+ Add" and "Add All" buttons

-7. Add individual threads or click "Add All"

-8. Verify context label turns green showing thread/email count

-9. Verify quick prompts switch to multi-thread prompts

-10. Ask a question → verify response has context from ALL selected threads

-11. Check response includes `context_stats` showing token count and if truncation happened

-12. Ask a follow-up question → verify conversation continues with history

-13. Refresh the page → click "History" → verify the session appears in the list

-14. Click to reload the session → verify messages, context, and model selection are restored

-15. Test backward compat: open a single thread view, click "Ask AI About This Thread" → still works

-16. Test with a contact that has many threads (e.g. 30+) → verify token budget truncation kicks in and AI still responds coherently

+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

# Plan: AI Context Builder with Multi-Thread Support

## Context

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

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

Additionally:
- **Token budget management** – don't exceed model context limits
- **Strip quoted/duplicate content** from emails (reuse existing `stripQuotedContent()`)
- **Persist chat sessions** server-side so conversations can be reloaded and continued

## Files to Modify

| File | Changes |
|------|---------|
| [public/api.php](public/api.php) | Accept `thread_ids[]` + `provider` + `session_id`; strip quoted content; token budget; save/load chat sessions |
| [public/assets/app.js](public/assets/app.js) | Extend `aiChat` state; add `PROVIDER_MODELS`; context builder functions; modify `sendAiChat()`; session persistence |
| [public/index.php](public/index.php) | Replace static model dropdown with provider+model selectors; add context builder HTML; add session list UI |
| [src/ThreadAnalyzer.php](src/ThreadAnalyzer.php) | Make `stripQuotedContent()` public static so it can be reused |

## Implementation Steps

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

**1a.** Parse new fields from JSON input:
- `thread_ids` (array of thread IDs) alongside existing `thread_id`
- `provider` (explicit provider code like "claude", "openai")
- `session_id` (optional – to continue an existing chat session)

**1b.** Strip quoted content from email bodies using `ThreadAnalyzer::stripQuotedContent()`:
- Currently the `ai_chat` sends raw `body_text` truncated to 800 chars (line 2347)
- Each email reply typically contains the entire previous conversation quoted below
- A 5-email thread sends ~5x duplicate content because email #5 quotes emails #1-4
- **Fix**: call `stripQuotedContent($body)` before truncating, to keep only the new content in each email
- This alone can reduce context size by 60-80% for long threads

**1c.** Replace single-thread context build (lines 2331-2363) with a multi-thread loop:
- If `thread_ids` array provided, loop and fetch emails for each thread
- Label each as "THREAD 1 OF N", "THREAD 2 OF N"
- Include per-thread analysis summaries if available
- If only `thread_id` (single) is sent, keep existing behavior (backward compat)

**1d.** Add token budget management after building context:
- Define model context limits map: `{'gpt-4o': 128000, 'claude-sonnet-4-5': 200000, ...}`
- Estimate tokens: `strlen($contextBlock) / 3.5` (rough chars-to-tokens ratio)
- Reserve ~30% for AI response + conversation history
- If context exceeds budget: truncate oldest emails first (keep newest), add "[X earlier emails omitted]" note
- Return `context_stats` in response: `{threads, emails, estimated_tokens, truncated}`

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

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

### Step 2: Chat Session Persistence (new)

**2a.** Create `ai_chat_sessions` collection/table:
```
{
  session_id: string (uuid),
  created_at: datetime,
  updated_at: datetime,
  title: string (auto-generated from first question),
  mode: string,
  provider: string,
  model: string,
  context_thread_ids: array,
  contact_id: int|null,
  contact_name: string|null,
  messages: [
    { role: 'user'|'assistant', content: string, timestamp: datetime, model: string|null }
  ]
}
```

**2b.** New API actions in `api.php`:
- `ai_chat_save` – saves/updates a session (called after each AI response)
- `ai_chat_sessions` – lists recent sessions (for the session list UI)
- `ai_chat_session` – loads a specific session by `session_id`
- `ai_chat_session_delete` – deletes a session

**2c.** Modify existing `ai_chat` action:
- Accept `session_id` in payload
- After successful AI response, auto-save the message pair to the session
- Return `session_id` in response (create new if not provided)

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

**3a.** Extend `aiChat` state (line 2672):
- Add `contextThreads: []` – array of `{thread_id, subject, email_count, contact_name}`
- Add `contextContact: null` – `{id, name, email}`
- Add `sessionId: null` – current chat session ID
- Add `sessions: []` – list of saved sessions

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

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

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

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

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

**3g.** Modify `sendAiChat()` (line 2756):
- Read provider from `#ai-chat-provider` select
- If `contextThreads.length > 0`, send `thread_ids: [...]` array
- Otherwise send single `thread_id` as before (backward compat)
- Add `provider` and `session_id` to payload
- On response: store returned `session_id`, display `context_stats` if truncation happened

**3h.** Add session management functions:
- `loadChatSessions()` – fetches recent sessions from API, renders in a dropdown/list
- `loadChatSession(sessionId)` – loads a session, restores messages + context + model selection
- `startNewSession()` – clears chat and starts fresh (generates new session_id on first message)
- `deleteChatSession(sessionId)` – deletes from server

### Step 4: HTML – index.php

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

**4b.** Add session controls to the chat header (line 531 area):
- "New Chat" button
- "History" button that toggles a session list overlay

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

### Step 5: Make `stripQuotedContent()` reusable – ThreadAnalyzer.php

**5a.** Change `private function stripQuotedContent(string $body): string` (line 1616) to `public static function stripQuotedContent(string $body): string`
- No other changes needed – the method is pure/stateless, no `$this` references

## Key Design Decisions

### Email deduplication strategy
Each email in a thread typically quotes all previous emails in the body. By calling `stripQuotedContent()` we keep only the **new content** per email. Since we send emails in chronological order, AI sees the full conversation without duplication. For a 10-email thread this can reduce context from ~40KB to ~8KB.

### Token budget approach
Rather than hard-failing, we use **smart truncation**:
1. Strip quoted content first (biggest win)
2. Calculate estimated tokens
3. If over budget: reduce per-email body length proportionally
4. If still over: omit oldest emails per thread, noting "[X earlier emails omitted for context budget]"
5. Never truncate the user's question or conversation history

### Chat session persistence
- Sessions are saved server-side (MongoDB `ai_chat_sessions` collection)
- Auto-saved after each AI response (not manually)
- Session title auto-generated from first user message
- Frontend sends `session_id` with each request; server appends to message history
- Loading a session restores: messages, context thread IDs, provider/model, mode
- This replaces the Chrome extension's `localStorage` approach with a proper server-side solution

## Verification

1. Open https://mssql.danmarcrm.com/dev1/extension2/public/
2. Click the AI chat FAB button (bottom-right)
3. Verify provider dropdown shows all 8 providers, model updates on provider change
4. Click the context summary bar → search area expands
5. Search for a contact email (e.g. "stephanie" or "david.zolanz")
6. Click a contact → threads load with "+ Add" and "Add All" buttons
7. Add individual threads or click "Add All"
8. Verify context label turns green showing thread/email count
9. Verify quick prompts switch to multi-thread prompts
10. Ask a question → verify response has context from ALL selected threads
11. Check response includes `context_stats` showing token count and if truncation happened
12. Ask a follow-up question → verify conversation continues with history
13. Refresh the page → click "History" → verify the session appears in the list
14. Click to reload the session → verify messages, context, and model selection are restored
15. Test backward compat: open a single thread view, click "Ask AI About This Thread" → still works
16. Test with a contact that has many threads (e.g. 30+) → verify token budget truncation kicks in and AI still responds coherently


ExitPlanMode

# 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


User has approved your plan. You can now start coding. Start with updating your todo list if applicable

Your plan has been saved to: /root/.claude/plans/scalable-doodling-peach.md
You can refer back to it if needed during implementation.

## Approved Plan:
# 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


Great, the plan is approved. Let me start implementing. This is a large feature, so I'll follow the execution order from the plan and track progress with todos.

Let me start:
1. Make `stripQuotedContent()` public static in ThreadAnalyzer.php
2. Backend multi-thread + token budget + stripped content in api.php
3. Frontend context builder + provider/model selector in app.js + index.php
4. Agent presets
5. Chat session persistence
6. Contact memory

Let me set up the todo list and start working.


Plan approved. Let me set up the task tracker and start implementing.


TodoWrite

Make stripQuotedContent() public static in ThreadAnalyzer.php

Making stripQuotedContent() public static

Backend: multi-thread context + token budget + stripped content in api.php

Implementing multi-thread context and token budget in api.php

Backend: new API actions (sessions CRUD, contact memory CRUD)

Backend: agent presets + auto-memory extraction in api.php

Adding agent presets and auto-memory extraction

Frontend: provider/model selector + agent selector + constants in app.js

Adding provider/model/agent selectors to frontend

Frontend: context builder (search, thread picker, cart) in app.js

Frontend: session management (save/load/history) in app.js

Frontend: contact memory UI in app.js

HTML: update index.php with new UI sections

Test end-to-end: multi-thread, agents, sessions, memory


Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable

Make stripQuotedContent() public static in ThreadAnalyzer.php

Making stripQuotedContent() public static

Backend: multi-thread context + token budget + stripped content in api.php

Implementing multi-thread context and token budget in api.php

Backend: new API actions (sessions CRUD, contact memory CRUD)

Backend: agent presets + auto-memory extraction in api.php

Adding agent presets and auto-memory extraction

Frontend: provider/model selector + agent selector + constants in app.js

Adding provider/model/agent selectors to frontend

Frontend: context builder (search, thread picker, cart) in app.js

Frontend: session management (save/load/history) in app.js

Frontend: contact memory UI in app.js

HTML: update index.php with new UI sections

Test end-to-end: multi-thread, agents, sessions, memory


### Step 1: Make `stripQuotedContent()` public static


  1610→     * Strip quoted content from email body to avoid duplication
  1611→     * Removes:
  1612→     * - Lines starting with > (quoted replies)
  1613→     * - "On X date, Y wrote:" patterns and everything after
  1614→     * - "From: ... Sent: ... To: ... Subject: ..." forwarded headers
  1615→     */
  1616→    private function stripQuotedContent(string $body): string
  1617→    {
  1618→        // Remove lines starting with > (quoted text)
  1619→        $body = preg_replace('/^>+.*$/m', '', $body);
  1620→
  1621→        // Remove "On [date], [person] wrote:" and everything after
  1622→        $patterns = [
  1623→            '/On\s+.{10,60}\s+wrote:\s*$/mi',  // "On Monday, Jan 1, John wrote:"
  1624→            '/On\s+\d{1,2}[\/-]\d{1,2}[\/-]\d{2,4}.+wrote:\s*$/mi',  // "On 01/02/2023, John wrote:"
  1625→            '/-{2,}.*Original Message.*-{2,}/i',  // "-----Original Message-----"
  1626→            '/From:\s+.+\nSent:\s+.+\nTo:\s+.+\nSubject:\s+.+/i',  // Outlook forwarded header
  1627→            '/_{5,}.*$/m',  // _____ separator lines
  1628→        ];
  1629→
  1630→        foreach ($patterns as $pattern) {
  1631→            if (preg_match($pattern, $body, $matches, PREG_OFFSET_CAPTURE)) {
  1632→                // Cut everything from the match onwards
  1633→                $body = substr($body, 0, $matches[0][1]);
  1634→            }
  1635→        }
  1636→
  1637→        // Clean up multiple consecutive newlines
  1638→        $body = preg_replace('/\n{3,}/', "\n\n", $body);
  1639→
  1640→        return trim($body);
  1641→    }
  1642→}
  1643→

     * Strip quoted content from email body to avoid duplication
     * Removes:
     * - Lines starting with > (quoted replies)
     * - "On X date, Y wrote:" patterns and everything after
     * - "From: ... Sent: ... To: ... Subject: ..." forwarded headers
     */
    private function stripQuotedContent(string $body): string
    {
        // Remove lines starting with > (quoted text)
        $body = preg_replace('/^>+.*$/m', '', $body);

        // Remove "On [date], [person] wrote:" and everything after
        $patterns = [
            '/On\s+.{10,60}\s+wrote:\s*$/mi',  // "On Monday, Jan 1, John wrote:"
            '/On\s+\d{1,2}[\/-]\d{1,2}[\/-]\d{2,4}.+wrote:\s*$/mi',  // "On 01/02/2023, John wrote:"
            '/-{2,}.*Original Message.*-{2,}/i',  // "-----Original Message-----"
            '/From:\s+.+\nSent:\s+.+\nTo:\s+.+\nSubject:\s+.+/i',  // Outlook forwarded header
            '/_{5,}.*$/m',  // _____ separator lines
        ];

        foreach ($patterns as $pattern) {
            if (preg_match($pattern, $body, $matches, PREG_OFFSET_CAPTURE)) {
                // Cut everything from the match onwards
                $body = substr($body, 0, $matches[0][1]);
            }
        }

        // Clean up multiple consecutive newlines
        $body = preg_replace('/\n{3,}/', "\n\n", $body);

        return trim($body);
    }
}


Edit

private function stripQuotedContent(string $body): string

public static function stripQuotedContent(string $body): string