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,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
|
+
}
|