@assistant-ui/react-generative-ui 0.0.24 → 0.1.0

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 (338) hide show
  1. package/README.md +2 -0
  2. package/dist/a2ui.d.ts +1 -4
  3. package/dist/a2ui.d.ts.map +1 -1
  4. package/dist/a2ui.js +1 -4
  5. package/dist/index.d.ts +1 -2
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1 -10
  8. package/dist/index.server.d.ts +1 -2
  9. package/dist/index.server.d.ts.map +1 -1
  10. package/dist/index.server.js +1 -10
  11. package/dist/ir.d.ts +1 -111
  12. package/dist/ir.d.ts.map +1 -1
  13. package/dist/ir.js +1 -175
  14. package/dist/slack.d.ts +1 -4
  15. package/dist/slack.d.ts.map +1 -1
  16. package/dist/slack.js +1 -4
  17. package/dist/teams.d.ts +1 -4
  18. package/dist/teams.d.ts.map +1 -1
  19. package/dist/teams.js +1 -4
  20. package/package.json +4 -5
  21. package/src/a2ui.ts +1 -22
  22. package/src/index.server.ts +1 -2
  23. package/src/index.test.ts +75 -0
  24. package/src/index.ts +1 -2
  25. package/src/ir.ts +1 -279
  26. package/src/slack.ts +1 -39
  27. package/src/teams.ts +1 -35
  28. package/dist/JSONGenerativeUI.client.d.ts +0 -28
  29. package/dist/JSONGenerativeUI.client.d.ts.map +0 -1
  30. package/dist/JSONGenerativeUI.client.js +0 -101
  31. package/dist/JSONGenerativeUI.client.js.map +0 -1
  32. package/dist/JSONGenerativeUI.server.d.ts +0 -21
  33. package/dist/JSONGenerativeUI.server.d.ts.map +0 -1
  34. package/dist/JSONGenerativeUI.server.js +0 -32
  35. package/dist/JSONGenerativeUI.server.js.map +0 -1
  36. package/dist/JSONGenerativeUI.shared.d.ts +0 -63
  37. package/dist/JSONGenerativeUI.shared.d.ts.map +0 -1
  38. package/dist/JSONGenerativeUI.shared.js +0 -31
  39. package/dist/JSONGenerativeUI.shared.js.map +0 -1
  40. package/dist/RadioGroupScope.d.ts +0 -6
  41. package/dist/RadioGroupScope.d.ts.map +0 -1
  42. package/dist/RadioGroupScope.js +0 -25
  43. package/dist/RadioGroupScope.js.map +0 -1
  44. package/dist/a2ui/BindingContext.d.ts +0 -11
  45. package/dist/a2ui/BindingContext.d.ts.map +0 -1
  46. package/dist/a2ui/BindingContext.js +0 -12
  47. package/dist/a2ui/BindingContext.js.map +0 -1
  48. package/dist/a2ui/PresentRenderer.d.ts +0 -11
  49. package/dist/a2ui/PresentRenderer.d.ts.map +0 -1
  50. package/dist/a2ui/PresentRenderer.js +0 -39
  51. package/dist/a2ui/PresentRenderer.js.map +0 -1
  52. package/dist/a2ui/convert.d.ts +0 -15
  53. package/dist/a2ui/convert.d.ts.map +0 -1
  54. package/dist/a2ui/convert.js +0 -728
  55. package/dist/a2ui/convert.js.map +0 -1
  56. package/dist/a2ui/dataModel.d.ts +0 -9
  57. package/dist/a2ui/dataModel.d.ts.map +0 -1
  58. package/dist/a2ui/dataModel.js +0 -44
  59. package/dist/a2ui/dataModel.js.map +0 -1
  60. package/dist/a2ui/reducer.d.ts +0 -8
  61. package/dist/a2ui/reducer.d.ts.map +0 -1
  62. package/dist/a2ui/reducer.js +0 -242
  63. package/dist/a2ui/reducer.js.map +0 -1
  64. package/dist/a2ui/snapshot.d.ts +0 -3
  65. package/dist/a2ui/snapshot.d.ts.map +0 -1
  66. package/dist/a2ui/snapshot.js +0 -33
  67. package/dist/a2ui/snapshot.js.map +0 -1
  68. package/dist/a2ui/types.d.ts +0 -88
  69. package/dist/a2ui/types.d.ts.map +0 -1
  70. package/dist/a2ui/types.js +0 -6
  71. package/dist/a2ui/types.js.map +0 -1
  72. package/dist/a2ui/valueFunctions.d.ts +0 -16
  73. package/dist/a2ui/valueFunctions.d.ts.map +0 -1
  74. package/dist/a2ui/valueFunctions.js +0 -332
  75. package/dist/a2ui/valueFunctions.js.map +0 -1
  76. package/dist/actionRegistry.d.ts +0 -29
  77. package/dist/actionRegistry.d.ts.map +0 -1
  78. package/dist/actionRegistry.js +0 -41
  79. package/dist/actionRegistry.js.map +0 -1
  80. package/dist/answeredValues.d.ts +0 -16
  81. package/dist/answeredValues.d.ts.map +0 -1
  82. package/dist/answeredValues.js +0 -37
  83. package/dist/answeredValues.js.map +0 -1
  84. package/dist/buildPresentParameters.d.ts +0 -21
  85. package/dist/buildPresentParameters.d.ts.map +0 -1
  86. package/dist/buildPresentParameters.js +0 -95
  87. package/dist/buildPresentParameters.js.map +0 -1
  88. package/dist/constants.d.ts +0 -15
  89. package/dist/constants.d.ts.map +0 -1
  90. package/dist/constants.js +0 -19
  91. package/dist/constants.js.map +0 -1
  92. package/dist/convert/boundSpec.d.ts +0 -48
  93. package/dist/convert/boundSpec.d.ts.map +0 -1
  94. package/dist/convert/boundSpec.js +0 -108
  95. package/dist/convert/boundSpec.js.map +0 -1
  96. package/dist/convert/copyBounded.d.ts +0 -25
  97. package/dist/convert/copyBounded.d.ts.map +0 -1
  98. package/dist/convert/copyBounded.js +0 -38
  99. package/dist/convert/copyBounded.js.map +0 -1
  100. package/dist/convert/isElement.d.ts +0 -3
  101. package/dist/convert/isElement.d.ts.map +0 -1
  102. package/dist/convert/isElement.js +0 -6
  103. package/dist/convert/isElement.js.map +0 -1
  104. package/dist/convert/takeRun.d.ts +0 -6
  105. package/dist/convert/takeRun.d.ts.map +0 -1
  106. package/dist/convert/takeRun.js +0 -20
  107. package/dist/convert/takeRun.js.map +0 -1
  108. package/dist/defineGenerativeComponents.d.ts +0 -29
  109. package/dist/defineGenerativeComponents.d.ts.map +0 -1
  110. package/dist/defineGenerativeComponents.js +0 -34
  111. package/dist/defineGenerativeComponents.js.map +0 -1
  112. package/dist/fieldReferences.d.ts +0 -9
  113. package/dist/fieldReferences.d.ts.map +0 -1
  114. package/dist/fieldReferences.js +0 -53
  115. package/dist/fieldReferences.js.map +0 -1
  116. package/dist/generativeUIToJSX.d.ts +0 -18
  117. package/dist/generativeUIToJSX.d.ts.map +0 -1
  118. package/dist/generativeUIToJSX.js +0 -76
  119. package/dist/generativeUIToJSX.js.map +0 -1
  120. package/dist/index.shared.d.ts +0 -15
  121. package/dist/index.shared.d.ts.map +0 -1
  122. package/dist/index.shared.js +0 -9
  123. package/dist/ir.js.map +0 -1
  124. package/dist/renderGenerativeUI.d.ts +0 -14
  125. package/dist/renderGenerativeUI.d.ts.map +0 -1
  126. package/dist/renderGenerativeUI.js +0 -83
  127. package/dist/renderGenerativeUI.js.map +0 -1
  128. package/dist/scopeSchema.d.ts +0 -6
  129. package/dist/scopeSchema.d.ts.map +0 -1
  130. package/dist/scopeSchema.js +0 -56
  131. package/dist/scopeSchema.js.map +0 -1
  132. package/dist/slack/constants.d.ts +0 -93
  133. package/dist/slack/constants.d.ts.map +0 -1
  134. package/dist/slack/constants.js +0 -146
  135. package/dist/slack/constants.js.map +0 -1
  136. package/dist/slack/decodeBlockAction.d.ts +0 -10
  137. package/dist/slack/decodeBlockAction.d.ts.map +0 -1
  138. package/dist/slack/decodeBlockAction.js +0 -129
  139. package/dist/slack/decodeBlockAction.js.map +0 -1
  140. package/dist/slack/fromSlackBlocks.d.ts +0 -27
  141. package/dist/slack/fromSlackBlocks.d.ts.map +0 -1
  142. package/dist/slack/fromSlackBlocks.js +0 -483
  143. package/dist/slack/fromSlackBlocks.js.map +0 -1
  144. package/dist/slack/toSlackBlocks.d.ts +0 -4
  145. package/dist/slack/toSlackBlocks.d.ts.map +0 -1
  146. package/dist/slack/toSlackBlocks.js +0 -901
  147. package/dist/slack/toSlackBlocks.js.map +0 -1
  148. package/dist/slack/types.d.ts +0 -204
  149. package/dist/slack/types.d.ts.map +0 -1
  150. package/dist/slack/types.js +0 -0
  151. package/dist/teams/constants.d.ts +0 -34
  152. package/dist/teams/constants.d.ts.map +0 -1
  153. package/dist/teams/constants.js +0 -68
  154. package/dist/teams/constants.js.map +0 -1
  155. package/dist/teams/decodeSubmitData.d.ts +0 -26
  156. package/dist/teams/decodeSubmitData.d.ts.map +0 -1
  157. package/dist/teams/decodeSubmitData.js +0 -70
  158. package/dist/teams/decodeSubmitData.js.map +0 -1
  159. package/dist/teams/temporalId.d.ts +0 -5
  160. package/dist/teams/temporalId.d.ts.map +0 -1
  161. package/dist/teams/temporalId.js +0 -28
  162. package/dist/teams/temporalId.js.map +0 -1
  163. package/dist/teams/toAdaptiveCard.d.ts +0 -45
  164. package/dist/teams/toAdaptiveCard.d.ts.map +0 -1
  165. package/dist/teams/toAdaptiveCard.js +0 -642
  166. package/dist/teams/toAdaptiveCard.js.map +0 -1
  167. package/dist/teams/toTeamsAttachments.d.ts +0 -10
  168. package/dist/teams/toTeamsAttachments.d.ts.map +0 -1
  169. package/dist/teams/toTeamsAttachments.js +0 -99
  170. package/dist/teams/toTeamsAttachments.js.map +0 -1
  171. package/dist/teams/types.d.ts +0 -224
  172. package/dist/teams/types.d.ts.map +0 -1
  173. package/dist/teams/types.js +0 -0
  174. package/dist/temporal.d.ts +0 -33
  175. package/dist/temporal.d.ts.map +0 -1
  176. package/dist/temporal.js +0 -153
  177. package/dist/temporal.js.map +0 -1
  178. package/dist/types.d.ts +0 -99
  179. package/dist/types.d.ts.map +0 -1
  180. package/dist/types.js +0 -0
  181. package/dist/vocabulary/alert.d.ts +0 -32
  182. package/dist/vocabulary/alert.d.ts.map +0 -1
  183. package/dist/vocabulary/alert.js +0 -160
  184. package/dist/vocabulary/alert.js.map +0 -1
  185. package/dist/vocabulary/collectFormValues.d.ts +0 -23
  186. package/dist/vocabulary/collectFormValues.d.ts.map +0 -1
  187. package/dist/vocabulary/collectFormValues.js +0 -44
  188. package/dist/vocabulary/collectFormValues.js.map +0 -1
  189. package/dist/vocabulary/data.d.ts +0 -66
  190. package/dist/vocabulary/data.d.ts.map +0 -1
  191. package/dist/vocabulary/data.js +0 -503
  192. package/dist/vocabulary/data.js.map +0 -1
  193. package/dist/vocabulary/dispatch.d.ts +0 -9
  194. package/dist/vocabulary/dispatch.d.ts.map +0 -1
  195. package/dist/vocabulary/dispatch.js +0 -50
  196. package/dist/vocabulary/dispatch.js.map +0 -1
  197. package/dist/vocabulary/fact.d.ts +0 -19
  198. package/dist/vocabulary/fact.d.ts.map +0 -1
  199. package/dist/vocabulary/fact.js +0 -51
  200. package/dist/vocabulary/fact.js.map +0 -1
  201. package/dist/vocabulary/form.d.ts +0 -11
  202. package/dist/vocabulary/form.d.ts.map +0 -1
  203. package/dist/vocabulary/form.js +0 -23
  204. package/dist/vocabulary/form.js.map +0 -1
  205. package/dist/vocabulary/formatValue.d.ts +0 -7
  206. package/dist/vocabulary/formatValue.d.ts.map +0 -1
  207. package/dist/vocabulary/formatValue.js +0 -76
  208. package/dist/vocabulary/formatValue.js.map +0 -1
  209. package/dist/vocabulary/icon.d.ts +0 -41
  210. package/dist/vocabulary/icon.d.ts.map +0 -1
  211. package/dist/vocabulary/icon.js +0 -272
  212. package/dist/vocabulary/icon.js.map +0 -1
  213. package/dist/vocabulary/index.d.ts +0 -12
  214. package/dist/vocabulary/index.d.ts.map +0 -1
  215. package/dist/vocabulary/index.js +0 -36
  216. package/dist/vocabulary/index.js.map +0 -1
  217. package/dist/vocabulary/interactive.d.ts +0 -230
  218. package/dist/vocabulary/interactive.d.ts.map +0 -1
  219. package/dist/vocabulary/interactive.js +0 -584
  220. package/dist/vocabulary/interactive.js.map +0 -1
  221. package/dist/vocabulary/layout.d.ts +0 -79
  222. package/dist/vocabulary/layout.d.ts.map +0 -1
  223. package/dist/vocabulary/layout.js +0 -139
  224. package/dist/vocabulary/layout.js.map +0 -1
  225. package/dist/vocabulary/list.d.ts +0 -14
  226. package/dist/vocabulary/list.d.ts.map +0 -1
  227. package/dist/vocabulary/list.js +0 -50
  228. package/dist/vocabulary/list.js.map +0 -1
  229. package/dist/vocabulary/media.d.ts +0 -25
  230. package/dist/vocabulary/media.d.ts.map +0 -1
  231. package/dist/vocabulary/media.js +0 -42
  232. package/dist/vocabulary/media.js.map +0 -1
  233. package/dist/vocabulary/text.d.ts +0 -57
  234. package/dist/vocabulary/text.d.ts.map +0 -1
  235. package/dist/vocabulary/text.js +0 -49
  236. package/dist/vocabulary/text.js.map +0 -1
  237. package/dist/vocabulary/toTextContent.d.ts +0 -2
  238. package/dist/vocabulary/toTextContent.d.ts.map +0 -1
  239. package/dist/vocabulary/toTextContent.js +0 -6
  240. package/dist/vocabulary/toTextContent.js.map +0 -1
  241. package/src/JSONGenerativeUI.client.tsx +0 -168
  242. package/src/JSONGenerativeUI.server.tsx +0 -39
  243. package/src/JSONGenerativeUI.shared.ts +0 -90
  244. package/src/JSONGenerativeUI.test.tsx +0 -1298
  245. package/src/RadioGroupScope.server.test.tsx +0 -24
  246. package/src/RadioGroupScope.tsx +0 -34
  247. package/src/a2ui/BindingContext.ts +0 -26
  248. package/src/a2ui/PresentRenderer.tsx +0 -83
  249. package/src/a2ui/convert.test.ts +0 -2923
  250. package/src/a2ui/convert.ts +0 -1207
  251. package/src/a2ui/dataModel.ts +0 -69
  252. package/src/a2ui/reducer.test.ts +0 -422
  253. package/src/a2ui/reducer.ts +0 -363
  254. package/src/a2ui/snapshot.test.ts +0 -52
  255. package/src/a2ui/snapshot.ts +0 -52
  256. package/src/a2ui/types.ts +0 -114
  257. package/src/a2ui/valueFunctions.test.ts +0 -271
  258. package/src/a2ui/valueFunctions.ts +0 -523
  259. package/src/actionRegistry.test.ts +0 -90
  260. package/src/actionRegistry.ts +0 -88
  261. package/src/answeredValues.test.ts +0 -33
  262. package/src/answeredValues.tsx +0 -70
  263. package/src/buildPresentParameters.test.ts +0 -246
  264. package/src/buildPresentParameters.ts +0 -130
  265. package/src/constants.ts +0 -17
  266. package/src/convert/boundSpec.test.ts +0 -192
  267. package/src/convert/boundSpec.ts +0 -134
  268. package/src/convert/copyBounded.test.ts +0 -131
  269. package/src/convert/copyBounded.ts +0 -38
  270. package/src/convert/isElement.ts +0 -6
  271. package/src/convert/takeRun.test.ts +0 -64
  272. package/src/convert/takeRun.ts +0 -19
  273. package/src/defineGenerativeComponents.ts +0 -38
  274. package/src/fieldReferences.test.ts +0 -66
  275. package/src/fieldReferences.ts +0 -83
  276. package/src/generativeUIToJSX.test.ts +0 -268
  277. package/src/generativeUIToJSX.ts +0 -152
  278. package/src/index.server.test.ts +0 -106
  279. package/src/index.shared.ts +0 -61
  280. package/src/ir.test.ts +0 -203
  281. package/src/renderGenerativeUI.test.tsx +0 -373
  282. package/src/renderGenerativeUI.tsx +0 -165
  283. package/src/scopeSchema.test.ts +0 -72
  284. package/src/scopeSchema.ts +0 -71
  285. package/src/slack/a2uiSlack.test.ts +0 -198
  286. package/src/slack/constants.ts +0 -194
  287. package/src/slack/datePicker.test.ts +0 -612
  288. package/src/slack/decodeBlockAction.test.ts +0 -233
  289. package/src/slack/decodeBlockAction.ts +0 -232
  290. package/src/slack/fromSlackBlocks.test.ts +0 -1465
  291. package/src/slack/fromSlackBlocks.ts +0 -789
  292. package/src/slack/toSlackBlocks.test.ts +0 -3065
  293. package/src/slack/toSlackBlocks.ts +0 -1818
  294. package/src/slack/types.ts +0 -264
  295. package/src/teams/constants.ts +0 -93
  296. package/src/teams/decodeSubmitData.test.ts +0 -397
  297. package/src/teams/decodeSubmitData.ts +0 -110
  298. package/src/teams/temporal.test.ts +0 -766
  299. package/src/teams/temporalId.ts +0 -56
  300. package/src/teams/toAdaptiveCard.test.ts +0 -1897
  301. package/src/teams/toAdaptiveCard.ts +0 -1057
  302. package/src/teams/toTeamsAttachments.test.ts +0 -74
  303. package/src/teams/toTeamsAttachments.ts +0 -133
  304. package/src/teams/types.ts +0 -266
  305. package/src/temporal.test.ts +0 -337
  306. package/src/temporal.ts +0 -263
  307. package/src/types.ts +0 -119
  308. package/src/vocabulary/alert.test.tsx +0 -427
  309. package/src/vocabulary/alert.tsx +0 -217
  310. package/src/vocabulary/collectFormValues.test.ts +0 -389
  311. package/src/vocabulary/collectFormValues.ts +0 -116
  312. package/src/vocabulary/data.test.tsx +0 -858
  313. package/src/vocabulary/data.tsx +0 -819
  314. package/src/vocabulary/datepicker.dom.test.tsx +0 -1457
  315. package/src/vocabulary/dispatch.ts +0 -83
  316. package/src/vocabulary/fact.test.tsx +0 -94
  317. package/src/vocabulary/fact.tsx +0 -54
  318. package/src/vocabulary/form.test.tsx +0 -113
  319. package/src/vocabulary/form.tsx +0 -39
  320. package/src/vocabulary/formatValue.test.ts +0 -72
  321. package/src/vocabulary/formatValue.ts +0 -127
  322. package/src/vocabulary/icon.test.tsx +0 -64
  323. package/src/vocabulary/icon.tsx +0 -174
  324. package/src/vocabulary/index.test.ts +0 -65
  325. package/src/vocabulary/index.ts +0 -33
  326. package/src/vocabulary/interactive.dispatch.test.tsx +0 -454
  327. package/src/vocabulary/interactive.dom.test.tsx +0 -1747
  328. package/src/vocabulary/interactive.test.tsx +0 -377
  329. package/src/vocabulary/interactive.tsx +0 -1109
  330. package/src/vocabulary/layout.test.tsx +0 -323
  331. package/src/vocabulary/layout.tsx +0 -247
  332. package/src/vocabulary/list.test.tsx +0 -222
  333. package/src/vocabulary/list.tsx +0 -50
  334. package/src/vocabulary/media.test.tsx +0 -85
  335. package/src/vocabulary/media.tsx +0 -56
  336. package/src/vocabulary/text.test.tsx +0 -102
  337. package/src/vocabulary/text.tsx +0 -60
  338. package/src/vocabulary/toTextContent.ts +0 -2
