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,673 @@
1
+ import { AIControl } from '../base/AIControl';
2
+ import { throttle, formatBytes } from '../base/utils';
3
+ import type {
4
+ ModelLoaderConfig,
5
+ ModelLoaderState,
6
+ ModelStage,
7
+ StageState,
8
+ StageStatus,
9
+ StageUpdate,
10
+ StageChangeEvent,
11
+ LoadCompleteEvent,
12
+ LoadErrorEvent,
13
+ } from './types';
14
+ import { styles } from './styles';
15
+
16
+ /**
17
+ * ModelLoader Component
18
+ *
19
+ * Displays multi-stage progress for AI model loading operations.
20
+ * Shows download progress, initialization stages, memory usage, and ETA.
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * // Create the component
25
+ * const loader = new ModelLoader({
26
+ * modelName: 'GPT-4 Vision',
27
+ * stages: ['download', 'load', 'initialize', 'ready'],
28
+ * showBytes: true,
29
+ * showMemoryUsage: true,
30
+ * showETA: true,
31
+ * });
32
+ *
33
+ * document.body.appendChild(loader);
34
+ *
35
+ * // Start loading
36
+ * loader.start();
37
+ *
38
+ * // Update download stage
39
+ * loader.updateStage('download', {
40
+ * bytesLoaded: 50000000,
41
+ * totalBytes: 100000000,
42
+ * message: 'Downloading model weights...'
43
+ * });
44
+ *
45
+ * // Move to next stage
46
+ * loader.setStage('load', { message: 'Loading into memory...' });
47
+ *
48
+ * // Complete
49
+ * loader.complete();
50
+ *
51
+ * // Listen to events
52
+ * loader.addEventListener('stagechange', (e) => {
53
+ * console.log('Stage changed', e.detail);
54
+ * });
55
+ * ```
56
+ *
57
+ * @fires loadstart - Fired when loading starts
58
+ * @fires stagechange - Fired when stage changes
59
+ * @fires stageupdate - Fired when stage progress updates
60
+ * @fires loadcomplete - Fired when loading completes
61
+ * @fires loaderror - Fired when an error occurs
62
+ */
63
+ export class ModelLoader extends AIControl {
64
+ protected override config: Required<ModelLoaderConfig>;
65
+ private state: ModelLoaderState;
66
+ private readonly updateThrottled: (update: StageUpdate) => void;
67
+
68
+ static get observedAttributes() {
69
+ return ['model-name', 'disabled', 'size', 'variant', 'animation'];
70
+ }
71
+
72
+ constructor(config: ModelLoaderConfig = {}) {
73
+ super({
74
+ debug: config.debug ?? false,
75
+ className: config.className,
76
+ ariaLabel: config.ariaLabel ?? 'Model Loading Progress',
77
+ cursorFeedback: config.cursorFeedback ?? true,
78
+ });
79
+
80
+ // Set default configuration
81
+ this.config = {
82
+ stages: config.stages ?? ['download', 'load', 'initialize', 'ready'],
83
+ modelName: config.modelName ?? 'AI Model',
84
+ showBytes: config.showBytes ?? true,
85
+ showMemoryUsage: config.showMemoryUsage ?? true,
86
+ showETA: config.showETA ?? true,
87
+ showRetryButton: config.showRetryButton ?? true,
88
+ smoothProgress: config.smoothProgress ?? true,
89
+ updateThrottle: config.updateThrottle ?? 100,
90
+ retryLabel: config.retryLabel ?? 'Retry',
91
+ cursorFeedback: config.cursorFeedback ?? true,
92
+ debug: config.debug ?? false,
93
+ className: config.className ?? '',
94
+ ariaLabel: config.ariaLabel ?? 'Model Loading Progress',
95
+ size: config.size ?? 'default',
96
+ variant: config.variant ?? 'default',
97
+ animation: config.animation ?? 'none',
98
+ };
99
+
100
+ // Initialize state
101
+ const initialStages: Record<ModelStage, StageState> = {
102
+ download: { status: 'pending', progress: 0 },
103
+ load: { status: 'pending', progress: 0 },
104
+ initialize: { status: 'pending', progress: 0 },
105
+ ready: { status: 'pending', progress: 0 },
106
+ };
107
+
108
+ this.state = {
109
+ currentStage: this.config.stages[0]!,
110
+ stages: initialStages,
111
+ isLoading: false,
112
+ hasError: false,
113
+ startTime: 0,
114
+ };
115
+
116
+ // Create throttled update function
117
+ this.updateThrottled = throttle(
118
+ this._updateStageInternal.bind(this),
119
+ this.config.updateThrottle
120
+ );
121
+
122
+ // Attach shadow DOM
123
+ this.attachShadow({ mode: 'open' });
124
+ }
125
+
126
+ override connectedCallback(): void {
127
+ super.connectedCallback();
128
+ this.log('ModelLoader mounted');
129
+ }
130
+
131
+ protected override getDefaultRole(): string {
132
+ return 'progressbar';
133
+ }
134
+
135
+ /**
136
+ * Update cursor based on component state
137
+ */
138
+ private updateCursor(): void {
139
+ if (!this.config.cursorFeedback) return;
140
+
141
+ if (this.state.isLoading) {
142
+ this.style.cursor = 'progress';
143
+ } else if (this.state.hasError) {
144
+ this.style.cursor = 'not-allowed';
145
+ } else {
146
+ this.style.cursor = 'default';
147
+ }
148
+ }
149
+
150
+ protected override handleAttributeChange(
151
+ name: string,
152
+ _oldValue: string,
153
+ newValue: string
154
+ ): void {
155
+ switch (name) {
156
+ case 'model-name':
157
+ this.config.modelName = newValue || 'AI Model';
158
+ this.render();
159
+ break;
160
+ case 'disabled':
161
+ this._disabled = newValue !== null;
162
+ this.render();
163
+ break;
164
+ case 'size':
165
+ this.config.size = newValue as any;
166
+ this.render();
167
+ break;
168
+ case 'variant':
169
+ this.config.variant = newValue as any;
170
+ this.render();
171
+ break;
172
+ case 'animation':
173
+ this.config.animation = newValue as any;
174
+ this.render();
175
+ break;
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Start loading
181
+ */
182
+ public start(initialMessage?: string): void {
183
+ if (this.state.isLoading) {
184
+ this.log('Already loading');
185
+ return;
186
+ }
187
+
188
+ // Reset all stages
189
+ const resetStages: Record<ModelStage, StageState> = {
190
+ download: { status: 'pending', progress: 0 },
191
+ load: { status: 'pending', progress: 0 },
192
+ initialize: { status: 'pending', progress: 0 },
193
+ ready: { status: 'pending', progress: 0 },
194
+ };
195
+
196
+ // Set first stage to in-progress
197
+ const firstStage = this.config.stages[0]!;
198
+ resetStages[firstStage] = {
199
+ status: 'in-progress',
200
+ progress: 0,
201
+ message: initialMessage,
202
+ startTime: Date.now(),
203
+ };
204
+
205
+ this.state = {
206
+ currentStage: firstStage,
207
+ stages: resetStages,
208
+ isLoading: true,
209
+ hasError: false,
210
+ startTime: Date.now(),
211
+ };
212
+
213
+ this.startTimer();
214
+ this.render();
215
+ this.updateCursor();
216
+ this.emit('loadstart', { stage: firstStage, timestamp: Date.now() });
217
+ this.log('Loading started', this.state);
218
+ }
219
+
220
+ /**
221
+ * Update current stage progress
222
+ */
223
+ public updateStage(stage: ModelStage, update: Omit<StageUpdate, 'stage'>): void {
224
+ if (!this.state.isLoading || this.state.hasError) {
225
+ return;
226
+ }
227
+
228
+ this.updateThrottled({ stage, ...update });
229
+ }
230
+
231
+ private _updateStageInternal(update: StageUpdate): void {
232
+ const { stage, progress, bytesLoaded, totalBytes, message, memoryUsage } = update;
233
+
234
+ if (!this.state.stages[stage]) {
235
+ this.logError(`Invalid stage: ${stage}`);
236
+ return;
237
+ }
238
+
239
+ // Calculate progress if bytes are provided
240
+ let calculatedProgress = progress;
241
+ if (calculatedProgress === undefined && bytesLoaded !== undefined && totalBytes !== undefined) {
242
+ calculatedProgress = this.calculatePercentage(bytesLoaded, totalBytes);
243
+ }
244
+
245
+ // Update stage state
246
+ const currentStageState = this.state.stages[stage];
247
+ const updatedStage: StageState = {
248
+ ...currentStageState,
249
+ progress: calculatedProgress ?? currentStageState.progress,
250
+ message,
251
+ bytesLoaded,
252
+ totalBytes,
253
+ };
254
+
255
+ this.state = {
256
+ ...this.state,
257
+ stages: {
258
+ ...this.state.stages,
259
+ [stage]: updatedStage,
260
+ },
261
+ memoryUsage: memoryUsage ?? this.state.memoryUsage,
262
+ };
263
+
264
+ this.render();
265
+ this.emit('stageupdate', { stage, ...updatedStage });
266
+ this.log('Stage updated', stage, updatedStage);
267
+ }
268
+
269
+ /**
270
+ * Move to a specific stage
271
+ */
272
+ public setStage(stage: ModelStage, options: { message?: string; progress?: number } = {}): void {
273
+ if (!this.state.isLoading || this.state.hasError) {
274
+ return;
275
+ }
276
+
277
+ if (!this.config.stages.includes(stage)) {
278
+ this.logError(`Stage ${stage} not in configured stages`);
279
+ return;
280
+ }
281
+
282
+ const previousStage = this.state.currentStage;
283
+
284
+ // Mark previous stage as completed
285
+ const prevStageState = this.state.stages[previousStage];
286
+ this.state.stages[previousStage] = {
287
+ ...prevStageState,
288
+ status: 'completed',
289
+ progress: 100,
290
+ endTime: Date.now(),
291
+ };
292
+
293
+ // Set new stage as in-progress
294
+ const newStageState = this.state.stages[stage];
295
+ this.state.stages[stage] = {
296
+ ...newStageState,
297
+ status: 'in-progress',
298
+ progress: options.progress ?? 0,
299
+ message: options.message,
300
+ startTime: Date.now(),
301
+ };
302
+
303
+ this.state = {
304
+ ...this.state,
305
+ currentStage: stage,
306
+ };
307
+
308
+ this.render();
309
+
310
+ const event: StageChangeEvent = {
311
+ previousStage,
312
+ currentStage: stage,
313
+ timestamp: Date.now(),
314
+ };
315
+
316
+ this.emit('stagechange', event);
317
+ this.log('Stage changed', event);
318
+ }
319
+
320
+ /**
321
+ * Mark current stage as completed and move to next
322
+ */
323
+ public completeStage(nextMessage?: string): void {
324
+ const currentStageIndex = this.config.stages.indexOf(this.state.currentStage);
325
+ const nextStage = this.config.stages[currentStageIndex + 1];
326
+
327
+ if (!nextStage) {
328
+ // No more stages, complete loading
329
+ this.complete();
330
+ return;
331
+ }
332
+
333
+ this.setStage(nextStage, { message: nextMessage });
334
+ }
335
+
336
+ /**
337
+ * Complete loading
338
+ */
339
+ public complete(): void {
340
+ if (!this.state.isLoading) {
341
+ return;
342
+ }
343
+
344
+ const duration = this.getElapsedTime();
345
+
346
+ // Mark all stages as completed
347
+ const completedStages: Record<ModelStage, StageState> = { ...this.state.stages };
348
+ this.config.stages.forEach((stage) => {
349
+ const stageState = completedStages[stage];
350
+ completedStages[stage] = {
351
+ ...stageState,
352
+ status: 'completed',
353
+ progress: 100,
354
+ endTime: stageState.endTime ?? Date.now(),
355
+ };
356
+ });
357
+
358
+ this.state = {
359
+ ...this.state,
360
+ stages: completedStages,
361
+ isLoading: false,
362
+ currentStage: 'ready',
363
+ };
364
+
365
+ const event: LoadCompleteEvent = {
366
+ duration,
367
+ memoryUsage: this.state.memoryUsage,
368
+ stages: this.state.stages,
369
+ };
370
+
371
+ this.render();
372
+ this.updateCursor();
373
+ this.emit('loadcomplete', event);
374
+ this.log('Loading completed', event);
375
+ }
376
+
377
+ /**
378
+ * Set error state
379
+ */
380
+ public error(message: string, stage?: ModelStage): void {
381
+ const errorStage = stage ?? this.state.currentStage;
382
+ const errorStageState = this.state.stages[errorStage];
383
+
384
+ this.state = {
385
+ ...this.state,
386
+ hasError: true,
387
+ errorMessage: message,
388
+ isLoading: false,
389
+ stages: {
390
+ ...this.state.stages,
391
+ [errorStage]: {
392
+ ...errorStageState,
393
+ status: 'error',
394
+ },
395
+ },
396
+ };
397
+
398
+ const event: LoadErrorEvent = {
399
+ stage: errorStage,
400
+ message,
401
+ timestamp: Date.now(),
402
+ };
403
+
404
+ this.render();
405
+ this.updateCursor();
406
+ this.emit('loaderror', event);
407
+ this.logError('Loading error', new Error(message));
408
+ }
409
+
410
+ /**
411
+ * Retry loading from the beginning
412
+ */
413
+ public retry(): void {
414
+ this.start();
415
+ this.emit('loadretry', { timestamp: Date.now() });
416
+ this.log('Retrying load');
417
+ }
418
+
419
+ /**
420
+ * Reset the component
421
+ */
422
+ public reset(): void {
423
+ const resetStages: Record<ModelStage, StageState> = {
424
+ download: { status: 'pending', progress: 0 },
425
+ load: { status: 'pending', progress: 0 },
426
+ initialize: { status: 'pending', progress: 0 },
427
+ ready: { status: 'pending', progress: 0 },
428
+ };
429
+
430
+ this.state = {
431
+ currentStage: this.config.stages[0]!,
432
+ stages: resetStages,
433
+ isLoading: false,
434
+ hasError: false,
435
+ startTime: 0,
436
+ };
437
+
438
+ this.render();
439
+ this.log('Component reset');
440
+ }
441
+
442
+ /**
443
+ * Get status icon for a stage
444
+ */
445
+ private getStageIcon(status: StageStatus): string {
446
+ switch (status) {
447
+ case 'pending':
448
+ return '○';
449
+ case 'in-progress':
450
+ return '◐';
451
+ case 'completed':
452
+ return '✓';
453
+ case 'error':
454
+ return '✕';
455
+ default:
456
+ return '○';
457
+ }
458
+ }
459
+
460
+ /**
461
+ * Calculate ETA for remaining stages
462
+ */
463
+ private calculateETA(): string {
464
+ if (!this.state.isLoading || this.state.hasError) {
465
+ return '--';
466
+ }
467
+
468
+ const elapsed = this.getElapsedTime();
469
+ const currentStageIndex = this.config.stages.indexOf(this.state.currentStage);
470
+ const totalStages = this.config.stages.length;
471
+ const completedStages = currentStageIndex;
472
+ const currentProgress = this.state.stages[this.state.currentStage]?.progress ?? 0;
473
+
474
+ if (completedStages === 0 && currentProgress === 0) {
475
+ return '--';
476
+ }
477
+
478
+ // Estimate based on completed stages + current progress
479
+ const effectiveProgress = (completedStages + currentProgress / 100) / totalStages;
480
+
481
+ if (effectiveProgress === 0) {
482
+ return '--';
483
+ }
484
+
485
+ const estimatedTotal = elapsed / effectiveProgress;
486
+ const remaining = estimatedTotal - elapsed;
487
+
488
+ return this.formatDuration(Math.max(0, remaining));
489
+ }
490
+
491
+ /**
492
+ * Get overall status for rendering
493
+ */
494
+ private getOverallStatus(): string {
495
+ if (this.state.hasError) return 'error';
496
+ if (this.state.isLoading) return 'loading';
497
+ if (!this.state.isLoading && this.state.stages.ready?.status === 'completed')
498
+ return 'completed';
499
+ return 'idle';
500
+ }
501
+
502
+ /**
503
+ * Get status badge text
504
+ */
505
+ private getStatusBadgeText(overallStatus: string): string {
506
+ if (this.state.hasError) return 'Error';
507
+ if (this.state.isLoading) return 'Loading';
508
+ if (overallStatus === 'completed') return 'Ready';
509
+ return 'Idle';
510
+ }
511
+
512
+ /**
513
+ * Generate HTML for all stages
514
+ */
515
+ private generateStagesHtml(): string {
516
+ return this.config.stages
517
+ .map((stage) => {
518
+ const stageState = this.state.stages[stage];
519
+ if (!stageState) return '';
520
+
521
+ const icon = this.getStageIcon(stageState.status);
522
+ const progressText = `${Math.round(stageState.progress)}%`;
523
+ const messageHtml = stageState.message
524
+ ? `<div class="stage-message">${stageState.message}</div>`
525
+ : '';
526
+
527
+ return `
528
+ <div class="stage ${stageState.status}">
529
+ <div class="stage-header">
530
+ <div class="stage-info">
531
+ <div class="stage-icon ${stageState.status}">${icon}</div>
532
+ <span class="stage-name">${stage}</span>
533
+ </div>
534
+ <span class="stage-progress-text">${progressText}</span>
535
+ </div>
536
+ ${messageHtml}
537
+ <div class="progress-bar">
538
+ <div class="progress-fill ${stageState.status === 'error' ? 'error' : ''}" style="width: ${stageState.progress}%"></div>
539
+ </div>
540
+ </div>
541
+ `;
542
+ })
543
+ .join('');
544
+ }
545
+
546
+ /**
547
+ * Generate HTML for stats section
548
+ */
549
+ private generateStatsHtml(): string {
550
+ const statsItems = [];
551
+
552
+ // Bytes (for download stage)
553
+ if (this.config.showBytes && this.state.stages.download) {
554
+ const { bytesLoaded, totalBytes } = this.state.stages.download;
555
+ if (bytesLoaded !== undefined && totalBytes !== undefined) {
556
+ statsItems.push(`
557
+ <div class="stat-item">
558
+ <span class="stat-label">Downloaded</span>
559
+ <span class="stat-value">${formatBytes(bytesLoaded)} / ${formatBytes(totalBytes)}</span>
560
+ </div>
561
+ `);
562
+ }
563
+ }
564
+
565
+ // Memory usage
566
+ if (this.config.showMemoryUsage && this.state.memoryUsage !== undefined) {
567
+ statsItems.push(`
568
+ <div class="stat-item">
569
+ <span class="stat-label">Memory</span>
570
+ <span class="stat-value">${this.state.memoryUsage.toFixed(0)} MB</span>
571
+ </div>
572
+ `);
573
+ }
574
+
575
+ // ETA
576
+ if (this.config.showETA && this.state.isLoading) {
577
+ statsItems.push(`
578
+ <div class="stat-item">
579
+ <span class="stat-label">ETA</span>
580
+ <span class="stat-value">${this.calculateETA()}</span>
581
+ </div>
582
+ `);
583
+ }
584
+
585
+ return statsItems.length > 0 ? `<div class="stats">${statsItems.join('')}</div>` : '';
586
+ }
587
+
588
+ /**
589
+ * Render the component
590
+ */
591
+ protected render(): void {
592
+ if (!this.shadowRoot) return;
593
+
594
+ // Sync attributes to host element for CSS selectors
595
+ if (this.config.size && this.getAttribute('size') !== this.config.size) {
596
+ this.setAttribute('size', this.config.size);
597
+ }
598
+ if (this.config.variant && this.getAttribute('variant') !== this.config.variant) {
599
+ this.setAttribute('variant', this.config.variant);
600
+ }
601
+ if (this.config.animation && this.getAttribute('animation') !== this.config.animation) {
602
+ this.setAttribute('animation', this.config.animation);
603
+ }
604
+
605
+ const overallStatus = this.getOverallStatus();
606
+ const statusBadgeText = this.getStatusBadgeText(overallStatus);
607
+ const stagesHtml = this.generateStagesHtml();
608
+ const statsHtml = this.generateStatsHtml();
609
+
610
+ // Error message
611
+ const errorHtml = this.state.hasError
612
+ ? `
613
+ <div class="error-message">
614
+ <div class="error-icon">⚠</div>
615
+ <div class="error-text">${this.state.errorMessage}</div>
616
+ </div>
617
+ `
618
+ : '';
619
+
620
+ // Retry button
621
+ const retryButtonHtml =
622
+ this.config.showRetryButton && this.state.hasError
623
+ ? `
624
+ <button class="retry-button" aria-label="Retry loading">
625
+ ${this.config.retryLabel}
626
+ </button>
627
+ `
628
+ : '';
629
+
630
+ this.shadowRoot.innerHTML = `
631
+ ${styles}
632
+ <div class="model-loader ${overallStatus} ${this.config.className}">
633
+ <div class="header">
634
+ <div class="model-name">${this.config.modelName}</div>
635
+ <div class="status-badge ${overallStatus}">${statusBadgeText}</div>
636
+ </div>
637
+ <div class="stages">
638
+ ${stagesHtml}
639
+ </div>
640
+ ${statsHtml}
641
+ ${errorHtml}
642
+ ${retryButtonHtml}
643
+ </div>
644
+ `;
645
+
646
+ // Attach event listeners
647
+ if (this.config.showRetryButton && this.state.hasError) {
648
+ const retryBtn = this.shadowRoot.querySelector('.retry-button');
649
+ if (retryBtn) {
650
+ retryBtn.addEventListener('click', () => this.retry());
651
+ }
652
+ }
653
+ }
654
+
655
+ /**
656
+ * Get current state (for debugging/inspection)
657
+ */
658
+ public getState(): Readonly<ModelLoaderState> {
659
+ return { ...this.state };
660
+ }
661
+
662
+ /**
663
+ * Get current configuration
664
+ */
665
+ public getConfig(): Readonly<Required<ModelLoaderConfig>> {
666
+ return { ...this.config };
667
+ }
668
+ }
669
+
670
+ // Register the custom element
671
+ if (!customElements.get('model-loader')) {
672
+ customElements.define('model-loader', ModelLoader);
673
+ }
@@ -0,0 +1,2 @@
1
+ export * from './ModelLoader';
2
+ export * from './types';