@growth-labs/cms 0.5.5 → 0.5.7

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 (164) hide show
  1. package/README.md +46 -0
  2. package/dist/engine/ai-dek-notifications.d.ts +16 -0
  3. package/dist/engine/ai-dek-notifications.d.ts.map +1 -0
  4. package/dist/engine/ai-dek-notifications.js +34 -0
  5. package/dist/engine/ai-dek-notifications.js.map +1 -0
  6. package/dist/engine/ai-dek.d.ts +35 -0
  7. package/dist/engine/ai-dek.d.ts.map +1 -0
  8. package/dist/engine/ai-dek.js +99 -0
  9. package/dist/engine/ai-dek.js.map +1 -0
  10. package/dist/engine/ai-prompts.d.ts +12 -0
  11. package/dist/engine/ai-prompts.d.ts.map +1 -1
  12. package/dist/engine/ai-prompts.js +17 -0
  13. package/dist/engine/ai-prompts.js.map +1 -1
  14. package/dist/engine/index.d.ts +3 -1
  15. package/dist/engine/index.d.ts.map +1 -1
  16. package/dist/engine/index.js +4 -1
  17. package/dist/engine/index.js.map +1 -1
  18. package/dist/index.d.ts +2 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +2 -0
  21. package/dist/index.js.map +1 -1
  22. package/dist/integration/index.d.ts.map +1 -1
  23. package/dist/integration/index.js +13 -0
  24. package/dist/integration/index.js.map +1 -1
  25. package/dist/integration/options.d.ts +19 -0
  26. package/dist/integration/options.d.ts.map +1 -1
  27. package/dist/integration/options.js +7 -0
  28. package/dist/integration/options.js.map +1 -1
  29. package/dist/providers/null.d.ts +2 -1
  30. package/dist/providers/null.d.ts.map +1 -1
  31. package/dist/providers/null.js +9 -0
  32. package/dist/providers/null.js.map +1 -1
  33. package/dist/providers/types.d.ts +10 -0
  34. package/dist/providers/types.d.ts.map +1 -1
  35. package/dist/providers/types.js.map +1 -1
  36. package/dist/routes/ai.d.ts +2 -0
  37. package/dist/routes/ai.d.ts.map +1 -1
  38. package/dist/routes/ai.js +42 -0
  39. package/dist/routes/ai.js.map +1 -1
  40. package/dist/routes/authz-matrix.d.ts +2 -1
  41. package/dist/routes/authz-matrix.d.ts.map +1 -1
  42. package/dist/routes/authz-matrix.js +5 -0
  43. package/dist/routes/authz-matrix.js.map +1 -1
  44. package/dist/routes/content.d.ts.map +1 -1
  45. package/dist/routes/content.js +43 -1
  46. package/dist/routes/content.js.map +1 -1
  47. package/dist/routes/context.d.ts +1 -1
  48. package/dist/routes/context.d.ts.map +1 -1
  49. package/dist/routes/context.js.map +1 -1
  50. package/dist/routes/index.d.ts +1 -0
  51. package/dist/routes/index.d.ts.map +1 -1
  52. package/dist/routes/index.js +2 -0
  53. package/dist/routes/index.js.map +1 -1
  54. package/dist/routes/surveys.d.ts +12 -0
  55. package/dist/routes/surveys.d.ts.map +1 -0
  56. package/dist/routes/surveys.js +109 -0
  57. package/dist/routes/surveys.js.map +1 -0
  58. package/dist/schema/insights-ingest.d.ts +46 -46
  59. package/dist/surveys/access.d.ts +4 -0
  60. package/dist/surveys/access.d.ts.map +1 -0
  61. package/dist/surveys/access.js +9 -0
  62. package/dist/surveys/access.js.map +1 -0
  63. package/dist/surveys/errors.d.ts +43 -0
  64. package/dist/surveys/errors.d.ts.map +1 -0
  65. package/dist/surveys/errors.js +42 -0
  66. package/dist/surveys/errors.js.map +1 -0
  67. package/dist/surveys/schema.d.ts +939 -0
  68. package/dist/surveys/schema.d.ts.map +1 -0
  69. package/dist/surveys/schema.js +385 -0
  70. package/dist/surveys/schema.js.map +1 -0
  71. package/dist/ui/api/_content-config.d.ts.map +1 -1
  72. package/dist/ui/api/_content-config.js +1 -0
  73. package/dist/ui/api/_content-config.js.map +1 -1
  74. package/dist/ui/api/content/[id]/ai/dek.d.ts +3 -0
  75. package/dist/ui/api/content/[id]/ai/dek.d.ts.map +1 -0
  76. package/dist/ui/api/content/[id]/ai/dek.js +5 -0
  77. package/dist/ui/api/content/[id]/ai/dek.js.map +1 -0
  78. package/dist/ui/api/surveys/[surveyId]/responses.csv.d.ts +3 -0
  79. package/dist/ui/api/surveys/[surveyId]/responses.csv.d.ts.map +1 -0
  80. package/dist/ui/api/surveys/[surveyId]/responses.csv.js +17 -0
  81. package/dist/ui/api/surveys/[surveyId]/responses.csv.js.map +1 -0
  82. package/dist/ui/api/surveys/[surveyId].d.ts +3 -0
  83. package/dist/ui/api/surveys/[surveyId].d.ts.map +1 -0
  84. package/dist/ui/api/surveys/[surveyId].js +17 -0
  85. package/dist/ui/api/surveys/[surveyId].js.map +1 -0
  86. package/dist/ui/client/mount.d.ts.map +1 -1
  87. package/dist/ui/client/mount.js +1 -0
  88. package/dist/ui/client/mount.js.map +1 -1
  89. package/dist/ui/commands.d.ts +5 -1
  90. package/dist/ui/commands.d.ts.map +1 -1
  91. package/dist/ui/commands.js +7 -0
  92. package/dist/ui/commands.js.map +1 -1
  93. package/dist/ui/components/CmsApp.d.ts +2 -0
  94. package/dist/ui/components/CmsApp.d.ts.map +1 -1
  95. package/dist/ui/components/CmsApp.js +14 -3
  96. package/dist/ui/components/CmsApp.js.map +1 -1
  97. package/dist/ui/components/CommandPalette.d.ts +2 -0
  98. package/dist/ui/components/CommandPalette.d.ts.map +1 -1
  99. package/dist/ui/components/CommandPalette.js +1 -1
  100. package/dist/ui/components/CommandPalette.js.map +1 -1
  101. package/dist/ui/components/Sidebar.d.ts +1 -0
  102. package/dist/ui/components/Sidebar.d.ts.map +1 -1
  103. package/dist/ui/components/Sidebar.js +1 -1
  104. package/dist/ui/components/Sidebar.js.map +1 -1
  105. package/dist/ui/editor/AiAssistPanel.d.ts +3 -1
  106. package/dist/ui/editor/AiAssistPanel.d.ts.map +1 -1
  107. package/dist/ui/editor/AiAssistPanel.js +28 -4
  108. package/dist/ui/editor/AiAssistPanel.js.map +1 -1
  109. package/dist/ui/editor/ContentForm.d.ts.map +1 -1
  110. package/dist/ui/editor/ContentForm.js +1 -1
  111. package/dist/ui/editor/ContentForm.js.map +1 -1
  112. package/dist/ui/editor/ai-assist.d.ts +2 -2
  113. package/dist/ui/editor/ai-assist.d.ts.map +1 -1
  114. package/dist/ui/hash-router.d.ts +1 -1
  115. package/dist/ui/hash-router.d.ts.map +1 -1
  116. package/dist/ui/hash-router.js +1 -0
  117. package/dist/ui/hash-router.js.map +1 -1
  118. package/dist/ui/nav.d.ts +5 -1
  119. package/dist/ui/nav.d.ts.map +1 -1
  120. package/dist/ui/nav.js +10 -2
  121. package/dist/ui/nav.js.map +1 -1
  122. package/dist/ui/screens/SurveyResponsesScreen.d.ts +28 -0
  123. package/dist/ui/screens/SurveyResponsesScreen.d.ts.map +1 -0
  124. package/dist/ui/screens/SurveyResponsesScreen.js +110 -0
  125. package/dist/ui/screens/SurveyResponsesScreen.js.map +1 -0
  126. package/dist/ui/screens/registry.d.ts +2 -1
  127. package/dist/ui/screens/registry.d.ts.map +1 -1
  128. package/dist/ui/screens/registry.js +5 -1
  129. package/dist/ui/screens/registry.js.map +1 -1
  130. package/package.json +1 -1
  131. package/src/engine/ai-dek-notifications.ts +50 -0
  132. package/src/engine/ai-dek.ts +138 -0
  133. package/src/engine/ai-prompts.ts +25 -0
  134. package/src/engine/index.ts +14 -0
  135. package/src/index.ts +2 -2
  136. package/src/integration/index.ts +13 -0
  137. package/src/integration/options.ts +8 -0
  138. package/src/providers/null.ts +19 -0
  139. package/src/providers/types.ts +7 -0
  140. package/src/routes/ai.ts +44 -0
  141. package/src/routes/authz-matrix.ts +14 -1
  142. package/src/routes/content.ts +45 -1
  143. package/src/routes/context.ts +3 -0
  144. package/src/routes/index.ts +6 -0
  145. package/src/routes/surveys.ts +130 -0
  146. package/src/surveys/access.ts +11 -0
  147. package/src/surveys/errors.ts +51 -0
  148. package/src/surveys/schema.ts +435 -0
  149. package/src/ui/api/_content-config.ts +2 -0
  150. package/src/ui/api/content/[id]/ai/dek.ts +8 -0
  151. package/src/ui/api/surveys/[surveyId]/responses.csv.ts +23 -0
  152. package/src/ui/api/surveys/[surveyId].ts +23 -0
  153. package/src/ui/client/mount.tsx +1 -0
  154. package/src/ui/commands.ts +14 -1
  155. package/src/ui/components/CmsApp.tsx +25 -3
  156. package/src/ui/components/CommandPalette.tsx +2 -1
  157. package/src/ui/components/Sidebar.tsx +10 -2
  158. package/src/ui/editor/AiAssistPanel.tsx +55 -3
  159. package/src/ui/editor/ContentForm.tsx +3 -1
  160. package/src/ui/editor/ai-assist.ts +2 -2
  161. package/src/ui/hash-router.ts +1 -0
  162. package/src/ui/nav.ts +15 -2
  163. package/src/ui/screens/SurveyResponsesScreen.tsx +271 -0
  164. package/src/ui/screens/registry.tsx +6 -0
