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,877 @@
1
+ import { AIControl } from '../base/AIControl';
2
+ import { ParameterSlider } from '../parameter-slider/ParameterSlider';
3
+ import type {
4
+ ParameterPanelConfig,
5
+ ParameterPanelState,
6
+ ParameterDefinition,
7
+ PanelChangeEvent,
8
+ PresetLoadEvent,
9
+ ConfigExportEvent,
10
+ ConfigImportEvent,
11
+ PanelResetEvent,
12
+ ValidationErrorEvent,
13
+ ExportedConfig,
14
+ } from './types';
15
+ import { styles } from './styles';
16
+
17
+ /**
18
+ * ParameterPanel Component
19
+ *
20
+ * Manages multiple AI parameters as a coordinated group with presets, validation, and export/import.
21
+ * Automatically creates and manages child ParameterSlider instances.
22
+ *
23
+ * @example
24
+ * ```typescript
25
+ * // Create LLM configuration panel
26
+ * const panel = new ParameterPanel({
27
+ * title: 'Model Configuration',
28
+ * parameters: [
29
+ * { id: 'temperature', label: 'Temperature', min: 0, max: 2, value: 0.7, step: 0.1 },
30
+ * { id: 'topP', label: 'Top-P', min: 0, max: 1, value: 0.9, step: 0.05 },
31
+ * { id: 'maxTokens', label: 'Max Tokens', min: 100, max: 4000, value: 2000, step: 100 }
32
+ * ],
33
+ * presets: {
34
+ * chatgpt: {
35
+ * name: 'ChatGPT',
36
+ * description: 'Balanced configuration',
37
+ * values: { temperature: 0.7, topP: 0.9, maxTokens: 2000 }
38
+ * },
39
+ * code: {
40
+ * name: 'Code Generation',
41
+ * description: 'More focused for code',
42
+ * values: { temperature: 0.2, topP: 0.8, maxTokens: 1000 }
43
+ * }
44
+ * }
45
+ * });
46
+ *
47
+ * document.body.appendChild(panel);
48
+ *
49
+ * // Get all values
50
+ * const config = panel.getAllValues();
51
+ * // { temperature: 0.7, topP: 0.9, maxTokens: 2000 }
52
+ *
53
+ * // Load preset
54
+ * panel.loadPreset('code');
55
+ *
56
+ * // Listen to changes
57
+ * panel.addEventListener('panelchange', (e) => {
58
+ * console.log('Changed parameter:', e.detail.parameterId);
59
+ * console.log('All values:', e.detail.allValues);
60
+ * });
61
+ * ```
62
+ *
63
+ * @fires panelchange - Fired when any parameter changes
64
+ * @fires presetload - Fired when preset is loaded
65
+ * @fires configexport - Fired when configuration is exported
66
+ * @fires configimport - Fired when configuration is imported
67
+ * @fires panelreset - Fired when panel is reset
68
+ * @fires validationerror - Fired when validation fails
69
+ */
70
+ export class ParameterPanel extends AIControl {
71
+ private readonly state: ParameterPanelState;
72
+ private readonly parameters: Map<string, ParameterSlider> = new Map();
73
+ private readonly parameterDefinitions: Map<string, ParameterDefinition> = new Map();
74
+ private readonly presets: Map<
75
+ string,
76
+ { name: string; description?: string; values: Record<string, number>; isBuiltIn: boolean }
77
+ >;
78
+ private readonly panelConfig: ParameterPanelConfig;
79
+
80
+ constructor(config: ParameterPanelConfig) {
81
+ super({ debug: config.debug, disabled: config.disabled });
82
+
83
+ this.panelConfig = config;
84
+
85
+ if (!config.parameters || config.parameters.length === 0) {
86
+ throw new Error('ParameterPanel requires at least one parameter');
87
+ }
88
+
89
+ // Store parameter definitions
90
+ config.parameters.forEach((param) => {
91
+ this.parameterDefinitions.set(param.id, param);
92
+ });
93
+
94
+ // Initialize presets
95
+ this.presets = new Map();
96
+ if (config.presets) {
97
+ Object.entries(config.presets).forEach(([id, preset]) => {
98
+ this.presets.set(id, { ...preset, isBuiltIn: preset.isBuiltIn ?? false });
99
+ });
100
+ }
101
+
102
+ // Initialize state
103
+ const initialValues: Record<string, number> = {};
104
+ config.parameters.forEach((param) => {
105
+ initialValues[param.id] = param.value;
106
+ });
107
+
108
+ this.state = {
109
+ values: initialValues,
110
+ activePreset: null,
111
+ isCollapsed: Boolean(this.panelConfig.collapsible && this.panelConfig.startCollapsed),
112
+ errors: {},
113
+ isDirty: false,
114
+ };
115
+
116
+ this.attachShadow({ mode: 'open' });
117
+
118
+ // Load persisted values if enabled
119
+ if (this.panelConfig.persistValues) {
120
+ this.loadFromStorage();
121
+ }
122
+
123
+ // Load persisted presets if enabled
124
+ if (this.panelConfig.persistPresets) {
125
+ this.loadPresetsFromStorage();
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Get all parameter values
131
+ */
132
+ getAllValues(): Record<string, number> {
133
+ return { ...this.state.values };
134
+ }
135
+
136
+ /**
137
+ * Get specific parameter value
138
+ */
139
+ getValue(parameterId: string): number | undefined {
140
+ return this.state.values[parameterId];
141
+ }
142
+
143
+ /**
144
+ * Set specific parameter value
145
+ */
146
+ setValue(
147
+ parameterId: string,
148
+ value: number,
149
+ source: 'slider' | 'input' | 'preset' | 'reset' | 'import' = 'slider'
150
+ ): void {
151
+ const paramDef = this.parameterDefinitions.get(parameterId);
152
+ if (!paramDef) {
153
+ this.log(`Parameter ${parameterId} not found`, 'warn');
154
+ return;
155
+ }
156
+
157
+ // Clamp value to range
158
+ value = Math.max(paramDef.min, Math.min(paramDef.max, value));
159
+
160
+ // Validate if enabled (but not during reset/import)
161
+ if (
162
+ this.panelConfig.validateOnChange &&
163
+ paramDef.validate &&
164
+ source !== 'reset' &&
165
+ source !== 'import'
166
+ ) {
167
+ const validationResult = paramDef.validate(value, this.state.values);
168
+ if (validationResult !== true) {
169
+ this.state.errors[parameterId] =
170
+ typeof validationResult === 'string' ? validationResult : 'Validation failed';
171
+ this.dispatchValidationError(parameterId, this.state.errors[parameterId]);
172
+ this.render();
173
+ return;
174
+ }
175
+ }
176
+
177
+ // Clear error for this parameter
178
+ delete this.state.errors[parameterId];
179
+
180
+ // Update value
181
+ const oldValue = this.state.values[parameterId];
182
+ this.state.values[parameterId] = value;
183
+
184
+ // Only mark dirty and clear preset for non-reset/import sources
185
+ if (source !== 'reset' && source !== 'import' && source !== 'preset') {
186
+ this.state.isDirty = true;
187
+ this.state.activePreset = null; // Clear active preset when manually changed
188
+ }
189
+
190
+ // Update child component if it exists
191
+ const slider = this.parameters.get(parameterId);
192
+ if (slider && oldValue !== value) {
193
+ // Map 'import' to 'reset' since ParameterSlider doesn't support 'import'
194
+ const sliderSource = source === 'import' ? 'reset' : source;
195
+ slider.setValue(value, sliderSource);
196
+ }
197
+
198
+ // Persist if enabled
199
+ if (this.panelConfig.persistValues) {
200
+ this.saveToStorage();
201
+ }
202
+
203
+ // Emit change event (only if called programmatically, not from child slider)
204
+ if (this.panelConfig.emitChangeEvents && oldValue !== value) {
205
+ this.dispatchPanelChange(parameterId, value, oldValue ?? paramDef.min, source);
206
+ }
207
+
208
+ // Re-render to update UI
209
+ this.render();
210
+ }
211
+
212
+ /**
213
+ * Load preset values
214
+ */
215
+ loadPreset(presetId: string): void {
216
+ const preset = this.presets.get(presetId);
217
+ if (!preset) {
218
+ this.log(`Preset ${presetId} not found`, 'warn');
219
+ return;
220
+ }
221
+
222
+ const previousValues = { ...this.state.values };
223
+
224
+ // Apply all preset values
225
+ Object.entries(preset.values).forEach(([parameterId, value]) => {
226
+ if (this.parameterDefinitions.has(parameterId)) {
227
+ this.setValue(parameterId, value, 'preset');
228
+ }
229
+ });
230
+
231
+ this.state.activePreset = presetId;
232
+ this.state.isDirty = false;
233
+
234
+ // Emit preset load event
235
+ this.dispatchPresetLoad(presetId, preset, previousValues);
236
+
237
+ this.render();
238
+ }
239
+
240
+ /**
241
+ * Reset all parameters to default values
242
+ */
243
+ resetAll(): void {
244
+ this.parameterDefinitions.forEach((paramDef, parameterId) => {
245
+ const defaultValue = paramDef.value;
246
+ this.setValue(parameterId, defaultValue, 'reset');
247
+ });
248
+
249
+ this.state.activePreset = null;
250
+ this.state.isDirty = false;
251
+ this.state.errors = {};
252
+
253
+ // Emit reset event
254
+ this.dispatchPanelReset();
255
+
256
+ this.render();
257
+ }
258
+
259
+ /**
260
+ * Export configuration as JSON
261
+ */
262
+ exportConfig(): ExportedConfig {
263
+ const parameters: Record<string, number> = {};
264
+
265
+ this.parameterDefinitions.forEach((_, parameterId) => {
266
+ parameters[parameterId] = this.state.values[parameterId] ?? 0;
267
+ });
268
+
269
+ const config: ExportedConfig = {
270
+ version: '1.0',
271
+ parameters,
272
+ };
273
+
274
+ if (this.state.activePreset) {
275
+ config.preset = this.state.activePreset;
276
+ }
277
+
278
+ config.metadata = {
279
+ created: new Date().toISOString(),
280
+ name: (this.config as any).title || 'Parameters',
281
+ };
282
+
283
+ // Emit export event
284
+ this.dispatchConfigExport(config);
285
+
286
+ return config;
287
+ }
288
+
289
+ /**
290
+ * Import configuration from JSON
291
+ */
292
+ importConfig(config: ExportedConfig): void {
293
+ if (!config.parameters) {
294
+ this.log('Invalid config: missing parameters', 'warn');
295
+ return;
296
+ }
297
+
298
+ // Apply imported values
299
+ Object.entries(config.parameters).forEach(([parameterId, value]) => {
300
+ if (this.parameterDefinitions.has(parameterId)) {
301
+ this.setValue(parameterId, value, 'import');
302
+ }
303
+ });
304
+
305
+ // Load active preset if specified
306
+ if (config.preset && this.presets.has(config.preset)) {
307
+ this.state.activePreset = config.preset;
308
+ } else {
309
+ this.state.activePreset = null;
310
+ }
311
+
312
+ this.state.isDirty = false;
313
+
314
+ // Emit import event
315
+ this.dispatchConfigImport(config);
316
+
317
+ this.render();
318
+ }
319
+
320
+ /**
321
+ * Toggle collapsed state
322
+ */
323
+ toggleCollapse(): void {
324
+ if (!this.panelConfig.collapsible) return;
325
+
326
+ this.state.isCollapsed = !this.state.isCollapsed;
327
+ this.render();
328
+ }
329
+
330
+ /**
331
+ * Validate all parameters
332
+ */
333
+ validateAll(): boolean {
334
+ let isValid = true;
335
+ this.state.errors = {};
336
+
337
+ this.parameterDefinitions.forEach((paramDef, parameterId) => {
338
+ if (paramDef.validate) {
339
+ const value = this.state.values[parameterId] ?? paramDef.min;
340
+ const validationResult = paramDef.validate(value, this.state.values);
341
+ if (validationResult !== true) {
342
+ isValid = false;
343
+ this.state.errors[parameterId] =
344
+ typeof validationResult === 'string' ? validationResult : 'Validation failed';
345
+ this.dispatchValidationError(parameterId, this.state.errors[parameterId]);
346
+ }
347
+ }
348
+ });
349
+
350
+ if (!isValid) {
351
+ this.render();
352
+ }
353
+
354
+ return isValid;
355
+ }
356
+
357
+ /**
358
+ * Add custom preset
359
+ */
360
+ addPreset(id: string, name: string, values: Record<string, number>, description?: string): void {
361
+ this.presets.set(id, { name, description, values, isBuiltIn: false });
362
+
363
+ if (this.panelConfig.persistPresets) {
364
+ this.savePresetsToStorage();
365
+ }
366
+
367
+ this.render();
368
+ }
369
+
370
+ /**
371
+ * Remove custom preset
372
+ */
373
+ removePreset(id: string): void {
374
+ const preset = this.presets.get(id);
375
+ if (preset && !preset.isBuiltIn) {
376
+ this.presets.delete(id);
377
+
378
+ if (this.state.activePreset === id) {
379
+ this.state.activePreset = null;
380
+ }
381
+
382
+ if (this.panelConfig.persistPresets) {
383
+ this.savePresetsToStorage();
384
+ }
385
+
386
+ this.render();
387
+ }
388
+ }
389
+
390
+ // Event dispatchers
391
+ private dispatchPanelChange(
392
+ parameterId: string,
393
+ value: number,
394
+ oldValue: number,
395
+ source: 'slider' | 'input' | 'preset' | 'reset' | 'import'
396
+ ): void {
397
+ const event = new CustomEvent<PanelChangeEvent>('panelchange', {
398
+ detail: {
399
+ parameterId,
400
+ value,
401
+ previousValue: oldValue,
402
+ allValues: this.getAllValues(),
403
+ source,
404
+ timestamp: Date.now(),
405
+ },
406
+ bubbles: true,
407
+ composed: true,
408
+ });
409
+ this.dispatchEvent(event);
410
+ }
411
+
412
+ private dispatchPresetLoad(
413
+ presetId: string,
414
+ preset: { name: string; description?: string; values: Record<string, number> },
415
+ previousValues: Record<string, number>
416
+ ): void {
417
+ const event = new CustomEvent<PresetLoadEvent>('presetload', {
418
+ detail: {
419
+ presetId,
420
+ preset: {
421
+ name: preset.name,
422
+ description: preset.description,
423
+ values: preset.values,
424
+ },
425
+ previousValues,
426
+ timestamp: Date.now(),
427
+ },
428
+ bubbles: true,
429
+ composed: true,
430
+ });
431
+ this.dispatchEvent(event);
432
+ }
433
+
434
+ private dispatchConfigExport(config: ExportedConfig): void {
435
+ const event = new CustomEvent<ConfigExportEvent>('configexport', {
436
+ detail: {
437
+ config,
438
+ format: 'json',
439
+ timestamp: Date.now(),
440
+ },
441
+ bubbles: true,
442
+ composed: true,
443
+ });
444
+ this.dispatchEvent(event);
445
+ }
446
+
447
+ private dispatchConfigImport(config: ExportedConfig): void {
448
+ const previousValues = { ...this.state.values };
449
+ const event = new CustomEvent<ConfigImportEvent>('configimport', {
450
+ detail: {
451
+ config,
452
+ previousValues,
453
+ timestamp: Date.now(),
454
+ },
455
+ bubbles: true,
456
+ composed: true,
457
+ });
458
+ this.dispatchEvent(event);
459
+ }
460
+
461
+ private dispatchPanelReset(): void {
462
+ const previousValues = { ...this.state.values };
463
+ const newValues: Record<string, number> = {};
464
+ this.parameterDefinitions.forEach((param, id) => {
465
+ newValues[id] = param.value;
466
+ });
467
+
468
+ const event = new CustomEvent<PanelResetEvent>('panelreset', {
469
+ detail: {
470
+ previousValues,
471
+ newValues,
472
+ timestamp: Date.now(),
473
+ },
474
+ bubbles: true,
475
+ composed: true,
476
+ });
477
+ this.dispatchEvent(event);
478
+ }
479
+
480
+ private dispatchValidationError(parameterId: string, message: string): void {
481
+ const event = new CustomEvent<ValidationErrorEvent>('validationerror', {
482
+ detail: {
483
+ parameterId,
484
+ error: message,
485
+ value: this.state.values[parameterId] || 0,
486
+ timestamp: Date.now(),
487
+ },
488
+ bubbles: true,
489
+ composed: true,
490
+ });
491
+ this.dispatchEvent(event);
492
+ }
493
+
494
+ // Storage methods
495
+ private saveToStorage(): void {
496
+ try {
497
+ const data = {
498
+ values: this.state.values,
499
+ activePreset: this.state.activePreset,
500
+ };
501
+ localStorage.setItem(this.panelConfig.storageKey as string, JSON.stringify(data));
502
+ } catch (error) {
503
+ this.log('Failed to save to storage', 'error', error);
504
+ }
505
+ }
506
+
507
+ private loadFromStorage(): void {
508
+ try {
509
+ const data = localStorage.getItem(this.panelConfig.storageKey as string);
510
+ if (data) {
511
+ const parsed = JSON.parse(data);
512
+ if (parsed.values) {
513
+ Object.entries(parsed.values).forEach(([parameterId, value]) => {
514
+ if (this.parameterDefinitions.has(parameterId)) {
515
+ this.state.values[parameterId] = value as number;
516
+ }
517
+ });
518
+ }
519
+ if (parsed.activePreset && this.presets.has(parsed.activePreset)) {
520
+ this.state.activePreset = parsed.activePreset;
521
+ }
522
+ }
523
+ } catch (error) {
524
+ this.log('Failed to load from storage', 'error', error);
525
+ }
526
+ }
527
+
528
+ private savePresetsToStorage(): void {
529
+ try {
530
+ const customPresets = Array.from(this.presets.entries())
531
+ .filter(([_, preset]) => !preset.isBuiltIn)
532
+ .reduce(
533
+ (acc, [id, preset]) => {
534
+ acc[id] = { name: preset.name, description: preset.description, values: preset.values };
535
+ return acc;
536
+ },
537
+ {} as Record<string, any>
538
+ );
539
+
540
+ localStorage.setItem(`${this.panelConfig.storageKey}-presets`, JSON.stringify(customPresets));
541
+ } catch (error) {
542
+ this.log('Failed to save presets', 'error', error);
543
+ }
544
+ }
545
+
546
+ private loadPresetsFromStorage(): void {
547
+ try {
548
+ const data = localStorage.getItem(`${this.panelConfig.storageKey}-presets`);
549
+ if (data) {
550
+ const customPresets = JSON.parse(data);
551
+ Object.entries(customPresets).forEach(([id, preset]: [string, any]) => {
552
+ this.presets.set(id, { ...preset, isBuiltIn: false });
553
+ });
554
+ }
555
+ } catch (error) {
556
+ this.log('Failed to load presets', 'error', error);
557
+ }
558
+ }
559
+
560
+ // Rendering
561
+ protected render(): void {
562
+ if (!this.shadowRoot) return;
563
+
564
+ const hasErrors = Object.keys(this.state.errors).length > 0;
565
+
566
+ this.shadowRoot.innerHTML = `
567
+ <style>${styles}</style>
568
+ <div class="container" style="--grid-columns: ${this.panelConfig.columns}">
569
+ ${this.renderHeader()}
570
+ <div class="content ${this.state.isCollapsed ? 'collapsed' : ''}">
571
+ ${hasErrors ? this.renderErrors() : ''}
572
+ ${this.panelConfig.showPresets && this.presets.size > 0 ? this.renderPresets() : ''}
573
+ ${this.renderParameters()}
574
+ ${this.panelConfig.showResetAll || this.panelConfig.showExportImport ? this.renderActions() : ''}
575
+ </div>
576
+ </div>
577
+ `;
578
+
579
+ // Attach event listeners
580
+ this.attachEventListeners();
581
+
582
+ // Create and attach parameter sliders
583
+ this.createParameterSliders();
584
+ }
585
+
586
+ private renderHeader(): string {
587
+ const collapsedClass = this.state.isCollapsed ? 'collapsed' : '';
588
+ const showTitle = this.panelConfig.title !== undefined && this.panelConfig.title !== null;
589
+
590
+ return `
591
+ <div class="header ${this.panelConfig.collapsible ? 'collapsible' : ''}" id="header">
592
+ ${
593
+ showTitle
594
+ ? `
595
+ <div class="title-section">
596
+ <h3 class="title">${this.panelConfig.title}</h3>
597
+ <span class="dirty-indicator ${this.state.isDirty ? 'show' : ''}"></span>
598
+ </div>
599
+ `
600
+ : `
601
+ <div class="title-section">
602
+ <span class="dirty-indicator ${this.state.isDirty ? 'show' : ''}"></span>
603
+ </div>
604
+ `
605
+ }
606
+ ${
607
+ this.panelConfig.collapsible
608
+ ? `
609
+ <span class="collapse-icon ${collapsedClass}">▼</span>
610
+ `
611
+ : ''
612
+ }
613
+ </div>
614
+ `;
615
+ }
616
+
617
+ private renderErrors(): string {
618
+ const errorEntries = Object.entries(this.state.errors);
619
+ if (errorEntries.length === 0) return '';
620
+
621
+ return `
622
+ <div class="validation-errors show">
623
+ ${errorEntries
624
+ .map(
625
+ ([parameterId, message]) => `
626
+ <div class="error-item">
627
+ <span class="error-icon">⚠</span>
628
+ <span>${this.parameterDefinitions.get(parameterId)?.label || parameterId}: ${message}</span>
629
+ </div>
630
+ `
631
+ )
632
+ .join('')}
633
+ </div>
634
+ `;
635
+ }
636
+
637
+ private renderPresets(): string {
638
+ return `
639
+ <div class="presets-section">
640
+ <div class="presets-label">Presets</div>
641
+ <div class="presets-buttons">
642
+ ${Array.from(this.presets.entries())
643
+ .map(
644
+ ([id, preset]) => `
645
+ <button
646
+ class="preset-btn ${this.state.activePreset === id ? 'active' : ''}"
647
+ data-preset-id="${id}"
648
+ title="${preset.description || preset.name}"
649
+ >
650
+ ${preset.name}
651
+ </button>
652
+ `
653
+ )
654
+ .join('')}
655
+ </div>
656
+ </div>
657
+ `;
658
+ }
659
+
660
+ private renderParameters(): string {
661
+ return `
662
+ <div class="parameters-section">
663
+ <div class="parameters-grid layout-${this.panelConfig.layout}">
664
+ ${Array.from(this.parameterDefinitions.keys())
665
+ .map(
666
+ (id) => `
667
+ <div class="parameter-wrapper" data-parameter-id="${id}"></div>
668
+ `
669
+ )
670
+ .join('')}
671
+ </div>
672
+ </div>
673
+ `;
674
+ }
675
+
676
+ private renderActions(): string {
677
+ return `
678
+ <div class="actions-section">
679
+ <div class="actions-left">
680
+ ${
681
+ this.panelConfig.showResetAll
682
+ ? `
683
+ <button class="action-btn danger" id="reset-btn">
684
+ <span>↻</span>
685
+ <span>Reset All</span>
686
+ </button>
687
+ `
688
+ : ''
689
+ }
690
+ </div>
691
+ <div class="actions-right">
692
+ ${
693
+ this.panelConfig.showExportImport
694
+ ? `
695
+ <button class="action-btn" id="import-btn">
696
+ <span>📥</span>
697
+ <span>Import</span>
698
+ </button>
699
+ <button class="action-btn primary" id="export-btn">
700
+ <span>📤</span>
701
+ <span>Export</span>
702
+ </button>
703
+ `
704
+ : ''
705
+ }
706
+ </div>
707
+ </div>
708
+ `;
709
+ }
710
+
711
+ private attachEventListeners(): void {
712
+ if (!this.shadowRoot) return;
713
+
714
+ // Header click for collapse
715
+ if (this.panelConfig.collapsible) {
716
+ const header = this.shadowRoot.getElementById('header');
717
+ header?.addEventListener('click', () => this.toggleCollapse());
718
+ }
719
+
720
+ // Preset buttons
721
+ const presetButtons = this.shadowRoot.querySelectorAll('.preset-btn');
722
+ presetButtons.forEach((btn) => {
723
+ btn.addEventListener('click', () => {
724
+ const presetId = (btn as HTMLElement).dataset.presetId;
725
+ if (presetId) this.loadPreset(presetId);
726
+ });
727
+ });
728
+
729
+ // Reset button
730
+ const resetBtn = this.shadowRoot.getElementById('reset-btn');
731
+ resetBtn?.addEventListener('click', () => this.resetAll());
732
+
733
+ // Export button
734
+ const exportBtn = this.shadowRoot.getElementById('export-btn');
735
+ exportBtn?.addEventListener('click', () => this.handleExport());
736
+
737
+ // Import button
738
+ const importBtn = this.shadowRoot.getElementById('import-btn');
739
+ importBtn?.addEventListener('click', () => this.handleImport());
740
+ }
741
+
742
+ private createParameterSliders(): void {
743
+ if (!this.shadowRoot) return;
744
+
745
+ // Clear existing sliders
746
+ this.parameters.clear();
747
+
748
+ // Create slider for each parameter
749
+ this.parameterDefinitions.forEach((paramDef, parameterId) => {
750
+ const wrapper = this.shadowRoot!.querySelector(`[data-parameter-id="${parameterId}"]`);
751
+ if (!wrapper) return;
752
+
753
+ // Create ParameterSlider instance
754
+ const slider = new ParameterSlider({
755
+ label: paramDef.label,
756
+ min: paramDef.min,
757
+ max: paramDef.max,
758
+ value: this.state.values[parameterId],
759
+ step: paramDef.step,
760
+ unit: paramDef.unit,
761
+ description: paramDef.description,
762
+ presets: paramDef.presets,
763
+ showInput: paramDef.showInput,
764
+ disabled: paramDef.disabled || this._disabled,
765
+ });
766
+
767
+ // Listen to value changes from child slider (user interaction only)
768
+ slider.addEventListener('valuechange', ((e: CustomEvent) => {
769
+ // Stop propagation to prevent event bubbling
770
+ e.stopPropagation();
771
+
772
+ // Update our state and emit our own event
773
+ const paramDef = this.parameterDefinitions.get(parameterId);
774
+ if (!paramDef) return;
775
+
776
+ const value = e.detail.value;
777
+ const source = e.detail.source;
778
+
779
+ // Clamp value to range
780
+ const clampedValue = Math.max(paramDef.min, Math.min(paramDef.max, value));
781
+
782
+ // Validate if enabled
783
+ if (this.panelConfig.validateOnChange && paramDef.validate) {
784
+ const validationResult = paramDef.validate(clampedValue, this.state.values);
785
+ if (validationResult !== true) {
786
+ this.state.errors[parameterId] =
787
+ typeof validationResult === 'string' ? validationResult : 'Validation failed';
788
+ this.dispatchValidationError(parameterId, this.state.errors[parameterId]);
789
+ this.render();
790
+ return;
791
+ }
792
+ }
793
+
794
+ // Clear error
795
+ delete this.state.errors[parameterId];
796
+
797
+ // Update value
798
+ const oldValue = this.state.values[parameterId];
799
+ this.state.values[parameterId] = clampedValue;
800
+
801
+ // Mark dirty and clear preset for user changes
802
+ if (source !== 'reset' && source !== 'import' && source !== 'preset') {
803
+ this.state.isDirty = true;
804
+ this.state.activePreset = null;
805
+ }
806
+
807
+ // Persist if enabled
808
+ if (this.panelConfig.persistValues) {
809
+ this.saveToStorage();
810
+ }
811
+
812
+ // Emit change event
813
+ if (this.panelConfig.emitChangeEvents && oldValue !== clampedValue) {
814
+ this.dispatchPanelChange(parameterId, clampedValue, oldValue ?? paramDef.min, source);
815
+ }
816
+
817
+ // Re-render to update UI
818
+ this.render();
819
+ }) as EventListener);
820
+
821
+ // Append to wrapper
822
+ wrapper.appendChild(slider);
823
+
824
+ // Store reference
825
+ this.parameters.set(parameterId, slider);
826
+ });
827
+ }
828
+
829
+ private handleExport(): void {
830
+ const config = this.exportConfig();
831
+ const json = JSON.stringify(config, null, 2);
832
+ const blob = new Blob([json], { type: 'application/json' });
833
+ const url = URL.createObjectURL(blob);
834
+ const a = document.createElement('a');
835
+ a.href = url;
836
+ a.download = `${this.panelConfig.title}-config.json`;
837
+ a.click();
838
+ URL.revokeObjectURL(url);
839
+ }
840
+
841
+ private handleImport(): void {
842
+ const input = document.createElement('input');
843
+ input.type = 'file';
844
+ input.accept = '.json';
845
+ input.onchange = (e) => {
846
+ const file = (e.target as HTMLInputElement).files?.[0];
847
+ if (!file) return;
848
+
849
+ file
850
+ .text()
851
+ .then((text) => {
852
+ try {
853
+ const config = JSON.parse(text);
854
+ this.importConfig(config);
855
+ } catch (error) {
856
+ this.log('Failed to parse config file', 'error', error);
857
+ }
858
+ })
859
+ .catch((error) => {
860
+ this.log('Failed to read config file', 'error', error);
861
+ });
862
+ };
863
+ input.click();
864
+ }
865
+
866
+ /**
867
+ * Clean up when component is removed
868
+ */
869
+ override disconnectedCallback(): void {
870
+ this.parameters.clear();
871
+ }
872
+ }
873
+
874
+ // Register custom element
875
+ if (!customElements.get('ai-parameter-panel')) {
876
+ customElements.define('ai-parameter-panel', ParameterPanel);
877
+ }