# 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" ```