# WHMCS AI Assistant - Implementation Plan (v3.0)

Xây dựng hệ thống hỗ trợ ticket WHMCS dựa trên AI, gồm 3 thành phần:
**Backend Node.js** (local server) + **Chrome Extension MV3** + **UI Panel React**.
Model AI sử dụng: **Qwen3-max** qua Alibaba DashScope (OpenAI-compatible endpoint).

## User Review Required

> [!IMPORTANT]
> **Thay đổi Tech Stack quan trọng:**  
> Plan gốc đề xuất dùng **WXT + React + TypeScript + shadcn/ui** nhưng codebase hiện tại là **Vanilla JS + Express + Next.js iframe**. Để đảm bảo continuity và tránh rebuild hoàn toàn, tôi đề xuất:
> - **Giữ nguyên Extension**: Vanilla JS (content.js + background.js + popup)
> - **UI Panel**: React (Vite) - served qua Node.js server (như hiện tại)
> - **Backend**: Node.js + Express (như hiện tại)

> [!WARNING]
> **Bảo mật API Key:**  
> `OPENAI_API_KEY` không được nhúng vào extension. Key **chỉ lưu trong `.env`** của backend server. Extension giao tiếp với server local (`http://127.0.0.1:3000`) - đây là thiết kế đúng và cần giữ nguyên.

> [!CAUTION]
> **Light Mode:**  
> Toàn bộ UI Panel sẽ dùng **Light Mode** (trắng/xám nhạt) theo yêu cầu. Plan gốc có dark mode elements cần loại bỏ.

---

## Đề xuất cải thiện so với Plan gốc

| # | Vấn đề trong Plan gốc | Đề xuất cải thiện |
|---|---|---|
| 1 | WXT/TypeScript quá phức tạp cho dự án nhỏ | Giữ Vanilla JS extension, dễ maintain |
| 2 | IndexedDB (Dexie.js) cho history lớn | Chưa cần thiết - dùng `chrome.storage.local` đủ cho MVP |
| 3 | Rate limiting chưa có trong server | Thêm in-memory rate limiter (token bucket) |
| 4 | Không có health check endpoint | Thêm `GET /api/health` để extension verify server |
| 5 | Auto-mode không có undo | Thêm "Undo" trong 30s sau auto-action (Activity Log) |
| 6 | UI không responsive | Sidebar co giãn theo màn hình, min-width 400px |
| 7 | Không xử lý server offline | Extension hiển thị warning khi server không sẵn sàng |
| 8 | Dark mode trong plan gốc | **Light mode** theo yêu cầu mới |
| 9 | scan viewticket.php chưa được thêm vào manifest | Thêm viewticket.php vào `content_scripts.matches` |
| 10 | Kết quả AI chỉ hiển thị trong sidebar | **Inject badge trực tiếp vào dòng ticket** trong WHMCS DOM |
| 11 | Manual/Auto mode chưa có luồng xử lý rõ ràng | Tách rõ 2 luồng: Manual (badge + nút bấm) vs Auto (tự động đổi status) |

---

## Cấu trúc thư mục đề xuất

```
nin_assistant_ticket/
├── server/                    # [NEW] Backend Node.js
│   ├── .env                   # [NEW] Environment variables
│   ├── package.json           # [NEW]
│   ├── server.js              # [NEW] Express entry point
│   ├── src/
│   │   ├── ai.js              # [NEW] Qwen3-max AI service
│   │   ├── routes/
│   │   │   ├── process.js     # [NEW] /api/process route
│   │   │   └── health.js      # [NEW] /api/health route
│   │   └── middleware/
│   │       └── cors.js        # [NEW] CORS config
│   └── panel/                 # [NEW] Vite React UI
│       ├── package.json
│       ├── src/
│       │   ├── App.jsx
│       │   ├── components/
│       │   │   ├── TicketTable.jsx
│       │   │   ├── SettingsTab.jsx
│       │   │   └── ActivityLog.jsx
│       │   └── index.css      # Light mode design system
│       └── dist/              # Built output served by Express
│
└── extension/                 # Chrome Extension (cải tiến)
    ├── manifest.json          # [MODIFY] Thêm background, popup, viewticket
    ├── content.js             # [MODIFY] Thêm server health check
    ├── background.js          # [NEW] Service Worker
    ├── popup.html             # [NEW]
    ├── popup.js               # [NEW]
    └── panel.css              # [MODIFY] Light mode tweaks
```

