@explorer02/cfm-survey-sdk 0.2.0 → 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.
Files changed (88) 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 -64
  9. package/templates/AGENT.md +15 -7
  10. package/templates/docs/00-integration/answer-logic-and-navigation.md +50 -0
  11. package/templates/docs/00-integration/aws-deploy.md +281 -0
  12. package/templates/docs/00-integration/client-integration-guide.md +351 -0
  13. package/templates/docs/00-integration/component-checklist.md +84 -0
  14. package/templates/docs/00-integration/constraints.md +65 -3
  15. package/templates/docs/00-integration/custom-field-logic-and-navigation.md +174 -0
  16. package/templates/docs/00-integration/display-logic-and-navigation.md +49 -0
  17. package/templates/docs/00-integration/end-page-logic.md +64 -0
  18. package/templates/docs/00-integration/partial-save-and-recovery.md +74 -0
  19. package/templates/docs/00-integration/placeholders-and-tokens.md +116 -0
  20. package/templates/docs/00-integration/progress.md +61 -0
  21. package/templates/docs/00-integration/question-display-variants.md +65 -0
  22. package/templates/docs/00-integration/question-numbering.md +51 -0
  23. package/templates/docs/00-integration/question-type-sdk-matrix.md +242 -0
  24. package/templates/docs/00-integration/setup.md +84 -0
  25. package/templates/docs/00-integration/skip-logic-and-navigation.md +80 -0
  26. package/templates/docs/00-integration/survey-lifecycle-analytics.md +94 -0
  27. package/templates/docs/00-integration/useSurveySDK.md +151 -23
  28. package/templates/docs/01-components/01-survey-page.md +76 -54
  29. package/templates/docs/01-components/02-question.md +77 -89
  30. package/templates/docs/01-components/03-rating-scale.md +11 -1
  31. package/templates/docs/01-components/04-star-rating-scale.md +45 -0
  32. package/templates/docs/01-components/05-csat-matrix-scale.md +20 -11
  33. package/templates/docs/01-components/06-likert-matrix-scale.md +12 -3
  34. package/templates/docs/01-components/07-slider-matrix-scale.md +11 -2
  35. package/templates/docs/01-components/08-file-upload-scale.md +10 -1
  36. package/templates/docs/01-components/09-custom-slider-track.md +2 -2
  37. package/templates/docs/01-components/11-progress-bar.md +10 -4
  38. package/templates/docs/01-components/13-matrix-dropdown.md +3 -1
  39. package/templates/docs/01-components/14-intro-page.md +34 -0
  40. package/templates/docs/01-components/15-end-page.md +31 -0
  41. package/templates/docs/01-components/16-paused-page.md +27 -0
  42. package/templates/docs/01-components/17-heatmap-scale.md +75 -0
  43. package/templates/docs/01-components/18-rank-order-scale.md +100 -0
  44. package/templates/docs/01-components/README.md +44 -36
  45. package/templates/docs/02-reference/config-field-index.md +141 -139
  46. package/templates/docs/02-reference/exports.md +149 -0
  47. package/templates/docs/02-reference/question-types/01-rating.md +31 -32
  48. package/templates/docs/02-reference/question-types/04-csat.md +51 -42
  49. package/templates/docs/02-reference/question-types/05-rating-scale.md +16 -11
  50. package/templates/docs/02-reference/question-types/06-slider.md +2 -2
  51. package/templates/docs/02-reference/question-types/07-matrix-cfm.md +44 -30
  52. package/templates/docs/02-reference/question-types/08-matrix-csat.md +4 -26
  53. package/templates/docs/02-reference/question-types/09-matrix-rating.md +29 -13
  54. package/templates/docs/02-reference/question-types/10-slider-matrix.md +15 -4
  55. package/templates/docs/02-reference/question-types/11-file-upload.md +13 -2
  56. package/templates/docs/02-reference/question-types/12-heatmap.md +59 -0
  57. package/templates/docs/02-reference/question-types/13-rank-order.md +101 -0
  58. package/templates/docs/02-reference/question-types/README.md +86 -47
  59. package/templates/docs/02-reference/routing-table.md +33 -18
  60. package/templates/docs/02-reference/value-derivation.md +63 -0
  61. package/templates/docs/03-ui-specs/00-question-shell.md +24 -80
  62. package/templates/docs/03-ui-specs/01-rating.md +12 -54
  63. package/templates/docs/03-ui-specs/02-radio.md +21 -33
  64. package/templates/docs/03-ui-specs/03-text.md +2 -2
  65. package/templates/docs/03-ui-specs/04-csat.md +38 -33
  66. package/templates/docs/03-ui-specs/05-rating-scale.md +21 -18
  67. package/templates/docs/03-ui-specs/06-slider.md +3 -1
  68. package/templates/docs/03-ui-specs/07-matrix-cfm.md +25 -22
  69. package/templates/docs/03-ui-specs/08-matrix-csat-rating.md +18 -46
  70. package/templates/docs/03-ui-specs/09-slider-matrix.md +2 -2
  71. package/templates/docs/03-ui-specs/10-file-upload.md +2 -2
  72. package/templates/docs/03-ui-specs/11-text-and-media.md +29 -29
  73. package/templates/docs/03-ui-specs/12-survey-chrome.md +20 -14
  74. package/templates/docs/03-ui-specs/13-heatmap.md +73 -0
  75. package/templates/docs/03-ui-specs/14-rank-order.md +59 -0
  76. package/templates/docs/03-ui-specs/README.md +14 -13
  77. package/templates/docs/03-ui-specs/shared/custom-slider-track.md +3 -3
  78. package/templates/docs/03-ui-specs/shared/matrix-dropdown.md +6 -1
  79. package/templates/docs/MANIFEST.json +454 -67
  80. package/templates/docs/index.md +84 -29
  81. package/templates/docs/templates/Question.tsx +310 -0
  82. package/templates/docs/templates/deploy-to-aws.sh +114 -0
  83. package/templates/docs/templates/heatmapCoords.ts +58 -0
  84. package/templates/docs/templates/implementation_plan.md +115 -49
  85. package/templates/docs/templates/survey-inventory.schema.json +72 -0
  86. package/templates/docs/templates/verify-agent-build.sh +53 -0
  87. package/templates/docs/01-components/04-csat-scale.md +0 -40
  88. package/templates/docs/02-question-types/01-rating.md +0 -44
