chore: import clean Agent Desk snapshot (1/7)

This commit is contained in:
2026-10-16 11:55:22 +00:00
parent 24f6423407
commit 79176541c7
323 changed files with 34882 additions and 2 deletions

View File

@@ -0,0 +1,246 @@
# Fix Agent Tool Naming, Prompt Catalog, Timezone, Channel Build Arg, and WebSocket Reconnect 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:** Resolve five production issues across AI runtime tool execution, prompt capability catalogs, timestamp timezone handling, Docker image channel building, and frontend WebSocket reconnection.
**Architecture:**
Sanitize wire function names in the AI agent loop so providers accepting only `^[a-zA-Z0-9_-]+$` succeed while mapping back to canonical codes on execution.
Align prompt catalog strings with direct function names and explicit instructions.
Normalize timestamps to UTC when timezone offsets are missing and emit RFC3339 from the backend.
Support `NEXT_PUBLIC_OPEN_IM_CHANNEL_ID` build argument in Dockerfile and ensure `beforeConnect` token validation precedes WebSocket connections.
**Tech Stack:** Go 1.26, Next.js 16 (React 19, TypeScript), WebSocket, Docker
**Spec:** User prompt requirements (5 targeted fixes)
## Global Constraints
- Preserve all existing staged and user-owned worktree changes.
- Follow layer boundaries: models -> repositories -> services -> handlers.
- Portable SQL / SQLite & MySQL compatibility.
- Format all Go files with `gofmt`.
- TypeScript checks must pass with `cd web && pnpm typecheck`.
- Maintain test coverage across changed backend and frontend surfaces.
## Review Focus
- Function name sanitization must only alter the wire definition, preserving registry, whitelist, and audit codes.
- Direct invocation and tool_search calls must both resolve to the correct capability code.
- Timestamp parsing must treat strings lacking timezone offsets as UTC while keeping explicit offsets untouched.
- Reconnection lifecycle must not create duplicate sockets or run stale connection loops when disconnected mid-flight.
- Docker ARG must properly inject into the Next.js builder stage.
---
### Task 1: Sanitize wire tool function names and reverse mapping on call
**Files:**
- Modify: `internal/pkg/toolx/builtin_tools.go`
- Modify: `internal/pkg/toolx/builtin_tools_test.go`
- Modify: `internal/ai/application/runtime/agent_loop_engine.go`
- Modify: `internal/ai/application/runtime/agent_loop_engine_test.go`
**Interfaces:**
- Consumes: `RegisteredToolSpecs`, `ToolSpec`, `ai.ToolDefinition`, `ai.ToolCall`
- Produces: `toolx.SanitizeToolFunctionName(toolCode string) string`, `toolx.ResolveToolFunctionName(name string) string`
- [ ] **Step 1: Write the failing tests for tool function name sanitization and resolution**
In `internal/pkg/toolx/builtin_tools_test.go`, add unit tests:
```go
func TestSanitizeToolFunctionName(t *testing.T) {
cases := map[string]string{
"builtin/knowledge_retrieve": "builtin_knowledge_retrieve",
"graph/triage_service_request": "graph_triage_service_request",
"skill/12": "skill_12",
"workflow/99": "workflow_99",
"tool_search": "tool_search",
}
for input, expected := range cases {
if got := SanitizeToolFunctionName(input); got != expected {
t.Errorf("SanitizeToolFunctionName(%q) = %q, want %q", input, got, expected)
}
}
}
func TestResolveToolFunctionName(t *testing.T) {
cases := map[string]string{
"builtin_knowledge_retrieve": "builtin/knowledge_retrieve",
"graph_triage_service_request": "graph/triage_service_request",
"knowledge_retrieve": "builtin/knowledge_retrieve",
"skill_12": "skill/12",
"workflow/99": "workflow/99",
"workflow_99": "workflow/99",
"tool_search": "tool_search",
}
for input, expected := range cases {
if got := ResolveToolFunctionName(input); got != expected {
t.Errorf("ResolveToolFunctionName(%q) = %q, want %q", input, got, expected)
}
}
}
```
- [ ] **Step 2: Run test to verify it fails**
Run: `& "C:\Program Files\Go\bin\go.exe" test ./internal/pkg/toolx -run TestSanitizeToolFunctionName`
Expected: Compilation failure or FAIL (functions undefined).
- [ ] **Step 3: Implement `SanitizeToolFunctionName` and `ResolveToolFunctionName` in `internal/pkg/toolx/builtin_tools.go`**
Implement character sanitization (`/` and any non `[a-zA-Z0-9_-]` runes mapped to `_`), spec code mapping, and prefix resolution for `skill_`, `workflow_`, `builtin_`, `graph_`.
- [ ] **Step 4: Update `internal/ai/application/runtime/agent_loop_engine.go`**
In `agentLoopToolDefinitions`:
Register capability definitions using `toolx.SanitizeToolFunctionName(code)` as `definition.Name`.
In `resolveAgentLoopToolCall`:
Resolve `call.Name` using `toolx.ResolveToolFunctionName(toolCode)`.
In `agent_loop_engine_test.go`:
Update `TestAgentLoopRegistersDirectCapabilityAliases` and add test verifying direct sanitized tool call execution.
- [ ] **Step 5: Run tests to verify they pass**
Run: `& "C:\Program Files\Go\bin\go.exe" test ./internal/pkg/toolx`
Run: `& "C:\Program Files\Go\bin\go.exe" test ./internal/ai/application/runtime`
Expected: PASS
---
### Task 2: Update prompt catalog with `| direct function: <sanitized>` and instruction prohibition
**Files:**
- Modify: `internal/ai/application/runtime/agent_turn.go`
- Modify: `internal/ai/application/runtime/agent_loop_engine_test.go`
**Interfaces:**
- Consumes: `toolx.SanitizeToolFunctionName`
- Produces: Prompt catalog lines including `| direct function: <sanitized>` and system prompt instructions forbidding capability codes with slashes as direct function names.
- [ ] **Step 1: Write test checking catalog format and instructions**
In `internal/ai/application/runtime/agent_loop_engine_test.go`, add test asserting catalog items in `prepareTurn` contain `| direct function:` and prompt specifies prohibition.
- [ ] **Step 2: Run test to verify it fails**
Run: `& "C:\Program Files\Go\bin\go.exe" test ./internal/ai/application/runtime -run TestAgentTurnCatalogFormat`
Expected: FAIL
- [ ] **Step 3: Implement catalog line updates and system prompt instruction in `internal/ai/application/runtime/agent_turn.go`**
For Builtin, Skill, Workflow, and MCP entries in `catalog`, append ` | direct function: <sanitized>`.
In `systemPrompt`, add instruction: `Never pass a capability code containing slashes as a tool function name; use the exact direct function name provided in the catalog or call tool_search with toolCode.`
- [ ] **Step 4: Run test to verify it passes**
Run: `& "C:\Program Files\Go\bin\go.exe" test ./internal/ai/application/runtime`
Expected: PASS
---
### Task 3: Timestamp parsing with UTC fallback & backend RFC3339 formatting
**Files:**
- Modify: `web/lib/utils.ts`
- Modify: `web/lib/api/im.ts`
- Modify: `web/lib/support-conversation-list.ts`
- Modify: `internal/services/customer_session_service.go`
- Modify: `internal/services/ws_service.go`
- Modify: `internal/services/auth_service.go`
- Modify: `web/lib/support-conversation-list.test.mjs`
**Interfaces:**
- Consumes: Raw date/time strings
- Produces: `parseApiTimestamp(value?: string | number | Date | null): number` in `web/lib/utils.ts`, RFC3339 timestamps in backend responses.
- [ ] **Step 1: Write tests for `parseApiTimestamp` in `web/lib/support-conversation-list.test.mjs` (or a dedicated test)**
Assert:
- `parseApiTimestamp("2026-10-11 12:00:00")` parses as UTC (`Date.parse("2026-10-11T12:00:00Z")`).
- `parseApiTimestamp("2026-10-11T12:00:00+03:00")` preserves `+03:00` offset.
- `parseApiTimestamp("2026-10-11T12:00:00Z")` preserves `Z`.
- Invalid strings return `0`.
- [ ] **Step 2: Run test to verify it fails**
Run: `node --test web/lib/support-conversation-list.test.mjs`
Expected: FAIL (`parseApiTimestamp` is not defined or fails).
- [ ] **Step 3: Implement `parseApiTimestamp` in `web/lib/utils.ts`**
Export `parseApiTimestamp`: if missing timezone offset (`/(Z|[+-]\d{2}(?::?\d{2})?)$/i`), append `Z` to treat as UTC.
Use in `web/lib/api/im.ts` for `parseExpiresAt`.
Use in `web/lib/support-conversation-list.ts` for `recencyTimestamp`.
- [ ] **Step 4: Update backend date formatting to `time.RFC3339`**
In:
- `internal/services/customer_session_service.go` lines 80 and 164
- `internal/services/ws_service.go` line 163
- `internal/services/auth_service.go` line 309
Use `.UTC().Format(time.RFC3339)` (or `.Format(time.RFC3339)`).
- [ ] **Step 5: Run frontend and backend tests**
Run: `node --test web/lib/support-conversation-list.test.mjs`
Run: `& "C:\Program Files\Go\bin\go.exe" test ./internal/services`
Expected: PASS
---
### Task 4: Add `ARG NEXT_PUBLIC_OPEN_IM_CHANNEL_ID` in `Dockerfile`
**Files:**
- Modify: `Dockerfile`
**Interfaces:**
- Docker build argument `NEXT_PUBLIC_OPEN_IM_CHANNEL_ID` available to `web-builder` stage.
- [ ] **Step 1: Update `Dockerfile`**
In `web-builder` stage before `RUN pnpm build:sdk && pnpm build`, add:
```dockerfile
ARG NEXT_PUBLIC_OPEN_IM_CHANNEL_ID=""
ENV NEXT_PUBLIC_OPEN_IM_CHANNEL_ID=${NEXT_PUBLIC_OPEN_IM_CHANNEL_ID}
```
- [ ] **Step 2: Verify `Dockerfile` syntax**
Check syntax and confirm no regressions.
---
### Task 5: WebSocket connection lifecycle with `beforeConnect: () => ensureCustomerSession()`
**Files:**
- Modify: `web/lib/realtime-connection.ts`
- Modify: `web/lib/stores/support-chat.ts`
- Create / Update: `web/lib/realtime-connection.test.mjs`
**Interfaces:**
- Consumes: `options.beforeConnect`
- Produces: Guaranteed customer session exchange/refresh before WebSocket handshake.
- [ ] **Step 1: Write test in `web/lib/realtime-connection.test.mjs`**
Verify that `createRealtimeConnectionManager`:
- Awaits `beforeConnect` before calling `createSocket`.
- If `beforeConnect` fails, reports connect error, enters disconnected status, and schedules reconnect.
- Re-runs `beforeConnect` on reconnect.
- [ ] **Step 2: Run test to verify it fails**
Run: `node --test web/lib/realtime-connection.test.mjs`
Expected: FAIL
- [ ] **Step 3: Implement `beforeConnect` in `web/lib/realtime-connection.ts` and wire into `web/lib/stores/support-chat.ts`**
Update `RealtimeConnectionManagerOptions` and `connect()` implementation.
Pass `beforeConnect: () => ensureCustomerSession()` in `useSupportChatStore`.
- [ ] **Step 4: Run tests and typecheck**
Run: `node --test web/lib/realtime-connection.test.mjs`
Run: `cd web && pnpm typecheck`
Expected: PASS

View File

