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,563 @@
1
+ import { AIControl } from '../base/AIControl';
2
+ import { throttle, formatTime } from '../base/utils';
3
+ import type {
4
+ QueueProgressConfig,
5
+ QueueProgressState,
6
+ QueueStatus,
7
+ QueueUpdate,
8
+ PositionChangeEvent,
9
+ QueueStartEvent,
10
+ QueueCompleteEvent,
11
+ QueueErrorEvent,
12
+ } from './types';
13
+ import { styles } from './styles';
14
+
15
+ /**
16
+ * QueueProgress Component
17
+ *
18
+ * Displays queue position and estimated wait time for rate-limited AI APIs.
19
+ * Shows position updates, processing rate, and estimated wait time.
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * // Create the component
24
+ * const queue = new QueueProgress({
25
+ * position: 47,
26
+ * queueSize: 120,
27
+ * estimatedWait: 180, // 3 minutes
28
+ * processingRate: 3, // 3 requests per second
29
+ * });
30
+ *
31
+ * document.body.appendChild(queue);
32
+ *
33
+ * // Start tracking
34
+ * queue.start();
35
+ *
36
+ * // Update position
37
+ * queue.update({
38
+ * position: 25,
39
+ * estimatedWait: 90
40
+ * });
41
+ *
42
+ * // Complete
43
+ * queue.complete();
44
+ *
45
+ * // Listen to events
46
+ * queue.addEventListener('positionchange', (e) => {
47
+ * console.log('Position changed', e.detail);
48
+ * });
49
+ * ```
50
+ *
51
+ * @fires queuestart - Fired when queue tracking starts
52
+ * @fires positionchange - Fired when position updates
53
+ * @fires queuecomplete - Fired when processing begins
54
+ * @fires queueerror - Fired when an error occurs
55
+ */
56
+ export class QueueProgress extends AIControl {
57
+ protected override config: Required<QueueProgressConfig>;
58
+ private state: QueueProgressState;
59
+ private readonly updateThrottled: (update: QueueUpdate) => void;
60
+ private timerInterval: ReturnType<typeof setInterval> | undefined;
61
+ private initialPosition: number = 0;
62
+
63
+ static get observedAttributes() {
64
+ return ['position', 'queue-size', 'disabled', 'size', 'variant', 'animation'];
65
+ }
66
+
67
+ constructor(config: QueueProgressConfig = {}) {
68
+ super({
69
+ debug: config.debug ?? false,
70
+ className: config.className,
71
+ ariaLabel: config.ariaLabel ?? 'Queue Progress',
72
+ });
73
+
74
+ // Set default configuration
75
+ this.config = {
76
+ position: config.position ?? 0,
77
+ queueSize: config.queueSize ?? 0,
78
+ estimatedWait: config.estimatedWait ?? 0,
79
+ processingRate: config.processingRate ?? 1,
80
+ showPosition: config.showPosition ?? true,
81
+ showWaitTime: config.showWaitTime ?? true,
82
+ showRate: config.showRate ?? true,
83
+ showQueueSize: config.showQueueSize ?? true,
84
+ showProgressBar: config.showProgressBar ?? true,
85
+ message: config.message ?? 'You are in the queue',
86
+ animate: config.animate ?? true,
87
+ updateThrottle: config.updateThrottle ?? 100,
88
+ cursorFeedback: config.cursorFeedback ?? true,
89
+ debug: config.debug ?? false,
90
+ className: config.className ?? '',
91
+ ariaLabel: config.ariaLabel ?? 'Queue Progress',
92
+ size: config.size ?? 'default',
93
+ variant: config.variant ?? 'default',
94
+ animation: config.animation ?? 'none',
95
+ };
96
+
97
+ // Initialize state
98
+ this.state = {
99
+ status: 'waiting',
100
+ position: this.config.position,
101
+ queueSize: this.config.queueSize,
102
+ estimatedWait: this.config.estimatedWait,
103
+ processingRate: this.config.processingRate,
104
+ startTime: 0,
105
+ message: this.config.message,
106
+ elapsedTime: 0,
107
+ };
108
+
109
+ this.initialPosition = this.config.position;
110
+
111
+ // Create throttled update function
112
+ this.updateThrottled = throttle(this._updateInternal.bind(this), this.config.updateThrottle);
113
+
114
+ // Attach shadow DOM
115
+ this.attachShadow({ mode: 'open' });
116
+ }
117
+
118
+ override connectedCallback(): void {
119
+ super.connectedCallback();
120
+ this.log('QueueProgress mounted');
121
+ }
122
+
123
+ override disconnectedCallback(): void {
124
+ this.stopTimer();
125
+ super.disconnectedCallback();
126
+ }
127
+
128
+ protected override getDefaultRole(): string {
129
+ return 'status';
130
+ }
131
+
132
+ protected override handleAttributeChange(
133
+ name: string,
134
+ _oldValue: string,
135
+ newValue: string
136
+ ): void {
137
+ switch (name) {
138
+ case 'position':
139
+ this.update({ position: Number.parseInt(newValue, 10) || 0 });
140
+ break;
141
+ case 'queue-size':
142
+ this.update({ queueSize: Number.parseInt(newValue, 10) || 0 });
143
+ break;
144
+ case 'disabled':
145
+ this._disabled = newValue !== null;
146
+ this.render();
147
+ break;
148
+ case 'size':
149
+ this.config.size = newValue as any;
150
+ this.render();
151
+ break;
152
+ case 'variant':
153
+ this.config.variant = newValue as any;
154
+ this.render();
155
+ break;
156
+ case 'animation':
157
+ this.config.animation = newValue as any;
158
+ this.render();
159
+ break;
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Start queue tracking
165
+ */
166
+ public start(message?: string): void {
167
+ this.state = {
168
+ ...this.state,
169
+ status: 'waiting',
170
+ startTime: Date.now(),
171
+ elapsedTime: 0,
172
+ message: message || this.config.message,
173
+ };
174
+
175
+ this.initialPosition = this.state.position;
176
+ this.startTimer();
177
+ this.render();
178
+ this.updateCursor();
179
+ this.emit('queuestart', {
180
+ position: this.state.position,
181
+ queueSize: this.state.queueSize,
182
+ estimatedWait: this.state.estimatedWait,
183
+ timestamp: Date.now(),
184
+ } as QueueStartEvent);
185
+ this.log('Queue tracking started', this.state);
186
+ }
187
+
188
+ /**
189
+ * Update queue position
190
+ */
191
+ public update(update: QueueUpdate): void {
192
+ if (this.state.status === 'completed' || this.state.status === 'error') {
193
+ return;
194
+ }
195
+
196
+ this.updateThrottled(update);
197
+ }
198
+
199
+ private _updateInternal(update: QueueUpdate): void {
200
+ const previousPosition = this.state.position;
201
+
202
+ this.state = {
203
+ ...this.state,
204
+ position: update.position ?? this.state.position,
205
+ queueSize: update.queueSize ?? this.state.queueSize,
206
+ estimatedWait: update.estimatedWait ?? this.state.estimatedWait,
207
+ processingRate: update.processingRate ?? this.state.processingRate,
208
+ message: update.message ?? this.state.message,
209
+ };
210
+
211
+ // Animate position number if changed
212
+ if (update.position !== undefined && update.position !== previousPosition) {
213
+ const positionEl = this.shadowRoot?.querySelector('.position-number');
214
+ if (positionEl && this.config.animate) {
215
+ positionEl.classList.add('changing');
216
+ setTimeout(() => positionEl.classList.remove('changing'), 500);
217
+ }
218
+
219
+ this.emit('positionchange', {
220
+ previousPosition,
221
+ currentPosition: this.state.position,
222
+ queueSize: this.state.queueSize,
223
+ estimatedWait: this.state.estimatedWait,
224
+ timestamp: Date.now(),
225
+ } as PositionChangeEvent);
226
+ }
227
+
228
+ this.render();
229
+ this.log('Position updated', this.state);
230
+ }
231
+
232
+ /**
233
+ * Mark as processing (reached front of queue)
234
+ */
235
+ public complete(): void {
236
+ this.state = {
237
+ ...this.state,
238
+ status: 'completed',
239
+ position: 0,
240
+ };
241
+
242
+ this.stopTimer();
243
+ this.render();
244
+ this.updateCursor();
245
+
246
+ const totalWaitTime = this.state.startTime > 0 ? Date.now() - this.state.startTime : 0;
247
+
248
+ this.emit('queuecomplete', {
249
+ totalWaitTime,
250
+ startPosition: this.initialPosition,
251
+ timestamp: Date.now(),
252
+ } as QueueCompleteEvent);
253
+
254
+ this.log('Queue completed', { totalWaitTime });
255
+ }
256
+
257
+ /**
258
+ * Cancel queue
259
+ */
260
+ public cancel(reason: string = 'Cancelled by user'): void {
261
+ this.state = {
262
+ ...this.state,
263
+ status: 'cancelled',
264
+ message: reason,
265
+ };
266
+
267
+ this.stopTimer();
268
+ this.render();
269
+ this.updateCursor();
270
+ this.log('Queue cancelled', reason);
271
+ }
272
+
273
+ /**
274
+ * Handle error
275
+ */
276
+ public error(errorMessage: string): void {
277
+ this.state = {
278
+ ...this.state,
279
+ status: 'error',
280
+ message: errorMessage,
281
+ };
282
+
283
+ this.stopTimer();
284
+ this.render();
285
+ this.updateCursor();
286
+
287
+ this.emit('queueerror', {
288
+ message: errorMessage,
289
+ position: this.state.position,
290
+ timestamp: Date.now(),
291
+ } as QueueErrorEvent);
292
+
293
+ this.logError('Queue error', new Error(errorMessage));
294
+ }
295
+
296
+ /**
297
+ * Reset to initial state
298
+ */
299
+ public reset(): void {
300
+ this.stopTimer();
301
+
302
+ this.state = {
303
+ status: 'waiting',
304
+ position: this.config.position,
305
+ queueSize: this.config.queueSize,
306
+ estimatedWait: this.config.estimatedWait,
307
+ processingRate: this.config.processingRate,
308
+ startTime: 0,
309
+ message: this.config.message,
310
+ elapsedTime: 0,
311
+ };
312
+
313
+ this.initialPosition = this.config.position;
314
+ this.render();
315
+ this.log('Queue reset');
316
+ }
317
+
318
+ /**
319
+ * Get current position
320
+ */
321
+ public getPosition(): number {
322
+ return this.state.position;
323
+ }
324
+
325
+ /**
326
+ * Get current status
327
+ */
328
+ public getStatus(): QueueStatus {
329
+ return this.state.status;
330
+ }
331
+
332
+ /**
333
+ * Start elapsed time timer
334
+ */
335
+ protected override startTimer(): void {
336
+ this.stopTimer();
337
+ this.timerInterval = globalThis.setInterval(() => {
338
+ if (this.state.startTime > 0) {
339
+ this.state.elapsedTime = Date.now() - this.state.startTime;
340
+ this.render();
341
+ }
342
+ }, 1000);
343
+ }
344
+
345
+ /**
346
+ * Stop timer
347
+ */
348
+ private stopTimer(): void {
349
+ if (this.timerInterval) {
350
+ clearInterval(this.timerInterval);
351
+ this.timerInterval = undefined;
352
+ }
353
+ }
354
+
355
+ /**
356
+ * Calculate progress percentage
357
+ */
358
+ private getProgressPercentage(): number {
359
+ if (this.state.queueSize === 0 || this.initialPosition === 0) return 0;
360
+ const processed = this.initialPosition - this.state.position;
361
+ return Math.min(100, Math.max(0, (processed / this.initialPosition) * 100));
362
+ }
363
+
364
+ /**
365
+ * Update cursor based on queue state
366
+ */
367
+ private updateCursor(): void {
368
+ if (!this.config.cursorFeedback) return;
369
+
370
+ if (this.state.status === 'waiting') {
371
+ this.style.cursor = 'wait';
372
+ } else if (this.state.status === 'error' || this.state.status === 'cancelled') {
373
+ this.style.cursor = 'not-allowed';
374
+ } else {
375
+ this.style.cursor = 'default';
376
+ }
377
+ }
378
+
379
+ /**
380
+ * Render metrics section
381
+ */
382
+ private renderMetrics(): string {
383
+ if (this.state.status !== 'waiting') return '';
384
+
385
+ let metricsHtml = '';
386
+
387
+ if (this.config.showQueueSize) {
388
+ metricsHtml += `
389
+ <div class="metric">
390
+ <div class="metric-value">${this.state.queueSize}</div>
391
+ <div class="metric-label">Queue Size</div>
392
+ </div>
393
+ `;
394
+ }
395
+
396
+ if (this.config.showWaitTime) {
397
+ metricsHtml += `
398
+ <div class="metric">
399
+ <div class="metric-value">${formatTime(Math.round(this.state.estimatedWait))}</div>
400
+ <div class="metric-label">Est. Wait</div>
401
+ </div>
402
+ `;
403
+ }
404
+
405
+ if (this.config.showRate) {
406
+ metricsHtml += `
407
+ <div class="metric">
408
+ <div class="metric-value">${this.state.processingRate.toFixed(1)}/s</div>
409
+ <div class="metric-label">Processing Rate</div>
410
+ </div>
411
+ `;
412
+ }
413
+
414
+ return metricsHtml
415
+ ? `
416
+ <div class="queue-metrics">
417
+ ${metricsHtml}
418
+ </div>
419
+ `
420
+ : '';
421
+ }
422
+
423
+ /**
424
+ * Render component
425
+ */
426
+ protected override render(): void {
427
+ if (!this.shadowRoot) return;
428
+
429
+ // Sync attributes to host element for CSS selectors
430
+ if (this.config.size && this.getAttribute('size') !== this.config.size) {
431
+ this.setAttribute('size', this.config.size);
432
+ }
433
+ if (this.config.variant && this.getAttribute('variant') !== this.config.variant) {
434
+ this.setAttribute('variant', this.config.variant);
435
+ }
436
+ if (this.config.animation && this.getAttribute('animation') !== this.config.animation) {
437
+ this.setAttribute('animation', this.config.animation);
438
+ }
439
+
440
+ const progress = this.getProgressPercentage();
441
+ const statusClass = this.state.status;
442
+ const statusText = this.getStatusText();
443
+
444
+ this.shadowRoot.innerHTML = `
445
+ <style>${styles}</style>
446
+ <div class="queue-container">
447
+ <div class="queue-header">
448
+ <div class="queue-icon">${this.getStatusIcon()}</div>
449
+ <div class="queue-title">
450
+ <h3 class="queue-status">${statusText}</h3>
451
+ <p class="queue-message">${this.state.message || ''}</p>
452
+ </div>
453
+ <span class="queue-badge ${statusClass}">${this.state.status}</span>
454
+ </div>
455
+
456
+ ${
457
+ this.config.showPosition && this.state.status === 'waiting'
458
+ ? `
459
+ <div class="queue-position">
460
+ <div class="position-number">${this.state.position}</div>
461
+ <div class="position-label">Position in Queue</div>
462
+ </div>
463
+ `
464
+ : ''
465
+ }
466
+
467
+ ${this.renderMetrics()}
468
+
469
+ ${
470
+ this.config.showProgressBar && this.state.status === 'waiting' && this.initialPosition > 0
471
+ ? `
472
+ <div class="progress-container">
473
+ <div class="progress-bar">
474
+ <div class="progress-fill" style="width: ${progress}%"></div>
475
+ </div>
476
+ <div class="progress-label">
477
+ <span>${Math.round(progress)}% through queue</span>
478
+ <span>${formatTime(Math.round(this.state.elapsedTime / 1000))} elapsed</span>
479
+ </div>
480
+ </div>
481
+ `
482
+ : ''
483
+ }
484
+
485
+ ${
486
+ this.state.status === 'waiting' && this.state.position <= 5
487
+ ? `
488
+ <div class="queue-info">
489
+ <span class="info-icon">🎯</span>
490
+ <p class="info-text">You're almost there! Processing will begin shortly.</p>
491
+ </div>
492
+ `
493
+ : ''
494
+ }
495
+
496
+ ${
497
+ this.state.status === 'completed'
498
+ ? `
499
+ <div class="queue-info">
500
+ <span class="info-icon">✅</span>
501
+ <p class="info-text">Your request is now being processed!</p>
502
+ </div>
503
+ `
504
+ : ''
505
+ }
506
+
507
+ ${
508
+ this.state.status === 'error'
509
+ ? `
510
+ <div class="error-message">
511
+ <span class="error-icon">⚠️</span>
512
+ <p class="error-text">${this.state.message}</p>
513
+ </div>
514
+ `
515
+ : ''
516
+ }
517
+ </div>
518
+ `;
519
+
520
+ // Update ARIA attributes
521
+ this.setAttribute('aria-valuenow', this.state.position.toString());
522
+ this.setAttribute('aria-valuetext', `Position ${this.state.position} in queue`);
523
+ }
524
+
525
+ private getStatusIcon(): string {
526
+ switch (this.state.status) {
527
+ case 'waiting':
528
+ return '⏳';
529
+ case 'processing':
530
+ return '⚙️';
531
+ case 'completed':
532
+ return '✅';
533
+ case 'cancelled':
534
+ return '🚫';
535
+ case 'error':
536
+ return '❌';
537
+ default:
538
+ return '⏳';
539
+ }
540
+ }
541
+
542
+ private getStatusText(): string {
543
+ switch (this.state.status) {
544
+ case 'waiting':
545
+ return 'Waiting in Queue';
546
+ case 'processing':
547
+ return 'Processing Your Request';
548
+ case 'completed':
549
+ return 'Processing Started';
550
+ case 'cancelled':
551
+ return 'Queue Cancelled';
552
+ case 'error':
553
+ return 'Queue Error';
554
+ default:
555
+ return 'Queue Status';
556
+ }
557
+ }
558
+ }
559
+
560
+ // Register the custom element
561
+ if (!customElements.get('queue-progress')) {
562
+ customElements.define('queue-progress', QueueProgress);
563
+ }
@@ -0,0 +1,5 @@
1
+ // Export types
2
+ export * from './types';
3
+
4
+ // Export component
5
+ export { QueueProgress } from './QueueProgress';