@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
@@ -1,7 +1,7 @@
1
1
  import { NamespaceEnums } from "../../../../generated/namespaceData";
2
2
  import { DBI } from "../../../DBI";
3
3
  import { DBIBaseInteraction, IDBIBaseExecuteCtx, TDBIReferencedData } from "../../Interaction";
4
- export type TDBIHTMLComponentsV2Omitted<TNamespace extends NamespaceEnums> = Omit<DBIHTMLComponentsV2<TNamespace>, "type" | "dbi" | "toJSON" | "description" | "send" | "destroy" | "destroyAll"> & {
4
+ export type TDBIHTMLComponentsV2Omitted<TNamespace extends NamespaceEnums> = Omit<DBIHTMLComponentsV2<TNamespace>, "type" | "dbi" | "toJSON" | "description" | "send" | "destroy" | "destroyAll" | "__lastRenderModals__" | "_pendingModals" | "_initPromise"> & {
5
5
  /**
6
6
  * Use 'svelte' for Svelte 5 components, 'eta' for Eta templates (default)
7
7
  */
@@ -34,9 +34,23 @@ export declare class DBIHTMLComponentsV2<TNamespace extends NamespaceEnums> exte
34
34
  private svelteComponentInfo;
35
35
  private _userOnExecute?;
36
36
  private _activeContexts;
37
+ _pendingModals: Map<string, {
38
+ resolve: (result: any) => void;
39
+ reject: (error: any) => void;
40
+ }>;
41
+ private _initPromise;
37
42
  constructor(dbi: DBI<TNamespace>, args: TDBIHTMLComponentsV2Omitted<TNamespace>);
38
43
  private _initSvelteComponent;
39
44
  private _handleExecute;
45
+ /**
46
+ * Execute a modal submit handler
47
+ */
48
+ private _executeModalSubmit;
49
+ /**
50
+ * Execute an element handler (button, select, etc.)
51
+ */
52
+ private _executeElementHandler;
53
+ __lastRenderModals__: Map<string, any> | null;
40
54
  toJSON(arg?: TDBIHTMLComponentsV2ToJSONArgs): Promise<any>;
41
55
  /**
42
56
  * Send the component to an interaction or channel and initialize lifecycle hooks (onMount)
@@ -80,6 +94,24 @@ export declare class DBIHTMLComponentsV2<TNamespace extends NamespaceEnums> exte
80
94
  * ```
81
95
  */
82
96
  destroy(refOrData: string | Record<string, any>): boolean;
97
+ /**
98
+ * Extract all field values from a modal interaction
99
+ * Supports text inputs, select menus (string, user, role, mentionable, channel), and file uploads
100
+ *
101
+ * Returns an object where keys are the custom_id of each component and values are:
102
+ * - For text inputs: string value
103
+ * - For string selects: string[] of selected values
104
+ * - For user selects: string[] of user IDs
105
+ * - For role selects: string[] of role IDs
106
+ * - For mentionable selects: { users: string[], roles: string[] }
107
+ * - For channel selects: string[] of channel IDs
108
+ * - For file uploads: attachment objects array
109
+ */
110
+ private _extractModalFields;
111
+ /**
112
+ * Recursively extract field values from component structure
113
+ */
114
+ private _extractFieldsFromComponents;
83
115
  /**
84
116
  * Destroy all active instances of this component
85
117
  * Useful for cleanup when the bot shuts down or component is unloaded
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/types/Components/HTMLComponentsV2/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,qCAAqC,CAAC;AACrE,OAAO,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AACnC,OAAO,EAAE,kBAAkB,EAAgB,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAM7G,MAAM,MAAM,2BAA2B,CAAC,UAAU,SAAS,cAAc,IAAI,IAAI,CAAC,mBAAmB,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,aAAa,GAAG,MAAM,GAAG,SAAS,GAAG,YAAY,CAAC,GAAG;IAClM;;OAEG;IACH,IAAI,CAAC,EAAE,QAAQ,GAAG,KAAK,CAAC;IACxB;;OAEG;IACH,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,8BAA8B,CAAC,UAAU,CAAC,KAAK,IAAI,CAAC;CACvE,CAAC;AAEF,MAAM,MAAM,8BAA8B,GAAG;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAC5B,CAAA;AAED,MAAM,WAAW,+BAA+B;IAC9C,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC3B,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,8FAA8F;IAC9F,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,8BAA8B,CAAC,UAAU,SAAS,cAAc,CAAE,SAAQ,kBAAkB,CAAC,UAAU,CAAC;IACvH,IAAI,EAAE,kBAAkB,EAAE,CAAC;CAC5B;AAED,qBAAa,mBAAmB,CAAC,UAAU,SAAS,cAAc,CAAE,SAAQ,kBAAkB,CAAC,UAAU,CAAC;IACxG,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,QAAQ,GAAG,KAAK,CAAS;IAC/B,OAAO,CAAC,mBAAmB,CAAa;IACxC,OAAO,CAAC,cAAc,CAAC,CAA4D;IAInF,OAAO,CAAC,eAAe,CAA+B;gBAE1C,GAAG,EAAE,GAAG,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,2BAA2B,CAAC,UAAU,CAAC;YAiCjE,oBAAoB;IAMlC,OAAO,CAAC,cAAc;IA0GP,MAAM,CAAC,GAAG,GAAE,8BAAmC,GAAG,OAAO,CAAC,GAAG,CAAC;IA4B7E;;;;;;;;;;;;;;;;;;;;OAoBG;IACG,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,OAAO,GAAE,+BAAoC,GAAG,OAAO,CAAC,GAAG,CAAC;IAyEpF;;;;;;;;;;;;;;;;;;OAkBG;IACH,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,OAAO;IAsBzD;;;;;OAKG;IACH,UAAU,IAAI,MAAM;IAapB,QAAQ,CAAC,EAAE,GAAG,EAAE,CAAM;CACvB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/types/Components/HTMLComponentsV2/index.ts"],"names":[],"mappings":"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"}
@@ -7,6 +7,151 @@ const parser_1 = require("./parser");
7
7
  const svelteRenderer_1 = require("./svelteRenderer");
8
8
  const svelteParser_1 = require("./svelteParser");
9
9
  const fs_1 = tslib_1.__importDefault(require("fs"));
10
+ /**
11
+ * Parse Discord API error and provide helpful context about which HTML element caused the error
12
+ */
13
+ function parseDiscordAPIError(error, source, componentName) {
14
+ // Check if it's a Discord API error with form body issues
15
+ const message = error.message || '';
16
+ const rawError = error.rawError || error;
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
+ if (!pathMatch) {
20
+ return error; // Not a parseable Discord API error
21
+ }
22
+ const errorPath = pathMatch[1];
23
+ const errorCode = message.match(/\[([A-Z_]+)\]/)?.[1] || 'UNKNOWN';
24
+ // Parse the path to understand the component structure
25
+ // e.g., "[0].components[0].accessory.media.url" -> component index 0, child 0, accessory.media.url
26
+ const parts = errorPath.split(/\.|\[|\]/).filter(Boolean);
27
+ // Build a human-readable path description
28
+ let description = '';
29
+ let elementHint = '';
30
+ let currentPath = [];
31
+ for (let i = 0; i < parts.length; i++) {
32
+ const part = parts[i];
33
+ if (!isNaN(Number(part))) {
34
+ currentPath.push(`[${part}]`);
35
+ }
36
+ else {
37
+ currentPath.push(part);
38
+ // Provide hints based on known Discord component structure
39
+ if (part === 'accessory') {
40
+ elementHint = '<section> element\'s accessory (thumbnail/button)';
41
+ }
42
+ else if (part === 'media') {
43
+ elementHint = '<thumbnail> or <media-gallery> element';
44
+ }
45
+ else if (part === 'url') {
46
+ elementHint = 'media URL attribute';
47
+ }
48
+ else if (part === 'components') {
49
+ // Skip, it's container
50
+ }
51
+ else if (part === 'content') {
52
+ elementHint = 'text content of a <text-display> element';
53
+ }
54
+ else if (part === 'label') {
55
+ elementHint = 'label attribute of a <button> or <option>';
56
+ }
57
+ else if (part === 'custom_id') {
58
+ elementHint = 'name attribute of an interactive element';
59
+ }
60
+ else if (part === 'placeholder') {
61
+ elementHint = 'placeholder attribute of a select menu';
62
+ }
63
+ else if (part === 'title') {
64
+ elementHint = 'title attribute of a <modal> or <section>';
65
+ }
66
+ else if (part === 'options') {
67
+ elementHint = '<option> elements inside a <string-select>';
68
+ }
69
+ else if (part === 'value') {
70
+ elementHint = 'value attribute of an <option> or <text-input>';
71
+ }
72
+ else if (part === 'description') {
73
+ elementHint = 'description attribute of an <option>';
74
+ }
75
+ }
76
+ }
77
+ // Map error codes to helpful messages
78
+ const errorMessages = {
79
+ 'BASE_TYPE_REQUIRED': 'This field is required but was empty or undefined',
80
+ 'STRING_TYPE_REQUIRED': 'This field must be a string',
81
+ 'NUMBER_TYPE_REQUIRED': 'This field must be a number',
82
+ 'BOOLEAN_TYPE_REQUIRED': 'This field must be a boolean',
83
+ 'INVALID_URL': 'The URL provided is not valid',
84
+ 'MAX_LENGTH': 'The value exceeds the maximum allowed length',
85
+ 'MIN_LENGTH': 'The value is shorter than the minimum required length',
86
+ 'CHOICE_NOT_FOUND': 'The selected value is not in the list of options',
87
+ };
88
+ const errorExplanation = errorMessages[errorCode] || `Error code: ${errorCode}`;
89
+ // Build the enhanced error message
90
+ let enhancedMessage = `
91
+ ╔══════════════════════════════════════════════════════════════╗
92
+ ║ Discord API Error - Invalid Component Data ║
93
+ ╚══════════════════════════════════════════════════════════════╝
94
+
95
+ 📍 Component: ${componentName || 'unknown'}
96
+ 📍 Error Path: data.components${errorPath}
97
+ 📍 Error Code: ${errorCode}
98
+
99
+ ❌ ${errorExplanation}
100
+ ${elementHint ? `\n💡 This error is likely in: ${elementHint}` : ''}
101
+
102
+ 🔍 What to check:
103
+ `;
104
+ // Add specific suggestions based on error type
105
+ if (errorPath.includes('media.url')) {
106
+ enhancedMessage += ` • Make sure the image/media URL is provided and valid
107
+ • Check that the data property used for the image exists
108
+ • Example: <thumbnail media={product?.image}> - is product.image defined?
109
+ `;
110
+ }
111
+ else if (errorPath.includes('accessory')) {
112
+ enhancedMessage += ` • The <section> element requires valid content in its accessory
113
+ • If using <thumbnail>, ensure the media URL is valid
114
+ `;
115
+ }
116
+ else if (errorPath.includes('content')) {
117
+ enhancedMessage += ` • Text content cannot be empty or undefined
118
+ • Check your template expressions like {variable?.property}
119
+ `;
120
+ }
121
+ else if (errorPath.includes('options')) {
122
+ enhancedMessage += ` • Select menu options must have valid value and label
123
+ • Each <option> needs: value="..." and text content
124
+ `;
125
+ }
126
+ else if (errorPath.includes('label') || errorPath.includes('custom_id')) {
127
+ enhancedMessage += ` • Interactive elements (buttons, selects) need valid labels/names
128
+ • Check that text content and name attributes are not empty
129
+ `;
130
+ }
131
+ // If we have source, try to highlight relevant section
132
+ if (source && elementHint) {
133
+ const elementType = elementHint.match(/<(\w+-?\w*)>/)?.[1];
134
+ if (elementType) {
135
+ const elementRegex = new RegExp(`<${elementType}[^>]*>`, 'g');
136
+ const matches = source.match(elementRegex);
137
+ if (matches && matches.length > 0) {
138
+ enhancedMessage += `\n📝 Found ${matches.length} <${elementType}> element(s) in template:`;
139
+ matches.slice(0, 3).forEach((m, i) => {
140
+ enhancedMessage += `\n ${i + 1}. ${m.substring(0, 80)}${m.length > 80 ? '...' : ''}`;
141
+ });
142
+ if (matches.length > 3) {
143
+ enhancedMessage += `\n ... and ${matches.length - 3} more`;
144
+ }
145
+ }
146
+ }
147
+ }
148
+ const enhancedError = new Error(enhancedMessage);
149
+ enhancedError.originalError = error;
150
+ enhancedError.type = 'discord-api-error';
151
+ enhancedError.path = errorPath;
152
+ enhancedError.code = errorCode;
153
+ return enhancedError;
154
+ }
10
155
  class DBIHTMLComponentsV2 extends Interaction_1.DBIBaseInteraction {
11
156
  template;
12
157
  file;
@@ -16,6 +161,11 @@ class DBIHTMLComponentsV2 extends Interaction_1.DBIBaseInteraction {
16
161
  // Store handler contexts per ref for lifecycle management
17
162
  // Key: ref id, Value: handlerContext with lifecycle hooks
18
163
  _activeContexts = new Map();
164
+ // Store pending modal promises for await showModal() support
165
+ // Key: modal customId, Value: { resolve, reject } functions
166
+ _pendingModals = new Map();
167
+ // Track initialization promise
168
+ _initPromise = null;
19
169
  constructor(dbi, args) {
20
170
  // Store user's onExecute callback before passing to super
21
171
  const userOnExecute = args.onExecute;
@@ -37,8 +187,8 @@ class DBIHTMLComponentsV2 extends Interaction_1.DBIBaseInteraction {
37
187
  }
38
188
  // Pre-extract Svelte handlers at registration time (lazy loaded)
39
189
  if (this.mode === 'svelte' && this.template) {
40
- // Defer the parsing to avoid sync import issues
41
- this._initSvelteComponent();
190
+ // Store the promise so we can await it in _handleExecute
191
+ this._initPromise = this._initSvelteComponent();
42
192
  }
43
193
  // Re-assign onExecute method after super() call because parent class sets it to undefined
44
194
  this.onExecute = this._handleExecute.bind(this);
@@ -48,7 +198,11 @@ class DBIHTMLComponentsV2 extends Interaction_1.DBIBaseInteraction {
48
198
  this.svelteComponentInfo = await (0, svelteParser_1.parseSvelteComponent)(this.template);
49
199
  }
50
200
  }
51
- _handleExecute(ctx) {
201
+ async _handleExecute(ctx) {
202
+ // Wait for Svelte component initialization if not yet completed
203
+ if (this._initPromise) {
204
+ await this._initPromise;
205
+ }
52
206
  // Call user's onExecute callback first if provided
53
207
  if (this._userOnExecute) {
54
208
  this._userOnExecute(ctx);
@@ -57,83 +211,158 @@ class DBIHTMLComponentsV2 extends Interaction_1.DBIBaseInteraction {
57
211
  if (this.mode === 'svelte' && this.svelteComponentInfo) {
58
212
  const [elementName, ...handlerData] = ctx.data;
59
213
  if (typeof elementName === 'string') {
60
- // Find the handler info for this element
214
+ // Check if this is a modal submit (elementName is the modal id)
215
+ const modalHandlerInfo = this.svelteComponentInfo.modalHandlers.get(elementName);
216
+ if (modalHandlerInfo) {
217
+ // This is a modal submit - execute the onsubmit handler (or just resolve promise if no handler)
218
+ this._executeModalSubmit(ctx, elementName, modalHandlerInfo.onsubmitHandler, handlerData);
219
+ return;
220
+ }
221
+ // Find the handler info for this element (button, select, etc.)
61
222
  const handlerInfo = this.svelteComponentInfo.handlers.get(elementName);
62
223
  if (handlerInfo) {
63
- // Extract current state from handlerData (refs that were passed)
64
- // The second element in data array contains the current state
65
- const currentState = handlerData[0] || {};
66
- // Get ref id for lifecycle tracking (if available)
67
- const refId = currentState?.$ref || null;
68
- // Check if this is first execution for this ref
69
- const isFirstExecution = refId && !this._activeContexts.has(refId);
70
- // Get existing context if any (to preserve lifecycle callbacks like intervals)
71
- const existingContext = refId ? this._activeContexts.get(refId) : null;
72
- // Create a NEW handler context for each execution with the current state
73
- // This ensures each interaction has its own isolated state
74
- // Pass 'this' so handlers can access component methods like toJSON()
75
- // Pass ctx so handlers can access ctx.interaction, ctx.data, ctx.locale, etc.
76
- const handlerContext = (0, svelteParser_1.createHandlerContext)(this.svelteComponentInfo.scriptContent, typeof currentState === 'object' ? currentState : {}, this, ctx);
77
- const handlerFn = handlerContext.handlers[handlerInfo.handlerName];
78
- if (handlerFn && typeof handlerFn === 'function') {
79
- try {
80
- // Store context for lifecycle management
81
- if (refId) {
82
- // If there's an existing context, transfer its destroy callbacks to the new context
83
- // This preserves intervals/timers created in onMount
84
- if (existingContext && existingContext.destroyCallbacks) {
85
- // Merge existing destroy callbacks (don't run them!)
86
- handlerContext.destroyCallbacks.push(...existingContext.destroyCallbacks);
87
- }
88
- this._activeContexts.set(refId, handlerContext);
89
- // Wrap $unRef to call onDestroy when ref is deleted (only wrap once)
90
- const stateObj = currentState;
91
- if (stateObj.$unRef && !stateObj.__unRefWrapped__) {
92
- const originalUnRef = stateObj.$unRef.bind(stateObj);
93
- stateObj.$unRef = () => {
94
- // Run destroy callbacks before unref
95
- const ctx = this._activeContexts.get(refId);
96
- if (ctx && ctx.runDestroy) {
97
- ctx.runDestroy();
98
- }
99
- this._activeContexts.delete(refId);
100
- return originalUnRef();
101
- };
102
- stateObj.__unRefWrapped__ = true;
103
- }
104
- }
105
- // Run onMount callbacks only on first execution
106
- if (isFirstExecution && handlerContext.runMount) {
107
- handlerContext.runMount();
108
- }
109
- // Mark that we're inside handler execution (prevents auto-render during handler)
110
- handlerContext.setInHandler(true);
111
- try {
112
- // Bind 'this' to the DBIHTMLComponentsV2 instance so handlers can use this.toJSON()
113
- // Pass wrappedCtx so handlers use the proxy-wrapped ctx that tracks interaction calls
114
- // This ensures __asyncInteractionCalled__ flag is set when handler calls ctx.interaction.reply() etc.
115
- handlerFn.call(this, handlerContext.wrappedCtx, ...handlerData.slice(1));
116
- }
117
- finally {
118
- // Always reset handler execution flag
119
- handlerContext.setInHandler(false);
120
- }
121
- // Run effects after handler execution (state may have changed)
122
- handlerContext.runEffects();
123
- // If there are pending data changes and no interaction method was called,
124
- // flush the render now (synchronously uses interaction.update)
125
- if (handlerContext.hasPendingRender()) {
126
- handlerContext.flushRender();
224
+ this._executeElementHandler(ctx, handlerInfo, handlerData);
225
+ }
226
+ }
227
+ }
228
+ }
229
+ /**
230
+ * Execute a modal submit handler
231
+ */
232
+ _executeModalSubmit(ctx, modalId, handlerName, handlerData) {
233
+ // Extract current state from handlerData (refs that were passed)
234
+ const currentState = handlerData[0] || {};
235
+ // Get ref id for lifecycle tracking (if available)
236
+ const refId = currentState?.$ref || null;
237
+ // Extract all field values from modal interaction (text inputs, selects, file uploads, etc.)
238
+ const modalInteraction = ctx.interaction;
239
+ const fields = this._extractModalFields(modalInteraction);
240
+ // Check if there's a pending promise for this modal (from await showModal())
241
+ // Use the modal's customId to find the pending promise
242
+ const pendingKey = modalInteraction.customId;
243
+ const pendingModal = this._pendingModals.get(pendingKey);
244
+ if (pendingModal) {
245
+ // Resolve the promise with fields and interaction
246
+ pendingModal.resolve({
247
+ fields,
248
+ interaction: modalInteraction,
249
+ ctx: ctx
250
+ });
251
+ this._pendingModals.delete(pendingKey);
252
+ }
253
+ // If no onsubmit handler defined, just return (promise-based usage)
254
+ if (!handlerName) {
255
+ return;
256
+ }
257
+ // Create handler context for modal submit
258
+ const handlerContext = (0, svelteParser_1.createHandlerContext)(this.svelteComponentInfo.scriptContent, typeof currentState === 'object' ? currentState : {}, this, ctx);
259
+ const handlerFn = handlerContext.handlers[handlerName];
260
+ if (handlerFn && typeof handlerFn === 'function') {
261
+ try {
262
+ // Store context for lifecycle management
263
+ if (refId) {
264
+ const existingContext = this._activeContexts.get(refId);
265
+ if (existingContext && existingContext.destroyCallbacks) {
266
+ handlerContext.destroyCallbacks.push(...existingContext.destroyCallbacks);
267
+ }
268
+ this._activeContexts.set(refId, handlerContext);
269
+ }
270
+ handlerContext.setInHandler(true);
271
+ try {
272
+ // Call handler with ctx and fields object
273
+ handlerFn.call(this, handlerContext.wrappedCtx, fields, ...handlerData.slice(1));
274
+ }
275
+ finally {
276
+ handlerContext.setInHandler(false);
277
+ }
278
+ handlerContext.runEffects();
279
+ if (handlerContext.hasPendingRender()) {
280
+ handlerContext.flushRender();
281
+ }
282
+ }
283
+ catch (error) {
284
+ // Modal handler execution failed
285
+ }
286
+ }
287
+ }
288
+ /**
289
+ * Execute an element handler (button, select, etc.)
290
+ */
291
+ _executeElementHandler(ctx, handlerInfo, handlerData) {
292
+ // Extract current state from handlerData (refs that were passed)
293
+ // The second element in data array contains the current state
294
+ const currentState = handlerData[0] || {};
295
+ // Get ref id for lifecycle tracking (if available)
296
+ const refId = currentState?.$ref || null;
297
+ // Check if this is first execution for this ref
298
+ const isFirstExecution = refId && !this._activeContexts.has(refId);
299
+ // Get existing context if any (to preserve lifecycle callbacks like intervals)
300
+ const existingContext = refId ? this._activeContexts.get(refId) : null;
301
+ // Create a NEW handler context for each execution with the current state
302
+ // This ensures each interaction has its own isolated state
303
+ // Pass 'this' so handlers can access component methods like toJSON()
304
+ // Pass ctx so handlers can access ctx.interaction, ctx.data, ctx.locale, etc.
305
+ const handlerContext = (0, svelteParser_1.createHandlerContext)(this.svelteComponentInfo.scriptContent, typeof currentState === 'object' ? currentState : {}, this, ctx);
306
+ const handlerFn = handlerContext.handlers[handlerInfo.handlerName];
307
+ if (handlerFn && typeof handlerFn === 'function') {
308
+ try {
309
+ // Store context for lifecycle management
310
+ if (refId) {
311
+ // If there's an existing context, transfer its destroy callbacks to the new context
312
+ // This preserves intervals/timers created in onMount
313
+ if (existingContext && existingContext.destroyCallbacks) {
314
+ // Merge existing destroy callbacks (don't run them!)
315
+ handlerContext.destroyCallbacks.push(...existingContext.destroyCallbacks);
316
+ }
317
+ this._activeContexts.set(refId, handlerContext);
318
+ // Wrap $unRef to call onDestroy when ref is deleted (only wrap once)
319
+ const stateObj = currentState;
320
+ if (stateObj.$unRef && !stateObj.__unRefWrapped__) {
321
+ const originalUnRef = stateObj.$unRef.bind(stateObj);
322
+ stateObj.$unRef = () => {
323
+ // Run destroy callbacks before unref
324
+ const ctx = this._activeContexts.get(refId);
325
+ if (ctx && ctx.runDestroy) {
326
+ ctx.runDestroy();
127
327
  }
128
- }
129
- catch (error) {
130
- // Handler execution failed
131
- }
328
+ this._activeContexts.delete(refId);
329
+ return originalUnRef();
330
+ };
331
+ stateObj.__unRefWrapped__ = true;
132
332
  }
133
333
  }
334
+ // Run onMount callbacks only on first execution
335
+ if (isFirstExecution && handlerContext.runMount) {
336
+ handlerContext.runMount();
337
+ }
338
+ // Mark that we're inside handler execution (prevents auto-render during handler)
339
+ handlerContext.setInHandler(true);
340
+ try {
341
+ // Bind 'this' to the DBIHTMLComponentsV2 instance so handlers can use this.toJSON()
342
+ // Pass wrappedCtx so handlers use the proxy-wrapped ctx that tracks interaction calls
343
+ // This ensures __asyncInteractionCalled__ flag is set when handler calls ctx.interaction.reply() etc.
344
+ handlerFn.call(this, handlerContext.wrappedCtx, ...handlerData.slice(1));
345
+ }
346
+ finally {
347
+ // Always reset handler execution flag
348
+ handlerContext.setInHandler(false);
349
+ }
350
+ // Run effects after handler execution (state may have changed)
351
+ handlerContext.runEffects();
352
+ // If there are pending data changes and no interaction method was called,
353
+ // flush the render now (synchronously uses interaction.update)
354
+ if (handlerContext.hasPendingRender()) {
355
+ handlerContext.flushRender();
356
+ }
357
+ }
358
+ catch (error) {
359
+ // Handler execution failed
134
360
  }
135
361
  }
136
362
  }
363
+ // Store last render's modals for showModal() to access
364
+ // Note: Used internally by handler context, not truly private
365
+ __lastRenderModals__ = null;
137
366
  async toJSON(arg = {}) {
138
367
  if (this.mode === 'svelte' && this.template) {
139
368
  // Render Svelte component
@@ -141,6 +370,8 @@ class DBIHTMLComponentsV2 extends Interaction_1.DBIBaseInteraction {
141
370
  data: arg.data,
142
371
  ttl: this.ttl
143
372
  });
373
+ // Store modals for showModal() to access
374
+ this.__lastRenderModals__ = result.modals;
144
375
  return result.components;
145
376
  }
146
377
  else {
@@ -173,8 +404,12 @@ class DBIHTMLComponentsV2 extends Interaction_1.DBIBaseInteraction {
173
404
  * ```
174
405
  */
175
406
  async send(target, options = {}) {
407
+ // Wait for Svelte component initialization if not yet completed
408
+ if (this._initPromise) {
409
+ await this._initPromise;
410
+ }
176
411
  const { data = {}, flags = ["IsComponentsV2"], content, ephemeral, reply, followUp } = options;
177
- // Render components (toJSON is async)
412
+ // Render components (toJSON is async) - this also creates $ref in data if not present
178
413
  const components = await this.toJSON({ data });
179
414
  // Build message options
180
415
  const messageOptions = { components, flags };
@@ -186,21 +421,32 @@ class DBIHTMLComponentsV2 extends Interaction_1.DBIBaseInteraction {
186
421
  let message;
187
422
  const isInteraction = target.reply && target.user; // Interactions have both reply method and user property
188
423
  const isChannel = target.send && !target.user; // Channels have send but no user
189
- if (isInteraction) {
190
- if (followUp) {
191
- message = await target.followUp(messageOptions);
424
+ try {
425
+ if (isInteraction) {
426
+ if (followUp) {
427
+ message = await target.followUp(messageOptions);
428
+ }
429
+ else {
430
+ message = await target.reply(messageOptions);
431
+ }
432
+ }
433
+ else if (isChannel) {
434
+ message = await target.send(messageOptions);
192
435
  }
193
436
  else {
194
- message = await target.reply(messageOptions);
437
+ throw new Error("Invalid target: must be an interaction or channel");
195
438
  }
196
439
  }
197
- else if (isChannel) {
198
- message = await target.send(messageOptions);
199
- }
200
- else {
201
- throw new Error("Invalid target: must be an interaction or channel");
440
+ catch (error) {
441
+ // Check if it's a Discord API error and enhance it with helpful context
442
+ if (error.code || error.rawError || (error.message && error.message.includes('Invalid Form Body'))) {
443
+ const source = this.file ? fs_1.default.readFileSync(this.file, 'utf-8') : this.template;
444
+ throw parseDiscordAPIError(error, source, this.name);
445
+ }
446
+ throw error;
202
447
  }
203
448
  // If Svelte mode, create initial handler context and run onMount
449
+ // After toJSON, data.$ref is guaranteed to exist (created in renderSvelteComponent)
204
450
  if (this.mode === 'svelte' && this.svelteComponentInfo && data.$ref) {
205
451
  const refId = data.$ref;
206
452
  // Create handler context with a fake ctx that has the message
@@ -268,6 +514,86 @@ class DBIHTMLComponentsV2 extends Interaction_1.DBIBaseInteraction {
268
514
  this.dbi.data.refs.delete(refId);
269
515
  return true;
270
516
  }
517
+ /**
518
+ * Extract all field values from a modal interaction
519
+ * Supports text inputs, select menus (string, user, role, mentionable, channel), and file uploads
520
+ *
521
+ * Returns an object where keys are the custom_id of each component and values are:
522
+ * - For text inputs: string value
523
+ * - For string selects: string[] of selected values
524
+ * - For user selects: string[] of user IDs
525
+ * - For role selects: string[] of role IDs
526
+ * - For mentionable selects: { users: string[], roles: string[] }
527
+ * - For channel selects: string[] of channel IDs
528
+ * - For file uploads: attachment objects array
529
+ */
530
+ _extractModalFields(modalInteraction) {
531
+ const fields = {};
532
+ // Handle classic text input fields via ModalSubmitFields
533
+ if (modalInteraction.fields && modalInteraction.fields.fields) {
534
+ for (const [customId, field] of modalInteraction.fields.fields) {
535
+ // Text input - field.value is the string
536
+ fields[customId] = field.value;
537
+ }
538
+ }
539
+ // Handle new modal components from interaction.data.components
540
+ // The new modal structure uses Label wrappers with nested components
541
+ const data = modalInteraction.data || modalInteraction.data;
542
+ if (data && data.components) {
543
+ this._extractFieldsFromComponents(data.components, fields);
544
+ }
545
+ return fields;
546
+ }
547
+ /**
548
+ * Recursively extract field values from component structure
549
+ */
550
+ _extractFieldsFromComponents(components, fields) {
551
+ for (const component of components) {
552
+ const type = component.type;
553
+ const customId = component.custom_id;
554
+ // Type 18 = Label - has nested component
555
+ if (type === 18 && component.component) {
556
+ this._extractFieldsFromComponents([component.component], fields);
557
+ continue;
558
+ }
559
+ // Type 1 = Action Row - has nested components array
560
+ if (type === 1 && component.components) {
561
+ this._extractFieldsFromComponents(component.components, fields);
562
+ continue;
563
+ }
564
+ if (!customId)
565
+ continue;
566
+ switch (type) {
567
+ case 4: // Text Input
568
+ fields[customId] = component.value || '';
569
+ break;
570
+ case 3: // String Select
571
+ fields[customId] = component.values || [];
572
+ break;
573
+ case 5: // User Select
574
+ fields[customId] = component.values || [];
575
+ break;
576
+ case 6: // Role Select
577
+ fields[customId] = component.values || [];
578
+ break;
579
+ case 7: // Mentionable Select - can have both users and roles
580
+ // Discord returns resolved data for mentionables
581
+ fields[customId] = {
582
+ values: component.values || [],
583
+ users: component.resolved?.users ? Object.keys(component.resolved.users) : [],
584
+ roles: component.resolved?.roles ? Object.keys(component.resolved.roles) : []
585
+ };
586
+ break;
587
+ case 8: // Channel Select
588
+ fields[customId] = component.values || [];
589
+ break;
590
+ case 19: // File Upload
591
+ // File uploads come through as attachments
592
+ fields[customId] = component.attachments || [];
593
+ break;
594
+ }
595
+ }
596
+ }
271
597
  /**
272
598
  * Destroy all active instances of this component
273
599
  * Useful for cleanup when the bot shuts down or component is unloaded