15 KiB
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:
- Prompt Starters: Channel web config (
WebChannelConfig.PromptStarters []string) is exposed viaGET /api/channel/config. When a visitor has zero messages,PromptStartersrenders clickable question pills that automatically dispatch the question. - CSAT Storage & Service: A persistent
ConversationSatisfactionmodel (conversation_id,score,feedback,customer_id,channel_id,assignee_id,ai_agent_id) records customer ratings. - API & Realtime:
POST /api/conversation/satisfactionsecurely accepts ratings from the conversation owner. The rating is included inConversationDetailResponseand displayed to operators. - UI: A modern, accessible
CSATCardcomponent appears in/support/chatwhen 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, andweb/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 []stringWidgetConfigResponse.PromptStarters []string<PromptStarters items={...} onSelect={...} />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
- In
internal/pkg/dto/dto.go, addPromptStarters []stringtoWebChannelConfig:PromptStarters []string `json:"promptStarters,omitempty"` - In
internal/pkg/dto/response/widget_response.go:PromptStarters []string `json:"promptStarters,omitempty"` - In
internal/handlers/api/channel_handler.go:- Map
cfg.PromptStartersinwebLikeWidgetConfigandWidgetConfigResponse.
- Map
- In
web/lib/api/im.ts:- Add
promptStarters?: string[]toWidgetConfigResponse.
- Add
- 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.
- In
web/components/support-chat/message-list.tsx:- When
messages.length === 0andpromptStarters?.length > 0, render<PromptStarters ... />.
- When
- 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
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.ConversationSatisfactionrepositories.ConversationSatisfactionRepositoryservices.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:
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
- In
internal/models/models.go:Addtype 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 }&ConversationSatisfaction{}tomodels.Models. - Create
internal/repositories/conversation_satisfaction_repository.go:GetByConversationID(db *gorm.DB, conversationID int64) *models.ConversationSatisfactionCreate(db *gorm.DB, item *models.ConversationSatisfaction) errorUpdate(db *gorm.DB, item *models.ConversationSatisfaction) error
- 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.
- Validate
- 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
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
- Route
-
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
- Create
internal/pkg/dto/request/satisfaction_request.go:type SubmitSatisfactionRequest struct { ConversationID int64 `json:"conversationId"` Score int `json:"score"` Feedback string `json:"feedback"` } - Create
internal/pkg/dto/response/satisfaction_response.go:type ConversationSatisfactionResponse struct { ID int64 `json:"id"` ConversationID int64 `json:"conversationId"` Score int `json:"score"` Feedback string `json:"feedback"` CreatedAt string `json:"createdAt"` } - Update
ConversationResponseandbuilders.BuildConversationWithLocaleto embed satisfaction if present. - Add
ConversationPostSatisfactionininternal/handlers/api/conversation_handler.go. - Register in
internal/bootstrap/routes.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
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.titlecsat.score1,csat.score2,csat.score3,csat.score4,csat.score5csat.commentPlaceholdercsat.submitcsat.submittingcsat.thankYoucsat.alreadyRatedpromptStarters.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:
"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
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(AddsubmitConversationSatisfaction) - 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 --noEmitandnode --test web/lib/*.test.mjs
Interfaces:
-
Consumes:
submitConversationSatisfaction,ConversationResponse.satisfaction -
Produces:
- Interactive CSAT 5-star rating widget in
/support/chatwhen conversation is closed. - CSAT rating badge displayed in operator conversation header.
- Interactive CSAT 5-star rating widget in
-
Step 1: Implement
submitConversationSatisfactioninweb/lib/api/im.ts
Add POST request to /api/conversation/satisfaction with customerSessionToken.
-
Step 2: Create
CSATCardcomponent inweb/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.tsxandchat-panel.tsx -
In
chat-shell.tsx: whenconversation.status === 4(closed), renderCSATCardabove the bottom container. -
In
chat-panel.tsx: ifconversation.satisfactionis present, display badge ⭐{satisfaction.score}/5with 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
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"