@kyyinfinite/lumina 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/package.json +1 -1
  2. package/src/builders/ai-rich.js +0 -165
  3. package/src/builders/base.js +4 -51
  4. package/src/builders/button-v2.js +13 -78
  5. package/src/builders/button.js +20 -234
  6. package/src/builders/card.js +9 -76
  7. package/src/builders/carousel.js +4 -61
  8. package/src/builders/index.js +1 -7
  9. package/src/builders/sticker.js +102 -0
  10. package/src/client/bot.js +28 -153
  11. package/src/client/connection.js +4 -111
  12. package/src/errors.js +0 -37
  13. package/src/index.d.ts +1 -28
  14. package/src/index.js +23 -121
  15. package/src/media/fetch.js +2 -33
  16. package/src/media/image.js +1 -41
  17. package/src/media/resolver.js +3 -55
  18. package/src/media/sticker.js +124 -0
  19. package/src/media/uploader.js +0 -30
  20. package/src/media/video.js +1 -39
  21. package/src/parsers/code-tokenizer-keywords.js +0 -12
  22. package/src/parsers/code-tokenizer.js +0 -42
  23. package/src/parsers/index.js +0 -7
  24. package/src/parsers/inline-entity.js +8 -117
  25. package/src/parsers/table-metadata.js +1 -35
  26. package/src/proto/enums.js +9 -65
  27. package/src/proto/layouts.js +3 -64
  28. package/src/proto/primitives.js +4 -91
  29. package/src/proto/relay-nodes.js +1 -32
  30. package/src/proto/rich-response.js +6 -57
  31. package/src/proto/updater.js +0 -85
  32. package/src/services/index.js +0 -7
  33. package/src/services/media-service.js +1 -102
  34. package/src/services/message-service.js +16 -158
  35. package/src/services/proto-service.js +3 -57
  36. package/src/utils/id.js +0 -25
  37. package/src/utils/logger.js +2 -39
  38. package/src/utils/mime.js +17 -73
  39. package/src/utils/promise.js +0 -26
  40. package/src/utils/validator.js +6 -71
@@ -1,31 +1,11 @@
1
- /**
2
- * @file code-tokenizer.js
3
- * @module lumina/parsers/code-tokenizer
4
1
  *
5
- * Lightweight hand-rolled lexer for code-block syntax highlighting inside
6
- * AI Rich Responses. Returns two parallel representations:
7
2
  *
8
- * - `codeBlock` — array of `{ codeContent, highlightType }` where
9
- * `highlightType` is the numeric 0-5 enum used by WAProto.
10
- * - `unifiedBlocks` — array of `{ content, type }` with the string label
11
- * (`'DEFAULT' | 'KEYWORD' | 'METHOD' | 'STR' | 'NUMBER' | 'COMMENT'`).
12
3
  *
13
- * The legacy `AIRich.tokenizer` rebuilt its keyword Set on every call and
14
- * inlined all 660 lines of language maps. Lumina splits the catalog into
15
- * `code-tokenizer-keywords.js` (built once) and keeps this file focused on
16
- * the lexer itself (~120 lines).
17
- */
18
4
 
19
5
  import { HighlightType, HighlightLabel } from '../proto/enums.js'
20
6
  import { KEYWORDS, SLASH_COMMENT_LANGS, HASH_COMMENT_LANGS, BLOCK_COMMENT_LANGS } from './code-tokenizer-keywords.js'
21
7
 
22
- /**
23
- * @typedef {object} TokenizedCode
24
- * @property {Array<{ codeContent: string, highlightType: number }>} codeBlock
25
- * @property {Array<{ content: string, type: string }>} unifiedBlocks
26
- */
27
8
 
