# Messages Hub 0.1% Plan

**Priority:** Next after Gamification
**Status:** ✅ COMPLETE (All 7 Phases)
**Created:** Jan 17, 2026
**CC1 Investigation Complete**
**CC2 Build Complete:** Jan 17, 2026

---

## Vision

Transform the Inbox into a **living, connected messaging platform** where:
- **Everything is clickable** - No dead text
- **Hold = Reveal context** - Long-press shows intel
- **Tap = Primary action** - One-tap to call, schedule, save
- **AI assists but doesn't block** - Smart suggestions, not gates

---

## Current State Audit

### What Exists (Good Foundation)

| Component | Location | Status |
|-----------|----------|--------|
| **Inbox Page** | `admin/src/pages/Inbox.jsx` (1167 lines) | ✅ Comprehensive |
| **AI Triage Sections** | `AITriageSection.jsx` | ✅ Groups by intent |
| **Enhanced Conversation Card** | `EnhancedConversationCard.jsx` | ✅ Shows triage, quick actions |
| **Filter Tabs** | `InboxFilterTabs.jsx` | ⚠️ Basic (All/Unread/Needs Reply/Hot) |
| **Intent Classification** | `src/utils/intent.js` | ✅ Pattern-based + buying signals |
| **AI Reply Suggestions** | `POST /api/sms/ai-suggest` | ✅ GPT-4o powered |
| **Template System** | Templates API | ✅ With merge fields |
| **Schedule Send** | SchedulePicker component | ✅ Quick options + custom |
| **Real-time Updates** | `RealtimeContext.jsx` | ✅ Supabase subscriptions |
| **Sound Notifications** | In RealtimeContext | ✅ Beep on new SMS |

### Triage Categories (Already Working)

| Category | Emoji | Priority | Detection |
|----------|-------|----------|-----------|
| `buying_signal` | 🔥 | 100+ | High-intent patterns |
| `callback_request` | ⏰ | 90 | "call me", "ring me" |
| `question` | ❓ | 70 | Question words, `?` |
| `positive` | 👍 | 50 | "yes", "interested", "keen" |
| `not_interested` | ❌ | 10 | "no", "stop", "unsubscribe" |
| `acknowledgment` | 👋 | 0 | Default |

### Data Available But Not Surfaced

| Data | Where | Usage |
|------|-------|-------|
| `metadata.intent` | Communications table | ✅ Used for triage |
| `metadata.buyingSignal.level` | Communications table | ✅ Used for triage |
| `metadata.buyingSignal.score` | Communications table | ⚠️ Not displayed |
| `metadata.buyingSignal.matchedPattern` | Communications table | ❌ Not surfaced |
| Lead score | Leads table | ✅ Shown as badge |
| Lead stage | Leads table | ✅ Shown |
| Conversion probability | Calculated | ⚠️ Only on buying_signal |
| Response time | Can calculate | ❌ Not tracked |

---

## Gap Analysis: Current vs 0.1%

### Critical Gaps

| Gap | Impact | Effort |
|-----|--------|--------|
| No long-press interactions | Can't reveal context without navigating | Medium |
| No entity extraction from messages | Phone numbers, dates are dead text | Medium |
| No "Waiting on Me" filter | Can't see who I owe a reply | Small |
| No response time tracking | No speed badge possible | Small |
| No mini lead card popup | Must tap through to profile | Medium |
| No AI "explain" for messages | Can't get context on confusing replies | Small |

### Nice-to-Have Gaps

| Gap | Impact | Effort |
|-----|--------|--------|
| No hold → context menu | Limited interaction options | Medium |
| No auto-save entities to lead | Manual data entry | Large |
| No gamification integration | No "fastest reply" badge | Small |
| No bulk actions | Can't process multiple at once | Medium |

---

## Technical Research Answers

### 1. Can we add hold/long-press handlers in React?

**YES.** Three approaches:

```jsx
// Approach 1: onTouchStart + onTouchEnd with timer (Mobile)
const [holdTimer, setHoldTimer] = useState(null);

const handleTouchStart = () => {
  setHoldTimer(setTimeout(() => {
    onLongPress();
  }, 500)); // 500ms hold time
};

const handleTouchEnd = () => {
  clearTimeout(holdTimer);
};

// Approach 2: onContextMenu (Right-click on desktop)
const handleContextMenu = (e) => {
  e.preventDefault();
  onLongPress();
};

// Approach 3: use-long-press library (recommended)
// npm install use-long-press
import { useLongPress } from 'use-long-press';

const bind = useLongPress(() => {
  onLongPress();
}, { threshold: 500 });

return <div {...bind()}>Content</div>;
```

**Recommendation:** Create `useLongPress` custom hook, use on message bubbles and contact cards.

### 2. What message data do we already have that's not surfaced?

- `buyingSignal.matchedPattern` - The exact text that triggered the signal
- `buyingSignal.score` - Numeric score (5/15/30)
- Full conversation history per lead
- Template usage stats
- Delivery status per message
- Read timestamps

### 3. Is intent classification already running?

**YES.** In `src/routes/webhooks.js` line 98-108:
- Every inbound SMS gets `classifyIntent()` called
- Result stored in `metadata.intent` and `metadata.buyingSignal`
- Used by `GET /api/sms/conversations/prioritized` for triage

### 4. How do messages currently link to leads?

- `communications.lead_id` → `leads.id` (UUID FK)
- Phone-based lookup if no lead_id
- Leads fetched fresh in `getConversation()` API

### 5. What would break if we restructure the Inbox?

**Low risk** - Current architecture is modular:
- `Inbox.jsx` is the only consumer of inbox components
- Components are already separated (can be enhanced individually)
- API contracts are stable
- Real-time subscriptions are event-based

---

## 0.1% Feature Specifications

### Feature 1: Smart Filters (Quick Win)

**Current:** All | Unread | Needs Reply | Hot Leads

**Upgrade to:**
```
🔥 Hot Replies    - Positive intent + needs response (triage priority 70+)
⏰ Waiting on Me  - They replied, I haven't responded (inbound was last)
📤 Waiting on Them - I sent, no reply yet (outbound was last, >24h ago)
💰 Buying Signals - High intent detected
😤 Needs Rescue   - Negative sentiment, can save?
📬 All           - Everything
```

**Scope:** Small - Modify `InboxFilterTabs.jsx` and `filterConversations()`

**Files to modify:**
- `admin/src/components/inbox/InboxFilterTabs.jsx`

**Changes:**
```jsx
const FILTER_TABS = [
  { key: 'hot', label: 'Hot', icon: Flame, color: 'text-red-600' },
  { key: 'waiting_on_me', label: 'Waiting on Me', icon: Clock, color: 'text-amber-600' },
  { key: 'waiting_on_them', label: 'Waiting on Them', icon: Send, color: 'text-blue-600' },
  { key: 'buying_signals', label: 'Buying Signals', icon: DollarSign, color: 'text-green-600' },
  { key: 'needs_rescue', label: 'Needs Rescue', icon: AlertTriangle, color: 'text-orange-600' },
  { key: 'all', label: 'All', icon: MessageSquare },
];

export function filterConversations(conversations, filter) {
  switch (filter) {
    case 'hot':
      return conversations.filter(c =>
        c.triage?.priority >= 70 && c.needsReply
      );
    case 'waiting_on_me':
      return conversations.filter(c => c.needsReply);
    case 'waiting_on_them':
      const oneDayAgo = Date.now() - 24 * 60 * 60 * 1000;
      return conversations.filter(c =>
        !c.needsReply &&
        new Date(c.lastMessage?.created_at) < oneDayAgo
      );
    case 'buying_signals':
      return conversations.filter(c =>
        c.triage?.category === 'buying_signal'
      );
    case 'needs_rescue':
      return conversations.filter(c =>
        c.triage?.category === 'not_interested' ||
        c.triage?.category === 'negative'
      );
    default:
      return conversations;
  }
}
```

---

### Feature 2: Lead Card Preview (Quick Win)

**What:** Show mini lead card at top of thread view

**Current:** Just name, phone, score badge
**Upgrade:** Full mini card with stats + quick actions

**Scope:** Small - Modify `ConversationView` in `Inbox.jsx`

**New Component:** `admin/src/components/inbox/LeadCardPreview.jsx`

```jsx
export default function LeadCardPreview({ lead, onCall, onSchedule, onViewProfile, onMarkHot, onAddNote }) {
  const name = `${lead.first_name || ''} ${lead.last_name || ''}`.trim() || 'Unknown';
  const isWorker = lead.lead_type === 'worker';

  return (
    <div className="p-4 bg-gradient-to-r from-slate-50 to-white border-b border-slate-200">
      {/* Row 1: Avatar + Name + Score */}
      <div className="flex items-center gap-3 mb-3">
        <div className={`w-12 h-12 rounded-full flex items-center justify-center ${
          isWorker ? 'bg-orange-100' : 'bg-blue-100'
        }`}>
          {isWorker ? <HardHat className="w-6 h-6 text-orange-600" /> : <Briefcase className="w-6 h-6 text-blue-600" />}
        </div>
        <div className="flex-1">
          <h2 className="font-bold text-slate-900">{name}</h2>
          {lead.company && <p className="text-sm text-slate-600">{lead.company}</p>}
        </div>
        <ScoreBadge score={lead.score} large />
      </div>

      {/* Row 2: Quick Stats */}
      <div className="flex items-center gap-4 mb-3 text-sm">
        <span className="text-slate-500">
          <Phone className="w-4 h-4 inline mr-1" />
          {formatPhone(lead.phone)}
        </span>
        {lead.platform_stage && (
          <span className="px-2 py-0.5 bg-purple-100 text-purple-700 rounded-full text-xs">
            {lead.platform_stage}
          </span>
        )}
      </div>

      {/* Row 3: Quick Actions */}
      <div className="flex gap-2">
        <button
          onClick={onCall}
          className="flex-1 flex items-center justify-center gap-2 px-3 py-2 bg-green-600 text-white rounded-lg font-semibold hover:bg-green-700"
        >
          <Phone className="w-4 h-4" />
          Call
        </button>
        <button
          onClick={onSchedule}
          className="flex-1 flex items-center justify-center gap-2 px-3 py-2 bg-slate-100 text-slate-700 rounded-lg font-semibold hover:bg-slate-200"
        >
          <Calendar className="w-4 h-4" />
          Schedule
        </button>
        <button
          onClick={onMarkHot}
          className="px-3 py-2 bg-slate-100 text-slate-700 rounded-lg hover:bg-slate-200"
          title="Mark Hot"
        >
          <Flame className="w-4 h-4" />
        </button>
        <button
          onClick={onAddNote}
          className="px-3 py-2 bg-slate-100 text-slate-700 rounded-lg hover:bg-slate-200"
          title="Add Note"
        >
          <FileText className="w-4 h-4" />
        </button>
        <button
          onClick={onViewProfile}
          className="px-3 py-2 bg-slate-100 text-slate-700 rounded-lg hover:bg-slate-200"
          title="View Profile"
        >
          <ExternalLink className="w-4 h-4" />
        </button>
      </div>
    </div>
  );
}
```