@@ -0,0 +1,209 @@
# AI Agent Context & Cost Optimization + Auto-Handoff on Fallback/Failure Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Enable granular UI control over AI agent token spending (context window size, ReAct reasoning steps limit, tool output truncation), and ensure that if the AI agent fails, has no answer, or the fallback does not work, the conversation automatically transfers to a human operator while notifying the customer with a clear localized message so they never feel ignored.
**Architecture:**
1. Extend `AIAgent` in `internal/models/models.go` and DTOs with `AutoHandoffOnFailure bool` and `HandoffMessage string` (while surfacing existing `MaxSteps` and `ContextWindow`).
2. In `internal/ai/runtime/reply_trigger_service.go`, when `s.executor.Run` errors or returns an empty reply without an answer, trigger automatic human handoff (`svc.ConversationHumanDispatchService.HandoffByAIWithRequestID`).
3. In `internal/services/conversation_human_dispatch_service.go`, ensure that on auto-handoff, the customer receives the agent's custom `HandoffMessage` or localized `conversation.handoff.waiting` ("Мы соединяем вас с оператором поддержки. Пожалуйста, подождите.").
4. In `internal/ai/application/runtime/agent_loop_engine.go` and `tool_result_reducer.go`, enforce `ContextWindow` message limits and truncate oversized tool outputs (2500 runes cap) to prevent context window blowouts.
5. In `web/app/(dashboard)/dashboard/ai-agents/_components/config-workbench.tsx`, provide an intuitive "Context & Cost Optimization" card and "Auto-Handoff on Failure" toggle with customizable handoff message.
6. Symmetrical i18n parity across `zh-CN`, `en-US`, and `ru-RU`.
**Tech Stack:** Go 1.26, GORM, Next.js 16 App Router, TypeScript, Base UI / Tailwind CSS, sonner.
---
## Global Constraints
- Follow AGENTS.md layer ownership (`handlers -> services -> repositories/builders`).
- Symmetrical 3-locale i18n parity (`zh-CN.json`, `en-US.json`, `ru-RU.json`).
- Non-destructive DB migrations (portable SQLite & MySQL compatibility).
- TypeScript compilation must pass cleanly (`tsc --noEmit`).
- Go formatting (`gofmt`) and all tests must pass cleanly.
---
### Task 1: Backend Model & Auto-Handoff Dispatch on Failure
**Files:**
- Modify: `internal/models/models.go`
- Modify: `internal/pkg/dto/request/ai_request.go`
- Modify: `internal/pkg/dto/response/ai_response.go`
- Modify: `internal/services/ai_agent_service.go`
- Modify: `internal/services/conversation_human_dispatch_service.go`
- Modify: `internal/ai/runtime/reply_trigger_service.go`
- Modify: `internal/services/conversation_human_dispatch_service_test.go`
- Modify: `internal/ai/runtime/reply_trigger_service_test.go`
- [ ] **Step 1: Add unit tests for Auto-Handoff on AI failure & empty reply**
In `internal/ai/runtime/reply_trigger_service_test.go`:
- Test that when `executor.Run` returns an error, if `AutoHandoffOnFailure` is true, `HandoffByAIWithRequestID` is invoked and sends a waiting message.
- Test that when `executor.Run` returns empty `ReplyText`, auto-handoff is invoked.
In `internal/services/conversation_human_dispatch_service_test.go`:
- Test that custom `HandoffMessage` on `AIAgent` is used when present, otherwise localized default is sent.
- [ ] **Step 2: Run tests to verify failure**
Run: `& 'C:\Program Files\Go\bin\go.exe' test ./internal/ai/runtime/... ./internal/services/...`
- [ ] **Step 3: Update `models.AIAgent` and DTOs**
In `internal/models/models.go`:
```go
AutoHandoffOnFailure bool `gorm:"type:tinyint(1);not null;default:1"` // AutoHandoffOnFailure: whether to transfer to human when AI fails or has no answer.
HandoffMessage string `gorm:"type:text"` // HandoffMessage: custom notification sent to customer upon handoff.
```
Update `internal/pkg/dto/request/ai_request.go` and `internal/pkg/dto/response/ai_response.go`.
Update `internal/services/ai_agent_service.go` to persist and return these fields.
- [ ] **Step 4: Implement auto-handoff on failure in `reply_trigger_service.go`**
In `executeReply`:
When `err != nil`:
```go
if replyCtx.AIAgent.AutoHandoffOnFailure {
_, handoffErr := svc.ConversationHumanDispatchService.HandoffByAIWithRequestID(
replyCtx.Conversation.ID,
replyCtx.AIAgent,
"AI execution failure: "+err.Error(),
replyCtx.Message.RequestID,
)
if handoffErr != nil {
slog.Error("failed to auto-handoff on ai failure", "error", handoffErr)
}
}
return err
```
When `summary != nil && strings.TrimSpace(summary.ReplyText) == "" && !summary.Interrupted && !summary.HandoffRequested`:
```go
if replyCtx.AIAgent.AutoHandoffOnFailure {
_, handoffErr := svc.ConversationHumanDispatchService.HandoffByAIWithRequestID(
replyCtx.Conversation.ID,
replyCtx.AIAgent,
"AI generated empty reply or fallback unavailable",
replyCtx.Message.RequestID,
)
if handoffErr != nil {
slog.Error("failed to auto-handoff on empty ai reply", "error", handoffErr)
}
}
```
- [ ] **Step 5: Send customer notification message in `dispatchAfterHandoffWithRequestID`**
In `internal/services/conversation_human_dispatch_service.go`:
```go
waitingMessage := strings.TrimSpace(aiAgent.HandoffMessage)
if waitingMessage == "" {
waitingMessage = HandoffWaitingMessageForLocale(conversation.Locale)
}
if err := s.sendAITextWithRequestID(conversationID, aiAgentID, waitingMessage, requestID); err != nil {
return nil, err
}
```
- [ ] **Step 6: Run tests and verify they pass**
Run: `& 'C:\Program Files\Go\bin\go.exe' test ./internal/ai/runtime/... ./internal/services/...`
---
### Task 2: Context Window & Tool Output Truncation
**Files:**
- Modify: `internal/ai/application/runtime/agent_turn.go`
- Modify: `internal/ai/application/runtime/agent_loop_engine.go`
- Modify: `internal/ai/runtime/tooling/tool_result_reducer.go`
- Modify: `internal/ai/runtime/tooling/tool_result_reducer_test.go`
- [ ] **Step 1: Write unit tests for tool output truncation**
In `internal/ai/runtime/tooling/tool_result_reducer_test.go`:
- Test that tool outputs exceeding 2500 runes are truncated with a clear ellipsis notice, preventing massive MCP responses from bloating the context window.
- [ ] **Step 2: Implement tool output truncation in `tool_result_reducer.go`**
Ensure outputs are capped to 2500 runes safely.
- [ ] **Step 3: Enforce `ContextWindow` and `MaxSteps` in `agent_loop_engine.go` and `agent_turn.go`**
In `agent_loop_engine.go`:
- If `req.AIAgent.ContextWindow > 0`, use it as `limit` (bounded between 2 and 30).
- If `req.AIAgent.MaxSteps > 0`, pass to `einoAgentLoop` (bounded between 1 and 8, default 4).
- [ ] **Step 4: Run Go tests**
Run: `& 'C:\Program Files\Go\bin\go.exe' test ./internal/ai/...`
---
### Task 3: Symmetrical i18n Translations
**Files:**
- Modify: `web/messages/en-US.json`
- Modify: `web/messages/ru-RU.json`
- Modify: `web/messages/zh-CN.json`
- [ ] **Step 1: Add symmetrical keys for context optimization and auto-handoff**
Add:
- `aiAgent.sectionContextCosts`: "Context & Cost Optimization" / "Контекст и оптимизация расходов" / "上下文与成本优化"
- `aiAgent.contextCostsDescription`: "Manage context window size, reasoning step limits, and tool payload sizes."
- `aiAgent.contextWindow`: "Context Window (messages)" / "Контекстное окно (сообщений)" / "上下文历史消息数"
- `aiAgent.contextWindowHelp`: "Number of recent conversation messages included in context. Smaller window reduces token usage."
- `aiAgent.maxSteps`: "Max Reasoning Steps" / "Максимум шагов ReAct (Max Steps)" / "单次最大推理步骤"
- `aiAgent.maxStepsHelp`: "Max tool call steps allowed per turn. Setting 3–4 prevents runaways and token spikes."
- `aiAgent.autoHandoffOnFailure`: "Auto-handoff on AI failure or missing answer" / "Автоперевод на оператора при сбое или отсутствии ответа" / "AI异常或无答案时自动转人工"
- `aiAgent.autoHandoffOnFailureHelp`: "When the AI model errors, exhausts steps, or has no knowledge, immediately transfer the customer to a human agent."
- `aiAgent.handoffMessage`: "Customer Notice upon Handoff" / "Сообщение клиенту при переводе к оператору" / "转人工提示文案"
- `aiAgent.handoffMessageHelp`: "Sent to the customer so they know they have been transferred and are not being ignored."
- `aiAgent.handoffMessagePlaceholder`: "We are connecting you to a human support agent. Please wait." / "Мы соединяем вас с оператором поддержки. Пожалуйста, подождите." / "正在为你接入人工客服,请稍候。"
- [ ] **Step 2: Run message integrity test**
Run: `node --test web/i18n/message-integrity.test.mjs`
---
### Task 4: Dashboard UI Workbench (`config-workbench.tsx`)
**Files:**
- Modify: `web/lib/api/admin.ts`
- Modify: `web/app/(dashboard)/dashboard/ai-agents/_components/config-workbench.tsx`
- Create: `web/app/(dashboard)/dashboard/ai-agents/_components/config-workbench-cost.test.mjs`
- [ ] **Step 1: Write component unit test for workbench form controls**
In `config-workbench-cost.test.mjs`:
- Verify `contextWindow`, `maxSteps`, `autoHandoffOnFailure`, and `handoffMessage` are rendered and included in the save payload.
- [ ] **Step 2: Run test to verify failure**
Run: `node --test web/app/\(dashboard\)/dashboard/ai-agents/_components/config-workbench-cost.test.mjs`
- [ ] **Step 3: Update `admin.ts` types**
Add `maxSteps`, `contextWindow`, `autoHandoffOnFailure`, `handoffMessage` to `AIAgent` and `CreateAIAgentPayload`.
- [ ] **Step 4: Update `config-workbench.tsx` UI**
- Add state:
- `contextWindow: string` (default "10")
- `maxSteps: string` (default "4")
- `autoHandoffOnFailure: boolean` (default true)
- `handoffMessage: string` (default "")
- In Section `setup` or new sub-section `sectionContextCosts`:
- Render `contextWindow` input with helper text.
- Render `maxSteps` input with helper text.
- In Section `service` under `sectionFallback`:
- Render Switch / Toggle for `autoHandoffOnFailure`.
- Render `handoffMessage` Textarea with placeholder and helper text.
- Update `buildPayload()` to include all 4 fields.
- Initialize all 4 fields in `fetchAIAgent` loading logic.
- [ ] **Step 5: Run tests and typecheck**
Run: `node --test web/app/\(dashboard\)/dashboard/ai-agents/_components/config-workbench-cost.test.mjs`
Run: `& "web/node_modules/.bin/tsc.cmd" --noEmit --project web/tsconfig.json`
---
### Task 5: Whole-Branch Integration Verification & Security Scan
- [ ] **Step 1: Run all backend Go tests**
`& 'C:\Program Files\Go\bin\go.exe' test ./internal/...`
- [ ] **Step 2: Run all frontend unit tests**
`node --test web/lib/*.test.mjs web/i18n/*.test.mjs web/components/*.test.mjs web/components/support-chat/*.test.mjs`
- [ ] **Step 3: Run TypeScript typecheck**
`& "web/node_modules/.bin/tsc.cmd" --noEmit --project web/tsconfig.json`
- [ ] **Step 4: Run Snyk code scan**
Call `snyk_code_scan` on changed directories.
- [ ] **Step 5: Git commit and push**
Commit changes and push to `origin main`.

View File