28
- /** Identifier character class per language (CSS & HTML allow `-` / `:`). */
29
9
  function identifierChar(lang) {
30
10
  switch (lang) {
31
11
  case 'css':
@@ -37,19 +17,12 @@ function identifierChar(lang) {
37
17
  }
38
18
  }
39
19
 
40
- /**
41
- * Tokenize a code string for syntax highlighting.
42
20
  *
43
- * @param {string} code
44
- * @param {string} [lang='javascript'] Lower-case language id. Unknown ids degrade gracefully to plain text.
45
- * @returns {TokenizedCode}
46
- */
47
21
  export function tokenizeCode(code, lang = 'javascript') {
48
22
  if (typeof code !== 'string' || code.length === 0) {
49
23
  return { codeBlock: [], unifiedBlocks: [] }
50
24
  }
51
25
 
52
- // Plain-text fast-path.
53
26
  if (!lang || lang === 'txt' || lang === 'text' || lang === 'plaintext') {
54
27
  return {
55
28
  codeBlock: [{ codeContent: code, highlightType: HighlightType.DEFAULT }],
@@ -64,12 +37,8 @@ export function tokenizeCode(code, lang = 'javascript') {
64
37
  const supportsHashComments = HASH_COMMENT_LANGS.has(lower)
65
38
  const supportsBlockComments = BLOCK_COMMENT_LANGS.has(lower)
66
39
 
67
- /** @type {Array<{ codeContent: string, highlightType: number }>} */
68
40
  const tokens = []
69
41
 
70
- /**
71
- * Push a token, merging with the previous one if both share the same type.
72
- */
73
42
  const push = (content, type) => {
74
43
  if (!content) return
75
44
  const last = tokens[tokens.length - 1]
@@ -81,7 +50,6 @@ export function tokenizeCode(code, lang = 'javascript') {
81
50
  while (i < code.length) {
82
51
  const c = code[i]
83
52
 
84
- // Whitespace.
85
53
  if (/\s/.test(c)) {
86
54
  const start = i
87
55
  while (i < code.length && /\s/.test(code[i])) i++
@@ -89,7 +57,6 @@ export function tokenizeCode(code, lang = 'javascript') {
89
57
  continue
90
58
  }
91
59
 
92
- // Block comment: /* ... */
93
60
  if (supportsBlockComments && c === '/' && code[i + 1] === '*') {
94
61
  const start = i
95
62
  i += 2
@@ -99,7 +66,6 @@ export function tokenizeCode(code, lang = 'javascript') {
99
66
  continue
100
67
  }
101
68
 
102
- // Slash line comment: //
103
69
  if (supportsSlashComments && c === '/' && code[i + 1] === '/') {
104
70
  const start = i
105
71
  while (i < code.length && code[i] !== '\n') i++
@@ -107,7 +73,6 @@ export function tokenizeCode(code, lang = 'javascript') {
107
73
  continue
108
74
  }
109
75
 
110
- // Hash line comment: # (python, bash, php, rust)
111
76
  if (supportsHashComments && c === '#') {
112
77
  const start = i
113
78
  while (i < code.length && code[i] !== '\n') i++
@@ -115,7 +80,6 @@ export function tokenizeCode(code, lang = 'javascript') {
115
80
  continue
116
81
  }
117
82
 
118
- // String literals: ', ", `
119
83
  if (c === '"' || c === "'" || c === '`') {
120
84
  const start = i
121
85
  const quote = c
@@ -131,7 +95,6 @@ export function tokenizeCode(code, lang = 'javascript') {
131
95
  continue
132
96
  }
133
97
 
134
- // Numbers.
135
98
  if (/[0-9]/.test(c)) {
136
99
  const start = i
137
100
  while (i < code.length && /[0-9._a-fxA-fX]/.test(code[i])) i++
@@ -139,7 +102,6 @@ export function tokenizeCode(code, lang = 'javascript') {
139
102
  continue
140
103
  }
141
104
 
142
- // Identifiers & keywords.
143
105
  if (/[a-zA-Z_$]/.test(c)) {
144
106
  const start = i
145
107
  while (i < code.length && isIdent.test(code[i])) i++
@@ -150,12 +112,10 @@ export function tokenizeCode(code, lang = 'javascript') {
150
112
  if (keywords.has(word)) {
151
113
  type = HighlightType.KEYWORD
152
114
  } else if (lower === 'css') {
153
- // Property name? next non-space char is ':'.
154
115
  let j = i
155
116
  while (j < code.length && /\s/.test(code[j])) j++
156
117
  if (code[j] === ':') type = HighlightType.KEYWORD
157
118
  } else if (lower === 'html') {
158
- // Tag name? previous non-space char is '<' or '</'.
159
119
  let p = start - 1
160
120
  while (p >= 0 && /\s/.test(code[p])) p--
161
121
  if (code[p] === '<' || (code[p] === '/' && code[p - 1] === '<')) {
@@ -163,7 +123,6 @@ export function tokenizeCode(code, lang = 'javascript') {
163
123
  }
164
124
  }
165
125
 
166
- // Function call? Next non-space char is '('.
167
126
  if (type === HighlightType.DEFAULT) {
168
127
  let j = i
169
128
  while (j < code.length && /\s/.test(code[j])) j++
@@ -174,7 +133,6 @@ export function tokenizeCode(code, lang = 'javascript') {
174
133
  continue
175
134
  }
176
135
 
177
- // Everything else: punctuation.
178
136
  push(c, HighlightType.DEFAULT)
179
137
  i++
180
138
  }
@@ -1,10 +1,3 @@
1
- /**
2
- * @file parsers/index.js
3
- * @module lumina/parsers
4
- *
5
- * Barrel re-export for the parsers layer.
6
- */
7
-
8
1
  export { extractInlineEntities } from './inline-entity.js'
9
2
  export { tokenizeCode } from './code-tokenizer.js'
10
3
  export { toTableMetadata } from './table-metadata.js'
@@ -1,42 +1,5 @@
1
- /**
2
- * @file inline-entity.js
3
- * @module lumina/parsers/inline-entity
4
- *
5
- * extractInlineEntities — parses three markdown-like inline syntaxes inside
6
- * AI-rich text content and returns the rewritten text plus proto-ready
7
- * inline-entity metadata.
8
- *
9
- * Supported syntaxes:
10
- *
11
- * [text](url) → hyperlink (untrusted if `url` starts with `!`)
12
- * [](url) → citation (renders as a numeric superscript)
13
- * [text]<url|w|h|fh|p> → latex (math image with dimensions)
14
- *
15
- * The rewritten text replaces each `[...]` span with `{{KEY}}...{{/KEY}}`
16
- * placeholders that WhatsApp's rich-response renderer substitutes back with
17
- * the entity's interactive UI element.
18
- *
19
- * This is the modernised, de-obfuscated successor to the legacy `extractIE`
20
- * function in `_build-m.js`. Key changes:
21
- *
22
- * 1. The `NIXEL_` prefix (obfuscated as `\u004E\u0049\u0058\u0045\u004C_`)
23
- * is replaced by the configurable `LUMINA_` prefix.
24
- * 2. The off-by-one bug on citation keys is fixed: every counter is now
25
- * zero-based and keys match `reference_id` consistently.
26
- * 3. Magic numbers (font_height, padding, default width/height) are pulled
27
- * into named constants.
28
- * 4. The `createIE` switch-by-if is replaced by a typed lookup table.
29
- * 5. Pure function, no side-effects — easily unit-testable.
30
- */
31
-
32
1
  import { entityKey } from '../utils/id.js'
33
2
 
34
- /** @typedef {'hyperlink'|'citation'|'latex'} EntityType */
35
-
36
- /**
37
- * Default LaTeX rendering parameters. Sourced from the legacy implementation.
38
- * Adjust if WhatsApp updates its rendering defaults.
39
- */
40
3
  const LATEX_DEFAULTS = Object.freeze({
41
4
  width: 100,
42
5
  height: 100,
@@ -44,11 +7,6 @@ const LATEX_DEFAULTS = Object.freeze({
44
7
  padding: 15,
45
8
  })
46
9
 
47
- /**
48
- * Per-type metadata shaper. Replaces the legacy `createIE` switch.
49
- *
50
- * @type {Record<EntityType, (item: any) => object>}
51
- */
52
10
  const SHAPERS = {
53
11
  hyperlink: (item) => ({
54
12
  key: item.key,
@@ -86,76 +44,35 @@ const SHAPERS = {
86
44
  }),
87
45
  }
88
46
 
89
- /**
90
- * @typedef {object} ExtractOptions
91
- * @property {boolean} [extract=true] Master switch — when false, returns the text unchanged.
92
- * @property {boolean} [hyperlink=true] Parse `[text](url)` syntax.
93
- * @property {boolean} [citation=true] Parse `[](url)` syntax.
94
- * @property {boolean} [latex=true] Parse `[text]<url|w|h|fh|p>` syntax.
95
- * @property {string} [prefix='LUMINA'] Override the entity-key prefix.
96
- */
97
-
98
- /**
99
- * @typedef {object} ExtractResult
100
- * @property {string} text Rewritten text with `{{KEY}}…{{/KEY}}` placeholders.
101
- * @property {Array<{ type: EntityType, key: string, [k: string]: any }>} entities
102
- * Raw per-entity descriptors (one per matched span).
103
- * @property {Array<{ key: string, metadata: object }>} metadata
104
- * Proto-ready inline-entity metadata, ready to attach to a primitive.
105
- */
106
-
107
- /**
108
- * Parse inline entities from text.
109
- *
110
- * @param {string} text
111
- * @param {ExtractOptions} [opts]
112
- * @returns {ExtractResult}
113
- */
114
47
  export function extractInlineEntities(text, opts = {}) {
115
- const {
116
- extract = true,
117
- hyperlink = true,
118
- citation = true,
119
- latex = true,
120
- prefix = 'LUMINA',
121
- } = opts
48
+ const { extract = true, hyperlink = true, citation = true, latex = true } = opts
122
49
 
123
50
  if (!extract || typeof text !== 'string' || text.length === 0) {
124
51
  return { text: text ?? '', entities: [], metadata: [] }
125
52
  }
126
53
 
127
- /** @type {Array<{ type: EntityType, key: string, [k: string]: any }>} */
128
54
  const entities = []
129
- /** @type {Array<{ key: string, metadata: object }>} */
130
55
  const metadata = []
131
56
  let rewritten = ''
132
57
  let last = 0
133
58
  let citationCount = 0
134
59
  let hyperlinkCount = 0
135
60
  let latexCount = 0
136
-
137
- /** Bracket stack — supports nested brackets inside the link text. */
138
61
  const stack = []
139
62
 
140
63
  for (let i = 0; i < text.length; i++) {
141
64
  const ch = text[i]
142
65
 
143
- // Push `[` (escaped `\[` ignored).
144
- if (ch === '[' && text[i - 1] !== '\\') {
145
- stack.push(i)
146
- continue
147
- }
66
+ if (ch === '[' && text[i - 1] !== '\\') { stack.push(i); continue }
148
67
 
149
- // `]` only triggers when followed by `(` (link) or `<` (latex).
150
68
  if (ch === ']' && (text[i + 1] === '(' || text[i + 1] === '<')) {
151
69
  const start = stack.pop()
152
70
  if (start == null) continue
153
71
 
154
72
  const open = text[i + 1]
155
73
  const close = open === '(' ? ')' : '>'
156
- const type = /** @type {EntityType} */ (open === '(' ? 'hyperlink-or-citation' : 'latex')
74
+ const type = open === '(' ? 'hyperlink-or-citation' : 'latex'
157
75
 
158
- // Walk to the matching close, respecting backslash escapes & nesting.
159
76
  let end = i + 2
160
77
  let depth = 1
161
78
  while (end < text.length && depth) {
@@ -163,61 +80,35 @@ export function extractInlineEntities(text, opts = {}) {
163
80
  else if (text[end] === close && text[end - 1] !== '\\') depth--
164
81
  end++
165
82
  }
166
- if (depth) continue // unbalanced — skip.
83
+ if (depth) continue
167
84
 
168
85
  const raw = text.slice(start + 1, i).trim()
169
86
  const url = text.slice(i + 2, end - 1).trim()
170
-
171
- /** @type {{ type: EntityType, key: string, [k: string]: any } | null} */
172
87
  let entry = null
173
88
 
174
89
  if (type === 'latex') {
175
90
  if (!latex) continue
176
- const [txt = '', width = null, height = null, fontHeight = null, padding = null] =
177
- raw.split('|')
91
+ const [txt = '', width = null, height = null, fontHeight = null, padding = null] = raw.split('|')
178
92
  const key = entityKey('LATEX', latexCount++)
179
- entry = {
180
- type: 'latex',
181
- key,
182
- text: txt,
183
- url,
184
- width,
185
- height,
186
- fontHeight,
187
- padding,
188
- }
93
+ entry = { type: 'latex', key, text: txt, url, width, height, fontHeight, padding }
189
94
  rewritten += text.slice(last, start) + `{{${key}}}${txt || 'image'}{{/${key}}}`
190
95
  } else if (raw) {
191
96
  if (!hyperlink) continue
192
- // Untrusted link → URL prefixed with `!`.
193
97
  const trusted = !url.startsWith('!')
194
98
  const cleanUrl = trusted ? url : url.slice(1)
195
99
  const key = entityKey('HYPERLINK', hyperlinkCount++)
196
- entry = {
197
- type: 'hyperlink',
198
- key,
199
- text: raw,
200
- url: cleanUrl,
201
- isTrusted: trusted,
202
- }
100
+ entry = { type: 'hyperlink', key, text: raw, url: cleanUrl, isTrusted: trusted }
203
101
  rewritten += text.slice(last, start) + `{{${key}}}${cleanUrl}{{/${key}}}`
204
102
  } else {
205
103
  if (!citation) continue
206
104
  const key = entityKey('CITATION', citationCount)
207
105
  citationCount++
208
- entry = {
209
- type: 'citation',
210
- key,
211
- referenceId: citationCount, // 1-based, matches key index (zero-based +1)
212
- text: '',
213
- url,
214
- }
106
+ entry = { type: 'citation', key, referenceId: citationCount, text: '', url }
215
107
  rewritten += text.slice(last, start) + `{{${key}}}${url}{{/${key}}}`
216
108
  }
217
109
 
218
110
  last = end
219
111
  i = end - 1
220
-
221
112
  entities.push(entry)
222
113
  const shaped = SHAPERS[entry.type](entry)
223
114
  if (shaped) metadata.push(shaped)
@@ -1,43 +1,13 @@
1
- /**
2
- * @file table-metadata.js
3
- * @module lumina/parsers/table-metadata
4
- *
5
- * Convert a 2-D array of strings into the metadata shape WhatsApp expects
6
- * inside a `richResponseMessage.submessages[].tableMetadata`.
7
- *
8
- * The legacy `AIRich.toTableMetadata` always returned `title: ''` with no
9
- * way to set it. Lumina accepts a `title` option.
10
- */
11
-
12
1
  import { extractInlineEntities } from './inline-entity.js'
13
2
 
14
- /**
15
- * @typedef {object} TableMetadata
16
- * @property {string} title
17
- * @property {Array<{ items: string[], isHeading?: boolean }>} rows Submessage metadata.
18
- * @property {Array<{ is_header: boolean, cells: string[], markdown_cells?: Array<{ text: string, inline_entities?: object[] }> }>} unifiedRows Section primitive rows.
19
- */
20
-
21
- /**
22
- * @param {string[][]} table 2-D array of strings. First row is the header.
23
- * @param {object} [opts]
24
- * @param {string} [opts.title='']
25
- * @param {boolean} [opts.hyperlink=true]
26
- * @param {boolean} [opts.citation=true]
27
- * @param {boolean} [opts.latex=true]
28
- * @returns {TableMetadata}
29
- */
30
3
  export function toTableMetadata(table, opts = {}) {
31
4
  if (!Array.isArray(table) || !table.every((row) => Array.isArray(row) && row.every((c) => typeof c === 'string'))) {
32
5
  throw new TypeError('Table must be a 2-D array of strings')
33
6
  }
34
7
 
35
8
  const { title = '', hyperlink = true, citation = true, latex = true } = opts
36
-
37
9
  const [header, ...rows] = table
38
10
  const maxLen = Math.max(header.length, ...rows.map((r) => r.length))
39
-
40
- /** Pad a row to the table's max column count. */
41
11
  const normalize = (r) => [...r, ...Array(Math.max(0, maxLen - r.length)).fill('')]
42
12
 
43
13
  const unifiedRows = [
@@ -50,12 +20,8 @@ export function toTableMetadata(table, opts = {}) {
50
20
  if (extracted.metadata.length) out.inline_entities = extracted.metadata
51
21
  return out
52
22
  })
53
-
54
23
  const hasInline = markdownCells.some((c) => c.inline_entities?.length)
55
- return {
56
- ...row,
57
- ...(hasInline ? { markdown_cells: markdownCells } : {}),
58
- }
24
+ return { ...row, ...(hasInline ? { markdown_cells: markdownCells } : {}) }
59
25
  })
60
26
 
61
27
  const rowsMeta = unifiedRows.map((r) => ({
@@ -1,93 +1,41 @@
1
- /**
2
- * @file enums.js
3
- * @module lumina/proto/enums
4
- *
5
- * Single source of truth for every magic number / magic string the WhatsApp
6
- * protocol expects inside interactive messages, rich responses, and relay
7
- * nodes. Update the wire format here once, and the whole framework follows.
8
- *
9
- * Replaces the 20+ scattered magic values documented in Tahap-1 analysis:
10
- * - messageType: 1, 2, 4, 5, 9
11
- * - forwardOrigin: 4
12
- * - headerType: 0,1,3,4,5,6
13
- * - native_flow v: '9'
14
- * - botJid: '0@bot'
15
- * - 12x __typename string literals
16
- * - highlightType: 0-5
17
- * - imagine_type: 'IMAGE' | 'ANIMATE'
18
- * - etc.
19
- */
20
-
21
- /**
22
- * Rich-response submessage type. Stored in `richResponseMessage.submessages[].messageType`.
23
- */
24
1
  export const MessageType = Object.freeze({
25
- /** Grid-image rich response. */
26
2
  RICH_RESPONSE: 1,
27
- /** Plain text submessage (also used as fallback for video/product/post). */
28
3
  TEXT: 2,
29
- /** Table metadata submessage. */
30
4
  TABLE: 4,
31
- /** Code-block metadata submessage. */
32
5
  CODE: 5,
33
- /** Reels / contentItems metadata submessage. */
34
6
  REELS: 9,
35
7
  })
36
8
 
37
- /**
38
- * `contextInfo.forwardOrigin` enum used by WhatsApp for AI bot messages.
39
- */
40
9
  export const ForwardOrigin = Object.freeze({
41
10
  BOT: 4,
42
11
  })
43
12
 
44
- /**
45
- * ButtonsMessage headerType enum. The legacy ButtonV2 fallback used 6
46
- * (LOCATION_THUMBNAIL) without documenting it — now explicit.
47
- */
48
13
  export const HeaderType = Object.freeze({
49
14
  EMPTY: 0,
50
15
  TEXT: 1,
51
16
  IMAGE: 3,
52
17
  VIDEO: 4,
53
18
  DOCUMENT: 5,
54
- /** Pre-existing hack: location message with a JPEG thumbnail. */
55
19
  LOCATION_THUMBNAIL: 6,
56
20
  })
57
21
 
58
- /**
59
- * Native-flow biz/interactive node versions.
60
- */
61
22
  export const NativeFlow = Object.freeze({
62
- /** Outer `<interactive v="1">` version. */
63
23
  OUTER_VERSION: '1',
64
- /** Inner `<native_flow v="9">` version. */
65
24
  FLOW_VERSION: '9',
66
- /** Default flow name when mixing button types. */
67
25
  FLOW_NAME_MIXED: 'mixed',
68
26
  TYPE: 'native_flow',
69
27
  BIZ_TAG: 'biz',
70
28
  INTERACTIVE_TAG: 'interactive',
71
29
  })
72
30
 
73
- /**
74
- * Hardcoded bot JID that WhatsApp expects inside `forwardedAiBotMessageInfo`.
75
- */
76
31
  export const BOT_JID = '0@bot'
77
32
 
78
- /**
79
- * Layout view-model kinds. Each becomes `GenAI{Kind}LayoutViewModel`.
80
- */
81
33
  export const LayoutKind = Object.freeze({
82
34
  SINGLE: 'Single',
83
35
  HSCROLL: 'HScroll',
84
36
  ACTION_ROW: 'ActionRow',
85
37
  })
86
38
 
87
- /**
88
- * Code-tokenizer highlight type numbers → string labels. Matches the
89
- * `TYPE_MAP` constant in the legacy AIRich.tokenizer.
90
- */
91
39
  export const HighlightType = Object.freeze({
92
40
  DEFAULT: 0,
93
41
  KEYWORD: 1,
@@ -97,7 +45,6 @@ export const HighlightType = Object.freeze({
97
45
  COMMENT: 5,
98
46
  })
99
47
 
100
- /** String labels corresponding to {@link HighlightType}. */
101
48
  export const HighlightLabel = Object.freeze({
102
49
  0: 'DEFAULT',
103
50
  1: 'KEYWORD',
@@ -107,34 +54,23 @@ export const HighlightLabel = Object.freeze({
107
54
  5: 'COMMENT',
108
55
  })
109
56
 
110
- /** `imagine_type` field on GenAIImaginePrimitive. */
111
57
  export const ImagineType = Object.freeze({
112
58
  IMAGE: 'IMAGE',
113
59
  ANIMATE: 'ANIMATE',
114
60
  })
115
61
 
116
- /** `source_type` field on GenAISearchResultPrimitive. */
117
62
  export const SourceType = Object.freeze({
118
63
  THIRD_PARTY: 'THIRD_PARTY',
119
64
  })
120
65
 
121
- /** `prompt_type` field on GenAIFollowUpSuggestionPillPrimitive. */
122
66
  export const PromptType = Object.freeze({
123
67
  SUGGESTED_PROMPT: 'SUGGESTED_PROMPT',
124
68
  })
125
69
 
126
- /** `sessionTransparencyType` field on botMetadata. */
127
70
  export const SessionTransparencyType = Object.freeze({
128
71
  DEFAULT: 1,
129
72
  })
130
73
 
131
- /**
132
- * All GenAI `__typename` strings. Single source of truth — typo-proof.
133
- *
134
- * NOTE: legacy code had `GenATableUXPrimitive` (missing the second 'I').
135
- * Verified against current WhatsApp Web wire format → the correct spelling
136
- * is `GenAITableUXPrimitive`. Lumina corrects this.
137
- */
138
74
  export const TYPENAME = Object.freeze({
139
75
  MARKDOWN_TEXT: 'GenAIMarkdownTextUXPrimitive',
140
76
  CODE: 'GenAICodeUXPrimitive',
@@ -147,12 +83,19 @@ export const TYPENAME = Object.freeze({
147
83
  METADATA_TEXT: 'GenAIMetadataTextPrimitive',
148
84
  FOLLOW_UP_PILL: 'GenAIFollowUpSuggestionPillPrimitive',
149
85
  UNIFIED_SECTION: 'GenAIUnifiedResponseSection',
150
- /** Layout view-models are suffixed `GenAI{Kind}LayoutViewModel`. */
151
86
  layout(kind) {
152
87
  return `GenAI${kind}LayoutViewModel`
153
88
  },
154
89
  })
155
90
 
91
+ export const SimpleButtonType = Object.freeze({
92
+ reply: 'quick_reply',
93
+ call: 'cta_call',
94
+ reminder: 'cta_reminder',
95
+ cancelReminder: 'cta_cancel_reminder',
96
+ address: 'address_message',
97
+ })
98
+
156
99
  export default {
157
100
  MessageType,
158
101
  ForwardOrigin,
@@ -167,4 +110,5 @@ export default {
167
110
  PromptType,
168
111
  SessionTransparencyType,
169
112
  TYPENAME,
113
+ SimpleButtonType,
170
114
  }
@@ -1,78 +1,17 @@
1
- /**
2
- * @file layouts.js
3
- * @module lumina/proto/layouts
4
- *
5
- * Layout view-model factories. Each layout wraps one or more UX primitives
6
- * inside `view_model`, with the proper `GenAI{Kind}LayoutViewModel` typename.
7
- *
8
- * Replaces the implicit `Array.isArray(data) ? 'primitives' : 'primitive'`
9
- * branching in the legacy `AIRich.newLayout` static method — explicit
10
- * single/hscroll/action-row variants instead.
11
- */
12
-
13
1
  import { LayoutKind, TYPENAME } from './enums.js'
14
2
 
15
- /**
16
- * Wrap a single primitive in a "Single" layout view-model.
17
- *
18
- * @param {object} primitive
19
- * @param {object} [extra] Additional fields merged at top-level.
20
- * @returns {object}
21
- */
22
3
  export function singleLayout(primitive, extra = {}) {
23
- return {
24
- ...extra,
25
- view_model: {
26
- primitive,
27
- __typename: TYPENAME.layout(LayoutKind.SINGLE),
28
- },
29
- }
4
+ return { ...extra, view_model: { primitive, __typename: TYPENAME.layout(LayoutKind.SINGLE) } }
30
5
  }
31
6
 
32
- /**
33
- * Wrap multiple primitives in an "HScroll" (horizontal scroll) layout.
34
- *
35
- * @param {object[]} primitives
36
- * @param {object} [extra]
37
- * @returns {object}
38
- */
39
7
  export function hscrollLayout(primitives, extra = {}) {
40
- return {
41
- ...extra,
42
- view_model: {
43
- primitives,
44
- __typename: TYPENAME.layout(LayoutKind.HSCROLL),
45
- },
46
- }
8
+ return { ...extra, view_model: { primitives, __typename: TYPENAME.layout(LayoutKind.HSCROLL) } }
47
9
  }
48
10
 
49
- /**
50
- * Wrap multiple primitives in an "ActionRow" layout (used for follow-up
51
- * suggestion pills that should appear as a horizontal row of buttons).
52
- *
53
- * @param {object[]} primitives
54
- * @param {object} [extra]
55
- * @returns {object}
56
- */
57
11
  export function actionRowLayout(primitives, extra = {}) {
58
- return {
59
- ...extra,
60
- view_model: {
61
- primitives,
62
- __typename: TYPENAME.layout(LayoutKind.ACTION_ROW),
63
- },
64
- }
12
+ return { ...extra, view_model: { primitives, __typename: TYPENAME.layout(LayoutKind.ACTION_ROW) } }
65
13
  }
66
14
 
67
- /**
68
- * Auto-pick helper. Picks `singleLayout` for a single primitive and
69
- * `hscrollLayout` for arrays — useful for API surfaces that accept both.
70
- *
71
- * @param {LayoutKind} kind Forced kind; overrides auto-detection.
72
- * @param {object|object[]} data
73
- * @param {object} [extra]
74
- * @returns {object}
75
- */
76
15
  export function layoutFor(kind, data, extra = {}) {
77
16
  switch (kind) {
78
17
  case LayoutKind.SINGLE: