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
package/package.json
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ai-progress-controls",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Framework-agnostic UI controls for AI/ML workflows - streaming progress, model loading, LLM parameters, and more",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ai",
|
|
7
|
+
"progress",
|
|
8
|
+
"slider",
|
|
9
|
+
"llm",
|
|
10
|
+
"web-components",
|
|
11
|
+
"openai",
|
|
12
|
+
"anthropic",
|
|
13
|
+
"streaming",
|
|
14
|
+
"ui-components",
|
|
15
|
+
"typescript",
|
|
16
|
+
"framework-agnostic",
|
|
17
|
+
"accessibility",
|
|
18
|
+
"developer-friendly"
|
|
19
|
+
],
|
|
20
|
+
"author": "Maneesh Thakur",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/Maneesh-Relanto/ai-progress-controls.git"
|
|
25
|
+
},
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/Maneesh-Relanto/ai-progress-controls/issues"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://github.com/Maneesh-Relanto/ai-progress-controls#readme",
|
|
30
|
+
"main": "dist/ai-progress-controls.umd.js",
|
|
31
|
+
"module": "dist/ai-progress-controls.es.js",
|
|
32
|
+
"types": "dist/index.d.ts",
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"src",
|
|
36
|
+
"README.md",
|
|
37
|
+
"LICENSE"
|
|
38
|
+
],
|
|
39
|
+
"scripts": {
|
|
40
|
+
"dev": "vite",
|
|
41
|
+
"build": "tsc && vite build",
|
|
42
|
+
"preview": "vite preview",
|
|
43
|
+
"test": "vitest",
|
|
44
|
+
"test:ui": "vitest --ui",
|
|
45
|
+
"test:coverage": "vitest --coverage",
|
|
46
|
+
"lint": "eslint src --ext .ts,.tsx",
|
|
47
|
+
"lint:fix": "eslint src --ext .ts,.tsx --fix",
|
|
48
|
+
"format": "prettier --write \"src/**/*.{ts,tsx,css,md}\"",
|
|
49
|
+
"format:check": "prettier --check \"src/**/*.{ts,tsx,css,md}\"",
|
|
50
|
+
"type-check": "tsc --noEmit",
|
|
51
|
+
"validate": "npm run type-check && npm run lint && npm run format:check && npm run test:coverage",
|
|
52
|
+
"security:audit": "npm audit --audit-level=moderate",
|
|
53
|
+
"security:fix": "npm audit fix",
|
|
54
|
+
"bundle:analyze": "vite build --mode analyze",
|
|
55
|
+
"lighthouse": "npm run lighthouse:index && npm run lighthouse:examples",
|
|
56
|
+
"lighthouse:index": "lighthouse http://localhost:5174 --output html --output-path ./lighthouse-reports/index.html --view",
|
|
57
|
+
"lighthouse:examples": "lighthouse http://localhost:5174/examples/index.html --output html --output-path ./lighthouse-reports/examples.html --view",
|
|
58
|
+
"lighthouse:ci": "lhci autorun",
|
|
59
|
+
"prepare": "husky install",
|
|
60
|
+
"a11y:check": "axe http://localhost:5174 --exit",
|
|
61
|
+
"deps:check": "depcheck",
|
|
62
|
+
"deps:update": "ncu -u",
|
|
63
|
+
"docs:dev": "vitepress dev docs",
|
|
64
|
+
"docs:build": "vitepress build docs",
|
|
65
|
+
"docs:preview": "vitepress preview docs"
|
|
66
|
+
},
|
|
67
|
+
"lint-staged": {
|
|
68
|
+
"*.{ts,tsx}": [
|
|
69
|
+
"eslint --fix --ignore-pattern 'test-apps/**' --ignore-pattern 'examples/**' --ignore-pattern 'adapters/**'",
|
|
70
|
+
"prettier --write"
|
|
71
|
+
],
|
|
72
|
+
"*.{json,md}": [
|
|
73
|
+
"prettier --write"
|
|
74
|
+
]
|
|
75
|
+
},
|
|
76
|
+
"devDependencies": {
|
|
77
|
+
"@axe-core/cli": "^4.11.0",
|
|
78
|
+
"@lhci/cli": "^0.15.1",
|
|
79
|
+
"@types/node": "^20.19.28",
|
|
80
|
+
"@typescript-eslint/eslint-plugin": "^6.13.0",
|
|
81
|
+
"@typescript-eslint/parser": "^6.13.0",
|
|
82
|
+
"@vitest/coverage-v8": "^1.6.1",
|
|
83
|
+
"@vitest/ui": "^1.0.0",
|
|
84
|
+
"bundle-analyzer": "^0.0.6",
|
|
85
|
+
"depcheck": "^1.4.7",
|
|
86
|
+
"eslint": "^8.55.0",
|
|
87
|
+
"husky": "^9.1.7",
|
|
88
|
+
"jsdom": "^27.4.0",
|
|
89
|
+
"lighthouse": "^13.0.1",
|
|
90
|
+
"lint-staged": "^16.2.7",
|
|
91
|
+
"npm-check-updates": "^19.3.1",
|
|
92
|
+
"prettier": "^3.1.0",
|
|
93
|
+
"terser": "^5.44.1",
|
|
94
|
+
"typescript": "^5.3.0",
|
|
95
|
+
"vite": "^5.0.0",
|
|
96
|
+
"vite-plugin-dts": "^3.6.0",
|
|
97
|
+
"vitepress": "^1.0.0",
|
|
98
|
+
"vitest": "^1.0.0",
|
|
99
|
+
"wait-on": "^9.0.3"
|
|
100
|
+
},
|
|
101
|
+
"engines": {
|
|
102
|
+
"node": ">=18.0.0",
|
|
103
|
+
"npm": ">=9.0.0"
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test setup file for Vitest
|
|
3
|
+
* Configures jsdom environment and utilities for Web Components testing
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { beforeAll, afterEach, vi } from 'vitest';
|
|
7
|
+
|
|
8
|
+
// Mock globalThis.customElements if not available
|
|
9
|
+
beforeAll(() => {
|
|
10
|
+
if (!globalThis.customElements) {
|
|
11
|
+
// @ts-expect-error - Mock for testing
|
|
12
|
+
globalThis.customElements = {
|
|
13
|
+
define: vi.fn(),
|
|
14
|
+
get: vi.fn(),
|
|
15
|
+
upgrade: vi.fn(),
|
|
16
|
+
whenDefined: vi.fn(() => Promise.resolve(class MockElement {} as CustomElementConstructor)),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Clean up after each test
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
document.body.innerHTML = '';
|
|
24
|
+
vi.clearAllMocks();
|
|
25
|
+
vi.clearAllTimers();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Polyfill for Custom Elements if needed
|
|
29
|
+
if (!globalThis.customElements) {
|
|
30
|
+
class CustomElementRegistry {
|
|
31
|
+
private readonly definitions = new Map<string, CustomElementConstructor>();
|
|
32
|
+
|
|
33
|
+
define(name: string, constructor: CustomElementConstructor) {
|
|
34
|
+
this.definitions.set(name, constructor);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get(name: string) {
|
|
38
|
+
return this.definitions.get(name);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
whenDefined(name: string): Promise<CustomElementConstructor> {
|
|
42
|
+
return Promise.resolve(this.definitions.get(name)!);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
upgrade(_root: Node) {
|
|
46
|
+
// No-op for testing
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// @ts-expect-error - Polyfill for testing
|
|
51
|
+
globalThis.customElements = new CustomElementRegistry();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Mock requestAnimationFrame for testing
|
|
55
|
+
globalThis.requestAnimationFrame = (callback: FrameRequestCallback) => {
|
|
56
|
+
return setTimeout(callback, 0) as unknown as number;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
globalThis.cancelAnimationFrame = (id: number) => {
|
|
60
|
+
clearTimeout(id);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Helper to wait for custom element to be defined
|
|
64
|
+
export async function waitForElement(element: HTMLElement): Promise<void> {
|
|
65
|
+
return new Promise((resolve) => {
|
|
66
|
+
if (element.shadowRoot) {
|
|
67
|
+
resolve();
|
|
68
|
+
} else {
|
|
69
|
+
setTimeout(resolve, 0);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Helper to wait for async operations
|
|
75
|
+
export function waitForNextTick(): Promise<void> {
|
|
76
|
+
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Helper to wait for multiple ticks
|
|
80
|
+
export function waitForTicks(count: number): Promise<void> {
|
|
81
|
+
return new Promise((resolve) => {
|
|
82
|
+
let ticks = 0;
|
|
83
|
+
const tick = () => {
|
|
84
|
+
ticks++;
|
|
85
|
+
if (ticks >= count) {
|
|
86
|
+
resolve();
|
|
87
|
+
} else {
|
|
88
|
+
setTimeout(tick, 0);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
tick();
|
|
92
|
+
});
|
|
93
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import type { AIControlConfig, ThemeConfig } from './types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Base class for all AI Progress Controls
|
|
5
|
+
* Provides common functionality for Web Components including:
|
|
6
|
+
* - Theme management
|
|
7
|
+
* - Event handling
|
|
8
|
+
* - Accessibility features
|
|
9
|
+
* - Debug logging
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* class MyControl extends AIControl {
|
|
14
|
+
* constructor() {
|
|
15
|
+
* super();
|
|
16
|
+
* this.attachShadow({ mode: 'open' });
|
|
17
|
+
* }
|
|
18
|
+
*
|
|
19
|
+
* protected render() {
|
|
20
|
+
* // Your rendering logic
|
|
21
|
+
* }
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export abstract class AIControl extends HTMLElement {
|
|
26
|
+
protected config: AIControlConfig;
|
|
27
|
+
protected _disabled: boolean = false;
|
|
28
|
+
protected startTime: number = 0;
|
|
29
|
+
|
|
30
|
+
constructor(config: AIControlConfig = {}) {
|
|
31
|
+
super();
|
|
32
|
+
this.config = {
|
|
33
|
+
debug: false,
|
|
34
|
+
disabled: false,
|
|
35
|
+
...config,
|
|
36
|
+
};
|
|
37
|
+
this._disabled = this.config.disabled ?? false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Called when the element is connected to the DOM
|
|
42
|
+
*/
|
|
43
|
+
connectedCallback(): void {
|
|
44
|
+
this.log('Component connected to DOM');
|
|
45
|
+
this.applyTheme();
|
|
46
|
+
this.setupAccessibility();
|
|
47
|
+
this.render();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Called when the element is disconnected from the DOM
|
|
52
|
+
*/
|
|
53
|
+
disconnectedCallback(): void {
|
|
54
|
+
this.log('Component disconnected from DOM');
|
|
55
|
+
this.cleanup();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Called when an observed attribute changes
|
|
60
|
+
*/
|
|
61
|
+
attributeChangedCallback(name: string, oldValue: string, newValue: string): void {
|
|
62
|
+
this.log(`Attribute ${name} changed from ${oldValue} to ${newValue}`);
|
|
63
|
+
this.handleAttributeChange(name, oldValue, newValue);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Apply theme using CSS custom properties
|
|
68
|
+
*/
|
|
69
|
+
protected applyTheme(theme?: ThemeConfig): void {
|
|
70
|
+
if (!theme) return;
|
|
71
|
+
|
|
72
|
+
const root = this.shadowRoot?.host as HTMLElement;
|
|
73
|
+
if (!root) return;
|
|
74
|
+
|
|
75
|
+
Object.entries(theme).forEach(([key, value]) => {
|
|
76
|
+
const cssVarName = `--ai-${key.replaceAll(/([A-Z])/g, '-$1').toLowerCase()}`;
|
|
77
|
+
root.style.setProperty(cssVarName, value);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Set up accessibility features (ARIA attributes, keyboard navigation)
|
|
83
|
+
*/
|
|
84
|
+
protected setupAccessibility(): void {
|
|
85
|
+
// Set role if not already set
|
|
86
|
+
if (!this.getAttribute('role')) {
|
|
87
|
+
this.setAttribute('role', this.getDefaultRole());
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Set aria-label if configured
|
|
91
|
+
if (this.config.ariaLabel) {
|
|
92
|
+
this.setAttribute('aria-label', this.config.ariaLabel);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Set disabled state
|
|
96
|
+
if (this._disabled) {
|
|
97
|
+
this.setAttribute('aria-disabled', 'true');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Emit a custom event
|
|
103
|
+
*/
|
|
104
|
+
protected emit<T>(eventName: string, detail?: T): void {
|
|
105
|
+
const event = new CustomEvent(eventName, {
|
|
106
|
+
detail,
|
|
107
|
+
bubbles: true,
|
|
108
|
+
composed: true,
|
|
109
|
+
});
|
|
110
|
+
this.dispatchEvent(event);
|
|
111
|
+
this.log(`Event emitted: ${eventName}`, detail);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Log debug messages (only if debug mode is enabled)
|
|
116
|
+
*/
|
|
117
|
+
protected log(message: string, ...args: unknown[]): void {
|
|
118
|
+
if (this.config.debug) {
|
|
119
|
+
console.log(`[${this.constructor.name}] ${message}`, ...args);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Log errors (always logged)
|
|
125
|
+
*/
|
|
126
|
+
protected logError(message: string, error?: Error): void {
|
|
127
|
+
console.error(`[${this.constructor.name}] ERROR: ${message}`, error);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Start timing an operation
|
|
132
|
+
*/
|
|
133
|
+
protected startTimer(): void {
|
|
134
|
+
this.startTime = Date.now();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get elapsed time since timer started
|
|
139
|
+
*/
|
|
140
|
+
protected getElapsedTime(): number {
|
|
141
|
+
return Date.now() - this.startTime;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Format numbers for display (e.g., 1234 -> "1.2K")
|
|
146
|
+
*/
|
|
147
|
+
protected formatNumber(num: number): string {
|
|
148
|
+
if (num >= 1_000_000) {
|
|
149
|
+
return `${(num / 1_000_000).toFixed(1)}M`;
|
|
150
|
+
}
|
|
151
|
+
if (num >= 1_000) {
|
|
152
|
+
return `${(num / 1_000).toFixed(1)}K`;
|
|
153
|
+
}
|
|
154
|
+
return num.toString();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Format duration in milliseconds to human-readable string
|
|
159
|
+
*/
|
|
160
|
+
protected formatDuration(ms: number): string {
|
|
161
|
+
const seconds = Math.floor(ms / 1000);
|
|
162
|
+
const minutes = Math.floor(seconds / 60);
|
|
163
|
+
const hours = Math.floor(minutes / 60);
|
|
164
|
+
|
|
165
|
+
if (hours > 0) {
|
|
166
|
+
return `${hours}h ${minutes % 60}m`;
|
|
167
|
+
}
|
|
168
|
+
if (minutes > 0) {
|
|
169
|
+
return `${minutes}m ${seconds % 60}s`;
|
|
170
|
+
}
|
|
171
|
+
return `${seconds}s`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Calculate percentage safely
|
|
176
|
+
*/
|
|
177
|
+
protected calculatePercentage(current: number, total: number): number {
|
|
178
|
+
if (total === 0) return 0;
|
|
179
|
+
return Math.min(100, Math.max(0, (current / total) * 100));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Get/Set disabled state
|
|
184
|
+
*/
|
|
185
|
+
get disabled(): boolean {
|
|
186
|
+
return this._disabled;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
set disabled(value: boolean) {
|
|
190
|
+
this._disabled = value;
|
|
191
|
+
if (value) {
|
|
192
|
+
this.setAttribute('aria-disabled', 'true');
|
|
193
|
+
this.setAttribute('disabled', '');
|
|
194
|
+
} else {
|
|
195
|
+
this.setAttribute('aria-disabled', 'false');
|
|
196
|
+
this.removeAttribute('disabled');
|
|
197
|
+
}
|
|
198
|
+
this.render();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Abstract method to render the component
|
|
203
|
+
* Must be implemented by subclasses
|
|
204
|
+
*/
|
|
205
|
+
protected abstract render(): void;
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Get the default ARIA role for this component
|
|
209
|
+
* Can be overridden by subclasses
|
|
210
|
+
*/
|
|
211
|
+
protected getDefaultRole(): string {
|
|
212
|
+
return 'region';
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Handle attribute changes
|
|
217
|
+
* Can be overridden by subclasses to handle specific attributes
|
|
218
|
+
*/
|
|
219
|
+
protected handleAttributeChange(_name: string, _oldValue: string, _newValue: string): void {
|
|
220
|
+
// Override in subclasses
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Cleanup resources when component is destroyed
|
|
225
|
+
* Can be overridden by subclasses
|
|
226
|
+
*/
|
|
227
|
+
protected cleanup(): void {
|
|
228
|
+
// Override in subclasses
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Size variant for components
|
|
3
|
+
*/
|
|
4
|
+
export type SizeVariant = 'compact' | 'default' | 'large';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Visual style variant for components
|
|
8
|
+
*/
|
|
9
|
+
export type VisualVariant = 'default' | 'minimal' | 'gradient' | 'glassmorphic';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Animation effect for progress indicators
|
|
13
|
+
*/
|
|
14
|
+
export type AnimationEffect = 'none' | 'striped' | 'pulse' | 'glow';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Base configuration for all AI Progress Controls
|
|
18
|
+
*/
|
|
19
|
+
export interface AIControlConfig {
|
|
20
|
+
/** Enable/disable console logging for debugging */
|
|
21
|
+
debug?: boolean;
|
|
22
|
+
/** Custom CSS class names to apply */
|
|
23
|
+
className?: string;
|
|
24
|
+
/** Whether the control is disabled */
|
|
25
|
+
disabled?: boolean;
|
|
26
|
+
/** ARIA label for accessibility */
|
|
27
|
+
ariaLabel?: string;
|
|
28
|
+
/** Enable automatic cursor feedback based on state (default: true) */
|
|
29
|
+
cursorFeedback?: boolean;
|
|
30
|
+
/** Size variant: compact, default, or large */
|
|
31
|
+
size?: SizeVariant;
|
|
32
|
+
/** Visual style variant: default, minimal, gradient, or glassmorphic */
|
|
33
|
+
variant?: VisualVariant;
|
|
34
|
+
/** Animation effect: none, striped, pulse, or glow */
|
|
35
|
+
animation?: AnimationEffect;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Theme configuration using CSS custom properties
|
|
40
|
+
*/
|
|
41
|
+
export interface ThemeConfig {
|
|
42
|
+
primaryColor?: string;
|
|
43
|
+
secondaryColor?: string;
|
|
44
|
+
backgroundColor?: string;
|
|
45
|
+
textColor?: string;
|
|
46
|
+
borderRadius?: string;
|
|
47
|
+
fontSize?: string;
|
|
48
|
+
fontFamily?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Event detail for progress updates
|
|
53
|
+
*/
|
|
54
|
+
export interface ProgressUpdateEvent {
|
|
55
|
+
current: number;
|
|
56
|
+
total?: number;
|
|
57
|
+
percentage?: number;
|
|
58
|
+
message?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Event detail for errors
|
|
63
|
+
*/
|
|
64
|
+
export interface ErrorEvent {
|
|
65
|
+
message: string;
|
|
66
|
+
code?: string;
|
|
67
|
+
timestamp: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Event detail for completion
|
|
72
|
+
*/
|
|
73
|
+
export interface CompleteEvent {
|
|
74
|
+
duration: number;
|
|
75
|
+
timestamp: number;
|
|
76
|
+
metadata?: Record<string, unknown>;
|
|
77
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for AI Progress Controls
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Debounce function calls
|
|
7
|
+
*/
|
|
8
|
+
export function debounce<T extends (...args: any[]) => any>(
|
|
9
|
+
func: T,
|
|
10
|
+
wait: number
|
|
11
|
+
): (...args: Parameters<T>) => void {
|
|
12
|
+
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
13
|
+
|
|
14
|
+
return function (this: any, ...args: Parameters<T>) {
|
|
15
|
+
if (timeout) clearTimeout(timeout);
|
|
16
|
+
timeout = setTimeout(() => func.apply(this, args), wait);
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Throttle function calls
|
|
22
|
+
*/
|
|
23
|
+
export function throttle<T extends (...args: any[]) => any>(
|
|
24
|
+
func: T,
|
|
25
|
+
limit: number
|
|
26
|
+
): (...args: Parameters<T>) => void {
|
|
27
|
+
let inThrottle: boolean;
|
|
28
|
+
return function (this: any, ...args: Parameters<T>) {
|
|
29
|
+
if (!inThrottle) {
|
|
30
|
+
func.apply(this, args);
|
|
31
|
+
inThrottle = true;
|
|
32
|
+
setTimeout(() => (inThrottle = false), limit);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Clamp a number between min and max
|
|
39
|
+
*/
|
|
40
|
+
export function clamp(value: number, min: number, max: number): number {
|
|
41
|
+
return Math.min(Math.max(value, min), max);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Linear interpolation
|
|
46
|
+
*/
|
|
47
|
+
export function lerp(start: number, end: number, t: number): number {
|
|
48
|
+
return start + (end - start) * t;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Easing function for smooth animations
|
|
53
|
+
*/
|
|
54
|
+
export function easeOutCubic(t: number): number {
|
|
55
|
+
return 1 - Math.pow(1 - t, 3);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Generate a unique ID
|
|
60
|
+
*/
|
|
61
|
+
export function generateId(prefix: string = 'ai-control'): string {
|
|
62
|
+
return `${prefix}-${Math.random().toString(36).slice(2, 11)}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Check if reduced motion is preferred
|
|
67
|
+
*/
|
|
68
|
+
export function prefersReducedMotion(): boolean {
|
|
69
|
+
return globalThis.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Format bytes to human-readable string
|
|
74
|
+
*/
|
|
75
|
+
export function formatBytes(bytes: number, decimals: number = 2): string {
|
|
76
|
+
if (bytes === 0) return '0 Bytes';
|
|
77
|
+
|
|
78
|
+
const k = 1024;
|
|
79
|
+
const dm = Math.max(0, decimals);
|
|
80
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
|
81
|
+
|
|
82
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
83
|
+
|
|
84
|
+
return `${Number.parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Format currency (USD)
|
|
89
|
+
*/
|
|
90
|
+
export function formatCurrency(amount: number, decimals: number = 4): string {
|
|
91
|
+
return `$${amount.toFixed(decimals)}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Format seconds to human-readable time string
|
|
96
|
+
*/
|
|
97
|
+
export function formatTime(seconds: number): string {
|
|
98
|
+
if (seconds < 60) {
|
|
99
|
+
return `${Math.round(seconds)}s`;
|
|
100
|
+
}
|
|
101
|
+
const minutes = Math.floor(seconds / 60);
|
|
102
|
+
const remainingSeconds = Math.round(seconds % 60);
|
|
103
|
+
if (minutes < 60) {
|
|
104
|
+
return remainingSeconds > 0 ? `${minutes}m ${remainingSeconds}s` : `${minutes}m`;
|
|
105
|
+
}
|
|
106
|
+
const hours = Math.floor(minutes / 60);
|
|
107
|
+
const remainingMinutes = minutes % 60;
|
|
108
|
+
return remainingMinutes > 0 ? `${hours}h ${remainingMinutes}m` : `${hours}h`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Calculate estimated time remaining
|
|
113
|
+
*/
|
|
114
|
+
export function calculateETA(current: number, total: number, startTime: number): number {
|
|
115
|
+
if (current === 0) return 0;
|
|
116
|
+
|
|
117
|
+
const elapsed = Date.now() - startTime;
|
|
118
|
+
const rate = current / elapsed;
|
|
119
|
+
const remaining = total - current;
|
|
120
|
+
|
|
121
|
+
return remaining / rate;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Parse CSS color to RGB
|
|
126
|
+
*/
|
|
127
|
+
export function parseColor(color: string): { r: number; g: number; b: number } | null {
|
|
128
|
+
const div = document.createElement('div');
|
|
129
|
+
div.style.color = color;
|
|
130
|
+
document.body.appendChild(div);
|
|
131
|
+
|
|
132
|
+
const computedColor = getComputedStyle(div).color;
|
|
133
|
+
div.remove();
|
|
134
|
+
|
|
135
|
+
const regex = /^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/;
|
|
136
|
+
const match = regex.exec(computedColor);
|
|
137
|
+
if (match?.[1] && match?.[2] && match?.[3]) {
|
|
138
|
+
return {
|
|
139
|
+
r: Number.parseInt(match[1], 10),
|
|
140
|
+
g: Number.parseInt(match[2], 10),
|
|
141
|
+
b: Number.parseInt(match[3], 10),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Create a style element with CSS
|
|
150
|
+
*/
|
|
151
|
+
export function createStyle(css: string): HTMLStyleElement {
|
|
152
|
+
const style = document.createElement('style');
|
|
153
|
+
style.textContent = css;
|
|
154
|
+
return style;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Check if an element is in viewport
|
|
159
|
+
*/
|
|
160
|
+
export function isInViewport(element: HTMLElement): boolean {
|
|
161
|
+
const rect = element.getBoundingClientRect();
|
|
162
|
+
return (
|
|
163
|
+
rect.top >= 0 &&
|
|
164
|
+
rect.left >= 0 &&
|
|
165
|
+
rect.bottom <= (globalThis.innerHeight || document.documentElement.clientHeight) &&
|
|
166
|
+
rect.right <= (globalThis.innerWidth || document.documentElement.clientWidth)
|
|
167
|
+
);
|
|
168
|
+
}
|