@@ -0,0 +1,256 @@
# AI Operator Co-Pilot & Conversation Auto-Summarization Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Empower human operators with an AI Co-Pilot that generates contextual draft reply suggestions with 1 click, and provides automated conversation summaries (for quick handoffs and close documentation).
**Architecture:** Create an `AICopilotService` in the Go backend utilizing `ai.LLM.Chat` with conversation history assembly, locale-aware prompt instructions, and graceful fallbacks. Expose endpoints `POST /api/dashboard/conversation/suggest_reply` and `POST /api/dashboard/conversation/summarize`. On the frontend, integrate a Sparkles (✨) AI Suggest action in `SharedMessageEditor` and an AI Summary inspection/close button in the conversation panel.
**Tech Stack:** Go 1.26, Next.js 16 App Router, TypeScript, Base UI / Tailwind CSS, Lucide icons (`Sparkles`, `FileText`), OpenAI/Qwen compatible LLM backend.
**Spec:** Agreed enhancements roadmap (Option 2 - AI operator features).
## Global Constraints
- Follow AGENTS.md layer ownership (`handlers -> services -> repositories/builders`). Handlers must not call LLM or repositories directly.
- Symmetrical 3-locale i18n parity (`zh-CN.json`, `en-US.json`, `ru-RU.json`).
- Resilient against absent/offline LLM config: return clean, localized errors rather than crashes or unhandled exceptions.
- TypeScript compilation must pass with 0 errors (`tsc --noEmit`).
- Go formatting (`gofmt`) and all Go tests must pass cleanly.
## Review Focus
1. Prompt formatting & injection safety: customer input must be clearly delimited so user messages cannot hijack system instructions.
2. Context window limits: only format the most recent 10-15 messages to prevent exceeding LLM context windows or incurring high latency.
3. Locale matching: if the customer spoke Russian or English, the draft reply must match the conversation's language.
4. Concurrent suggestions: disable the AI Suggest button while generation is in flight to prevent duplicate requests.
5. Base UI compatibility: avoid Radix `asChild` assumptions; follow AGENTS.md styling guidelines (`rounded-md`, clean spacing).
---
### Task 1: Backend AI Co-Pilot Service (`SuggestReply` and `SummarizeConversation`)
**Files:**
- Create: `internal/services/ai_copilot_service.go`
- Create: `internal/services/ai_copilot_service_test.go`
**Interfaces:**
- Produces:
```go
var AICopilotService = newAICopilotService()
type AICopilotService interface {
SuggestReply(ctx context.Context, conversationID int64, locale string) (string, error)
SummarizeConversation(ctx context.Context, conversationID int64, locale string) (string, error)
}
```
- [ ] **Step 1: Write unit tests for AICopilotService**
In `internal/services/ai_copilot_service_test.go`:
- Test prompt generation with recent messages.
- Test message normalization and conversation history formatting.
- Test fallback behavior when no messages exist.
- [ ] **Step 2: Run test to verify it fails**
Run: `& 'C:\Program Files\Go\bin\go.exe' test -run TestAICopilotService ./internal/services/...`
Expected: FAIL
- [ ] **Step 3: Implement `AICopilotService`**
In `internal/services/ai_copilot_service.go`:
- Fetch recent messages (up to 12) from `MessageService`.
- Build conversation transcript formatted as `Customer: ...` / `Agent: ...`.
- Assemble system and user prompts respecting `locale`.
- Call `ai.LLM.Chat(ctx, systemPrompt, userPrompt)`.
- [ ] **Step 4: Run test to verify it passes**
Run: `& 'C:\Program Files\Go\bin\go.exe' test -run TestAICopilotService ./internal/services/...`
Expected: PASS
- [ ] **Step 5: Run gofmt and commit**
```bash
git add internal/services/ai_copilot_service.go internal/services/ai_copilot_service_test.go
git commit -m "feat(ai): implement AICopilotService for draft replies and conversation summarization"
```
---
### Task 2: Dashboard API Endpoints for AI Co-Pilot
**Files:**
- Create: `internal/pkg/dto/request/ai_copilot_request.go`
- Create: `internal/pkg/dto/response/ai_copilot_response.go`
- Modify: `internal/handlers/dashboard/conversation_handler.go`
- Modify: `internal/handlers/dashboard/conversation_handler_test.go`
- Modify: `internal/bootstrap/routes.go`
**Interfaces:**
- Produces:
- `POST /api/dashboard/conversation/suggest_reply`: takes `{ conversationId: int64 }`, returns `{ suggestion: string }`.
- `POST /api/dashboard/conversation/summarize`: takes `{ conversationId: int64 }`, returns `{ summary: string }`.
- Permission: requires `constants.PermissionConversationView`.
- [ ] **Step 1: Write handler tests**
In `internal/handlers/dashboard/conversation_handler_test.go`:
Add tests for `ConversationPostSuggest_reply` and `ConversationPostSummarize` (validation, permission check, success).
- [ ] **Step 2: Run test to verify it fails**
Run: `& 'C:\Program Files\Go\bin\go.exe' test -run TestConversationSuggestReply ./internal/handlers/dashboard/...`
Expected: FAIL
- [ ] **Step 3: Implement DTOs, handlers, and routes**
1. Create `internal/pkg/dto/request/ai_copilot_request.go` with `SuggestReplyRequest` and `SummarizeConversationRequest`.
2. Create `internal/pkg/dto/response/ai_copilot_response.go` with `SuggestReplyResponse` and `ConversationSummaryResponse`.
3. In `internal/handlers/dashboard/conversation_handler.go`: implement `ConversationPostSuggest_reply` and `ConversationPostSummarize`.
4. In `internal/bootstrap/routes.go`: register both routes under dashboard conversation group.
- [ ] **Step 4: Run test to verify it passes**
Run: `& 'C:\Program Files\Go\bin\go.exe' test ./internal/handlers/dashboard/...`
Expected: PASS
- [ ] **Step 5: Run gofmt and commit**
```bash
git add internal/pkg/dto/request/ai_copilot_request.go internal/pkg/dto/response/ai_copilot_response.go internal/handlers/dashboard/conversation_handler.go internal/handlers/dashboard/conversation_handler_test.go internal/bootstrap/routes.go
git commit -m "feat(api): register /suggest_reply and /summarize dashboard endpoints"
```
---
### Task 3: Symmetrical i18n Translations for AI Co-Pilot
**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:**
- Produces: Symmetrical keys across all 3 locales:
- `aiCopilot.suggestReply`: "AI Suggest" / "Подсказать ответ" / "AI 生成回复"
- `aiCopilot.generating`: "Generating reply..." / "Генерация ответа..." / "正在生成回复..."
- `aiCopilot.insertReply`: "Use this reply" / "Использовать ответ" / "填入输入框"
- `aiCopilot.discardReply`: "Dismiss" / "Отклонить" / "放弃"
- `aiCopilot.summarize`: "AI Summary" / "AI Резюме" / "AI 总结"
- `aiCopilot.summarizing`: "Summarizing..." / "Составление резюме..." / "正在生成总结..."
- `aiCopilot.summaryTitle`: "Conversation Summary" / "Краткое резюме диалога" / "会话小结"
- `aiCopilot.noSummary`: "No summary available yet" / "Резюме пока недоступно" / "暂无会话总结"
- `aiCopilot.errorGenerating`: "Failed to generate AI suggestion" / "Не удалось сгенерировать ответ" / "生成 AI 建议失败"
- [ ] **Step 1: Check baseline integrity**
Run: `node --test web/i18n/message-integrity.test.mjs`
- [ ] **Step 2: Add keys symmetrically to all 3 JSON files**
Add `aiCopilot.*` keys with exact placeholder parity.
- [ ] **Step 3: Run integrity tests**
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 AI copilot draft replies and summarization"
```
---
### Task 4: AI Reply Suggestion UI in Message Editor
**Files:**
- Modify: `web/lib/api/agent.ts`
- Modify: `web/components/chat/shared-message-editor.tsx`
- Modify: `web/app/(dashboard)/dashboard/conversations/_components/agent-message-editor.tsx`
- Test: `web/node_modules/.bin/tsc.cmd --noEmit` and node tests
**Interfaces:**
- Consumes: `POST /api/dashboard/conversation/suggest_reply`
- Produces:
- Sparkles icon button in operator message editor toolbar.
- When clicked: requests suggestion, renders an interactive floating/docked suggestion card with:
- Generated text preview
- "Use this reply" button (inserts text into editor and closes card)
- "Dismiss" button
- Disabled / loading state with spinner while generating.
- [ ] **Step 1: Add API client methods in `web/lib/api/agent.ts`**
Export `suggestAgentReply(conversationId: number)` and `summarizeAgentConversation(conversationId: number)`.
- [ ] **Step 2: Add AI Suggest button & suggestion drawer in `SharedMessageEditor`**
In `SharedMessageEditor`:
- Add `aiCopilot?: { onSuggest: () => Promise<string | null>; loading?: boolean }` prop.
- Render `SparklesIcon` button in operator toolbar.
- When suggestion is available, render a clean draft suggestion card above the editor with "Use this reply" and "Dismiss".
- [ ] **Step 3: Wire into `AgentMessageEditor`**
In `agent-message-editor.tsx`:
- Connect `aiCopilot` calling `suggestAgentReply(selectedConversationId)`.
- Handle insertion and toast notification.
- [ ] **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`
Expected: ALL PASS with 0 errors.
- [ ] **Step 5: Commit**
```bash
git add web/lib/api/agent.ts web/components/chat/shared-message-editor.tsx web/app/\(dashboard\)/dashboard/conversations/_components/agent-message-editor.tsx
git commit -m "feat(ui): add AI reply suggestion action and preview card to operator message editor"
```
---
### Task 5: AI Conversation Summary in Header & Close Dialog
**Files:**
- Modify: `web/app/(dashboard)/dashboard/conversations/_components/chat-panel.tsx`
- Modify: `web/app/(dashboard)/dashboard/conversations/_components/conversation-info-panel.tsx`
- Modify: `web/app/(dashboard)/dashboard/conversations/_components/conversation-close-dialog.tsx` (if present)
- Test: `web/node_modules/.bin/tsc.cmd --noEmit` and node tests
**Interfaces:**
- Consumes: `summarizeAgentConversation(conversationId)`
- Produces:
- "AI Summary" popover / dialog button in conversation header or info panel.
- Generates summary of the conversation turns on demand.
- Displays structured summary in dialog/card.
- [ ] **Step 1: Add AI Summary dialog/popover component**
Create `web/app/(dashboard)/dashboard/conversations/_components/conversation-summary-dialog.tsx` rendering summary with loading state.
- [ ] **Step 2: Mount summary trigger in header/info panel**
In `chat-panel.tsx` or `conversation-info-panel.tsx`:
Add an action button with `FileTextIcon` / `SparklesIcon` labeled `aiCopilot.summarize` opening the summary dialog.
- [ ] **Step 3: Run typecheck, node tests, and Go backend 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 ./internal/...`
Expected: ALL PASS with 0 errors.
- [ ] **Step 4: Commit**
```bash
git add web/app/\(dashboard\)/dashboard/conversations/_components/conversation-summary-dialog.tsx web/app/\(dashboard\)/dashboard/conversations/_components/chat-panel.tsx web/app/\(dashboard\)/dashboard/conversations/_components/conversation-info-panel.tsx
git commit -m "feat(ui): add AI conversation summarization dialog and header action"
```

View File

@@ -0,0 +1,345 @@
# 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`
- `<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**
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 `<PromptStarters ... />`.
- [ ] **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"
```

View File