---

### Feature 3: Long-Press Context Menu (Medium)

**What:** Hold on message bubble → context menu with actions

**Menu Options:**
- AI Explain - "What does this mean?"
- Copy Text
- Extract Contact Info (if detected)
- Flag Message
- Mark as Important

**Scope:** Medium - New hook + component

**New Files:**
- `admin/src/hooks/useLongPress.js`
- `admin/src/components/inbox/MessageContextMenu.jsx`

**Hook Implementation:**
```javascript
// admin/src/hooks/useLongPress.js
import { useCallback, useRef, useState } from 'react';

export function useLongPress(callback, options = {}) {
  const { threshold = 500, onStart, onFinish, onCancel } = options;
  const [longPressTriggered, setLongPressTriggered] = useState(false);
  const timeout = useRef();
  const target = useRef();

  const start = useCallback(
    (event) => {
      if (event.target) {
        event.target.addEventListener('touchend', clear, { passive: true });
        target.current = event.target;
      }
      onStart?.(event);

      timeout.current = setTimeout(() => {
        callback(event);
        setLongPressTriggered(true);
      }, threshold);
    },
    [callback, threshold, onStart]
  );

  const clear = useCallback(
    (event, shouldTriggerClick = true) => {
      timeout.current && clearTimeout(timeout.current);
      if (longPressTriggered) {
        onFinish?.(event);
      } else if (shouldTriggerClick) {
        onCancel?.(event);
      }
      setLongPressTriggered(false);

      if (target.current) {
        target.current.removeEventListener('touchend', clear);
      }
    },
    [longPressTriggered, onFinish, onCancel]
  );

  return {
    onMouseDown: start,
    onMouseUp: clear,
    onMouseLeave: (e) => clear(e, false),
    onTouchStart: start,
    onTouchEnd: clear,
  };
}
```

**Context Menu Component:**
```jsx
// admin/src/components/inbox/MessageContextMenu.jsx
import { useState, useRef, useEffect } from 'react';
import { Sparkles, Copy, Phone, Calendar, Flag, Star } from 'lucide-react';

export default function MessageContextMenu({
  message,
  position,
  onClose,
  onExplain,
  onCopy,
  onExtractPhone,
  onFlag
}) {
  const menuRef = useRef();

  // Close on click outside
  useEffect(() => {
    const handleClick = (e) => {
      if (menuRef.current && !menuRef.current.contains(e.target)) {
        onClose();
      }
    };
    document.addEventListener('mousedown', handleClick);
    return () => document.removeEventListener('mousedown', handleClick);
  }, [onClose]);

  // Detect phone numbers in message
  const phoneRegex = /(\+?61|0)?[\s-]?[4]\d{2}[\s-]?\d{3}[\s-]?\d{3}/g;
  const phones = message.content?.match(phoneRegex) || [];

  const actions = [
    { icon: Sparkles, label: 'AI Explain', action: () => onExplain(message) },
    { icon: Copy, label: 'Copy Text', action: () => onCopy(message.content) },
    ...(phones.length > 0 ? [
      { icon: Phone, label: `Save ${phones[0]}`, action: () => onExtractPhone(phones[0]) }
    ] : []),
    { icon: Flag, label: 'Flag Message', action: () => onFlag(message) },
  ];

  return (
    <div
      ref={menuRef}
      className="fixed z-50 bg-white rounded-xl shadow-xl border border-slate-200 py-1 min-w-48"
      style={{ top: position.y, left: position.x }}
    >
      {actions.map((action, i) => (
        <button
          key={i}
          onClick={() => { action.action(); onClose(); }}
          className="w-full flex items-center gap-3 px-4 py-3 hover:bg-slate-50 transition-colors text-left"
        >
          <action.icon className="w-4 h-4 text-slate-500" />
          <span className="text-sm font-medium text-slate-700">{action.label}</span>
        </button>
      ))}
    </div>
  );
}
```

---

### Feature 4: Entity Detection & Linking (Medium)

**What:** Auto-detect phone numbers, emails, dates, addresses in messages

**Implementation:** Process message content on render, wrap detected entities

**New Component:** `admin/src/components/inbox/SmartMessageContent.jsx`

