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.
- package/LICENSE +21 -0
- package/README.md +823 -0
- package/dist/ai-progress-controls.es.js +7191 -0
- package/dist/ai-progress-controls.es.js.map +1 -0
- package/dist/ai-progress-controls.umd.js +2 -0
- package/dist/ai-progress-controls.umd.js.map +1 -0
- package/dist/index.d.ts +2212 -0
- package/package.json +105 -0
- package/src/__tests__/setup.ts +93 -0
- package/src/core/base/AIControl.ts +230 -0
- package/src/core/base/index.ts +3 -0
- package/src/core/base/types.ts +77 -0
- package/src/core/base/utils.ts +168 -0
- package/src/core/batch-progress/BatchProgress.test.ts +458 -0
- package/src/core/batch-progress/BatchProgress.ts +760 -0
- package/src/core/batch-progress/index.ts +14 -0
- package/src/core/batch-progress/styles.ts +480 -0
- package/src/core/batch-progress/types.ts +169 -0
- package/src/core/model-loader/ModelLoader.test.ts +311 -0
- package/src/core/model-loader/ModelLoader.ts +673 -0
- package/src/core/model-loader/index.ts +2 -0
- package/src/core/model-loader/styles.ts +496 -0
- package/src/core/model-loader/types.ts +127 -0
- package/src/core/parameter-panel/ParameterPanel.test.ts +856 -0
- package/src/core/parameter-panel/ParameterPanel.ts +877 -0
- package/src/core/parameter-panel/index.ts +14 -0
- package/src/core/parameter-panel/styles.ts +323 -0
- package/src/core/parameter-panel/types.ts +278 -0
- package/src/core/parameter-slider/ParameterSlider.test.ts +299 -0
- package/src/core/parameter-slider/ParameterSlider.ts +653 -0
- package/src/core/parameter-slider/index.ts +8 -0
- package/src/core/parameter-slider/styles.ts +493 -0
- package/src/core/parameter-slider/types.ts +107 -0
- package/src/core/queue-progress/QueueProgress.test.ts +344 -0
- package/src/core/queue-progress/QueueProgress.ts +563 -0
- package/src/core/queue-progress/index.ts +5 -0
- package/src/core/queue-progress/styles.ts +469 -0
- package/src/core/queue-progress/types.ts +130 -0
- package/src/core/retry-progress/RetryProgress.test.ts +397 -0
- package/src/core/retry-progress/RetryProgress.ts +957 -0
- package/src/core/retry-progress/index.ts +6 -0
- package/src/core/retry-progress/styles.ts +530 -0
- package/src/core/retry-progress/types.ts +176 -0
- package/src/core/stream-progress/StreamProgress.test.ts +531 -0
- package/src/core/stream-progress/StreamProgress.ts +517 -0
- package/src/core/stream-progress/index.ts +2 -0
- package/src/core/stream-progress/styles.ts +349 -0
- package/src/core/stream-progress/types.ts +82 -0
- 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
|
+
}
|