waengine 1.7.3 → 1.7.4

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.
@@ -0,0 +1,560 @@
1
+ export class UIComponents {
2
+ constructor(client) {
3
+ this.client = client;
4
+ }
5
+
6
+ // ===== CAROUSEL MESSAGES =====
7
+
8
+ /**
9
+ * Send carousel message with multiple cards
10
+ */
11
+ async sendCarousel(chatId, cards, options = {}) {
12
+ try {
13
+ // WhatsApp doesn't support native carousels, so we create a rich text alternative
14
+ let carouselText = `🎠 **${options.title || 'Carousel'}**\n\n`;
15
+
16
+ cards.forEach((card, index) => {
17
+ carouselText += `**${index + 1}. ${card.title}**\n`;
18
+ if (card.subtitle) carouselText += `${card.subtitle}\n`;
19
+ if (card.body) carouselText += `${card.body}\n`;
20
+ if (card.price) carouselText += `💰 ${card.price}\n`;
21
+
22
+ // Add buttons as text options
23
+ if (card.buttons && card.buttons.length > 0) {
24
+ carouselText += `Options: `;
25
+ card.buttons.forEach((btn, btnIndex) => {
26
+ carouselText += `[${btnIndex + 1}] ${btn.title} `;
27
+ });
28
+ carouselText += `\n`;
29
+ }
30
+
31
+ carouselText += `\n`;
32
+ });
33
+
34
+ if (options.footer) {
35
+ carouselText += `\n_${options.footer}_`;
36
+ }
37
+
38
+ // Send with image if provided
39
+ if (cards[0]?.image) {
40
+ await this.client.socket.sendMessage(chatId, {
41
+ image: { url: cards[0].image },
42
+ caption: carouselText
43
+ });
44
+ } else {
45
+ await this.client.socket.sendMessage(chatId, { text: carouselText });
46
+ }
47
+
48
+ return { success: true, type: 'carousel', cards: cards.length };
49
+
50
+ } catch (error) {
51
+ // Fallback to simple text
52
+ const fallbackText = `📋 **${options.title || 'Options'}**\n\n` +
53
+ cards.map((card, i) => `${i + 1}. ${card.title}`).join('\n');
54
+
55
+ await this.client.socket.sendMessage(chatId, { text: fallbackText });
56
+ return { success: true, type: 'fallback', cards: cards.length };
57
+ }
58
+ }
59
+
60
+ // ===== PERSISTENT MENU =====
61
+
62
+ /**
63
+ * Set persistent menu for user
64
+ */
65
+ async setPersistentMenu(userId, menuItems) {
66
+ // Store menu in user data
67
+ this.client.storage.write.in("ui").set(`persistentMenu.${userId}`, {
68
+ items: menuItems,
69
+ created: new Date(),
70
+ active: true
71
+ });
72
+
73
+ // Send menu as message
74
+ let menuText = `📋 **Hauptmenü**\n\n`;
75
+ menuItems.forEach((item, index) => {
76
+ menuText += `${item.emoji || '▫️'} **${item.title}**\n`;
77
+ if (item.description) menuText += ` ${item.description}\n`;
78
+ menuText += ` Befehl: \`${item.payload || item.command}\`\n\n`;
79
+ });
80
+
81
+ menuText += `_Verwende die Befehle oben oder tippe "menu" für dieses Menü._`;
82
+
83
+ try {
84
+ await this.client.socket.sendMessage(userId, { text: menuText });
85
+ } catch (error) {
86
+ console.error('❌ Fehler beim Senden des Menüs:', error);
87
+ throw error;
88
+ }
89
+
90
+ return { success: true, items: menuItems.length };
91
+ }
92
+
93
+ /**
94
+ * Get persistent menu for user
95
+ */
96
+ getPersistentMenu(userId) {
97
+ return this.client.storage.read.from("ui").get(`persistentMenu.${userId}`);
98
+ }
99
+
100
+ /**
101
+ * Show persistent menu
102
+ */
103
+ async showPersistentMenu(userId) {
104
+ const menu = this.getPersistentMenu(userId);
105
+ if (!menu || !menu.active) {
106
+ return { success: false, error: 'No active menu found' };
107
+ }
108
+
109
+ return await this.setPersistentMenu(userId, menu.items);
110
+ }
111
+
112
+ // ===== QUICK REPLIES =====
113
+
114
+ /**
115
+ * Send message with quick reply options
116
+ */
117
+ async sendQuickReplies(chatId, text, replies, options = {}) {
118
+ let message = `${text}\n\n`;
119
+
120
+ // Add quick reply options
121
+ message += `**Quick Replies:**\n`;
122
+ replies.forEach((reply, index) => {
123
+ const emoji = reply.emoji || `${index + 1}️⃣`;
124
+ message += `${emoji} ${reply.title}\n`;
125
+ });
126
+
127
+ if (options.footer) {
128
+ message += `\n_${options.footer}_`;
129
+ }
130
+
131
+ try {
132
+ await this.client.socket.sendMessage(chatId, { text: message });
133
+ } catch (error) {
134
+ console.error('❌ Fehler beim Senden der Quick Replies:', error);
135
+ throw error;
136
+ }
137
+
138
+ // Store quick replies for processing
139
+ this.client.storage.write.in("ui").set(`quickReplies.${chatId}`, {
140
+ replies: replies,
141
+ timestamp: Date.now(),
142
+ expires: Date.now() + (options.timeout || 300000) // 5 minutes default
143
+ });
144
+
145
+ return { success: true, replies: replies.length };
146
+ }
147
+
148
+ /**
149
+ * Process quick reply response
150
+ */
151
+ processQuickReply(chatId, userInput) {
152
+ const quickReplies = this.client.storage.read.from("ui").get(`quickReplies.${chatId}`);
153
+
154
+ if (!quickReplies || Date.now() > quickReplies.expires) {
155
+ return null;
156
+ }
157
+
158
+ // Find matching reply
159
+ const reply = quickReplies.replies.find((r, index) => {
160
+ return userInput.toLowerCase() === r.title.toLowerCase() ||
161
+ userInput === `${index + 1}` ||
162
+ userInput === r.payload;
163
+ });
164
+
165
+ if (reply) {
166
+ // Clean up stored replies
167
+ this.client.storage.delete.from("ui").key(`quickReplies.${chatId}`);
168
+ return reply;
169
+ }
170
+
171
+ return null;
172
+ }
173
+
174
+ // ===== RICH MEDIA TEMPLATES =====
175
+
176
+ /**
177
+ * Send product template
178
+ */
179
+ async sendProductTemplate(chatId, product, options = {}) {
180
+ const template = `🛍️ **${product.name}**\n\n` +
181
+ `${product.description || 'No description available'}\n\n` +
182
+ `💰 **Price:** ${product.price} ${product.currency || 'EUR'}\n` +
183
+ `📦 **Stock:** ${product.inStock ? 'Available' : 'Out of Stock'}\n` +
184
+ `🏷️ **Category:** ${product.category || 'General'}\n\n` +
185
+ `**Actions:**\n` +
186
+ `🛒 Buy Now - \`buy ${product.id}\`\n` +
187
+ `ℹ️ More Info - \`info ${product.id}\`\n` +
188
+ `❤️ Add to Wishlist - \`wishlist ${product.id}\``;
189
+
190
+ if (product.images && product.images.length > 0) {
191
+ await this.client.socket.sendMessage(chatId, {
192
+ image: { url: product.images[0] },
193
+ caption: template
194
+ });
195
+ } else {
196
+ await this.client.socket.sendMessage(chatId, { text: template });
197
+ }
198
+
199
+ return { success: true, type: 'product', productId: product.id };
200
+ }
201
+
202
+ /**
203
+ * Send service template
204
+ */
205
+ async sendServiceTemplate(chatId, service, options = {}) {
206
+ const template = `⚙️ **${service.name}**\n\n` +
207
+ `${service.description || 'Professional service'}\n\n` +
208
+ `💰 **Price:** ${service.price} ${service.currency || 'EUR'}\n` +
209
+ `⏱️ **Duration:** ${service.duration || 'Varies'}\n` +
210
+ `📅 **Availability:** ${service.available ? 'Available' : 'Booked'}\n\n` +
211
+ `**Book Service:**\n` +
212
+ `📞 Book Now - \`book ${service.id}\`\n` +
213
+ `💬 Ask Questions - \`ask ${service.id}\`\n` +
214
+ `📋 View Details - \`details ${service.id}\``;
215
+
216
+ await this.client.socket.sendMessage(chatId, { text: template });
217
+
218
+ return { success: true, type: 'service', serviceId: service.id };
219
+ }
220
+
221
+ // ===== INTERACTIVE FORMS =====
222
+
223
+ /**
224
+ * Create interactive form
225
+ */
226
+ async createForm(chatId, formConfig) {
227
+ const form = {
228
+ id: `form_${Date.now()}`,
229
+ chatId: chatId,
230
+ title: formConfig.title,
231
+ fields: formConfig.fields,
232
+ currentField: 0,
233
+ responses: {},
234
+ created: Date.now(),
235
+ status: 'active'
236
+ };
237
+
238
+ // Store form
239
+ this.client.storage.write.in("ui").set(`forms.${form.id}`, form);
240
+ this.client.storage.write.in("ui").set(`activeForm.${chatId}`, form.id);
241
+
242
+ // Start form
243
+ await this.sendFormField(chatId, form, 0);
244
+
245
+ return { success: true, formId: form.id };
246
+ }
247
+
248
+ /**
249
+ * Send form field
250
+ */
251
+ async sendFormField(chatId, form, fieldIndex) {
252
+ if (fieldIndex >= form.fields.length) {
253
+ return await this.completeForm(chatId, form);
254
+ }
255
+
256
+ const field = form.fields[fieldIndex];
257
+ let message = `📝 **${form.title}** (${fieldIndex + 1}/${form.fields.length})\n\n`;
258
+ message += `**${field.label}**\n`;
259
+
260
+ if (field.description) {
261
+ message += `${field.description}\n\n`;
262
+ }
263
+
264
+ // Add field-specific instructions
265
+ switch (field.type) {
266
+ case 'text':
267
+ message += `✏️ Please enter your text:`;
268
+ break;
269
+ case 'number':
270
+ message += `🔢 Please enter a number:`;
271
+ break;
272
+ case 'email':
273
+ message += `📧 Please enter your email:`;
274
+ break;
275
+ case 'phone':
276
+ message += `📱 Please enter your phone number:`;
277
+ break;
278
+ case 'choice':
279
+ message += `**Choose one:**\n`;
280
+ field.options.forEach((option, i) => {
281
+ message += `${i + 1}. ${option}\n`;
282
+ });
283
+ break;
284
+ case 'multiple':
285
+ message += `**Choose multiple (separate with commas):**\n`;
286
+ field.options.forEach((option, i) => {
287
+ message += `${i + 1}. ${option}\n`;
288
+ });
289
+ break;
290
+ }
291
+
292
+ if (field.required) {
293
+ message += `\n_*Required field_`;
294
+ }
295
+
296
+ await this.client.socket.sendMessage(chatId, { text: message });
297
+
298
+ return { success: true, field: fieldIndex };
299
+ }
300
+
301
+ /**
302
+ * Process form response
303
+ */
304
+ async processFormResponse(chatId, userInput) {
305
+ const activeFormId = this.client.storage.read.from("ui").get(`activeForm.${chatId}`);
306
+ if (!activeFormId) return null;
307
+
308
+ const form = this.client.storage.read.from("ui").get(`forms.${activeFormId}`);
309
+ if (!form || form.status !== 'active') return null;
310
+
311
+ const currentField = form.fields[form.currentField];
312
+ let processedValue = userInput;
313
+
314
+ // Validate and process based on field type
315
+ switch (currentField.type) {
316
+ case 'number':
317
+ processedValue = parseFloat(userInput);
318
+ if (isNaN(processedValue)) {
319
+ await this.client.socket.sendMessage(chatId, {
320
+ text: '❌ Please enter a valid number.'
321
+ });
322
+ return { success: false, retry: true };
323
+ }
324
+ break;
325
+
326
+ case 'email':
327
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
328
+ if (!emailRegex.test(userInput)) {
329
+ await this.client.socket.sendMessage(chatId, {
330
+ text: '❌ Please enter a valid email address.'
331
+ });
332
+ return { success: false, retry: true };
333
+ }
334
+ break;
335
+
336
+ case 'choice':
337
+ const choiceIndex = parseInt(userInput) - 1;
338
+ if (choiceIndex < 0 || choiceIndex >= currentField.options.length) {
339
+ await this.client.socket.sendMessage(chatId, {
340
+ text: '❌ Please select a valid option number.'
341
+ });
342
+ return { success: false, retry: true };
343
+ }
344
+ processedValue = currentField.options[choiceIndex];
345
+ break;
346
+
347
+ case 'multiple':
348
+ const selectedIndices = userInput.split(',').map(s => parseInt(s.trim()) - 1);
349
+ const validSelections = selectedIndices.filter(i =>
350
+ i >= 0 && i < currentField.options.length
351
+ );
352
+ if (validSelections.length === 0) {
353
+ await this.client.socket.sendMessage(chatId, {
354
+ text: '❌ Please select valid option numbers.'
355
+ });
356
+ return { success: false, retry: true };
357
+ }
358
+ processedValue = validSelections.map(i => currentField.options[i]);
359
+ break;
360
+ }
361
+
362
+ // Store response
363
+ form.responses[currentField.name] = processedValue;
364
+ form.currentField++;
365
+
366
+ // Update form
367
+ this.client.storage.write.in("ui").set(`forms.${activeFormId}`, form);
368
+
369
+ // Send next field or complete form
370
+ if (form.currentField < form.fields.length) {
371
+ await this.sendFormField(chatId, form, form.currentField);
372
+ return { success: true, nextField: form.currentField };
373
+ } else {
374
+ return await this.completeForm(chatId, form);
375
+ }
376
+ }
377
+
378
+ /**
379
+ * Complete form
380
+ */
381
+ async completeForm(chatId, form) {
382
+ form.status = 'completed';
383
+ form.completed = Date.now();
384
+
385
+ // Update form
386
+ this.client.storage.write.in("ui").set(`forms.${form.id}`, form);
387
+ this.client.storage.delete.from("ui").key(`activeForm.${chatId}`);
388
+
389
+ // Send completion message
390
+ let completionMessage = `✅ **Form Completed: ${form.title}**\n\n`;
391
+ completionMessage += `**Your Responses:**\n`;
392
+
393
+ Object.entries(form.responses).forEach(([fieldName, value]) => {
394
+ const field = form.fields.find(f => f.name === fieldName);
395
+ completionMessage += `**${field.label}:** ${Array.isArray(value) ? value.join(', ') : value}\n`;
396
+ });
397
+
398
+ completionMessage += `\n_Form ID: ${form.id}_\n`;
399
+ completionMessage += `_Submitted: ${new Date().toLocaleString()}_`;
400
+
401
+ await this.client.socket.sendMessage(chatId, { text: completionMessage });
402
+
403
+ // Emit form completion event
404
+ this.client.emit('form_completed', {
405
+ formId: form.id,
406
+ chatId: chatId,
407
+ responses: form.responses,
408
+ form: form
409
+ });
410
+
411
+ return { success: true, formId: form.id, responses: form.responses };
412
+ }
413
+
414
+ // ===== PROGRESS INDICATORS =====
415
+
416
+ /**
417
+ * Send progress bar
418
+ */
419
+ async sendProgressBar(chatId, current, total, title = "Progress") {
420
+ const percentage = Math.round((current / total) * 100);
421
+ const filledBars = Math.round((current / total) * 10);
422
+ const emptyBars = 10 - filledBars;
423
+
424
+ const progressBar = '█'.repeat(filledBars) + '░'.repeat(emptyBars);
425
+
426
+ const message = `📊 **${title}**\n\n` +
427
+ `${progressBar} ${percentage}%\n` +
428
+ `${current} / ${total}`;
429
+
430
+ await this.client.socket.sendMessage(chatId, { text: message });
431
+
432
+ return { success: true, percentage, current, total };
433
+ }
434
+
435
+ /**
436
+ * Send loading animation with proper cleanup
437
+ */
438
+ async sendLoadingAnimation(chatId, text = "Loading", duration = 3000) {
439
+ const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
440
+ let frameIndex = 0;
441
+ let interval = null;
442
+ let isActive = true;
443
+
444
+ try {
445
+ const message = await this.client.socket.sendMessage(chatId, {
446
+ text: `${frames[frameIndex]} ${text}...`
447
+ });
448
+
449
+ // Sichere Interval-Verwaltung mit Cleanup
450
+ interval = setInterval(async () => {
451
+ if (!isActive) {
452
+ clearInterval(interval);
453
+ return;
454
+ }
455
+
456
+ frameIndex = (frameIndex + 1) % frames.length;
457
+ try {
458
+ await this.client.socket.sendMessage(chatId, {
459
+ text: `${frames[frameIndex]} ${text}...`,
460
+ edit: message.key
461
+ });
462
+ } catch (error) {
463
+ // Editing not supported, send new message
464
+ try {
465
+ await this.client.socket.sendMessage(chatId, {
466
+ text: `${frames[frameIndex]} ${text}...`
467
+ });
468
+ } catch (sendError) {
469
+ // Stop animation on send error
470
+ isActive = false;
471
+ clearInterval(interval);
472
+ }
473
+ }
474
+ }, 200);
475
+
476
+ // Garantierte Bereinigung nach Timeout
477
+ const cleanup = () => {
478
+ isActive = false;
479
+ if (interval) {
480
+ clearInterval(interval);
481
+ interval = null;
482
+ }
483
+ };
484
+
485
+ // Timeout mit garantierter Bereinigung
486
+ const timeoutId = setTimeout(cleanup, duration || 3000);
487
+
488
+ // Cleanup-Funktion für externe Verwendung
489
+ return {
490
+ success: true,
491
+ duration,
492
+ stop: () => {
493
+ clearTimeout(timeoutId);
494
+ cleanup();
495
+ }
496
+ };
497
+
498
+ } catch (error) {
499
+ // Bereinigung bei Fehler
500
+ isActive = false;
501
+ if (interval) {
502
+ clearInterval(interval);
503
+ }
504
+
505
+ console.error('❌ Loading animation error:', error);
506
+ return { success: false, error: error.message };
507
+ }
508
+ }
509
+
510
+ // ===== UTILITY METHODS =====
511
+
512
+ /**
513
+ * Get UI statistics
514
+ */
515
+ getUIStats() {
516
+ const forms = this.client.storage.read.from("ui").get("forms") || {};
517
+ const menus = this.client.storage.read.from("ui").get("persistentMenu") || {};
518
+ const quickReplies = this.client.storage.read.from("ui").get("quickReplies") || {};
519
+
520
+ return {
521
+ forms: {
522
+ total: Object.keys(forms).length,
523
+ completed: Object.values(forms).filter(f => f.status === 'completed').length,
524
+ active: Object.values(forms).filter(f => f.status === 'active').length
525
+ },
526
+ menus: {
527
+ total: Object.keys(menus).length,
528
+ active: Object.values(menus).filter(m => m.active).length
529
+ },
530
+ quickReplies: {
531
+ active: Object.keys(quickReplies).length
532
+ }
533
+ };
534
+ }
535
+
536
+ /**
537
+ * Clean up expired UI elements
538
+ */
539
+ cleanupExpiredElements() {
540
+ const now = Date.now();
541
+
542
+ // Clean up expired quick replies
543
+ const quickReplies = this.client.storage.read.from("ui").get("quickReplies") || {};
544
+ Object.entries(quickReplies).forEach(([chatId, data]) => {
545
+ if (now > data.expires) {
546
+ this.client.storage.delete.from("ui").key(`quickReplies.${chatId}`);
547
+ }
548
+ });
549
+
550
+ // Clean up old forms (older than 24 hours)
551
+ const forms = this.client.storage.read.from("ui").get("forms") || {};
552
+ Object.entries(forms).forEach(([formId, form]) => {
553
+ if (now - form.created > 24 * 60 * 60 * 1000) {
554
+ this.client.storage.delete.from("ui").key(`forms.${formId}`);
555
+ }
556
+ });
557
+
558
+ return { success: true, cleaned: true };
559
+ }
560
+ }