```jsx
import { Phone, Mail, Calendar, MapPin } from 'lucide-react';

const ENTITY_PATTERNS = {
  phone: /(\+?61|0)?[\s-]?[4]\d{2}[\s-]?\d{3}[\s-]?\d{3}/g,
  email: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
  date: /\b(monday|tuesday|wednesday|thursday|friday|saturday|sunday|tomorrow|today|next week)\b/gi,
  time: /\b([01]?[0-9]|2[0-3]):?([0-5][0-9])?\s?(am|pm)?\b/gi,
};

export default function SmartMessageContent({
  content,
  onPhoneClick,
  onEmailClick,
  onDateClick
}) {
  if (!content) return null;

  // Find all entities with positions
  const entities = [];

  for (const [type, pattern] of Object.entries(ENTITY_PATTERNS)) {
    let match;
    while ((match = pattern.exec(content)) !== null) {
      entities.push({
        type,
        text: match[0],
        start: match.index,
        end: match.index + match[0].length,
      });
    }
  }

  // Sort by position
  entities.sort((a, b) => a.start - b.start);

  // Build content with clickable entities
  if (entities.length === 0) {
    return <span>{content}</span>;
  }

  const parts = [];
  let lastEnd = 0;

  for (const entity of entities) {
    // Add text before entity
    if (entity.start > lastEnd) {
      parts.push(
        <span key={`text-${lastEnd}`}>
          {content.slice(lastEnd, entity.start)}
        </span>
      );
    }

    // Add entity link
    const handlers = {
      phone: () => onPhoneClick?.(entity.text),
      email: () => onEmailClick?.(entity.text),
      date: () => onDateClick?.(entity.text),
      time: () => onDateClick?.(entity.text),
    };

    const icons = {
      phone: Phone,
      email: Mail,
      date: Calendar,
      time: Calendar,
    };

    const Icon = icons[entity.type];

    parts.push(
      <button
        key={`entity-${entity.start}`}
        onClick={handlers[entity.type]}
        className="inline-flex items-center gap-1 px-1.5 py-0.5 bg-blue-50 text-blue-700 rounded font-medium hover:bg-blue-100 transition-colors"
      >
        <Icon className="w-3 h-3" />
        {entity.text}
      </button>
    );

    lastEnd = entity.end;
  }

  // Add remaining text
  if (lastEnd < content.length) {
    parts.push(
      <span key={`text-${lastEnd}`}>
        {content.slice(lastEnd)}
      </span>
    );
  }

  return <>{parts}</>;
}
```

---

### Feature 5: AI Explain Modal (Small)

**What:** Tap "AI Explain" from context menu → modal explains the message in context

**New Component:** `admin/src/components/inbox/AIExplainModal.jsx`

```jsx
import { useState, useEffect } from 'react';
import { X, Sparkles, Loader2 } from 'lucide-react';
import { aiApi } from '../../api/client';

export default function AIExplainModal({ message, lead, onClose }) {
  const [explanation, setExplanation] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    loadExplanation();
  }, [message.id]);

  const loadExplanation = async () => {
    try {
      const result = await aiApi.explainMessage(message.content, lead);
      setExplanation(result);
    } catch (error) {
      setExplanation({ error: 'Failed to analyze message' });
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="fixed inset-0 z-50 bg-black/50 flex items-end sm:items-center justify-center">
      <div className="w-full max-w-md bg-white rounded-t-3xl sm:rounded-2xl overflow-hidden">
        {/* Header */}
        <div className="flex items-center justify-between px-4 py-3 border-b border-slate-200 bg-gradient-to-r from-purple-50 to-blue-50">
          <div className="flex items-center gap-2">
            <Sparkles className="w-5 h-5 text-purple-600" />
            <h3 className="font-bold text-slate-900">AI Analysis</h3>
          </div>
          <button onClick={onClose} className="p-2 hover:bg-white/50 rounded-lg">
            <X className="w-5 h-5 text-slate-500" />
          </button>
        </div>

        {/* Original Message */}
        <div className="px-4 py-3 bg-slate-50 border-b border-slate-100">
          <p className="text-xs text-slate-500 mb-1">Their message:</p>
          <p className="text-sm text-slate-700">"{message.content}"</p>
        </div>

        {/* Explanation */}
        <div className="p-4">
          {loading ? (
            <div className="flex items-center justify-center py-8">
              <Loader2 className="w-6 h-6 animate-spin text-purple-600" />
              <span className="ml-2 text-slate-600">Analyzing...</span>
            </div>
          ) : explanation?.error ? (
            <p className="text-red-500 text-center py-4">{explanation.error}</p>
          ) : (
            <div className="space-y-4">
              {/* Intent */}
              <div>
                <p className="text-xs font-semibold text-slate-500 mb-1">Intent</p>
                <p className="text-sm text-slate-700">{explanation.intent}</p>
              </div>

              {/* Key Points */}
              {explanation.keyPoints?.length > 0 && (
                <div>
                  <p className="text-xs font-semibold text-slate-500 mb-1">Key Points</p>
                  <ul className="text-sm text-slate-700 space-y-1">
                    {explanation.keyPoints.map((point, i) => (
                      <li key={i} className="flex items-start gap-2">
                        <span className="text-purple-500">•</span>
                        {point}
                      </li>
                    ))}
                  </ul>
                </div>
              )}

              {/* Suggested Response */}
              {explanation.suggestedResponse && (
                <div>
                  <p className="text-xs font-semibold text-slate-500 mb-1">Suggested Response</p>
                  <p className="text-sm text-slate-700 bg-purple-50 p-3 rounded-lg">
                    {explanation.suggestedResponse}
                  </p>
                </div>
              )}
            </div>
          )}
        </div>

        {/* Close Button */}
        <div className="p-4 pt-0">
          <button
            onClick={onClose}
            className="w-full py-3 bg-slate-100 text-slate-700 font-semibold rounded-xl hover:bg-slate-200"
          >
            Close
          </button>
        </div>
      </div>
    </div>
  );
}
```

**Backend endpoint needed:**

```javascript
// Add to src/routes/ai.js
router.post('/explain-message', async (req, res) => {
  const { message, leadContext } = req.body;

  try {
    const completion = await openai.chat.completions.create({
      model: 'gpt-4o-mini',
      messages: [
        {
          role: 'system',
          content: `You analyze SMS messages from leads for a sales team.
Given a message, explain:
1. What the person is saying/asking (intent)
2. Key points to note
3. A suggested response

