@mostfeatured/dbi 0.2.13 → 0.2.15

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 (45) hide show
  1. package/dist/src/types/Components/HTMLComponentsV2/index.d.ts +33 -1
  2. package/dist/src/types/Components/HTMLComponentsV2/index.d.ts.map +1 -1
  3. package/dist/src/types/Components/HTMLComponentsV2/index.js +408 -82
  4. package/dist/src/types/Components/HTMLComponentsV2/index.js.map +1 -1
  5. package/dist/src/types/Components/HTMLComponentsV2/parser.d.ts +52 -0
  6. package/dist/src/types/Components/HTMLComponentsV2/parser.d.ts.map +1 -1
  7. package/dist/src/types/Components/HTMLComponentsV2/parser.js +275 -0
  8. package/dist/src/types/Components/HTMLComponentsV2/parser.js.map +1 -1
  9. package/dist/src/types/Components/HTMLComponentsV2/svelteParser.d.ts +26 -0
  10. package/dist/src/types/Components/HTMLComponentsV2/svelteParser.d.ts.map +1 -1
  11. package/dist/src/types/Components/HTMLComponentsV2/svelteParser.js +509 -34
  12. package/dist/src/types/Components/HTMLComponentsV2/svelteParser.js.map +1 -1
  13. package/dist/src/types/Components/HTMLComponentsV2/svelteRenderer.d.ts +10 -0
  14. package/dist/src/types/Components/HTMLComponentsV2/svelteRenderer.d.ts.map +1 -1
  15. package/dist/src/types/Components/HTMLComponentsV2/svelteRenderer.js +76 -11
  16. package/dist/src/types/Components/HTMLComponentsV2/svelteRenderer.js.map +1 -1
  17. package/dist/test/index.js +76 -3
  18. package/dist/test/index.js.map +1 -1
  19. package/docs/ADVANCED_FEATURES.md +4 -0
  20. package/docs/API_REFERENCE.md +4 -0
  21. package/docs/CHAT_INPUT.md +4 -0
  22. package/docs/COMPONENTS.md +4 -0
  23. package/docs/EVENTS.md +4 -0
  24. package/docs/GETTING_STARTED.md +4 -0
  25. package/docs/LOCALIZATION.md +4 -0
  26. package/docs/README.md +4 -0
  27. package/docs/SVELTE_COMPONENTS.md +162 -6
  28. package/docs/llm/ADVANCED_FEATURES.txt +521 -0
  29. package/docs/llm/API_REFERENCE.txt +659 -0
  30. package/docs/llm/CHAT_INPUT.txt +514 -0
  31. package/docs/llm/COMPONENTS.txt +595 -0
  32. package/docs/llm/EVENTS.txt +449 -0
  33. package/docs/llm/GETTING_STARTED.txt +296 -0
  34. package/docs/llm/LOCALIZATION.txt +501 -0
  35. package/docs/llm/README.txt +193 -0
  36. package/docs/llm/SVELTE_COMPONENTS.txt +566 -0
  37. package/generated/svelte-dbi.d.ts +122 -0
  38. package/package.json +1 -1
  39. package/src/types/Components/HTMLComponentsV2/index.ts +466 -94
  40. package/src/types/Components/HTMLComponentsV2/parser.ts +317 -0
  41. package/src/types/Components/HTMLComponentsV2/svelteParser.ts +567 -35
  42. package/src/types/Components/HTMLComponentsV2/svelteRenderer.ts +91 -13
  43. package/test/index.ts +76 -3
  44. package/test/product-showcase.svelte +380 -24
  45. package/llm.txt +0 -1088