---

## Proposed Changes

### Component 1: Backend Server

#### [NEW] server/.env
```
OPENAI_BASE_URL=https://dashscope-intl.aliyuncs.com/compatible-mode/v1
OPENAI_API_KEY=sk-04a823302989460bac7e79f0691bfb69
OPENAI_MODEL=qwen3-max
PORT=3000
ALLOWED_ORIGIN=https://staff.vinahost.vn,http://beta.vinahost.vn,https://beta.vinahost.vn,http://localhost:3000,http://127.0.0.1:3000
```

#### [NEW] server/package.json
Dependencies: `express`, `cors`, `openai`, `dotenv`  
Dev: `nodemon`  
Scripts: [start](file:///D:/MyProject/Antigravity/nin_assistant_ticket/extension/content.js#471-477), `dev`

#### [NEW] server/server.js
- Express app khởi động trên `PORT=3000`
- Mount routes: `/api/process`, `/api/health`
- Serve `panel/dist` tại `/`
- CORS middleware từ `ALLOWED_ORIGIN`

#### [NEW] server/src/ai.js
- Tạo OpenAI client với `baseURL` và `apiKey` từ env
- `analyzeTicketList(tickets, config)`: batch phân tích, trả về `{results}`
- `analyzeTicketDetail(ticket, config)`: phân tích 1 ticket, trả về `{intent, reply, confidence}`
- Spam detection: multi-factor score (keywords + patterns + AI)
- Token optimization: chỉ gửi subject + 500 ký tự đầu của message đầu tiên cho list scan

#### [NEW] server/src/routes/process.js
```
POST /api/process
Body: { page: 'list'|'detail', tickets?, ticket?, config }
Response: { results: [...] }
```
- List mode: gọi `analyzeTicketList`
- Detail mode: gọi `analyzeTicketDetail`

---

### Component 2: UI Panel (React + Vite, Light Mode)

#### [NEW] server/panel/src/App.jsx
Layout chính:
- Header: logo VinaHost + nút đóng + status indicator
- Tabs: **Ticket List** (default) | **Settings**
- Tab Ticket List: bảng compact + scan controls
- Tab Settings: config form + sub-tab Activity Log

#### [NEW] server/panel/src/index.css
**Design System — Neo Style, Light Mode:**

| Token | Giá trị | Ghi chú |
|---|---|---|
| `--primary` | `#e38a00` | VinaHost brand |
| `--primary-hover` | `#c97a00` | Hover state |
| `--bg` | `#ffffff` | Nền chính |
| `--bg-subtle` | `#f6f7f9` | Nền phụ, sidebar |
| `--text` | `#1c1c1e` | Text chính |
| `--text-muted` | `#6e6e73` | Label, placeholder |
| `--border` | `#e0e0e5` | Viền card, input |
| `--accent-spam` | `#fff4e0` / `#a05e00` | Nền / chữ badge spam |
| `--accent-support` | `#e8f4fb` / `#1a6a8a` | Nền / chữ badge support |
| `--accent-success` | `#eaf7f0` / `#1e7a4a` | Nền / chữ success |
| `--radius` | `8px` | Bo góc card, button |
| `--radius-sm` | `4px` | Badge, tag |
| `--shadow` | `0 1px 3px rgba(0,0,0,.08), 0 4px 12px rgba(0,0,0,.05)` | Card shadow (Neo) |

**Typography:**
- Font: `Inter` (Google Fonts) — `font-family: 'Inter', sans-serif`
- Không dùng chữ **IN HOA** toàn bộ (`text-transform: uppercase` bị cấm)
- Heading: `font-weight: 700` (bold)
- Label / sub-heading: `font-weight: 600` (semibold)
- Body: `font-weight: 400`
- Badge / tag: `font-weight: 600`

**Neo Style đặc trưng:**
- Card có `border: 1px solid var(--border)` + `box-shadow: var(--shadow)`
- Button primary: nền `#e38a00`, bo `8px`, không uppercase, font semibold
- Input: border `#e0e0e5`, focus ring `#e38a00` (outline 2px)
- Không dùng flat design hoàn toàn — có depth nhẹ qua shadow và border

#### [NEW] server/panel/src/components/TicketTable.jsx
- Bảng compact: ID | Subject | Status | Type | Confidence | Actions
- Row color theo type: spam (vàng nhạt), support (xanh nhạt), notification (xám)
- Badge confidence %
- Nút action: Mark Answered (manual mode)

#### [NEW] server/panel/src/components/SettingsTab.jsx
Settings với các trường:
- `spamSubjects`: danh sách, thêm/xóa
- `safeKeywords`: danh sách
- `confidenceThreshold`: slider 0-100%
- `autoEnabled`: toggle
- `scanIntervalSec`: input số (0 = tắt)
- `maxRowsToScan`: input số
- Sub-tab: Activity Log

---

### Component 3: Chrome Extension

#### [MODIFY] extension/manifest.json
```diff
+ "background": { "service_worker": "background.js" },
+ "action": { "default_popup": "popup.html" },
  "permissions": [
    "storage",
+   "alarms",
+   "notifications"
  ],
  "content_scripts": [{
    "matches": [
      "https://staff.vinahost.vn/ac/admin/supporttickets.php*",
      "https://beta.vinahost.vn/*/admin/supporttickets.php*",
+     "https://staff.vinahost.vn/ac/admin/viewticket.php*",
+     "https://beta.vinahost.vn/*/admin/viewticket.php*"
    ]
  }]
```

#### [NEW] extension/background.js
- `chrome.alarms` keep-alive (MV3)
- Message relay giữa popup và content script
- Lưu mode state

#### [NEW] extension/popup.html + popup.js
- Quick stats: đã quét, spam phát hiện
- Mode toggle: Manual ↔ Auto
- Nút mở UI panel
- Status server (online/offline)

#### [MODIFY] extension/content.js
- Thêm `checkServerHealth()` khi khởi động - hiển thị warning nếu offline
- Cải thiện error message cho users
- Thêm support cho `viewticket.php` (detail page)

**🏷️ DOM Badge Injection (List View)**

Sau khi AI trả kết quả, [content.js](file:///D:/MyProject/Antigravity/nin_assistant_ticket/extension/content.js) inject badge trực tiếp vào **từng dòng `<tr>`** trên trang WHMCS — không phụ thuộc sidebar:

```js
// Sau khi nhận kết quả từ /api/process:
function injectBadgeToRow(ticketId, type, confidence) {
  const row = document.querySelector(
    `tr:has(input[value="${ticketId}"]),
     tr:has(a[href*="id=${ticketId}"])`
  );
  if (!row) return;

  // Xóa badge cũ nếu có
  row.querySelector('.ai-badge')?.remove();

  const badge = document.createElement('span');
  badge.className = 'ai-badge';
  badge.dataset.type = type; // spam | support | notification
  badge.style.cssText = `
    display: inline-block; margin-left: 6px;
    padding: 1px 6px; border-radius: 4px; font-size: 11px;
    font-weight: 600; vertical-align: middle;
  `;

  const config = {
    spam:         { text: '🚫 SPAM',     bg: '#fff3cd', color: '#856404' },
    support:      { text: '💬 Support',   bg: '#d1ecf1', color: '#0c5460' },
    notification: { text: '📢 Notify',   bg: '#f8f9fa', color: '#495057' },
  };
  const { text, bg, color } = config[type] || config.notification;
  badge.textContent = `${text} ${Math.round(confidence * 100)}%`;
  badge.style.background = bg;
  badge.style.color = color;

  // Gắn vào ô Subject (cell chứa link ticket)
  const subjectCell = row.querySelector('td:has(a[href*="id="])');
  if (subjectCell) subjectCell.appendChild(badge);
}
```

**🔀 Manual vs Auto Mode Flow**

```
Ticket Created/Updated
        ↓
Content Script Detects Change
        ↓
Send Ticket to Background
        ↓
Spam Detector Analyzes
        ↓
    Check Mode
        ↓
┌───────┴───────┐
↓               ↓
Manual          Auto
↓               ↓
injectBadge     Check Threshold
+ Button        ↓
↓           Confidence ≥ Threshold?
Wait User       ↓
Action      Yes → Auto Change Status
↓               ↓
User Click      Update Ticket to Answered
"✓ Answered"    ↓
↓               Log Action
Change Status   ↓
to Answered     Show Notification
↓               ↓
Add Note        Refresh UI (row mờ + badge 🤖 Auto)
```

**Manual Mode** — `autoEnabled = false`:
1. `injectBadgeToRow(id, type, confidence)` — badge xuất hiện ngay trong dòng TR
2. `injectActionButton(id)` — thêm nút nhỏ **"✓ Answered"** vào cuối dòng
3. Staff click nút → confirm dialog → gọi [performActionAjax(id, 'Answered')](file:///D:/MyProject/Antigravity/nin_assistant_ticket/extension/content.js#397-436)
4. Sau khi xong: badge chuyển xanh + row mờ đi

**Auto Mode** — `autoEnabled = true`:
1. Check `confidence >= confidenceThreshold`
2. Nếu đúng: tự gọi [performActionAjax(id, 'Answered')](file:///D:/MyProject/Antigravity/nin_assistant_ticket/extension/content.js#397-436) ngay không cần click
3. `injectBadgeToRow()` với style đặc biệt `🤖 Auto` + row mờ
4. Ghi vào Activity Log: `"Auto-marked #ID as Answered (90% confidence)"`
5. UI panel nhận `ADD_LOG` message để hiển thị

**Nút Action trong Manual Mode** (inject vào `<td>` cuối của row):
```js
function injectActionButton(ticketId) {
  const row = document.querySelector(`tr:has(input[value="${ticketId}"])`);
  if (!row || row.querySelector('.ai-action-btn')) return;
  const lastCell = row.querySelector('td:last-child');
  const btn = document.createElement('button');
  btn.className = 'ai-action-btn';
  btn.textContent = '✓ Answered';
  btn.style.cssText = 'font-size:11px;padding:2px 8px;border:1px solid #e38a00;color:#e38a00;background:#fff;border-radius:4px;cursor:pointer;margin-left:4px;';
  btn.onclick = async (e) => {
    e.preventDefault();
    if (confirm(`Đánh dấu ticket #${ticketId} là Answered?`)) {
      await performActionAjax(ticketId, 'Answered');
      btn.remove();
    }
  };
  if (lastCell) lastCell.appendChild(btn);
}
```

---

## Verification Plan

### Automated Tests
Hiện tại không có test suite. Sẽ verify thủ công:

```bash
# 1. Khởi động server
cd server && npm install && npm run dev

# 2. Kiểm tra health endpoint
curl http://localhost:3000/api/health
# Expected: { "status": "ok", "model": "qwen3-max" }

# 3. Test API process (list mode)
curl -X POST http://localhost:3000/api/process \
  -H "Content-Type: application/json" \
  -d '{"page":"list","tickets":[{"id":"123","subject":"Free SEO tools","status":"open"}],"config":{"confidence_threshold":0.8}}'
# Expected: { "results": [{ "id": "123", "type": "spam", "confidence": 0.9 }] }

# 4. Test API process (detail mode)
curl -X POST http://localhost:3000/api/process \
  -H "Content-Type: application/json" \
  -d '{"page":"detail","ticket":{"id":"456","subject":"Cannot login","status":"open","history":[{"role":"customer","text":"I cannot login to my account"}]},"config":{}}'
# Expected: { "results": [{ "intent": "support", "reply": "..." }] }
```

### Manual Verification

1. **Cài extension vào Chrome**:
   - Mở `chrome://extensions` → Load unpacked → chọn thư mục `extension/`
   
2. **Test List View**:
   - Vào `https://staff.vinahost.vn/ac/admin/supporttickets.php`
   - Click icon chatbot → sidebar mở ra với Light Mode
   - Click "Scan" → thấy bảng kết quả với badge spam/support
   
3. **Test Detail View**:
   - Mở một ticket bất kỳ (`viewticket.php?id=...`)
   - Click icon chatbot → thấy tab Ticket với nút "Analyze"
   - Click "Analyze" → AI gợi ý reply được điền vào TinyMCE editor
   
4. **Test Auto-mark spam**:
   - Vào Settings → bật Auto Mode + thêm spam subject test
   - Scan list → ticket khớp subject tự động chuyển "Answered"
   
5. **Test Server offline**:
   - Tắt server → reload trang → thấy warning "Server không khả dụng"
   
6. **Test Light Mode**:
   - Kiểm tra toàn bộ UI: nền trắng, text tối, không có dark background