Lead context: ${JSON.stringify(leadContext)}

Return JSON: { intent: string, keyPoints: string[], suggestedResponse: string }`
        },
        { role: 'user', content: `Analyze: "${message}"` }
      ],
      max_tokens: 300,
      temperature: 0.3,
    });

    const result = JSON.parse(completion.choices[0].message.content);
    res.json(result);
  } catch (error) {
    res.status(500).json({ error: 'Failed to analyze' });
  }
});
```

---

### Feature 6: Hold Contact → Mini Lead Popup (Medium)

**What:** Long-press on contact in list → popup with lead summary + quick actions

**New Component:** `admin/src/components/inbox/MiniLeadPopup.jsx`

```jsx
import { Phone, Calendar, ExternalLink, Flame, X } from 'lucide-react';

export default function MiniLeadPopup({
  lead,
  position,
  onClose,
  onCall,
  onSchedule,
  onViewProfile
}) {
  const name = `${lead.first_name || ''} ${lead.last_name || ''}`.trim();
  const tierColor = lead.score >= 70 ? 'text-green-600' :
                    lead.score >= 40 ? 'text-amber-600' : 'text-slate-500';
  const tier = lead.score >= 70 ? 'Hot' : lead.score >= 40 ? 'Warm' : 'Cold';

  return (
    <>
      {/* Backdrop */}
      <div className="fixed inset-0 z-40" onClick={onClose} />

      {/* Popup */}
      <div
        className="fixed z-50 bg-white rounded-xl shadow-2xl border border-slate-200 w-72 overflow-hidden"
        style={{ top: position.y, left: Math.min(position.x, window.innerWidth - 300) }}
      >
        {/* Header */}
        <div className="p-4 bg-gradient-to-r from-slate-50 to-white border-b border-slate-100">
          <div className="flex items-start justify-between">
            <div>
              <h3 className="font-bold text-slate-900">{name || 'Unknown'}</h3>
              {lead.company && <p className="text-sm text-slate-600">{lead.company}</p>}
            </div>
            <button onClick={onClose} className="p-1 hover:bg-slate-100 rounded">
              <X className="w-4 h-4 text-slate-400" />
            </button>
          </div>

          {/* Score + Tier */}
          <div className="flex items-center gap-2 mt-2">
            <span className={`text-2xl font-black ${tierColor}`}>{lead.score}</span>
            <span className={`px-2 py-0.5 rounded-full text-xs font-bold ${
              lead.score >= 70 ? 'bg-green-100 text-green-700' :
              lead.score >= 40 ? 'bg-amber-100 text-amber-700' :
              'bg-slate-100 text-slate-600'
            }`}>
              {tier}
            </span>
          </div>
        </div>

        {/* AI Summary (if available) */}
        {lead.ai_summary && (
          <div className="px-4 py-3 bg-purple-50 border-b border-purple-100">
            <p className="text-xs text-purple-700">{lead.ai_summary}</p>
          </div>
        )}

        {/* Quick Actions */}
        <div className="p-3 flex gap-2">
          <button
            onClick={() => { onCall(); onClose(); }}
            className="flex-1 flex items-center justify-center gap-2 px-3 py-2 bg-green-600 text-white rounded-lg font-semibold text-sm hover:bg-green-700"
          >
            <Phone className="w-4 h-4" />
            Call Now
          </button>
          <button
            onClick={() => { onSchedule(); onClose(); }}
            className="flex-1 flex items-center justify-center gap-2 px-3 py-2 bg-slate-100 text-slate-700 rounded-lg font-semibold text-sm hover:bg-slate-200"
          >
            <Calendar className="w-4 h-4" />
            Schedule
          </button>
          <button
            onClick={() => { onViewProfile(); onClose(); }}
            className="px-3 py-2 bg-slate-100 text-slate-700 rounded-lg hover:bg-slate-200"
          >
            <ExternalLink className="w-4 h-4" />
          </button>
        </div>
      </div>
    </>
  );
}
```

---

### Feature 7: Response Time Tracking (Small)

**What:** Track how long it takes to respond to inbound messages

**Backend Changes:**

```sql
-- Add to communications table
ALTER TABLE communications ADD COLUMN IF NOT EXISTS response_time_seconds INTEGER;

-- When outbound sent, calculate response time from last inbound
```

**In webhook handler or sms.js:**

```javascript
// After sending outbound SMS, calculate response time
const lastInbound = await supabase
  .from('communications')
  .select('created_at')
  .eq('lead_id', leadId)
  .eq('direction', 'inbound')
  .order('created_at', { ascending: false })
  .limit(1)
  .single();

if (lastInbound?.data) {
  const responseTime = Math.floor(
    (Date.now() - new Date(lastInbound.data.created_at)) / 1000
  );

  // Update the outbound message with response time
  await supabase
    .from('communications')
    .update({ response_time_seconds: responseTime })
    .eq('id', outboundId);

  // Track for gamification
  await updateResponseTimeStats(userId, responseTime);
}
```

**Gamification Integration:**

```javascript
// In badgesService.js - add Speed Demon calculation
const avgResponseTime = await supabase
  .from('communications')
  .select('response_time_seconds')
  .eq('user_id', userId)
  .gte('created_at', weekStart.toISOString())
  .not('response_time_seconds', 'is', null);

// Calculate average
const avg = avgResponseTime.data.reduce((a, b) => a + b.response_time_seconds, 0)
  / avgResponseTime.data.length;

// Award Speed Demon badge to fastest average
```