@@ -6,7 +6,151 @@ import { renderSvelteComponent, renderSvelteComponentFromFile, SvelteRenderResul
6
6
  import { parseSvelteComponent, createHandlerContext, HandlerContextResult } from "./svelteParser";
7
7
  import fs from "fs";
8
8
 
9
- export type TDBIHTMLComponentsV2Omitted<TNamespace extends NamespaceEnums> = Omit<DBIHTMLComponentsV2<TNamespace>, "type" | "dbi" | "toJSON" | "description" | "send" | "destroy" | "destroyAll"> & {
9
+ /**
10
+ * Parse Discord API error and provide helpful context about which HTML element caused the error
11
+ */
12
+ function parseDiscordAPIError(error: any, source?: string, componentName?: string): Error {
13
+ // Check if it's a Discord API error with form body issues
14
+ const message = error.message || '';
15
+ const rawError = error.rawError || error;
16
+
17
+ // Extract the path from error message like "data.components[0].components[0].accessory.media.url"
18
+ const pathMatch = message.match(/data\.components(\[[^\]]+\](?:\.[^\[\s\[]+|\[[^\]]+\])*)/);
19
+
20
+ if (!pathMatch) {
21
+ return error; // Not a parseable Discord API error
22
+ }
23
+
24
+ const errorPath = pathMatch[1];
25
+ const errorCode = message.match(/\[([A-Z_]+)\]/)?.[1] || 'UNKNOWN';
26
+
27
+ // Parse the path to understand the component structure
28
+ // e.g., "[0].components[0].accessory.media.url" -> component index 0, child 0, accessory.media.url
29
+ const parts = errorPath.split(/\.|\[|\]/).filter(Boolean);
30
+
31
+ // Build a human-readable path description
32
+ let description = '';
33
+ let elementHint = '';
34
+ let currentPath = [];
35
+
36
+ for (let i = 0; i < parts.length; i++) {
37
+ const part = parts[i];
38
+ if (!isNaN(Number(part))) {
39
+ currentPath.push(`[${part}]`);
40
+ } else {
41
+ currentPath.push(part);
42
+
43
+ // Provide hints based on known Discord component structure
44
+ if (part === 'accessory') {
45
+ elementHint = '<section> element\'s accessory (thumbnail/button)';
46
+ } else if (part === 'media') {
47
+ elementHint = '<thumbnail> or <media-gallery> element';
48
+ } else if (part === 'url') {
49
+ elementHint = 'media URL attribute';
50
+ } else if (part === 'components') {
51
+ // Skip, it's container
52
+ } else if (part === 'content') {
53
+ elementHint = 'text content of a <text-display> element';
54
+ } else if (part === 'label') {
55
+ elementHint = 'label attribute of a <button> or <option>';
56
+ } else if (part === 'custom_id') {
57
+ elementHint = 'name attribute of an interactive element';
58
+ } else if (part === 'placeholder') {
59
+ elementHint = 'placeholder attribute of a select menu';
60
+ } else if (part === 'title') {
61
+ elementHint = 'title attribute of a <modal> or <section>';
62
+ } else if (part === 'options') {
63
+ elementHint = '<option> elements inside a <string-select>';
64
+ } else if (part === 'value') {
65
+ elementHint = 'value attribute of an <option> or <text-input>';
66
+ } else if (part === 'description') {
67
+ elementHint = 'description attribute of an <option>';
68
+ }
69
+ }
70
+ }
71
+
72
+ // Map error codes to helpful messages
73
+ const errorMessages: Record<string, string> = {
74
+ 'BASE_TYPE_REQUIRED': 'This field is required but was empty or undefined',
75
+ 'STRING_TYPE_REQUIRED': 'This field must be a string',
76
+ 'NUMBER_TYPE_REQUIRED': 'This field must be a number',
77
+ 'BOOLEAN_TYPE_REQUIRED': 'This field must be a boolean',
78
+ 'INVALID_URL': 'The URL provided is not valid',
79
+ 'MAX_LENGTH': 'The value exceeds the maximum allowed length',
80
+ 'MIN_LENGTH': 'The value is shorter than the minimum required length',
81
+ 'CHOICE_NOT_FOUND': 'The selected value is not in the list of options',
82
+ };
83
+
84
+ const errorExplanation = errorMessages[errorCode] || `Error code: ${errorCode}`;
85
+
86
+ // Build the enhanced error message
87
+ let enhancedMessage = `
88
+ ╔══════════════════════════════════════════════════════════════╗
89
+ ║ Discord API Error - Invalid Component Data ║
90
+ ╚══════════════════════════════════════════════════════════════╝
91
+
92
+ 📍 Component: ${componentName || 'unknown'}
93
+ 📍 Error Path: data.components${errorPath}
94
+ 📍 Error Code: ${errorCode}
95
+
96
+ ❌ ${errorExplanation}
97
+ ${elementHint ? `\n💡 This error is likely in: ${elementHint}` : ''}
98
+
99
+ 🔍 What to check:
100
+ `;
101
+
102
+ // Add specific suggestions based on error type
103
+ if (errorPath.includes('media.url')) {
104
+ enhancedMessage += ` • Make sure the image/media URL is provided and valid
105
+ • Check that the data property used for the image exists
106
+ • Example: <thumbnail media={product?.image}> - is product.image defined?
107
+ `;
108
+ } else if (errorPath.includes('accessory')) {
109
+ enhancedMessage += ` • The <section> element requires valid content in its accessory
110
+ • If using <thumbnail>, ensure the media URL is valid
111
+ `;
112
+ } else if (errorPath.includes('content')) {
113
+ enhancedMessage += ` • Text content cannot be empty or undefined
114
+ • Check your template expressions like {variable?.property}
115
+ `;
116
+ } else if (errorPath.includes('options')) {
117
+ enhancedMessage += ` • Select menu options must have valid value and label
118
+ • Each <option> needs: value="..." and text content
119
+ `;
120
+ } else if (errorPath.includes('label') || errorPath.includes('custom_id')) {
121
+ enhancedMessage += ` • Interactive elements (buttons, selects) need valid labels/names
122
+ • Check that text content and name attributes are not empty
123
+ `;
124
+ }
125
+
126
+ // If we have source, try to highlight relevant section
127
+ if (source && elementHint) {
128
+ const elementType = elementHint.match(/<(\w+-?\w*)>/)?.[1];
129
+ if (elementType) {
130
+ const elementRegex = new RegExp(`<${elementType}[^>]*>`, 'g');
131
+ const matches = source.match(elementRegex);
132
+ if (matches && matches.length > 0) {
133
+ enhancedMessage += `\n📝 Found ${matches.length} <${elementType}> element(s) in template:`;
134
+ matches.slice(0, 3).forEach((m, i) => {
135
+ enhancedMessage += `\n ${i + 1}. ${m.substring(0, 80)}${m.length > 80 ? '...' : ''}`;
136
+ });
137
+ if (matches.length > 3) {
138
+ enhancedMessage += `\n ... and ${matches.length - 3} more`;
139
+ }
140
+ }
141
+ }
142
+ }
143
+
144
+ const enhancedError = new Error(enhancedMessage);
145
+ (enhancedError as any).originalError = error;
146
+ (enhancedError as any).type = 'discord-api-error';
147
+ (enhancedError as any).path = errorPath;
148
+ (enhancedError as any).code = errorCode;
149
+
150
+ return enhancedError;
151
+ }
152
+
153
+ export type TDBIHTMLComponentsV2Omitted<TNamespace extends NamespaceEnums> = Omit<DBIHTMLComponentsV2<TNamespace>, "type" | "dbi" | "toJSON" | "description" | "send" | "destroy" | "destroyAll" | "__lastRenderModals__" | "_pendingModals" | "_initPromise"> & {
10
154
  /**
11
155
  * Use 'svelte' for Svelte 5 components, 'eta' for Eta templates (default)
12
156
  */
@@ -47,6 +191,13 @@ export class DBIHTMLComponentsV2<TNamespace extends NamespaceEnums> extends DBIB
47
191
  // Key: ref id, Value: handlerContext with lifecycle hooks
48
192
  private _activeContexts: Map<string, any> = new Map();
49
193
 
194
+ // Store pending modal promises for await showModal() support
195
+ // Key: modal customId, Value: { resolve, reject } functions
196
+ _pendingModals: Map<string, { resolve: (result: any) => void; reject: (error: any) => void }> = new Map();
197
+
198
+ // Track initialization promise
199
+ private _initPromise: Promise<void> | null = null;
200
+
50
201
  constructor(dbi: DBI<TNamespace>, args: TDBIHTMLComponentsV2Omitted<TNamespace>) {
51
202
  // Store user's onExecute callback before passing to super
52
203
  const userOnExecute = (args as any).onExecute;
@@ -72,8 +223,8 @@ export class DBIHTMLComponentsV2<TNamespace extends NamespaceEnums> extends DBIB
72
223
 
73
224
  // Pre-extract Svelte handlers at registration time (lazy loaded)
74
225
  if (this.mode === 'svelte' && this.template) {
75
- // Defer the parsing to avoid sync import issues
76
- this._initSvelteComponent();
226
+ // Store the promise so we can await it in _handleExecute
227
+ this._initPromise = this._initSvelteComponent();
77
228
  }
78
229
 
79
230
  // Re-assign onExecute method after super() call because parent class sets it to undefined
@@ -86,7 +237,12 @@ export class DBIHTMLComponentsV2<TNamespace extends NamespaceEnums> extends DBIB
86
237
  }
87
238
  }
88
239
 
89
- private _handleExecute(ctx: IDBIHTMLComponentsV2ExecuteCtx<TNamespace>) {
240
+ private async _handleExecute(ctx: IDBIHTMLComponentsV2ExecuteCtx<TNamespace>) {
241
+ // Wait for Svelte component initialization if not yet completed
242
+ if (this._initPromise) {
243
+ await this._initPromise;
244
+ }
245
+
90
246
  // Call user's onExecute callback first if provided
91
247
  if (this._userOnExecute) {
92
248
  this._userOnExecute(ctx);
@@ -97,101 +253,205 @@ export class DBIHTMLComponentsV2<TNamespace extends NamespaceEnums> extends DBIB
97
253
  const [elementName, ...handlerData] = ctx.data;
98
254
 
99
255
  if (typeof elementName === 'string') {
100
- // Find the handler info for this element
256
+ // Check if this is a modal submit (elementName is the modal id)
257
+ const modalHandlerInfo = this.svelteComponentInfo.modalHandlers.get(elementName);
258
+
259
+ if (modalHandlerInfo) {
260
+ // This is a modal submit - execute the onsubmit handler (or just resolve promise if no handler)
261
+ this._executeModalSubmit(ctx, elementName, modalHandlerInfo.onsubmitHandler, handlerData);
262
+ return;
263
+ }
264
+
265
+ // Find the handler info for this element (button, select, etc.)
101
266
  const handlerInfo = this.svelteComponentInfo.handlers.get(elementName);
102
267
 
103
268
  if (handlerInfo) {
104
- // Extract current state from handlerData (refs that were passed)
105
- // The second element in data array contains the current state
106
- const currentState = handlerData[0] || {} as Record<string, any>;
107
-
108
- // Get ref id for lifecycle tracking (if available)
109
- const refId = (currentState as any)?.$ref || null;
110
-
111
- // Check if this is first execution for this ref
112
- const isFirstExecution = refId && !this._activeContexts.has(refId);
113
-
114
- // Get existing context if any (to preserve lifecycle callbacks like intervals)
115
- const existingContext = refId ? this._activeContexts.get(refId) : null;
116
-
117
- // Create a NEW handler context for each execution with the current state
118
- // This ensures each interaction has its own isolated state
119
- // Pass 'this' so handlers can access component methods like toJSON()
120
- // Pass ctx so handlers can access ctx.interaction, ctx.data, ctx.locale, etc.
121
- const handlerContext = createHandlerContext(
122
- this.svelteComponentInfo.scriptContent,
123
- typeof currentState === 'object' ? currentState : {},
124
- this,
125
- ctx
126
- );
127
-
128
- const handlerFn = handlerContext.handlers[handlerInfo.handlerName];
129
-
130
- if (handlerFn && typeof handlerFn === 'function') {
131
- try {
132
- // Store context for lifecycle management
133
- if (refId) {
134
- // If there's an existing context, transfer its destroy callbacks to the new context
135
- // This preserves intervals/timers created in onMount
136
- if (existingContext && existingContext.destroyCallbacks) {
137
- // Merge existing destroy callbacks (don't run them!)
138
- handlerContext.destroyCallbacks.push(...existingContext.destroyCallbacks);
139
- }
140
-
141
- this._activeContexts.set(refId, handlerContext);
142
-
143
- // Wrap $unRef to call onDestroy when ref is deleted (only wrap once)
144
- const stateObj = currentState as any;
145
- if (stateObj.$unRef && !stateObj.__unRefWrapped__) {
146
- const originalUnRef = stateObj.$unRef.bind(stateObj);
147
- stateObj.$unRef = () => {
148
- // Run destroy callbacks before unref
149
- const ctx = this._activeContexts.get(refId);
150
- if (ctx && ctx.runDestroy) {
151
- ctx.runDestroy();
152
- }
153
- this._activeContexts.delete(refId);
154
- return originalUnRef();
155
- };
156
- stateObj.__unRefWrapped__ = true;
157
- }
158
- }
269
+ this._executeElementHandler(ctx, handlerInfo, handlerData);
270
+ }
271
+ }
272
+ }
273
+ }
159
274
 
160
- // Run onMount callbacks only on first execution
161
- if (isFirstExecution && handlerContext.runMount) {
162
- handlerContext.runMount();
163
- }
275
+ /**
276
+ * Execute a modal submit handler
277
+ */
278
+ private _executeModalSubmit(
279
+ ctx: IDBIHTMLComponentsV2ExecuteCtx<TNamespace>,
280
+ modalId: string,
281
+ handlerName: string | undefined,
282
+ handlerData: any[]
283
+ ) {
284
+ // Extract current state from handlerData (refs that were passed)
285
+ const currentState = handlerData[0] || {} as Record<string, any>;
286
+
287
+ // Get ref id for lifecycle tracking (if available)
288
+ const refId = (currentState as any)?.$ref || null;
289
+
290
+ // Extract all field values from modal interaction (text inputs, selects, file uploads, etc.)
291
+ const modalInteraction = ctx.interaction as any;
292
+ const fields = this._extractModalFields(modalInteraction);
293
+
294
+ // Check if there's a pending promise for this modal (from await showModal())
295
+ // Use the modal's customId to find the pending promise
296
+ const pendingKey = modalInteraction.customId;
297
+ const pendingModal = this._pendingModals.get(pendingKey);
298
+
299
+ if (pendingModal) {
300
+ // Resolve the promise with fields and interaction
301
+ pendingModal.resolve({
302
+ fields,
303
+ interaction: modalInteraction,
304
+ ctx: ctx
305
+ });
306
+ this._pendingModals.delete(pendingKey);
307
+ }
164
308
 
165
- // Mark that we're inside handler execution (prevents auto-render during handler)
166
- handlerContext.setInHandler(true);
167
-
168
- try {
169
- // Bind 'this' to the DBIHTMLComponentsV2 instance so handlers can use this.toJSON()
170
- // Pass wrappedCtx so handlers use the proxy-wrapped ctx that tracks interaction calls
171
- // This ensures __asyncInteractionCalled__ flag is set when handler calls ctx.interaction.reply() etc.
172
- handlerFn.call(this, handlerContext.wrappedCtx, ...handlerData.slice(1));
173
- } finally {
174
- // Always reset handler execution flag
175
- handlerContext.setInHandler(false);
176
- }
309
+ // If no onsubmit handler defined, just return (promise-based usage)
310
+ if (!handlerName) {
311
+ return;
312
+ }
313
+
314
+ // Create handler context for modal submit
315
+ const handlerContext = createHandlerContext(
316
+ this.svelteComponentInfo!.scriptContent,
317
+ typeof currentState === 'object' ? currentState : {},
318
+ this,
319
+ ctx
320
+ );
321
+
322
+ const handlerFn = handlerContext.handlers[handlerName];
323
+
324
+ if (handlerFn && typeof handlerFn === 'function') {
325
+ try {
326
+ // Store context for lifecycle management
327
+ if (refId) {
328
+ const existingContext = this._activeContexts.get(refId);
329
+ if (existingContext && existingContext.destroyCallbacks) {
330
+ handlerContext.destroyCallbacks.push(...existingContext.destroyCallbacks);
331
+ }
332
+ this._activeContexts.set(refId, handlerContext);
333
+ }
334
+
335
+ handlerContext.setInHandler(true);
336
+
337
+ try {
338
+ // Call handler with ctx and fields object
339
+ handlerFn.call(this, handlerContext.wrappedCtx, fields, ...handlerData.slice(1));
340
+ } finally {
341
+ handlerContext.setInHandler(false);
342
+ }
177
343
 
178
- // Run effects after handler execution (state may have changed)
179
- handlerContext.runEffects();
344
+ handlerContext.runEffects();
180
345
 
181
- // If there are pending data changes and no interaction method was called,
182
- // flush the render now (synchronously uses interaction.update)
183
- if (handlerContext.hasPendingRender()) {
184
- handlerContext.flushRender();
346
+ if (handlerContext.hasPendingRender()) {
347
+ handlerContext.flushRender();
348
+ }
349
+ } catch (error) {
350
+ // Modal handler execution failed
351
+ }
352
+ }
353
+ }
354
+
355
+ /**
356
+ * Execute an element handler (button, select, etc.)
357
+ */
358
+ private _executeElementHandler(
359
+ ctx: IDBIHTMLComponentsV2ExecuteCtx<TNamespace>,
360
+ handlerInfo: any,
361
+ handlerData: any[]
362
+ ) {
363
+ // Extract current state from handlerData (refs that were passed)
364
+ // The second element in data array contains the current state
365
+ const currentState = handlerData[0] || {} as Record<string, any>;
366
+
367
+ // Get ref id for lifecycle tracking (if available)
368
+ const refId = (currentState as any)?.$ref || null;
369
+
370
+ // Check if this is first execution for this ref
371
+ const isFirstExecution = refId && !this._activeContexts.has(refId);
372
+
373
+ // Get existing context if any (to preserve lifecycle callbacks like intervals)
374
+ const existingContext = refId ? this._activeContexts.get(refId) : null;
375
+
376
+ // Create a NEW handler context for each execution with the current state
377
+ // This ensures each interaction has its own isolated state
378
+ // Pass 'this' so handlers can access component methods like toJSON()
379
+ // Pass ctx so handlers can access ctx.interaction, ctx.data, ctx.locale, etc.
380
+ const handlerContext = createHandlerContext(
381
+ this.svelteComponentInfo!.scriptContent,
382
+ typeof currentState === 'object' ? currentState : {},
383
+ this,
384
+ ctx
385
+ );
386
+
387
+ const handlerFn = handlerContext.handlers[handlerInfo.handlerName];
388
+
389
+ if (handlerFn && typeof handlerFn === 'function') {
390
+ try {
391
+ // Store context for lifecycle management
392
+ if (refId) {
393
+ // If there's an existing context, transfer its destroy callbacks to the new context
394
+ // This preserves intervals/timers created in onMount
395
+ if (existingContext && existingContext.destroyCallbacks) {
396
+ // Merge existing destroy callbacks (don't run them!)
397
+ handlerContext.destroyCallbacks.push(...existingContext.destroyCallbacks);
398
+ }
399
+
400
+ this._activeContexts.set(refId, handlerContext);
401
+
402
+ // Wrap $unRef to call onDestroy when ref is deleted (only wrap once)
403
+ const stateObj = currentState as any;
404
+ if (stateObj.$unRef && !stateObj.__unRefWrapped__) {
405
+ const originalUnRef = stateObj.$unRef.bind(stateObj);
406
+ stateObj.$unRef = () => {
407
+ // Run destroy callbacks before unref
408
+ const ctx = this._activeContexts.get(refId);
409
+ if (ctx && ctx.runDestroy) {
410
+ ctx.runDestroy();
185
411
  }
186
- } catch (error) {
187
- // Handler execution failed
188
- }
412
+ this._activeContexts.delete(refId);
413
+ return originalUnRef();
414
+ };
415
+ stateObj.__unRefWrapped__ = true;
189
416
  }
190
417
  }
418
+
419
+ // Run onMount callbacks only on first execution
420
+ if (isFirstExecution && handlerContext.runMount) {
421
+ handlerContext.runMount();
422
+ }
423
+
424
+ // Mark that we're inside handler execution (prevents auto-render during handler)
425
+ handlerContext.setInHandler(true);
426
+
427
+ try {
428
+ // Bind 'this' to the DBIHTMLComponentsV2 instance so handlers can use this.toJSON()
429
+ // Pass wrappedCtx so handlers use the proxy-wrapped ctx that tracks interaction calls
430
+ // This ensures __asyncInteractionCalled__ flag is set when handler calls ctx.interaction.reply() etc.
431
+ handlerFn.call(this, handlerContext.wrappedCtx, ...handlerData.slice(1));
432
+ } finally {
433
+ // Always reset handler execution flag
434
+ handlerContext.setInHandler(false);
435
+ }
436
+
437
+ // Run effects after handler execution (state may have changed)
438
+ handlerContext.runEffects();
439
+
440
+ // If there are pending data changes and no interaction method was called,
441
+ // flush the render now (synchronously uses interaction.update)
442
+ if (handlerContext.hasPendingRender()) {
443
+ handlerContext.flushRender();
444
+ }
445
+ } catch (error) {
446
+ // Handler execution failed
191
447
  }
192
448
  }
193
449
  }
194
450
 
451
+ // Store last render's modals for showModal() to access
452
+ // Note: Used internally by handler context, not truly private
453
+ __lastRenderModals__: Map<string, any> | null = null;
454
+
195
455
  override async toJSON(arg: TDBIHTMLComponentsV2ToJSONArgs = {}): Promise<any> {
196
456
  if (this.mode === 'svelte' && this.template) {
197
457
  // Render Svelte component
@@ -205,6 +465,9 @@ export class DBIHTMLComponentsV2<TNamespace extends NamespaceEnums> extends DBIB
205
465
  }
206
466
  );
207
467
 
468
+ // Store modals for showModal() to access
469
+ this.__lastRenderModals__ = result.modals;
470
+
208
471
  return result.components;
209
472
  } else {
210
473
  // Use Eta template parsing (default)
@@ -242,9 +505,14 @@ export class DBIHTMLComponentsV2<TNamespace extends NamespaceEnums> extends DBIB
242
505
  * ```
243
506
  */
244
507
  async send(target: any, options: TDBIHTMLComponentsV2SendOptions = {}): Promise<any> {
508
+ // Wait for Svelte component initialization if not yet completed
509
+ if (this._initPromise) {
510
+ await this._initPromise;
511
+ }
512
+
245
513
  const { data = {}, flags = ["IsComponentsV2"], content, ephemeral, reply, followUp } = options;
246
514
 
247
- // Render components (toJSON is async)
515
+ // Render components (toJSON is async) - this also creates $ref in data if not present
248
516
  const components = await this.toJSON({ data });
249
517
 
250
518
  // Build message options
@@ -257,19 +525,29 @@ export class DBIHTMLComponentsV2<TNamespace extends NamespaceEnums> extends DBIB
257
525
  const isInteraction = target.reply && target.user; // Interactions have both reply method and user property
258
526
  const isChannel = target.send && !target.user; // Channels have send but no user
259
527
 
260
- if (isInteraction) {
261
- if (followUp) {
262
- message = await target.followUp(messageOptions);
528
+ try {
529
+ if (isInteraction) {
530
+ if (followUp) {
531
+ message = await target.followUp(messageOptions);
532
+ } else {
533
+ message = await target.reply(messageOptions);
534
+ }
535
+ } else if (isChannel) {
536
+ message = await target.send(messageOptions);
263
537
  } else {
264
- message = await target.reply(messageOptions);
538
+ throw new Error("Invalid target: must be an interaction or channel");
265
539
  }
266
- } else if (isChannel) {
267
- message = await target.send(messageOptions);
268
- } else {
269
- throw new Error("Invalid target: must be an interaction or channel");
540
+ } catch (error: any) {
541
+ // Check if it's a Discord API error and enhance it with helpful context
542
+ if (error.code || error.rawError || (error.message && error.message.includes('Invalid Form Body'))) {
543
+ const source = this.file ? fs.readFileSync(this.file, 'utf-8') : this.template;
544
+ throw parseDiscordAPIError(error, source, this.name);
545
+ }
546
+ throw error;
270
547
  }
271
548
 
272
549
  // If Svelte mode, create initial handler context and run onMount
550
+ // After toJSON, data.$ref is guaranteed to exist (created in renderSvelteComponent)
273
551
  if (this.mode === 'svelte' && this.svelteComponentInfo && data.$ref) {
274
552
  const refId = data.$ref;
275
553
 
@@ -355,6 +633,100 @@ export class DBIHTMLComponentsV2<TNamespace extends NamespaceEnums> extends DBIB
355
633
  return true;
356
634
  }
357
635
 
636
+ /**
637
+ * Extract all field values from a modal interaction
638
+ * Supports text inputs, select menus (string, user, role, mentionable, channel), and file uploads
639
+ *
640
+ * Returns an object where keys are the custom_id of each component and values are:
641
+ * - For text inputs: string value
642
+ * - For string selects: string[] of selected values
643
+ * - For user selects: string[] of user IDs
644
+ * - For role selects: string[] of role IDs
645
+ * - For mentionable selects: { users: string[], roles: string[] }
646
+ * - For channel selects: string[] of channel IDs
647
+ * - For file uploads: attachment objects array
648
+ */
649
+ private _extractModalFields(modalInteraction: any): Record<string, any> {
650
+ const fields: Record<string, any> = {};
651
+
652
+ // Handle classic text input fields via ModalSubmitFields
653
+ if (modalInteraction.fields && modalInteraction.fields.fields) {
654
+ for (const [customId, field] of modalInteraction.fields.fields) {
655
+ // Text input - field.value is the string
656
+ fields[customId] = field.value;
657
+ }
658
+ }
659
+
660
+ // Handle new modal components from interaction.data.components
661
+ // The new modal structure uses Label wrappers with nested components
662
+ const data = modalInteraction.data || (modalInteraction as any).data;
663
+ if (data && data.components) {
664
+ this._extractFieldsFromComponents(data.components, fields);
665
+ }
666
+
667
+ return fields;
668
+ }
669
+
670
+ /**
671
+ * Recursively extract field values from component structure
672
+ */
673
+ private _extractFieldsFromComponents(components: any[], fields: Record<string, any>) {
674
+ for (const component of components) {
675
+ const type = component.type;
676
+ const customId = component.custom_id;
677
+
678
+ // Type 18 = Label - has nested component
679
+ if (type === 18 && component.component) {
680
+ this._extractFieldsFromComponents([component.component], fields);
681
+ continue;
682
+ }
683
+
684
+ // Type 1 = Action Row - has nested components array
685
+ if (type === 1 && component.components) {
686
+ this._extractFieldsFromComponents(component.components, fields);
687
+ continue;
688
+ }
689
+
690
+ if (!customId) continue;
691
+
692
+ switch (type) {
693
+ case 4: // Text Input
694
+ fields[customId] = component.value || '';
695
+ break;
696
+
697
+ case 3: // String Select
698
+ fields[customId] = component.values || [];
699
+ break;
700
+
701
+ case 5: // User Select
702
+ fields[customId] = component.values || [];
703
+ break;
704
+
705
+ case 6: // Role Select
706
+ fields[customId] = component.values || [];
707
+ break;
708
+
709
+ case 7: // Mentionable Select - can have both users and roles
710
+ // Discord returns resolved data for mentionables
711
+ fields[customId] = {
712
+ values: component.values || [],
713
+ users: component.resolved?.users ? Object.keys(component.resolved.users) : [],
714
+ roles: component.resolved?.roles ? Object.keys(component.resolved.roles) : []
715
+ };
716
+ break;
717
+
718
+ case 8: // Channel Select
719
+ fields[customId] = component.values || [];
720
+ break;
721
+
722
+ case 19: // File Upload
723
+ // File uploads come through as attachments
724
+ fields[customId] = component.attachments || [];
725
+ break;
726
+ }
727
+ }
728
+ }
729
+
358
730
  /**
359
731
  * Destroy all active instances of this component
360
732
  * Useful for cleanup when the bot shuts down or component is unloaded