@@ -4,22 +4,36 @@
4
4
 
5
5
  Master guide for AI coding agents. Machine-readable index: `MANIFEST.json`.
6
6
 
7
+ **Installed package path:** `node_modules/@explorer02/cfm-survey-sdk/templates/docs/` (docs ship inside `templates/` — no postinstall copy needed).
8
+
7
9
  ## Documentation Layout
8
10
 
9
11
  ```
10
- docs/
12
+ templates/docs/ ← installed npm package layout
13
+ packages/sdk/templates/docs/ ← monorepo layout
11
14
  ├── index.md ← YOU ARE HERE
12
15
  ├── MANIFEST.json ← Phase + question-type routing (for tooling)
13
16
  ├── 00-integration/ ← Setup, hook contract, constraints
14
- ├── 01-components/ ← Wiring blueprints (read ALL before coding)
15
- ├── 02-reference/ ← Routing table + config-field-index (on demand)
16
- ├── 03-ui-specs/ ← Authoritative UI specs (reference-client driven)
17
+ ├── 01-components/ ← Wiring blueprints (read per type from MANIFEST)
18
+ ├── 02-reference/ ← Routing table, exports, config-field-index
19
+ ├── 03-ui-specs/ ← Authoritative UI specs
17
20
  └── templates/
18
- └── implementation_plan.md
21
+ ├── implementation_plan.md
22
+ ├── survey-inventory.schema.json
23
+ ├── verify-agent-build.sh
24
+ ├── deploy-to-aws.sh
25
+ ├── heatmapCoords.ts
26
+ └── Question.tsx ← Canonical dispatcher (copy before building)
19
27
  ```
20
28
 
21
29
  **Import rule:** Use whatever package name appears in the project's `package.json` (`@explorer02/cfm-survey-sdk` or `@repo/sdk`).
22
30
 
31
+ ## Do Not Read
32
+
33
+ - Internal SDK mapper/source paths (`packages/sdk/src/**` or `node_modules/.../src/**`)
34
+ - `cfm-sdk init`, `survey-config.json`, or any setup wizard
35
+ - Stale `apps/client` code without verifying type literals against `MANIFEST.json` `typeLiterals`
36
+
23
37
  ---
24
38
 
25
39
  ## Client Inputs (3 only)
@@ -36,7 +50,9 @@ docs/
36
50
 
37
51
  ### PHASE 0: ORIENT
38
52
 
39
- - Read this file and `AGENT.md`
53
+ - Read this file and `AGENT.md`:
54
+ - **Monorepo:** `packages/sdk/templates/AGENT.md`
55
+ - **Installed npm:** `node_modules/@explorer02/cfm-survey-sdk/templates/AGENT.md`
40
56
  - Detect framework: read `package.json` — Next.js / Vite / CRA (see `00-integration/setup.md`)
41
57
  - Extract branding and layout from client prompt
42
58
  - For existing apps: read project structure; do not blindly scaffold
@@ -55,56 +71,93 @@ Ask: *"Please provide your survey instance ID (JWT token)."*
55
71
 
56
72
  ### PHASE 3: SDK CONTRACT
57
73
 
58
- Read: `00-integration/useSurveySDK.md`
74
+ Read: `00-integration/client-integration-guide.md` (start here), `00-integration/question-type-sdk-matrix.md`, `00-integration/useSurveySDK.md`, `02-reference/exports.md`, `00-integration/skip-logic-and-navigation.md`, `00-integration/display-logic-and-navigation.md`, `00-integration/question-display-variants.md`, `00-integration/question-numbering.md`, `00-integration/placeholders-and-tokens.md`, `00-integration/partial-save-and-recovery.md`, `00-integration/survey-lifecycle-analytics.md`, `00-integration/end-page-logic.md`, `00-integration/answer-logic-and-navigation.md`, `00-integration/custom-field-logic-and-navigation.md`, `00-integration/progress.md`, `02-reference/value-derivation.md`
59
75
 
60
- Understand `useSurveySDK`, `onAction`, states. Do **not** read internal mapper/source paths.
76
+ Understand `useSurveySDK`, `onAction`, `state.answers`. Do **not** read internal mapper/source paths.
61
77
 
62
78
  Optional: read `02-reference/routing-table.md` for type overview.
63
79
 
64
80
  ### PHASE 4: PLAN + INGEST (CRITICAL)
65
81
 