---

### Feature 8: AI Suggestion Improvements (Small)

**Current:** Single suggestion below messages
**Upgrade:** 3 reply options above keyboard

**Modify AiSuggestionBar:**

```jsx
function AiSuggestionBar({ suggestions, onUse, loading }) {
  if (loading) {
    return (
      <div className="flex items-center gap-2 px-4 py-3 bg-purple-50 border-t border-purple-100">
        <Loader2 className="w-4 h-4 animate-spin text-purple-600" />
        <span className="text-sm text-purple-600">Generating suggestions...</span>
      </div>
    );
  }

  if (!suggestions?.length) return null;

  return (
    <div className="px-4 py-3 bg-gradient-to-r from-purple-50 to-blue-50 border-t border-purple-100">
      <p className="text-xs text-purple-600 font-medium mb-2 flex items-center gap-1">
        <Zap className="w-3 h-3" />
        AI Suggestions
      </p>
      <div className="flex gap-2 overflow-x-auto pb-1">
        {suggestions.map((suggestion, i) => (
          <button
            key={i}
            onClick={() => onUse(suggestion.text)}
            className="flex-shrink-0 px-3 py-2 bg-white border border-purple-200 rounded-lg text-sm text-slate-700 hover:bg-purple-50 transition-colors max-w-[200px] text-left"
          >
            <span className="line-clamp-2">{suggestion.text}</span>
            {suggestion.confidence && (
              <span className="block text-xs text-purple-500 mt-1">
                {suggestion.confidence === 'high' ? '✓ Confident' : 'Suggestion'}
              </span>
            )}
          </button>
        ))}
      </div>
    </div>
  );
}
```

**Backend - Return 3 suggestions:**

```javascript
// Modify /api/sms/ai-suggest to return array
res.json({
  suggestions: [
    { text: primarySuggestion, confidence: 'high' },
    { text: alternativeSuggestion, confidence: 'medium' },
    { text: shortSuggestion, confidence: 'medium' },
  ],
  context: { leadName, company, stage, intent, buyingSignal }
});
```

---

### Feature 9: AI Message Writer 0.1% (Medium)

**Current State (Basic):**
- Generic quick options (Follow up, Check in, etc.)
- Empty text area waiting for input
- Single "Generate" button
- No context shown
- No tone control

**0.1% Upgrade:**

**On Modal Open (immediate, no click required):**
1. Lead snapshot header: Score badge, last contact date, "waiting on reply" warning
2. Context summary: AI-generated one-liner of last conversation
3. 3 AI suggestions pre-loaded and ready to send

**Each Suggestion Card:**
- Full message text visible (not truncated)
- **[Send]** button - one tap to send immediately
- **[Edit]** button - loads into compose area for tweaking

**Tone Selector:**
- Three options: `Professional` | `Friendly` | `Urgent`
- Changing tone regenerates the 3 suggestions
- Default based on lead relationship (new = Professional, existing = Friendly)

**Compose Area:**
- Only used if user wants custom message
- Voice input remains
- "Generate" creates 3 new suggestions based on typed prompt

**New Components:**

```jsx
// admin/src/components/inbox/LeadSnapshotHeader.jsx
export default function LeadSnapshotHeader({ lead, lastContact, waitingOnReply }) {
  const tierColor = lead.score >= 70 ? 'text-green-600' :
                    lead.score >= 40 ? 'text-amber-600' : 'text-slate-500';

  return (
    <div className="flex items-center justify-between p-3 bg-slate-50 border-b border-slate-200">
      <div className="flex items-center gap-3">
        <span className={`text-2xl font-black ${tierColor}`}>{lead.score}</span>
        <div>
          <p className="font-semibold text-slate-900">
            {lead.first_name} {lead.last_name}
          </p>
          <p className="text-xs text-slate-500">{lead.company}</p>
        </div>
      </div>
      <div className="text-right">
        <p className="text-xs text-slate-500">Last contact: {lastContact}</p>
        {waitingOnReply && (
          <span className="inline-flex items-center gap-1 px-2 py-0.5 bg-amber-100 text-amber-700 rounded-full text-xs font-medium">
            <Clock className="w-3 h-3" />
            Waiting on reply
          </span>
        )}
      </div>
    </div>
  );
}

// admin/src/components/inbox/ToneSelector.jsx
const TONES = [
  { id: 'professional', label: 'Professional', icon: Briefcase },
  { id: 'friendly', label: 'Friendly', icon: Smile },
  { id: 'urgent', label: 'Urgent', icon: Zap },
];

export default function ToneSelector({ value, onChange }) {
  return (
    <div className="flex gap-2 p-3 border-b border-slate-100">
      {TONES.map((tone) => (
        <button
          key={tone.id}
          onClick={() => onChange(tone.id)}
          className={`flex items-center gap-1.5 px-3 py-1.5 rounded-full text-sm font-medium transition-colors ${
            value === tone.id
              ? 'bg-purple-600 text-white'
              : 'bg-slate-100 text-slate-600 hover:bg-slate-200'
          }`}
        >
          <tone.icon className="w-3.5 h-3.5" />
          {tone.label}
        </button>
      ))}
    </div>
  );
}

// admin/src/components/inbox/SuggestionCard.jsx
export default function SuggestionCard({ suggestion, onSend, onEdit, sending }) {
  return (
    <div className="p-4 bg-white border border-slate-200 rounded-xl hover:border-purple-200 transition-colors">
      <p className="text-sm text-slate-700 mb-3 whitespace-pre-wrap">
        {suggestion.text}
      </p>
      <div className="flex gap-2">
        <button
          onClick={() => onSend(suggestion.text)}
          disabled={sending}
          className="flex-1 flex items-center justify-center gap-2 px-4 py-2 bg-green-600 text-white rounded-lg font-semibold text-sm hover:bg-green-700 disabled:opacity-50"
        >
          {sending ? <Loader2 className="w-4 h-4 animate-spin" /> : <Send className="w-4 h-4" />}
          Send
        </button>
        <button
          onClick={() => onEdit(suggestion.text)}
          className="px-4 py-2 bg-slate-100 text-slate-700 rounded-lg font-semibold text-sm hover:bg-slate-200"
        >
          Edit
        </button>
      </div>
    </div>
  );
}
```