@@ -0,0 +1,292 @@
# Operator Workspace: Slash Commands, Collision Detection & Alerts Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Enhance the human operator workspace with inline slash command (`/`) quick replies, real-time operator collision detection (preventing two operators from answering the same customer at once), and audio-visual alerts for incoming messages.
**Architecture:** Extend the existing WebSocket service with a lightweight `operator.presence` topic broadcast (`viewing`, `typing`, `left`). Integrate slash command trigger into `SharedMessageEditor` for instant macro insertion. Add client-side presence state tracking with automatic TTL decay, rendering an operator collision banner in the chat panel. Provide synthesized Web Audio chime alerts for incoming customer messages.
**Tech Stack:** Go 1.26, Gorilla WebSocket, Next.js 16 App Router, TypeScript, Tiptap, Base UI / Tailwind CSS, Web Audio API.
**Spec:** Agreed enhancements roadmap (Option 2 - Operator Workspace improvements).
## Global Constraints
- Follow AGENTS.md layer ownership (no direct SQL in handlers, clean WebSocket envelope structures).
- Symmetrical 3-locale i18n parity (`zh-CN.json`, `en-US.json`, `ru-RU.json`).
- Zero external binary/audio assets — synthesize audio chime using native Web Audio API.
- TypeScript compilation must pass with 0 errors (`tsc --noEmit`).
- Go formatting (`gofmt`) and all Go tests must pass cleanly.
## Review Focus
1. Operator presence event storms: typing events must be throttled (max once per 3s).
2. Presence cleanup: presence records must auto-expire after TTL (15s) so disconnected or crashed tabs don't show ghost operators.
3. Slash command replacement: selecting a quick reply via `/` must replace the `/` text cleanly without leaving orphan slashes.
4. Audio context autoplay policies: Web Audio API must initialize lazily upon user interaction and safely no-op when blocked.
5. Multi-operator collision UI: banner must be informative, non-disruptive, and properly localized across all 3 languages.
---
### Task 1: Backend Operator Presence Event & WebSocket Protocol
**Files:**
- Modify: `internal/pkg/enums/im.go`
- Modify: `internal/services/ws_realtime_types.go`
- Modify: `internal/services/ws_service.go`
- Test: `internal/services/ws_service_test.go`
**Interfaces:**
- Produces:
- `enums.IMRealtimeEventOperatorPresence = "operator.presence"`
- `RealtimeOperatorPresencePayload`:
```go
type RealtimeOperatorPresencePayload struct {
ConversationID int64 `json:"conversationId"`
UserID int64 `json:"userId"`
Username string `json:"username"`
Nickname string `json:"nickname"`
Action string `json:"action"` // "viewing", "typing", "left"
At string `json:"at"`
}
```
- `RealtimeOperatorPresenceEvent` implementing `RealtimeDomainEvent`.
- Client message parsing in `ws_service.go` for `type: "presence"`.
- `WsService.PublishOperatorPresence(conversationID int64, principal *dto.AuthPrincipal, action string, excludeConnID string)`.
- [ ] **Step 1: Write test for Operator Presence publishing**
In `internal/services/ws_service_test.go`:
Add `TestPublishOperatorPresence` verifying that publishing presence sends the event to subscribers on `conversation:{id}` while skipping `excludeConnID`.
- [ ] **Step 2: Run test to verify it fails**
Run: `& 'C:\Program Files\Go\bin\go.exe' test -run TestPublishOperatorPresence ./internal/services/...`
Expected: FAIL
- [ ] **Step 3: Implement enum, payload, event, and WsService methods**
1. In `internal/pkg/enums/im.go`: define `IMRealtimeEventOperatorPresence = "operator.presence"`.
2. In `internal/services/ws_realtime_types.go`: define `RealtimeOperatorPresencePayload` and `RealtimeOperatorPresenceEvent`.
3. In `internal/services/ws_service.go`:
- Handle client message `type == "presence"`: parse `conversationId` and `action`, call `PublishOperatorPresence`.
- Implement `PublishOperatorPresence(conversationID int64, principal *dto.AuthPrincipal, action string, excludeConnID string)`.
- [ ] **Step 4: Run test to verify it passes**
Run: `& 'C:\Program Files\Go\bin\go.exe' test -run TestPublishOperatorPresence ./internal/services/...`
Expected: PASS
- [ ] **Step 5: Run gofmt and commit**
```bash
git add internal/pkg/enums/im.go internal/services/ws_realtime_types.go internal/services/ws_service.go internal/services/ws_service_test.go
git commit -m "feat(ws): support operator.presence realtime event and client presence messages"
```
---
### Task 2: Symmetrical i18n Translations for Collision Alerts and Audio Notifications
**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:**
- Produces: Symmetrical keys across all 3 locales:
- `operatorPresence.viewing`: "{name} is viewing this conversation" / "{name} просматривает этот диалог" / "{name} 正在查看此会话"
- `operatorPresence.typing`: "{name} is typing a reply..." / "{name} печатает ответ..." / "{name} 正在输入回复..."
- `operatorPresence.multipleViewing`: "{count} operators are viewing this conversation" / "{count} оператора просматривают этот диалог" / "{count} 位客服正在查看此会话"
- `operatorPresence.collisionWarning`: "Warning: Another operator is active here to avoid duplicate replies." / "Внимание: другой оператор активен в этом диалоге во избежание дублирования ответов." / "注意:其他客服正在处理此会话,请避免重复回复。"
- `conversation.quickReplySlashHint`: "Type / for quick replies" / "Введите / для быстрого ответа" / "输入 / 快捷回复"
- [ ] **Step 1: Check baseline integrity**
Run: `node --test web/i18n/message-integrity.test.mjs`
- [ ] **Step 2: Add keys symmetrically to all 3 JSON files**
Add `operatorPresence.*` and `conversation.quickReplySlashHint` with exact placeholder parity `{name}` and `{count}`.
- [ ] **Step 3: Run integrity tests**
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 operator collision and slash commands"
```
---
### Task 3: Slash Commands (`/`) Quick Reply Autocomplete in SharedMessageEditor
**Files:**
- Modify: `web/components/chat/shared-message-editor.tsx`
- Modify: `web/app/(dashboard)/dashboard/conversations/_components/agent-message-editor.tsx`
- Test: `web/node_modules/.bin/tsc.cmd --noEmit` and manual verification
**Interfaces:**
- Consumes: `quickReplies` prop in `SharedMessageEditor`
- Produces:
- When typing `/` as first character or when editor text matches `/query`, opens the Quick Replies popover and filters items by `query`.
- Selecting a quick reply removes the `/query` prefix and inserts the chosen quick reply content.
- Shows subtle hint `conversation.quickReplySlashHint` in editor toolbar for operator variant.
- [ ] **Step 1: Implement slash trigger and query tracking**
In `web/components/chat/shared-message-editor.tsx`:
1. In `editor.on("update")`:
- Inspect plain text of current block.
- If text matches `^/([^\s]*)$`:
- Open quick replies popover.
- Set quick reply search query to match the typed characters.
2. In `handleInsertQuickReply(item)`:
- If current text starts with `/`, delete the slash prefix before inserting `item.content`.
3. In toolbar: show subtle hint for operators (`conversation.quickReplySlashHint`).
- [ ] **Step 2: Run typecheck**
Run: `& "web/node_modules/.bin/tsc.cmd" --noEmit --project web/tsconfig.json`
Expected: 0 errors.
- [ ] **Step 3: Commit**
```bash
git add web/components/chat/shared-message-editor.tsx web/app/\(dashboard\)/dashboard/conversations/_components/agent-message-editor.tsx
git commit -m "feat(editor): support slash command / for quick reply autocomplete"
```
---
### Task 4: Operator Presence Tracking & Collision Warning UI
**Files:**
- Create: `web/lib/operator-presence-state.ts`
- Create: `web/lib/operator-presence-state.test.mjs`
- Modify: `web/hooks/use-agent-conversation-realtime.ts`
- Modify: `web/app/(dashboard)/dashboard/conversations/_components/chat-panel.tsx`
- Test: `node --test web/lib/operator-presence-state.test.mjs` and `web/node_modules/.bin/tsc.cmd --noEmit`
**Interfaces:**
- Produces:
- `resolveOperatorPresence(currentList, incomingEvent, currentUserId)` -> pure function returning active peer presences, purging expired entries (>15s TTL).
- Outgoing presence messages:
- `"viewing"` sent when selecting conversation.
- `"typing"` sent when operator types (throttled to 3s).
- `"left"` sent when unselecting conversation.
- In `chat-panel.tsx`:
- Renders an alert banner above the message editor or below conversation header if peer operators are active.
- Displays avatar badge and text e.g. "Jane is viewing this conversation" or "Jane is typing a reply...".
- [ ] **Step 1: Write unit tests for pure presence state transitions**
Create `web/lib/operator-presence-state.test.mjs` verifying:
- Adds new peer operator with timestamp.
- Ignores self (currentUserId).
- Updates action from "viewing" to "typing".
- Removes operator on action "left".
- Filters out records older than 15 seconds.
- [ ] **Step 2: Run test to verify it fails**
Run: `node --test web/lib/operator-presence-state.test.mjs`
Expected: FAIL
- [ ] **Step 3: Implement `web/lib/operator-presence-state.ts`**
Export:
- `OperatorPresenceRecord`
- `updateOperatorPresenceList`
- `purgeExpiredOperatorPresences`
- [ ] **Step 4: Run test to verify it passes**
Run: `node --test web/lib/operator-presence-state.test.mjs`
Expected: PASS
- [ ] **Step 5: Integrate into `use-agent-conversation-realtime.ts` and `chat-panel.tsx`**
1. In `use-agent-conversation-realtime.ts`:
- Send `{"type": "presence", "conversationId": id, "action": "viewing"}` on subscribe.
- Send `{"type": "presence", "conversationId": id, "action": "left"}` on unsubscribe.
- Listen for `operator.presence` events and update store presence state.
2. In `chat-panel.tsx`:
- When active peer operators exist on `currentConversation.id`:
- Render collision warning banner with `UsersIcon` and localized description.
- [ ] **Step 6: Run typecheck and tests**
Run: `& "web/node_modules/.bin/tsc.cmd" --noEmit --project web/tsconfig.json`
Run: `node --test web/lib/operator-presence-state.test.mjs`
Expected: ALL PASS with 0 errors.
- [ ] **Step 7: Commit**
```bash
git add web/lib/operator-presence-state.ts web/lib/operator-presence-state.test.mjs web/hooks/use-agent-conversation-realtime.ts web/app/\(dashboard\)/dashboard/conversations/_components/chat-panel.tsx
git commit -m "feat(presence): detect operator collision and render warning banner"
```
---
### Task 5: Incoming Message Audio & Expanded Browser Alerting
**Files:**
- Create: `web/lib/sound-alert.ts`
- Create: `web/lib/sound-alert.test.mjs`
- Modify: `web/hooks/use-agent-conversation-realtime.ts`
- Test: `node --test web/lib/sound-alert.test.mjs` and `web/node_modules/.bin/tsc.cmd --noEmit`
**Interfaces:**
- Produces:
- `playIncomingMessageSound()`: synthesizes a clean 587Hz -> 880Hz (D5 -> A5) chime via Web Audio API. Safe when audio is suspended or unsupported.
- In `use-agent-conversation-realtime.ts`:
- When `message.created` arrives from a customer:
- If message is from customer and either:
- assigned to current operator, OR
- conversation is unassigned / pending
- Play sound alert via `playIncomingMessageSound()`.
- Show browser notification if tab is in background.
- [ ] **Step 1: Write unit test for sound alert helper**
In `web/lib/sound-alert.test.mjs`:
Test safe fallback when `AudioContext` is undefined (Node.js/SSR environment).
- [ ] **Step 2: Run test to verify it fails**
Run: `node --test web/lib/sound-alert.test.mjs`
Expected: FAIL
- [ ] **Step 3: Implement `web/lib/sound-alert.ts`**
Synthesizes gentle dual-tone sine oscillator with exponential decay.
- [ ] **Step 4: Run test to verify it passes**
Run: `node --test web/lib/sound-alert.test.mjs`
Expected: PASS
- [ ] **Step 5: Wire into `use-agent-conversation-realtime.ts`**
Trigger `playIncomingMessageSound()` on new customer messages for assigned/pending conversations.
- [ ] **Step 6: Run typecheck and all 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 ./internal/...`
Expected: ALL PASS with 0 errors.
- [ ] **Step 7: Commit**
```bash
git add web/lib/sound-alert.ts web/lib/sound-alert.test.mjs web/hooks/use-agent-conversation-realtime.ts
git commit -m "feat(notification): add audio chime and expanded alerts for incoming customer messages"
```

View File

@@ -0,0 +1,513 @@
# Realtime AI Agent Activity Indicator 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 immediate visual feedback in support chat and the operator dashboard by broadcasting real-time agent execution states ("thinking", "searching knowledge base...", "calling tool...", "running workflow...") over WebSocket during AI agent processing turns.
**Architecture:** The Go backend emits lightweight `agent.activity` domain events via `WsService` to the conversation's WebSocket topic at each phase of the Agent Loop (start, knowledge search, tool invocation, completion/failure). The Next.js frontend catches these events in `support-chat` and `agent-conversations` stores, rendering an animated, localized activity indicator bubble at the bottom of the message scroller until the final response message arrives.
**Tech Stack:** Go 1.26 (Gin, Gorilla WebSocket, Eino agent loop), React 19 / Next.js 16, TypeScript, Zustand, Lucide icons, Tailwind CSS, i18n (`zh-CN`, `en-US`, `ru-RU`).
**Spec:** Variant 2: Real-time Execution Step Statuses via WebSocket (non-token-streaming step indicators).
## Global Constraints
- Backend must maintain full 3-locale i18n support (`zh-CN`, `en-US`, `ru-RU`).
- All new translation keys must be added symmetrically to `web/messages/zh-CN.json`, `web/messages/en-US.json`, and `web/messages/ru-RU.json`.
- Event contracts must preserve existing WebSocket protocol conventions in `internal/services/ws_realtime_types.go`.
- Code changes must adhere to layer ownership defined in `AGENTS.md` (no direct DB access from handlers, pure builders, structured `slog` logging).
- No mock data or unhandled error drops; activity indicator must automatically dismiss on message arrival or conversation change.
## Review Focus
- Visitor switches conversation while AI agent is thinking in previous conversation (must not leak indicator across conversations).
- AI agent encounters error or times out during execution (must clean up indicator with "failed" status and not get stuck indefinitely).
- Multi-step tool loop where several tools run sequentially (must update step text dynamically without layout flicker).
- Rapid message send right after another (activity state must reset gracefully for the new turn).
- Offline evaluation or background test runs where `req.Conversation.ID <= 0` (must safely no-op without attempting WebSocket broadcast).
---
### Task 1: Backend WebSocket Event Contract & WsService Method
**Files:**
- Modify: `internal/pkg/enums/im.go:240-265`
- Modify: `internal/services/ws_realtime_types.go:210-250`
- Modify: `internal/services/ws_service.go:460-490`
- Test: `internal/services/ws_service_test.go`
**Interfaces:**
- Consumes: `models.Conversation`, `enums.IMRealtimeEventAgentActivity`
- Produces:
- `RealtimeAgentActivityPayload{ConversationID int64, StepCode string, ToolName string, Status string, At string}`
- `RealtimeAgentActivityEvent` implementing `RealtimeDomainEvent`
- `WsService.PublishAgentActivity(conversation *models.Conversation, stepCode, toolName, status string)`
- [ ] **Step 1: Write the failing test**
Add `TestPublishAgentActivity` in `internal/services/ws_service_test.go`:
```go
func TestPublishAgentActivity(t *testing.T) {
ws := newWsService()
session := &ClientSession{
ID: "test-sess",
Topics: map[string]struct{}{"conversation:100": {}},
Send: make(chan []byte, 10),
}
ws.manager.Register(session)
conv := &models.Conversation{ID: 100}
ws.PublishAgentActivity(conv, "searching_knowledge", "", "running")
select {
case payload := <-session.Send:
var event RealtimeEvent
if err := json.Unmarshal(payload, &event); err != nil {
t.Fatalf("failed to unmarshal event: %v", err)
}
if event.Type != enums.IMRealtimeEventAgentActivity {
t.Fatalf("expected type %s, got %s", enums.IMRealtimeEventAgentActivity, event.Type)
}
case <-time.After(time.Second):
t.Fatal("timed out waiting for agent activity event")
}
}
```
- [ ] **Step 2: Run test to verify it fails**
Run: `& 'C:\Program Files\Go\bin\go.exe' test ./internal/services -run TestPublishAgentActivity`
Expected: FAIL (missing `IMRealtimeEventAgentActivity` or `PublishAgentActivity`)
- [ ] **Step 3: Implement event types and `PublishAgentActivity`**
1. In `internal/pkg/enums/im.go`, define:
```go
IMRealtimeEventAgentActivity = "agent.activity"
```
2. In `internal/services/ws_realtime_types.go`, define:
```go
type RealtimeAgentActivityPayload struct {
ConversationID int64 `json:"conversationId"`
StepCode string `json:"stepCode"`
ToolName string `json:"toolName,omitempty"`
Status string `json:"status"`
At string `json:"at"`
}
func (RealtimeAgentActivityPayload) realtimeEventPayload() {}
type RealtimeAgentActivityEvent struct {
Payload RealtimeAgentActivityPayload
}
func (e RealtimeAgentActivityEvent) EventType() string {
return enums.IMRealtimeEventAgentActivity
}
func (e RealtimeAgentActivityEvent) EventPayload() RealtimeEventPayload {
return e.Payload
}
```
3. In `internal/services/ws_service.go`, add:
```go
func (s *wsService) PublishAgentActivity(conversation *models.Conversation, stepCode, toolName, status string) {
if conversation == nil || conversation.ID <= 0 {
return
}
now := time.Now()
event := s.newEvent(s.conversationTopic(conversation.ID), RealtimeAgentActivityEvent{
Payload: RealtimeAgentActivityPayload{
ConversationID: conversation.ID,
StepCode: stepCode,
ToolName: toolName,
Status: status,
At: formatWsTime(&now),
},
})
s.PublishToTopics(s.routeConversationTopics(conversation), event)
}
```
- [ ] **Step 4: Run test to verify it passes**
Run: `& 'C:\Program Files\Go\bin\go.exe' test ./internal/services -run TestPublishAgentActivity`
Expected: PASS
- [ ] **Step 5: Commit**
```bash
git add internal/pkg/enums/im.go internal/services/ws_realtime_types.go internal/services/ws_service.go internal/services/ws_service_test.go
git commit -m "feat(realtime): add agent.activity event and PublishAgentActivity"
```
---
### Task 2: Emit Agent Activity During Loop Execution
**Files:**
- Modify: `internal/ai/application/runtime/agent_loop_engine.go:55-120,615-680`
- Test: `internal/ai/application/runtime/agent_loop_engine_test.go`
**Interfaces:**
- Consumes: `RunInput.Conversation`, `svc.WsService.PublishAgentActivity`
- Produces: Realtime step broadcasts during:
- Turn preparation / start (`stepCode: "thinking"`, `status: "started"`)
- Knowledge retrieval (`stepCode: "searching_knowledge"`, `status: "running"`)
- Tool execution (`stepCode: "calling_tool"`, `toolName: <name>`, `status: "running"`)
- Workflow execution (`stepCode: "running_workflow"`, `toolName: <name>`, `status: "running"`)
- Turn completed (`stepCode: "completed"`, `status: "completed"`)
- Turn failed (`stepCode: "failed"`, `status: "failed"`)
- [ ] **Step 1: Write the failing test**
In `internal/ai/application/runtime/agent_loop_engine_test.go`, add test asserting that `toolSearchExecutor` and `Run` safely execute without errors when `req.Conversation.ID > 0`.
- [ ] **Step 2: Run test to verify behavior**
Run: `& 'C:\Program Files\Go\bin\go.exe' test ./internal/ai/application/runtime -run TestAgentLoopEngine`
Expected: PASS existing tests.
- [ ] **Step 3: Implement activity emissions in `AgentLoopEngine`**
1. In `Run(ctx context.Context, req RunInput) (*RunResult, error)`:
- Emit start:
```go
if req.Conversation.ID > 0 {
svc.WsService.PublishAgentActivity(&req.Conversation, "thinking", "", "started")
}
```
- Before knowledge retrieval in `retrieveKnowledge`:
```go
if req.Conversation.ID > 0 {
svc.WsService.PublishAgentActivity(&req.Conversation, "searching_knowledge", "", "running")
}
```
- On exit (after `resolveAgentLoopReply` / error):
```go
if req.Conversation.ID > 0 {
if loopErr != nil || err != nil {
svc.WsService.PublishAgentActivity(&req.Conversation, "failed", "", "failed")
} else {
svc.WsService.PublishAgentActivity(&req.Conversation, "completed", "", "completed")
}
}
```
2. In `toolSearchExecutor`:
- When dispatching tool call:
```go
if runInput.Conversation.ID > 0 {
svc.WsService.PublishAgentActivity(&runInput.Conversation, "calling_tool", call.Name, "running")
}
```
- [ ] **Step 4: Run Go package tests**
Run: `& 'C:\Program Files\Go\bin\go.exe' test ./internal/ai/application/runtime/...`
Expected: PASS
- [ ] **Step 5: Commit**
```bash
git add internal/ai/application/runtime/agent_loop_engine.go internal/ai/application/runtime/agent_loop_engine_test.go
git commit -m "feat(runtime): publish realtime agent activity steps during loop execution"
```
---
### Task 3: Frontend i18n Translations for Agent Activity
**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 under `agentActivity.*`
- Produces: Identical keys across all 3 locales:
- `agentActivity.thinking`
- `agentActivity.searchingKnowledge`
- `agentActivity.callingTool`
- `agentActivity.runningWorkflow`
- `agentActivity.default`
- [ ] **Step 1: Write the failing test / check**
Run message integrity test: `node --test web/i18n/message-integrity.test.mjs`
Expected: PASS (current baseline).
- [ ] **Step 2: Add matching translation keys**
1. In `web/messages/en-US.json`:
```json
"agentActivity": {
"thinking": "AI Agent is thinking...",
"searchingKnowledge": "Searching knowledge base...",
"callingTool": "Executing action: {name}...",
"runningWorkflow": "Running workflow: {name}...",
"default": "AI Agent is working on your request..."
}
```
2. In `web/messages/ru-RU.json`:
```json
"agentActivity": {
"thinking": "AI-ассистент думает...",
"searchingKnowledge": "Поиск в базе знаний...",
"callingTool": "Выполнение действия: {name}...",
"runningWorkflow": "Выполнение сценария: {name}...",
"default": "AI-ассистент обрабатывает запрос..."
}
```
3. In `web/messages/zh-CN.json`:
```json
"agentActivity": {
"thinking": "AI 思考中...",
"searchingKnowledge": "检索知识库中...",
"callingTool": "执行工具:{name}...",
"runningWorkflow": "执行流程:{name}...",
"default": "AI 正在处理您的请求..."
}
```
- [ ] **Step 3: Run message integrity test**
Run: `node --test web/i18n/message-integrity.test.mjs`
Expected: PASS (all keys match in all supported locales).
- [ ] **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 strings for agent activity states"
```
---
### Task 4: Frontend State & Realtime Event Integration
**Files:**
- Modify: `web/lib/im-realtime.ts:1-25`
- Modify: `web/lib/stores/support-chat.ts:60-100,230-310,480-520`
- Modify: `web/lib/stores/agent-conversations.ts:80-120,400-440`
- Test: `web/lib/agent-activity-state.test.mjs`
**Interfaces:**
- Consumes: `ImRealtimeEnvelope` with `type: "agent.activity"`, payload `RealtimeAgentActivityPayload`
- Produces:
- Store slice `activeAgentActivity: { conversationId: number; stepCode: string; toolName?: string } | null`
- Reducer/event-handler updating `activeAgentActivity` on `agent.activity`, and clearing on `message.created`, `conversation.closed`, or conversation switch.
- [ ] **Step 1: Write the failing test**
Create `web/lib/agent-activity-state.test.mjs`:
```javascript
import test from "node:test"
import assert from "node:assert/strict"
import { resolveAgentActivityState } from "./agent-activity-state.ts"
test("sets activity state when event matches active conversation and status is running", () => {
const current = null
const event = {
conversationId: 42,
stepCode: "searching_knowledge",
status: "running",
}
const next = resolveAgentActivityState(current, 42, event)
assert.deepEqual(next, { conversationId: 42, stepCode: "searching_knowledge" })
})
test("clears activity state when status is completed or failed", () => {
const current = { conversationId: 42, stepCode: "thinking" }
const event = {
conversationId: 42,
stepCode: "completed",
status: "completed",
}
const next = resolveAgentActivityState(current, 42, event)
assert.equal(next, null)
})
test("ignores events belonging to a different conversation", () => {
const current = null
const event = {
conversationId: 99,
stepCode: "thinking",
status: "started",
}
const next = resolveAgentActivityState(current, 42, event)
assert.equal(next, null)
})
```
- [ ] **Step 2: Run test to verify it fails**
Run: `node --test web/lib/agent-activity-state.test.mjs`
Expected: FAIL (module not found).
- [ ] **Step 3: Implement `agent-activity-state.ts` and wire into stores**
1. Create `web/lib/agent-activity-state.ts` with pure transition helper:
```typescript
export type AgentActivityInfo = {
conversationId: number
stepCode: string
toolName?: string
}
export type AgentActivityEventPayload = {
conversationId: number
stepCode: string
toolName?: string
status: string
}
export function resolveAgentActivityState(
current: AgentActivityInfo | null,
activeConversationId: number | undefined,
payload: AgentActivityEventPayload | null | undefined
): AgentActivityInfo | null {
if (!payload || !activeConversationId || payload.conversationId !== activeConversationId) {
return current
}
if (payload.status === "completed" || payload.status === "failed") {
return null
}
return {
conversationId: payload.conversationId,
stepCode: payload.stepCode,
toolName: payload.toolName,
}
}
```
2. Update `web/lib/im-realtime.ts` to include `RealtimeAgentActivityPayload` in `ImRealtimeEnvelope`.
3. In `web/lib/stores/support-chat.ts`:
- Add `activeAgentActivity: AgentActivityInfo | null` to `SupportChatStore`.
- In WebSocket `onMessage`:
- If `event.type === "agent.activity"`, update `activeAgentActivity` via `resolveAgentActivityState`.
- In `message.created`, clear `activeAgentActivity: null` when message belongs to active conversation.
- In `selectConversation`, reset `activeAgentActivity: null`.
4. In `web/lib/stores/agent-conversations.ts`:
- Track `activeAgentActivity` similarly for the operator's active conversation.
- [ ] **Step 4: Run test to verify it passes**
Run: `node --test web/lib/agent-activity-state.test.mjs`
Expected: PASS
- [ ] **Step 5: Commit**
```bash
git add web/lib/agent-activity-state.ts web/lib/agent-activity-state.test.mjs web/lib/im-realtime.ts web/lib/stores/support-chat.ts web/lib/stores/agent-conversations.ts
git commit -m "feat(store): handle agent.activity realtime events in support chat and agent stores"
```
---
### Task 5: UI Agent Activity Bubble Components
**Files:**
- Create: `web/components/chat/agent-activity-bubble.tsx`
- Modify: `web/components/support-chat/message-list.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: `activeAgentActivity`, `useI18n()`
- Produces:
- `<AgentActivityBubble stepCode={...} toolName={...} />` component rendering:
- Subtle AI avatar (`BotIcon` or `SparklesIcon`)
- Animated pulse dots (`span className="animate-pulse"`)
- Localized step label
- Seamless insertion at bottom of message list in both customer support chat and operator chat panel.
- [ ] **Step 1: Create `AgentActivityBubble` component**
In `web/components/chat/agent-activity-bubble.tsx`:
```tsx
"use client"
import { memo } from "react"
import { BotIcon, Loader2Icon } from "lucide-react"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { ConversationMessageRow } from "@/components/chat/conversation-message-row"
import { ConversationMessageBubble } from "@/components/chat/conversation-message-bubble"
import { useI18n } from "@/i18n/provider"
import { cn } from "@/lib/utils"
export type AgentActivityBubbleProps = {
stepCode: string
toolName?: string
className?: string
}
export const AgentActivityBubble = memo(function AgentActivityBubble({
stepCode,
toolName,
className,
}: AgentActivityBubbleProps) {
const t = useI18n()
let label = t("agentActivity.default")
if (stepCode === "thinking") {
label = t("agentActivity.thinking")
} else if (stepCode === "searching_knowledge") {
label = t("agentActivity.searchingKnowledge")
} else if (stepCode === "calling_tool") {
label = t("agentActivity.callingTool", { name: toolName || "..." })
} else if (stepCode === "running_workflow") {
label = t("agentActivity.runningWorkflow", { name: toolName || "..." })
}
return (
<ConversationMessageRow
align="start"
avatar={
<Avatar className="size-8">
<AvatarFallback className="bg-primary/10 text-primary">
<BotIcon className="size-4" />
</AvatarFallback>
</Avatar>
}
contentClassName="max-w-[86%] gap-1.5"
className={cn("my-1 animate-in fade-in-50 duration-200", className)}
>
<ConversationMessageBubble
variant="system"
className="flex items-center gap-2 rounded-lg border border-border/60 bg-muted/50 px-3 py-2 text-xs text-muted-foreground shadow-sm"
>
<Loader2Icon className="size-3.5 animate-spin text-primary shrink-0" />
<span className="font-medium text-foreground/80">{label}</span>
</ConversationMessageBubble>
</ConversationMessageRow>
)
})
```
- [ ] **Step 2: Mount bubble in `SupportChatMessageList`**
In `web/components/support-chat/message-list.tsx`:
- Receive `agentActivity?: AgentActivityInfo | null` prop (or read from `useSupportChatStore`).
- Render `<AgentActivityBubble ... />` right after the last message item inside `ConversationMessageScroller`.
- Trigger `scrollerRef.current?.scrollToBottom()` when activity state transitions from null to non-null if user was near bottom.
- [ ] **Step 3: Mount bubble in operator `chat-panel.tsx`**
In `web/app/(dashboard)/dashboard/conversations/_components/chat-panel.tsx`:
- Read `activeAgentActivity` from `useAgentConversationsStore`.
- Render `<AgentActivityBubble ... />` in `messagesScroller` when `activeAgentActivity` matches current conversation.
- [ ] **Step 4: Run TypeScript check and test suites**
Run:
1. `& "web/node_modules/.bin/tsc.cmd" --noEmit --project web/tsconfig.json`
2. `node --test web/lib/*.test.mjs web/i18n/*.test.mjs`
3. `& 'C:\Program Files\Go\bin\go.exe' test ./...`
Expected: ALL PASS with zero errors.
- [ ] **Step 5: Commit**
```bash
git add web/components/chat/agent-activity-bubble.tsx web/components/support-chat/message-list.tsx web/app/\(dashboard\)/dashboard/conversations/_components/chat-panel.tsx
git commit -m "feat(ui): display real-time agent activity indicators in customer and operator chat"
```

