# CSAT (Customer Satisfaction Survey) & Channel Prompt Starters Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Provide actionable customer feedback collection (CSAT 1-5 star ratings + optional comments) upon conversation closure, and display quick starter question pills (Prompt Starters) for new visitor chats, with complete multi-locale support and operator visibility.
**Architecture:**
1. **Prompt Starters:** Channel web config (`WebChannelConfig.PromptStarters []string`) is exposed via `GET /api/channel/config`. When a visitor has zero messages, `PromptStarters` renders clickable question pills that automatically dispatch the question.
2. **CSAT Storage & Service:** A persistent `ConversationSatisfaction` model (`conversation_id`, `score`, `feedback`, `customer_id`, `channel_id`, `assignee_id`, `ai_agent_id`) records customer ratings.
3. **API & Realtime:** `POST /api/conversation/satisfaction` securely accepts ratings from the conversation owner. The rating is included in `ConversationDetailResponse` and displayed to operators.
4. **UI:** A modern, accessible `CSATCard` component appears in `/support/chat` when the conversation is closed, allowing star selection (1-5) and feedback submission.
**Tech Stack:** Go 1.26 (GORM, Gin), SQLite/MySQL, Next.js 16, React 19, TypeScript, Tailwind CSS, Lucide icons, i18n (`zh-CN`, `en-US`, `ru-RU`).
**Spec:** Complete end-to-end customer satisfaction and prompt starters system.
## Global Constraints
- Full 3-locale i18n support (`zh-CN`, `en-US`, `ru-RU`).
- All keys must be added symmetrically to `web/messages/zh-CN.json`, `web/messages/en-US.json`, and `web/messages/ru-RU.json`.
- Strict AGENTS.md layer ownership (`models -> repositories -> services -> handlers -> builders/DTOs`).
- Every transactional DB operation must use `ctx.Tx`.
- No raw SQL errors exposed to clients; return localized errors via `errorsx.*I18n`.
- Frontend code must typecheck cleanly (`tsc --noEmit`) and pass all node test suites.
## Review Focus
- Visitor attempting to rate a conversation they do not own (must return forbidden error).
- Submitting duplicate satisfaction ratings for the same conversation (must reject or update idempotently).
- Submitting invalid rating score (e.g. 0, 6, -1 — must enforce 1-5 constraint).
- Channel with empty or absent `PromptStarters` (must gracefully render standard empty state without layout shifts).
- Switching between closed and open conversations (must update CSAT display without stale state leaks).
---
### Task 1: Prompt Starters in Channel Configuration and Widget API
**Files:**
- Modify: `internal/pkg/dto/dto.go:16-24`
- Modify: `internal/pkg/dto/response/widget_response.go:3-12`
- Modify: `internal/handlers/api/channel_handler.go:25-58`
- Test: `internal/handlers/api/channel_handler_test.go`
- Create: `web/components/support-chat/prompt-starters.tsx`
- Modify: `web/components/support-chat/message-list.tsx`
- Modify: `web/lib/api/im.ts`
**Interfaces:**
- Consumes: `Channel.ConfigJSON`, `services.ChannelService.ParseWebChannelConfig`
- Produces:
- `WebChannelConfig.PromptStarters []string`
- `WidgetConfigResponse.PromptStarters []string`
- `` component
- [ ] **Step 1: Write failing test for channel config PromptStarters**
In `internal/handlers/api/channel_handler_test.go`, test that `resolveWidgetConfig` parses `promptStarters` from channel JSON and maps it to `WidgetConfigResponse`.
- [ ] **Step 2: Run test to verify failure**
Run: `& 'C:\Program Files\Go\bin\go.exe' test ./internal/handlers/api -run TestChannelConfigPromptStarters`
Expected: FAIL
- [ ] **Step 3: Implement PromptStarters backend and frontend**
1. In `internal/pkg/dto/dto.go`, add `PromptStarters []string` to `WebChannelConfig`:
```go
PromptStarters []string `json:"promptStarters,omitempty"`
```
2. In `internal/pkg/dto/response/widget_response.go`:
```go
PromptStarters []string `json:"promptStarters,omitempty"`
```
3. In `internal/handlers/api/channel_handler.go`:
- Map `cfg.PromptStarters` in `webLikeWidgetConfig` and `WidgetConfigResponse`.
4. In `web/lib/api/im.ts`:
- Add `promptStarters?: string[]` to `WidgetConfigResponse`.
5. Create `web/components/support-chat/prompt-starters.tsx`:
- Renders a clean grid/flex list of buttons with question icon and hover effects.
- Calls `onSelect(text)` on click.
6. In `web/components/support-chat/message-list.tsx`:
- When `messages.length === 0` and `promptStarters?.length > 0`, render ``.
- [ ] **Step 4: Run test to verify pass**
Run: `& 'C:\Program Files\Go\bin\go.exe' test ./internal/handlers/api -run TestChannelConfigPromptStarters`
Run: `& "web/node_modules/.bin/tsc.cmd" --noEmit --project web/tsconfig.json`
Expected: PASS
- [ ] **Step 5: Commit**
```bash
git add internal/pkg/dto/dto.go internal/pkg/dto/response/widget_response.go internal/handlers/api/channel_handler.go internal/handlers/api/channel_handler_test.go web/components/support-chat/prompt-starters.tsx web/components/support-chat/message-list.tsx web/lib/api/im.ts
git commit -m "feat(channel): support prompt starters for visitor onboarding"
```
---
### Task 2: Conversation Satisfaction (CSAT) Model, Repository & Service
**Files:**
- Modify: `internal/models/models.go:70-80,410-430`
- Create: `internal/repositories/conversation_satisfaction_repository.go`
- Create: `internal/services/conversation_satisfaction_service.go`
- Create: `internal/services/conversation_satisfaction_service_test.go`
**Interfaces:**
- Consumes: `models.Conversation`, `openidentity.ExternalUser`
- Produces:
- `models.ConversationSatisfaction`
- `repositories.ConversationSatisfactionRepository`
- `services.ConversationSatisfactionService.SubmitSatisfaction(conversationID int64, externalUser openidentity.ExternalUser, score int, feedback string) (*models.ConversationSatisfaction, error)`
- `services.ConversationSatisfactionService.GetByConversationID(conversationID int64) *models.ConversationSatisfaction`
- [ ] **Step 1: Write failing test**
In `internal/services/conversation_satisfaction_service_test.go`:
```go
func TestSubmitSatisfaction(t *testing.T) {
// Tests:
// 1. Rejects score < 1 or score > 5
// 2. Rejects if external user is not conversation owner
// 3. Creates record when valid and closed
// 4. Returns existing record if already rated (idempotent)
}
```
- [ ] **Step 2: Run test to verify failure**
Run: `& 'C:\Program Files\Go\bin\go.exe' test ./internal/services -run TestSubmitSatisfaction`
Expected: FAIL
- [ ] **Step 3: Implement model, repository, and service**
1. In `internal/models/models.go`:
```go
type ConversationSatisfaction struct {
ID int64 `gorm:"primaryKey;autoIncrement"`
ConversationID int64 `gorm:"type:bigint;not null;uniqueIndex"`
Score int `gorm:"type:int;not null;default:5;index"`
Feedback string `gorm:"type:text"`
ChannelID int64 `gorm:"type:bigint;not null;default:0;index"`
CustomerID int64 `gorm:"type:bigint;not null;default:0;index"`
AssigneeID int64 `gorm:"type:bigint;not null;default:0;index"`
AIAgentID int64 `gorm:"type:bigint;not null;default:0;index"`
AuditFields
}
```
Add `&ConversationSatisfaction{}` to `models.Models`.
2. Create `internal/repositories/conversation_satisfaction_repository.go`:
- `GetByConversationID(db *gorm.DB, conversationID int64) *models.ConversationSatisfaction`
- `Create(db *gorm.DB, item *models.ConversationSatisfaction) error`
- `Update(db *gorm.DB, item *models.ConversationSatisfaction) error`
3. Create `internal/services/conversation_satisfaction_service.go`:
- Validate `score >= 1 && score <= 5`.
- Validate ownership via `ConversationService.IsCustomerConversationOwner`.
- Save or update existing satisfaction record idempotently inside `sqls.WithTransaction`.
- [ ] **Step 4: Run test to verify pass**
Run: `& 'C:\Program Files\Go\bin\go.exe' test ./internal/services -run TestSubmitSatisfaction`
Expected: PASS
- [ ] **Step 5: Commit**
```bash
git add internal/models/models.go internal/repositories/conversation_satisfaction_repository.go internal/services/conversation_satisfaction_service.go internal/services/conversation_satisfaction_service_test.go
git commit -m "feat(csat): add ConversationSatisfaction model, repository and service"
```
---
### Task 3: Customer CSAT API Route & Response Builder
**Files:**
- Create: `internal/pkg/dto/request/satisfaction_request.go`
- Create: `internal/pkg/dto/response/satisfaction_response.go`
- Modify: `internal/pkg/dto/response/conversation_response.go`
- Modify: `internal/builders/conversation_builder.go`
- Modify: `internal/handlers/api/conversation_handler.go`
- Modify: `internal/bootstrap/routes.go:90-100`
- Test: `internal/handlers/api/conversation_handler_test.go`
**Interfaces:**
- Consumes: `services.ConversationSatisfactionService`
- Produces:
- Route `POST /api/conversation/satisfaction`
- `ConversationResponse.Satisfaction *ConversationSatisfactionResponse`
- [ ] **Step 1: Write failing test**
In `internal/handlers/api/conversation_handler_test.go`:
Test `POST /api/conversation/satisfaction` with valid payload, score validation, and response mapping.
- [ ] **Step 2: Run test to verify failure**
Run: `& 'C:\Program Files\Go\bin\go.exe' test ./internal/handlers/api -run TestConversationSatisfactionAPI`
Expected: FAIL
- [ ] **Step 3: Implement route and handler**
1. Create `internal/pkg/dto/request/satisfaction_request.go`:
```go
type SubmitSatisfactionRequest struct {
ConversationID int64 `json:"conversationId"`
Score int `json:"score"`
Feedback string `json:"feedback"`
}
```
2. Create `internal/pkg/dto/response/satisfaction_response.go`:
```go
type ConversationSatisfactionResponse struct {
ID int64 `json:"id"`
ConversationID int64 `json:"conversationId"`
Score int `json:"score"`
Feedback string `json:"feedback"`
CreatedAt string `json:"createdAt"`
}
```
3. Update `ConversationResponse` and `builders.BuildConversationWithLocale` to embed satisfaction if present.
4. Add `ConversationPostSatisfaction` in `internal/handlers/api/conversation_handler.go`.
5. Register in `internal/bootstrap/routes.go`:
```go
group.POST("/satisfaction", api.ConversationPostSatisfaction)
```
- [ ] **Step 4: Run test to verify pass**
Run: `& 'C:\Program Files\Go\bin\go.exe' test ./internal/handlers/api/...`
Expected: PASS
- [ ] **Step 5: Commit**
```bash
git add internal/pkg/dto/request/satisfaction_request.go internal/pkg/dto/response/satisfaction_response.go internal/pkg/dto/response/conversation_response.go internal/builders/conversation_builder.go internal/handlers/api/conversation_handler.go internal/bootstrap/routes.go internal/handlers/api/conversation_handler_test.go
git commit -m "feat(api): register /api/conversation/satisfaction endpoint"
```
---
### Task 4: Symmetrical i18n Translations for CSAT and Prompt Starters
**Files:**
- Modify: `web/messages/en-US.json`
- Modify: `web/messages/ru-RU.json`
- Modify: `web/messages/zh-CN.json`
- Test: `web/i18n/message-integrity.test.mjs`
**Interfaces:**
- Consumes: i18n keys for CSAT and prompt starters
- Produces: Identical keys in all 3 locales:
- `csat.title`
- `csat.score1`, `csat.score2`, `csat.score3`, `csat.score4`, `csat.score5`
- `csat.commentPlaceholder`
- `csat.submit`
- `csat.submitting`
- `csat.thankYou`
- `csat.alreadyRated`
- `promptStarters.title`
- [ ] **Step 1: Write test / integrity check**
Run `node --test web/i18n/message-integrity.test.mjs` (must pass baseline).
- [ ] **Step 2: Add keys symmetrically to all 3 locales**
Add:
```json
"csat": {
"title": "How would you rate our support?",
"score1": "Very dissatisfied",
"score2": "Dissatisfied",
"score3": "Neutral",
"score4": "Satisfied",
"score5": "Very satisfied",
"commentPlaceholder": "Tell us what could be improved... (optional)",
"submit": "Submit Rating",
"submitting": "Submitting...",
"thankYou": "Thank you for your feedback!",
"alreadyRated": "You rated this conversation {score}/5"
},
"promptStarters": {
"title": "Frequently Asked Questions"
}
```
And exact Russian and Chinese equivalents.
- [ ] **Step 3: Run integrity test**
Run: `node --test web/i18n/message-integrity.test.mjs`
Expected: PASS (0 missing keys, 0 placeholder mismatches).
- [ ] **Step 4: Commit**
```bash
git add web/messages/en-US.json web/messages/ru-RU.json web/messages/zh-CN.json
git commit -m "feat(i18n): add localized messages for CSAT and prompt starters"
```
---
### Task 5: Customer Chat CSAT UI & Operator View
**Files:**
- Modify: `web/lib/api/im.ts` (Add `submitConversationSatisfaction`)
- Create: `web/components/support-chat/csat-card.tsx`
- Modify: `web/components/support-chat/chat-shell.tsx`
- Modify: `web/app/(dashboard)/dashboard/conversations/_components/chat-panel.tsx`
- Test: `web/node_modules/.bin/tsc.cmd --noEmit` and `node --test web/lib/*.test.mjs`
**Interfaces:**
- Consumes: `submitConversationSatisfaction`, `ConversationResponse.satisfaction`
- Produces:
- Interactive CSAT 5-star rating widget in `/support/chat` when conversation is closed.
- CSAT rating badge displayed in operator conversation header.
- [ ] **Step 1: Implement `submitConversationSatisfaction` in `web/lib/api/im.ts`**
Add POST request to `/api/conversation/satisfaction` with `customerSessionToken`.
- [ ] **Step 2: Create `CSATCard` component in `web/components/support-chat/csat-card.tsx`**
- Renders 5 star icons with hover and active selection.
- Displays score label (`csat.score1`..`csat.score5`).
- Shows optional comment textarea.
- Submit button with pending state.
- Upon success or if already rated: renders clean thank-you confirmation with score.
- [ ] **Step 3: Mount in `chat-shell.tsx` and `chat-panel.tsx`**
- In `chat-shell.tsx`: when `conversation.status === 4` (closed), render `CSATCard` above the bottom container.
- In `chat-panel.tsx`: if `conversation.satisfaction` is present, display badge ⭐ `{satisfaction.score}/5` with tooltip showing feedback.
- [ ] **Step 4: Run typecheck and tests**
Run: `& "web/node_modules/.bin/tsc.cmd" --noEmit --project web/tsconfig.json`
Run: `node --test web/lib/*.test.mjs web/i18n/*.test.mjs`
Run: `& 'C:\Program Files\Go\bin\go.exe' test ./...`
Expected: ALL PASS with 0 errors.
- [ ] **Step 5: Commit**
```bash
git add web/lib/api/im.ts web/components/support-chat/csat-card.tsx web/components/support-chat/chat-shell.tsx web/app/\(dashboard\)/dashboard/conversations/_components/chat-panel.tsx
git commit -m "feat(ui): render CSAT rating card on closed conversations and display in operator dashboard"
```