@explorer02/cfm-survey-sdk 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +18 -18
- package/dist/cli/index.mjs +14 -14
- package/dist/index.d.mts +465 -580
- package/dist/index.d.ts +465 -580
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +6 -4
- package/postinstall.js +96 -69
- package/templates/AGENT.md +14 -8
- package/templates/docs/00-integration/answer-logic-and-navigation.md +50 -0
- package/templates/docs/00-integration/aws-deploy.md +281 -0
- package/templates/docs/00-integration/client-integration-guide.md +351 -0
- package/templates/docs/00-integration/component-checklist.md +45 -22
- package/templates/docs/00-integration/constraints.md +49 -4
- package/templates/docs/00-integration/custom-field-logic-and-navigation.md +174 -0
- package/templates/docs/00-integration/display-logic-and-navigation.md +49 -0
- package/templates/docs/00-integration/end-page-logic.md +64 -0
- package/templates/docs/00-integration/partial-save-and-recovery.md +74 -0
- package/templates/docs/00-integration/placeholders-and-tokens.md +116 -0
- package/templates/docs/00-integration/progress.md +61 -0
- package/templates/docs/00-integration/question-display-variants.md +65 -0
- package/templates/docs/00-integration/question-numbering.md +51 -0
- package/templates/docs/00-integration/question-type-sdk-matrix.md +242 -0
- package/templates/docs/00-integration/setup.md +84 -0
- package/templates/docs/00-integration/skip-logic-and-navigation.md +80 -0
- package/templates/docs/00-integration/survey-lifecycle-analytics.md +94 -0
- package/templates/docs/00-integration/useSurveySDK.md +151 -23
- package/templates/docs/01-components/01-survey-page.md +76 -54
- package/templates/docs/01-components/02-question.md +61 -58
- package/templates/docs/01-components/03-rating-scale.md +11 -1
- package/templates/docs/01-components/04-star-rating-scale.md +45 -0
- package/templates/docs/01-components/05-csat-matrix-scale.md +20 -11
- package/templates/docs/01-components/06-likert-matrix-scale.md +12 -3
- package/templates/docs/01-components/07-slider-matrix-scale.md +11 -2
- package/templates/docs/01-components/08-file-upload-scale.md +10 -1
- package/templates/docs/01-components/09-custom-slider-track.md +2 -2
- package/templates/docs/01-components/11-progress-bar.md +10 -4
- package/templates/docs/01-components/13-matrix-dropdown.md +3 -1
- package/templates/docs/01-components/14-intro-page.md +34 -0
- package/templates/docs/01-components/15-end-page.md +31 -0
- package/templates/docs/01-components/16-paused-page.md +27 -0
- package/templates/docs/01-components/17-heatmap-scale.md +75 -0
- package/templates/docs/01-components/18-rank-order-scale.md +100 -0
- package/templates/docs/01-components/README.md +42 -36
- package/templates/docs/02-reference/config-field-index.md +141 -139
- package/templates/docs/02-reference/exports.md +149 -0
- package/templates/docs/02-reference/question-types/01-rating.md +31 -32
- package/templates/docs/02-reference/question-types/04-csat.md +51 -42
- package/templates/docs/02-reference/question-types/05-rating-scale.md +16 -11
- package/templates/docs/02-reference/question-types/06-slider.md +2 -2
- package/templates/docs/02-reference/question-types/07-matrix-cfm.md +44 -30
- package/templates/docs/02-reference/question-types/08-matrix-csat.md +4 -26
- package/templates/docs/02-reference/question-types/09-matrix-rating.md +29 -13
- package/templates/docs/02-reference/question-types/10-slider-matrix.md +15 -4
- package/templates/docs/02-reference/question-types/11-file-upload.md +13 -2
- package/templates/docs/02-reference/question-types/12-heatmap.md +59 -0
- package/templates/docs/02-reference/question-types/13-rank-order.md +101 -0
- package/templates/docs/02-reference/question-types/README.md +86 -47
- package/templates/docs/02-reference/routing-table.md +33 -18
- package/templates/docs/02-reference/value-derivation.md +63 -0
- package/templates/docs/03-ui-specs/00-question-shell.md +19 -88
- package/templates/docs/03-ui-specs/01-rating.md +12 -54
- package/templates/docs/03-ui-specs/02-radio.md +21 -33
- package/templates/docs/03-ui-specs/03-text.md +2 -2
- package/templates/docs/03-ui-specs/04-csat.md +38 -33
- package/templates/docs/03-ui-specs/05-rating-scale.md +21 -18
- package/templates/docs/03-ui-specs/06-slider.md +3 -1
- package/templates/docs/03-ui-specs/07-matrix-cfm.md +24 -21
- package/templates/docs/03-ui-specs/08-matrix-csat-rating.md +17 -45
- package/templates/docs/03-ui-specs/09-slider-matrix.md +1 -1
- package/templates/docs/03-ui-specs/10-file-upload.md +1 -1
- package/templates/docs/03-ui-specs/11-text-and-media.md +29 -29
- package/templates/docs/03-ui-specs/12-survey-chrome.md +20 -14
- package/templates/docs/03-ui-specs/13-heatmap.md +73 -0
- package/templates/docs/03-ui-specs/14-rank-order.md +59 -0
- package/templates/docs/03-ui-specs/README.md +13 -13
- package/templates/docs/03-ui-specs/shared/custom-slider-track.md +2 -2
- package/templates/docs/03-ui-specs/shared/matrix-dropdown.md +5 -0
- package/templates/docs/MANIFEST.json +451 -69
- package/templates/docs/index.md +80 -29
- package/templates/docs/templates/Question.tsx +221 -154
- package/templates/docs/templates/deploy-to-aws.sh +114 -0
- package/templates/docs/templates/heatmapCoords.ts +58 -0
- package/templates/docs/templates/implementation_plan.md +108 -69
- package/templates/docs/templates/survey-inventory.schema.json +72 -0
- package/templates/docs/templates/verify-agent-build.sh +53 -0
- package/templates/docs/01-components/04-csat-scale.md +0 -40
- package/templates/docs/02-question-types/01-rating.md +0 -44
|
@@ -13,9 +13,7 @@ import {
|
|
|
13
13
|
type UseSurveySDKReturn,
|
|
14
14
|
type SurveyQuestion,
|
|
15
15
|
type AnswerValue,
|
|
16
|
-
type
|
|
17
|
-
CsatEmojiSet,
|
|
18
|
-
getEmojiForIndex,
|
|
16
|
+
type MatrixRowAnswers,
|
|
19
17
|
} from '@explorer02/cfm-survey-sdk'; // or @repo/sdk in monorepo
|
|
20
18
|
```
|
|
21
19
|
|
|
@@ -28,12 +26,14 @@ Ask the client: *"Please provide your survey instance ID (JWT token)."*
|
|
|
28
26
|
## Hook Signature
|
|
29
27
|
|
|
30
28
|
```typescript
|
|
31
|
-
const { surveyQueryResults, submitSurveyResults, state, onAction } = useSurveySDK({
|
|
29
|
+
const { surveyQueryResults, submitSurveyResults, partialSaveResults, state, onAction } = useSurveySDK({
|
|
32
30
|
options: {
|
|
33
31
|
instanceId: string, // JWT from client
|
|
34
32
|
language: string | undefined, // Active language code
|
|
35
33
|
placeholders: Record<string, string>, // Token replacements — MUST be stable ref
|
|
34
|
+
customFieldValues?: CustomFieldValueMap, // CRM/transaction custom field values — MUST be stable ref
|
|
36
35
|
debug: boolean, // Console logging
|
|
36
|
+
savePartialResponse: boolean | { debounceMs?: number }, // Optional partial save (default 1500ms)
|
|
37
37
|
},
|
|
38
38
|
});
|
|
39
39
|
```
|
|
@@ -42,9 +42,52 @@ Define `placeholders` **outside** the component to avoid infinite re-fetch loops
|
|
|
42
42
|
|
|
43
43
|
```typescript
|
|
44
44
|
const SURVEY_PLACEHOLDERS = { FIRST_NAME: 'John' };
|
|
45
|
-
// inside component: options: { placeholders: SURVEY_PLACEHOLDERS }
|
|
46
45
|
```
|
|
47
46
|
|
|
47
|
+
Define `customFieldValues` **outside** the component (same stable-ref rule as `placeholders`):
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import type { CustomFieldValueMap } from '@explorer02/cfm-survey-sdk';
|
|
51
|
+
|
|
52
|
+
const CUSTOM_FIELD_VALUES: CustomFieldValueMap = {
|
|
53
|
+
'_c_6a436c1244764c9d4a6ce2fc': 'q5', // TEXT — string
|
|
54
|
+
'_c_6a436c3644764c9d4a6ce3f5': 2, // NUMBER — number or numeric string
|
|
55
|
+
'_c_6a436c9e44764c9d4a6ce717': 'option-id', // PICKLIST — option id string
|
|
56
|
+
'_c_6a436cbb44764c9d4a6ce7e5': ['opt-a'], // PICKLIST_MULTISELECT — option id array
|
|
57
|
+
'_c_6a4386dc44208eb8b08a9056': 1781029800000, // DATETIME — epoch ms (recommended)
|
|
58
|
+
};
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Wrap hook options in `useMemo` inside the component:
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
const surveyOptions = useMemo(
|
|
65
|
+
() => ({
|
|
66
|
+
instanceId,
|
|
67
|
+
language: selectedLanguage,
|
|
68
|
+
placeholders: SURVEY_PLACEHOLDERS,
|
|
69
|
+
customFieldValues: CUSTOM_FIELD_VALUES,
|
|
70
|
+
}),
|
|
71
|
+
[selectedLanguage]
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
useSurveySDK({ options: surveyOptions });
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Keys are the `_c_…` custom field ids from survey logic rules (`conditionEntity.id` in skip/display/answer logic). The SDK evaluates skip, display, and answer logic against these values when passed via hook options.
|
|
78
|
+
|
|
79
|
+
| Custom field type | Client value shape |
|
|
80
|
+
|---|---|
|
|
81
|
+
| TEXT / TEXT_MULTI / TEXTAREA | `string` |
|
|
82
|
+
| NUMBER | `number` or numeric string (`0` is a valid value) |
|
|
83
|
+
| PICKLIST | `string` (option id) |
|
|
84
|
+
| PICKLIST_MULTISELECT | `string[]` (option ids) |
|
|
85
|
+
| DATETIME | epoch ms (`number` or numeric string); ISO date strings also accepted |
|
|
86
|
+
|
|
87
|
+
Missing ids in the map are treated as empty for logic evaluation.
|
|
88
|
+
|
|
89
|
+
When the survey uses custom-field logic, also pass the same map into `Question.tsx` visibility helpers — see [`custom-field-logic-and-navigation.md`](custom-field-logic-and-navigation.md).
|
|
90
|
+
|
|
48
91
|
## Return Shape
|
|
49
92
|
|
|
50
93
|
| Namespace | Field | Type | Purpose |
|
|
@@ -55,42 +98,129 @@ const SURVEY_PLACEHOLDERS = { FIRST_NAME: 'John' };
|
|
|
55
98
|
| `submitSurveyResults` | `data` | `SubmitResult \| null` | Success payload |
|
|
56
99
|
| | `error` | `Error \| null` | Submit failure |
|
|
57
100
|
| | `isLoading` | `boolean` | Submit in progress |
|
|
58
|
-
| `
|
|
59
|
-
| | `
|
|
60
|
-
| | `
|
|
101
|
+
| `partialSaveResults` | `data` | `SubmitResult \| null` | Last partial save success payload (when enabled) |
|
|
102
|
+
| | `error` | `Error \| null` | Partial save failure |
|
|
103
|
+
| | `isLoading` | `boolean` | Partial save in progress |
|
|
104
|
+
| `state` | `phase` | `SurveyPhase` | Survey flow phase (see below) |
|
|
105
|
+
| | `currentPageIndex` | `number` | 0-based page index (when `phase.kind === 'content'`) |
|
|
106
|
+
| | `currentQuestions` | `SurveyQuestion[]` | Questions on current page; `questionText` and `questionNumber` resolved by SDK |
|
|
107
|
+
| | `answers` | `SurveyAnswers` | All answers so far |
|
|
61
108
|
| | `validationErrors` | `Record<string, string>` | Per-question error messages |
|
|
62
|
-
| | `progressPercentage` | `number` | 0–100 completion |
|
|
109
|
+
| | `progressPercentage` | `number` | 0–100 path-scoped completion (see `progress.md`) |
|
|
110
|
+
| | `totalQuestionsCount` | `number` | Answerable questions on active path (progress denominator) |
|
|
111
|
+
| | `answeredQuestionsCount` | `number` | Earned credits on active path (progress numerator) |
|
|
112
|
+
| | `canGoBack` | `boolean` | Show Back button |
|
|
113
|
+
| | `primaryButtonType` | `'next' \| 'submit'` | Primary nav label; SDK maps `NEXT` → `SUBMIT` when `'submit'` |
|
|
114
|
+
| | `specialPage` | `SpecialPage \| undefined` | Intro / end / paused page content when applicable |
|
|
63
115
|
| `onAction` | — | `(action: SurveyAction) => void` | **Only** way to mutate state |
|
|
64
116
|
|
|
117
|
+
### Survey phases (`state.phase.kind`)
|
|
118
|
+
|
|
119
|
+
| Phase | When | UI |
|
|
120
|
+
|-------|------|-----|
|
|
121
|
+
| `intro` | Survey has intro page | Render intro; dispatch `START` |
|
|
122
|
+
| `content` | Active questions | Question map + nav |
|
|
123
|
+
| `end` | Successful submit | End/thank-you page from `specialPage` |
|
|
124
|
+
| `paused` | Survey paused flag | Paused message (read-only) |
|
|
125
|
+
| `submitted` | Already filled | Terminal message |
|
|
126
|
+
| `expired` | Survey expired | Terminal message |
|
|
127
|
+
| `quota_fulfilled` | Quota reached | Terminal message |
|
|
128
|
+
|
|
65
129
|
## Action Dispatching
|
|
66
130
|
|
|
67
131
|
Never mutate `state` directly. Use `onAction`:
|
|
68
132
|
|
|
69
133
|
```typescript
|
|
70
134
|
// Answer a question
|
|
71
|
-
onAction({ type: ACTIONS.CHANGE, payload: { questionId: 'q1',
|
|
135
|
+
onAction({ type: ACTIONS.CHANGE, payload: { questionId: 'q1', answerValue: 4 } });
|
|
72
136
|
|
|
73
137
|
// Matrix answer
|
|
74
|
-
onAction({ type: ACTIONS.CHANGE, payload: { questionId: 'q2',
|
|
138
|
+
onAction({ type: ACTIONS.CHANGE, payload: { questionId: 'q2', answerValue: { row1: 5, row2: 2 } } });
|
|
139
|
+
|
|
140
|
+
// Intro → content
|
|
141
|
+
onAction({ type: ACTIONS.START });
|
|
75
142
|
|
|
76
143
|
// Navigate
|
|
77
|
-
onAction({ type: ACTIONS.NEXT }); // Validates current page;
|
|
78
|
-
onAction({ type: ACTIONS.PREVIOUS });
|
|
79
|
-
onAction({ type: ACTIONS.SUBMIT }); // Validates
|
|
144
|
+
onAction({ type: ACTIONS.NEXT }); // Validates current page; maps to SUBMIT when primaryButtonType is 'submit'
|
|
145
|
+
onAction({ type: ACTIONS.PREVIOUS }); // Uses pageHistory (skip-aware Back)
|
|
146
|
+
onAction({ type: ACTIONS.SUBMIT }); // Validates non-skipped pages, then submits
|
|
147
|
+
|
|
148
|
+
// Lifecycle (client detects scroll / visibility, SDK handles API calls)
|
|
149
|
+
onAction({ type: ACTIONS.SCROLL }); // First scroll — marks instance as started (once)
|
|
150
|
+
onAction({
|
|
151
|
+
type: ACTIONS.RECORD_QUESTION_VIEW,
|
|
152
|
+
payload: { questionIds: ['q1'] }, // Question entered viewport
|
|
153
|
+
});
|
|
80
154
|
```
|
|
81
155
|
|
|
82
|
-
String literals also work: `'NEXT'`, `'PREVIOUS'`, `'SUBMIT'`, `'CHANGE'`.
|
|
156
|
+
String literals also work: `'START'`, `'NEXT'`, `'PREVIOUS'`, `'SUBMIT'`, `'CHANGE'`, `'SCROLL'`, `'RECORD_QUESTION_VIEW'`.
|
|
83
157
|
|
|
84
158
|
## Validation
|
|
85
159
|
|
|
86
|
-
- Required questions must be answered before `NEXT` or `SUBMIT`
|
|
160
|
+
- Required questions must be answered before `NEXT` or `SUBMIT` on the **current page**
|
|
161
|
+
- `NEXT` validates current page only; skip logic may bypass intermediate pages
|
|
162
|
+
- `SUBMIT` validates **non-skipped pages** only (pages bypassed by skip logic are excluded)
|
|
87
163
|
- `null` is a valid answer (N/A option)
|
|
88
|
-
- `
|
|
164
|
+
- `TEXT_AND_MEDIA` is display-only — always skipped for validation
|
|
89
165
|
- On failure, SDK scrolls to first error via `document.getElementById(questionId)` — **requires `id={question.id}` on question wrappers**
|
|
90
166
|
|
|
167
|
+
See [`skip-logic-and-navigation.md`](skip-logic-and-navigation.md) for skip, Back, and answer-sanitize behavior.
|
|
168
|
+
|
|
91
169
|
## Submission
|
|
92
170
|
|
|
93
|
-
Triggered by `onAction({ type: ACTIONS.SUBMIT })` or `NEXT`
|
|
171
|
+
Triggered by `onAction({ type: ACTIONS.SUBMIT })` or `NEXT` when `primaryButtonType === 'submit'`. While submitting, `submitSurveyResults.isLoading` is `true`. On success, render end UI when `state.phase.kind === 'end'` (and `state.specialPage` for content). `submitSurveyResults.data` confirms API success.
|
|
172
|
+
|
|
173
|
+
## Mark as Started
|
|
174
|
+
|
|
175
|
+
On the **first scroll or answer change**, the client should dispatch `onAction({ type: ACTIONS.SCROLL })` (once, on first scroll) and/or the SDK fires automatically on `CHANGE`. Both call a one-time GET to `markResponseInstanceAsStarted`. Fire-and-forget — errors do not block the UI.
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
const onScroll = () => onAction({ type: ACTIONS.SCROLL });
|
|
180
|
+
window.addEventListener('scroll', onScroll, { passive: true, once: true });
|
|
181
|
+
return () => window.removeEventListener('scroll', onScroll);
|
|
182
|
+
}, [onAction]);
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Question Activity (VIEWED)
|
|
186
|
+
|
|
187
|
+
When a question enters the viewport, dispatch `RECORD_QUESTION_VIEW` with its id(s). The SDK POSTs to `recordQuestionActivityV2` with deduplication (each question recorded once). Requires `id={question.id}` on question wrappers.
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
const observer = new IntersectionObserver(entries => {
|
|
192
|
+
for (const entry of entries) {
|
|
193
|
+
if (entry.isIntersecting && entry.target.id) {
|
|
194
|
+
onAction({
|
|
195
|
+
type: ACTIONS.RECORD_QUESTION_VIEW,
|
|
196
|
+
payload: { questionIds: [entry.target.id] },
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}, { threshold: 0.5 });
|
|
201
|
+
|
|
202
|
+
state.currentQuestions.forEach(q => {
|
|
203
|
+
const el = document.getElementById(q.id);
|
|
204
|
+
if (el) observer.observe(el);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
return () => observer.disconnect();
|
|
208
|
+
}, [state.currentQuestions, onAction]);
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Partial Save
|
|
212
|
+
|
|
213
|
+
When `savePartialResponse: true` (or `{ debounceMs: 1500 }`) is passed in options, the SDK debounces answer changes and POSTs partial responses with `surveySubmitted=false`. Partial saves begin only after the first user interaction (not MCQ default seeding). Monitor `partialSaveResults.isLoading` / `partialSaveResults.error` if you need UI feedback.
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
useSurveySDK({
|
|
217
|
+
options: {
|
|
218
|
+
instanceId,
|
|
219
|
+
savePartialResponse: true,
|
|
220
|
+
// or: savePartialResponse: { debounceMs: 2000 },
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
```
|
|
94
224
|
|
|
95
225
|
## Language & Placeholders
|
|
96
226
|
|
|
@@ -98,10 +228,8 @@ Triggered by `onAction({ type: ACTIONS.SUBMIT })` or `NEXT` on the last page. Wh
|
|
|
98
228
|
- `survey.languages` lists available languages for `LanguageSelector`
|
|
99
229
|
- Placeholders replace `{{FIRST_NAME}}` tokens in question text at map time
|
|
100
230
|
|
|
101
|
-
## CSAT
|
|
231
|
+
## CSAT / rating emoji UI (client-implemented)
|
|
102
232
|
|
|
103
|
-
|
|
104
|
-
import { CsatEmojiSet, getEmojiForIndex, CsatStarIcons } from '@explorer02/cfm-survey-sdk';
|
|
105
|
-
```
|
|
233
|
+
CSAT emoji and star icons are **not** exported from the SDK. Implement client-side with `react-icons` (or your icon library). Map column index to sentiment icons proportionally for non-5-point scales.
|
|
106
234
|
|
|
107
|
-
|
|
235
|
+
See `03-ui-specs/04-csat.md` and [`question-type-sdk-matrix.md#csat_matrix`](question-type-sdk-matrix.md#csat_matrix).
|
|
@@ -3,91 +3,113 @@
|
|
|
3
3
|
> **UI spec (authoritative):** `03-ui-specs/12-survey-chrome.md`
|
|
4
4
|
> **Reference implementation:** `apps/client/src/components/SurveyPage.tsx`
|
|
5
5
|
> **Target Component**: `SurveyPage`
|
|
6
|
-
> **Role**:
|
|
6
|
+
> **Role**: Wires `useSurveySDK` to the UI — loading/error states, phase routing, pagination, submission.
|
|
7
7
|
|
|
8
8
|
## Core Responsibility
|
|
9
|
-
This component MUST act as the state machine for the entire survey flow. It should never render questions directly—it delegates that to `Question.tsx`.
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
Act as the **phase state machine** for the survey. Never render questions directly — delegate to `Question.tsx`.
|
|
12
11
|
|
|
13
|
-
|
|
12
|
+
## Setup & Wiring (CRITICAL)
|
|
14
13
|
|
|
15
14
|
```tsx
|
|
15
|
+
import type { CustomFieldValueMap } from '@explorer02/cfm-survey-sdk';
|
|
16
|
+
|
|
16
17
|
const SURVEY_PLACEHOLDERS = { FIRST_NAME: 'Customer' }; // Must be outside component!
|
|
18
|
+
const CUSTOM_FIELD_VALUES: CustomFieldValueMap = {}; // Populate when survey uses custom-field logic
|
|
17
19
|
|
|
18
20
|
export default function SurveyPage() {
|
|
19
|
-
const [selectedLanguage, setSelectedLanguage] = useState<string | undefined>(
|
|
21
|
+
const [selectedLanguage, setSelectedLanguage] = useState<string | undefined>();
|
|
20
22
|
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
instanceId: 'YOUR_INSTANCE_ID_HERE',
|
|
23
|
+
const surveyOptions = useMemo(
|
|
24
|
+
() => ({
|
|
25
|
+
instanceId: 'YOUR_INSTANCE_ID_HERE',
|
|
24
26
|
language: selectedLanguage,
|
|
25
27
|
placeholders: SURVEY_PLACEHOLDERS,
|
|
28
|
+
customFieldValues: CUSTOM_FIELD_VALUES,
|
|
26
29
|
debug: true,
|
|
27
|
-
}
|
|
30
|
+
}),
|
|
31
|
+
[selectedLanguage]
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const { surveyQueryResults, submitSurveyResults, state, onAction } = useSurveySDK({
|
|
35
|
+
options: surveyOptions,
|
|
28
36
|
});
|
|
29
|
-
|
|
37
|
+
|
|
30
38
|
const survey = surveyQueryResults.data;
|
|
31
|
-
|
|
32
|
-
|
|
39
|
+
const allQuestions = useMemo(
|
|
40
|
+
() => survey?.pages.flatMap(page => page.questions) ?? [],
|
|
41
|
+
[survey]
|
|
42
|
+
);
|
|
43
|
+
// ... phase routing below
|
|
33
44
|
}
|
|
34
45
|
```
|
|
35
46
|
|
|
36
|
-
|
|
47
|
+
See [`client-integration-guide.md`](../00-integration/client-integration-guide.md) for lifecycle effects (`SCROLL`, `RECORD_QUESTION_VIEW`).
|
|
37
48
|
|
|
38
|
-
|
|
49
|
+
## Render Order (MANDATORY)
|
|
39
50
|
|
|
40
|
-
|
|
41
|
-
If `surveyQueryResults.isLoading` is true, render a Skeleton or Spinner.
|
|
42
|
-
*Include your CSS-only Header and Footer around the spinner.*
|
|
51
|
+
Handle in this order:
|
|
43
52
|
|
|
44
|
-
###
|
|
45
|
-
If `surveyQueryResults.error` is true, render a red Error banner containing `surveyQueryResults.error.message`.
|
|
53
|
+
### 1. Loading / Error / Empty
|
|
46
54
|
|
|
47
|
-
|
|
48
|
-
|
|
55
|
+
| Condition | UI |
|
|
56
|
+
|-----------|-----|
|
|
57
|
+
| `surveyQueryResults.isLoading` | Spinner + Header/Footer |
|
|
58
|
+
| `surveyQueryResults.error` | Error message |
|
|
59
|
+
| `!survey` | "No survey data" |
|
|
49
60
|
|
|
50
|
-
###
|
|
51
|
-
If `submitSurveyResults.data` exists, the survey is complete.
|
|
52
|
-
*Render the Thank You Screen (e.g., "Thank you for your feedback!"). Do not render the question list.*
|
|
61
|
+
### 2. Special phases (`state.phase`)
|
|
53
62
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
63
|
+
| `phase.kind` | Component | Action |
|
|
64
|
+
|--------------|-----------|--------|
|
|
65
|
+
| `intro` | `IntroPage` | `onAction({ type: 'START' })` |
|
|
66
|
+
| `paused` | `PausedPage` | Read-only |
|
|
67
|
+
| `end` | `EndPage` | Thank-you from `state.specialPage` |
|
|
68
|
+
| `submitted` | Terminal message | Already filled |
|
|
69
|
+
| `expired` | Terminal message | Survey expired |
|
|
70
|
+
| `quota_fulfilled` | Terminal message | Quota reached |
|
|
71
|
+
|
|
72
|
+
See `14-intro-page.md`, `15-end-page.md`, `16-paused-page.md`.
|
|
73
|
+
|
|
74
|
+
### 3. Active content (`phase.kind === 'content'`)
|
|
75
|
+
|
|
76
|
+
1. `Header`
|
|
77
|
+
2. `ProgressBar` (`state.progressPercentage`) — see `00-integration/progress.md`
|
|
78
|
+
3. `LanguageSelector`
|
|
79
|
+
4. Question map (below)
|
|
80
|
+
5. Navigation buttons (below)
|
|
61
81
|
6. `Footer`
|
|
62
82
|
|
|
63
83
|
## The Question Map
|
|
64
84
|
|
|
65
|
-
|
|
85
|
+
Pass **all answers and questions** for placeholder resolution (`{{SURVEY_QUESTION-:-qId}}`):
|
|
66
86
|
|
|
67
87
|
```tsx
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
88
|
+
{state.currentQuestions.map(question => (
|
|
89
|
+
<Question
|
|
90
|
+
key={question.id}
|
|
91
|
+
question={question}
|
|
92
|
+
selectedValue={state.answers[question.id]}
|
|
93
|
+
validationError={state.validationErrors[question.id]}
|
|
94
|
+
allAnswers={state.answers}
|
|
95
|
+
allQuestions={allQuestions}
|
|
96
|
+
customFieldValues={CUSTOM_FIELD_VALUES}
|
|
97
|
+
onSelect={answerValue =>
|
|
98
|
+
onAction({ type: 'CHANGE', payload: { questionId: question.id, answerValue } })
|
|
99
|
+
}
|
|
100
|
+
/>
|
|
101
|
+
))}
|
|
82
102
|
```
|
|
83
103
|
|
|
84
|
-
## Navigation Buttons
|
|
104
|
+
## Navigation Buttons
|
|
85
105
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
-
|
|
89
|
-
- `onClick={() => onAction({ type: 'PREVIOUS' })}`
|
|
90
|
-
- **Next/Submit Button**:
|
|
106
|
+
- **Back:** Render when `state.canGoBack` — `onAction({ type: 'PREVIOUS' })`
|
|
107
|
+
- **Primary:** Label from `state.primaryButtonType` (`'next'` | `'submit'`)
|
|
108
|
+
- Always dispatch `onAction({ type: 'NEXT' })` — SDK maps to submit when `primaryButtonType === 'submit'`
|
|
91
109
|
- `disabled={submitSurveyResults.isLoading}`
|
|
92
|
-
|
|
93
|
-
|
|
110
|
+
|
|
111
|
+
Do **not** use `currentPageIndex === 0` alone to hide Back — intro page and skip-aware history use `canGoBack`.
|
|
112
|
+
|
|
113
|
+
## Scroll & question activity
|
|
114
|
+
|
|
115
|
+
Wire `SCROLL` (once) and `RECORD_QUESTION_VIEW` as documented in `useSurveySDK.md`.
|
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
> **UI spec (authoritative):** `03-ui-specs/00-question-shell.md`
|
|
4
4
|
> **Canonical template:** [`templates/Question.tsx`](../templates/Question.tsx) — copy verbatim, then create imported components
|
|
5
|
-
> **Monorepo reference:** `apps/client/src/components/Question.tsx`
|
|
5
|
+
> **Monorepo reference:** `apps/client/src/components/Question.tsx`
|
|
6
6
|
> **Target Component**: `Question`
|
|
7
|
-
> **Role**: Wrapper card component that routes to the correct scale component based on `question.type`.
|
|
8
7
|
|
|
9
8
|
## Start Here (CRITICAL)
|
|
10
9
|
|
|
@@ -12,71 +11,40 @@
|
|
|
12
11
|
2. Create every imported scale component — see [`component-checklist.md`](../00-integration/component-checklist.md).
|
|
13
12
|
3. Read `03-ui-specs/` for each type in your fetched survey **before** editing the dispatcher.
|
|
14
13
|
|
|
15
|
-
**Do NOT** write
|
|
14
|
+
**Do NOT** write partial skeletons with `default` fallbacks or `"not yet implemented"` messages.
|
|
16
15
|
|
|
17
|
-
##
|
|
18
|
-
Render a visually separated container (card) for each question. Handle rich text parsing, required asterisks, description text, and error validation messages. Most importantly, it MUST act as an exhaustive switch statement dispatching to ALL supported question types.
|
|
16
|
+
## Dispatcher (11 type literals)
|
|
19
17
|
|
|
20
|
-
|
|
18
|
+
Use `QUESTION_TYPE` from the SDK:
|
|
21
19
|
|
|
22
|
-
|
|
20
|
+
| `question.type` | Renders |
|
|
21
|
+
|-----------------|---------|
|
|
22
|
+
| `NPS_SCALE` | `RatingScale` (NPS) |
|
|
23
|
+
| `MCQ` | Inline radio/checkbox |
|
|
24
|
+
| `TEXTFIELD` | Inline textarea/input |
|
|
25
|
+
| `CFM_MATRIX` | `LikertMatrixScale` |
|
|
26
|
+
| `CSAT_MATRIX`, `RATING_MATRIX` | `CsatMatrixScale` |
|
|
27
|
+
| `SLIDER_MATRIX` | `SliderMatrixScale` |
|
|
28
|
+
| `FILE_UPLOAD` | `FileUploadScale` |
|
|
29
|
+
| `TEXT_AND_MEDIA` | Media only (no answer) |
|
|
30
|
+
| `HEATMAP` | `HeatmapScale` |
|
|
31
|
+
| `RANK_ORDER` | `RankOrderScale` |
|
|
23
32
|
|
|
24
|
-
|
|
25
|
-
2. **`csat`**: Renders `CsatScale`. This covers CSAT specific faces/icons.
|
|
26
|
-
3. **`radio`**: Renders inline MCQ radio buttons.
|
|
27
|
-
4. **`text`**: Renders an inline `<textarea>` or `<input>`.
|
|
28
|
-
5. **`matrix`** — **requires `subType`**:
|
|
29
|
-
- If `subType === 'CFM_MATRIX'`, render `LikertMatrixScale`
|
|
30
|
-
- If `subType === 'CSAT_MATRIX'` or `'RATING_MATRIX'`, render `CsatMatrixScale`
|
|
31
|
-
- Routing on `type === 'matrix'` alone renders **nothing**
|
|
32
|
-
6. **`slider_matrix`**: Renders `SliderMatrixScale`
|
|
33
|
-
7. **`file_upload`**: Renders `FileUploadScale`
|
|
34
|
-
8. **`text_and_media`**: Renders inline `<video>` or `<img>` based on `question.mediaUrl`. No answer input.
|
|
35
|
-
9. **`rating_scale`**: Star/emoji scale — see `03-ui-specs/05-rating-scale.md`
|
|
36
|
-
10. **`slider`**: Standalone slider — see `03-ui-specs/06-slider.md`
|
|
37
|
-
|
|
38
|
-
## Forbidden Anti-Patterns
|
|
33
|
+
**Never** branch on `subType`.
|
|
39
34
|
|
|
40
35
|
```tsx
|
|
41
|
-
|
|
42
|
-
default:
|
|
43
|
-
return <p>Question type "{question.type}" is not yet implemented in this client.</p>;
|
|
44
|
-
|
|
45
|
-
// ❌ FORBIDDEN — matrix without subType
|
|
46
|
-
{question.type === 'matrix' && <SomeMatrix />}
|
|
36
|
+
import { QUESTION_TYPE } from '@explorer02/cfm-survey-sdk';
|
|
47
37
|
|
|
48
|
-
|
|
49
|
-
{question.type === 'matrix' && question.subType === 'CFM_MATRIX' && <LikertMatrixScale ... />}
|
|
50
|
-
{question.type === 'matrix' && (question.subType === 'CSAT_MATRIX' || question.subType === 'RATING_MATRIX') && <CsatMatrixScale ... />}
|
|
38
|
+
{question.type === QUESTION_TYPE.CFM_MATRIX && <LikertMatrixScale ... />}
|
|
51
39
|
```
|
|
52
40
|
|
|
53
|
-
##
|
|
54
|
-
|
|
55
|
-
Every question MUST be wrapped in a standard HTML `<section>` to visually separate it.
|
|
56
|
-
|
|
57
|
-
1. **Wrapper styling**:
|
|
58
|
-
- Border: `border border-gray-200`
|
|
59
|
-
- Radius: `rounded-xl`
|
|
60
|
-
- Background: `bg-white`
|
|
61
|
-
- Padding: `p-6 md:p-8`
|
|
62
|
-
- Shadow & Hover: `shadow-sm transition-all hover:shadow-md`
|
|
63
|
-
- **Crucial**: ID must be set to `question.id` (`id={question.id}`) so scroll-to-error works!
|
|
64
|
-
|
|
65
|
-
2. **Heading (`question.text`)**:
|
|
66
|
-
- MUST use `dangerouslySetInnerHTML={{ __html: question.text }}` because text from Sprinklr contains rich HTML (bold, links, italics).
|
|
67
|
-
- If `question.required === true`, append a red asterisk `*`.
|
|
41
|
+
## Placeholder resolution
|
|
68
42
|
|
|
69
|
-
|
|
70
|
-
- Render conditionally if `question.description` exists.
|
|
71
|
-
- MUST use `dangerouslySetInnerHTML`.
|
|
72
|
-
- Style with slightly smaller text, greyish color (e.g., `text-sm text-gray-600 mt-2`).
|
|
43
|
+
Pass `allAnswers` and `allQuestions` so question text can reference prior answers:
|
|
73
44
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
## Hover Tooltips & Micro-interactions
|
|
78
|
-
- Focus states on text inputs MUST use the brand primary color (e.g., magenta `#e20074` for Telekom).
|
|
79
|
-
- Radio button wrappers should have a subtle background hover effect (e.g., `hover:bg-pink-50`).
|
|
45
|
+
```tsx
|
|
46
|
+
resolveSurveyQuestionPlaceholders(question.questionText, allAnswers, questionsById)
|
|
47
|
+
```
|
|
80
48
|
|
|
81
49
|
## Props Contract
|
|
82
50
|
|
|
@@ -85,8 +53,43 @@ type QuestionProps = {
|
|
|
85
53
|
question: SurveyQuestion;
|
|
86
54
|
selectedValue?: AnswerValue;
|
|
87
55
|
validationError?: string;
|
|
88
|
-
|
|
56
|
+
allAnswers: SurveyAnswers;
|
|
57
|
+
allQuestions: SurveyQuestion[];
|
|
58
|
+
onSelect: (answerValue: AnswerValue) => void;
|
|
89
59
|
};
|
|
90
60
|
```
|
|
91
61
|
|
|
92
|
-
Wire `onSelect` → `onAction({ type: 'CHANGE', payload: { questionId,
|
|
62
|
+
Wire `onSelect` → `onAction({ type: 'CHANGE', payload: { questionId, answerValue } })`.
|
|
63
|
+
|
|
64
|
+
## Container UI Structure
|
|
65
|
+
|
|
66
|
+
- Wrapper: `<section id={question.id} ...>` — **required** for scroll-to-error
|
|
67
|
+
- `questionText` / `questionDescription`: `dangerouslySetInnerHTML`
|
|
68
|
+
- Required asterisk when `question.required`
|
|
69
|
+
- Validation error banner when `validationError` is set
|
|
70
|
+
|
|
71
|
+
## Matrix value storage
|
|
72
|
+
|
|
73
|
+
Store matrix cell values as column **`id`** via `matrixColumnStoredValue(col)` — see [`value-derivation.md`](../02-reference/value-derivation.md).
|
|
74
|
+
|
|
75
|
+
## SDK Integration Checklist — inline types (agent)
|
|
76
|
+
|
|
77
|
+
### MCQ (`QUESTION_TYPE.MCQ`)
|
|
78
|
+
|
|
79
|
+
- [ ] Filter options with `getVisibleMcqOptionsForAnswers` before render
|
|
80
|
+
- [ ] Store `option.id`; support `isMultiSelect` array shape when configured
|
|
81
|
+
- [ ] onAction CHANGE: `{ questionId, answerValue: optionId | optionId[] }`
|
|
82
|
+
|
|
83
|
+
### TEXTFIELD (`QUESTION_TYPE.TEXTFIELD`)
|
|
84
|
+
|
|
85
|
+
- [ ] Render `inputVariant` (`short` single-line vs `long` textarea)
|
|
86
|
+
- [ ] onAction CHANGE: `{ questionId, answerValue: string }`
|
|
87
|
+
- [ ] Debounce long text if partial save enabled
|
|
88
|
+
|
|
89
|
+
### TEXT_AND_MEDIA (`QUESTION_TYPE.TEXT_AND_MEDIA`)
|
|
90
|
+
|
|
91
|
+
- [ ] No answer input — no CHANGE action
|
|
92
|
+
- [ ] Render media block when `question.media?.url` present
|
|
93
|
+
- [ ] User advances with NEXT
|
|
94
|
+
|
|
95
|
+
See [`question-type-sdk-matrix.md`](../00-integration/question-type-sdk-matrix.md) for all 11 types.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> **UI spec (authoritative):** `03-ui-specs/01-rating.md` (NPS) · `03-ui-specs/05-rating-scale.md` (star/emoji gap)
|
|
4
4
|
> **Reference implementation:** `apps/client/src/components/RatingScale.tsx`
|
|
5
5
|
> **Target Component**: `RatingScale`
|
|
6
|
-
> **Handles**: `type: '
|
|
6
|
+
> **Handles**: `type: 'NPS_SCALE'` (via `QUESTION_TYPE.NPS_SCALE`)
|
|
7
7
|
|
|
8
8
|
## Core Responsibility
|
|
9
9
|
Render a horizontal sequence of numbered badges.
|
|
@@ -28,3 +28,13 @@ The UI must exactly match enterprise standards:
|
|
|
28
28
|
3. **Selected State**: The selected badge MUST pop out prominently: `border-2 border-[#e20074] shadow-md scale-105 opacity-100 font-bold`.
|
|
29
29
|
4. **Hover State**: `hover:scale-105 hover:border-[#e20074] transition-all cursor-pointer`.
|
|
30
30
|
5. **Tooltips**: Every badge MUST have `title={option.label}` applied for accessibility and semantic context (e.g. hovering '10' shows 'Extremely Likely').
|
|
31
|
+
|
|
32
|
+
## SDK Integration Checklist (agent)
|
|
33
|
+
|
|
34
|
+
- [ ] Type literal: `QUESTION_TYPE.NPS_SCALE`
|
|
35
|
+
- [ ] Answer stored as: `number`
|
|
36
|
+
- [ ] onAction CHANGE: `{ questionId, answerValue: number }`
|
|
37
|
+
- [ ] Visibility helper: `getVisibleScaleColumnsForAnswers` when scale points have answer logic
|
|
38
|
+
- [ ] Custom fields: 4th arg on scale visibility helper when needed
|
|
39
|
+
- [ ] Logic features: skip ✓ display ✓ answer ✓ variants ✓ numbering ✓
|
|
40
|
+
- [ ] Matrix row: [`question-type-sdk-matrix.md`](../00-integration/question-type-sdk-matrix.md#nps_scale)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Star Rating Scale Architectural Blueprint
|
|
2
|
+
|
|
3
|
+
> **DEPRECATED:** API RATING pages map to `NPS_SCALE` or matrix display styles — not a separate `question.type`. Keep for historical reference only.
|
|
4
|
+
>
|
|
5
|
+
> **UI spec (authoritative):** `03-ui-specs/05-rating-scale.md`
|
|
6
|
+
> **SDK type:** `StarRatingQuestion` — `type: 'star_rating'`
|
|
7
|
+
> **Target Component**: `RatingScale` (star/emoji variant)
|
|
8
|
+
> **Handles**: `type: 'star_rating'` only — not `CSAT_MATRIX` (that is a matrix grid)
|
|
9
|
+
|
|
10
|
+
## Core Responsibility
|
|
11
|
+
|
|
12
|
+
Render star or emoji rating scales ensuring that active selections are unmistakably highlighted using bounding boxes or strong border accents, NEVER just by changing the SVG fill color.
|
|
13
|
+
|
|
14
|
+
## State Management & Mapping
|
|
15
|
+
|
|
16
|
+
- **Emojis**: `scaleStyle === 'emoji'`
|
|
17
|
+
- Map values using client-side emoji icons (react-icons) proportional to scale length.
|
|
18
|
+
- **Stars**: `scaleStyle === 'star'`
|
|
19
|
+
- Active stars (index <= selectedValue) use filled star icons from react-icons.
|
|
20
|
+
- **Numbers**: Derive display/submit value from `optionLabel` (see `02-reference/value-derivation.md`).
|
|
21
|
+
|
|
22
|
+
## UX Bounding Box Rules (CRITICAL)
|
|
23
|
+
|
|
24
|
+
### For Emojis & Stars:
|
|
25
|
+
|
|
26
|
+
- **Unselected**: `inline-flex p-2 border-2 border-transparent opacity-40 hover:opacity-100 hover:scale-110 transition-all cursor-pointer`.
|
|
27
|
+
- **Selected**: The specific selected icon MUST be wrapped in a prominent square box with rounded corners.
|
|
28
|
+
- **CSS**: `border-2 border-[#e20074] rounded-lg p-2 bg-pink-50 shadow-sm scale-110 opacity-100`.
|
|
29
|
+
|
|
30
|
+
### For Numbered Badges:
|
|
31
|
+
|
|
32
|
+
- **Shape**: MUST be square with rounded corners (`rounded-lg w-12 h-12`). Do not use full circles.
|
|
33
|
+
- **Selected**: `border-2 border-[#e20074] opacity-100 font-bold scale-105 shadow-md`.
|
|
34
|
+
- **Unselected**: `opacity-30 border-transparent`.
|
|
35
|
+
|
|
36
|
+
## Bounding Boxes inside Matrices
|
|
37
|
+
|
|
38
|
+
When emojis or stars are rendered inside `CsatMatrixScale`, the exact same bounding box logic applies. See `05-csat-matrix-scale.md`.
|
|
39
|
+
|
|
40
|
+
## Tooltips & Label Positions
|
|
41
|
+
|
|
42
|
+
- **Tooltips**: Every emoji, star, and badge MUST have `title={optionLabel}` on hover.
|
|
43
|
+
- **Labels (`minLabel`, `midLabel`, `maxLabel`)**:
|
|
44
|
+
- Render in `Question.tsx` above the scale (same pattern as `nps`).
|
|
45
|
+
- `midLabel` position: `style={{ left: \`\${(midLabelIndex / (options.length - 1)) * 100}%\` }}`.
|