@explorer02/cfm-survey-sdk 0.2.1 → 0.2.3

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.
Files changed (95) hide show
  1. package/dist/cli/index.js +18 -18
  2. package/dist/cli/index.mjs +14 -14
  3. package/dist/index.d.mts +465 -580
  4. package/dist/index.d.ts +465 -580
  5. package/dist/index.js +1 -1
  6. package/dist/index.mjs +1 -1
  7. package/package.json +6 -4
  8. package/postinstall.js +96 -69
  9. package/templates/AGENT.md +18 -8
  10. package/templates/docs/00-integration/analytics-events-catalog.md +54 -0
  11. package/templates/docs/00-integration/answer-logic-and-navigation.md +50 -0
  12. package/templates/docs/00-integration/aws-deploy.md +281 -0
  13. package/templates/docs/00-integration/client-integration-guide.md +351 -0
  14. package/templates/docs/00-integration/component-checklist.md +55 -22
  15. package/templates/docs/00-integration/constraints.md +49 -4
  16. package/templates/docs/00-integration/custom-field-logic-and-navigation.md +174 -0
  17. package/templates/docs/00-integration/display-logic-and-navigation.md +64 -0
  18. package/templates/docs/00-integration/end-page-logic.md +64 -0
  19. package/templates/docs/00-integration/file-upload-aws.md +116 -0
  20. package/templates/docs/00-integration/logic-fields-catalog.md +105 -0
  21. package/templates/docs/00-integration/partial-save-and-recovery.md +98 -0
  22. package/templates/docs/00-integration/placeholders-and-tokens.md +116 -0
  23. package/templates/docs/00-integration/progress.md +61 -0
  24. package/templates/docs/00-integration/question-display-variants.md +65 -0
  25. package/templates/docs/00-integration/question-numbering.md +51 -0
  26. package/templates/docs/00-integration/question-type-sdk-matrix.md +244 -0
  27. package/templates/docs/00-integration/setup.md +84 -0
  28. package/templates/docs/00-integration/skip-logic-and-navigation.md +80 -0
  29. package/templates/docs/00-integration/survey-lifecycle-analytics.md +94 -0
  30. package/templates/docs/00-integration/useSurveySDK.md +151 -23
  31. package/templates/docs/01-components/01-survey-page.md +76 -54
  32. package/templates/docs/01-components/02-question.md +61 -58
  33. package/templates/docs/01-components/03-rating-scale.md +11 -1
  34. package/templates/docs/01-components/04-star-rating-scale.md +45 -0
  35. package/templates/docs/01-components/05-csat-matrix-scale.md +20 -11
  36. package/templates/docs/01-components/06-likert-matrix-scale.md +14 -4
  37. package/templates/docs/01-components/07-slider-matrix-scale.md +11 -2
  38. package/templates/docs/01-components/08-file-upload-scale.md +23 -51
  39. package/templates/docs/01-components/09-custom-slider-track.md +2 -2
  40. package/templates/docs/01-components/11-progress-bar.md +10 -4
  41. package/templates/docs/01-components/13-matrix-dropdown.md +29 -32
  42. package/templates/docs/01-components/14-intro-page.md +34 -0
  43. package/templates/docs/01-components/15-end-page.md +31 -0
  44. package/templates/docs/01-components/16-paused-page.md +27 -0
  45. package/templates/docs/01-components/17-heatmap-scale.md +75 -0
  46. package/templates/docs/01-components/18-rank-order-scale.md +93 -0
  47. package/templates/docs/01-components/README.md +42 -36
  48. package/templates/docs/02-reference/config-field-index.md +141 -139
  49. package/templates/docs/02-reference/exports.md +149 -0
  50. package/templates/docs/02-reference/question-types/01-rating.md +31 -32
  51. package/templates/docs/02-reference/question-types/04-csat.md +51 -42
  52. package/templates/docs/02-reference/question-types/05-rating-scale.md +16 -11
  53. package/templates/docs/02-reference/question-types/06-slider.md +2 -2
  54. package/templates/docs/02-reference/question-types/07-matrix-cfm.md +44 -30
  55. package/templates/docs/02-reference/question-types/08-matrix-csat.md +4 -26
  56. package/templates/docs/02-reference/question-types/09-matrix-rating.md +29 -13
  57. package/templates/docs/02-reference/question-types/10-slider-matrix.md +15 -4
  58. package/templates/docs/02-reference/question-types/11-file-upload.md +43 -17
  59. package/templates/docs/02-reference/question-types/12-heatmap.md +59 -0
  60. package/templates/docs/02-reference/question-types/13-rank-order.md +101 -0
  61. package/templates/docs/02-reference/question-types/README.md +96 -47
  62. package/templates/docs/02-reference/routing-table.md +33 -18
  63. package/templates/docs/02-reference/value-derivation.md +63 -0
  64. package/templates/docs/03-ui-specs/00-question-shell.md +19 -88
  65. package/templates/docs/03-ui-specs/01-rating.md +18 -52
  66. package/templates/docs/03-ui-specs/02-radio.md +80 -37
  67. package/templates/docs/03-ui-specs/03-text.md +2 -2
  68. package/templates/docs/03-ui-specs/04-csat.md +68 -33
  69. package/templates/docs/03-ui-specs/05-rating-scale.md +21 -18
  70. package/templates/docs/03-ui-specs/06-slider.md +3 -1
  71. package/templates/docs/03-ui-specs/07-matrix-cfm.md +44 -25
  72. package/templates/docs/03-ui-specs/08-matrix-csat-rating.md +19 -43
  73. package/templates/docs/03-ui-specs/09-slider-matrix.md +18 -1
  74. package/templates/docs/03-ui-specs/10-file-upload.md +42 -21
  75. package/templates/docs/03-ui-specs/11-text-and-media.md +29 -29
  76. package/templates/docs/03-ui-specs/12-survey-chrome.md +58 -14
  77. package/templates/docs/03-ui-specs/13-heatmap.md +80 -0
  78. package/templates/docs/03-ui-specs/14-rank-order.md +128 -0
  79. package/templates/docs/03-ui-specs/README.md +16 -14
  80. package/templates/docs/03-ui-specs/shared/custom-slider-track.md +28 -35
  81. package/templates/docs/03-ui-specs/shared/matrix-dropdown.md +5 -0
  82. package/templates/docs/MANIFEST.json +476 -69
  83. package/templates/docs/index.md +82 -30
  84. package/templates/docs/templates/CustomSliderTrack.tsx +144 -0
  85. package/templates/docs/templates/MatrixDropdown.tsx +216 -0
  86. package/templates/docs/templates/Question.tsx +221 -154
  87. package/templates/docs/templates/RankOrderScale.tsx +353 -0
  88. package/templates/docs/templates/deploy-to-aws.sh +114 -0
  89. package/templates/docs/templates/heatmapCoords.ts +58 -0
  90. package/templates/docs/templates/implementation_plan.md +126 -67
  91. package/templates/docs/templates/survey-inventory.schema.json +77 -0
  92. package/templates/docs/templates/surveyUiIcons.tsx +52 -0
  93. package/templates/docs/templates/verify-agent-build.sh +85 -0
  94. package/templates/docs/01-components/04-csat-scale.md +0 -40
  95. package/templates/docs/02-question-types/01-rating.md +0 -44