**API Changes:**

```javascript
// Modify: POST /api/ai/draft-message
router.post('/draft-message', async (req, res) => {
  const { leadId, prompt, tone = 'friendly', count = 3, includeContext = true } = req.body;

  // Get lead and conversation context
  const lead = await getLeadWithContext(leadId);
  const lastMessages = await getLastMessages(leadId, 5);

  // Generate context summary
  let contextSummary = null;
  if (includeContext && lastMessages.length > 0) {
    contextSummary = await generateContextSummary(lastMessages);
  }

  // Generate multiple suggestions with specified tone
  const suggestions = await generateSuggestions({
    lead,
    lastMessages,
    prompt,
    tone,
    count,
  });

  // Calculate lead snapshot
  const lastContact = formatTimeAgo(lead.last_contact_at);
  const waitingOnReply = lastMessages[0]?.direction === 'inbound';

  res.json({
    suggestions: suggestions.map((text, i) => ({
      id: i + 1,
      text,
      tone,
    })),
    context: contextSummary,
    leadSnapshot: {
      score: lead.score,
      tier: lead.score >= 70 ? 'hot' : lead.score >= 40 ? 'warm' : 'cold',
      lastContact,
      waitingOnReply,
    },
  });
});
```

**Response Shape:**
```json
{
  "suggestions": [
    { "id": 1, "text": "Hey Michael, just following up on the formworker pricing...", "tone": "friendly" },
    { "id": 2, "text": "Hi Michael, wanted to check if you had any questions about...", "tone": "friendly" },
    { "id": 3, "text": "Michael - circling back on your inquiry. Ready when you are!", "tone": "friendly" }
  ],
  "context": "He asked about pricing for 3 formworkers",
  "leadSnapshot": {
    "score": 85,
    "tier": "hot",
    "lastContact": "2 days ago",
    "waitingOnReply": true
  }
}
```

**Scope:** Medium - Modify existing modal + add 3 components + API changes

---

## Database Migration

```sql
-- messages-hub-migration.sql

-- Response time tracking
ALTER TABLE communications ADD COLUMN IF NOT EXISTS response_time_seconds INTEGER;
CREATE INDEX IF NOT EXISTS idx_comms_response_time ON communications(response_time_seconds) WHERE response_time_seconds IS NOT NULL;

-- Message flags
ALTER TABLE communications ADD COLUMN IF NOT EXISTS flagged BOOLEAN DEFAULT FALSE;
ALTER TABLE communications ADD COLUMN IF NOT EXISTS flagged_at TIMESTAMPTZ;
ALTER TABLE communications ADD COLUMN IF NOT EXISTS flagged_reason VARCHAR(100);

-- Extracted entities (for auto-save feature)
CREATE TABLE IF NOT EXISTS extracted_entities (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  communication_id UUID REFERENCES communications(id),
  lead_id UUID REFERENCES leads(id),
  entity_type VARCHAR(50) NOT NULL,  -- 'phone', 'email', 'date', 'address'
  entity_value TEXT NOT NULL,
  saved_to_lead BOOLEAN DEFAULT FALSE,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_extracted_entities_lead ON extracted_entities(lead_id);
```

---

## Build Sequence for CC2

### Phase 0: Rebrand ✅ COMPLETE

0. ✅ **Rename "Inbox" → "Messages"** everywhere:
   - `admin/src/pages/Inbox.jsx` → renamed to `Messages.jsx`
   - Updated `App.jsx` route and import
   - Updated `BottomNav.jsx` label: "Inbox" → "Messages"
   - Updated page header text: "AI Inbox" → "Messages"

### Phase 1: Quick Wins ✅ COMPLETE

1. ✅ Create migration file, add to PENDING_MIGRATIONS.md
2. ✅ **Smart Filters** - Modified `InboxFilterTabs.jsx` with 6 new filters
3. ✅ **Lead Card Preview** - Created `LeadCardPreview.jsx`, added to `ConversationView`
4. ✅ Build and test filters + preview

### Phase 2: Long-Press & Context ✅ COMPLETE

5. ✅ Create `useLongPress.js` hook
6. ✅ Create `MessageContextMenu.jsx`
7. ✅ Add long-press to `MessageBubble` in `Messages.jsx`
8. ✅ Create `AIExplainModal.jsx`
9. ✅ Add `POST /api/ai/explain-message` endpoint
10. ✅ Build and test context menu + AI explain

### Phase 3: Entity Detection ✅ COMPLETE

11. ✅ Create `SmartMessageContent.jsx`
12. ✅ Replace plain text rendering in `MessageBubble`
13. ✅ Add phone click → call, date click → schedule callback
14. ✅ Build and test entity linking