66
- 1. Read **every** file in `01-components/` (all 14 blueprints)
67
- 2. Copy `docs/templates/implementation_plan.md` `./implementation_plan.md`
68
- 3. Fill every section do **not** invent a different structure
69
- 4. **Auto-proceed to Phase 5** do not wait for approval
82
+ 1. Fetch survey with `instanceId`; extract unique `question.type` values from `survey.pages`
83
+ 2. Write inventory to `implementation_plan.md` §3b (validate against `templates/survey-inventory.schema.json`)
84
+ 3. Read **always**: `component-checklist.md`, `00-integration/question-type-sdk-matrix.md`, `03-ui-specs/00-question-shell.md`, `03-ui-specs/12-survey-chrome.md`, `01-components/01-survey-page.md`, `01-components/02-question.md`, `01-components/14-intro-page.md`, `01-components/15-end-page.md`, `01-components/16-paused-page.md`
85
+ 4. For **each type in the survey**, read MANIFEST `questionTypes.{type}` blueprint + reference + uiSpec; cross-check the matching row in `question-type-sdk-matrix.md`
86
+ 5. When config flags match MANIFEST `sharedUiSpecTriggers` (e.g. `gridLayout:dropdown`, `displayStyle:graphics`) → read matching `03-ui-specs/shared/` specs
87
+ 6. Copy `docs/templates/implementation_plan.md` → `./implementation_plan.md` and fill every section
88
+ 7. Copy `docs/templates/Question.tsx` as the dispatcher starting point
89
+ 8. **Auto-proceed to Phase 5** — do not wait for approval
70
90
 
71
- For each question type in the fetched survey, also read matching docs from `02-reference/question-types/` (see `MANIFEST.json` or `02-reference/routing-table.md`).
91
+ **Fallback:** If survey fetch fails, read all `01-components/*.md` blueprints before coding.
72
92
 
73
93
  ### PHASE 5: BUILD
74
94
 
75
- Read: `00-integration/constraints.md`
95
+ Read: `00-integration/constraints.md`, `00-integration/component-checklist.md`, `00-integration/question-type-sdk-matrix.md`
76
96
 
77
- For each question in `state.currentQuestions`:
97
+ Create all required scale component files **before** wiring `Question.tsx`. For each question in the survey:
78
98
 
79
- 1. Read `02-reference/question-types/{type}.md` for data shape
80
- 2. Read `03-ui-specs/{NN}-{type}.md` (use `MANIFEST.json` `uiSpecs`; split matrix by `subType`)
81
- 3. If `matrixFormat`, `buttonType`, or `sliderType` is present → read `03-ui-specs/shared/` specs
99
+ 1. Read the matching row in `question-type-sdk-matrix.md` (answer shape, visibility helper, performance)
100
+ 2. Read `02-reference/question-types/` doc for data shape (MANIFEST `reference`)
101
+ 2. Read matching `03-ui-specs/` (MANIFEST `uiSpec`)
102
+ 3. If `gridLayout`, `displayStyle`, or `statementLayout` triggers shared specs → read `03-ui-specs/shared/`
82
103
  4. Cross-check `02-reference/config-field-index.md` for every non-default field
83
- 5. Match reference client behavior in `apps/client/src/components/` when available
104
+ 5. Implement per ui-spec trust SDK Integration Checklist in blueprint/reference + `templates/Question.tsx` + exported SDK types + `02-reference/value-derivation.md`
84
105
 
85
106
  Implement components per blueprints + UI specs. Use exported types:
86
107
 
87
108
  ```typescript
88
- import { useSurveySDK, ACTIONS, type SurveyAction } from '@explorer02/cfm-survey-sdk';
109
+ import { useSurveySDK, ACTIONS, QUESTION_TYPE, type SurveyAction } from '@explorer02/cfm-survey-sdk';
89
110
  ```
90
111
 
112
+ **Dispatch on flat `question.type` only** — **11** `QUESTION_TYPE` literals, no `subType`. See `MANIFEST.json` `typeLiterals`.
113
+
114
+ **AWS deploy prep (when client prompt mentions AWS / CloudFront / S3 / EC2 / Sprinklr):** apply Next.js static export config **during this phase**, not at deploy time. Read `00-integration/setup.md#nextjs-aws-static-export` and `00-integration/aws-deploy.md` § Build Config. Fill `implementation_plan.md` §8.
115
+
91
116
  ### PHASE 6: VERIFY
92
117
 
93
118
  Read: `00-integration/constraints.md` (troubleshooting)
94
119
 
120
+ Run: `bash templates/verify-agent-build.sh` (from project root, after copying script if needed)
121
+
95
122
  Checklist:
96
123
 
97
- - [ ] `Question.tsx` switch covers all 12 `question.type` values
124
+ - [ ] `Question.tsx` copied from `docs/templates/Question.tsx` — covers all **11** `question.type` values
125
+ - [ ] Each matrix variant routes by its own type literal (`CFM_MATRIX`, `CSAT_MATRIX`, `RATING_MATRIX`) — **no `subType`**
126
+ - [ ] All scale components from `component-checklist.md` exist for types in survey
127
+ - [ ] **Zero stub text:** `grep -r "not yet implemented" src/ components/ app/` returns no matches
98
128
  - [ ] Every question wrapper has `id={question.id}`