@@ -1,789 +0,0 @@
1
- import { copyBounded } from "../convert/copyBounded";
2
- import type { Action, UIElement } from "../ir";
3
- import { formatTemporalUnixSeconds } from "../temporal";
4
- import {
5
- ACTIONS_ELEMENT_CAP,
6
- CARD_ACTIONS_CAP,
7
- CAROUSEL_CARD_CAP,
8
- CHECKBOX_OPTION_CAP,
9
- CONTEXT_ELEMENT_CAP,
10
- DATA_TABLE_COLUMN_CAP,
11
- DATA_TABLE_ROW_CAP,
12
- FACT_FIELD_CAP,
13
- INBOUND_BLOCK_CAP,
14
- RADIO_OPTION_CAP,
15
- SELECT_OPTION_CAP,
16
- } from "./constants";
17
- import type { FromSlackBlocksResult, SlackConversionWarning } from "./types";
18
-
19
- const isRecord = (value: unknown): value is Record<string, unknown> =>
20
- typeof value === "object" && value !== null && !Array.isArray(value);
21
-
22
- const isDefined = <T>(value: T | undefined): value is T => value !== undefined;
23
-
24
- const asString = (value: unknown): string =>
25
- typeof value === "string" ? value : "";
26
-
27
- const textOf = (value: unknown): string =>
28
- isRecord(value) && typeof value["text"] === "string" ? value["text"] : "";
29
-
30
- const warn = (
31
- warnings: SlackConversionWarning[],
32
- code: SlackConversionWarning["code"],
33
- component: string,
34
- detail: string,
35
- ) => {
36
- warnings.push({ code, component, detail });
37
- };
38
-
39
- /**
40
- * The shared bounded-iteration primitive: copies `value` to at most `cap`
41
- * entries without ever reading past that many indices, so a hostile array
42
- * (sparse or proxied with a fabricated `length`) cannot stall the event loop.
43
- */
44
- const clampArray = (
45
- value: unknown,
46
- cap: number,
47
- ): { readonly items: unknown[]; readonly truncated: boolean } => {
48
- if (!Array.isArray(value)) return { items: [], truncated: false };
49
- return copyBounded(value, cap);
50
- };
51
-
52
- const boundedArray = (
53
- value: unknown,
54
- cap: number,
55
- warnings: SlackConversionWarning[],
56
- component: string,
57
- detail: string,
58
- ): unknown[] => {
59
- const { items, truncated } = clampArray(value, cap);
60
- if (truncated) warn(warnings, "clamped", component, detail);
61
- return items;
62
- };
63
-
64
- const parseValuePayload = (value: unknown): Record<string, unknown> => {
65
- if (typeof value !== "string") return {};
66
- try {
67
- const parsed: unknown = JSON.parse(value);
68
- return isRecord(parsed) ? parsed : {};
69
- } catch {
70
- return {};
71
- }
72
- };
73
-
74
- const decodeAction = (actionId: unknown, value: unknown): Action => ({
75
- ...parseValuePayload(value),
76
- type: typeof actionId === "string" ? actionId : "action",
77
- });
78
-
79
- const headerFrom = (block: Record<string, unknown>): UIElement => ({
80
- $type: "Header",
81
- text: textOf(block["text"]),
82
- });
83
-
84
- const imageFrom = (block: Record<string, unknown>): UIElement => ({
85
- $type: "Image",
86
- src: asString(block["image_url"]),
87
- alt: asString(block["alt_text"]),
88
- });
89
-
90
- const markdownFrom = (block: Record<string, unknown>): UIElement => ({
91
- $type: "Markdown",
92
- value: asString(block["text"]),
93
- });
94
-
95
- const FACT_FIELD_PATTERN = /^\*([\s\S]*?)\*\n([\s\S]*)$/;
96
-
97
- const factFrom = (
98
- field: unknown,
99
- warnings: SlackConversionWarning[],
100
- ): UIElement => {
101
- const text = textOf(field);
102
- const match = FACT_FIELD_PATTERN.exec(text);
103
- if (match) {
104
- const [, label, value] = match;
105
- return { $type: "Fact", label, value };
106
- }
107
- warn(
108
- warnings,
109
- "fallback",
110
- "Fact",
111
- "A section field that did not match the label/value format was kept as a value-only fact.",
112
- );
113
- return { $type: "Fact", label: "", value: text };
114
- };
115
-
116
- const listViewItemFrom = (
117
- text: string,
118
- button: Record<string, unknown>,
119
- ): UIElement => ({
120
- $type: "ListViewItem",
121
- $action: decodeAction(button["action_id"], button["value"]),
122
- children: { $type: "Text", value: text },
123
- });
124
-
125
- /**
126
- * Recognizes a section block that stands for a `ListViewItem`: mrkdwn text
127
- * paired with a button accessory, and no `fields` (a fields section is
128
- * always a Facts section, never a list row).
129
- */
130
- const listViewItemFromSection = (block: unknown): UIElement | undefined => {
131
- if (!isRecord(block) || block["type"] !== "section") return undefined;
132
- if (Array.isArray(block["fields"])) return undefined;
133
- const accessory = block["accessory"];
134
- if (!isRecord(accessory) || accessory["type"] !== "button") {
135
- return undefined;
136
- }
137
- return listViewItemFrom(textOf(block["text"]), accessory);
138
- };
139
-
140
- const isDividerBlock = (block: unknown): boolean =>
141
- isRecord(block) && block["type"] === "divider";
142
-
143
- const sectionFrom = (
144
- block: Record<string, unknown>,
145
- warnings: SlackConversionWarning[],
146
- ): UIElement[] => {
147
- const fields = block["fields"];
148
- if (Array.isArray(fields)) {
149
- return boundedArray(
150
- fields,
151
- FACT_FIELD_CAP,
152
- warnings,
153
- "Fact",
154
- `fields were clamped to ${FACT_FIELD_CAP} entries.`,
155
- ).map((field) => factFrom(field, warnings));
156
- }
157
- return [{ $type: "Text", value: textOf(block["text"]) }];
158
- };
159
-
160
- const captionFrom = (element: unknown): UIElement => ({
161
- $type: "Caption",
162
- value: textOf(element),
163
- });
164
-
165
- const contextFrom = (
166
- block: Record<string, unknown>,
167
- warnings: SlackConversionWarning[],
168
- ): UIElement => {
169
- const elements = boundedArray(
170
- block["elements"],
171
- CONTEXT_ELEMENT_CAP,
172
- warnings,
173
- "Row",
174
- `elements were clamped to ${CONTEXT_ELEMENT_CAP} entries.`,
175
- );
176
- if (elements.length > 1) {
177
- return { $type: "Row", children: elements.map(captionFrom) };
178
- }
179
- return {
180
- $type: "Caption",
181
- value: elements.length === 1 ? textOf(elements[0]) : "",
182
- };
183
- };
184
-
185
- const optionFrom = (
186
- option: unknown,
187
- ): { label: string; value: string } | undefined => {
188
- if (!isRecord(option) || typeof option["value"] !== "string") {
189
- return undefined;
190
- }
191
- return { label: textOf(option["text"]), value: option["value"] };
192
- };
193
-
194
- const buttonFrom = (element: Record<string, unknown>): UIElement => {
195
- const style = element["style"];
196
- return {
197
- $type: "Button",
198
- label: textOf(element["text"]),
199
- ...(style === "primary" || style === "danger"
200
- ? { buttonStyle: style }
201
- : {}),
202
- $action: decodeAction(element["action_id"], element["value"]),
203
- };
204
- };
205
-
206
- const selectFrom = (
207
- element: Record<string, unknown>,
208
- warnings: SlackConversionWarning[],
209
- ): UIElement => {
210
- const options = boundedArray(
211
- element["options"],
212
- SELECT_OPTION_CAP,
213
- warnings,
214
- "Select",
215
- `options were clamped to ${SELECT_OPTION_CAP} entries.`,
216
- );
217
- const convertedOptions = options.map(optionFrom).filter(isDefined);
218
- const initialOption = element["initial_option"];
219
- const defaultValue =
220
- isRecord(initialOption) && typeof initialOption["value"] === "string"
221
- ? initialOption["value"]
222
- : undefined;
223
- const placeholder = element["placeholder"];
224
- return {
225
- $type: "Select",
226
- options: convertedOptions,
227
- ...(defaultValue !== undefined &&
228
- convertedOptions.some((option) => option.value === defaultValue)
229
- ? { defaultValue }
230
- : {}),
231
- ...(isRecord(placeholder) ? { placeholder: textOf(placeholder) } : {}),
232
- $action: decodeAction(element["action_id"], element["value"]),
233
- };
234
- };
235
-
236
- const datePickerFrom = (element: Record<string, unknown>): UIElement => ({
237
- $type: "DatePicker",
238
- ...(typeof element["initial_date"] === "string"
239
- ? { value: element["initial_date"] }
240
- : {}),
241
- $action: decodeAction(element["action_id"], element["value"]),
242
- });
243
-
244
- const checkboxFrom = (element: Record<string, unknown>): UIElement => {
245
- const options = Array.isArray(element["options"]) ? element["options"] : [];
246
- const first = options[0];
247
- const initialOptions = Array.isArray(element["initial_options"])
248
- ? element["initial_options"]
249
- : [];
250
- const firstValue =
251
- isRecord(first) && typeof first["value"] === "string" ? first["value"] : "";
252
- const firstChecked = copyBounded(
253
- initialOptions,
254
- SELECT_OPTION_CAP,
255
- ).items.some((option) => isRecord(option) && option["value"] === firstValue);
256
- return {
257
- $type: "Checkbox",
258
- label: isRecord(first) ? textOf(first["text"]) : "",
259
- name: firstValue,
260
- ...(firstChecked ? { defaultChecked: true } : {}),
261
- $action: decodeAction(element["action_id"], element["value"]),
262
- };
263
- };
264
-
265
- const checkboxGroupFrom = (
266
- element: Record<string, unknown>,
267
- warnings: SlackConversionWarning[],
268
- ): UIElement => {
269
- const options = boundedArray(
270
- element["options"],
271
- CHECKBOX_OPTION_CAP,
272
- warnings,
273
- "CheckboxGroup",
274
- `options were clamped to ${CHECKBOX_OPTION_CAP} entries.`,
275
- )
276
- .map(optionFrom)
277
- .filter(isDefined);
278
- const defaultValue = clampArray(
279
- element["initial_options"],
280
- CHECKBOX_OPTION_CAP,
281
- ).items.flatMap((initialOption) => {
282
- const value =
283
- isRecord(initialOption) && typeof initialOption["value"] === "string"
284
- ? initialOption["value"]
285
- : undefined;
286
- return value !== undefined &&
287
- options.some((option) => option.value === value)
288
- ? [value]
289
- : [];
290
- });
291
- return {
292
- $type: "CheckboxGroup",
293
- options,
294
- ...(defaultValue.length > 0 ? { defaultValue } : {}),
295
- $action: decodeAction(element["action_id"], element["value"]),
296
- };
297
- };
298
-
299
- const radioGroupFrom = (
300
- element: Record<string, unknown>,
301
- warnings: SlackConversionWarning[],
302
- ): UIElement => {
303
- const options = boundedArray(
304
- element["options"],
305
- RADIO_OPTION_CAP,
306
- warnings,
307
- "RadioGroup",
308
- `options were clamped to ${RADIO_OPTION_CAP} entries.`,
309
- );
310
- const initialOption = element["initial_option"];
311
- const initialValue =
312
- isRecord(initialOption) && typeof initialOption["value"] === "string"
313
- ? initialOption["value"]
314
- : undefined;
315
- return {
316
- $type: "RadioGroup",
317
- options: options.map(optionFrom).filter(isDefined),
318
- ...(initialValue !== undefined ? { defaultValue: initialValue } : {}),
319
- $action: decodeAction(element["action_id"], element["value"]),
320
- };
321
- };
322
-
323
- const actionElementFrom = (
324
- element: unknown,
325
- warnings: SlackConversionWarning[],
326
- ): UIElement | undefined => {
327
- if (isRecord(element)) {
328
- switch (element["type"]) {
329
- case "button":
330
- return buttonFrom(element);
331
- case "static_select":
332
- return selectFrom(element, warnings);
333
- case "datepicker":
334
- return datePickerFrom(element);
335
- case "timepicker":
336
- return {
337
- $type: "DatePicker",
338
- inputType: "time",
339
- ...(typeof element["initial_time"] === "string"
340
- ? { value: element["initial_time"] }
341
- : {}),
342
- $action: decodeAction(element["action_id"], element["value"]),
343
- };
344
- case "datetimepicker": {
345
- const value =
346
- typeof element["initial_date_time"] === "number"
347
- ? formatTemporalUnixSeconds(element["initial_date_time"])
348
- : undefined;
349
- return {
350
- $type: "DatePicker",
351
- inputType: "datetime",
352
- ...(value !== undefined ? { value } : {}),
353
- $action: decodeAction(element["action_id"], element["value"]),
354
- };
355
- }
356
- case "checkboxes":
357
- return Array.isArray(element["options"]) &&
358
- element["options"].length > 1
359
- ? checkboxGroupFrom(element, warnings)
360
- : checkboxFrom(element);
361
- case "radio_buttons":
362
- return radioGroupFrom(element, warnings);
363
- }
364
- }
365
- warn(
366
- warnings,
367
- "dropped",
368
- isRecord(element) && typeof element["type"] === "string"
369
- ? element["type"]
370
- : "unknown",
371
- "Unknown action element type was dropped.",
372
- );
373
- return undefined;
374
- };
375
-
376
- const actionsFrom = (
377
- block: Record<string, unknown>,
378
- warnings: SlackConversionWarning[],
379
- ): UIElement[] => {
380
- const elements = boundedArray(
381
- block["elements"],
382
- ACTIONS_ELEMENT_CAP,
383
- warnings,
384
- "Actions",
385
- `elements were clamped to ${ACTIONS_ELEMENT_CAP} entries.`,
386
- );
387
- return elements
388
- .map((element) => actionElementFrom(element, warnings))
389
- .filter(isDefined);
390
- };
391
-
392
- const inputFrom = (
393
- block: Record<string, unknown>,
394
- warnings: SlackConversionWarning[],
395
- ): UIElement[] => {
396
- const element = block["element"];
397
- if (
398
- isRecord(element) &&
399
- (element["type"] === "timepicker" || element["type"] === "datetimepicker")
400
- ) {
401
- const picker = actionElementFrom(element, warnings);
402
- return picker ? [{ ...picker, label: textOf(block["label"]) }] : [];
403
- }
404
- if (!isRecord(element) || element["type"] !== "plain_text_input") {
405
- warn(
406
- warnings,
407
- "dropped",
408
- "Input",
409
- "Unknown input element type was dropped.",
410
- );
411
- return [];
412
- }
413
- const placeholder = element["placeholder"];
414
- return [
415
- {
416
- $type: "Input",
417
- label: textOf(block["label"]),
418
- ...(isRecord(placeholder) ? { placeholder: textOf(placeholder) } : {}),
419
- ...(element["multiline"] === true ? { multiline: true } : {}),
420
- ...(typeof element["initial_value"] === "string"
421
- ? { defaultValue: element["initial_value"] }
422
- : {}),
423
- $action: decodeAction(element["action_id"], undefined),
424
- },
425
- ];
426
- };
427
-
428
- type CardFooterButton = { readonly label: string; readonly $action: Action };
429
-
430
- const cardFooterButtonFrom = (button: unknown): CardFooterButton | undefined =>
431
- isRecord(button)
432
- ? {
433
- label: textOf(button["text"]),
434
- $action: decodeAction(button["action_id"], button["value"]),
435
- }
436
- : undefined;
437
-
438
- /**
439
- * Assigns a card's footer actions by style rather than position: the
440
- * primary-styled button is the confirm action and a non-primary button is
441
- * cancel, since that is the pairing {@link toSlackBlocks} itself emits.
442
- * Position is only a fallback for input that does not carry that pairing
443
- * (zero or multiple primary-styled buttons among two or more actions).
444
- */
445
- const assignCardFooterActions = (
446
- actions: unknown[],
447
- warnings: SlackConversionWarning[],
448
- ): {
449
- readonly confirm: CardFooterButton | undefined;
450
- readonly cancel: CardFooterButton | undefined;
451
- readonly leftover: unknown[];
452
- } => {
453
- const isPrimaryButton = (action: unknown): boolean =>
454
- isRecord(action) && action["style"] === "primary";
455
- const primaryButtons = actions.filter(isPrimaryButton);
456
-
457
- let confirmSource: unknown = undefined;
458
- let cancelSource: unknown = undefined;
459
- let leftover: unknown[] = [];
460
-
461
- if (primaryButtons.length === 1) {
462
- const [confirmButton] = primaryButtons;
463
- const [cancelButton, ...rest] = actions.filter(
464
- (action) => !isPrimaryButton(action),
465
- );
466
- confirmSource = confirmButton;
467
- cancelSource = cancelButton;
468
- leftover = rest;
469
- } else if (primaryButtons.length === 0 && actions.length <= 1) {
470
- const [onlyButton] = actions;
471
- cancelSource = onlyButton;
472
- } else if (actions.length > 0) {
473
- const [first, second, ...rest] = actions;
474
- confirmSource = first;
475
- cancelSource = second;
476
- leftover = rest;
477
- warn(
478
- warnings,
479
- "fallback",
480
- "Card",
481
- "Confirm and cancel buttons could not be assigned by style and fell back to position.",
482
- );
483
- }
484
-
485
- return {
486
- confirm: cardFooterButtonFrom(confirmSource),
487
- cancel: cardFooterButtonFrom(cancelSource),
488
- leftover,
489
- };
490
- };
491
-
492
- const cardFrom = (
493
- block: Record<string, unknown>,
494
- warnings: SlackConversionWarning[],
495
- ): UIElement => {
496
- const titleText = textOf(block["title"]);
497
- const children: UIElement[] = [];
498
- const heroImage = block["hero_image"];
499
- if (isRecord(heroImage)) {
500
- children.push({
501
- $type: "Image",
502
- src: asString(heroImage["image_url"]),
503
- alt: asString(heroImage["alt_text"]),
504
- });
505
- }
506
- const body = block["body"];
507
- if (isRecord(body)) {
508
- children.push({ $type: "Text", value: textOf(body) });
509
- }
510
- const subtext = block["subtext"];
511
- if (isRecord(subtext)) {
512
- children.push({ $type: "Caption", value: textOf(subtext) });
513
- }
514
- const actions = boundedArray(
515
- block["actions"],
516
- CARD_ACTIONS_CAP,
517
- warnings,
518
- "Card",
519
- `actions were clamped to ${CARD_ACTIONS_CAP} entries.`,
520
- );
521
- const { confirm, cancel, leftover } = assignCardFooterActions(
522
- actions,
523
- warnings,
524
- );
525
- if (leftover.length > 0) {
526
- for (const extra of leftover) {
527
- if (isRecord(extra)) children.push(buttonFrom(extra));
528
- }
529
- warn(
530
- warnings,
531
- "fallback",
532
- "Card",
533
- "Card actions beyond confirm and cancel were appended as Button children.",
534
- );
535
- }
536
- return {
537
- $type: "Card",
538
- ...(titleText ? { title: titleText } : {}),
539
- ...(children.length > 0 ? { children } : {}),
540
- ...(confirm !== undefined ? { confirm } : {}),
541
- ...(cancel !== undefined ? { cancel } : {}),
542
- };
543
- };
544
-
545
- const carouselFrom = (
546
- block: Record<string, unknown>,
547
- warnings: SlackConversionWarning[],
548
- ): UIElement => {
549
- const elements = boundedArray(
550
- block["elements"],
551
- CAROUSEL_CARD_CAP,
552
- warnings,
553
- "Carousel",
554
- `cards were clamped to ${CAROUSEL_CARD_CAP} entries.`,
555
- );
556
- const cards = elements
557
- .filter(isRecord)
558
- .map((card) => cardFrom(card, warnings));
559
- return { $type: "Carousel", children: cards };
560
- };
561
-
562
- const ALERT_LEVEL_TONES: Record<string, string> = {
563
- error: "danger",
564
- success: "success",
565
- warning: "warning",
566
- info: "info",
567
- };
568
-
569
- const alertFrom = (block: Record<string, unknown>): UIElement => {
570
- const level = block["level"];
571
- const tone = typeof level === "string" ? ALERT_LEVEL_TONES[level] : undefined;
572
- return {
573
- $type: "Alert",
574
- description: textOf(block["text"]),
575
- ...(tone !== undefined ? { tone } : {}),
576
- };
577
- };
578
-
579
- const dataTableCellValue = (cell: unknown): string | number => {
580
- if (!isRecord(cell)) return "";
581
- const text = asString(cell["text"]);
582
- if (cell["type"] === "raw_number") {
583
- const parsed = Number(text);
584
- if (Number.isFinite(parsed)) return parsed;
585
- }
586
- return text;
587
- };
588
-
589
- const dataTableFrom = (
590
- block: Record<string, unknown>,
591
- warnings: SlackConversionWarning[],
592
- ): UIElement => {
593
- const { items: rows, truncated: rowsTruncated } = clampArray(
594
- block["rows"],
595
- DATA_TABLE_ROW_CAP + 1,
596
- );
597
- if (rowsTruncated) {
598
- warn(
599
- warnings,
600
- "clamped",
601
- "Table",
602
- `rows were clamped to ${DATA_TABLE_ROW_CAP} entries.`,
603
- );
604
- }
605
- const [headerRow, ...dataRows] = rows;
606
- const { items: headerCells, truncated: headerTruncated } = clampArray(
607
- headerRow,
608
- DATA_TABLE_COLUMN_CAP,
609
- );
610
- let columnsTruncated = headerTruncated;
611
- const normalizedRows = dataRows.map((row) => {
612
- const { items: cells, truncated } = clampArray(row, DATA_TABLE_COLUMN_CAP);
613
- if (truncated) columnsTruncated = true;
614
- return cells;
615
- });
616
- if (columnsTruncated) {
617
- warn(
618
- warnings,
619
- "clamped",
620
- "Table",
621
- `columns were clamped to ${DATA_TABLE_COLUMN_CAP} entries.`,
622
- );
623
- }
624
- const headerAllEmpty = headerCells.every(
625
- (cell) => !isRecord(cell) || asString(cell["text"]) === "",
626
- );
627
- const columns = headerCells.map((cell) => ({
628
- label: isRecord(cell) ? asString(cell["text"]) : "",
629
- }));
630
- return {
631
- $type: "Table",
632
- ...(headerAllEmpty ? {} : { columns }),
633
- rows: normalizedRows.map((row) => row.map(dataTableCellValue)),
634
- };
635
- };
636
-
637
- function convertBlock(
638
- block: unknown,
639
- warnings: SlackConversionWarning[],
640
- ): UIElement[] {
641
- if (isRecord(block)) {
642
- switch (block["type"]) {
643
- case "header":
644
- return [headerFrom(block)];
645
- case "section":
646
- return sectionFrom(block, warnings);
647
- case "context":
648
- return [contextFrom(block, warnings)];
649
- case "image":
650
- return [imageFrom(block)];
651
- case "divider":
652
- return [{ $type: "Divider" }];
653
- case "actions":
654
- return actionsFrom(block, warnings);
655
- case "input":
656
- return inputFrom(block, warnings);
657
- case "markdown":
658
- return [markdownFrom(block)];
659
- case "card":
660
- return [cardFrom(block, warnings)];
661
- case "carousel":
662
- return [carouselFrom(block, warnings)];
663
- case "alert":
664
- return [alertFrom(block)];
665
- case "data_table":
666
- return [dataTableFrom(block, warnings)];
667
- }
668
- }
669
- warn(
670
- warnings,
671
- "dropped",
672
- isRecord(block) && typeof block["type"] === "string"
673
- ? block["type"]
674
- : "unknown",
675
- "Unknown block type was dropped.",
676
- );
677
- return [];
678
- }
679
-
680
- const extractBlockList = (input: unknown): unknown[] | undefined => {
681
- if (Array.isArray(input)) return input;
682
- if (isRecord(input) && Array.isArray(input["blocks"])) return input["blocks"];
683
- return undefined;
684
- };
685
-
686
- /**
687
- * Walks the (already bounded) block list, folding a run of one or more
688
- * consecutive `ListViewItem`-shaped section blocks, optionally separated by
689
- * single divider blocks, into one `ListView` container instead of emitting
690
- * root-level `ListViewItem` nodes or the interleaved dividers. A divider that
691
- * is not directly between two such items is left for {@link convertBlock} to
692
- * decode as a literal `Divider`.
693
- */
694
- function convertBlockList(
695
- blocks: unknown[],
696
- warnings: SlackConversionWarning[],
697
- ): UIElement[] {
698
- const nodes: UIElement[] = [];
699
- let index = 0;
700
- while (index < blocks.length) {
701
- const firstItem = listViewItemFromSection(blocks[index]);
702
- if (firstItem === undefined) {
703
- nodes.push(...convertBlock(blocks[index], warnings));
704
- index += 1;
705
- continue;
706
- }
707
- const items: UIElement[] = [firstItem];
708
- let cursor = index + 1;
709
- while (true) {
710
- if (isDividerBlock(blocks[cursor])) {
711
- const candidate = listViewItemFromSection(blocks[cursor + 1]);
712
- if (candidate === undefined) break;
713
- items.push(candidate);
714
- cursor += 2;
715
- continue;
716
- }
717
- const candidate = listViewItemFromSection(blocks[cursor]);
718
- if (candidate === undefined) break;
719
- items.push(candidate);
720
- cursor += 1;
721
- }
722
- nodes.push({ $type: "ListView", children: items });
723
- index = cursor;
724
- }
725
- return nodes;
726
- }
727
-
728
- /**
729
- * Converts Slack Block Kit JSON back into a generative-ui IR tree, inverting
730
- * {@link toSlackBlocks}. Accepts a blocks array or a `{ blocks }` wrapper;
731
- * malformed input resolves to an empty node list plus a warning, and an
732
- * unrecognized block or action element is skipped with a "dropped" warning.
733
- * Every array read along the way (blocks, fields, options, elements, cards,
734
- * rows, cells) is bounded to its site's cap before it is mapped over, so a
735
- * hostile array (sparse or proxied with a fabricated `length`) is clamped
736
- * with a "clamped" warning instead of stalling the event loop. Never throws.
737
- *
738
- * The Slack shape is lossier than the IR, so some information cannot be
739
- * recovered: a context block's elements always decode as `Caption` (the
740
- * original `Badge` vs `Caption` distinction does not survive the round trip),
741
- * a button's `buttonStyle` is only recovered for the `primary`/`danger` Slack
742
- * styles, a native alert block's `title` and `description` merge into a
743
- * single `description` string, and a card's layout collapses to its `title`,
744
- * a flat `children` list (image, then text, then caption, in that order),
745
- * and `confirm`/`cancel` footer buttons. A section field's label and value
746
- * are split on the first `*`+newline delimiter, so a label or value that
747
- * itself contains that exact delimiter cannot be split back unambiguously.
748
- * A data-table cell that Slack carries as `raw_text` always decodes as a
749
- * string, even when its text is the stringified form of a boolean, since the
750
- * Slack shape does not distinguish a boolean from ordinary text.
751
- */
752
- export function fromSlackBlocks(blocks: unknown): FromSlackBlocksResult {
753
- const warnings: SlackConversionWarning[] = [];
754
- try {
755
- const list = extractBlockList(blocks);
756
- if (list === undefined) {
757
- return {
758
- nodes: [],
759
- warnings: [
760
- {
761
- code: "dropped",
762
- component: "Root",
763
- detail: "Input could not be converted.",
764
- },
765
- ],
766
- };
767
- }
768
- const boundedList = boundedArray(
769
- list,
770
- INBOUND_BLOCK_CAP,
771
- warnings,
772
- "Root",
773
- `blocks were clamped to ${INBOUND_BLOCK_CAP} entries.`,
774
- );
775
- const nodes = convertBlockList(boundedList, warnings);
776
- return { nodes, warnings };
777
- } catch {
778
- return {
779
- nodes: [],
780
- warnings: [
781
- {
782
- code: "dropped",
783
- component: "Root",
784
- detail: "Input could not be converted.",
785
- },
786
- ],
787
- };
788
- }
789
- }