package/dist/ui/nav.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // src/ui/nav.ts — the Masthead sidebar nav model. Two groups, 11 items
2
2
  // (spec §6: Social Share added between Subscriptions and Settings).
3
+ import { canViewSurveyResponses } from '../surveys/access.js';
3
4
  export const NAV_MODEL = [
4
5
  {
5
6
  group: 'Workspace',
@@ -18,16 +19,23 @@ export const NAV_MODEL = [
18
19
  { id: 'topics', label: 'Topics & tags', icon: 'tag' },
19
20
  { id: 'content-insights', label: 'Content Insights', icon: 'sparkle' },
20
21
  { id: 'subscriptions', label: 'Subscriptions', icon: 'inbox' },
22
+ { id: 'survey-responses', label: 'Survey responses', icon: 'chart' },
21
23
  { id: 'social-share', label: 'Social Share', icon: 'share' },
22
24
  { id: 'settings', label: 'Settings', icon: 'settings' },
23
25
  ],
24
26
  },
25
27
  ];
26
28
  /** Build the nav with live counts merged onto matching item ids. */
27
- export function navItems(counts = {}) {
29
+ export function navItems(counts = {}, options = {
30
+ surveyResultsEnabled: true,
31
+ role: 'owner',
32
+ }) {
33
+ const canViewSurvey = options.surveyResultsEnabled !== false && canViewSurveyResponses(options.role);
28
34
  return NAV_MODEL.map((g) => ({
29
35
  group: g.group,
30
- items: g.items.map((i) => (counts[i.id] != null ? { ...i, count: counts[i.id] } : { ...i })),
36
+ items: g.items
37
+ .filter((item) => item.id !== 'survey-responses' || canViewSurvey)
38
+ .map((i) => (counts[i.id] != null ? { ...i, count: counts[i.id] } : { ...i })),
31
39
  }));
32
40
  }
33
41
  //# sourceMappingURL=nav.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"nav.js","sourceRoot":"","sources":["../../src/ui/nav.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,oEAAoE;AAgBpE,MAAM,CAAC,MAAM,SAAS,GAAe;IACpC;QACC,KAAK,EAAE,WAAW;QAClB,KAAK,EAAE;YACN,EAAE,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,WAAW,EAAE;YAC1D,EAAE,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE;YACpD,EAAE,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE;YACvD,EAAE,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE;YAC9C,EAAE,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,OAAO,EAAE;SACtD;KACD;IACD;QACC,KAAK,EAAE,QAAQ;QACf,KAAK,EAAE;YACN,EAAE,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,mBAAmB,EAAE,IAAI,EAAE,OAAO,EAAE;YAC5D,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,eAAe,EAAE,IAAI,EAAE,KAAK,EAAE;YACrD,EAAE,EAAE,EAAE,kBAAkB,EAAE,KAAK,EAAE,kBAAkB,EAAE,IAAI,EAAE,SAAS,EAAE;YACtE,EAAE,EAAE,EAAE,eAAe,EAAE,KAAK,EAAE,eAAe,EAAE,IAAI,EAAE,OAAO,EAAE;YAC9D,EAAE,EAAE,EAAE,cAAc,EAAE,KAAK,EAAE,cAAc,EAAE,IAAI,EAAE,OAAO,EAAE;YAC5D,EAAE,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE;SACvD;KACD;CACD,CAAA;AAED,oEAAoE;AACpE,MAAM,UAAU,QAAQ,CAAC,SAAyC,EAAE;IACnE,OAAO,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC5B,KAAK,EAAE,CAAC,CAAC,KAAK;QACd,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;KAC5F,CAAC,CAAC,CAAA;AACJ,CAAC"}
1
+ {"version":3,"file":"nav.js","sourceRoot":"","sources":["../../src/ui/nav.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,oEAAoE;AAGpE,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAA;AAe7D,MAAM,CAAC,MAAM,SAAS,GAAe;IACpC;QACC,KAAK,EAAE,WAAW;QAClB,KAAK,EAAE;YACN,EAAE,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,WAAW,EAAE;YAC1D,EAAE,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE;YACpD,EAAE,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE;YACvD,EAAE,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE;YAC9C,EAAE,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,OAAO,EAAE;SACtD;KACD;IACD;QACC,KAAK,EAAE,QAAQ;QACf,KAAK,EAAE;YACN,EAAE,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,mBAAmB,EAAE,IAAI,EAAE,OAAO,EAAE;YAC5D,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,eAAe,EAAE,IAAI,EAAE,KAAK,EAAE;YACrD,EAAE,EAAE,EAAE,kBAAkB,EAAE,KAAK,EAAE,kBAAkB,EAAE,IAAI,EAAE,SAAS,EAAE;YACtE,EAAE,EAAE,EAAE,eAAe,EAAE,KAAK,EAAE,eAAe,EAAE,IAAI,EAAE,OAAO,EAAE;YAC9D,EAAE,EAAE,EAAE,kBAAkB,EAAE,KAAK,EAAE,kBAAkB,EAAE,IAAI,EAAE,OAAO,EAAE;YACpE,EAAE,EAAE,EAAE,cAAc,EAAE,KAAK,EAAE,cAAc,EAAE,IAAI,EAAE,OAAO,EAAE;YAC5D,EAAE,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE;SACvD;KACD;CACD,CAAA;AAED,oEAAoE;AACpE,MAAM,UAAU,QAAQ,CACvB,SAAyC,EAAE,EAC3C,UAAqE;IACpE,oBAAoB,EAAE,IAAI;IAC1B,IAAI,EAAE,OAAO;CACb;IAED,MAAM,aAAa,GAClB,OAAO,CAAC,oBAAoB,KAAK,KAAK,IAAI,sBAAsB,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/E,OAAO,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC5B,KAAK,EAAE,CAAC,CAAC,KAAK;QACd,KAAK,EAAE,CAAC,CAAC,KAAK;aACZ,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,kBAAkB,IAAI,aAAa,CAAC;aACjE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;KAC/E,CAAC,CAAC,CAAA;AACJ,CAAC"}
@@ -0,0 +1,28 @@
1
+ import { type SurveyResults } from '../../surveys/schema.js';
2
+ export type SurveyResponsesState = {
3
+ kind: 'loading';
4
+ } | {
5
+ kind: 'ready';
6
+ data: SurveyResults;
7
+ } | {
8
+ kind: 'forbidden';
9
+ } | {
10
+ kind: 'unavailable';
11
+ } | {
12
+ kind: 'malformed';
13
+ } | {
14
+ kind: 'not_found';
15
+ } | {
16
+ kind: 'retryable';
17
+ };
18
+ export declare function parseSurveyResultsPayload(value: unknown): SurveyResults | null;
19
+ export declare function classifySurveyResultsResponse(response: Response): Promise<SurveyResponsesState>;
20
+ export declare function SurveyResponsesScreen({ surveyId }: {
21
+ surveyId: string;
22
+ }): import("react").JSX.Element;
23
+ export declare function SurveyResponsesBody({ surveyId, state, onRetry, }: {
24
+ surveyId: string;
25
+ state: SurveyResponsesState;
26
+ onRetry?: () => void;
27
+ }): import("react").JSX.Element;
28
+ //# sourceMappingURL=SurveyResponsesScreen.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SurveyResponsesScreen.d.ts","sourceRoot":"","sources":["../../../src/ui/screens/SurveyResponsesScreen.tsx"],"names":[],"mappings":"AAOA,OAAO,EAAE,KAAK,aAAa,EAAuB,MAAM,yBAAyB,CAAA;AAEjF,MAAM,MAAM,oBAAoB,GAC7B;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,aAAa,CAAA;CAAE,GACtC;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,GACrB;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,GACvB;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,GACrB;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,GACrB;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,CAAA;AAExB,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,aAAa,GAAG,IAAI,CAG9E;AAED,wBAAsB,6BAA6B,CAClD,QAAQ,EAAE,QAAQ,GAChB,OAAO,CAAC,oBAAoB,CAAC,CAkB/B;AAED,wBAAgB,qBAAqB,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,+BAiBvE;AAED,wBAAgB,mBAAmB,CAAC,EACnC,QAAQ,EACR,KAAK,EACL,OAAO,GACP,EAAE;IACF,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,oBAAoB,CAAA;IAC3B,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;CACpB,+BA8HA"}
@@ -0,0 +1,110 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useState } from 'react';
3
+ import { isSurveyErrorCode, SURVEY_ERROR_CODE_HEADER, SURVEY_ERROR_DEFINITIONS, } from '../../surveys/errors.js';
4
+ import { SurveyResultsSchema } from '../../surveys/schema.js';
5
+ export function parseSurveyResultsPayload(value) {
6
+ const parsed = SurveyResultsSchema.safeParse(value);
7
+ return parsed.success ? parsed.data : null;
8
+ }
9
+ export async function classifySurveyResultsResponse(response) {
10
+ if (response.ok) {
11
+ const data = parseSurveyResultsPayload(await response.json().catch(() => null));
12
+ return data ? { kind: 'ready', data } : { kind: 'malformed' };
13
+ }
14
+ const code = await readSurveyErrorCode(response);
15
+ if ((response.status === 401 || response.status === 403) && code === 'survey_forbidden') {
16
+ return { kind: 'forbidden' };
17
+ }
18
+ if (response.status === 404 && code === 'survey_not_found')
19
+ return { kind: 'not_found' };
20
+ if (response.status === 502 && code === 'survey_provider_malformed') {
21
+ return { kind: 'malformed' };
22
+ }
23
+ if (response.status === 503 && code === 'survey_provider_unavailable') {
24
+ return { kind: 'unavailable' };
25
+ }
26
+ return { kind: 'retryable' };
27
+ }
28
+ export function SurveyResponsesScreen({ surveyId }) {
29
+ const [state, setState] = useState({ kind: 'loading' });
30
+ const load = useCallback(async () => {
31
+ setState({ kind: 'loading' });
32
+ try {
33
+ const response = await fetch(`/admin/api/surveys/${encodeURIComponent(surveyId)}`);
34
+ setState(await classifySurveyResultsResponse(response));
35
+ }
36
+ catch {
37
+ setState({ kind: 'retryable' });
38
+ }
39
+ }, [surveyId]);
40
+ useEffect(() => {
41
+ void load();
42
+ }, [load]);
43
+ return _jsx(SurveyResponsesBody, { surveyId: surveyId, state: state, onRetry: load });
44
+ }
45
+ export function SurveyResponsesBody({ surveyId, state, onRetry, }) {
46
+ if (state.kind === 'loading')
47
+ return _jsx(StatusPage, { title: "Loading survey responses\u2026" });
48
+ if (state.kind === 'forbidden')
49
+ return _jsx(StatusPage, { title: "You do not have permission to view survey responses." });
50
+ if (state.kind === 'unavailable')
51
+ return _jsx(StatusPage, { title: "Survey reporting is not configured for this site." });
52
+ if (state.kind === 'malformed')
53
+ return _jsx(StatusPage, { title: "Masthead received invalid survey data.", retry: onRetry });
54
+ if (state.kind === 'not_found')
55
+ return _jsx(StatusPage, { title: "The configured survey could not be found." });
56
+ if (state.kind === 'retryable')
57
+ return _jsx(StatusPage, { title: "Survey responses could not be loaded.", retry: onRetry });
58
+ const { data } = state;
59
+ const freeTextByQuestion = new Map();
60
+ for (const entry of data.freeText) {
61
+ const entries = freeTextByQuestion.get(entry.questionId) ?? [];
62
+ entries.push(entry);
63
+ freeTextByQuestion.set(entry.questionId, entries);
64
+ }
65
+ return (_jsxs("div", { className: "page page-wide fade-in", style: { maxWidth: 1340 }, children: [_jsxs("div", { className: "page-head", children: [_jsxs("div", { children: [_jsx("h1", { className: "page-title", children: data.survey.title }), _jsxs("div", { className: "page-sub", children: [data.survey.responseCount, " ", data.survey.responseCount === 1 ? 'response' : 'responses', ' ', "\u00B7 ", data.survey.status] })] }), _jsx("a", { className: "btn", href: `/admin/api/surveys/${encodeURIComponent(surveyId)}/responses.csv`, children: "Export CSV" })] }), data.survey.responseCount === 0 ? (_jsxs("div", { className: "card", role: "status", children: [_jsx("h2", { className: "card-title", children: "No responses yet" }), _jsx("p", { className: "page-sub", children: "Aggregates and free-text answers will appear after submission." })] })) : (_jsxs("section", { className: "card", "aria-labelledby": "survey-response-list", children: [_jsx("h2", { className: "card-title", id: "survey-response-list", children: "Recent responses" }), _jsx("div", { className: "masthead-rule" }), _jsxs("table", { className: "tbl", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "Submitted" }), _jsx("th", { children: "Channel" }), _jsx("th", { children: "Audience" }), _jsx("th", { children: "Attribution" })] }) }), _jsx("tbody", { children: data.responses.map((response) => (_jsxs("tr", { children: [_jsx("td", { children: _jsx("time", { dateTime: response.submittedAt, children: formatTime(response.submittedAt) }) }), _jsx("td", { children: response.channel }), _jsx("td", { children: segmentLabel(response.respondentSegment) }), _jsx("td", { children: response.anonymous ? 'Anonymous' : 'Known respondent' })] }, response.id))) })] })] })), _jsxs("section", { "aria-labelledby": "survey-aggregates", style: { marginTop: 24 }, children: [_jsx("h2", { className: "page-title", id: "survey-aggregates", style: { fontSize: 22 }, children: "Question aggregates" }), _jsx("div", { className: "grid-split-wide", style: { alignItems: 'start', marginTop: 12 }, children: data.questions.map((question) => (_jsxs("article", { className: "card", children: [_jsxs("div", { className: "kicker", children: ["Q", question.position + 1, " \u00B7 ", questionAudienceLabel(question.audience)] }), _jsx("h3", { className: "card-title", children: question.prompt }), question.counts && _jsx(CountTable, { values: question.counts }), question.distribution && (_jsxs(_Fragment, { children: [_jsxs("p", { className: "page-sub", children: ["Average: ", question.average === null ? '—' : question.average.toFixed(2)] }), _jsx(CountTable, { values: question.distribution })] })), question.kind === 'text' && (_jsxs("p", { className: "page-sub", children: [question.responseCount ?? 0, " free-text responses"] }))] }, question.id))) })] }), _jsxs("section", { "aria-labelledby": "survey-free-text", style: { marginTop: 24 }, children: [_jsx("h2", { className: "page-title", id: "survey-free-text", style: { fontSize: 22 }, children: "Free-text responses" }), data.questions
66
+ .filter((question) => question.kind === 'text')
67
+ .map((question) => (_jsxs("div", { className: "card", style: { marginTop: 12 }, children: [_jsx("h3", { className: "card-title", children: question.prompt }), (freeTextByQuestion.get(question.id) ?? []).length === 0 ? (_jsx("p", { className: "page-sub", children: "No free-text responses." })) : (_jsx("ul", { children: (freeTextByQuestion.get(question.id) ?? []).map((entry) => (_jsx("li", { children: entry.value }, `${entry.responseId}:${entry.submittedAt}`))) }))] }, question.id)))] })] }));
68
+ }
69
+ async function readSurveyErrorCode(response) {
70
+ const headerCode = response.headers.get(SURVEY_ERROR_CODE_HEADER);
71
+ if (!isSurveyErrorCode(headerCode))
72
+ return null;
73
+ if (headerCode === 'survey_forbidden' && (response.status === 401 || response.status === 403)) {
74
+ return headerCode;
75
+ }
76
+ const body = await response.json().catch(() => null);
77
+ if (!isPlainRecord(body) || Object.keys(body).length !== 2)
78
+ return null;
79
+ if (!isSurveyErrorCode(body.code) || body.code !== headerCode)
80
+ return null;
81
+ return body.error === SURVEY_ERROR_DEFINITIONS[body.code].error ? body.code : null;
82
+ }
83
+ function isPlainRecord(value) {
84
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
85
+ }
86
+ function StatusPage({ title, detail, retry, }) {
87
+ return (_jsxs("div", { className: "page", role: "status", children: [_jsx("h1", { className: "page-title", children: "Survey responses" }), _jsx("p", { className: "page-sub", children: title }), detail && _jsx("p", { className: "page-sub", children: detail }), retry && (_jsx("button", { type: "button", className: "btn", onClick: retry, children: "Retry" }))] }));
88
+ }
89
+ function CountTable({ values }) {
90
+ return (_jsx("table", { className: "tbl", children: _jsx("tbody", { children: Object.entries(values).map(([label, count]) => (_jsxs("tr", { children: [_jsx("th", { scope: "row", children: label }), _jsx("td", { className: "tnum", children: count })] }, label))) }) }));
91
+ }
92
+ function formatTime(value) {
93
+ const date = new Date(value);
94
+ return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
95
+ }
96
+ function segmentLabel(value) {
97
+ if (value === 'member')
98
+ return 'Member';
99
+ if (value === 'non_member')
100
+ return 'Non-member';
101
+ return 'Unknown';
102
+ }
103
+ function questionAudienceLabel(value) {
104
+ if (value === 'member')
105
+ return 'Members only';
106
+ if (value === 'non_member')
107
+ return 'Non-members only';
108
+ return 'All respondents';
109
+ }
110
+ //# sourceMappingURL=SurveyResponsesScreen.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SurveyResponsesScreen.js","sourceRoot":"","sources":["../../../src/ui/screens/SurveyResponsesScreen.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AACxD,OAAO,EACN,iBAAiB,EACjB,wBAAwB,EACxB,wBAAwB,GAExB,MAAM,yBAAyB,CAAA;AAChC,OAAO,EAAsB,mBAAmB,EAAE,MAAM,yBAAyB,CAAA;AAWjF,MAAM,UAAU,yBAAyB,CAAC,KAAc;IACvD,MAAM,MAAM,GAAG,mBAAmB,CAAC,SAAS,CAAC,KAAK,CAAC,CAAA;IACnD,OAAO,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAA;AAC3C,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,6BAA6B,CAClD,QAAkB;IAElB,IAAI,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,GAAG,yBAAyB,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC,CAAA;QAC/E,OAAO,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,CAAA;IAC9D,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,mBAAmB,CAAC,QAAQ,CAAC,CAAA;IAChD,IAAI,CAAC,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,CAAC,IAAI,IAAI,KAAK,kBAAkB,EAAE,CAAC;QACzF,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,CAAA;IAC7B,CAAC;IACD,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,IAAI,KAAK,kBAAkB;QAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,CAAA;IACxF,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,IAAI,KAAK,2BAA2B,EAAE,CAAC;QACrE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,CAAA;IAC7B,CAAC;IACD,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,IAAI,KAAK,6BAA6B,EAAE,CAAC;QACvE,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,CAAA;IAC/B,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,CAAA;AAC7B,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,EAAE,QAAQ,EAAwB;IACvE,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAuB,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAA;IAC7E,MAAM,IAAI,GAAG,WAAW,CAAC,KAAK,IAAI,EAAE;QACnC,QAAQ,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAA;QAC7B,IAAI,CAAC;YACJ,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,sBAAsB,kBAAkB,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAA;YAClF,QAAQ,CAAC,MAAM,6BAA6B,CAAC,QAAQ,CAAC,CAAC,CAAA;QACxD,CAAC;QAAC,MAAM,CAAC;YACR,QAAQ,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAA;QAChC,CAAC;IACF,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAA;IAEd,SAAS,CAAC,GAAG,EAAE;QACd,KAAK,IAAI,EAAE,CAAA;IACZ,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAA;IAEV,OAAO,KAAC,mBAAmB,IAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,GAAI,CAAA;AAChF,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,EACnC,QAAQ,EACR,KAAK,EACL,OAAO,GAKP;IACA,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS;QAAE,OAAO,KAAC,UAAU,IAAC,KAAK,EAAC,gCAA2B,GAAG,CAAA;IACrF,IAAI,KAAK,CAAC,IAAI,KAAK,WAAW;QAC7B,OAAO,KAAC,UAAU,IAAC,KAAK,EAAC,sDAAsD,GAAG,CAAA;IACnF,IAAI,KAAK,CAAC,IAAI,KAAK,aAAa;QAC/B,OAAO,KAAC,UAAU,IAAC,KAAK,EAAC,mDAAmD,GAAG,CAAA;IAChF,IAAI,KAAK,CAAC,IAAI,KAAK,WAAW;QAC7B,OAAO,KAAC,UAAU,IAAC,KAAK,EAAC,wCAAwC,EAAC,KAAK,EAAE,OAAO,GAAI,CAAA;IACrF,IAAI,KAAK,CAAC,IAAI,KAAK,WAAW;QAC7B,OAAO,KAAC,UAAU,IAAC,KAAK,EAAC,2CAA2C,GAAG,CAAA;IACxE,IAAI,KAAK,CAAC,IAAI,KAAK,WAAW;QAC7B,OAAO,KAAC,UAAU,IAAC,KAAK,EAAC,uCAAuC,EAAC,KAAK,EAAE,OAAO,GAAI,CAAA;IAEpF,MAAM,EAAE,IAAI,EAAE,GAAG,KAAK,CAAA;IACtB,MAAM,kBAAkB,GAAG,IAAI,GAAG,EAAqC,CAAA;IACvE,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;QACnC,MAAM,OAAO,GAAG,kBAAkB,CAAC,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,EAAE,CAAA;QAC9D,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACnB,kBAAkB,CAAC,GAAG,CAAC,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAAA;IAClD,CAAC;IAED,OAAO,CACN,eAAK,SAAS,EAAC,wBAAwB,EAAC,KAAK,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,aAChE,eAAK,SAAS,EAAC,WAAW,aACzB,0BACC,aAAI,SAAS,EAAC,YAAY,YAAE,IAAI,CAAC,MAAM,CAAC,KAAK,GAAM,EACnD,eAAK,SAAS,EAAC,UAAU,aACvB,IAAI,CAAC,MAAM,CAAC,aAAa,OAAG,IAAI,CAAC,MAAM,CAAC,aAAa,KAAK,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,aACzF,IAAI,CAAC,MAAM,CAAC,MAAM,IAChB,IACD,EACN,YACC,SAAS,EAAC,KAAK,EACf,IAAI,EAAE,sBAAsB,kBAAkB,CAAC,QAAQ,CAAC,gBAAgB,2BAGrE,IACC,EAEL,IAAI,CAAC,MAAM,CAAC,aAAa,KAAK,CAAC,CAAC,CAAC,CAAC,CAClC,eAAK,SAAS,EAAC,MAAM,EAAC,IAAI,EAAC,QAAQ,aAClC,aAAI,SAAS,EAAC,YAAY,iCAAsB,EAChD,YAAG,SAAS,EAAC,UAAU,+EAAmE,IACrF,CACN,CAAC,CAAC,CAAC,CACH,mBAAS,SAAS,EAAC,MAAM,qBAAiB,sBAAsB,aAC/D,aAAI,SAAS,EAAC,YAAY,EAAC,EAAE,EAAC,sBAAsB,iCAE/C,EACL,cAAK,SAAS,EAAC,eAAe,GAAG,EACjC,iBAAO,SAAS,EAAC,KAAK,aACrB,0BACC,yBACC,qCAAkB,EAClB,mCAAgB,EAChB,oCAAiB,EACjB,uCAAoB,IAChB,GACE,EACR,0BACE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CACjC,yBACC,uBACC,eAAM,QAAQ,EAAE,QAAQ,CAAC,WAAW,YAAG,UAAU,CAAC,QAAQ,CAAC,WAAW,CAAC,GAAQ,GAC3E,EACL,uBAAK,QAAQ,CAAC,OAAO,GAAM,EAC3B,uBAAK,YAAY,CAAC,QAAQ,CAAC,iBAAiB,CAAC,GAAM,EACnD,uBAAK,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,kBAAkB,GAAM,KANxD,QAAQ,CAAC,EAAE,CAOf,CACL,CAAC,GACK,IACD,IACC,CACV,EAED,sCAAyB,mBAAmB,EAAC,KAAK,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE,aACpE,aAAI,SAAS,EAAC,YAAY,EAAC,EAAE,EAAC,mBAAmB,EAAC,KAAK,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,oCAEpE,EACL,cAAK,SAAS,EAAC,iBAAiB,EAAC,KAAK,EAAE,EAAE,UAAU,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,EAAE,YAC5E,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CACjC,mBAAS,SAAS,EAAC,MAAM,aACxB,eAAK,SAAS,EAAC,QAAQ,kBACpB,QAAQ,CAAC,QAAQ,GAAG,CAAC,cAAK,qBAAqB,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAC/D,EACN,aAAI,SAAS,EAAC,YAAY,YAAE,QAAQ,CAAC,MAAM,GAAM,EAChD,QAAQ,CAAC,MAAM,IAAI,KAAC,UAAU,IAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,GAAI,EAC1D,QAAQ,CAAC,YAAY,IAAI,CACzB,8BACC,aAAG,SAAS,EAAC,UAAU,0BACZ,QAAQ,CAAC,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,IACpE,EACJ,KAAC,UAAU,IAAC,MAAM,EAAE,QAAQ,CAAC,YAAY,GAAI,IAC3C,CACH,EACA,QAAQ,CAAC,IAAI,KAAK,MAAM,IAAI,CAC5B,aAAG,SAAS,EAAC,UAAU,aAAE,QAAQ,CAAC,aAAa,IAAI,CAAC,4BAAyB,CAC7E,KAhB6B,QAAQ,CAAC,EAAE,CAiBhC,CACV,CAAC,GACG,IACG,EAEV,sCAAyB,kBAAkB,EAAC,KAAK,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE,aACnE,aAAI,SAAS,EAAC,YAAY,EAAC,EAAE,EAAC,kBAAkB,EAAC,KAAK,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,oCAEnE,EACJ,IAAI,CAAC,SAAS;yBACb,MAAM,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,KAAK,MAAM,CAAC;yBAC9C,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAClB,eAAK,SAAS,EAAC,MAAM,EAAmB,KAAK,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE,aAC/D,aAAI,SAAS,EAAC,YAAY,YAAE,QAAQ,CAAC,MAAM,GAAM,EAChD,CAAC,kBAAkB,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAC3D,YAAG,SAAS,EAAC,UAAU,wCAA4B,CACnD,CAAC,CAAC,CAAC,CACH,uBACE,CAAC,kBAAkB,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAC3D,uBAAsD,KAAK,CAAC,KAAK,IAAxD,GAAG,KAAK,CAAC,UAAU,IAAI,KAAK,CAAC,WAAW,EAAE,CAAoB,CACvE,CAAC,GACE,CACL,KAVyB,QAAQ,CAAC,EAAE,CAWhC,CACN,CAAC,IACM,IACL,CACN,CAAA;AACF,CAAC;AAED,KAAK,UAAU,mBAAmB,CAAC,QAAkB;IACpD,MAAM,UAAU,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,wBAAwB,CAAC,CAAA;IACjE,IAAI,CAAC,iBAAiB,CAAC,UAAU,CAAC;QAAE,OAAO,IAAI,CAAA;IAC/C,IAAI,UAAU,KAAK,kBAAkB,IAAI,CAAC,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,CAAC,EAAE,CAAC;QAC/F,OAAO,UAAU,CAAA;IAClB,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAA;IACpD,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IACvE,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,KAAK,UAAU;QAAE,OAAO,IAAI,CAAA;IAC1E,OAAO,IAAI,CAAC,KAAK,KAAK,wBAAwB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAA;AACnF,CAAC;AAED,SAAS,aAAa,CAAC,KAAc;IACpC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;AAC5E,CAAC;AAED,SAAS,UAAU,CAAC,EACnB,KAAK,EACL,MAAM,EACN,KAAK,GAKL;IACA,OAAO,CACN,eAAK,SAAS,EAAC,MAAM,EAAC,IAAI,EAAC,QAAQ,aAClC,aAAI,SAAS,EAAC,YAAY,iCAAsB,EAChD,YAAG,SAAS,EAAC,UAAU,YAAE,KAAK,GAAK,EAClC,MAAM,IAAI,YAAG,SAAS,EAAC,UAAU,YAAE,MAAM,GAAK,EAC9C,KAAK,IAAI,CACT,iBAAQ,IAAI,EAAC,QAAQ,EAAC,SAAS,EAAC,KAAK,EAAC,OAAO,EAAE,KAAK,sBAE3C,CACT,IACI,CACN,CAAA;AACF,CAAC;AAED,SAAS,UAAU,CAAC,EAAE,MAAM,EAAsC;IACjE,OAAO,CACN,gBAAO,SAAS,EAAC,KAAK,YACrB,0BACE,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAC/C,yBACC,aAAI,KAAK,EAAC,KAAK,YAAE,KAAK,GAAM,EAC5B,aAAI,SAAS,EAAC,MAAM,YAAE,KAAK,GAAM,KAFzB,KAAK,CAGT,CACL,CAAC,GACK,GACD,CACR,CAAA;AACF,CAAC;AAED,SAAS,UAAU,CAAC,KAAa;IAChC,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,CAAA;IAC5B,OAAO,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,cAAc,EAAE,CAAA;AACpE,CAAC;AAED,SAAS,YAAY,CAAC,KAA0C;IAC/D,IAAI,KAAK,KAAK,QAAQ;QAAE,OAAO,QAAQ,CAAA;IACvC,IAAI,KAAK,KAAK,YAAY;QAAE,OAAO,YAAY,CAAA;IAC/C,OAAO,SAAS,CAAA;AACjB,CAAC;AAED,SAAS,qBAAqB,CAAC,KAAsC;IACpE,IAAI,KAAK,KAAK,QAAQ;QAAE,OAAO,cAAc,CAAA;IAC7C,IAAI,KAAK,KAAK,YAAY;QAAE,OAAO,kBAAkB,CAAA;IACrD,OAAO,iBAAiB,CAAA;AACzB,CAAC"}
@@ -1,6 +1,7 @@
1
1
  import type { ScreenRegistry } from '../components/CmsApp.js';
