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,517 @@
1
+ import { AIControl } from '../base/AIControl';
2
+ import { throttle, formatCurrency } from '../base/utils';
3
+ import type {
4
+ StreamProgressConfig,
5
+ StreamProgressState,
6
+ StreamProgressUpdate,
7
+ StreamCompleteEvent,
8
+ StreamCancelEvent,
9
+ } from './types';
10
+ import { styles } from './styles';
11
+
12
+ /**
13
+ * StreamProgress Component
14
+ *
15
+ * Displays real-time progress for streaming AI responses (e.g., LLM token generation).
16
+ * Shows token count, generation rate, cost estimation, and provides cancel functionality.
17
+ *
18
+ * @example
19
+ * ```typescript
20
+ * // Create the component
21
+ * const progress = new StreamProgress({
22
+ * maxTokens: 2000,
23
+ * costPerToken: 0.00002,
24
+ * showRate: true,
25
+ * showCost: true,
26
+ * });
27
+ *
28
+ * document.body.appendChild(progress);
29
+ *
30
+ * // Start streaming
31
+ * progress.start();
32
+ *
33
+ * // Update as tokens stream in
34
+ * progress.update({
35
+ * tokensGenerated: 150,
36
+ * tokensPerSecond: 25,
37
+ * message: 'Generating response...'
38
+ * });
39
+ *
40
+ * // Complete the stream
41
+ * progress.complete();
42
+ *
43
+ * // Listen to events
44
+ * progress.addEventListener('cancel', (e) => {
45
+ * console.log('Stream cancelled', e.detail);
46
+ * });
47
+ * ```
48
+ *
49
+ * @fires streamstart - Fired when streaming starts
50
+ * @fires streamupdate - Fired when progress is updated
51
+ * @fires streamcomplete - Fired when streaming completes
52
+ * @fires streamcancel - Fired when streaming is cancelled
53
+ */
54
+ export class StreamProgress extends AIControl {
55
+ protected override config: Required<StreamProgressConfig>;
56
+ private state: StreamProgressState;
57
+ private readonly updateThrottled: (update: StreamProgressUpdate) => void;
58
+ private animationFrame: number = 0;
59
+ private displayTokens: number = 0;
60
+
61
+ static get observedAttributes() {
62
+ return ['max-tokens', 'cost-per-token', 'disabled', 'size', 'variant', 'animation'];
63
+ }
64
+
65
+ constructor(config: StreamProgressConfig = {}) {
66
+ super({
67
+ debug: config.debug ?? false,
68
+ className: config.className,
69
+ ariaLabel: config.ariaLabel ?? 'AI Stream Progress',
70
+ });
71
+
72
+ // Set default configuration
73
+ this.config = {
74
+ maxTokens: config.maxTokens ?? 4000,
75
+ costPerToken: config.costPerToken ?? 0.00002,
76
+ currency: config.currency ?? '$',
77
+ showRate: config.showRate ?? true,
78
+ showCost: config.showCost ?? true,
79
+ showProgressBar: config.showProgressBar ?? true,
80
+ showCancelButton: config.showCancelButton ?? true,
81
+ smoothProgress: config.smoothProgress ?? true,
82
+ updateThrottle: config.updateThrottle ?? 100,
83
+ cancelLabel: config.cancelLabel ?? 'Cancel',
84
+ debug: config.debug ?? false,
85
+ className: config.className ?? '',
86
+ ariaLabel: config.ariaLabel ?? 'AI Stream Progress',
87
+ cursorFeedback: config.cursorFeedback ?? true,
88
+ size: config.size ?? 'default',
89
+ variant: config.variant ?? 'default',
90
+ animation: config.animation ?? 'none',
91
+ };
92
+
93
+ // Initialize state
94
+ this.state = {
95
+ tokensGenerated: 0,
96
+ tokensPerSecond: 0,
97
+ totalCost: 0,
98
+ isStreaming: false,
99
+ isPaused: false,
100
+ isCancelled: false,
101
+ startTime: 0,
102
+ lastUpdateTime: 0,
103
+ };
104
+
105
+ // Create throttled update function
106
+ this.updateThrottled = throttle(this._updateInternal.bind(this), this.config.updateThrottle);
107
+
108
+ // Attach shadow DOM
109
+ this.attachShadow({ mode: 'open' });
110
+ }
111
+
112
+ override connectedCallback(): void {
113
+ super.connectedCallback();
114
+ this.log('StreamProgress mounted');
115
+ }
116
+
117
+ override disconnectedCallback(): void {
118
+ this.cleanup();
119
+ super.disconnectedCallback();
120
+ }
121
+
122
+ protected override cleanup(): void {
123
+ if (this.animationFrame) {
124
+ cancelAnimationFrame(this.animationFrame);
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Update cursor based on component state
130
+ */
131
+ private updateCursor(): void {
132
+ if (!this.config.cursorFeedback) return;
133
+
134
+ if (this.state.isStreaming && !this.state.isPaused) {
135
+ this.style.cursor = 'progress';
136
+ } else if (this.state.isCancelled) {
137
+ this.style.cursor = 'not-allowed';
138
+ } else {
139
+ this.style.cursor = 'default';
140
+ }
141
+ }
142
+
143
+ protected override getDefaultRole(): string {
144
+ return 'progressbar';
145
+ }
146
+
147
+ protected override handleAttributeChange(
148
+ name: string,
149
+ _oldValue: string,
150
+ newValue: string
151
+ ): void {
152
+ switch (name) {
153
+ case 'max-tokens':
154
+ this.config.maxTokens = Number.parseInt(newValue, 10) || 4000;
155
+ this.render();
156
+ break;
157
+ case 'cost-per-token':
158
+ this.config.costPerToken = Number.parseFloat(newValue) || 0.00002;
159
+ this.render();
160
+ break;
161
+ case 'disabled':
162
+ this._disabled = newValue !== null;
163
+ this.render();
164
+ break;
165
+ case 'size':
166
+ this.config.size = newValue as any;
167
+ this.render();
168
+ break;
169
+ case 'variant':
170
+ this.config.variant = newValue as any;
171
+ this.render();
172
+ break;
173
+ case 'animation':
174
+ this.config.animation = newValue as any;
175
+ this.render();
176
+ break;
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Start streaming
182
+ */
183
+ public start(message?: string): void {
184
+ if (this.state.isStreaming) {
185
+ this.log('Already streaming');
186
+ return;
187
+ }
188
+
189
+ this.state = {
190
+ ...this.state,
191
+ isStreaming: true,
192
+ isPaused: false,
193
+ isCancelled: false,
194
+ tokensGenerated: 0,
195
+ tokensPerSecond: 0,
196
+ totalCost: 0,
197
+ startTime: Date.now(),
198
+ lastUpdateTime: Date.now(),
199
+ message,
200
+ };
201
+
202
+ this.displayTokens = 0;
203
+ this.startTimer();
204
+ this.render();
205
+ this.updateCursor();
206
+ this.emit('streamstart', { startTime: this.state.startTime, message });
207
+ this.log('Stream started', this.state);
208
+ }
209
+
210
+ /**
211
+ * Update progress
212
+ */
213
+ public update(update: StreamProgressUpdate): void {
214
+ if (!this.state.isStreaming || this.state.isCancelled) {
215
+ return;
216
+ }
217
+
218
+ this.updateThrottled(update);
219
+ }
220
+
221
+ private _updateInternal(update: StreamProgressUpdate): void {
222
+ const now = Date.now();
223
+ const tokensGenerated = update.tokensGenerated;
224
+
225
+ // Calculate rate if not provided
226
+ let tokensPerSecond = update.tokensPerSecond ?? 0;
227
+ if (!tokensPerSecond && this.state.lastUpdateTime) {
228
+ const timeDiff = (now - this.state.lastUpdateTime) / 1000;
229
+ const tokenDiff = tokensGenerated - this.state.tokensGenerated;
230
+ tokensPerSecond = timeDiff > 0 ? tokenDiff / timeDiff : 0;
231
+ }
232
+
233
+ // Calculate cost
234
+ const totalCost = tokensGenerated * this.config.costPerToken;
235
+
236
+ // Update state
237
+ this.state = {
238
+ ...this.state,
239
+ tokensGenerated,
240
+ tokensPerSecond,
241
+ totalCost,
242
+ lastUpdateTime: now,
243
+ message: update.message ?? this.state.message,
244
+ };
245
+
246
+ // Smooth animation
247
+ if (this.config.smoothProgress) {
248
+ this.animateProgress();
249
+ } else {
250
+ this.displayTokens = tokensGenerated;
251
+ this.render();
252
+ }
253
+
254
+ // Update ARIA attributes
255
+ this.setAttribute('aria-valuenow', tokensGenerated.toString());
256
+ this.setAttribute('aria-valuemax', this.config.maxTokens.toString());
257
+
258
+ this.emit('streamupdate', this.state);
259
+ this.log('Progress updated', this.state);
260
+ }
261
+
262
+ /**
263
+ * Smooth animation for progress
264
+ */
265
+ private animateProgress(): void {
266
+ const animate = () => {
267
+ const diff = this.state.tokensGenerated - this.displayTokens;
268
+ if (Math.abs(diff) < 0.1) {
269
+ this.displayTokens = this.state.tokensGenerated;
270
+ this.render();
271
+ return;
272
+ }
273
+
274
+ this.displayTokens += diff * 0.2;
275
+ this.render();
276
+ this.animationFrame = requestAnimationFrame(animate);
277
+ };
278
+
279
+ if (this.animationFrame) {
280
+ cancelAnimationFrame(this.animationFrame);
281
+ }
282
+ this.animationFrame = requestAnimationFrame(animate);
283
+ }
284
+
285
+ /**
286
+ * Complete streaming
287
+ */
288
+ public complete(): void {
289
+ if (!this.state.isStreaming) {
290
+ return;
291
+ }
292
+
293
+ const duration = this.getElapsedTime();
294
+ const averageRate = this.state.tokensGenerated / (duration / 1000);
295
+
296
+ const event: StreamCompleteEvent = {
297
+ tokensGenerated: this.state.tokensGenerated,
298
+ duration,
299
+ totalCost: this.state.totalCost,
300
+ averageRate,
301
+ };
302
+
303
+ this.state = {
304
+ ...this.state,
305
+ isStreaming: false,
306
+ };
307
+
308
+ this.render();
309
+ this.updateCursor();
310
+ this.emit('streamcomplete', event);
311
+ this.log('Stream completed', event);
312
+ }
313
+
314
+ /**
315
+ * Cancel streaming
316
+ */
317
+ public cancel(reason: 'user' | 'error' | 'timeout' = 'user'): void {
318
+ if (!this.state.isStreaming || this.state.isCancelled) {
319
+ return;
320
+ }
321
+
322
+ const duration = this.getElapsedTime();
323
+
324
+ const event: StreamCancelEvent = {
325
+ tokensGenerated: this.state.tokensGenerated,
326
+ duration,
327
+ reason,
328
+ };
329
+
330
+ this.state = {
331
+ ...this.state,
332
+ isStreaming: false,
333
+ isCancelled: true,
334
+ };
335
+
336
+ this.render();
337
+ this.updateCursor();
338
+ this.emit('streamcancel', event);
339
+ this.log('Stream cancelled', event);
340
+ }
341
+
342
+ /**
343
+ * Reset the component
344
+ */
345
+ public reset(): void {
346
+ this.state = {
347
+ tokensGenerated: 0,
348
+ tokensPerSecond: 0,
349
+ totalCost: 0,
350
+ isStreaming: false,
351
+ isPaused: false,
352
+ isCancelled: false,
353
+ startTime: 0,
354
+ lastUpdateTime: 0,
355
+ };
356
+
357
+ this.displayTokens = 0;
358
+ if (this.animationFrame) {
359
+ cancelAnimationFrame(this.animationFrame);
360
+ }
361
+
362
+ this.render();
363
+ this.updateCursor();
364
+ this.log('Component reset');
365
+ }
366
+
367
+ /**
368
+ * Sync config attributes to host element
369
+ */
370
+ private _syncAttributes(): void {
371
+ if (this.config.size && this.getAttribute('size') !== this.config.size) {
372
+ this.setAttribute('size', this.config.size);
373
+ }
374
+ if (this.config.variant && this.getAttribute('variant') !== this.config.variant) {
375
+ this.setAttribute('variant', this.config.variant);
376
+ }
377
+ if (this.config.animation && this.getAttribute('animation') !== this.config.animation) {
378
+ this.setAttribute('animation', this.config.animation);
379
+ }
380
+ }
381
+
382
+ /**
383
+ * Get progress bar HTML
384
+ */
385
+ private _getProgressBarHtml(percentage: number): string {
386
+ if (!this.config.showProgressBar) {
387
+ return '';
388
+ }
389
+ return `
390
+ <div class="progress-bar">
391
+ <div class="progress-fill" style="width: ${percentage}%"></div>
392
+ </div>
393
+ `;
394
+ }
395
+
396
+ /**
397
+ * Get stats HTML
398
+ */
399
+ private _getStatsHtml(tokensDisplay: number, rateDisplay: number, costDisplay: string): string {
400
+ const rateHtml = this.config.showRate
401
+ ? `
402
+ <div class="stat-item">
403
+ <span class="stat-label">Rate:</span>
404
+ <span class="stat-value">${rateDisplay} tokens/s</span>
405
+ </div>
406
+ `
407
+ : '';
408
+
409
+ const costHtml = this.config.showCost
410
+ ? `
411
+ <div class="stat-item">
412
+ <span class="stat-label">Cost:</span>
413
+ <span class="stat-value">${costDisplay}</span>
414
+ </div>
415
+ `
416
+ : '';
417
+
418
+ return `
419
+ <div class="stats">
420
+ <div class="stat-item">
421
+ <span class="stat-label">Tokens:</span>
422
+ <span class="stat-value">${tokensDisplay} / ${this.config.maxTokens}</span>
423
+ </div>
424
+ ${rateHtml}
425
+ ${costHtml}
426
+ </div>
427
+ `;
428
+ }
429
+
430
+ /**
431
+ * Get cancel button HTML
432
+ */
433
+ private _getCancelButtonHtml(): string {
434
+ if (!this.config.showCancelButton || !this.state.isStreaming) {
435
+ return '';
436
+ }
437
+ return `
438
+ <button class="cancel-button" aria-label="Cancel streaming">
439
+ ${this.config.cancelLabel}
440
+ </button>
441
+ `;
442
+ }
443
+
444
+ /**
445
+ * Get status CSS class
446
+ */
447
+ private _getStatusClass(): string {
448
+ if (this.state.isCancelled) return 'cancelled';
449
+ if (this.state.isStreaming) return 'streaming';
450
+ return 'idle';
451
+ }
452
+
453
+ /**
454
+ * Attach event listeners to rendered elements
455
+ */
456
+ private _attachEventListeners(): void {
457
+ if (!this.config.showCancelButton) return;
458
+
459
+ const cancelBtn = this.shadowRoot?.querySelector('.cancel-button');
460
+ if (cancelBtn) {
461
+ cancelBtn.addEventListener('click', () => this.cancel('user'));
462
+ }
463
+ }
464
+
465
+ /**
466
+ * Render the component
467
+ */
468
+ protected render(): void {
469
+ if (!this.shadowRoot) return;
470
+
471
+ this._syncAttributes();
472
+
473
+ const percentage = this.calculatePercentage(this.displayTokens, this.config.maxTokens);
474
+ const tokensDisplay = Math.round(this.displayTokens);
475
+ const rateDisplay = Math.round(this.state.tokensPerSecond);
476
+ const costDisplay = formatCurrency(this.state.totalCost);
477
+
478
+ const progressBarHtml = this._getProgressBarHtml(percentage);
479
+ const statsHtml = this._getStatsHtml(tokensDisplay, rateDisplay, costDisplay);
480
+ const messageHtml = this.state.message
481
+ ? `<div class="message">${this.state.message}</div>`
482
+ : '';
483
+ const cancelButtonHtml = this._getCancelButtonHtml();
484
+ const statusClass = this._getStatusClass();
485
+
486
+ this.shadowRoot.innerHTML = `
487
+ ${styles}
488
+ <div class="stream-progress ${statusClass} ${this.config.className}">
489
+ ${messageHtml}
490
+ ${progressBarHtml}
491
+ ${statsHtml}
492
+ ${cancelButtonHtml}
493
+ </div>
494
+ `;
495
+
496
+ this._attachEventListeners();
497
+ }
498
+
499
+ /**
500
+ * Get current state (for debugging/inspection)
501
+ */
502
+ public getState(): Readonly<StreamProgressState> {
503
+ return { ...this.state };
504
+ }
505
+
506
+ /**
507
+ * Get current configuration
508
+ */
509
+ public getConfig(): Readonly<Required<StreamProgressConfig>> {
510
+ return { ...this.config };
511
+ }
512
+ }
513
+
514
+ // Register the custom element
515
+ if (!customElements.get('stream-progress')) {
516
+ customElements.define('stream-progress', StreamProgress);
517
+ }
@@ -0,0 +1,2 @@
1
+ export * from './StreamProgress';
2
+ export * from './types';