@@ -0,0 +1,80 @@
1
+ # Skip Logic and Navigation
2
+
3
+ > Integrator-facing behavior contract. Skip evaluation is handled inside the SDK — do not import internal `surveyLogic` paths.
4
+
5
+ ## When skip rules run
6
+
7
+ - Skip rules live on individual questions (`question.skipLogics`).
8
+ - When a question has **multiple skip logic blocks**, the SDK evaluates them **top-to-bottom (API order)** and applies the **first** whose condition is true.
9
+ - On **Next**, the SDK evaluates skip logic on the **last question of the current page** only.
10
+ - Skip targets:
11
+ - `QUESTION` — jump forward to another question's page
12
+ - `END_OF_SURVEY` — trigger submit flow (validation bypass for skipped pages)
13
+
14
+ **Forward-only:** backward skip targets are rejected. The respondent cannot skip to an earlier page via skip logic.
15
+
16
+ ## Multiselect MCQ — count of choice
17
+
18
+ Multiselect MCQ skip rules may use `field: 'NUM_OF_SELECTED_OPTIONS'` (**Count of Choice** in the builder). The SDK compares the number of selected option ids with numeric operators (`EQUALS`, `NOT_EQUALS`, `GT`, `GTE`, `LT`, `LTE`). Zero selections is treated as missing for `MISSING` / `EXISTS`.
19
+
20
+ ## Custom field conditions
21
+
22
+ Skip rules may reference **custom fields** (`conditionEntity.entityType: 'CUSTOM_FIELD'`). See [`custom-field-logic-and-navigation.md`](custom-field-logic-and-navigation.md) for value shapes, operator contract, and the two wiring points (hook options + visibility helpers).
23
+
24
+ ## Simple example
25
+
26
+ | Page | Question | Rule |
27
+ |------|----------|------|
28
+ | 1 | Q1 (NPS) | If score > 7 → skip to Q4 |
29
+ | 2 | Q2 | (middle) |
30
+ | 3 | Q3 | (middle) |
31
+ | 4 | Q4 | destination |
32
+
33
+ 1. User sets Q1 = **5** → path is pages 1 → 2 → 3 → 4 (sequential).
34
+ 2. User sets Q1 = **9** → simulated path becomes pages **1 → 4** (pages 2–3 skipped).
35
+ 3. User clicks **Next** from page 1 → lands on page 4.
36
+
37
+ ## Answer changes (`onChange`)
38
+
39
+ When the edited question has skip logic, the SDK compares the route **before** and **after** the edit.
40
+
41
+ | Check | Meaning | SDK action |
42
+ |-------|---------|------------|
43
+ | **Path changed** | Page route reshaped (e.g. sequential → skip) | Clear answers on all pages after current page; reset skip/visit tracking ahead; refresh progress |
44
+ | **Pruned only** | Route unchanged but progress credits were stale | Remove off-path earned credits; refresh progress; **keep answers** |
45
+
46
+ **Example — path changed:** User walked page 2, answered Q2, then on page 1 changes Q1 from 5 → 9. Q2 answer is cleared because page 2 is no longer on the active path.
47
+
48
+ **Example — pruned only:** User already on skip path; edits Q1 from 8 → 9 (still skips). Route unchanged; stale progress credits for pages visited earlier may be removed without clearing answers.
49
+
50
+ ## Back navigation
51
+
52
+ - Back uses **`pageHistory`**, not `currentPageIndex - 1`.
53
+ - After a skip jump (e.g. page 1 → page 4), Back returns to the **skip origin** (page 1), not the sequential previous page.
54
+ - **Answers are preserved on Back** — the SDK does not sanitize answers when going back.
55
+ - Skip/visit tracking for pages ahead of the back target is reset.
56
+
57
+ ## Forward navigation (`onNext`)
58
+
59
+ - Validates **current page only** before moving.
60
+ - On skip: marks intermediate pages in `skippedPageIndices`; may sanitize answers on skipped pages.
61
+ - Sequential Next skips empty pages automatically.
62
+
63
+ ## Submit validation
64
+
65
+ - Submit validates **non-skipped pages** only (pages marked skipped via skip logic are excluded).
66
+ - End-of-survey skip triggers submit with `skipValidation: true` for remaining pages.
67
+
68
+ ## Optional question telemetry
69
+
70
+ When the user leaves a page (Next or Submit), the SDK logs analytics for **optional questions left blank** on that page. This does not block navigation or affect answers.
71
+
72
+ Event: `optional_question_skipped` with `questionId` and `pageIndex`.
73
+
74
+ ## Client responsibilities
75
+
76
+ - Wire `onAction({ type: 'NEXT' })`, `onAction({ type: 'PREVIOUS' })`, and `onAction({ type: 'CHANGE', payload: { questionId, answerValue } })`.
77
+ - Use `state.canGoBack` for the Back button (not `currentPageIndex === 0`).
78
+ - Use `state.primaryButtonType` for Next vs Submit label; SDK maps `NEXT` → `SUBMIT` on the last page when appropriate.
79
+
80
+ See also: [`progress.md`](progress.md), [`useSurveySDK.md`](useSurveySDK.md), [`display-logic-and-navigation.md`](display-logic-and-navigation.md), [`answer-logic-and-navigation.md`](answer-logic-and-navigation.md), [`logic-fields-catalog.md`](logic-fields-catalog.md).
@@ -0,0 +1,94 @@
1
+ # Survey Lifecycle and Analytics
2
+
3
+ > Covers `sdkFeatures.markAsStarted` and `sdkFeatures.questionActivity` — first-interaction tracking and per-question view telemetry.
4
+
5
+ ---
6
+
7
+ ## Mark as started
8
+
9
+ The SDK automatically marks the survey as started on the **first `CHANGE` action** (any answer update). No separate client call is required when using `useSurveySDK`.
10
+
11
+ Additionally, fire **`SCROLL`** once on the user's first scroll to capture engagement before first answer:
12
+
13
+ ```typescript
14
+ useEffect(() => {
15
+ let scrolled = false;
16
+ const onScroll = () => {
17
+ if (!scrolled) {
18
+ scrolled = true;
19
+ onAction({ type: 'SCROLL' });
20
+ }
21
+ };
22
+ window.addEventListener('scroll', onScroll, { passive: true });
23
+ return () => window.removeEventListener('scroll', onScroll);
24
+ }, [onAction]);
25
+ ```
26
+
27
+ Attach to the survey container scroll root if the page uses an inner scroll area instead of `window`.
28
+
29
+ ---
30
+
31
+ ## Question activity (`RECORD_QUESTION_VIEW`)
32
+
33
+ Fire when a question enters the viewport so the backend records view events. Use `IntersectionObserver`:
34
+
35
+ ```typescript
36
+ useEffect(() => {
37
+ const seen = new Set<string>();
38
+ const observer = new IntersectionObserver(
39
+ (entries) => {
40
+ const ids: string[] = [];
41
+ for (const entry of entries) {
42
+ if (entry.isIntersecting) {
43
+ const id = entry.target.id.replace(/^question-/, '');
44
+ if (!seen.has(id)) {
45
+ seen.add(id);
46
+ ids.push(id);
47
+ }
48
+ }
49
+ }
50
+ if (ids.length > 0) {
51
+ onAction({ type: 'RECORD_QUESTION_VIEW', payload: { questionIds: ids } });
52
+ }
53
+ },
54
+ { threshold: 0.5 }
55
+ );
56
+
57
+ for (const q of currentQuestions) {
58
+ const el = document.getElementById(`question-${q.id}`);
59
+ if (el) observer.observe(el);
60
+ }
61
+
62
+ return () => observer.disconnect();
63
+ }, [currentQuestions, onAction]);
64
+ ```
65
+
66
+ ### Requirements
67
+
68
+ - Every question wrapper must have `id={`question-${question.id}`}` (Phase 6 verify).
69
+ - Re-run the effect when `currentQuestions` changes (multi-page surveys, display logic).
70
+ - SDK **deduplicates** question IDs server-side per session — client `seen` set avoids duplicate actions in one mount.
71
+
72
+ ---
73
+
74
+ ## Batching
75
+
76
+ `RECORD_QUESTION_VIEW` accepts `{ questionIds: string[] }`. Batch all newly visible IDs from one observer callback tick rather than one action per question when multiple enter view together.
77
+
78
+ ---
79
+
80
+ ## Debug: `logic_session_summary`
81
+
82
+ When `debug: true` is set in hook options, the SDK emits structured console events including lifecycle transitions. Full event catalog: [`analytics-events-catalog.md`](analytics-events-catalog.md).
83
+
84
+ ---
85
+
86
+ ## Wiring checklist
87
+
88
+ - [ ] `SCROLL` fired once on first scroll
89
+ - [ ] First `CHANGE` left to SDK (auto mark-as-started)
90
+ - [ ] `IntersectionObserver` on `#question-{id}` wrappers
91
+ - [ ] Effect depends on `currentQuestions` — re-observe on page change
92
+ - [ ] No duplicate `RECORD_QUESTION_VIEW` spam (client dedup + SDK dedup)
93
+
94
+ See [`client-integration-guide.md`](client-integration-guide.md) and [`implementation_plan.md`](../templates/implementation_plan.md) §1b.
@@ -13,9 +13,7 @@ import {
13
13
  type UseSurveySDKReturn,
14
14
  type SurveyQuestion,
15
15
  type AnswerValue,
16
- type MatrixAnswerMap,
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
- | `state` | `currentPageIndex` | `number` | 0-based page index |
59
- | | `currentQuestions` | `SurveyQuestion[]` | Questions on current page |
60
- | | `values` | `Record<string, AnswerValue>` | All answers so far |
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', value: 4 } });
135
+ onAction({ type: ACTIONS.CHANGE, payload: { questionId: 'q1', answerValue: 4 } });
72
136
 
73
137
  // Matrix answer
74
- onAction({ type: ACTIONS.CHANGE, payload: { questionId: 'q2', value: { row1: 5, row2: 2 } } });
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; auto-SUBMIT on last page
78
- onAction({ type: ACTIONS.PREVIOUS });
79
- onAction({ type: ACTIONS.SUBMIT }); // Validates ALL pages, then submits
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
- - `text_and_media` is display-only — always skipped
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` on the last page. While submitting, `submitSurveyResults.isLoading` is `true`. On success, show thank-you UI when `submitSurveyResults.data` is set.
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 Icons (from package)
231
+ ## CSAT / rating emoji UI (client-implemented)
102
232
 
103
- ```typescript
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
- Use `getEmojiForIndex(index, totalOptions)` for proportional emoji mapping on non-5-point scales. Install `react-icons` in the client project.
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**: The main entry point that wires the UI to the `useSurveySDK` hook, manages loading/error states, orchestrates pagination, and controls submission.
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
- ## Setup & Wiring (CRITICAL)
10
+ Act as the **phase state machine** for the survey. Never render questions directly — delegate to `Question.tsx`.
12
11
 
13
- The component MUST invoke `useSurveySDK`, passing the **instanceId** from the client prompt.
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 { surveyQueryResults, submitSurveyResults, state, onAction } = useSurveySDK({
22
- options: {
23
- instanceId: 'YOUR_INSTANCE_ID_HERE', // From client prompt
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
- // ... state machine rendering
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
- ## The 5 State Machine Renders (MANDATORY)
47
+ See [`client-integration-guide.md`](../00-integration/client-integration-guide.md) for lifecycle effects (`SCROLL`, `RECORD_QUESTION_VIEW`).
37
48
 
38
- You MUST structure your render logic to strictly handle these 5 states in order to avoid UI crashes when data is not yet available.
49
+ ## Render Order (MANDATORY)
39
50
 
40
- ### 1. Loading State
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
- ### 2. Error State
45
- If `surveyQueryResults.error` is true, render a red Error banner containing `surveyQueryResults.error.message`.
53
+ ### 1. Loading / Error / Empty
46
54
 
47
- ### 3. Empty State
48
- If `!survey`, render a generic "Survey not found" message.
55
+ | Condition | UI |
56
+ |-----------|-----|
57
+ | `surveyQueryResults.isLoading` | Spinner + Header/Footer |
58
+ | `surveyQueryResults.error` | Error message |
59
+ | `!survey` | "No survey data" |
49
60
 
50
- ### 4. Submitted State
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
- ### 5. Active Survey State
55
- If the above 4 conditions are met, render the active survey page:
56
- 1. `Header` (Logo)
57
- 2. `ProgressBar` (Pass `state.progressPercentage`)
58
- 3. `LanguageSelector` (Pass `survey.languages`, `survey.language`, `setSelectedLanguage`)
59
- 4. **The Question Map** (See below)
60
- 5. **Navigation Buttons** (See below)
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
- You MUST iterate over `state.currentQuestions` and dispatch to `Question.tsx`.
85
+ Pass **all answers and questions** for placeholder resolution (`{{SURVEY_QUESTION-:-qId}}`):
66
86
 
67
87
  ```tsx
68
- <div className="space-y-12">
69
- {state.currentQuestions.map(question => (
70
- <Question
71
- key={question.id}
72
- question={question}
73
- selectedValue={state.values[question.id]}
74
- validationError={state.validationErrors[question.id]}
75
- onSelect={value => onAction({
76
- type: 'CHANGE',
77
- payload: { questionId: question.id, value }
78
- })}
79
- />
80
- ))}
81
- </div>
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 Logic
104
+ ## Navigation Buttons
85
105
 
86
- You MUST conditionally render "Back" and "Next/Submit" buttons.
87
-
88
- - **Back Button**: Hide if `state.currentPageIndex === 0`.
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
- - If `state.currentPageIndex < survey.pages.length - 1`, label is "Next", action is `type: 'NEXT'`.
93
- - If on the last page, label is "Submit", action is `type: 'NEXT'` (the SDK intercepts NEXT on the last page to trigger submission automatically!).
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`.