99
- - [ ] No imports from `@explorer02/cfm-survey-sdk/src/...`
100
- - [ ] `SurveyPage` handles loading / error / empty / submitted / active
129
+ - [ ] No imports from `@explorer02/cfm-survey-sdk/src/...` or `@repo/sdk/src/...`
130
+ - [ ] `SurveyPage` handles loading / error / empty / all `state.phase` values / active content
131
+ - [ ] Hook uses `state.answers`, `state.canGoBack`, `state.primaryButtonType`, and `payload.answerValue`
132
+ - [ ] Stable `customFieldValues` ref when survey uses custom-field logic; passed to hook and `Question` visibility helpers
133
+ - [ ] `SURVEY_PLACEHOLDERS` stable ref; `surveyOptions` in `useMemo` (see `placeholders-and-tokens.md`)
134
+ - [ ] `SCROLL` + `RECORD_QUESTION_VIEW` lifecycle effects wired (see `survey-lifecycle-analytics.md`)
135
+ - [ ] Monorepo: `packages/sdk` built before client dev (`npm run build`)
101
136
  - [ ] `npm run build` exits 0
137
+ - [ ] **AWS intent:** `out/index.html` exists; file count ≤ 500; Next.js static export configured (`output: export`, `assetPrefix: "./"`)
102
138
 
103
139
  ### PHASE 7: DEPLOY (INPUT 3)
104
140
 
105
141
  Ask: *"Your survey is built and verified. Ready to deploy?"*
106
142
 
107
- If yes: `npx cfm-sdk deploy` (preferred) or `npx vercel --prod --yes`
143
+ **Route by client prompt** (see MANIFEST `deployTargets` and `phases[7].selectTargetFromPrompt`):
144
+
145
+ | Prompt mentions | Target | Action |
146
+ |-----------------|--------|--------|
147
+ | AWS, CloudFront, S3, EC2, Sprinklr | **AWS** | Read `00-integration/aws-deploy.md`; set env vars; run 3-channel deploy or `bash templates/deploy-to-aws.sh` |
148
+ | Vercel or unspecified (default) | **Vercel** | `npx cfm-sdk deploy` (requires `VERCEL_TOKEN` or Vercel CLI) |
149
+
150
+ **AWS env vars (dev machine):**
151
+
152
+ ```bash
153
+ export CFM_INSTANCE_ID="<same JWT as SurveyPage>"
154
+ export CFM_DEPLOY_API_BASE="http://43.204.26.213:3000"
155
+ export OUT_DIR="<absolute path to out/ or dist/>"
156
+ ```
157
+
158
+ **AWS success:** print live CloudFront HTTPS URL from `/deploy/complete`; ask client to open in browser.
159
+
160
+ **Vercel success:** CLI prints production URL.
108
161
 
109
162
  ---
110
163
 
@@ -112,9 +165,11 @@ If yes: `npx cfm-sdk deploy` (preferred) or `npx vercel --prod --yes`
112
165
 
113
166
  | When | Read |
114
167
  |------|------|
115
- | Always first | `index.md`, `00-integration/setup.md`, `00-integration/useSurveySDK.md`, `00-integration/constraints.md` |
116
- | Before any React code | All `01-components/*.md` |
117
- | On demand | `02-reference/question-types/<type>.md` for types in survey |
118
- | Before building each question | `03-ui-specs/` per `MANIFEST.uiSpecs` |
168
+ | Always first | `index.md`, `00-integration/client-integration-guide.md`, `00-integration/question-type-sdk-matrix.md`, `00-integration/setup.md`, `00-integration/useSurveySDK.md`, `00-integration/constraints.md` |
169
+ | Lifecycle & placeholders | `00-integration/survey-lifecycle-analytics.md`, `00-integration/placeholders-and-tokens.md`, `00-integration/partial-save-and-recovery.md` |
170
+ | Skip logic & progress | `00-integration/skip-logic-and-navigation.md`, `00-integration/display-logic-and-navigation.md`, `00-integration/question-display-variants.md`, `00-integration/question-numbering.md`, `00-integration/end-page-logic.md`, `00-integration/answer-logic-and-navigation.md`, `00-integration/custom-field-logic-and-navigation.md`, `00-integration/progress.md` |
171
+ | Before any React code | `component-checklist.md`, `templates/Question.tsx`, MANIFEST `readAlways` docs |
172
+ | After instanceId fetch | Per-type docs from MANIFEST `questionTypes` (blocking: `CFM_MATRIX`, `CSAT_MATRIX`, `RATING_MATRIX`, `SLIDER_MATRIX`, `FILE_UPLOAD`) |
119
173
  | Config audit | `02-reference/config-field-index.md` |
