Now let me verify the syntax:


Bash

php -l /var/www/mssql.danmarcrm.com/dev1/extension2v1/public/thread_view.php 2>&1

PHP syntax check


No syntax errors detected in /var/www/mssql.danmarcrm.com/dev1/extension2v1/public/thread_view.php


Let me also quickly verify the JS isn't broken by checking for basic JS syntax issues — matching braces, etc:


3060:        async function runDeepAnalysis() {


3333:        // Submit to AI

3333:        // Submit to AI


Good — `runDeepAnalysis()` spans lines 3060-3331, then `submitToAI()` starts at 3334. Let me verify the function is properly closed by reading the boundary:


  3326→            } finally {
  3327→                document.getElementById('loading-overlay').classList.remove('active');
  3328→                document.getElementById('submit-ai-btn').disabled = false;
  3329→                document.getElementById('deep-analysis-btn').disabled = false;
  3330→            }
  3331→        }
  3332→
  3333→        // Submit to AI
  3334→        async function submitToAI() {
  3335→            var selectedProvidersList = getSelectedProviders();
  3336→
  3337→            if (selectedProvidersList.length === 0) {

            } finally {
                document.getElementById('loading-overlay').classList.remove('active');
                document.getElementById('submit-ai-btn').disabled = false;
                document.getElementById('deep-analysis-btn').disabled = false;
            }
        }

        // Submit to AI
        async function submitToAI() {
            var selectedProvidersList = getSelectedProviders();

            if (selectedProvidersList.length === 0) {


TodoWrite

Add Deep Analysis preset prompts as JS constant

Add Deep Analysis button and results panel to thread_view.php UI

Implement automated multi-pass analysis function

Implementing automated multi-pass analysis

Add progress tracking and sequential pass execution

Test the automated flow


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

Add Deep Analysis preset prompts as JS constant

Add Deep Analysis button and results panel to thread_view.php UI

Implement automated multi-pass analysis function

Implementing automated multi-pass analysis

Add progress tracking and sequential pass execution

Test the automated flow

Test the automated flow


Now let me update the memory file with what was built today:


     1→# Email Intelligence System – Project Memory
     2→
     3→## Project Summary
     4→AI-powered Gmail email thread analysis system with Chrome extension.
     5→See full overview: `/var/www/mssql.danmarcrm.com/dev1/extension2/PROJECT_OVERVIEW.md`
     6→
     7→## Key Paths
     8→- **Server root (v1):** `/var/www/mssql.danmarcrm.com/dev1/extension2v1/`
     9→- **Server root (original):** `/var/www/mssql.danmarcrm.com/dev1/extension2/`
    10→- **Web app:** `public/` in either root

# Email Intelligence System – Project Memory

## Project Summary
AI-powered Gmail email thread analysis system with Chrome extension.
See full overview: `/var/www/mssql.danmarcrm.com/dev1/extension2/PROJECT_OVERVIEW.md`

## Key Paths
- **Server root (v1):** `/var/www/mssql.danmarcrm.com/dev1/extension2v1/`
- **Server root (original):** `/var/www/mssql.danmarcrm.com/dev1/extension2/`
- **Web app:** `public/` in either root


    95→- API: `import_gmail_threads` — loops through selected thread_ids, fetches from Gmail API, saves emails+contacts
    96→- UI: "Import from Gmail" panel in contacts tab with search input and results with checkboxes
    97→
    98→### D3.js Chart (Task 8)
    99→- D3.js v7 CDN loaded in thread_view.php
   100→- "Generate Chart" button in follow-up bar
   101→- Sends AI response to AI asking for structured JSON (nodes + links)
   102→- `renderD3Chart()` — force-directed graph, color-coded by type, drag/zoom/pan, tooltips, legend
   103→
   104→### Other Changes
   105→- **Task 9:** Products tab hidden from navigation
   106→- **Task 10:** AI models updated: Claude Opus 4.6, Sonnet 4.6 added as defaults
   107→- **Task 7:** Chrome extension manual-only mode — documented but not implemented (extension on local Mac)
   108→
   109→### New API Endpoints
   110→- `delete_contact`, `delete_thread`, `delete_ai_response`, `delete_threads_by_email`
   111→- `search_gmail_threads`, `import_gmail_threads`
   112→
   113→### Files Modified
   114→- `src/QueryAdapter.php` — 3 new delete methods, sort/filter params, contact name fix (to_email)
   115→- `public/api.php` — 6 new endpoints (delete + import)
   116→- `public/assets/app.js` — delete/filter/sort/sync JS functions
   117→- `public/index.php` — sort dropdowns, filter bars, import panel, hidden Products tab
   118→- `public/thread_view.php` — delete AI response button, D3.js chart, MODEL_LIMITS update
   119→- `config/config.php` — CLAUDE_MODEL → claude-sonnet-4-6
   120→- `aiapi/api_opus/config/api_keys.php` — Claude 4.6 models (both extension2 + extension2v1)
   121→

- API: `import_gmail_threads` — loops through selected thread_ids, fetches from Gmail API, saves emails+contacts
- UI: "Import from Gmail" panel in contacts tab with search input and results with checkboxes

### D3.js Chart (Task 8)
- D3.js v7 CDN loaded in thread_view.php
- "Generate Chart" button in follow-up bar
- Sends AI response to AI asking for structured JSON (nodes + links)
- `renderD3Chart()` — force-directed graph, color-coded by type, drag/zoom/pan, tooltips, legend

### Other Changes
- **Task 9:** Products tab hidden from navigation
- **Task 10:** AI models updated: Claude Opus 4.6, Sonnet 4.6 added as defaults
- **Task 7:** Chrome extension manual-only mode — documented but not implemented (extension on local Mac)

### New API Endpoints
- `delete_contact`, `delete_thread`, `delete_ai_response`, `delete_threads_by_email`
- `search_gmail_threads`, `import_gmail_threads`

### Files Modified
- `src/QueryAdapter.php` — 3 new delete methods, sort/filter params, contact name fix (to_email)
- `public/api.php` — 6 new endpoints (delete + import)
- `public/assets/app.js` — delete/filter/sort/sync JS functions
- `public/index.php` — sort dropdowns, filter bars, import panel, hidden Products tab
- `public/thread_view.php` — delete AI response button, D3.js chart, MODEL_LIMITS update
- `config/config.php` — CLAUDE_MODEL → claude-sonnet-4-6
- `aiapi/api_opus/config/api_keys.php` — Claude 4.6 models (both extension2 + extension2v1)


Edit

- `aiapi/api_opus/config/api_keys.php` — Claude 4.6 models (both extension2 + extension2v1)

- `aiapi/api_opus/config/api_keys.php` — Claude 4.6 models (both extension2 + extension2v1)

---

## 2026-02-21 (Session 2): Bug Fixes + Deep Analysis Feature

### Bug Fixes
- **Thread pagination broken**: `renderPagination` uses `loadFunction.name` for onclick — anonymous arrow `(p) => loadEmails(p)` has empty `.name`. Fixed: pass `loadEmails` directly.
- **Thread search only searched subject**: Added `$or` on `subject`, `from_email`, `to_email` in `getThreadsMongo()`
- **Context builder search limit was 5**: Increased to 100 threads, 10 contacts. Added "Add All" buttons and "Load More"
- **Contacts empty on last_contact sort**: Removed expensive `$lookup`, use existing `last_contact` field on contact documents

### Context Builder Enhancements
- `addAllSearchThreads()` — adds loaded threads to context
- `addAllSearchThreadsFull()` — fetches ALL pages then adds every thread
- `loadMoreSearchThreads()` — pagination for large result sets
- Thread search results show total count, "Add All X Threads" (blue, fetches all), "Add Shown X"
- Scrollable results area increased to 400px
- Contact thread limit raised from 20 to 100

### Bulk Delete Found Threads
- New API endpoint: `delete_threads_bulk` — accepts `{thread_ids: [...]}`, max 500, calls `deleteThread()` for each
- Dashboard: "Delete All X Found" button appears when search/filter active
- Safety: reads exact same filters, fetches all matching thread_ids, double-confirm for >50, deletes by exact ID only

### Deep Analysis (5-Pass Automated)
- `DEEP_ANALYSIS_PASSES` constant: 5 passes (Timeline, Pressure, Profiles, Financial, Win-Back)
- Each pass uses appropriate agent (general, deception_detector, contact_profiler, quote_analyst)
- `runDeepAnalysis()`: builds content once, runs 5 passes sequentially with conversation history building
- Results displayed in dedicated panel with per-pass sections, progress indicator
- Each response saved to MongoDB with `deep_analysis_pass` and `deep_analysis_name` metadata
- 2-second pause between passes to avoid rate limiting
- "Deep Analysis (5-Pass)" button next to "Send to AI" in thread_view.php

### Files Modified
- `public/thread_view.php` — Deep Analysis button+container+JS, context builder enhancements
- `public/assets/app.js` — pagination fix, deleteFoundThreads(), bulk delete button visibility
- `public/api.php` — `delete_threads_bulk` endpoint
- `src/QueryAdapter.php` — thread search `$or` on subject/from/to


The file /root/.claude/projects/-var-www-mssql-danmarcrm-com-dev1-extension2/memory/MEMORY.md has been updated successfully.

- `aiapi/api_opus/config/api_keys.php` — Claude 4.6 models (both extension2 + extension2v1)

---

## 2026-02-21 (Session 2): Bug Fixes + Deep Analysis Feature

### Bug Fixes
- **Thread pagination broken**: `renderPagination` uses `loadFunction.name` for onclick — anonymous arrow `(p) => loadEmails(p)` has empty `.name`. Fixed: pass `loadEmails` directly.
- **Thread search only searched subject**: Added `$or` on `subject`, `from_email`, `to_email` in `getThreadsMongo()`
- **Context builder search limit was 5**: Increased to 100 threads, 10 contacts. Added "Add All" buttons and "Load More"
- **Contacts empty on last_contact sort**: Removed expensive `$lookup`, use existing `last_contact` field on contact documents

### Context Builder Enhancements
- `addAllSearchThreads()` — adds loaded threads to context
- `addAllSearchThreadsFull()` — fetches ALL pages then adds every thread
- `loadMoreSearchThreads()` — pagination for large result sets
- Thread search results show total count, "Add All X Threads" (blue, fetches all), "Add Shown X"
- Scrollable results area increased to 400px
- Contact thread limit raised from 20 to 100

### Bulk Delete Found Threads
- New API endpoint: `delete_threads_bulk` — accepts `{thread_ids: [...]}`, max 500, calls `deleteThread()` for each
- Dashboard: "Delete All X Found" button appears when search/filter active
- Safety: reads exact same filters, fetches all matching thread_ids, double-confirm for >50, deletes by exact ID only

### Deep Analysis (5-Pass Automated)
- `DEEP_ANALYSIS_PASSES` constant: 5 passes (Timeline, Pressure, Profiles, Financial, Win-Back)
- Each pass uses appropriate agent (general, deception_detector, contact_profiler, quote_analyst)
- `runDeepAnalysis()`: builds content once, runs 5 passes sequentially with conversation history building
- Results displayed in dedicated panel with per-pass sections, progress indicator
- Each response saved to MongoDB with `deep_analysis_pass` and `deep_analysis_name` metadata
- 2-second pause between passes to avoid rate limiting
- "Deep Analysis (5-Pass)" button next to "Send to AI" in thread_view.php

### Files Modified
- `public/thread_view.php` — Deep Analysis button+container+JS, context builder enhancements
- `public/assets/app.js` — pagination fix, deleteFoundThreads(), bulk delete button visibility
- `public/api.php` — `delete_threads_bulk` endpoint
- `src/QueryAdapter.php` — thread search `$or` on subject/from/to

# Email Intelligence System – Project Memory

## Project Summary
AI-powered Gmail email thread analysis system with Chrome extension.
See full overview: `/var/www/mssql.danmarcrm.com/dev1/extension2/PROJECT_OVERVIEW.md`

## Key Paths
- **Server root (v1):** `/var/www/mssql.danmarcrm.com/dev1/extension2v1/`
- **Server root (original):** `/var/www/mssql.danmarcrm.com/dev1/extension2/`
- **Web app:** `public/` in either root
- **PHP classes:** `src/` in either root
- **Chrome ext (local Mac, active):** `/Users/dan/Documents/extension2/extension2v1/chrome-extension`
- **AI APIs:** `aiapi/` — two proxies: `api_codex51` (single), `api_opus` (multi-provider)
- **Config:** `config/`

## Live URLs
- https://mssql.danmarcrm.com/dev1/extension2v1/public/
- https://mssql.danmarcrm.com/dev1/extension2/public/

## Tech Stack
- Backend: PHP, DBs: MongoDB (primary) + SQL (legacy, mostly empty)
- AI: Multi-provider via `aiapi/api_codex51` (single) and `aiapi/api_opus` (multi-provider)
- Chrome Extension: Manifest V3, targets mail.google.com

## Key Architecture Patterns
- `MongoDatabase::insertOrUpdate()` for upserts (NOT `upsert()`)
- `ThreadAnalyzer::stripQuotedContent()` – public static, strips quoted email replies
- AI proxy models keyed by provider: `[$provider => $model]` (NOT `[$model]`)
- `QueryAdapter` handles MySQL/MongoDB routing — always use it (NOT raw `$db` SQL)
- Node.js on server is v10.19 — don't use for JS syntax checks
- `thread_view.php` uses `api_opus` proxy for multi-provider simultaneous requests
- Floating AI panel (index.php) uses `api_codex51` proxy for single-provider requests

## Bugs Fixed
- `contacts` API: was raw SQL → fixed to `QueryAdapter::getContactsWithProfiles()`
- AI proxy model override: `[$chatModel]` → `[$chatProvider => $chatModel]`
- Thread contact name: `$max`/`MAX()` picked wrong contact → fixed to find most frequent inbound sender

---

## 2026-02-20: AI Chat Enhancement — Full Implementation (extension2 + extension2v1)

### Floating AI Chat Panel (index.php + app.js)
- Multi-thread context builder, 5 AI agents, 8 providers, session persistence, contact memory
- Token budget management, `stripQuotedContent()` deduplication
- New API endpoints: `ai_chat`, `ai_chat_sessions`, `ai_chat_session`, `ai_chat_session_delete`, `ai_contact_memories`, `ai_contact_memory_add`, `ai_contact_memory_delete`

### Thread View Enhancement (thread_view.php)
- Full-width layout (was max-width 1600px, now 100%)
- 5 AI agent buttons (General, Quotes, Deception, Profiler, Composer) with quick prompts
- Context builder: search contacts/threads, add any thread from any contact
- Token estimation with model limits, warning when approaching max
- Agent system prompt injection into content sent to AI
- Debug file logging (`logs/ai_debug.log`) via `log_ai_debug` API endpoint
- Contact memory injection from `ai_contact_memory` collection
- Keeps existing: multi-provider simultaneous sending, conversation history, saved responses

### Dashboard Fixes (index.php + app.js + QueryAdapter.php)
- Hidden "AI Thread Analysis" section (slow N+1 queries, shows "Unknown")
- Reduced thread/contact load limits from 20 to 10
- Fixed thread contact name bug: `getThreadsMongo()` collects all contacts, post-processes to find most frequent inbound external sender; `getThreadsSQL()` uses subquery

### Files modified in extension2v1
- `src/ThreadAnalyzer.php` — `stripQuotedContent()` public static
- `src/QueryAdapter.php` — fixed `getThreadsMongo()` + `getThreadsSQL()` contact resolution
- `public/api.php` — `ai_chat` action, 6 new endpoints, `log_ai_debug` endpoint
- `public/assets/app.js` — AI chat panel JS, reduced limits, disabled analysis refresh
- `public/index.php` — floating AI panel HTML, hidden analysis section
- `public/thread_view.php` — full-width, agents, context builder, tokens, logging

---

## 2026-02-21: Dashboard Enhancements — 10 Tasks

### Delete Functionality (Task 1+6)
- API endpoints: `delete_contact`, `delete_thread`, `delete_ai_response`, `delete_threads_by_email`
- `QueryAdapter::deleteContact()` — cascades: emails, thread_analysis, contact_profiles, ai_contact_memory, ai_query_responses
- `QueryAdapter::deleteThread()` — cascades: emails, thread_analysis, ai_query_responses
- `QueryAdapter::deleteThreadsByEmail($email, $domain)` — aggregates thread_ids, calls deleteThread for each
- Frontend: Del buttons on contacts, threads, saved AI responses; bulk delete by email/domain prompt

### Sorting & Filters (Task 2+3)
- Sort dropdowns: contacts (Name A-Z/Z-A, Last Activity), threads (Last Activity, Most Emails, Newest)
- Filters: contacts (type, date range), threads (contact name, date range, analysis status)
- `getContactsWithProfilesMongo()` — accepts type/dateFrom/dateTo, $lookup emails for last_contact sort
- `getThreadsMongo()` — accepts sort/order/contactName/dateFrom/dateTo/status params

### Contact Name Fix (Task 4)
- `getThreadsMongo()` now pushes `to_email` alongside `from_email` into all_contacts
- For outbound emails, uses `to_email` (recipient) as external contact (weight 1x)
- For inbound, uses `from_email` (sender, weight 2x)

### Sync by Domain/Email (Task 5)
- API: `search_gmail_threads` — builds Gmail query, calls `GmailClient::searchThreads()`, checks which already imported
- API: `import_gmail_threads` — loops through selected thread_ids, fetches from Gmail API, saves emails+contacts
- UI: "Import from Gmail" panel in contacts tab with search input and results with checkboxes

### D3.js Chart (Task 8)
- D3.js v7 CDN loaded in thread_view.php
- "Generate Chart" button in follow-up bar
- Sends AI response to AI asking for structured JSON (nodes + links)
- `renderD3Chart()` — force-directed graph, color-coded by type, drag/zoom/pan, tooltips, legend

### Other Changes
- **Task 9:** Products tab hidden from navigation
- **Task 10:** AI models updated: Claude Opus 4.6, Sonnet 4.6 added as defaults
- **Task 7:** Chrome extension manual-only mode — documented but not implemented (extension on local Mac)

### New API Endpoints
- `delete_contact`, `delete_thread`, `delete_ai_response`, `delete_threads_by_email`
- `search_gmail_threads`, `import_gmail_threads`

### Files Modified
- `src/QueryAdapter.php` — 3 new delete methods, sort/filter params, contact name fix (to_email)
- `public/api.php` — 6 new endpoints (delete + import)
- `public/assets/app.js` — delete/filter/sort/sync JS functions
- `public/index.php` — sort dropdowns, filter bars, import panel, hidden Products tab
- `public/thread_view.php` — delete AI response button, D3.js chart, MODEL_LIMITS update
- `config/config.php` — CLAUDE_MODEL → claude-sonnet-4-6
- `aiapi/api_opus/config/api_keys.php` — Claude 4.6 models (both extension2 + extension2v1)

+## 2026-02-21 (Session 2): Bug Fixes + Deep Analysis Feature

+- **Thread pagination broken**: `renderPagination` uses `loadFunction.name` for onclick — anonymous arrow `(p) => loadEmails(p)` has empty `.name`. Fixed: pass `loadEmails` directly.

+- **Thread search only searched subject**: Added `$or` on `subject`, `from_email`, `to_email` in `getThreadsMongo()`

+- **Context builder search limit was 5**: Increased to 100 threads, 10 contacts. Added "Add All" buttons and "Load More"

+- **Contacts empty on last_contact sort**: Removed expensive `$lookup`, use existing `last_contact` field on contact documents

+- `addAllSearchThreads()` — adds loaded threads to context

+- `addAllSearchThreadsFull()` — fetches ALL pages then adds every thread

+- `loadMoreSearchThreads()` — pagination for large result sets

+- Thread search results show total count, "Add All X Threads" (blue, fetches all), "Add Shown X"

+- Scrollable results area increased to 400px

+- Contact thread limit raised from 20 to 100

+- New API endpoint: `delete_threads_bulk` — accepts `{thread_ids: [...]}`, max 500, calls `deleteThread()` for each

+- Dashboard: "Delete All X Found" button appears when search/filter active

+- Safety: reads exact same filters, fetches all matching thread_ids, double-confirm for >50, deletes by exact ID only

+- `DEEP_ANALYSIS_PASSES` constant: 5 passes (Timeline, Pressure, Profiles, Financial, Win-Back)

+- Each pass uses appropriate agent (general, deception_detector, contact_profiler, quote_analyst)

+- `runDeepAnalysis()`: builds content once, runs 5 passes sequentially with conversation history building

+- Results displayed in dedicated panel with per-pass sections, progress indicator

+- Each response saved to MongoDB with `deep_analysis_pass` and `deep_analysis_name` metadata

+- 2-second pause between passes to avoid rate limiting

+- "Deep Analysis (5-Pass)" button next to "Send to AI" in thread_view.php

+- `public/thread_view.php` — Deep Analysis button+container+JS, context builder enhancements

+- `public/assets/app.js` — pagination fix, deleteFoundThreads(), bulk delete button visibility

+- `public/api.php` — `delete_threads_bulk` endpoint

+- `src/QueryAdapter.php` — thread search `$or` on subject/from/to


Done. Here's what's been built:

## Deep Analysis (5-Pass Automated)

A new **"Deep Analysis (5-Pass)"** button sits next to "Send to AI" in thread_view.php (gradient purple/blue).

**How it works:**
1. Click the button — it confirms the 5 passes with you
2. Builds email content **once** (including all context threads)
3. Runs 5 passes **sequentially**, each using the right AI agent:

| Pass | Agent | Analysis |
|------|-------|----------|
| 1 | General | Business relationship timeline, milestones, turning points, mistakes |
| 2 | Deception | Pressure tactics, broken promises, blame shifting, escalation patterns |
| 3 | Profiler | Psychological profiles of each person, CEO focus, approach strategies |
| 4 | Quotes | Financial patterns, pricing trends, scope creep, competitor mentions |
| 5 | General | Win-back strategy — who, when, how, what to offer, red lines |

**Key features:**
- Conversation history **builds up** across passes — Pass 5 sees all previous answers
- Results displayed in a dedicated panel, each pass in its own collapsible section
- Every response **auto-saved** to MongoDB
- Progress indicator shows which pass is running
- Uses whichever providers/models you've selected
- 2-second pause between passes to avoid rate limiting
- Works with context builder — add all 296 Delta threads, then hit Deep Analysis

**To use for Delta Engineering:** Open a key thread → Context Builder → "Add All" delta-engineering.be threads → select Gemini 2.5 Pro + Claude Opus 4.6 → set timeout to 300s → click "Deep Analysis (5-Pass)".


I've run it one time, worked, displayed content, but after leaving that page how can I load it again?


Good point — the Deep Analysis results are only in-memory during the session. But each pass response was saved to MongoDB via `saveAIResponse()`. Let me check how saved responses are currently loaded to make sure the deep analysis ones show up there.


110:$savedResponsesLimit = 5; // Initial load limit
111:$savedResponses = $mongo->findAll('ai_query_responses',
113:    ['sort' => ['created_at' => -1], 'limit' => $savedResponsesLimit]
117:$totalSavedResponses = $mongo->count('ai_query_responses', ['thread_ids' => $threadId]);
1223:                <?php if (!empty($savedResponses)): ?>
1229:                        <?php foreach ($savedResponses as $saved):
1286:                    <?php if ($totalSavedResponses > $savedResponsesLimit): ?>
1289:                                data-offset="<?= $savedResponsesLimit ?>"
1291:                            Load More (<?= $totalSavedResponses - $savedResponsesLimit ?> remaining)

[Showing results with pagination = limit: 15, offset: 0]


   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

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


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

  1225→

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

  1227→

  1228→

  1229→                        <?php foreach ($savedResponses as $saved):
  1230→                            // Determine what to display as the "question"
  1231→                            $displayQuestion = $saved['question'] ?? 'General Analysis';
  1232→                            $hasContext = !empty($saved['context']);
  1233→                            $isDefaultQuestion = strpos($displayQuestion, 'Please analyze this email conversation') === 0;
  1234→
  1235→                            // If question is the default and user provided context, show context instead
  1236→                            if ($isDefaultQuestion && $hasContext) {
  1237→                                $displayQuestion = '[Context] ' . $saved['context'];
  1238→                            } elseif ($hasContext && !$isDefaultQuestion) {
  1239→                                // If both custom question and context exist, show question + indicator
  1240→                                $displayQuestion = $displayQuestion . ' (+context)';
  1241→                            }
  1242→
  1243→                            // Truncate to 200 chars instead of 100
  1244→                            $displayQuestion = strlen($displayQuestion) > 200
  1245→                                ? substr($displayQuestion, 0, 200) . '...'
  1246→                                : $displayQuestion;
  1247→
  1248→                            // Get response excerpt for easier identification
  1249→                            $responseText = $saved['response']['response'] ?? '';
  1250→                            $responseExcerpt = strlen($responseText) > 120
  1251→                                ? substr($responseText, 0, 120) . '...'
  1252→                                : $responseText;
  1253→                        ?>
  1254→                        <div class="saved-response" id="saved-response-<?= $saved['_id'] ?>" data-id="<?= $saved['_id'] ?>">
  1255→

  1256→
  1257→                                    <?= htmlspecialchars($displayQuestion) ?>
  1258→
  1259→
  1260→                                    <?= date('M j, Y g:i A', strtotime($saved['created_at'] ?? 'now')) ?>
  1261→
  1262→

  1263→

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

  1267→                            <?php if ($responseExcerpt): ?>
  1268→

  1269→                                <?= htmlspecialchars($responseExcerpt) ?>
  1270→

  1271→                            <?php endif; ?>
  1272→

  1273→                                <button class="btn-view" onclick="event.stopPropagation(); quickViewResponse('<?= $saved['_id'] ?>')">
  1274→                                    👁 Quick View
  1275→                                </button>
  1276→                                <button class="btn-expand" onclick="event.stopPropagation(); loadSavedResponse('<?= $saved['_id'] ?>')">
  1277→                                    📄 Full View
  1278→                                </button>
  1279→                                <button style="color:#ef4444;background:none;border:1px solid #ef4444;padding:4px 8px;border-radius:4px;cursor:pointer;font-size:0.85em;" onclick="event.stopPropagation(); deleteAiResponse('<?= $saved['_id'] ?>')">
  1280→                                    Del
  1281→                                </button>
  1282→

  1283→

  1284→                        <?php endforeach; ?>
  1285→

  1286→                    <?php if ($totalSavedResponses > $savedResponsesLimit): ?>
  1287→

  1288→                        <button class="btn btn-secondary" id="load-more-btn" onclick="loadMoreResponses()"
  1289→                                data-offset="<?= $savedResponsesLimit ?>"
  1290→                                data-total="<?= $totalSavedResponses ?>">
  1291→                            Load More (<?= $totalSavedResponses - $savedResponsesLimit ?> remaining)
  1292→                        </button>
  1293→

  1294→                    <?php endif; ?>
  1295→

  1296→                <?php endif; ?>
  1297→

  1298→
  1299→
  1300→

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

<?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>
                                <button style="color:#ef4444;background:none;border:1px solid #ef4444;padding:4px 8px;border-radius:4px;cursor:pointer;font-size:0.85em;" onclick="event.stopPropagation(); deleteAiResponse('<?= $saved['_id'] ?>')">
                                    Del
                                </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)): ?>


The saved responses already show up in the "Previous AI Analyses" section — but with 5 per pass x possibly 2 providers = 10 entries, and the default limit is 5, the user would need to click "Load More". Also, there's no visual grouping for deep analysis passes.

Let me enhance this: add a **"Deep Analysis" badge** to the saved responses that came from deep analysis, and also add a **"Load Deep Analysis"** button that reconstitutes the full 5-pass view from saved data.


1254→                        <div class="saved-response" id="saved-response-<?= $saved['_id'] ?>" data-id="<?= $saved['_id'] ?>">
  1255→

  1256→
  1257→                                    <?= htmlspecialchars($displayQuestion) ?>
  1258→
  1259→
  1260→                                    <?= date('M j, Y g:i A', strtotime($saved['created_at'] ?? 'now')) ?>
  1261→
  1262→

  1263→

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

  1267→                            <?php if ($responseExcerpt): ?>
  1268→

  1269→                                <?= htmlspecialchars($responseExcerpt) ?>
  1270→

  1271→                            <?php endif; ?>
  1272→

  1273→                                <button class="btn-view" onclick="event.stopPropagation(); quickViewResponse('<?= $saved['_id'] ?>')">
  1274→                                    👁 Quick View
  1275→                                </button>
  1276→                                <button class="btn-expand" onclick="event.stopPropagation(); loadSavedResponse('<?= $saved['_id'] ?>')">
  1277→                                    📄 Full View
  1278→                                </button>
  1279→                                <button style="color:#ef4444;background:none;border:1px solid #ef4444;padding:4px 8px;border-radius:4px;cursor:pointer;font-size:0.85em;" onclick="event.stopPropagation(); deleteAiResponse('<?= $saved['_id'] ?>')">
  1280→                                    Del
  1281→                                </button>
  1282→

  1283→

<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>
                                <button style="color:#ef4444;background:none;border:1px solid #ef4444;padding:4px 8px;border-radius:4px;cursor:pointer;font-size:0.85em;" onclick="event.stopPropagation(); deleteAiResponse('<?= $saved['_id'] ?>')">
                                    Del
                                </button>


Now let me check the `saveAIResponse` function to see what fields get stored, and also check the `loadMoreResponses` and `quickViewResponse` functions to understand the full loading flow:


  3584→                        addToConversation(
  3585→                            actualQuestion,
  3586→                            firstSuccessfulResponse.response,
  3587→                            firstSuccessfulProvider,
  3588→                            modelsMap[firstSuccessfulProvider],
  3589→                            context
  3590→                        );
  3591→                    }
  3592→
  3593→                    // Clear the question field for next question
  3594→                    document.getElementById('ai-question').value = '';
  3595→                    updateTokenEstimate();
  3596→
  3597→                    // Store Round 1 info for potential Round 2 / follow-ups
  3598→                    lastRound1Providers = selectedProvidersList.slice();
  3599→                    lastRound1Models = Object.assign({}, modelsMap);
  3600→                    lastRound1Question = actualQuestion;
  3601→                    lastFullContent = fullContent;
  3602→
  3603→                    // Display Round 1 response
  3604→                    displayAIResponse(data, 'Round 1');
  3605→
  3606→                    // Check if Two-Round mode is enabled → auto-trigger Round 2
  3607→                    var twoRoundCheckbox = document.getElementById('two-round-mode');
  3608→                    if (twoRoundCheckbox && twoRoundCheckbox.checked) {
  3609→                        // Count successful responses
  3610→                        var r1SuccessCount = 0;
  3611→                        for (var rk in lastRound1Responses) {
  3612→                            if (lastRound1Responses[rk] && lastRound1Responses[rk].success) r1SuccessCount++;
  3613→                        }
  3614→                        if (r1SuccessCount > 0) {
  3615→                            // Keep loading overlay active — sendRound2 will manage it
  3616→                            document.getElementById('loading-message').textContent = 'Round 1 complete (' + r1SuccessCount + ' responses). Starting Round 2 synthesis...';
  3617→                            sendRound2(lastRound1Responses, lastRound1Providers, lastRound1Models, lastRound1Question);
  3618→                            return; // Don't hide overlay — sendRound2's finally will do it
  3619→                        }
  3620→                    }
  3621→                } else {
  3622→                    alert('AI request failed: ' + (data.error || 'Unknown error'));
  3623→                }

                        addToConversation(
                            actualQuestion,
                            firstSuccessfulResponse.response,
                            firstSuccessfulProvider,
                            modelsMap[firstSuccessfulProvider],
                            context
                        );
                    }

                    // Clear the question field for next question
                    document.getElementById('ai-question').value = '';
                    updateTokenEstimate();

                    // Store Round 1 info for potential Round 2 / follow-ups
                    lastRound1Providers = selectedProvidersList.slice();
                    lastRound1Models = Object.assign({}, modelsMap);
                    lastRound1Question = actualQuestion;
                    lastFullContent = fullContent;

                    // Display Round 1 response
                    displayAIResponse(data, 'Round 1');

                    // Check if Two-Round mode is enabled → auto-trigger Round 2
                    var twoRoundCheckbox = document.getElementById('two-round-mode');
                    if (twoRoundCheckbox && twoRoundCheckbox.checked) {
                        // Count successful responses
                        var r1SuccessCount = 0;
                        for (var rk in lastRound1Responses) {
                            if (lastRound1Responses[rk] && lastRound1Responses[rk].success) r1SuccessCount++;
                        }
                        if (r1SuccessCount > 0) {
                            // Keep loading overlay active — sendRound2 will manage it
                            document.getElementById('loading-message').textContent = 'Round 1 complete (' + r1SuccessCount + ' responses). Starting Round 2 synthesis...';
                            sendRound2(lastRound1Responses, lastRound1Providers, lastRound1Models, lastRound1Question);
                            return; // Don't hide overlay — sendRound2's finally will do it
                        }
                    }
                } else {
                    alert('AI request failed: ' + (data.error || 'Unknown error'));
                }