View File

@@ -0,0 +1,159 @@
# Step-by-Step Setup & Operator Routing Readiness Wizard 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:** Build a complete step-by-step setup wizard, operator routing readiness diagnostic, live route simulator, and one-click quick-fix system across backend and frontend in all three locales (`en-US`, `ru-RU`, `zh-CN`).
**Architecture:** A dedicated `SetupReadinessService` in Go evaluates the 6-step customer-to-operator routing chain (`AgentTeam` -> `AgentTeamSchedule` -> `AgentProfile` -> `AIAgent` -> `Channel` -> End-to-End path), simulates channel dispatch dry-runs, and executes atomic one-click fixes. The Next.js frontend exposes this through a dedicated `/dashboard/setup` page, a setup banner on `/dashboard`, a sidebar navigation entry, and an interactive operator readiness indicator in `WorkbenchHeader` and `SiteHeader`.
**Tech Stack:** Go 1.26, Gin, GORM (`github.com/mlogclub/simple/sqls`), Next.js 16 App Router, React 19, TypeScript, Tailwind CSS, Base UI / shadcn primitives.
**Spec:** `docs/superpowers/specs/2026-10-16-onboarding-readiness-wizard-design.md`
## Global Constraints
- Follow `models -> repositories -> services -> handlers -> builders/response DTOs` layer ownership (`AGENTS.md` 4.1).
- Use `any` instead of `interface{}` in new Go code (`AGENTS.md` 3).
- Support all 3 locales on both backend (`internal/pkg/i18nx/locales/{en-US,ru-RU,zh-CN}.yml`) and frontend (`web/messages/{en-US,ru-RU,zh-CN}.json`).
- Route all frontend API calls through `web/lib/api/client.ts` (`AGENTS.md` 5.1).
- Use `rounded-md` and no `shadow` on support/dashboard cards, and use `OptionCombobox` for dropdowns (`AGENTS.md` 5.1, 5.4).
## Review Focus
1. **Off-hours schedule false-positive:** `AgentTeamSchedule` exists for a team, but its `StartAt`/`EndAt` window does not cover `time.Now()` -> `schedules` step must return `"warning"` with `no_schedule_now` and `can_receive_requests_now = false`.
2. **AI-Only mode blocking human handoff:** All active `AIAgent` records have `ServiceMode == IMConversationServiceModeAIOnly` -> `ai_agents` step must flag `"agent_ai_only_mode"` and simulator must report `"blocked_ai_only_mode"`.
3. **Empty `AIAgent.TeamIDs` or deleted teams:** `AIAgent.TeamIDs` is `""` or references only disabled/non-existent team IDs -> `ai_agents` step must flag `"agent_no_teams"` and `fix_ai_agent_handoff` must bind valid active team IDs.
4. **Operator profile busy or `AutoAssignEnabled == false`:** Operator is in an active team with an active schedule, but `ServiceStatus == ServiceStatusBusy` or `AutoAssignEnabled == false` -> `current_operator.can_receive_now` must be `false` with exact `blocker_codes` (`status_busy` / `auto_assign_disabled`), and `enable_my_operator_profile` must restore `ServiceStatusIdle` and `AutoAssignEnabled = true`.
5. **Idempotent `quick_setup_all` on partially configured database:** Running `quick_setup_all` twice in a row must not duplicate default teams, channels, or schedules when valid active ones already exist.
---
### Task 1: Backend DTOs, Locales, and SetupReadinessService (with TDD)
**Files:**
- Create: `internal/pkg/dto/request/setup_readiness_req.go`
- Create: `internal/pkg/dto/response/setup_readiness_resp.go`
- Create: `internal/services/setup_readiness_service.go`
- Create: `internal/services/setup_readiness_service_test.go`
- Modify: `internal/pkg/i18nx/locales/en-US.yml`
- Modify: `internal/pkg/i18nx/locales/ru-RU.yml`
- Modify: `internal/pkg/i18nx/locales/zh-CN.yml`
**Interfaces:**
- Produces:
- `request.SetupSimulateRouteRequest { ChannelID int64; BypassAI bool }`
- `request.SetupQuickFixRequest { Action string; EntityID int64 }`
- `response.SetupReadinessResponse`, `response.SetupSimulateRouteResponse`, `response.SetupQuickFixResponse`
- `services.SetupReadinessService.GetReadiness(operator *dto.AuthPrincipal) (*response.SetupReadinessResponse, error)`
- `services.SetupReadinessService.SimulateRoute(req request.SetupSimulateRouteRequest, operator *dto.AuthPrincipal) (*response.SetupSimulateRouteResponse, error)`
- `services.SetupReadinessService.ApplyQuickFix(req request.SetupQuickFixRequest, operator *dto.AuthPrincipal) (*response.SetupQuickFixResponse, error)`
- [ ] **Step 1: Write failing tests in `internal/services/setup_readiness_service_test.go`**
Cover:
1. `TestSetupReadiness_EmptyDBAndQuickSetupAll` (verifies all 6 steps fail on empty DB, then `ApplyQuickFix("quick_setup_all")` makes all 6 steps `"ok"`, `CanReceiveRequestsNow == true`, `CurrentOperator.CanReceiveNow == true`, and a second call is idempotent).
2. `TestSetupReadiness_OffHoursAndBusyOperator` (verifies schedule outside `now` triggers `no_schedule_now` and `team_off_hours`, and busy/auto-assign-off operator triggers `status_busy`/`auto_assign_disabled`, then `enable_my_operator_profile` and `ensure_team_and_schedule` resolve them).
3. `TestSetupReadiness_SimulateRoute_AIOnlyAndMissingTeams` (verifies `SimulateRoute` reports `blocked_ai_only_mode` when `ServiceMode == IMConversationServiceModeAIOnly`, `blocked_agent_no_teams` when `TeamIDs == ""`, and `assigned_to_operator` with matched operator after `fix_ai_agent_handoff`).
- [ ] **Step 2: Run test to verify it fails**
Run: `go test -v ./internal/services -run TestSetupReadiness`
Expected: FAIL (missing types and `SetupReadinessService`)
- [ ] **Step 3: Implement DTOs, backend locale keys, and `SetupReadinessService`**
Create `internal/pkg/dto/request/setup_readiness_req.go`, `internal/pkg/dto/response/setup_readiness_resp.go`, and `internal/services/setup_readiness_service.go`. Add `error.setup.*` keys to `en-US.yml`, `ru-RU.yml`, and `zh-CN.yml`.
- [ ] **Step 4: Run tests to verify they pass**
Run: `go test -v ./internal/services -run TestSetupReadiness`
Expected: PASS
---
### Task 2: Backend HTTP Handlers and Route Registration
**Files:**
- Create: `internal/handlers/dashboard/setup_readiness_handler.go`
- Modify: `internal/bootstrap/routes.go`
- Modify: `internal/bootstrap/server.go`
**Interfaces:**
- Consumes: `services.SetupReadinessService` from Task 1.
- Produces:
- `GET /api/dashboard/setup/readiness` (`dashboard.SetupGetReadiness`)
- `POST /api/dashboard/setup/simulate_route` (`dashboard.SetupPostSimulate_route`)
- `POST /api/dashboard/setup/quick_fix` (`dashboard.SetupPostQuick_fix`)
- [ ] **Step 1: Create `internal/handlers/dashboard/setup_readiness_handler.go`**
Implement `SetupGetReadiness`, `SetupPostSimulate_route`, and `SetupPostQuick_fix` using `services.AuthService.GetCurrentPrincipal(ctx)` and `httpx.WriteJSON`.
- [ ] **Step 2: Register routes in `internal/bootstrap/routes.go` and `internal/bootstrap/server.go`**
Add `registerDashboardSetupRoutes(dashboardGroup.Group("/setup"))` in `server.go` and define `registerDashboardSetupRoutes` in `routes.go`.
- [ ] **Step 3: Run `gofmt` and Go build/tests**
Run: `gofmt -w internal/pkg/dto/request/setup_readiness_req.go internal/pkg/dto/response/setup_readiness_resp.go internal/services/setup_readiness_service.go internal/services/setup_readiness_service_test.go internal/handlers/dashboard/setup_readiness_handler.go internal/bootstrap/routes.go internal/bootstrap/server.go && go test ./internal/services -run TestSetupReadiness`
Expected: PASS with zero compile/test errors.
---
### Task 3: Frontend API Client & 3-Locale Translations (`en-US`, `ru-RU`, `zh-CN`)
**Files:**
- Create: `web/lib/api/setup-readiness.ts`
- Modify: `web/messages/en-US.json`
- Modify: `web/messages/ru-RU.json`
- Modify: `web/messages/zh-CN.json`
- Modify: `web/lib/navigation.tsx`
**Interfaces:**
- Produces:
- `fetchSetupReadiness(): Promise<SetupReadinessResponse>`
- `simulateSetupRoute(payload: { channel_id?: number; bypass_ai?: boolean }): Promise<SetupSimulateRouteResponse>`
- `applySetupQuickFix(payload: { action: SetupQuickFixAction; entity_id?: number }): Promise<SetupQuickFixResponse>`
- `nav.setupWizard` and `setupWizard.*` translation keys in `en-US.json`, `ru-RU.json`, and `zh-CN.json`
- Navigation entry `/dashboard/setup` in `web/lib/navigation.tsx`
- [ ] **Step 1: Create `web/lib/api/setup-readiness.ts`**
Define exact TypeScript interfaces matching `SetupReadinessResponse`, `SetupSimulateRouteResponse`, and `SetupQuickFixResponse`, using `requestData` from `@/lib/api/client`.
- [ ] **Step 2: Add `/dashboard/setup` to `web/lib/navigation.tsx`**
Add `{ titleKey: "nav.setupWizard", url: "/dashboard/setup", icon: <ListChecksIcon /> }` to `dashboardNavSections` under Reception Center (`nav.receptionCenter`) right after Overview, and add page title mapping in `getPageTitleKey`.
- [ ] **Step 3: Add complete `setupWizard` and `nav.setupWizard` translations in `en-US.json`, `ru-RU.json`, and `zh-CN.json`**
Delegate translation generation/verification to a **Gemini Flash** subagent (`model: "flash"`) and apply matching keys across all three JSON files.
---
### Task 4: Frontend Setup Wizard Page, Dashboard Banner, and Operator Readiness Popover
**Files:**
- Create: `web/app/(dashboard)/dashboard/setup/page.tsx`
- Create: `web/app/(dashboard)/dashboard/setup/_components/setup-wizard-view.tsx`
- Create: `web/app/(dashboard)/dashboard/_components/setup-readiness-banner.tsx`
- Create: `web/components/operator-readiness-indicator.tsx`
- Modify: `web/app/(dashboard)/dashboard/_components/dashboard-home.tsx`
- Modify: `web/components/workbench-header.tsx`
- Modify: `web/components/site-header.tsx`
**Interfaces:**
- Consumes: `web/lib/api/setup-readiness.ts` and `setupWizard.*` i18n keys from Task 3.
- Produces:
- `/dashboard/setup` page with 6-step interactive checklist, current operator readiness card, 1-click quick fixes, and live route simulator with candidate operator table.
- `SetupReadinessBanner` displayed at the top of `DashboardHome` whenever any step is not `"ok"` or current operator cannot receive chats.
- `OperatorReadinessIndicator` displayed in `WorkbenchHeader` and `SiteHeader` showing live operator readiness status and 1-click fix popover.
- [ ] **Step 1: Build `OperatorReadinessIndicator` (`web/components/operator-readiness-indicator.tsx`)**
Compact header button + popover showing whether customer requests can reach the current operator right now, listing specific blockers (`missing_profile`, `no_team`, `team_off_hours`, `status_busy`, `auto_assign_disabled`, `at_capacity`, or global chain issues), with a 1-click **"Enable Chat Reception"** (`enable_my_operator_profile` / `quick_setup_all`) button and link to `/dashboard/setup`.
- [ ] **Step 2: Build `SetupReadinessBanner` (`web/app/(dashboard)/dashboard/_components/setup-readiness-banner.tsx`)**
Alert card on `/dashboard` home page when setup is incomplete or current operator is blocked, showing `ready_steps / total_steps`, broken step badges, 1-click **"Fix Automatically"** button, and **"Open Step-by-Step Setup"** button.
- [ ] **Step 3: Build `/dashboard/setup` page and `SetupWizardView` (`web/app/(dashboard)/dashboard/setup/_components/setup-wizard-view.tsx`)**
Implement the 6-step readiness checklist with per-step issues, 1-click quick-fix actions, management links (`/dashboard/agents`, `/dashboard/agent-team-schedules`, `/dashboard/ai-agents`, `/dashboard/channels`), current operator status card, and the interactive Route Simulator (`OptionCombobox` for channel selection, 5-hop visual pipeline, and candidate operator table).
- [ ] **Step 4: Wire into `DashboardHome`, `WorkbenchHeader`, and `SiteHeader`**
Mount `<SetupReadinessBanner />` in `web/app/(dashboard)/dashboard/_components/dashboard-home.tsx`, and mount `<OperatorReadinessIndicator />` in `web/components/workbench-header.tsx` and `web/components/site-header.tsx`.
- [ ] **Step 5: Run TypeScript check, ESLint, Go tests, and Snyk security scan**
Run:
- `cd web && pnpm typecheck`
- `cd web && pnpm lint`
- `go test ./internal/services/...`
- Snyk code scan (`snyk_code_scan`) on changed files.

