@mostfeatured/dbi 0.2.14 → 0.2.16
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.
- package/dist/src/types/Components/HTMLComponentsV2/index.d.ts +35 -1
- package/dist/src/types/Components/HTMLComponentsV2/index.d.ts.map +1 -1
- package/dist/src/types/Components/HTMLComponentsV2/index.js +416 -83
- package/dist/src/types/Components/HTMLComponentsV2/index.js.map +1 -1
- package/dist/src/types/Components/HTMLComponentsV2/parser.d.ts +52 -0
- package/dist/src/types/Components/HTMLComponentsV2/parser.d.ts.map +1 -1
- package/dist/src/types/Components/HTMLComponentsV2/parser.js +275 -0
- package/dist/src/types/Components/HTMLComponentsV2/parser.js.map +1 -1
- package/dist/src/types/Components/HTMLComponentsV2/svelteParser.d.ts +28 -1
- package/dist/src/types/Components/HTMLComponentsV2/svelteParser.d.ts.map +1 -1
- package/dist/src/types/Components/HTMLComponentsV2/svelteParser.js +478 -34
- package/dist/src/types/Components/HTMLComponentsV2/svelteParser.js.map +1 -1
- package/dist/src/types/Components/HTMLComponentsV2/svelteRenderer.d.ts +12 -0
- package/dist/src/types/Components/HTMLComponentsV2/svelteRenderer.d.ts.map +1 -1
- package/dist/src/types/Components/HTMLComponentsV2/svelteRenderer.js +102 -18
- package/dist/src/types/Components/HTMLComponentsV2/svelteRenderer.js.map +1 -1
- package/dist/test/index.js +76 -3
- package/dist/test/index.js.map +1 -1
- package/dist/test/test.d.ts +2 -0
- package/dist/test/test.d.ts.map +1 -0
- package/dist/test/test.js +7 -0
- package/dist/test/test.js.map +1 -0
- package/docs/ADVANCED_FEATURES.md +4 -0
- package/docs/API_REFERENCE.md +4 -0
- package/docs/CHAT_INPUT.md +4 -0
- package/docs/COMPONENTS.md +4 -0
- package/docs/EVENTS.md +4 -0
- package/docs/GETTING_STARTED.md +4 -0
- package/docs/LOCALIZATION.md +4 -0
- package/docs/README.md +4 -0
- package/docs/SVELTE_COMPONENTS.md +162 -6
- package/docs/llm/ADVANCED_FEATURES.txt +521 -0
- package/docs/llm/API_REFERENCE.txt +659 -0
- package/docs/llm/CHAT_INPUT.txt +514 -0
- package/docs/llm/COMPONENTS.txt +595 -0
- package/docs/llm/EVENTS.txt +449 -0
- package/docs/llm/GETTING_STARTED.txt +296 -0
- package/docs/llm/LOCALIZATION.txt +501 -0
- package/docs/llm/README.txt +193 -0
- package/docs/llm/SVELTE_COMPONENTS.txt +566 -0
- package/generated/svelte-dbi.d.ts +122 -0
- package/package.json +1 -1
- package/src/types/Components/HTMLComponentsV2/index.ts +478 -95
- package/src/types/Components/HTMLComponentsV2/parser.ts +317 -0
- package/src/types/Components/HTMLComponentsV2/svelteParser.ts +536 -35
- package/src/types/Components/HTMLComponentsV2/svelteRenderer.ts +121 -20
- package/test/index.ts +76 -3
- package/test/product-showcase.svelte +383 -24
- package/test/test.ts +3 -0
- 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
|
-
|
|
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
|
*/
|
|
@@ -42,11 +186,20 @@ export class DBIHTMLComponentsV2<TNamespace extends NamespaceEnums> extends DBIB
|
|
|
42
186
|
mode: 'svelte' | 'eta' = 'eta';
|
|
43
187
|
private svelteComponentInfo: any = null;
|
|
44
188
|
private _userOnExecute?: (ctx: IDBIHTMLComponentsV2ExecuteCtx<TNamespace>) => void;
|
|
189
|
+
/** The directory of the source file (used for resolving relative imports) */
|
|
190
|
+
private _sourceDir?: string;
|
|
45
191
|
|
|
46
192
|
// Store handler contexts per ref for lifecycle management
|
|
47
193
|
// Key: ref id, Value: handlerContext with lifecycle hooks
|
|
48
194
|
private _activeContexts: Map<string, any> = new Map();
|
|
49
195
|
|
|
196
|
+
// Store pending modal promises for await showModal() support
|
|
197
|
+
// Key: modal customId, Value: { resolve, reject } functions
|
|
198
|
+
_pendingModals: Map<string, { resolve: (result: any) => void; reject: (error: any) => void }> = new Map();
|
|
199
|
+
|
|
200
|
+
// Track initialization promise
|
|
201
|
+
private _initPromise: Promise<void> | null = null;
|
|
202
|
+
|
|
50
203
|
constructor(dbi: DBI<TNamespace>, args: TDBIHTMLComponentsV2Omitted<TNamespace>) {
|
|
51
204
|
// Store user's onExecute callback before passing to super
|
|
52
205
|
const userOnExecute = (args as any).onExecute;
|
|
@@ -65,6 +218,12 @@ export class DBIHTMLComponentsV2<TNamespace extends NamespaceEnums> extends DBIB
|
|
|
65
218
|
this.handlers = args.handlers || [];
|
|
66
219
|
this.mode = args.mode || 'eta';
|
|
67
220
|
|
|
221
|
+
// Store source directory for resolving relative imports in Svelte components
|
|
222
|
+
if (this.file) {
|
|
223
|
+
const path = require("path");
|
|
224
|
+
this._sourceDir = path.dirname(path.resolve(this.file));
|
|
225
|
+
}
|
|
226
|
+
|
|
68
227
|
// Store user's onExecute callback if provided
|
|
69
228
|
if (userOnExecute) {
|
|
70
229
|
this._userOnExecute = userOnExecute;
|
|
@@ -72,8 +231,8 @@ export class DBIHTMLComponentsV2<TNamespace extends NamespaceEnums> extends DBIB
|
|
|
72
231
|
|
|
73
232
|
// Pre-extract Svelte handlers at registration time (lazy loaded)
|
|
74
233
|
if (this.mode === 'svelte' && this.template) {
|
|
75
|
-
//
|
|
76
|
-
this._initSvelteComponent();
|
|
234
|
+
// Store the promise so we can await it in _handleExecute
|
|
235
|
+
this._initPromise = this._initSvelteComponent();
|
|
77
236
|
}
|
|
78
237
|
|
|
79
238
|
// Re-assign onExecute method after super() call because parent class sets it to undefined
|
|
@@ -86,7 +245,12 @@ export class DBIHTMLComponentsV2<TNamespace extends NamespaceEnums> extends DBIB
|
|
|
86
245
|
}
|
|
87
246
|
}
|
|
88
247
|
|
|
89
|
-
private _handleExecute(ctx: IDBIHTMLComponentsV2ExecuteCtx<TNamespace>) {
|
|
248
|
+
private async _handleExecute(ctx: IDBIHTMLComponentsV2ExecuteCtx<TNamespace>) {
|
|
249
|
+
// Wait for Svelte component initialization if not yet completed
|
|
250
|
+
if (this._initPromise) {
|
|
251
|
+
await this._initPromise;
|
|
252
|
+
}
|
|
253
|
+
|
|
90
254
|
// Call user's onExecute callback first if provided
|
|
91
255
|
if (this._userOnExecute) {
|
|
92
256
|
this._userOnExecute(ctx);
|
|
@@ -97,101 +261,207 @@ export class DBIHTMLComponentsV2<TNamespace extends NamespaceEnums> extends DBIB
|
|
|
97
261
|
const [elementName, ...handlerData] = ctx.data;
|
|
98
262
|
|
|
99
263
|
if (typeof elementName === 'string') {
|
|
100
|
-
//
|
|
264
|
+
// Check if this is a modal submit (elementName is the modal id)
|
|
265
|
+
const modalHandlerInfo = this.svelteComponentInfo.modalHandlers.get(elementName);
|
|
266
|
+
|
|
267
|
+
if (modalHandlerInfo) {
|
|
268
|
+
// This is a modal submit - execute the onsubmit handler (or just resolve promise if no handler)
|
|
269
|
+
this._executeModalSubmit(ctx, elementName, modalHandlerInfo.onsubmitHandler, handlerData);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Find the handler info for this element (button, select, etc.)
|
|
101
274
|
const handlerInfo = this.svelteComponentInfo.handlers.get(elementName);
|
|
102
275
|
|
|
103
276
|
if (handlerInfo) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
}
|
|
277
|
+
this._executeElementHandler(ctx, handlerInfo, handlerData);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
159
282
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
283
|
+
/**
|
|
284
|
+
* Execute a modal submit handler
|
|
285
|
+
*/
|
|
286
|
+
private _executeModalSubmit(
|
|
287
|
+
ctx: IDBIHTMLComponentsV2ExecuteCtx<TNamespace>,
|
|
288
|
+
modalId: string,
|
|
289
|
+
handlerName: string | undefined,
|
|
290
|
+
handlerData: any[]
|
|
291
|
+
) {
|
|
292
|
+
// Extract current state from handlerData (refs that were passed)
|
|
293
|
+
const currentState = handlerData[0] || {} as Record<string, any>;
|
|
294
|
+
|
|
295
|
+
// Get ref id for lifecycle tracking (if available)
|
|
296
|
+
const refId = (currentState as any)?.$ref || null;
|
|
297
|
+
|
|
298
|
+
// Extract all field values from modal interaction (text inputs, selects, file uploads, etc.)
|
|
299
|
+
const modalInteraction = ctx.interaction as any;
|
|
300
|
+
const fields = this._extractModalFields(modalInteraction);
|
|
301
|
+
|
|
302
|
+
// Check if there's a pending promise for this modal (from await showModal())
|
|
303
|
+
// Use the modal's customId to find the pending promise
|
|
304
|
+
const pendingKey = modalInteraction.customId;
|
|
305
|
+
const pendingModal = this._pendingModals.get(pendingKey);
|
|
306
|
+
|
|
307
|
+
if (pendingModal) {
|
|
308
|
+
// Resolve the promise with fields and interaction
|
|
309
|
+
pendingModal.resolve({
|
|
310
|
+
fields,
|
|
311
|
+
interaction: modalInteraction,
|
|
312
|
+
ctx: ctx
|
|
313
|
+
});
|
|
314
|
+
this._pendingModals.delete(pendingKey);
|
|
315
|
+
}
|
|
164
316
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
}
|
|
317
|
+
// If no onsubmit handler defined, just return (promise-based usage)
|
|
318
|
+
if (!handlerName) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
177
321
|
|
|
178
|
-
|
|
179
|
-
|
|
322
|
+
// Create handler context for modal submit
|
|
323
|
+
const handlerContext = createHandlerContext(
|
|
324
|
+
this.svelteComponentInfo!.scriptContent,
|
|
325
|
+
typeof currentState === 'object' ? currentState : {},
|
|
326
|
+
this,
|
|
327
|
+
ctx,
|
|
328
|
+
this._sourceDir
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
const handlerFn = handlerContext.handlers[handlerName];
|
|
332
|
+
|
|
333
|
+
if (handlerFn && typeof handlerFn === 'function') {
|
|
334
|
+
try {
|
|
335
|
+
// Store context for lifecycle management
|
|
336
|
+
if (refId) {
|
|
337
|
+
const existingContext = this._activeContexts.get(refId);
|
|
338
|
+
if (existingContext && existingContext.destroyCallbacks) {
|
|
339
|
+
handlerContext.destroyCallbacks.push(...existingContext.destroyCallbacks);
|
|
340
|
+
}
|
|
341
|
+
this._activeContexts.set(refId, handlerContext);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
handlerContext.setInHandler(true);
|
|
345
|
+
|
|
346
|
+
try {
|
|
347
|
+
// Call handler with ctx and fields object
|
|
348
|
+
handlerFn.call(this, handlerContext.wrappedCtx, fields, ...handlerData.slice(1));
|
|
349
|
+
} finally {
|
|
350
|
+
handlerContext.setInHandler(false);
|
|
351
|
+
}
|
|
180
352
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
353
|
+
handlerContext.runEffects();
|
|
354
|
+
|
|
355
|
+
if (handlerContext.hasPendingRender()) {
|
|
356
|
+
handlerContext.flushRender();
|
|
357
|
+
}
|
|
358
|
+
} catch (error) {
|
|
359
|
+
// Modal handler execution failed
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Execute an element handler (button, select, etc.)
|
|
366
|
+
*/
|
|
367
|
+
private _executeElementHandler(
|
|
368
|
+
ctx: IDBIHTMLComponentsV2ExecuteCtx<TNamespace>,
|
|
369
|
+
handlerInfo: any,
|
|
370
|
+
handlerData: any[]
|
|
371
|
+
) {
|
|
372
|
+
// Extract current state from handlerData (refs that were passed)
|
|
373
|
+
// The second element in data array contains the current state
|
|
374
|
+
const currentState = handlerData[0] || {} as Record<string, any>;
|
|
375
|
+
|
|
376
|
+
// Get ref id for lifecycle tracking (if available)
|
|
377
|
+
const refId = (currentState as any)?.$ref || null;
|
|
378
|
+
|
|
379
|
+
// Check if this is first execution for this ref
|
|
380
|
+
const isFirstExecution = refId && !this._activeContexts.has(refId);
|
|
381
|
+
|
|
382
|
+
// Get existing context if any (to preserve lifecycle callbacks like intervals)
|
|
383
|
+
const existingContext = refId ? this._activeContexts.get(refId) : null;
|
|
384
|
+
|
|
385
|
+
// Create a NEW handler context for each execution with the current state
|
|
386
|
+
// This ensures each interaction has its own isolated state
|
|
387
|
+
// Pass 'this' so handlers can access component methods like toJSON()
|
|
388
|
+
// Pass ctx so handlers can access ctx.interaction, ctx.data, ctx.locale, etc.
|
|
389
|
+
const handlerContext = createHandlerContext(
|
|
390
|
+
this.svelteComponentInfo!.scriptContent,
|
|
391
|
+
typeof currentState === 'object' ? currentState : {},
|
|
392
|
+
this,
|
|
393
|
+
ctx,
|
|
394
|
+
this._sourceDir
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
const handlerFn = handlerContext.handlers[handlerInfo.handlerName];
|
|
398
|
+
|
|
399
|
+
if (handlerFn && typeof handlerFn === 'function') {
|
|
400
|
+
try {
|
|
401
|
+
// Store context for lifecycle management
|
|
402
|
+
if (refId) {
|
|
403
|
+
// If there's an existing context, transfer its destroy callbacks to the new context
|
|
404
|
+
// This preserves intervals/timers created in onMount
|
|
405
|
+
if (existingContext && existingContext.destroyCallbacks) {
|
|
406
|
+
// Merge existing destroy callbacks (don't run them!)
|
|
407
|
+
handlerContext.destroyCallbacks.push(...existingContext.destroyCallbacks);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
this._activeContexts.set(refId, handlerContext);
|
|
411
|
+
|
|
412
|
+
// Wrap $unRef to call onDestroy when ref is deleted (only wrap once)
|
|
413
|
+
const stateObj = currentState as any;
|
|
414
|
+
if (stateObj.$unRef && !stateObj.__unRefWrapped__) {
|
|
415
|
+
const originalUnRef = stateObj.$unRef.bind(stateObj);
|
|
416
|
+
stateObj.$unRef = () => {
|
|
417
|
+
// Run destroy callbacks before unref
|
|
418
|
+
const ctx = this._activeContexts.get(refId);
|
|
419
|
+
if (ctx && ctx.runDestroy) {
|
|
420
|
+
ctx.runDestroy();
|
|
185
421
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
422
|
+
this._activeContexts.delete(refId);
|
|
423
|
+
return originalUnRef();
|
|
424
|
+
};
|
|
425
|
+
stateObj.__unRefWrapped__ = true;
|
|
189
426
|
}
|
|
190
427
|
}
|
|
428
|
+
|
|
429
|
+
// Run onMount callbacks only on first execution
|
|
430
|
+
if (isFirstExecution && handlerContext.runMount) {
|
|
431
|
+
handlerContext.runMount();
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Mark that we're inside handler execution (prevents auto-render during handler)
|
|
435
|
+
handlerContext.setInHandler(true);
|
|
436
|
+
|
|
437
|
+
try {
|
|
438
|
+
// Bind 'this' to the DBIHTMLComponentsV2 instance so handlers can use this.toJSON()
|
|
439
|
+
// Pass wrappedCtx so handlers use the proxy-wrapped ctx that tracks interaction calls
|
|
440
|
+
// This ensures __asyncInteractionCalled__ flag is set when handler calls ctx.interaction.reply() etc.
|
|
441
|
+
handlerFn.call(this, handlerContext.wrappedCtx, ...handlerData.slice(1));
|
|
442
|
+
} finally {
|
|
443
|
+
// Always reset handler execution flag
|
|
444
|
+
handlerContext.setInHandler(false);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Run effects after handler execution (state may have changed)
|
|
448
|
+
handlerContext.runEffects();
|
|
449
|
+
|
|
450
|
+
// If there are pending data changes and no interaction method was called,
|
|
451
|
+
// flush the render now (synchronously uses interaction.update)
|
|
452
|
+
if (handlerContext.hasPendingRender()) {
|
|
453
|
+
handlerContext.flushRender();
|
|
454
|
+
}
|
|
455
|
+
} catch (error) {
|
|
456
|
+
// Handler execution failed
|
|
191
457
|
}
|
|
192
458
|
}
|
|
193
459
|
}
|
|
194
460
|
|
|
461
|
+
// Store last render's modals for showModal() to access
|
|
462
|
+
// Note: Used internally by handler context, not truly private
|
|
463
|
+
__lastRenderModals__: Map<string, any> | null = null;
|
|
464
|
+
|
|
195
465
|
override async toJSON(arg: TDBIHTMLComponentsV2ToJSONArgs = {}): Promise<any> {
|
|
196
466
|
if (this.mode === 'svelte' && this.template) {
|
|
197
467
|
// Render Svelte component
|
|
@@ -205,6 +475,9 @@ export class DBIHTMLComponentsV2<TNamespace extends NamespaceEnums> extends DBIB
|
|
|
205
475
|
}
|
|
206
476
|
);
|
|
207
477
|
|
|
478
|
+
// Store modals for showModal() to access
|
|
479
|
+
this.__lastRenderModals__ = result.modals;
|
|
480
|
+
|
|
208
481
|
return result.components;
|
|
209
482
|
} else {
|
|
210
483
|
// Use Eta template parsing (default)
|
|
@@ -242,9 +515,14 @@ export class DBIHTMLComponentsV2<TNamespace extends NamespaceEnums> extends DBIB
|
|
|
242
515
|
* ```
|
|
243
516
|
*/
|
|
244
517
|
async send(target: any, options: TDBIHTMLComponentsV2SendOptions = {}): Promise<any> {
|
|
518
|
+
// Wait for Svelte component initialization if not yet completed
|
|
519
|
+
if (this._initPromise) {
|
|
520
|
+
await this._initPromise;
|
|
521
|
+
}
|
|
522
|
+
|
|
245
523
|
const { data = {}, flags = ["IsComponentsV2"], content, ephemeral, reply, followUp } = options;
|
|
246
524
|
|
|
247
|
-
// Render components (toJSON is async)
|
|
525
|
+
// Render components (toJSON is async) - this also creates $ref in data if not present
|
|
248
526
|
const components = await this.toJSON({ data });
|
|
249
527
|
|
|
250
528
|
// Build message options
|
|
@@ -257,19 +535,29 @@ export class DBIHTMLComponentsV2<TNamespace extends NamespaceEnums> extends DBIB
|
|
|
257
535
|
const isInteraction = target.reply && target.user; // Interactions have both reply method and user property
|
|
258
536
|
const isChannel = target.send && !target.user; // Channels have send but no user
|
|
259
537
|
|
|
260
|
-
|
|
261
|
-
if (
|
|
262
|
-
|
|
538
|
+
try {
|
|
539
|
+
if (isInteraction) {
|
|
540
|
+
if (followUp) {
|
|
541
|
+
message = await target.followUp(messageOptions);
|
|
542
|
+
} else {
|
|
543
|
+
message = await target.reply(messageOptions);
|
|
544
|
+
}
|
|
545
|
+
} else if (isChannel) {
|
|
546
|
+
message = await target.send(messageOptions);
|
|
263
547
|
} else {
|
|
264
|
-
|
|
548
|
+
throw new Error("Invalid target: must be an interaction or channel");
|
|
265
549
|
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
550
|
+
} catch (error: any) {
|
|
551
|
+
// Check if it's a Discord API error and enhance it with helpful context
|
|
552
|
+
if (error.code || error.rawError || (error.message && error.message.includes('Invalid Form Body'))) {
|
|
553
|
+
const source = this.file ? fs.readFileSync(this.file, 'utf-8') : this.template;
|
|
554
|
+
throw parseDiscordAPIError(error, source, this.name);
|
|
555
|
+
}
|
|
556
|
+
throw error;
|
|
270
557
|
}
|
|
271
558
|
|
|
272
559
|
// If Svelte mode, create initial handler context and run onMount
|
|
560
|
+
// After toJSON, data.$ref is guaranteed to exist (created in renderSvelteComponent)
|
|
273
561
|
if (this.mode === 'svelte' && this.svelteComponentInfo && data.$ref) {
|
|
274
562
|
const refId = data.$ref;
|
|
275
563
|
|
|
@@ -286,7 +574,8 @@ export class DBIHTMLComponentsV2<TNamespace extends NamespaceEnums> extends DBIB
|
|
|
286
574
|
this.svelteComponentInfo.scriptContent,
|
|
287
575
|
data,
|
|
288
576
|
this,
|
|
289
|
-
fakeCtx
|
|
577
|
+
fakeCtx,
|
|
578
|
+
this._sourceDir
|
|
290
579
|
);
|
|
291
580
|
|
|
292
581
|
// Store the context for lifecycle management
|
|
@@ -355,6 +644,100 @@ export class DBIHTMLComponentsV2<TNamespace extends NamespaceEnums> extends DBIB
|
|
|
355
644
|
return true;
|
|
356
645
|
}
|
|
357
646
|
|
|
647
|
+
/**
|
|
648
|
+
* Extract all field values from a modal interaction
|
|
649
|
+
* Supports text inputs, select menus (string, user, role, mentionable, channel), and file uploads
|
|
650
|
+
*
|
|
651
|
+
* Returns an object where keys are the custom_id of each component and values are:
|
|
652
|
+
* - For text inputs: string value
|
|
653
|
+
* - For string selects: string[] of selected values
|
|
654
|
+
* - For user selects: string[] of user IDs
|
|
655
|
+
* - For role selects: string[] of role IDs
|
|
656
|
+
* - For mentionable selects: { users: string[], roles: string[] }
|
|
657
|
+
* - For channel selects: string[] of channel IDs
|
|
658
|
+
* - For file uploads: attachment objects array
|
|
659
|
+
*/
|
|
660
|
+
private _extractModalFields(modalInteraction: any): Record<string, any> {
|
|
661
|
+
const fields: Record<string, any> = {};
|
|
662
|
+
|
|
663
|
+
// Handle classic text input fields via ModalSubmitFields
|
|
664
|
+
if (modalInteraction.fields && modalInteraction.fields.fields) {
|
|
665
|
+
for (const [customId, field] of modalInteraction.fields.fields) {
|
|
666
|
+
// Text input - field.value is the string
|
|
667
|
+
fields[customId] = field.value;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Handle new modal components from interaction.data.components
|
|
672
|
+
// The new modal structure uses Label wrappers with nested components
|
|
673
|
+
const data = modalInteraction.data || (modalInteraction as any).data;
|
|
674
|
+
if (data && data.components) {
|
|
675
|
+
this._extractFieldsFromComponents(data.components, fields);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return fields;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Recursively extract field values from component structure
|
|
683
|
+
*/
|
|
684
|
+
private _extractFieldsFromComponents(components: any[], fields: Record<string, any>) {
|
|
685
|
+
for (const component of components) {
|
|
686
|
+
const type = component.type;
|
|
687
|
+
const customId = component.custom_id;
|
|
688
|
+
|
|
689
|
+
// Type 18 = Label - has nested component
|
|
690
|
+
if (type === 18 && component.component) {
|
|
691
|
+
this._extractFieldsFromComponents([component.component], fields);
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Type 1 = Action Row - has nested components array
|
|
696
|
+
if (type === 1 && component.components) {
|
|
697
|
+
this._extractFieldsFromComponents(component.components, fields);
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (!customId) continue;
|
|
702
|
+
|
|
703
|
+
switch (type) {
|
|
704
|
+
case 4: // Text Input
|
|
705
|
+
fields[customId] = component.value || '';
|
|
706
|
+
break;
|
|
707
|
+
|
|
708
|
+
case 3: // String Select
|
|
709
|
+
fields[customId] = component.values || [];
|
|
710
|
+
break;
|
|
711
|
+
|
|
712
|
+
case 5: // User Select
|
|
713
|
+
fields[customId] = component.values || [];
|
|
714
|
+
break;
|
|
715
|
+
|
|
716
|
+
case 6: // Role Select
|
|
717
|
+
fields[customId] = component.values || [];
|
|
718
|
+
break;
|
|
719
|
+
|
|
720
|
+
case 7: // Mentionable Select - can have both users and roles
|
|
721
|
+
// Discord returns resolved data for mentionables
|
|
722
|
+
fields[customId] = {
|
|
723
|
+
values: component.values || [],
|
|
724
|
+
users: component.resolved?.users ? Object.keys(component.resolved.users) : [],
|
|
725
|
+
roles: component.resolved?.roles ? Object.keys(component.resolved.roles) : []
|
|
726
|
+
};
|
|
727
|
+
break;
|
|
728
|
+
|
|
729
|
+
case 8: // Channel Select
|
|
730
|
+
fields[customId] = component.values || [];
|
|
731
|
+
break;
|
|
732
|
+
|
|
733
|
+
case 19: // File Upload
|
|
734
|
+
// File uploads come through as attachments
|
|
735
|
+
fields[customId] = component.attachments || [];
|
|
736
|
+
break;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
358
741
|
/**
|
|
359
742
|
* Destroy all active instances of this component
|
|
360
743
|
* Useful for cleanup when the bot shuts down or component is unloaded
|