120
- | Tooling | `MANIFEST.json`, `templates/implementation_plan.md` |
174
+ | AWS deploy | `00-integration/aws-deploy.md`, `setup.md#nextjs-aws-static-export`, `templates/deploy-to-aws.sh` |
175
+ | Tooling | `MANIFEST.json`, `templates/implementation_plan.md`, `templates/survey-inventory.schema.json` |
@@ -0,0 +1,310 @@
1
+ /**
2
+ * CANONICAL QUESTION DISPATCHER TEMPLATE
3
+ *
4
+ * Agents: copy this file to your project's components folder (e.g. src/components/Question.tsx).
5
+ * Covers all 11 QUESTION_TYPE literals — see question-type-sdk-matrix.md for per-type wiring.
6
+ * Route on `question.type` / `QUESTION_TYPE` only — no subType.
7
+ *
8
+ * Replace @explorer02/cfm-survey-sdk with your package.json import name if different.
9
+ */
10
+ 'use client';
11
+
12
+ import React, { useMemo } from 'react';
13
+ import type { SurveyQuestion, AnswerValue, SurveyAnswers, CustomFieldValueMap, HeatmapAnswer, HeatmapQuestion } from '@explorer02/cfm-survey-sdk';
14
+ import { QUESTION_TYPE, resolveSurveyQuestionPlaceholders, getVisibleMatrixItemsForAnswers, getVisibleMcqOptionsForAnswers, getVisibleRankOrderOptionsForAnswers } from '@explorer02/cfm-survey-sdk';
15
+ import { CsatMatrixScale } from './CsatMatrixScale';
16
+ import { SliderMatrixScale } from './SliderMatrixScale';
17
+ import { LikertMatrixScale } from './LikertMatrixScale';
18
+ import { FileUploadScale } from './FileUploadScale';
19
+ import { HeatmapScale } from './HeatmapScale';
20
+ import { RankOrderScale } from './RankOrderScale';
21
+ import RatingScale from './RatingScale';
22
+
23
+ type QuestionProps = {
24
+ question: SurveyQuestion;
25
+ selectedValue?: AnswerValue;
26
+ validationError?: string;
27
+ allAnswers: SurveyAnswers;
28
+ allQuestions: SurveyQuestion[];
29
+ customFieldValues?: CustomFieldValueMap;
30
+ onSelect: (answerValue: AnswerValue) => void;
31
+ };
32
+
33
+ export default function Question({
34
+ question,
35
+ selectedValue,
36
+ validationError,
37
+ allAnswers,
38
+ allQuestions,
39
+ customFieldValues = {},
40
+ onSelect,
41
+ }: QuestionProps) {
42
+ const questionsById = useMemo(
43
+ () => new Map(allQuestions.map(q => [q.id, q])),
44
+ [allQuestions]
45
+ );
46
+
47
+ const displayQuestionText = useMemo(
48
+ () => resolveSurveyQuestionPlaceholders(question.questionText, allAnswers, questionsById),
49
+ [question.questionText, allAnswers, questionsById]
50
+ );
51
+
52
+ const displayQuestionDescription = useMemo(
53
+ () =>
54
+ question.questionDescription
55
+ ? resolveSurveyQuestionPlaceholders(question.questionDescription, allAnswers, questionsById)
56
+ : undefined,
57
+ [question.questionDescription, allAnswers, questionsById]
58
+ );
59
+
60
+ const matrixQuestion = useMemo(() => {
61
+ const isMatrix =
62
+ question.type === QUESTION_TYPE.CFM_MATRIX ||
63
+ question.type === QUESTION_TYPE.CSAT_MATRIX ||
64
+ question.type === QUESTION_TYPE.RATING_MATRIX ||
65
+ question.type === QUESTION_TYPE.SLIDER_MATRIX;
66
+
67
+ if (!isMatrix) return question;
68
+
69
+ const { statementRows, scaleColumns } = getVisibleMatrixItemsForAnswers(
70
+ question,
71
+ allAnswers,
72
+ allQuestions,
73
+ customFieldValues
74
+ );
75
+
76
+ return {
77
+ ...question,
78
+ statementRows,
79
+ ...(question.type === QUESTION_TYPE.CSAT_MATRIX || question.type === QUESTION_TYPE.CFM_MATRIX
80
+ ? { scaleColumns }
81
+ : {}),
82
+ };
83
+ }, [question, allAnswers, allQuestions, customFieldValues]);
84
+
85
+ const visibleMcqOptions = useMemo(() => {
86
+ if (question.type !== QUESTION_TYPE.MCQ) return [];
87
+ return getVisibleMcqOptionsForAnswers(question, allAnswers, allQuestions, customFieldValues);
88
+ }, [question, allAnswers, allQuestions, customFieldValues]);
89
+
90
+ const rankOrderQuestion = useMemo(() => {
91
+ if (question.type !== QUESTION_TYPE.RANK_ORDER) return null;
92
+ return {
93
+ ...question,
94
+ options: getVisibleRankOrderOptionsForAnswers(
95
+ question,
96
+ allAnswers,
97
+ allQuestions,
98
+ customFieldValues
99
+ ),
100
+ };
101
+ }, [question, allAnswers, allQuestions, customFieldValues]);
102
+
103
+ return (
104
+ <section id={question.id} className="space-y-4 rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
105
+ <h2 className="text-lg font-medium leading-relaxed text-gray-900">
106
+ <span dangerouslySetInnerHTML={{ __html: displayQuestionText }} />
107
+ {question.required && <span className="text-red-500 ml-1">*</span>}
108
+ </h2>
109
+ {displayQuestionDescription && (
110
+ <div
111
+ className="text-sm text-gray-600 mt-2 leading-relaxed"
112
+ dangerouslySetInnerHTML={{ __html: displayQuestionDescription }}
113
+ />
114
+ )}
115
+
116
+ {question.type === QUESTION_TYPE.NPS_SCALE && (
117
+ <div className="space-y-3">
118
+ {(question.minLabel || question.midLabel || question.maxLabel) && (
119
+ <div className="relative w-full h-6 text-xs font-semibold text-gray-500">
120
+ <span
121
+ className="absolute left-0 top-0 max-w-[30%] text-left leading-tight"
122
+ dangerouslySetInnerHTML={{ __html: question.minLabel ?? '' }}
123
+ />
124
+ {question.midLabel && (
125
+ <span
126
+ className="absolute top-0 -translate-x-1/2 text-center max-w-[40%] leading-tight"
127
+ style={{
128
+ left: `${question.midLabelIndex !== undefined && question.options.length > 1
129
+ ? (question.midLabelIndex / (question.options.length - 1)) * 100
130
+ : 50}%`,
131
+ }}
132
+ dangerouslySetInnerHTML={{ __html: question.midLabel }}
133
+ />
134
+ )}
135
+ <span
136
+ className="absolute right-0 top-0 max-w-[30%] text-right leading-tight"
137
+ dangerouslySetInnerHTML={{ __html: question.maxLabel ?? '' }}
138
+ />
139
+ </div>
140
+ )}
141
+ <RatingScale
142
+ questionId={question.id}
143
+ options={question.options}
144
+ selectedValue={selectedValue}
145
+ onSelect={onSelect}
146
+ variant="nps"
147
+ buttonStyle={question.buttonStyle}
148
+ />
149
+ </div>
150
+ )}
151
+
152
+ {question.type === QUESTION_TYPE.MCQ && (() => {
153
+ const effectiveValue =
154
+ selectedValue ??
155
+ (question.selectionMode === 'single'
156
+ ? question.defaultOptionIds?.[0]
157
+ : question.defaultOptionIds);
158
+
159
+ if (question.selectionMode === 'multiple') {
160
+ const selectedIds: string[] = Array.isArray(effectiveValue)
161
+ ? effectiveValue.filter((id): id is string => typeof id === 'string')
162
+ : typeof effectiveValue === 'string'
163
+ ? [effectiveValue]
164
+ : [];
165
+
166
+ const handleToggle = (optionId: string) => {
167
+ const next = selectedIds.includes(optionId)
168
+ ? selectedIds.filter(id => id !== optionId)
169
+ : question.maxSelections && selectedIds.length >= question.maxSelections
170
+ ? selectedIds
171
+ : [...selectedIds, optionId];
172
+ onSelect(next);
173
+ };
174
+
175
+ return (
176
+ <div className="space-y-3">
177
+ {visibleMcqOptions.map(option => {
178
+ const isSelected = selectedIds.includes(option.id);
179
+ return (
180
+ <label
181
+ key={option.id}
182
+ className={`flex items-center gap-4 rounded-lg border py-4 px-5 cursor-pointer transition-all ${isSelected ? 'border-[#e20074] bg-[#fdf2f8]' : 'border-[#e5e5e5] bg-white hover:bg-gray-50/50'}`}
183
+ >
184
+ <input
185
+ type="checkbox"
186
+ name={question.id}
187
+ checked={isSelected}
188
+ onChange={() => handleToggle(option.id)}
189
+ className="sr-only"
190
+ />
191
+ <span
192
+ className="text-[15px] font-medium text-gray-900 leading-tight"
193
+ dangerouslySetInnerHTML={{ __html: option.optionLabel }}
194
+ />
195
+ </label>
196
+ );
197
+ })}
198
+ </div>
199
+ );
200
+ }
201
+
202
+ const singleSelected = typeof effectiveValue === 'string' ? effectiveValue : undefined;
203
+
204
+ return (
205
+ <div className="space-y-3">
206
+ {visibleMcqOptions.map(option => {
207
+ const isSelected = singleSelected === option.id;
208
+ return (
209
+ <label
210
+ key={option.id}
211
+ className={`flex items-center gap-4 rounded-lg border py-4 px-5 cursor-pointer transition-all ${isSelected ? 'border-[#e20074] bg-[#fdf2f8]' : 'border-[#e5e5e5] bg-white hover:bg-gray-50/50'}`}
212
+ >
213
+ <input
214
+ type="radio"
215
+ name={question.id}
216
+ checked={isSelected}
217
+ onChange={() => onSelect(option.id)}
218
+ className="sr-only"
219
+ />
220
+ <span
221
+ className="text-[15px] font-medium text-gray-900 leading-tight"
222
+ dangerouslySetInnerHTML={{ __html: option.optionLabel }}
223
+ />
224
+ </label>
225
+ );
226
+ })}
227
+ </div>
228
+ );
229
+ })()}
230
+
231
+ {question.type === QUESTION_TYPE.RANK_ORDER && rankOrderQuestion && (
232
+ <RankOrderScale
233
+ question={rankOrderQuestion}
234
+ selectedValue={
235
+ typeof selectedValue === 'object' && selectedValue !== null && !Array.isArray(selectedValue)
236
+ ? selectedValue
237
+ : undefined
238
+ }
239
+ onSelect={onSelect}
240
+ />
241
+ )}
242
+
243
+ {question.type === QUESTION_TYPE.TEXTFIELD && (
244
+ <textarea
245
+ rows={question.inputVariant === 'long' ? 4 : 1}
246
+ value={(selectedValue as string) ?? ''}
247
+ onChange={e => onSelect(e.target.value)}
248
+ placeholder={question.placeholder || 'Type your response here...'}
249
+ className="w-full rounded border border-gray-300 p-4 text-sm outline-none focus:border-[#e20074]"
250
+ />
251
+ )}
252
+
253
+ {question.type === QUESTION_TYPE.CFM_MATRIX && (
254
+ <LikertMatrixScale
255
+ question={matrixQuestion as typeof question & { type: typeof QUESTION_TYPE.CFM_MATRIX }}
256
+ selectedValue={typeof selectedValue === 'object' && selectedValue !== null && !Array.isArray(selectedValue) ? selectedValue : {}}
257
+ onSelect={onSelect}
258
+ />
259
+ )}
260
+
261
+ {(question.type === QUESTION_TYPE.CSAT_MATRIX ||
262
+ question.type === QUESTION_TYPE.RATING_MATRIX) && (
263
+ <CsatMatrixScale
264
+ question={matrixQuestion as Extract<SurveyQuestion, { type: typeof QUESTION_TYPE.CSAT_MATRIX | typeof QUESTION_TYPE.RATING_MATRIX }>}
265
+ selectedValue={typeof selectedValue === 'object' && selectedValue !== null && !Array.isArray(selectedValue) ? selectedValue : {}}
266
+ onSelect={onSelect}
267
+ />
268
+ )}
269
+
270
+ {question.type === QUESTION_TYPE.SLIDER_MATRIX && (
271
+ <SliderMatrixScale
272
+ question={matrixQuestion as typeof question & { type: typeof QUESTION_TYPE.SLIDER_MATRIX }}
273
+ selectedValue={typeof selectedValue === 'object' && selectedValue !== null && !Array.isArray(selectedValue) ? selectedValue : {}}
274
+ onSelect={onSelect}
275
+ />
276
+ )}
277
+
278
+ {question.type === QUESTION_TYPE.FILE_UPLOAD && (
279
+ <FileUploadScale question={question} selectedValue={selectedValue} onSelect={onSelect} />
280
+ )}
281
+
282
+ {question.type === QUESTION_TYPE.HEATMAP && (
283
+ <HeatmapScale
284
+ question={question as HeatmapQuestion}
285
+ selectedValue={Array.isArray(selectedValue) ? (selectedValue as HeatmapAnswer) : undefined}
286
+ onSelect={onSelect}
287
+ />
288
+ )}
289
+
290
+ {question.type === QUESTION_TYPE.TEXT_AND_MEDIA && question.media?.url && (
291
+ <div className={`mt-4 flex w-full ${question.media.alignment?.includes('CENTER') ? 'justify-center' : question.media.alignment?.includes('RIGHT') ? 'justify-end' : 'justify-start'}`}>
292
+ <div style={{ width: question.media.size ? `${question.media.size}%` : '100%' }}>
293
+ {question.media.mimeType?.startsWith('video/') || question.media.mimeType === 'VIDEO' ? (
294
+ <video controls src={question.media.url} className="max-w-full rounded-md" />
295
+ ) : (
296
+ <img src={question.media.url} alt={question.media.title || ''} className="max-w-full rounded-md" />
297
+ )}
298
+ {question.media.title && <p className="text-sm text-gray-500 text-center mt-2">{question.media.title}</p>}
299
+ </div>
300
+ </div>
301
+ )}
302
+
303
+ {validationError && (
304
+ <div className="mt-4 rounded border border-gray-800 bg-gray-200 py-3 px-4">
305
+ <p className="text-sm font-bold text-gray-900">{validationError}</p>
306
+ </div>
307
+ )}
308
+ </section>
309
+ );
310
+ }
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env bash
2
+ # Deploy static survey UI to AWS via EC2 middle API (S3 + CloudFront).
3
+ # Prerequisites: CFM_INSTANCE_ID, CFM_DEPLOY_API_BASE, OUT_DIR set in environment.
4
+ # Run from project root after a successful static build.
5
+ set -euo pipefail
6
+
7
+ : "${CFM_INSTANCE_ID:?Set CFM_INSTANCE_ID to the same JWT as SurveyPage SURVEY_INSTANCE_ID}"
8
+ : "${CFM_DEPLOY_API_BASE:?Set CFM_DEPLOY_API_BASE (e.g. http://43.204.26.213:3000)}"
9
+ : "${OUT_DIR:?Set OUT_DIR to absolute path of out/ (Next.js) or dist/ (Vite)}"
10
+
11
+ if [ ! -f "$OUT_DIR/index.html" ]; then
12
+ echo "FAIL: $OUT_DIR/index.html not found — run build first"
13
+ exit 1
14
+ fi
15
+
16
+ FILE_COUNT=$(find "$OUT_DIR" -type f | wc -l | tr -d ' ')
17
+ if [ "$FILE_COUNT" -gt 500 ]; then
18
+ echo "FAIL: $FILE_COUNT files exceeds API limit of 500"
19
+ exit 1
20
+ fi
21
+
22
+ echo "=== AWS Deploy ==="
23
+ echo "API: $CFM_DEPLOY_API_BASE"
24
+ echo "OUT: $OUT_DIR ($FILE_COUNT files)"
25
+ echo
26
+
27
+ HEALTH=$(curl -s "$CFM_DEPLOY_API_BASE/health")
28
+ echo "Health: $HEALTH"
29
+ echo
30
+
31
+ python3 <<'PY' > /tmp/deploy-manifest.json
32
+ import json, os, mimetypes
33
+ from pathlib import Path
34
+ out = Path(os.environ["OUT_DIR"])
35
+ files = []
36
+ for path in sorted(out.rglob("*")):
37
+ if not path.is_file():
38
+ continue
39
+ rel = path.relative_to(out).as_posix()
40
+ mime, _ = mimetypes.guess_type(rel)
41
+ files.append({
42
+ "path": rel,
43
+ "contentType": mime or "application/octet-stream",
44
+ "sizeBytes": path.stat().st_size,
45
+ })
46
+ print(json.dumps({"files": files}))
47
+ print(f"files: {len(files)}", file=__import__('sys').stderr)
48
+ PY
49
+
50
+ echo "Channel 1: deploy/start"
51
+ curl -s -X POST "$CFM_DEPLOY_API_BASE/deploy/start" \
52
+ -H "Authorization: Bearer $CFM_INSTANCE_ID" \
53
+ -H "Content-Type: application/json" \
54
+ -d @/tmp/deploy-manifest.json \
55
+ | tee /tmp/deploy-start.json
56
+ echo
57
+
58
+ if ! python3 -c "import json; json.load(open('/tmp/deploy-start.json'))['deployId']" 2>/dev/null; then
59
+ echo "FAIL: deploy/start did not return deployId"
60
+ exit 1
61
+ fi
62
+
63
+ echo "Channel 2: S3 uploads"
64
+ python3 <<'PY'
65
+ import json, os, subprocess
66
+ from pathlib import Path
67
+
68
+ out = Path(os.environ["OUT_DIR"])
69
+ start = json.load(open("/tmp/deploy-start.json"))
70
+ manifest = {f["path"]: f["contentType"] for f in json.load(open("/tmp/deploy-manifest.json"))["files"]}
71
+ failed = []
72
+
73
+ for item in start["uploads"]:
74
+ path = item["path"]
75
+ url = item["uploadUrl"]
76
+ mime = manifest.get(path, "application/octet-stream")
77
+ proc = subprocess.run(
78
+ ["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}",
79
+ "-X", "PUT", url, "-H", f"Content-Type: {mime}",
80
+ "--data-binary", f"@{out / path}"],
81
+ capture_output=True, text=True,
82
+ )
83
+ code = proc.stdout.strip()
84
+ mark = "OK" if code == "200" else "FAIL"
85
+ print(f"{mark} {code} {path}")
86
+ if code != "200":
87
+ failed.append(path)
88
+
89
+ if failed:
90
+ print(f"\n{len(failed)} failed — rerun deploy (presigned URLs expire)")
91
+ raise SystemExit(1)
92
+ print("\nAll uploads succeeded")
93
+ PY
94
+
95
+ echo
96
+ echo "Channel 3: deploy/complete"
97
+ DEPLOY_ID=$(python3 -c "import json; print(json.load(open('/tmp/deploy-start.json'))['deployId'])")
98
+ curl -s -X POST "$CFM_DEPLOY_API_BASE/deploy/complete" \
99
+ -H "Authorization: Bearer $CFM_INSTANCE_ID" \
100
+ -H "Content-Type: application/json" \
101
+ -d "{\"deployId\":\"$DEPLOY_ID\"}" \
102
+ | tee /tmp/deploy-complete.json
103
+ echo
104
+
105
+ LIVE_URL=$(python3 -c "import json; print(json.load(open('/tmp/deploy-complete.json'))['url'])")
106
+ echo "Live URL: $LIVE_URL"
107
+ HTTP=$(curl -s -o /dev/null -w "%{http_code}" "$LIVE_URL")
108
+ echo "Verify HTTP: $HTTP"
109
+ if [ "$HTTP" != "200" ]; then
110
+ echo "WARN: index.html returned $HTTP — check CloudFront path or invalidation"
111
+ exit 1
112
+ fi
113
+
114
+ echo "=== Deploy complete ==="
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Portable heatmap coordinate helpers for agent projects.
3
+ *
4
+ * Copy to e.g. src/lib/heatmapCoords.ts — do not import from monorepo-only paths.
5
+ * See 01-components/17-heatmap-scale.md and question-type-sdk-matrix.md#heatmap.
6
+ */
7
+ import type { MouseEvent } from 'react';
8
+ import type { HeatmapClickPoint } from '@explorer02/cfm-survey-sdk';
9
+
10
+ export const SPOT_HIT_RADIUS_PX = 14;
11
+
12
+ export function roundCoordinate(value: number): number {
13
+ return Math.round(value * 100000) / 100000;
14
+ }
15
+
16
+ export function getNormalizedClickFromImage(
17
+ event: MouseEvent,
18
+ imageElement: HTMLImageElement
19
+ ): { x: number; y: number } {
20
+ const rect = imageElement.getBoundingClientRect();
21
+ if (rect.width === 0 || rect.height === 0) {
22
+ return { x: 0, y: 0 };
23
+ }
24
+
25
+ const x = (event.clientX - rect.left) / rect.width;
26
+ const y = (event.clientY - rect.top) / rect.height;
27
+
28
+ return {
29
+ x: roundCoordinate(Math.min(1, Math.max(0, x))),
30
+ y: roundCoordinate(Math.min(1, Math.max(0, y))),
31
+ };
32
+ }
33
+
34
+ export function findSpotNearClick(
35
+ clickX: number,
36
+ clickY: number,
37
+ spots: HeatmapClickPoint[],
38
+ imageRect: DOMRect
39
+ ): HeatmapClickPoint | undefined {
40
+ const clickPixelX = clickX * imageRect.width;
41
+ const clickPixelY = clickY * imageRect.height;
42
+
43
+ for (const spot of spots) {
44
+ const spotPixelX = spot.x * imageRect.width;
45
+ const spotPixelY = spot.y * imageRect.height;
46
+ const distance = Math.hypot(spotPixelX - clickPixelX, spotPixelY - clickPixelY);
47
+
48
+ if (distance <= SPOT_HIT_RADIUS_PX) {
49
+ return spot;
50
+ }
51
+ }
52
+
53
+ return undefined;
54
+ }
55
+
56
+ export function createNewSpot(x: number, y: number): HeatmapClickPoint {
57
+ return { id: crypto.randomUUID(), x, y };
58
+ }