ai-progress-controls 0.1.0

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 (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +823 -0
  3. package/dist/ai-progress-controls.es.js +7191 -0
  4. package/dist/ai-progress-controls.es.js.map +1 -0
  5. package/dist/ai-progress-controls.umd.js +2 -0
  6. package/dist/ai-progress-controls.umd.js.map +1 -0
  7. package/dist/index.d.ts +2212 -0
  8. package/package.json +105 -0
  9. package/src/__tests__/setup.ts +93 -0
  10. package/src/core/base/AIControl.ts +230 -0
  11. package/src/core/base/index.ts +3 -0
  12. package/src/core/base/types.ts +77 -0
  13. package/src/core/base/utils.ts +168 -0
  14. package/src/core/batch-progress/BatchProgress.test.ts +458 -0
  15. package/src/core/batch-progress/BatchProgress.ts +760 -0
  16. package/src/core/batch-progress/index.ts +14 -0
  17. package/src/core/batch-progress/styles.ts +480 -0
  18. package/src/core/batch-progress/types.ts +169 -0
  19. package/src/core/model-loader/ModelLoader.test.ts +311 -0
  20. package/src/core/model-loader/ModelLoader.ts +673 -0
  21. package/src/core/model-loader/index.ts +2 -0
  22. package/src/core/model-loader/styles.ts +496 -0
  23. package/src/core/model-loader/types.ts +127 -0
  24. package/src/core/parameter-panel/ParameterPanel.test.ts +856 -0
  25. package/src/core/parameter-panel/ParameterPanel.ts +877 -0
  26. package/src/core/parameter-panel/index.ts +14 -0
  27. package/src/core/parameter-panel/styles.ts +323 -0
  28. package/src/core/parameter-panel/types.ts +278 -0
  29. package/src/core/parameter-slider/ParameterSlider.test.ts +299 -0
  30. package/src/core/parameter-slider/ParameterSlider.ts +653 -0
  31. package/src/core/parameter-slider/index.ts +8 -0
  32. package/src/core/parameter-slider/styles.ts +493 -0
  33. package/src/core/parameter-slider/types.ts +107 -0
  34. package/src/core/queue-progress/QueueProgress.test.ts +344 -0
  35. package/src/core/queue-progress/QueueProgress.ts +563 -0
  36. package/src/core/queue-progress/index.ts +5 -0
  37. package/src/core/queue-progress/styles.ts +469 -0
  38. package/src/core/queue-progress/types.ts +130 -0
  39. package/src/core/retry-progress/RetryProgress.test.ts +397 -0
  40. package/src/core/retry-progress/RetryProgress.ts +957 -0
  41. package/src/core/retry-progress/index.ts +6 -0
  42. package/src/core/retry-progress/styles.ts +530 -0
  43. package/src/core/retry-progress/types.ts +176 -0
  44. package/src/core/stream-progress/StreamProgress.test.ts +531 -0
  45. package/src/core/stream-progress/StreamProgress.ts +517 -0
  46. package/src/core/stream-progress/index.ts +2 -0
  47. package/src/core/stream-progress/styles.ts +349 -0
  48. package/src/core/stream-progress/types.ts +82 -0
  49. package/src/index.ts +19 -0
@@ -0,0 +1,760 @@
1
+ import { AIControl } from '../base/AIControl';
2
+ import type {
3
+ BatchProgressConfig,
4
+ BatchProgressState,
5
+ BatchProgressUpdate,
6
+ BatchItem,
7
+ BatchItemStatus,
8
+ BatchStartEvent,
9
+ BatchItemUpdateEvent,
10
+ BatchCompleteEvent,
11
+ BatchCancelEvent,
12
+ BatchItemCompleteEvent,
13
+ BatchItemFailedEvent,
14
+ } from './types';
15
+ import { styles } from './styles';
16
+
17
+ /**
18
+ * BatchProgress Component
19
+ *
20
+ * Displays progress for batch operations processing multiple items.
21
+ * Perfect for processing multiple AI requests, documents, or images in parallel.
22
+ *
23
+ * @example
24
+ * ```typescript
25
+ * // Create batch progress
26
+ * const batch = new BatchProgress({
27
+ * totalItems: 50,
28
+ * concurrency: 5,
29
+ * showItems: true,
30
+ * showStats: true
31
+ * });
32
+ *
33
+ * document.body.appendChild(batch);
34
+ *
35
+ * // Start batch
36
+ * batch.start();
37
+ *
38
+ * // Add items
39
+ * for (let i = 0; i < 50; i++) {
40
+ * batch.addItem(`item-${i}`, `Process item ${i}`);
41
+ * }
42
+ *
43
+ * // Update item progress
44
+ * batch.updateItem({
45
+ * itemId: 'item-0',
46
+ * status: 'processing',
47
+ * progress: 50
48
+ * });
49
+ *
50
+ * // Complete item
51
+ * batch.completeItem('item-0', { result: 'success' });
52
+ *
53
+ * // Listen to events
54
+ * batch.addEventListener('batchcomplete', (e) => {
55
+ * console.log(`Completed ${e.detail.successCount}/${e.detail.totalItems}`);
56
+ * });
57
+ * ```
58
+ *
59
+ * @fires batchstart - Fired when batch processing starts
60
+ * @fires itemupdate - Fired when batch item is updated
61
+ * @fires itemcomplete - Fired when batch item completes
62
+ * @fires itemfailed - Fired when batch item fails
63
+ * @fires batchcomplete - Fired when all items are processed
64
+ * @fires batchcancel - Fired when batch is cancelled
65
+ */
66
+ export class BatchProgress extends AIControl {
67
+ protected override config: Required<BatchProgressConfig>;
68
+ private readonly state: BatchProgressState;
69
+ private updateThrottleTimer: ReturnType<typeof setTimeout> | null = null;
70
+
71
+ static get observedAttributes() {
72
+ return ['total-items', 'disabled', 'size', 'variant', 'animation'];
73
+ }
74
+
75
+ constructor(config: BatchProgressConfig = {}) {
76
+ super({
77
+ debug: config.debug ?? false,
78
+ className: config.className,
79
+ ariaLabel: config.ariaLabel ?? 'Batch Progress',
80
+ });
81
+
82
+ this.config = {
83
+ totalItems: config.totalItems ?? 0,
84
+ concurrency: config.concurrency ?? 5,
85
+ showItems: config.showItems ?? true,
86
+ maxDisplayItems: config.maxDisplayItems ?? 100,
87
+ showProgressBar: config.showProgressBar ?? true,
88
+ showStats: config.showStats ?? true,
89
+ showTime: config.showTime ?? true,
90
+ showRate: config.showRate ?? true,
91
+ allowCancel: config.allowCancel ?? true,
92
+ cancelLabel: config.cancelLabel ?? 'Cancel Batch',
93
+ collapseCompleted: config.collapseCompleted ?? false,
94
+ message: config.message ?? 'Processing batch...',
95
+ disabled: config.disabled ?? false,
96
+ debug: config.debug ?? false,
97
+ className: config.className ?? '',
98
+ ariaLabel: config.ariaLabel ?? 'Batch Progress',
99
+ cursorFeedback: config.cursorFeedback ?? true,
100
+ size: config.size ?? 'default',
101
+ variant: config.variant ?? 'default',
102
+ animation: config.animation ?? 'none',
103
+ };
104
+
105
+ this.state = {
106
+ status: 'idle',
107
+ items: new Map(),
108
+ totalItems: this.config.totalItems,
109
+ completedCount: 0,
110
+ failedCount: 0,
111
+ successCount: 0,
112
+ currentConcurrency: 0,
113
+ startTime: null,
114
+ endTime: null,
115
+ message: this.config.message,
116
+ };
117
+
118
+ this.attachShadow({ mode: 'open' });
119
+ }
120
+
121
+ override connectedCallback(): void {
122
+ super.connectedCallback();
123
+ this.render();
124
+ }
125
+
126
+ override attributeChangedCallback(
127
+ name: string,
128
+ oldValue: string | null,
129
+ newValue: string | null
130
+ ): void {
131
+ if (oldValue === newValue) return;
132
+
133
+ switch (name) {
134
+ case 'total-items':
135
+ this.state.totalItems = Number.parseInt(newValue || '0', 10);
136
+ this.render();
137
+ break;
138
+ case 'disabled':
139
+ this._disabled = newValue !== null;
140
+ this.render();
141
+ break;
142
+ case 'size':
143
+ this.config.size = newValue as any;
144
+ this.render();
145
+ break;
146
+ case 'variant':
147
+ this.config.variant = newValue as any;
148
+ this.render();
149
+ break;
150
+ case 'animation':
151
+ this.config.animation = newValue as any;
152
+ this.render();
153
+ break;
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Update cursor based on component state
159
+ */
160
+ private updateCursor(): void {
161
+ if (!this.config.cursorFeedback) return;
162
+
163
+ if (this.state.status === 'processing') {
164
+ this.style.cursor = 'progress';
165
+ } else if (this.state.status === 'cancelled') {
166
+ this.style.cursor = 'not-allowed';
167
+ } else {
168
+ this.style.cursor = 'default';
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Start batch processing
174
+ */
175
+ public start(message?: string): void {
176
+ if (this.config.disabled) return;
177
+
178
+ this.state.status = 'processing';
179
+ this.state.startTime = Date.now();
180
+ this.state.message = message || this.config.message;
181
+ this.state.completedCount = 0;
182
+ this.state.failedCount = 0;
183
+ this.state.successCount = 0;
184
+
185
+ this.render();
186
+ this.updateCursor();
187
+
188
+ const event: BatchStartEvent = {
189
+ totalItems: this.state.totalItems,
190
+ startTime: this.state.startTime,
191
+ };
192
+
193
+ this.dispatchEvent(
194
+ new CustomEvent('batchstart', {
195
+ detail: event,
196
+ bubbles: true,
197
+ composed: true,
198
+ })
199
+ );
200
+
201
+ this.log('Batch started', event);
202
+ }
203
+
204
+ /**
205
+ * Add item to batch
206
+ */
207
+ public addItem(itemId: string, label?: string): void {
208
+ const item: BatchItem = {
209
+ id: itemId,
210
+ label: label || itemId,
211
+ status: 'pending',
212
+ progress: 0,
213
+ };
214
+
215
+ this.state.items.set(itemId, item);
216
+ this.state.totalItems = this.state.items.size;
217
+ this.render();
218
+
219
+ this.log('Item added', item);
220
+ }
221
+
222
+ /**
223
+ * Update batch item
224
+ */
225
+ public updateItem(update: BatchProgressUpdate): void {
226
+ const item = this.state.items.get(update.itemId);
227
+ if (!item) {
228
+ this.log(`Item not found: ${update.itemId}`);
229
+ return;
230
+ }
231
+
232
+ if (update.status) item.status = update.status;
233
+ if (update.progress !== undefined) item.progress = update.progress;
234
+ if (update.error) item.error = update.error;
235
+ if (update.result !== undefined) item.result = update.result;
236
+ if (update.label) item.label = update.label;
237
+
238
+ // Track status changes
239
+ if (update.status === 'completed') {
240
+ item.endTime = Date.now();
241
+ this.state.completedCount++;
242
+ if (!item.error) this.state.successCount++;
243
+ } else if (update.status === 'failed') {
244
+ item.endTime = Date.now();
245
+ this.state.completedCount++;
246
+ this.state.failedCount++;
247
+ }
248
+
249
+ this.throttledRender();
250
+
251
+ const overallProgress = this.getOverallProgress();
252
+
253
+ const event: BatchItemUpdateEvent = {
254
+ ...item,
255
+ totalCompleted: this.state.completedCount,
256
+ totalFailed: this.state.failedCount,
257
+ overallProgress,
258
+ };
259
+
260
+ this.dispatchEvent(
261
+ new CustomEvent('itemupdate', {
262
+ detail: event,
263
+ bubbles: true,
264
+ composed: true,
265
+ })
266
+ );
267
+
268
+ // Check if batch is complete
269
+ if (this.state.completedCount === this.state.totalItems && this.state.status === 'processing') {
270
+ this.complete();
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Complete a batch item
276
+ */
277
+ public completeItem(itemId: string, result?: any): void {
278
+ this.updateItem({
279
+ itemId,
280
+ status: 'completed',
281
+ progress: 100,
282
+ result,
283
+ });
284
+
285
+ const item = this.state.items.get(itemId);
286
+ if (!item) return;
287
+
288
+ const event: BatchItemCompleteEvent = {
289
+ item,
290
+ totalCompleted: this.state.completedCount,
291
+ remainingItems: this.state.totalItems - this.state.completedCount,
292
+ };
293
+
294
+ this.dispatchEvent(
295
+ new CustomEvent('itemcomplete', {
296
+ detail: event,
297
+ bubbles: true,
298
+ composed: true,
299
+ })
300
+ );
301
+ }
302
+
303
+ /**
304
+ * Fail a batch item
305
+ */
306
+ public failItem(itemId: string, error: string): void {
307
+ this.updateItem({
308
+ itemId,
309
+ status: 'failed',
310
+ error,
311
+ });
312
+
313
+ const item = this.state.items.get(itemId);
314
+ if (!item) return;
315
+
316
+ const event: BatchItemFailedEvent = {
317
+ item,
318
+ error,
319
+ totalFailed: this.state.failedCount,
320
+ };
321
+
322
+ this.dispatchEvent(
323
+ new CustomEvent('itemfailed', {
324
+ detail: event,
325
+ bubbles: true,
326
+ composed: true,
327
+ })
328
+ );
329
+ }
330
+
331
+ /**
332
+ * Complete batch processing
333
+ */
334
+ public complete(): void {
335
+ this.state.status = 'completed';
336
+ this.state.endTime = Date.now();
337
+
338
+ this.render();
339
+ this.updateCursor();
340
+
341
+ const duration = this.state.endTime - (this.state.startTime || 0);
342
+ const averageRate = this.state.totalItems / (duration / 1000);
343
+
344
+ const event: BatchCompleteEvent = {
345
+ totalItems: this.state.totalItems,
346
+ successCount: this.state.successCount,
347
+ failedCount: this.state.failedCount,
348
+ duration,
349
+ averageRate,
350
+ startTime: this.state.startTime!,
351
+ endTime: this.state.endTime,
352
+ };
353
+
354
+ this.dispatchEvent(
355
+ new CustomEvent('batchcomplete', {
356
+ detail: event,
357
+ bubbles: true,
358
+ composed: true,
359
+ })
360
+ );
361
+
362
+ this.log('Batch completed', event);
363
+ }
364
+
365
+ /**
366
+ * Cancel batch processing
367
+ */
368
+ public cancel(reason?: string): void {
369
+ this.state.status = 'cancelled';
370
+ this.state.endTime = Date.now();
371
+
372
+ // Cancel all pending items
373
+ let cancelledCount = 0;
374
+ this.state.items.forEach((item) => {
375
+ if (item.status === 'pending' || item.status === 'processing') {
376
+ item.status = 'cancelled';
377
+ cancelledCount++;
378
+ }
379
+ });
380
+
381
+ this.render();
382
+ this.updateCursor();
383
+
384
+ const event: BatchCancelEvent = {
385
+ completedCount: this.state.completedCount,
386
+ failedCount: this.state.failedCount,
387
+ cancelledCount,
388
+ reason,
389
+ };
390
+
391
+ this.dispatchEvent(
392
+ new CustomEvent('batchcancel', {
393
+ detail: event,
394
+ bubbles: true,
395
+ composed: true,
396
+ })
397
+ );
398
+
399
+ this.log('Batch cancelled', event);
400
+ }
401
+
402
+ /**
403
+ * Reset batch to initial state
404
+ */
405
+ public reset(): void {
406
+ this.state.status = 'idle';
407
+ this.state.items.clear();
408
+ this.state.totalItems = this.config.totalItems;
409
+ this.state.completedCount = 0;
410
+ this.state.failedCount = 0;
411
+ this.state.successCount = 0;
412
+ this.state.currentConcurrency = 0;
413
+ this.state.startTime = null;
414
+ this.state.endTime = null;
415
+ this.state.message = this.config.message;
416
+
417
+ this.render();
418
+ this.log('Batch reset');
419
+ }
420
+
421
+ /**
422
+ * Get overall progress percentage
423
+ */
424
+ public getOverallProgress(): number {
425
+ if (this.state.totalItems === 0) return 0;
426
+ return (this.state.completedCount / this.state.totalItems) * 100;
427
+ }
428
+
429
+ /**
430
+ * Get processing rate (items/second)
431
+ */
432
+ public getRate(): number {
433
+ if (!this.state.startTime || this.state.completedCount === 0) return 0;
434
+ const elapsed = (Date.now() - this.state.startTime) / 1000;
435
+ return this.state.completedCount / elapsed;
436
+ }
437
+
438
+ /**
439
+ * Get batch statistics
440
+ */
441
+ public getStats() {
442
+ return {
443
+ total: this.state.totalItems,
444
+ completed: this.state.completedCount,
445
+ success: this.state.successCount,
446
+ failed: this.state.failedCount,
447
+ pending: this.state.totalItems - this.state.completedCount,
448
+ progress: this.getOverallProgress(),
449
+ rate: this.getRate(),
450
+ duration: this._calculateDuration(),
451
+ };
452
+ }
453
+
454
+ /**
455
+ * Throttled render to avoid excessive updates
456
+ */
457
+ private throttledRender(): void {
458
+ if (this.updateThrottleTimer) return;
459
+
460
+ this.updateThrottleTimer = globalThis.setTimeout(() => {
461
+ this.render();
462
+ this.updateThrottleTimer = null;
463
+ }, 100);
464
+ }
465
+
466
+ /**
467
+ * Calculate duration based on state
468
+ */
469
+ private _calculateDuration(): number {
470
+ if (this.state.endTime) {
471
+ return this.state.endTime - (this.state.startTime || 0);
472
+ }
473
+ if (this.state.startTime) {
474
+ return Date.now() - this.state.startTime;
475
+ }
476
+ return 0;
477
+ }
478
+
479
+ /**
480
+ * Get rate display HTML
481
+ */
482
+ private _getRateDisplay(rate: number): string {
483
+ if (!this.config.showRate) {
484
+ return '';
485
+ }
486
+ return `
487
+ <div class="stat-item">
488
+ <span class="stat-label">Rate</span>
489
+ <span class="stat-value rate">${rate.toFixed(1)}/s</span>
490
+ </div>
491
+ `;
492
+ }
493
+
494
+ /**
495
+ * Sync config attributes to host element
496
+ */
497
+ private _syncAttributes(): void {
498
+ if (this.config.size && this.getAttribute('size') !== this.config.size) {
499
+ this.setAttribute('size', this.config.size);
500
+ }
501
+ if (this.config.variant && this.getAttribute('variant') !== this.config.variant) {
502
+ this.setAttribute('variant', this.config.variant);
503
+ }
504
+ if (this.config.animation && this.getAttribute('animation') !== this.config.animation) {
505
+ this.setAttribute('animation', this.config.animation);
506
+ }
507
+ }
508
+
509
+ /**
510
+ * Get status badge CSS class
511
+ */
512
+ private _getStatusBadgeClass(status: string): string {
513
+ switch (status) {
514
+ case 'processing':
515
+ return 'processing';
516
+ case 'completed':
517
+ return 'completed';
518
+ case 'cancelled':
519
+ return 'cancelled';
520
+ default:
521
+ return '';
522
+ }
523
+ }
524
+
525
+ /**
526
+ * Get status display text
527
+ */
528
+ private _getStatusText(status: string): string {
529
+ switch (status) {
530
+ case 'idle':
531
+ return 'Ready';
532
+ case 'processing':
533
+ return 'Processing';
534
+ case 'completed':
535
+ return 'Completed';
536
+ case 'cancelled':
537
+ return 'Cancelled';
538
+ default:
539
+ return '';
540
+ }
541
+ }
542
+
543
+ /**
544
+ * Get stats section HTML
545
+ */
546
+ private _getStatsHtml(stats: ReturnType<typeof this.getStats>, rate: number): string {
547
+ if (!this.config.showStats) {
548
+ return '';
549
+ }
550
+ return `
551
+ <div class="stats">
552
+ <div class="stat-item">
553
+ <span class="stat-label">Total</span>
554
+ <span class="stat-value">${stats.total}</span>
555
+ </div>
556
+ <div class="stat-item">
557
+ <span class="stat-label">Success</span>
558
+ <span class="stat-value success">${stats.success}</span>
559
+ </div>
560
+ <div class="stat-item">
561
+ <span class="stat-label">Failed</span>
562
+ <span class="stat-value error">${stats.failed}</span>
563
+ </div>
564
+ ${this._getRateDisplay(rate)}
565
+ </div>
566
+ `;
567
+ }
568
+
569
+ /**
570
+ * Get progress bar HTML
571
+ */
572
+ private _getProgressBarHtml(
573
+ overallProgress: number,
574
+ stats: ReturnType<typeof this.getStats>
575
+ ): string {
576
+ if (!this.config.showProgressBar) {
577
+ return '';
578
+ }
579
+ return `
580
+ <div class="overall-progress">
581
+ <div class="progress-header">
582
+ <span>Overall Progress</span>
583
+ <span>${stats.completed} / ${stats.total} (${overallProgress.toFixed(0)}%)</span>
584
+ </div>
585
+ <div class="progress-bar">
586
+ <div class="progress-fill" style="width: ${overallProgress}%"></div>
587
+ </div>
588
+ </div>
589
+ `;
590
+ }
591
+
592
+ /**
593
+ * Get controls HTML
594
+ */
595
+ private _getControlsHtml(): string {
596
+ if (!this.config.allowCancel || this.state.status !== 'processing') {
597
+ return '';
598
+ }
599
+ const disabledAttr = this.config.disabled ? 'disabled' : '';
600
+ return `
601
+ <div class="controls">
602
+ <button class="cancel-btn" id="cancel-btn" ${disabledAttr}>
603
+ ${this.config.cancelLabel}
604
+ </button>
605
+ </div>
606
+ `;
607
+ }
608
+
609
+ /**
610
+ * Attach event listeners to rendered elements
611
+ */
612
+ private _attachEventListeners(): void {
613
+ if (!this.config.allowCancel) return;
614
+
615
+ const cancelBtn = this.shadowRoot?.getElementById('cancel-btn');
616
+ if (cancelBtn) {
617
+ cancelBtn.addEventListener('click', () => this.cancel('User cancelled'));
618
+ }
619
+ }
620
+
621
+ /**
622
+ * Render the component
623
+ */
624
+ protected render(): void {
625
+ if (!this.shadowRoot) return;
626
+
627
+ this._syncAttributes();
628
+
629
+ const overallProgress = this.getOverallProgress();
630
+ const rate = this.getRate();
631
+ const stats = this.getStats();
632
+ const statusBadgeClass = this._getStatusBadgeClass(this.state.status);
633
+ const statusText = this._getStatusText(this.state.status);
634
+
635
+ const statsHtml = this._getStatsHtml(stats, rate);
636
+ const progressHtml = this._getProgressBarHtml(overallProgress, stats);
637
+ const itemsHtml = this.config.showItems ? this.renderItems() : '';
638
+ const controlsHtml = this._getControlsHtml();
639
+
640
+ this.shadowRoot.innerHTML = `
641
+ <style>${styles}</style>
642
+ <div class="container">
643
+ <div class="header">
644
+ <div class="status-message">${this.state.message}</div>
645
+ ${statusBadgeClass ? `<span class="status-badge ${statusBadgeClass}">${statusText}</span>` : ''}
646
+ </div>
647
+ ${statsHtml}
648
+ ${progressHtml}
649
+ ${itemsHtml}
650
+ ${controlsHtml}
651
+ </div>
652
+ `;
653
+
654
+ this._attachEventListeners();
655
+ }
656
+
657
+ /**
658
+ * Render batch items
659
+ */
660
+ private renderItems(): string {
661
+ if (this.state.items.size === 0) {
662
+ return `
663
+ <div class="empty-state">
664
+ <div class="empty-state-icon">📦</div>
665
+ <div>No items in batch</div>
666
+ </div>
667
+ `;
668
+ }
669
+
670
+ const items = Array.from(this.state.items.values());
671
+ const displayItems = items.slice(0, this.config.maxDisplayItems);
672
+
673
+ const itemsHtml = displayItems
674
+ .map((item) => {
675
+ const statusIcon = this.getStatusIcon(item.status);
676
+ const collapsed =
677
+ this.config.collapseCompleted && item.status === 'completed' ? 'collapsed' : '';
678
+
679
+ const progressBarHtml =
680
+ item.status === 'processing' && item.progress !== undefined
681
+ ? `
682
+ <div class="item-progress-bar">
683
+ <div class="item-progress-fill" style="width: ${item.progress}%"></div>
684
+ </div>
685
+ `
686
+ : '';
687
+
688
+ const errorHtml = item.error
689
+ ? `
690
+ <div class="item-error">
691
+ ${item.error}
692
+ </div>
693
+ `
694
+ : '';
695
+
696
+ return `
697
+ <div class="batch-item ${item.status} ${collapsed}">
698
+ <div class="item-header">
699
+ <span class="item-label">${item.label || item.id}</span>
700
+ <span class="item-status ${item.status}">
701
+ <span class="item-status-icon">${statusIcon}</span>
702
+ ${item.status}
703
+ </span>
704
+ </div>
705
+ ${progressBarHtml}
706
+ ${errorHtml}
707
+ </div>
708
+ `;
709
+ })
710
+ .join('');
711
+
712
+ return `
713
+ <div class="items-container">
714
+ ${itemsHtml}
715
+ </div>
716
+ `;
717
+ }
718
+
719
+ /**
720
+ * Get status icon
721
+ */
722
+ private getStatusIcon(status: BatchItemStatus): string {
723
+ switch (status) {
724
+ case 'pending':
725
+ return '⏳';
726
+ case 'processing':
727
+ return '<span class="spinner"></span>';
728
+ case 'completed':
729
+ return '✅';
730
+ case 'failed':
731
+ return '❌';
732
+ case 'cancelled':
733
+ return '⛔';
734
+ default:
735
+ return '';
736
+ }
737
+ }
738
+
739
+ /**
740
+ * Get/set disabled state
741
+ */
742
+ override get disabled(): boolean {
743
+ return this.config.disabled;
744
+ }
745
+
746
+ override set disabled(value: boolean) {
747
+ this.config.disabled = value;
748
+ if (value) {
749
+ this.setAttribute('disabled', '');
750
+ } else {
751
+ this.removeAttribute('disabled');
752
+ }
753
+ this.render();
754
+ }
755
+ }
756
+
757
+ // Define custom element
758
+ if (!customElements.get('batch-progress')) {
759
+ customElements.define('batch-progress', BatchProgress);
760
+ }