agentgui 1.0.822 → 1.0.824
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/CHANGELOG.md +16 -0
- package/lib/jsonl-watcher.js +13 -64
- package/package.json +2 -1
- package/static/index.html +4 -1
- package/static/js/dialogs-types.js +111 -0
- package/static/js/dialogs.js +53 -267
- package/static/js/image-loader-element.js +76 -0
- package/static/js/image-loader.js +0 -74
- package/static/js/syntax-highlighter-render.js +72 -0
- package/static/js/syntax-highlighter.js +0 -84
- package/static/js/ui-components-rendering.js +61 -182
- package/static/js/ws-latency.js +88 -0
- package/static/js/event-filter.js +0 -118
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
|
|
2
|
+
Object.assign(ImageLoader.prototype, {
|
|
3
|
+
createImageElement(imagePath, options = {}) {
|
|
4
|
+
const container = document.createElement('div');
|
|
5
|
+
container.className = 'image-container';
|
|
6
|
+
container.dataset.imagePath = imagePath;
|
|
7
|
+
container.style.cssText = `
|
|
8
|
+
display: flex;
|
|
9
|
+
flex-direction: column;
|
|
10
|
+
gap: 0.5rem;
|
|
11
|
+
padding: 0.75rem;
|
|
12
|
+
border-radius: 0.375rem;
|
|
13
|
+
background: var(--color-bg-secondary);
|
|
14
|
+
border: 1px solid var(--color-border);
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
const placeholder = document.createElement('div');
|
|
18
|
+
placeholder.className = 'image-placeholder';
|
|
19
|
+
placeholder.style.cssText = `
|
|
20
|
+
background: linear-gradient(90deg, var(--color-bg-tertiary) 25%, var(--color-bg-secondary) 50%, var(--color-bg-tertiary) 75%);
|
|
21
|
+
background-size: 200% 100%;
|
|
22
|
+
animation: loading 1.5s infinite;
|
|
23
|
+
border-radius: 0.375rem;
|
|
24
|
+
aspect-ratio: 16/9;
|
|
25
|
+
display: flex;
|
|
26
|
+
align-items: center;
|
|
27
|
+
justify-content: center;
|
|
28
|
+
color: var(--color-text-secondary);
|
|
29
|
+
font-size: 0.875rem;
|
|
30
|
+
`;
|
|
31
|
+
placeholder.innerHTML = 'Loading image...';
|
|
32
|
+
placeholder.dataset.path = imagePath;
|
|
33
|
+
|
|
34
|
+
const img = document.createElement('img');
|
|
35
|
+
img.className = 'lazy-image';
|
|
36
|
+
img.alt = imagePath;
|
|
37
|
+
img.style.cssText = `
|
|
38
|
+
max-width: 100%;
|
|
39
|
+
max-height: ${this.config.maxImageDisplaySize};
|
|
40
|
+
border-radius: 0.375rem;
|
|
41
|
+
display: none;
|
|
42
|
+
`;
|
|
43
|
+
img.dataset.src = imagePath;
|
|
44
|
+
|
|
45
|
+
const caption = document.createElement('div');
|
|
46
|
+
caption.className = 'image-caption';
|
|
47
|
+
caption.style.cssText = `
|
|
48
|
+
font-size: 0.75rem;
|
|
49
|
+
color: var(--color-text-secondary);
|
|
50
|
+
word-break: break-all;
|
|
51
|
+
font-family: 'Monaco', 'Menlo', monospace;
|
|
52
|
+
`;
|
|
53
|
+
caption.textContent = imagePath;
|
|
54
|
+
|
|
55
|
+
container.appendChild(placeholder);
|
|
56
|
+
container.appendChild(img);
|
|
57
|
+
container.appendChild(caption);
|
|
58
|
+
|
|
59
|
+
img.addEventListener('load', () => {
|
|
60
|
+
placeholder.style.display = 'none';
|
|
61
|
+
img.style.display = 'block';
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
img.addEventListener('error', () => {
|
|
65
|
+
placeholder.textContent = 'Failed to load image';
|
|
66
|
+
placeholder.style.background = 'var(--color-bg-error)';
|
|
67
|
+
placeholder.style.color = 'var(--color-text-error)';
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (this.intersectionObserver) {
|
|
71
|
+
this.intersectionObserver.observe(container);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return container;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
@@ -89,80 +89,6 @@ class ImageLoader {
|
|
|
89
89
|
return images;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
createImageElement(imagePath, options = {}) {
|
|
93
|
-
const container = document.createElement('div');
|
|
94
|
-
container.className = 'image-container';
|
|
95
|
-
container.dataset.imagePath = imagePath;
|
|
96
|
-
container.style.cssText = `
|
|
97
|
-
display: flex;
|
|
98
|
-
flex-direction: column;
|
|
99
|
-
gap: 0.5rem;
|
|
100
|
-
padding: 0.75rem;
|
|
101
|
-
border-radius: 0.375rem;
|
|
102
|
-
background: var(--color-bg-secondary);
|
|
103
|
-
border: 1px solid var(--color-border);
|
|
104
|
-
`;
|
|
105
|
-
|
|
106
|
-
const placeholder = document.createElement('div');
|
|
107
|
-
placeholder.className = 'image-placeholder';
|
|
108
|
-
placeholder.style.cssText = `
|
|
109
|
-
background: linear-gradient(90deg, var(--color-bg-tertiary) 25%, var(--color-bg-secondary) 50%, var(--color-bg-tertiary) 75%);
|
|
110
|
-
background-size: 200% 100%;
|
|
111
|
-
animation: loading 1.5s infinite;
|
|
112
|
-
border-radius: 0.375rem;
|
|
113
|
-
aspect-ratio: 16/9;
|
|
114
|
-
display: flex;
|
|
115
|
-
align-items: center;
|
|
116
|
-
justify-content: center;
|
|
117
|
-
color: var(--color-text-secondary);
|
|
118
|
-
font-size: 0.875rem;
|
|
119
|
-
`;
|
|
120
|
-
placeholder.innerHTML = 'Loading image...';
|
|
121
|
-
placeholder.dataset.path = imagePath;
|
|
122
|
-
|
|
123
|
-
const img = document.createElement('img');
|
|
124
|
-
img.className = 'lazy-image';
|
|
125
|
-
img.alt = imagePath;
|
|
126
|
-
img.style.cssText = `
|
|
127
|
-
max-width: 100%;
|
|
128
|
-
max-height: ${this.config.maxImageDisplaySize};
|
|
129
|
-
border-radius: 0.375rem;
|
|
130
|
-
display: none;
|
|
131
|
-
`;
|
|
132
|
-
img.dataset.src = imagePath;
|
|
133
|
-
|
|
134
|
-
const caption = document.createElement('div');
|
|
135
|
-
caption.className = 'image-caption';
|
|
136
|
-
caption.style.cssText = `
|
|
137
|
-
font-size: 0.75rem;
|
|
138
|
-
color: var(--color-text-secondary);
|
|
139
|
-
word-break: break-all;
|
|
140
|
-
font-family: 'Monaco', 'Menlo', monospace;
|
|
141
|
-
`;
|
|
142
|
-
caption.textContent = imagePath;
|
|
143
|
-
|
|
144
|
-
container.appendChild(placeholder);
|
|
145
|
-
container.appendChild(img);
|
|
146
|
-
container.appendChild(caption);
|
|
147
|
-
|
|
148
|
-
img.addEventListener('load', () => {
|
|
149
|
-
placeholder.style.display = 'none';
|
|
150
|
-
img.style.display = 'block';
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
img.addEventListener('error', () => {
|
|
154
|
-
placeholder.textContent = 'Failed to load image';
|
|
155
|
-
placeholder.style.background = 'var(--color-bg-error)';
|
|
156
|
-
placeholder.style.color = 'var(--color-text-error)';
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
if (this.intersectionObserver) {
|
|
160
|
-
this.intersectionObserver.observe(container);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return container;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
92
|
initIntersectionObserver() {
|
|
167
93
|
this.intersectionObserver = new IntersectionObserver(
|
|
168
94
|
(entries) => {
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
Object.assign(SyntaxHighlighter.prototype, {
|
|
2
|
+
async highlight(code, language = 'plaintext') {
|
|
3
|
+
if (!code) return '';
|
|
4
|
+
if (this.config.lazyLoad) {
|
|
5
|
+
try {
|
|
6
|
+
await this.ensureLoaded();
|
|
7
|
+
} catch (error) {
|
|
8
|
+
console.warn('Prism loading failed, returning unformatted code');
|
|
9
|
+
return this.escapeHtml(code);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const cacheKey = `${language}:${code}`;
|
|
13
|
+
if (this.config.enableCache && this.highlightCache.has(cacheKey)) return this.highlightCache.get(cacheKey);
|
|
14
|
+
let highlighted;
|
|
15
|
+
try {
|
|
16
|
+
if (typeof Prism !== 'undefined' && Prism.languages[language]) {
|
|
17
|
+
highlighted = Prism.highlight(code, Prism.languages[language], language);
|
|
18
|
+
} else {
|
|
19
|
+
highlighted = this.escapeHtml(code);
|
|
20
|
+
}
|
|
21
|
+
} catch (error) {
|
|
22
|
+
console.error('Highlight error:', error);
|
|
23
|
+
highlighted = this.escapeHtml(code);
|
|
24
|
+
}
|
|
25
|
+
if (this.config.enableCache) {
|
|
26
|
+
this.highlightCache.set(cacheKey, highlighted);
|
|
27
|
+
if (this.highlightCache.size > this.config.maxCacheSize) {
|
|
28
|
+
const firstKey = this.highlightCache.keys().next().value;
|
|
29
|
+
this.highlightCache.delete(firstKey);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return highlighted;
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
async createHighlightedElement(code, language = 'plaintext') {
|
|
36
|
+
const pre = document.createElement('pre');
|
|
37
|
+
const code_el = document.createElement('code');
|
|
38
|
+
code_el.className = `language-${language}`;
|
|
39
|
+
if (this.config.lazyLoad) {
|
|
40
|
+
try {
|
|
41
|
+
await this.ensureLoaded();
|
|
42
|
+
const highlighted = await this.highlight(code, language);
|
|
43
|
+
code_el.innerHTML = highlighted;
|
|
44
|
+
} catch (error) {
|
|
45
|
+
code_el.textContent = code;
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
code_el.textContent = code;
|
|
49
|
+
}
|
|
50
|
+
pre.appendChild(code_el);
|
|
51
|
+
return pre;
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
async highlightElement(element) {
|
|
55
|
+
if (!element || !element.querySelector('code')) return;
|
|
56
|
+
if (this.config.lazyLoad) {
|
|
57
|
+
try {
|
|
58
|
+
await this.ensureLoaded();
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.warn('Prism loading failed, skipping highlighting');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (typeof Prism !== 'undefined') {
|
|
65
|
+
try {
|
|
66
|
+
Prism.highlightElement(element.querySelector('code'));
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error('Element highlighting error:', error);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
});
|
|
@@ -81,90 +81,6 @@ class SyntaxHighlighter {
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
async highlight(code, language = 'plaintext') {
|
|
85
|
-
if (!code) return '';
|
|
86
|
-
|
|
87
|
-
if (this.config.lazyLoad) {
|
|
88
|
-
try {
|
|
89
|
-
await this.ensureLoaded();
|
|
90
|
-
} catch (error) {
|
|
91
|
-
console.warn('Prism loading failed, returning unformatted code');
|
|
92
|
-
return this.escapeHtml(code);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const cacheKey = `${language}:${code}`;
|
|
97
|
-
if (this.config.enableCache && this.highlightCache.has(cacheKey)) {
|
|
98
|
-
return this.highlightCache.get(cacheKey);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
let highlighted;
|
|
102
|
-
try {
|
|
103
|
-
if (typeof Prism !== 'undefined' && Prism.languages[language]) {
|
|
104
|
-
highlighted = Prism.highlight(code, Prism.languages[language], language);
|
|
105
|
-
} else {
|
|
106
|
-
highlighted = this.escapeHtml(code);
|
|
107
|
-
}
|
|
108
|
-
} catch (error) {
|
|
109
|
-
console.error('Highlight error:', error);
|
|
110
|
-
highlighted = this.escapeHtml(code);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (this.config.enableCache) {
|
|
114
|
-
this.highlightCache.set(cacheKey, highlighted);
|
|
115
|
-
|
|
116
|
-
if (this.highlightCache.size > this.config.maxCacheSize) {
|
|
117
|
-
const firstKey = this.highlightCache.keys().next().value;
|
|
118
|
-
this.highlightCache.delete(firstKey);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return highlighted;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
async createHighlightedElement(code, language = 'plaintext') {
|
|
126
|
-
const pre = document.createElement('pre');
|
|
127
|
-
const code_el = document.createElement('code');
|
|
128
|
-
|
|
129
|
-
code_el.className = `language-${language}`;
|
|
130
|
-
|
|
131
|
-
if (this.config.lazyLoad) {
|
|
132
|
-
try {
|
|
133
|
-
await this.ensureLoaded();
|
|
134
|
-
const highlighted = await this.highlight(code, language);
|
|
135
|
-
code_el.innerHTML = highlighted;
|
|
136
|
-
} catch (error) {
|
|
137
|
-
code_el.textContent = code;
|
|
138
|
-
}
|
|
139
|
-
} else {
|
|
140
|
-
code_el.textContent = code;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
pre.appendChild(code_el);
|
|
144
|
-
return pre;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
async highlightElement(element) {
|
|
148
|
-
if (!element || !element.querySelector('code')) return;
|
|
149
|
-
|
|
150
|
-
if (this.config.lazyLoad) {
|
|
151
|
-
try {
|
|
152
|
-
await this.ensureLoaded();
|
|
153
|
-
} catch (error) {
|
|
154
|
-
console.warn('Prism loading failed, skipping highlighting');
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
if (typeof Prism !== 'undefined') {
|
|
160
|
-
try {
|
|
161
|
-
Prism.highlightElement(element.querySelector('code'));
|
|
162
|
-
} catch (error) {
|
|
163
|
-
console.error('Element highlighting error:', error);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
84
|
detectLanguage(code) {
|
|
169
85
|
if (!code) return 'plaintext';
|
|
170
86
|
|
|
@@ -1,183 +1,62 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
return false;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
html += `<
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
</div>
|
|
62
|
-
</details>
|
|
63
|
-
`;
|
|
64
|
-
|
|
65
|
-
return container;
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
UIComponents.createInput = function(config = {}) {
|
|
69
|
-
const {
|
|
70
|
-
type = 'text',
|
|
71
|
-
name = '',
|
|
72
|
-
label = '',
|
|
73
|
-
placeholder = '',
|
|
74
|
-
value = '',
|
|
75
|
-
required = false
|
|
76
|
-
} = config;
|
|
77
|
-
|
|
78
|
-
const container = document.createElement('div');
|
|
79
|
-
container.className = 'form-group mb-4';
|
|
80
|
-
|
|
81
|
-
let html = '';
|
|
82
|
-
if (label) {
|
|
83
|
-
html += `<label class="block text-sm font-medium mb-2">${UIComponents.escapeHtml(label)}</label>`;
|
|
1
|
+
Object.assign(UIComponents, {
|
|
2
|
+
escapeHtml(text) { return window._escHtml ? window._escHtml(String(text)) : String(text).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); },
|
|
3
|
+
|
|
4
|
+
copyToClipboard(text) {
|
|
5
|
+
return navigator.clipboard.writeText(text).catch(() => false);
|
|
6
|
+
},
|
|
7
|
+
|
|
8
|
+
downloadFile(data, filename, mimeType = 'text/plain') {
|
|
9
|
+
const blob = new Blob([data], { type: mimeType });
|
|
10
|
+
const url = URL.createObjectURL(blob);
|
|
11
|
+
const link = document.createElement('a');
|
|
12
|
+
link.href = url; link.download = filename;
|
|
13
|
+
document.body.appendChild(link); link.click(); document.body.removeChild(link);
|
|
14
|
+
URL.revokeObjectURL(url);
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
createInput(config = {}) {
|
|
18
|
+
const { type = 'text', name = '', label = '', placeholder = '', value = '', required = false } = config;
|
|
19
|
+
const container = document.createElement('div');
|
|
20
|
+
container.className = 'form-group mb-4';
|
|
21
|
+
let html = '';
|
|
22
|
+
if (label) html += `<label class="block text-sm font-medium mb-2">${UIComponents.escapeHtml(label)}</label>`;
|
|
23
|
+
html += `<input type="${type}" name="${name}" placeholder="${UIComponents.escapeHtml(placeholder)}" value="${UIComponents.escapeHtml(value)}" ${required ? 'required' : ''} class="input input-block input-solid" />`;
|
|
24
|
+
container.innerHTML = html;
|
|
25
|
+
return container;
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
createSelect(config = {}) {
|
|
29
|
+
const { name = '', label = '', options = [], value = '', required = false } = config;
|
|
30
|
+
const container = document.createElement('div');
|
|
31
|
+
container.className = 'form-group mb-4';
|
|
32
|
+
let html = '';
|
|
33
|
+
if (label) html += `<label class="block text-sm font-medium mb-2">${UIComponents.escapeHtml(label)}</label>`;
|
|
34
|
+
html += `<select name="${name}" ${required ? 'required' : ''} class="select select-block select-solid">${options.map(opt => `<option value="${opt.value}" ${opt.value === value ? 'selected' : ''}>${UIComponents.escapeHtml(opt.label)}</option>`).join('')}</select>`;
|
|
35
|
+
container.innerHTML = html;
|
|
36
|
+
return container;
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
createButtonGroup(config = {}) {
|
|
40
|
+
const { buttons = [], vertical = false } = config;
|
|
41
|
+
const container = document.createElement('div');
|
|
42
|
+
container.className = `button-group flex gap-2 ${vertical ? 'flex-col' : 'flex-row'}`;
|
|
43
|
+
buttons.forEach(btn => {
|
|
44
|
+
const button = document.createElement('button');
|
|
45
|
+
button.className = `btn btn-${btn.variant || 'secondary'} flex-${vertical ? '1' : 'none'}`;
|
|
46
|
+
button.textContent = btn.label;
|
|
47
|
+
if (btn.onClick) button.addEventListener('click', btn.onClick);
|
|
48
|
+
container.appendChild(button);
|
|
49
|
+
});
|
|
50
|
+
return container;
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
createBadge(config = {}) {
|
|
54
|
+
const { label = '', variant = 'default', size = 'medium' } = config;
|
|
55
|
+
const sizeClasses = { small: 'badge-sm', medium: 'badge-md', large: 'badge-lg' };
|
|
56
|
+
const variantClasses = { default: 'badge-flat', primary: 'badge-flat-primary', success: 'badge-flat-success', warning: 'badge-flat-warning', error: 'badge-flat-error' };
|
|
57
|
+
const badge = document.createElement('span');
|
|
58
|
+
badge.className = `badge ${sizeClasses[size] || sizeClasses.medium} ${variantClasses[variant] || variantClasses.default}`;
|
|
59
|
+
badge.textContent = label;
|
|
60
|
+
return badge;
|
|
84
61
|
}
|
|
85
|
-
|
|
86
|
-
html += `
|
|
87
|
-
<input
|
|
88
|
-
type="${type}"
|
|
89
|
-
name="${name}"
|
|
90
|
-
placeholder="${UIComponents.escapeHtml(placeholder)}"
|
|
91
|
-
value="${UIComponents.escapeHtml(value)}"
|
|
92
|
-
${required ? 'required' : ''}
|
|
93
|
-
class="input input-block input-solid"
|
|
94
|
-
/>
|
|
95
|
-
`;
|
|
96
|
-
|
|
97
|
-
container.innerHTML = html;
|
|
98
|
-
return container;
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
UIComponents.createSelect = function(config = {}) {
|
|
102
|
-
const {
|
|
103
|
-
name = '',
|
|
104
|
-
label = '',
|
|
105
|
-
options = [],
|
|
106
|
-
value = '',
|
|
107
|
-
required = false
|
|
108
|
-
} = config;
|
|
109
|
-
|
|
110
|
-
const container = document.createElement('div');
|
|
111
|
-
container.className = 'form-group mb-4';
|
|
112
|
-
|
|
113
|
-
let html = '';
|
|
114
|
-
if (label) {
|
|
115
|
-
html += `<label class="block text-sm font-medium mb-2">${UIComponents.escapeHtml(label)}</label>`;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
html += `
|
|
119
|
-
<select
|
|
120
|
-
name="${name}"
|
|
121
|
-
${required ? 'required' : ''}
|
|
122
|
-
class="select select-block select-solid"
|
|
123
|
-
>
|
|
124
|
-
${options.map(opt => `
|
|
125
|
-
<option value="${opt.value}" ${opt.value === value ? 'selected' : ''}>
|
|
126
|
-
${UIComponents.escapeHtml(opt.label)}
|
|
127
|
-
</option>
|
|
128
|
-
`).join('')}
|
|
129
|
-
</select>
|
|
130
|
-
`;
|
|
131
|
-
|
|
132
|
-
container.innerHTML = html;
|
|
133
|
-
return container;
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
UIComponents.createButtonGroup = function(config = {}) {
|
|
137
|
-
const {
|
|
138
|
-
buttons = [],
|
|
139
|
-
vertical = false
|
|
140
|
-
} = config;
|
|
141
|
-
|
|
142
|
-
const container = document.createElement('div');
|
|
143
|
-
container.className = `button-group flex gap-2 ${vertical ? 'flex-col' : 'flex-row'}`;
|
|
144
|
-
|
|
145
|
-
buttons.forEach(btn => {
|
|
146
|
-
const button = document.createElement('button');
|
|
147
|
-
button.className = `btn btn-${btn.variant || 'secondary'} flex-${vertical ? '1' : 'none'}`;
|
|
148
|
-
button.textContent = btn.label;
|
|
149
|
-
if (btn.onClick) {
|
|
150
|
-
button.addEventListener('click', btn.onClick);
|
|
151
|
-
}
|
|
152
|
-
container.appendChild(button);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
return container;
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
UIComponents.createBadge = function(config = {}) {
|
|
159
|
-
const {
|
|
160
|
-
label = '',
|
|
161
|
-
variant = 'default',
|
|
162
|
-
size = 'medium'
|
|
163
|
-
} = config;
|
|
164
|
-
|
|
165
|
-
const sizeClasses = {
|
|
166
|
-
'small': 'badge-sm',
|
|
167
|
-
'medium': 'badge-md',
|
|
168
|
-
'large': 'badge-lg'
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
const variantClasses = {
|
|
172
|
-
'default': 'badge-flat',
|
|
173
|
-
'primary': 'badge-flat-primary',
|
|
174
|
-
'success': 'badge-flat-success',
|
|
175
|
-
'warning': 'badge-flat-warning',
|
|
176
|
-
'error': 'badge-flat-error'
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
const badge = document.createElement('span');
|
|
180
|
-
badge.className = `badge ${sizeClasses[size] || sizeClasses['medium']} ${variantClasses[variant] || variantClasses['default']}`;
|
|
181
|
-
badge.textContent = label;
|
|
182
|
-
return badge;
|
|
183
|
-
};
|
|
62
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
Object.assign(WebSocketManager.prototype, {
|
|
2
|
+
startHeartbeat() {
|
|
3
|
+
this.stopHeartbeat();
|
|
4
|
+
this.heartbeatTimer = setInterval(() => { this.ping(); }, this.config.heartbeatInterval);
|
|
5
|
+
},
|
|
6
|
+
|
|
7
|
+
stopHeartbeat() {
|
|
8
|
+
if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; }
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
_reportLatency() {
|
|
12
|
+
this.emit('latency', { current: this.latency.current, avg: this.latency.avg, jitter: this.latency.jitter, quality: this.latency.quality, trend: this.latency.trend });
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
_handleVisibilityChange() {
|
|
16
|
+
if (document.visibilityState === 'visible' && !this.isConnected && !this.isManuallyDisconnected) this.connect().catch(() => {});
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
_handleOnline() {
|
|
20
|
+
if (!this.isConnected && !this.isManuallyDisconnected) this.connect().catch(() => {});
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
ping() {
|
|
24
|
+
if (!this.isConnected) return;
|
|
25
|
+
this.latency.pingCounter++;
|
|
26
|
+
const id = this.latency.pingCounter;
|
|
27
|
+
const pongTimer = setTimeout(() => {
|
|
28
|
+
this.latency.missedPongs++;
|
|
29
|
+
this.emit('latency', { current: null, avg: this.latency.avg, jitter: this.latency.jitter, quality: 'poor', missed: true });
|
|
30
|
+
if (this.latency.missedPongs >= 3) { this.onClose(); }
|
|
31
|
+
}, this.config.pongTimeout);
|
|
32
|
+
this.requestMap.set('ping:' + id, { type: 'ping', sentAt: Date.now(), pongTimer });
|
|
33
|
+
try {
|
|
34
|
+
this.ws.send(window._codec ? window._codec.encode({ type: 'ping', id }) : msgpackr.pack({ type: 'ping', id }));
|
|
35
|
+
} catch (_) { clearTimeout(pongTimer); this.requestMap.delete('ping:' + id); }
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
_handlePong(data) {
|
|
39
|
+
const key = 'ping:' + data.id;
|
|
40
|
+
const pending = this.requestMap.get(key);
|
|
41
|
+
if (!pending) return;
|
|
42
|
+
clearTimeout(pending.pongTimer);
|
|
43
|
+
this.requestMap.delete(key);
|
|
44
|
+
this.latency.missedPongs = 0;
|
|
45
|
+
const rtt = Date.now() - pending.sentAt;
|
|
46
|
+
this._recordLatency(rtt);
|
|
47
|
+
this._checkDegradation();
|
|
48
|
+
this._reportLatency();
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
_recordLatency(rtt) {
|
|
52
|
+
const samples = this.latency.samples;
|
|
53
|
+
samples.push(rtt);
|
|
54
|
+
if (samples.length > this.config.latencyWindowSize) samples.shift();
|
|
55
|
+
this.latency.current = rtt;
|
|
56
|
+
this.latency.avg = Math.round(samples.reduce((a, b) => a + b, 0) / samples.length);
|
|
57
|
+
const jitterSamples = samples.slice(1).map((v, i) => Math.abs(v - samples[i]));
|
|
58
|
+
this.latency.jitter = jitterSamples.length ? Math.round(jitterSamples.reduce((a, b) => a + b, 0) / jitterSamples.length) : 0;
|
|
59
|
+
this.latency.quality = this._qualityTier(this.latency.avg);
|
|
60
|
+
if (this._latencyEma === null) this._latencyEma = rtt;
|
|
61
|
+
else this._latencyEma = 0.3 * rtt + 0.7 * this._latencyEma;
|
|
62
|
+
this.latency.predicted = Math.round(this._latencyEma);
|
|
63
|
+
this.stats.avgLatency = this.latency.avg;
|
|
64
|
+
this._trendHistory.push(rtt);
|
|
65
|
+
if (this._trendHistory.length > 5) this._trendHistory.shift();
|
|
66
|
+
if (this._trendHistory.length >= 3) {
|
|
67
|
+
const h = this._trendHistory;
|
|
68
|
+
const slope = (h[h.length - 1] - h[0]) / h.length;
|
|
69
|
+
this.latency.trend = slope > 5 ? 'rising' : slope < -5 ? 'falling' : 'stable';
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
_qualityTier(avg) {
|
|
74
|
+
if (avg < 50) return 'excellent';
|
|
75
|
+
if (avg < 150) return 'good';
|
|
76
|
+
if (avg < 300) return 'fair';
|
|
77
|
+
return 'poor';
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
_checkDegradation() {
|
|
81
|
+
const tier = this.latency.quality;
|
|
82
|
+
const prev = this._lastQualityTier;
|
|
83
|
+
if (tier !== prev) {
|
|
84
|
+
this._lastQualityTier = tier;
|
|
85
|
+
this.emit('quality_change', { quality: tier, latency: this.latency.avg });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
});
|