Now let me check the API endpoint that loads more responses (used by the JS `loadMoreResponses` function):


3883:        async function quickViewResponse(responseId) {
3966:        async function loadMoreResponses() {


3966→        async function loadMoreResponses() {
  3967→            const btn = document.getElementById('load-more-btn');
  3968→            if (!btn) return;
  3969→
  3970→            const currentOffset = parseInt(btn.dataset.offset) || 0;
  3971→            const total = parseInt(btn.dataset.total) || 0;
  3972→            const limit = 5; // Load 5 more at a time
  3973→
  3974→            btn.disabled = true;
  3975→            btn.textContent = 'Loading...';
  3976→
  3977→            try {
  3978→                const response = await fetch(`api.php?action=list_ai_responses&thread_id=${currentThreadId}&limit=${limit}&offset=${currentOffset}`);
  3979→                const data = await response.json();
  3980→
  3981→                if (data.success && data.responses && data.responses.length > 0) {
  3982→                    const container = document.getElementById('saved-responses');
  3983→
  3984→                    // Append new responses
  3985→                    data.responses.forEach(saved => {
  3986→                        // Determine what to display as the "question"
  3987→                        let displayQuestion = saved.question || 'General Analysis';
  3988→                        const hasContext = saved.context && saved.context.length > 0;
  3989→                        const isDefaultQuestion = displayQuestion.startsWith('Please analyze this email conversation');
  3990→
  3991→                        if (isDefaultQuestion && hasContext) {
  3992→                            displayQuestion = '[Context] ' + saved.context;
  3993→                        } else if (hasContext) {
  3994→                            displayQuestion = displayQuestion + ' (+context)';
  3995→                        }
  3996→
  3997→                        // Truncate to 200 chars
  3998→                        if (displayQuestion.length > 200) {
  3999→                            displayQuestion = displayQuestion.substring(0, 200) + '...';
  4000→                        }
  4001→
  4002→                        // Get response excerpt
  4003→                        const responseText = saved.response?.response || '';
  4004→                        let responseExcerpt = responseText.length > 120
  4005→                            ? responseText.substring(0, 120) + '...'
  4006→                            : responseText;
  4007→
  4008→                        const responseEl = document.createElement('div');
  4009→                        responseEl.className = 'saved-response';
  4010→                        responseEl.id = 'saved-response-' + saved._id;
  4011→                        responseEl.dataset.id = saved._id;
  4012→                        responseEl.innerHTML = `
  4013→

  4014→                                ${escapeHtml(displayQuestion)}
  4015→                                ${saved.created_at || 'Unknown date'}
  4016→

  4017→

  4018→                                ${escapeHtml(saved.provider || 'Unknown')} /
  4019→                                ${escapeHtml(saved.model || 'Unknown')}
  4020→

  4021→                            ${responseExcerpt ? `<div class="saved-response-excerpt">${escapeHtml(responseExcerpt)}</div>` : ''}
  4022→

  4023→                                <button class="btn-view" onclick="event.stopPropagation(); quickViewResponse('${saved._id}')">
  4024→                                    👁 Quick View
  4025→                                </button>

async function loadMoreResponses() {
            const btn = document.getElementById('load-more-btn');
            if (!btn) return;

            const currentOffset = parseInt(btn.dataset.offset) || 0;
            const total = parseInt(btn.dataset.total) || 0;
            const limit = 5; // Load 5 more at a time

            btn.disabled = true;
            btn.textContent = 'Loading...';

            try {
                const response = await fetch(`api.php?action=list_ai_responses&thread_id=${currentThreadId}&limit=${limit}&offset=${currentOffset}`);
                const data = await response.json();

                if (data.success && data.responses && data.responses.length > 0) {
                    const container = document.getElementById('saved-responses');

                    // Append new responses
                    data.responses.forEach(saved => {
                        // Determine what to display as the "question"
                        let displayQuestion = saved.question || 'General Analysis';
                        const hasContext = saved.context && saved.context.length > 0;
                        const isDefaultQuestion = displayQuestion.startsWith('Please analyze this email conversation');

                        if (isDefaultQuestion && hasContext) {
                            displayQuestion = '[Context] ' + saved.context;
                        } else if (hasContext) {
                            displayQuestion = displayQuestion + ' (+context)';
                        }

                        // Truncate to 200 chars
                        if (displayQuestion.length > 200) {
                            displayQuestion = displayQuestion.substring(0, 200) + '...';
                        }

                        // Get response excerpt
                        const responseText = saved.response?.response || '';
                        let responseExcerpt = responseText.length > 120
                            ? responseText.substring(0, 120) + '...'
                            : responseText;

                        const responseEl = document.createElement('div');
                        responseEl.className = 'saved-response';
                        responseEl.id = 'saved-response-' + saved._id;
                        responseEl.dataset.id = saved._id;
                        responseEl.innerHTML = `

                                ${escapeHtml(displayQuestion)}
                                ${saved.created_at || 'Unknown date'}

                                ${escapeHtml(saved.provider || 'Unknown')} /
                                ${escapeHtml(saved.model || 'Unknown')}

                            ${responseExcerpt ? `<div class="saved-response-excerpt">${escapeHtml(responseExcerpt)}</div>` : ''}

                                <button class="btn-view" onclick="event.stopPropagation(); quickViewResponse('${saved._id}')">
                                    👁 Quick View
                                </button>


  4025→                                </button>
  4026→                                <button class="btn-expand" onclick="event.stopPropagation(); loadSavedResponse('${saved._id}')">
  4027→                                    📄 Full View
  4028→                                </button>
  4029→                                <button style="color:#ef4444;background:none;border:1px solid #ef4444;padding:4px 8px;border-radius:4px;cursor:pointer;font-size:0.85em;" onclick="event.stopPropagation(); deleteAiResponse('${saved._id}')">
  4030→                                    Del
  4031→                                </button>
  4032→                            </div>
  4033→                        `;
  4034→
  4035→                        container.appendChild(responseEl);
  4036→                    });
  4037→
  4038→                    // Update offset
  4039→                    const newOffset = currentOffset + data.responses.length;
  4040→                    btn.dataset.offset = newOffset;
  4041→
  4042→                    // Update button text or hide if no more
  4043→                    const remaining = total - newOffset;
  4044→                    if (remaining > 0) {
  4045→                        btn.textContent = `Load More (${remaining} remaining)`;
  4046→                        btn.disabled = false;
  4047→                    } else {
  4048→                        btn.parentElement.style.display = 'none';
  4049→                    }
  4050→                } else {
  4051→                    btn.parentElement.style.display = 'none';
  4052→                }
  4053→            } catch (error) {
  4054→                console.error('Failed to load more responses:', error);

                                </button>
                                <button class="btn-expand" onclick="event.stopPropagation(); loadSavedResponse('${saved._id}')">
                                    📄 Full View
                                </button>
                                <button style="color:#ef4444;background:none;border:1px solid #ef4444;padding:4px 8px;border-radius:4px;cursor:pointer;font-size:0.85em;" onclick="event.stopPropagation(); deleteAiResponse('${saved._id}')">
                                    Del
                                </button>
                            </div>
                        `;

                        container.appendChild(responseEl);
                    });

                    // Update offset
                    const newOffset = currentOffset + data.responses.length;
                    btn.dataset.offset = newOffset;

                    // Update button text or hide if no more
                    const remaining = total - newOffset;
                    if (remaining > 0) {
                        btn.textContent = `Load More (${remaining} remaining)`;
                        btn.disabled = false;
                    } else {
                        btn.parentElement.style.display = 'none';
                    }
                } else {
                    btn.parentElement.style.display = 'none';
                }
            } catch (error) {
                console.error('Failed to load more responses:', error);


Now let me check what the `list_ai_responses` API endpoint returns:


4887:        case 'list_ai_responses':
4888-            $threadId = $_GET['thread_id'] ?? '';
4889-            $contactId = isset($_GET['contact_id']) ? (int)$_GET['contact_id'] : null;
4890-            $limit = min(50, max(1, (int)($_GET['limit'] ?? 20)));
4891-            $offset = max(0, (int)($_GET['offset'] ?? 0));
4892-
4893-            $mongo = \EmailIntelligence\MongoDatabase::getInstance();
4894-            if (!$mongo->isAvailable()) {
4895-                jsonError('MongoDB not available');
4896-            }
4897-
4898-            $filter = [];
4899-            if ($threadId) {
4900-                $threadId = resolveThreadId($threadId);
4901-                $filter['thread_ids'] = $threadId;
4902-            }
4903-            if ($contactId) {
4904-                $filter['contact_id'] = $contactId;
4905-            }
4906-
4907-            $responses = $mongo->findAll('ai_query_responses', $filter, [


  4907→            $responses = $mongo->findAll('ai_query_responses', $filter, [
  4908→                'sort' => ['created_at' => -1],
  4909→                'limit' => $limit,
  4910→                'skip' => $offset
  4911→            ]);
  4912→
  4913→            // Get total count for pagination
  4914→            $total = $mongo->count('ai_query_responses', $filter);
  4915→
  4916→            jsonResponse([
  4917→                'success' => true,
  4918→                'responses' => $responses,
  4919→                'count' => count($responses),
  4920→                'total' => $total,
  4921→                'offset' => $offset,

            $responses = $mongo->findAll('ai_query_responses', $filter, [
                'sort' => ['created_at' => -1],
                'limit' => $limit,
                'skip' => $offset
            ]);

            // Get total count for pagination
            $total = $mongo->count('ai_query_responses', $filter);

            jsonResponse([
                'success' => true,
                'responses' => $responses,
                'count' => count($responses),
                'total' => $total,
                'offset' => $offset,


Good — the API already returns all fields from MongoDB including `deep_analysis_pass` and `deep_analysis_name`. Now I need to:

1. Add a new API endpoint to load deep analysis responses grouped by pass
2. Add a "Load Last Deep Analysis" button in the PHP-rendered section
3. Add a JS function that fetches all deep analysis responses and re-renders the same panel

Let me implement this:


4887:        case 'list_ai_responses':

4887:        case 'list_ai_responses':


  4920→                'total' => $total,
  4921→                'offset' => $offset,
  4922→                'limit' => $limit
  4923→            ]);
  4924→            break;
  4925→
  4926→
  4927→        // ==========================================
  4928→        // Product Search - Start a new product search
  4929→        // ==========================================

                'total' => $total,
                'offset' => $offset,
                'limit' => $limit
            ]);
            break;

        // ==========================================
        // Product Search - Start a new product search
        // ==========================================


Add a new API endpoint right after `list_ai_responses`:


        // ==========================================
        // Load Deep Analysis responses for a thread
        // ==========================================
        case 'load_deep_analysis':
            $threadId = $_GET['thread_id'] ?? '';
            if (!$threadId) jsonError('thread_id required');
            $threadId = resolveThreadId($threadId);

            $mongo = \EmailIntelligence\MongoDatabase::getInstance();

            // Find all responses with deep_analysis_pass field for this thread
            $responses = $mongo->findAll('ai_query_responses', [
                'thread_ids' => $threadId,
                'deep_analysis_pass' => ['$exists' => true]
            ], [
                'sort' => ['deep_analysis_pass' => 1, 'created_at' => -1]
            ]);

            // Group by pass
            $passes = [];
            foreach ($responses as $r) {
                $passNum = $r['deep_analysis_pass'] ?? 0;
                if (!isset($passes[$passNum])) {
                    $passes[$passNum] = [
                        'pass' => $passNum,
                        'name' => $r['deep_analysis_name'] ?? 'Pass ' . $passNum,
                        'agent' => $r['agent'] ?? 'general',
                        'responses' => []
                    ];
                }
                $passes[$passNum]['responses'][] = [
                    '_id' => (string)$r['_id'],
                    'provider' => $r['provider'] ?? '',
                    'model' => $r['model'] ?? '',
                    'response' => $r['response']['response'] ?? '',
                    'cost' => $r['response']['cost'] ?? 0,
                    'request_time' => $r['response']['request_time'] ?? 0,
                    'created_at' => $r['created_at'] ?? ''
                ];
            }

            // Re-index to sequential array
            ksort($passes);
            $passes = array_values($passes);

            jsonResponse([
                'success' => true,
                'passes' => $passes,
                'total_responses' => count($responses)
            ]);
            break;

        // ==========================================
        // Product Search - Start a new product search
        // ==========================================