2
- export declare function createCmsScreens({ allowInlineAuthorCreation, }?: {
2
+ export declare function createCmsScreens({ allowInlineAuthorCreation, surveyId, }?: {
3
3
  allowInlineAuthorCreation?: boolean;
4
+ surveyId?: string;
4
5
  }): ScreenRegistry;
5
6
  export declare const cmsScreens: ScreenRegistry;
6
7
  //# sourceMappingURL=registry.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../../src/ui/screens/registry.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;AA2B7D,wBAAgB,gBAAgB,CAAC,EAChC,yBAAgC,GAChC,GAAE;IACF,yBAAyB,CAAC,EAAE,OAAO,CAAA;CAC9B,GAAG,cAAc,CAgBtB;AAED,eAAO,MAAM,UAAU,EAAE,cAAmC,CAAA"}
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../../src/ui/screens/registry.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;AA4B7D,wBAAgB,gBAAgB,CAAC,EAChC,yBAAgC,EAChC,QAAQ,GACR,GAAE;IACF,yBAAyB,CAAC,EAAE,OAAO,CAAA;IACnC,QAAQ,CAAC,EAAE,MAAM,CAAA;CACZ,GAAG,cAAc,CAmBtB;AAED,eAAO,MAAM,UAAU,EAAE,cAAmC,CAAA"}
@@ -10,6 +10,7 @@ import { MediaScreen } from './MediaScreen.js';
10
10
  import { SettingsScreen } from './SettingsScreen.js';
11
11
  import { SocialShareScreen } from './SocialShareScreen.js';
12
12
  import { SubscriptionsScreen } from './SubscriptionsScreen.js';
13
+ import { SurveyResponsesScreen } from './SurveyResponsesScreen.js';
13
14
  import { TopicsScreen } from './TopicsScreen.js';
14
15
  // Thin wrappers that supply the openDoc seam for screens that need it.
15
16
  // On click, we navigate to the content route; the ContentRoute will handle
@@ -22,7 +23,7 @@ function CalendarRoute() {
22
23
  const { navigate } = useHashRouter();
23
24
  return _jsx(CalendarScreen, { openDoc: () => navigate('content') });
24
25
  }
25
- export function createCmsScreens({ allowInlineAuthorCreation = true, } = {}) {
26
+ export function createCmsScreens({ allowInlineAuthorCreation = true, surveyId, } = {}) {
26
27
  return {
27
28
  content: () => _jsx(ContentRoute, { allowInlineAuthorCreation: allowInlineAuthorCreation }),
28
29
  'content-insights': () => _jsx(ContentInsightsScreen, {}),
@@ -33,6 +34,9 @@ export function createCmsScreens({ allowInlineAuthorCreation = true, } = {}) {
33
34
  authors: () => _jsx(AuthorsScreen, {}),
34
35
  topics: () => _jsx(TopicsScreen, {}),
35
36
  subscriptions: () => _jsx(SubscriptionsScreen, {}),
37
+ ...(surveyId
38
+ ? { 'survey-responses': () => _jsx(SurveyResponsesScreen, { surveyId: surveyId }) }
39
+ : {}),
36
40
  'social-share': () => (_jsx(SocialShareScreen, { allowInlineAuthorCreation: allowInlineAuthorCreation })),
37
41
  settings: () => _jsx(SettingsScreen, {}),
38
42
  };
@@ -1 +1 @@
1
- {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../../src/ui/screens/registry.tsx"],"names":[],"mappings":";AAcA,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AACpD,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAA;AAClE,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAC1D,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9D,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAEhD,uEAAuE;AACvE,2EAA2E;AAC3E,+EAA+E;AAC/E,SAAS,cAAc;IACtB,MAAM,EAAE,QAAQ,EAAE,GAAG,aAAa,EAAE,CAAA;IACpC,OAAO,KAAC,eAAe,IAAC,OAAO,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,GAAI,CAAA;AAC/D,CAAC;AAED,SAAS,aAAa;IACrB,MAAM,EAAE,QAAQ,EAAE,GAAG,aAAa,EAAE,CAAA;IACpC,OAAO,KAAC,cAAc,IAAC,OAAO,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,GAAI,CAAA;AAC9D,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,EAChC,yBAAyB,GAAG,IAAI,MAG7B,EAAE;IACL,OAAO;QACN,OAAO,EAAE,GAAG,EAAE,CAAC,KAAC,YAAY,IAAC,yBAAyB,EAAE,yBAAyB,GAAI;QACrF,kBAAkB,EAAE,GAAG,EAAE,CAAC,KAAC,qBAAqB,KAAG;QACnD,SAAS,EAAE,GAAG,EAAE,CAAC,KAAC,cAAc,KAAG;QACnC,QAAQ,EAAE,GAAG,EAAE,CAAC,KAAC,aAAa,KAAG;QACjC,KAAK,EAAE,GAAG,EAAE,CAAC,KAAC,WAAW,KAAG;QAC5B,SAAS,EAAE,GAAG,EAAE,CAAC,KAAC,eAAe,KAAG;QACpC,OAAO,EAAE,GAAG,EAAE,CAAC,KAAC,aAAa,KAAG;QAChC,MAAM,EAAE,GAAG,EAAE,CAAC,KAAC,YAAY,KAAG;QAC9B,aAAa,EAAE,GAAG,EAAE,CAAC,KAAC,mBAAmB,KAAG;QAC5C,cAAc,EAAE,GAAG,EAAE,CAAC,CACrB,KAAC,iBAAiB,IAAC,yBAAyB,EAAE,yBAAyB,GAAI,CAC3E;QACD,QAAQ,EAAE,GAAG,EAAE,CAAC,KAAC,cAAc,KAAG;KAClC,CAAA;AACF,CAAC;AAED,MAAM,CAAC,MAAM,UAAU,GAAmB,gBAAgB,EAAE,CAAA"}
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../../src/ui/screens/registry.tsx"],"names":[],"mappings":";AAcA,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AACpD,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAA;AAClE,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAC1D,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9D,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAA;AAClE,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAEhD,uEAAuE;AACvE,2EAA2E;AAC3E,+EAA+E;AAC/E,SAAS,cAAc;IACtB,MAAM,EAAE,QAAQ,EAAE,GAAG,aAAa,EAAE,CAAA;IACpC,OAAO,KAAC,eAAe,IAAC,OAAO,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,GAAI,CAAA;AAC/D,CAAC;AAED,SAAS,aAAa;IACrB,MAAM,EAAE,QAAQ,EAAE,GAAG,aAAa,EAAE,CAAA;IACpC,OAAO,KAAC,cAAc,IAAC,OAAO,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,GAAI,CAAA;AAC9D,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,EAChC,yBAAyB,GAAG,IAAI,EAChC,QAAQ,MAIL,EAAE;IACL,OAAO;QACN,OAAO,EAAE,GAAG,EAAE,CAAC,KAAC,YAAY,IAAC,yBAAyB,EAAE,yBAAyB,GAAI;QACrF,kBAAkB,EAAE,GAAG,EAAE,CAAC,KAAC,qBAAqB,KAAG;QACnD,SAAS,EAAE,GAAG,EAAE,CAAC,KAAC,cAAc,KAAG;QACnC,QAAQ,EAAE,GAAG,EAAE,CAAC,KAAC,aAAa,KAAG;QACjC,KAAK,EAAE,GAAG,EAAE,CAAC,KAAC,WAAW,KAAG;QAC5B,SAAS,EAAE,GAAG,EAAE,CAAC,KAAC,eAAe,KAAG;QACpC,OAAO,EAAE,GAAG,EAAE,CAAC,KAAC,aAAa,KAAG;QAChC,MAAM,EAAE,GAAG,EAAE,CAAC,KAAC,YAAY,KAAG;QAC9B,aAAa,EAAE,GAAG,EAAE,CAAC,KAAC,mBAAmB,KAAG;QAC5C,GAAG,CAAC,QAAQ;YACX,CAAC,CAAC,EAAE,kBAAkB,EAAE,GAAG,EAAE,CAAC,KAAC,qBAAqB,IAAC,QAAQ,EAAE,QAAQ,GAAI,EAAE;YAC7E,CAAC,CAAC,EAAE,CAAC;QACN,cAAc,EAAE,GAAG,EAAE,CAAC,CACrB,KAAC,iBAAiB,IAAC,yBAAyB,EAAE,yBAAyB,GAAI,CAC3E;QACD,QAAQ,EAAE,GAAG,EAAE,CAAC,KAAC,cAAc,KAAG;KAClC,CAAA;AACF,CAAC;AAED,MAAM,CAAC,MAAM,UAAU,GAAmB,gBAAgB,EAAE,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@growth-labs/cms",
3
- "version": "0.5.5",
3
+ "version": "0.5.7",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "growth-labs-cms-migrations": "./dist/cli/migrations.js"
@@ -0,0 +1,50 @@
1
+ import type { D1Database } from './d1.js'
2
+
3
+ export interface DekFailureNotificationInput {
4
+ db: D1Database
5
+ workspaceId: string
6
+ contentId: string
7
+ title: string
8
+ reason: string
9
+ userId?: string | null
10
+ }
11
+
12
+ /**
13
+ * Create unread, in-app notifications when publication ships without a dek.
14
+ * Interactive publishes target the actor; background publishes target every
15
+ * editorial member because no actor is present to receive the failure.
16
+ */
17
+ export async function createDekFailureNotifications(
18
+ input: DekFailureNotificationInput,
19
+ ): Promise<number> {
20
+ let userIds: string[]
21
+ if (input.userId) {
22
+ userIds = [input.userId]
23
+ } else {
24
+ const rows = await input.db
25
+ .prepare(
26
+ `SELECT user_id
27
+ FROM workspace_memberships
28
+ WHERE workspace_id = ?
29
+ AND role IN ('owner', 'senior_editor', 'editor')
30
+ ORDER BY user_id ASC`,
31
+ )
32
+ .bind(input.workspaceId)
33
+ .all<{ user_id: string }>()
34
+ userIds = (rows.results ?? []).map((row) => row.user_id)
35
+ }
36
+
37
+ const reason = input.reason.trim().slice(0, 160) || 'unknown error'
38
+ const body = `Content was published without a subheading: “${input.title}”. AI generation failed (${reason}). Open the item to add one manually.`
39
+ for (const userId of [...new Set(userIds)]) {
40
+ await input.db
41
+ .prepare(
42
+ `INSERT INTO cms_notifications
43
+ (id, workspace_id, user_id, kind, body, created_at)
44
+ VALUES (?, ?, ?, 'ai_dek_failed', ?, unixepoch())`,
45
+ )
46
+ .bind(crypto.randomUUID(), input.workspaceId, userId, body)
47
+ .run()
48
+ }
49
+ return new Set(userIds).size
50
+ }
@@ -0,0 +1,138 @@
1
+ import type { LlmDispatch } from '../providers/types.js'
2
+ import { dekPrompt } from './ai-prompts.js'
3
+ import { applyAiWrite, parseLockedFields } from './ai-writeback.js'
4
+ import type { D1Database } from './d1.js'
5
+ import { type ContentItemRecord, getContentItem } from './publisher.js'
6
+
7
+ export const DEK_EXEMPLAR_LIMIT = 6
8
+
9
+ export interface DekGenerationContext {
10
+ item: ContentItemRecord
11
+ source: string
12
+ exemplars: string[]
13
+ }
14
+
15
+ export type GenerateContentDekResult =
16
+ | { status: 'generated'; dek: string; written: true }
17
+ | { status: 'locked'; dek: string; written: false }
18
+ | { status: 'unavailable'; dek: null; written: false; reason: string }
19
+ | { status: 'not_found'; dek: null; written: false; reason: 'not_found' }
20
+
21
+ async function loadTypeSource(db: D1Database, item: ContentItemRecord): Promise<string> {
22
+ if (item.type === 'video') {
23
+ const row = await db
24
+ .prepare('SELECT script FROM video_content WHERE content_id = ? LIMIT 1')
25
+ .bind(item.id)
26
+ .first<{ script: string | null }>()
27
+ return row?.script?.trim() ?? ''
28
+ }
29
+ if (item.type === 'podcast') {
30
+ const row = await db
31
+ .prepare('SELECT transcript FROM podcast_content WHERE content_id = ? LIMIT 1')
32
+ .bind(item.id)
33
+ .first<{ transcript: string | null }>()
34
+ return row?.transcript?.trim() ?? ''
35
+ }
36
+ const row = await db
37
+ .prepare(
38
+ `SELECT body_markdown, body_html, subtitle
39
+ FROM article_content WHERE content_id = ? LIMIT 1`,
40
+ )
41
+ .bind(item.id)
42
+ .first<{
43
+ body_markdown: string | null
44
+ body_html: string | null
45
+ subtitle: string | null
46
+ }>()
47
+ return (row?.body_markdown || row?.body_html || row?.subtitle || '').trim()
48
+ }
49
+
50
+ export async function loadDekGenerationContext(
51
+ db: D1Database,
52
+ contentId: string,
53
+ ): Promise<DekGenerationContext | null> {
54
+ const item = await getContentItem(db, contentId)
55
+ if (!item) return null
56
+
57
+ const typeSource = await loadTypeSource(db, item)
58
+ const description = item.description?.trim() ?? ''
59
+ const source = [description, typeSource].filter(Boolean).join('\n\n')
60
+ const rows = await db
61
+ .prepare(
62
+ `SELECT title, excerpt
63
+ FROM content_items
64
+ WHERE status = 'published'
65
+ AND deleted_at IS NULL
66
+ AND id <> ?
67
+ AND excerpt IS NOT NULL
68
+ AND trim(excerpt) <> ''
69
+ ORDER BY CASE WHEN type = ? THEN 0 ELSE 1 END,
70
+ COALESCE(published_at, 0) DESC,
71
+ id ASC
72
+ LIMIT ?`,
73
+ )
74
+ .bind(contentId, item.type, DEK_EXEMPLAR_LIMIT)
75
+ .all<{ title: string; excerpt: string }>()
76
+ const exemplars = (rows.results ?? []).map(
77
+ (row) => `Headline: ${row.title}\nDek: ${row.excerpt.trim()}`,
78
+ )
79
+
80
+ return { item, source, exemplars }
81
+ }
82
+
83
+ function normalizeDek(text: string): string {
84
+ return text
85
+ .trim()
86
+ .replace(/^(["“])|(["”])$/g, '')
87
+ .replace(/\s+/g, ' ')
88
+ .trim()
89
+ }
90
+
91
+ export async function generateContentDek(input: {
92
+ db: D1Database
93
+ contentId: string
94
+ llm: LlmDispatch
95
+ }): Promise<GenerateContentDekResult> {
96
+ const context = await loadDekGenerationContext(input.db, input.contentId)
97
+ if (!context) {
98
+ return { status: 'not_found', dek: null, written: false, reason: 'not_found' }
99
+ }
100
+
101
+ const result = await input.llm.dispatch({
102
+ ...dekPrompt({
103
+ title: context.item.title,
104
+ source: context.source,
105
+ exemplars: context.exemplars,
106
+ }),
107
+ stream: false,
108
+ })
109
+ if (result instanceof ReadableStream) {
110
+ return { status: 'unavailable', dek: null, written: false, reason: 'stream_result' }
111
+ }
112
+ if (result.status === 'unavailable') {
113
+ return {
114
+ status: 'unavailable',
115
+ dek: null,
116
+ written: false,
117
+ reason: result.reason ?? 'unavailable',
118
+ }
119
+ }
120
+
121
+ const dek = normalizeDek(result.text)
122
+ if (!dek) {
123
+ return { status: 'unavailable', dek: null, written: false, reason: 'empty_result' }
124
+ }
125
+
126
+ const write = applyAiWrite({
127
+ locked: parseLockedFields(context.item.ai_locked_fields),
128
+ field: 'excerpt',
129
+ value: dek,
130
+ })
131
+ if (!write.written) return { status: 'locked', dek, written: false }
132
+
133
+ await input.db
134
+ .prepare('UPDATE content_items SET excerpt = ?, updated_at = unixepoch() WHERE id = ?')
135
+ .bind(dek, input.contentId)
136
+ .run()
137
+ return { status: 'generated', dek, written: true }
138
+ }
@@ -47,12 +47,37 @@ export interface MetaDescriptionPromptInput {
47
47
  body: string
48
48
  }
49
49
 
50
+ /** Input to dekPrompt. A dek is display copy, not SEO metadata. */
51
+ export interface DekPromptInput {
52
+ title: string
53
+ source: string
54
+ exemplars?: string[]
55
+ }
56
+
50
57
  /** The typed prompt contract shape consumed by LlmDispatch.dispatch(). */
51
58
  export interface AiPromptContract {
52
59
  task: string
53
60
  prompt: string
54
61
  }
55
62
 
63
+ /**
64
+ * Build the prompt for a standfirst-style display subheading (dek).
65
+ * This deliberately differs from the SEO meta-description task: it tells the
66
+ * reader what the piece argues, reveals, or shows in the publication's voice.
67
+ */
68
+ export function dekPrompt(input: DekPromptInput): AiPromptContract {
69
+ const exemplarsBlock = buildExemplarsBlock(input.exemplars)
70
+ const prompt = `${HOUSE_STYLE_SYSTEM}${exemplarsBlock}
71
+
72
+ Task: Write one standfirst-style subheading (dek) for this piece. It must be 1–2 sentences, roughly 120–200 characters, and reader-facing. State what the piece argues, reveals, or shows. It must not repeat or lightly rephrase the headline. Avoid clickbait and never use the boilerplate shape “with context on the stakes for…”. Return only the dek text with no label, quotation marks, commentary, or preamble.
73
+
74
+ Headline: ${input.title}
75
+
76
+ Source material:
77
+ ${input.source}`
78
+ return { task: 'cms.dek', prompt }
79
+ }
80
+
56
81
  /** Build the exemplars block when exemplars are supplied. */
57
82
  function buildExemplarsBlock(exemplars: string[] | undefined): string {
58
83
  if (!exemplars || exemplars.length === 0) return ''
@@ -5,9 +5,23 @@
5
5
 
6
6
  // Activity log write helper — first-party D1, cms_activity_log (P5 Task 3).
7
7
  export { type ActivityEntry, logActivity } from './activity-log.js'
8
+ // Display-subheading generation with deterministic house exemplars.
9
+ export {
10
+ DEK_EXEMPLAR_LIMIT,
11
+ type DekGenerationContext,
12
+ type GenerateContentDekResult,
13
+ generateContentDek,
14
+ loadDekGenerationContext,
15
+ } from './ai-dek.js'
16
+ export {
17
+ createDekFailureNotifications,
18
+ type DekFailureNotificationInput,
19
+ } from './ai-dek-notifications.js'
8
20
  // AI prompt contracts + house-style system prompt (P4 Task 1).
9
21
  export {
10
22
  type AiPromptContract,
23
+ type DekPromptInput,
24
+ dekPrompt,
11
25
  type HeadlinesPromptInput,
12
26
  HOUSE_STYLE_SYSTEM,
13
27
  headlinesPrompt,
package/src/index.ts CHANGED
@@ -34,10 +34,8 @@ export * from './engine/index.js'
34
34
 
35
35
  // Injected-provider interfaces + null implementations (spec §3.3).
36
36
  export * from './providers/index.js'
37
-
38
37
  // Route-layer user types (exported at root for convenience; full routes surface at @growth-labs/cms/routes).
39
38
  export type { CmsUser } from './routes/context.js'
40
-
41
39
  export {
42
40
  type ArticleContentRow,
43
41
  type AuthorRow,
@@ -82,3 +80,5 @@ export {
82
80
  type WorkspaceMembershipRow,
83
81
  type WorkspaceSettingsRow,
84
82
  } from './schema/index.js'
83
+ export * from './surveys/errors.js'
84
+ export * from './surveys/schema.js'
@@ -121,6 +121,10 @@ export default function growthLabsCms(userOptions: CmsIntegrationOptions): Astro
121
121
  entrypoint: resolveEntrypoint('../ui/api/content/[id]'),
122
122
  })
123
123
  // AI sub-routes
124
+ injectRoute({
125
+ pattern: `${base}/api/content/[id]/ai/dek`,
126
+ entrypoint: resolveEntrypoint('../ui/api/content/[id]/ai/dek'),
127
+ })
124
128
  injectRoute({
125
129
  pattern: `${base}/api/content/[id]/ai/headlines`,
126
130
  entrypoint: resolveEntrypoint('../ui/api/content/[id]/ai/headlines'),
@@ -229,6 +233,15 @@ export default function growthLabsCms(userOptions: CmsIntegrationOptions): Astro
229
233
  pattern: `${base}/api/analytics`,
230
234
  entrypoint: resolveEntrypoint('../ui/api/analytics'),
231
235
  })
236
+ // Survey reporting — protected JSON plus CSV, backed by the optional host provider.
237
+ injectRoute({
238
+ pattern: `${base}/api/surveys/[surveyId]`,
239
+ entrypoint: resolveEntrypoint('../ui/api/surveys/[surveyId]'),
240
+ })
241
+ injectRoute({
242
+ pattern: `${base}/api/surveys/[surveyId]/responses.csv`,
243
+ entrypoint: resolveEntrypoint('../ui/api/surveys/[surveyId]/responses.csv'),
244
+ })
232
245
  // Calendar
233
246
  injectRoute({
234
247
  pattern: `${base}/api/calendar`,
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod'
2
+ import { SurveyIdSchema } from '../surveys/schema.js'
2
3
 
3
4
  const workspaceSchema = z.object({
4
5
  id: z.string(),
@@ -22,6 +23,12 @@ const socialSharingSchema = z
22
23
  })
23
24
  .default({})
24
25
 
26
+ const surveyResultsSchema = z
27
+ .object({
28
+ surveyId: SurveyIdSchema,
29
+ })
30
+ .strict()
31
+
25
32
  export const cmsIntegrationOptionsSchema = z
26
33
  .object({
27
34
  workspaces: z.array(workspaceSchema).min(1, 'at least one workspace is required'),
@@ -30,6 +37,7 @@ export const cmsIntegrationOptionsSchema = z
30
37
  searchEnabled: z.boolean().default(true),
31
38
  theme: themeSchema,
32
39
  socialSharing: socialSharingSchema,
40
+ surveyResults: surveyResultsSchema.optional(),
33
41
  })
34
42
  .transform((o) => ({ ...o, activeWorkspaceId: o.activeWorkspaceId ?? o.workspaces[0].id }))
35
43
 
@@ -7,6 +7,7 @@ import type {
7
7
  SeoProvider,
8
8
  SettingsProvider,
9
9
  SubscriptionsProvider,
10
+ SurveyProvider,
10
11
  } from './types.js'
11
12
  import { ProviderHttpError } from './types.js'
12
13
 
@@ -179,6 +180,23 @@ export const nullSettings: SettingsProvider = {
179
180
  },
180
181
  }
181
182
 
183
+ export const nullSurveys: SurveyProvider = {
184
+ async getResults() {
185
+ throw new ProviderHttpError(
186
+ 503,
187
+ 'survey_provider_unavailable',
188
+ 'Survey provider not configured',
189
+ )
190
+ },
191
+ async getCsv() {
192
+ throw new ProviderHttpError(
193
+ 503,
194
+ 'survey_provider_unavailable',
195
+ 'Survey provider not configured',
196
+ )
197
+ },
198
+ }
199
+
182
200
  export const nullProviders: CmsProviders = {
183
201
  analytics: nullAnalytics,
184
202
  subscriptions: nullSubscriptions,
@@ -186,4 +204,5 @@ export const nullProviders: CmsProviders = {
186
204
  seo: defaultSeo,
187
205
  search: nullSearch,
188
206
  settings: nullSettings,
207
+ surveys: nullSurveys,
189
208
  }