### Phase 4: Contact Popup ✅ COMPLETE

15. ✅ Create `MiniLeadPopup.jsx`
16. ✅ Add long-press to `EnhancedConversationCard`
17. ✅ Build and test popup

### Phase 5: Response Time & Gamification ✅ COMPLETE

18. ✅ Add response time calculation to `sms.js`
19. ✅ Add Speed Demon badge calculation
20. ✅ Build and test

### Phase 6: AI Suggestions ✅ COMPLETE

21. ✅ Modify `/api/sms/ai-suggest` to return 3 options
22. ✅ Modify `AiSuggestionBar` for multiple suggestions
23. ✅ Build and test suggestions

### Phase 7: AI Message Writer 0.1% ✅ COMPLETE

24. ✅ Modify `POST /api/ai/draft-message` - add tone, count, includeContext params
25. ✅ Create `LeadSnapshotHeader.jsx` (inline in AIMessageWriter)
26. ✅ Create `ToneSelector.jsx` (inline in AIMessageWriter)
27. ✅ Create `SuggestionCard.jsx` (inline in AIMessageWriter, with Send/Edit buttons)
28. ✅ Modify `AIMessageWriter.jsx` - integrate new components, fetch on mount
29. ✅ Test: Open modal → 3 suggestions appear immediately
30. ✅ Test: Tap Send → message sends without extra clicks
31. ✅ Test: Change tone → suggestions regenerate
32. ✅ Build, test, deploy

---

## Files Summary

### CREATE NEW

| File | Purpose |
|------|---------|
| `admin/src/hooks/useLongPress.js` | Long-press gesture hook |
| `admin/src/components/inbox/LeadCardPreview.jsx` | Mini lead card at thread top |
| `admin/src/components/inbox/MessageContextMenu.jsx` | Long-press context menu |
| `admin/src/components/inbox/SmartMessageContent.jsx` | Entity detection & linking |
| `admin/src/components/inbox/AIExplainModal.jsx` | AI message explanation |
| `admin/src/components/inbox/MiniLeadPopup.jsx` | Contact long-press popup |
| `admin/src/components/inbox/LeadSnapshotHeader.jsx` | Lead context in AI writer |
| `admin/src/components/inbox/ToneSelector.jsx` | Professional/Friendly/Urgent toggle |
| `admin/src/components/inbox/SuggestionCard.jsx` | AI suggestion with Send/Edit buttons |
| `supabase/messages-hub-migration.sql` | DB changes |

### MODIFY

| File | Changes |
|------|---------|
| `admin/src/components/inbox/InboxFilterTabs.jsx` | New smart filters |
| `admin/src/pages/Inbox.jsx` → `Messages.jsx` | Rename + add LeadCardPreview, context menu, entity handling |
| `admin/src/components/AIMessageWriter.jsx` | Fetch on open, tone selector, suggestion cards |
| `src/routes/sms.js` | Response time calculation, multi-suggestion |
| `src/routes/ai.js` | Add explain-message endpoint, modify draft-message |
| `src/services/badgesService.js` | Speed Demon badge |

---

## What to Reuse

| Existing | Reuse For |
|----------|-----------|
| `classifyIntent()` in `intent.js` | Keep using for triage |
| `TriageBadge` component | Already works |
| `EnhancedConversationCard` | Add long-press handler |
| `AiSuggestionBar` | Extend for multi-suggestion |
| Real-time subscriptions | Already working |
| Template system | No changes needed |

---

## Quick Wins Summary

| Feature | Effort | Impact | Do First? |
|---------|--------|--------|-----------|
| Rebrand Inbox → Messages | Tiny | Medium | ✅ |
| Smart Filters | Small | High | ✅ |
| Lead Card Preview | Small | High | ✅ |
| AI Explain Modal | Small | Medium | ✅ |
| Response Time Tracking | Small | Medium | ✅ |
| Long-Press Context Menu | Medium | High | ✅ |
| Entity Detection | Medium | High | |
| Mini Lead Popup | Medium | Medium | |
| Multi-Suggestion | Small | Medium | |
| AI Message Writer 0.1% | Medium | High | ✅ |

---

## 0.1% Quality Check

- [x] Would the best CRM do it this way? **Yes - matches Intercom, Front**
- [x] Is there friction? **Reduced - hold reveals context, tap takes action**
- [x] Could AI make it smarter? **Yes - AI explain, multi-suggestion**
- [x] One click when could be zero? **Yes - entity auto-detection**

---

## Risks & Mitigations

| Risk | Mitigation |
|------|------------|
| Long-press conflicts with scroll | Use 500ms threshold, test on mobile |
| AI explain costs | Use gpt-4o-mini, cache responses |
| Entity false positives | Validate patterns, allow dismiss |

---

## Future Enhancements (Post-Launch)

Nice-to-haves for later iteration:

| Feature | Description | Effort |
|---------|-------------|--------|
| Hold Send → Schedule | Long-press send button reveals schedule picker | Small |
| Tone Selector | Professional / Friendly / Urgent modes for AI suggestions | Medium |
| Bulk Actions | Select multiple → mark read, archive, assign | Medium |
| Conversation Search | Full-text search across all messages | Medium |
| Read Receipts UI | Show when messages were read by lead | Small |
| Auto-Archive | Archive conversations after 30 days inactive | Small |

---

## Blockers

None identified. All required infrastructure exists.

---

**Plan ready: docs/messages-hub-plan.md - Ready for CC2**