View File

@@ -0,0 +1,194 @@
# Step-by-Step Setup & Operator Routing Readiness Wizard Design
**Date:** 2026-10-16
**Topic:** Step-by-Step Onboarding Setup, Readiness Diagnostics, Route Simulator, and One-Click Quick Fixes (`en-US`, `ru-RU`, `zh-CN`)
---
## 1. Problem Statement & User Intent
When deploying or configuring `agent-desk-ng`, administrators and operators must configure six interdependent entities for customer requests to reach a human operator:
1. **Operator Teams (`AgentTeam`)**: At least one active team (`Status == StatusOk`).
2. **Working Schedules (`AgentTeamSchedule`)**: At least one active schedule (`Status == StatusOk`) covering the current timestamp (`start_at <= now < end_at`) for the target team. Without an active shift, conversations stay in `Pending` and return an off-hours message.
3. **Operator Profiles (`AgentProfile` & `User`)**: An active user (`User.Status == StatusOk`) with an active `AgentProfile` (`Status == StatusOk`), assigned to the active team, with `AutoAssignEnabled == true`, `ServiceStatus == ServiceStatusIdle (0)`, `MaxConcurrentCount > 0`, and available capacity (`activeCount < MaxConcurrentCount`).
4. **AI Agent & Handoff Mode (`AIAgent`)**: An active `AIAgent` (`Status == StatusOk`) whose `ServiceMode` allows reaching a human (`HumanOnly` or `AIFirst`), and whose `TeamIDs` field references at least one valid, active `AgentTeam`.
5. **Inbound Channels (`Channel`)**: At least one active `Channel` (`Status == StatusOk`) linked to a valid `AIAgentID` with `AIAgentRolloutPercent > 0`.
6. **End-to-End Routing Path**: A complete, unbroken path from an active `Channel` -> active `AIAgent` (with handoff enabled) -> active `AgentTeam` -> currently active `AgentTeamSchedule` -> at least one eligible, idle `AgentProfile` with free capacity.
Currently, if any single link in this chain is missing or misconfigured (e.g., missing shift schedule for the current time, empty `TeamIDs` on the AI agent, operator `AutoAssignEnabled == false` or status `Busy`, or `AIOnly` service mode), requests silently fail to reach the operator without explaining why.
### Goals
1. **Step-by-Step Readiness Check**: Inspect all 6 steps of the operator routing chain and report exact status (`ok`, `warning`, `error`), specific blocking reasons, and actionable fixes.
2. **Current Operator Diagnostic**: Tell the currently logged-in operator whether they personally can receive customer conversations right now, and if not, why (missing profile, no team, team off-hours, busy status, auto-assign disabled, at capacity).
3. **Interactive Route Simulator ("Why isn't my request reaching an operator?")**: Dry-run the actual dispatch evaluation for a selected channel (or all channels) and visualize every hop from Channel -> AI Agent -> Team -> Shift Schedule -> Candidate Operators -> Winning Operator (or exact failure point).
4. **One-Click Quick Fixes**: Provide safe, atomic backend actions to fix common blockers in one click (create/enable default team & 24/7 shift schedule, activate current user's operator profile and set idle, bind teams & handoff mode on AI agents, create/link default web channel, or run a complete "Quick Setup All" action).
5. **Multi-Surface UI**:
- Dedicated `/dashboard/setup` page with Step-by-Step Wizard and Route Simulator.
- Setup Readiness Banner on `/dashboard` Overview page whenever any step is incomplete or degraded.
- Sidebar navigation item under Dashboard.
- Operator Readiness status indicator & popover with 1-click fix inside the `/support` operator workbench header.
6. **Full Localization**: Complete support across all three repository locales: `en-US`, `ru-RU`, and `zh-CN` (both backend and frontend).
---
## 2. Backend Architecture
Following `AGENTS.md` layer ownership (`models -> repositories -> services -> handlers -> builders/response DTOs`):
### 2.1 Endpoints (`internal/bootstrap/routes.go`)
Mounted under `/api/dashboard/setup`:
- `GET /api/dashboard/setup/readiness` -> `SetupReadinessHandler.GetReadiness`
- `POST /api/dashboard/setup/simulate_route` -> `SetupReadinessHandler.PostSimulateRoute`
- `POST /api/dashboard/setup/quick_fix` -> `SetupReadinessHandler.PostQuickFix`
Permissions:
- `GET /api/dashboard/setup/readiness` and `POST /api/dashboard/setup/simulate_route` and `POST /api/dashboard/setup/quick_fix` are available to authenticated dashboard/support users so both administrators on `/dashboard` and operators on `/support` can diagnose and fix their readiness. Write actions that modify global resources check appropriate permissions or allow self-profile/bootstrap setup safely.
### 2.2 Request & Response DTOs (`internal/pkg/dto/request/setup_readiness_req.go`, `internal/pkg/dto/response/setup_readiness_resp.go`)
#### `SetupReadinessResponse`
- `overall_status`: `"ok" | "warning" | "error"`
- `ready_steps`: `int`
- `total_steps`: `int` (6)
- `can_receive_requests_now`: `bool`
- `steps`: `[]SetupStepItem`
- `key`: `"teams" | "schedules" | "operators" | "ai_agents" | "channels" | "end_to_end"`
- `step_number`: `1..6`
- `status`: `"ok" | "warning" | "error"`
- `summary_code`: machine-readable reason code (e.g. `"ok"`, `"no_active_team"`, `"no_schedule_now"`, `"no_idle_operator"`, `"agent_ai_only"`, `"agent_missing_teams"`, `"no_active_channel"`, `"no_end_to_end_path"`)
- `total_count`: `int`
- `ready_count`: `int`
- `issues`: `[]SetupIssueItem`
- `code`: string (e.g. `"team_no_active_schedule"`, `"agent_ai_only_mode"`, `"agent_no_teams"`, `"operator_busy"`, `"operator_auto_assign_off"`, `"operator_no_team"`, `"operator_at_capacity"`, `"channel_no_agent"`, `"channel_zero_rollout"`)
- `severity`: `"warning" | "error"`
- `entity_type`: `"team" | "schedule" | "operator" | "ai_agent" | "channel" | "system"`
- `entity_id`: `int64`
- `entity_name`: `string`
- `quick_fix_action`: `string` (optional action key)
- `quick_fix_actions`: `[] string` (e.g. `["ensure_team_and_schedule"]`)
- `manage_path`: `string` (e.g. `"/dashboard/agent-team"`)
- `current_operator`: `CurrentOperatorDiagnostic`
- `user_id`: `int64`
- `username`: `string`
- `nickname`: `string`
- `has_profile`: `bool`
- `profile_status_ok`: `bool`
- `team_id`: `int64`
- `team_name`: `string`
- `team_active`: `bool`
- `team_has_schedule_now`: `bool`
- `service_status`: `int` (`0` = Idle, `1` = Busy)
- `auto_assign_enabled`: `bool`
- `active_conversation_count`: `int64`
- `max_concurrent_count`: `int`
- `can_receive_now`: `bool`
- `blocker_codes`: `[]string` (e.g. `["missing_profile", "no_team", "team_off_hours", "status_busy", "auto_assign_disabled", "at_capacity"]`)
#### `SetupSimulateRouteRequest` & `SetupSimulateRouteResponse`
- Request:
- `channel_id`: `int64` (optional; if `0`, picks the first active channel or simulates all active channels)
- `bypass_ai`: `bool` (optional; simulates what happens when customer requests a human / handoff triggers)
- Response:
- `channel_id`: `int64`
- `channel_name`: `string`
- `channel_type`: `string`
- `will_reach_operator`: `bool`
- `final_outcome_code`: `"assigned_to_operator" | "blocked_no_channel" | "blocked_channel_disabled" | "blocked_zero_rollout" | "blocked_missing_ai_agent" | "blocked_ai_only_mode" | "queued_ai_first_requires_handoff" | "blocked_agent_no_teams" | "queued_off_hours_no_schedule" | "queued_no_eligible_operator"`
- `matched_operator`: `*SimulatedOperatorCandidate`
- `hops`: `[]SimulatedRouteHop`
- `hop_key`: `"channel" | "ai_agent" | "teams" | "schedules" | "operators"`
- `status`: `"ok" | "warning" | "error"`
- `reason_code`: string
- `entity_name`: string
- `quick_fix_action`: string
- `candidate_operators`: `[]SimulatedOperatorCandidate`
- `user_id`: `int64`
- `display_name`: `string`
- `team_id`: `int64`
- `team_name`: `string`
- `eligible`: `bool`
- `skip_reason_code`: `"" | "profile_disabled" | "user_disabled" | "team_not_in_agent" | "team_off_hours" | "auto_assign_off" | "status_busy" | "at_capacity"`
- `active_count`: `int64`
- `max_concurrent_count`: `int`
- `service_status`: `int`
- `auto_assign_enabled`: `bool`
#### `SetupQuickFixRequest` & `SetupQuickFixResponse`
- Request:
- `action`:
- `"quick_setup_all"` — ensures an active team exists, ensures a 24/7 active shift schedule exists for all active teams without a current schedule, ensures the current user has an active idle `AgentProfile` in an active team with `AutoAssignEnabled = true`, ensures at least one active `AIAgent` is linked to the active teams and allows human handoff (`AIFirst` with `AutoHandoffOnFailure = true` or `HumanOnly`), and ensures at least one active Web `Channel` is linked to that `AIAgent` with `RolloutPercent = 100`.
- `"ensure_team_and_schedule"` — creates a default team if none exists, and creates an active shift schedule (`now - 1h` to `now + 365d`) for any active team lacking a currently active schedule.
- `"enable_my_operator_profile"` — creates or updates the current user's `AgentProfile`: assigns to first active team (creating one + schedule if needed), sets `Status = StatusOk`, `AutoAssignEnabled = true`, `ServiceStatus = ServiceStatusIdle`, `MaxConcurrentCount = max(existing, 10)`. Also triggers pending conversation dispatch.
- `"fix_ai_agent_handoff"` — for `entity_id` (or all active AI agents): binds all active `TeamIDs` if empty, switches `AIOnly` to `AIFirst` (or `HumanOnly` if no model is configured), sets `AutoHandoffOnFailure = true`, and ensures `Status = StatusOk`. If no `AIAgent` exists at all, creates a default `HumanOnly` reception agent bound to the active team.
- `"ensure_default_channel"` — if no active channel exists, creates a default Web channel linked to an active `AIAgent` with `AIAgentRolloutPercent = 100`; if channels exist with missing `AIAgentID` or `0%` rollout, fixes them.
- `entity_id`: `int64` (optional target ID)
- Response:
- `applied_actions`: `[]string`
- `readiness`: `SetupReadinessResponse` (fresh snapshot after fix)
---
## 3. Frontend Architecture
### 3.1 API Module (`web/lib/api/setup-readiness.ts`)
- Typed client functions calling `/api/dashboard/setup/readiness`, `/api/dashboard/setup/simulate_route`, and `/api/dashboard/setup/quick_fix` via `web/lib/api/client.ts`.
### 3.2 Dedicated Setup & Diagnostics Page (`web/app/(dashboard)/dashboard/setup/page.tsx`)
- Uses `DashboardPage` layout and standard `rounded-md` border styling without shadows (`AGENTS.md` 5.4).
- **Top Readiness Header Card**:
- Shows overall status badge (`Ready` / `Needs Attention`), progress bar (`X / 6 steps ready`), current operator status summary, and primary CTA **"Quick Setup All in 1 Click"** (`quick_setup_all`) + **"Refresh Check"**.
- **Two Main Sections**:
1. **Step-by-Step Setup Checklist (Steps 1 to 6)**:
- Renders each of the 6 steps in logical order:
1. Operator Teams
2. Working Hours & Active Shift Schedules
3. Operator Profiles & Online Availability
4. AI Agent & Handoff Configuration
5. Inbound Channels
6. End-to-End Dispatch Verification
- Each step displays its status badge, counts (`ready / total`), specific detected issues (with entity names and localized explanations), a **1-Click Quick Fix** button when applicable, and a secondary **"Open Settings"** link to the corresponding management CRUD page.
2. **Interactive Route Simulator ("Why isn't a request reaching an operator?")**:
- Allows selecting any channel via `OptionCombobox` (or auto-selects the first channel) and running the live route trace.
- Displays the 5-hop visual pipeline (`Channel` -> `AI Agent` -> `Target Teams` -> `Active Schedule` -> `Operator Selection`) with status indicators and inline 1-click fix buttons on any failing hop.
- Displays the **Candidate Operators Table** showing every operator profile, their team, load (`active / max`), status (`Idle` / `Busy`), auto-assign state, and exact reason why they were selected or skipped.
### 3.3 Dashboard Home Readiness Banner (`web/app/(dashboard)/dashboard/_components/setup-readiness-banner.tsx`)
- Rendered at the top of `DashboardHome` (`web/app/(dashboard)/dashboard/_components/dashboard-home.tsx`).
- When `can_receive_requests_now == false` or `overall_status != "ok"` (or when the current operator cannot receive requests), shows a clear alert banner listing the broken steps with:
- **"Fix Automatically in 1 Click"** button
- **"Open Step-by-Step Setup"** button linking to `/dashboard/setup`
### 3.4 Operator Workbench Readiness Indicator (`web/app/(support)/support/_components/operator-readiness-Button.tsx` / Header integration)
- Integrated into the `/support` top bar (`web/app/(support)/support/page.tsx` or header component).
- Shows a compact badge indicating whether the operator is currently able to receive incoming customer chats:
- Green (`Ready to Receive Chats`) when `current_operator.can_receive_now && readiness.can_receive_requests_now`.
- Amber/Red (`Routing Blocked: N issues`) when blocked.
- Clicking opens a popover showing:
- Why requests are not reaching this operator right now (e.g., *"Your team has no active shift schedule right now (Off-Hours)"*, *"Your operator profile is Busy"*, *"Auto-assign is disabled"*, *"AI Agent has no team assigned"*).
- **"Enable Chat Reception in 1 Click"** button (`enable_my_operator_profile` / `quick_setup_all`).
- Link to `/dashboard/setup` for full diagnostics.
### 3.5 Navigation (`web/components/app-sidebar.tsx`)
- Adds a navigation item for `/dashboard/setup` under the Overview section so users can always find the step-by-step setup wizard.
---
## 4. Localization (`en-US`, `ru-RU`, `zh-CN`)
- All backend error messages in `internal/pkg/i18nx/locales/{en-US,ru-RU,zh-CN}.yml`.
- All frontend UI copy, step titles, issue descriptions, simulator hop reasons, candidate skip reasons, and action labels in `web/messages/{en-US,ru-RU,zh-CN}.json`.
- Subagents running **Gemini Flash** (`model: "flash"`) will be used to prepare and verify translations and assist with parallel inspections.
---
## 5. Testing & Verification
1. **Backend Unit Tests (`internal/services/setup_readiness_service_test.go`)**:
- Empty database -> `readiness` reports missing steps (`teams`, `schedules`, `operators`, `ai_agents`, `channels`, `end_to_end`).
- Running `quick_Fix("quick_setup_all")` -> creates/fixes the chain atomically so `can_receive_requests_now` becomes `true` and all 6 steps become `ok`.
- Route simulator (`SimulateRoute`) accurately identifies off-hours schedule blocks, `AIOnly` blocks, `Busy` operator blocks, and selects the winning operator when ready.
2. **Code Formatting & Static Checks**:
- `gofmt` and `go test ./internal/services/...`
- `cd web && pnpm typecheck` and `cd web && pnpm lint`
- Locale parity check across `en-US.json`, `ru-RU.json`, and `zh-CN.json`.
- Security scan via Snyk MCP (`snyk_code_scan`) per global rules.