agentgui 1.0.260 → 1.0.262

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.
@@ -494,7 +494,7 @@ class AgentGUIClient {
494
494
  this.handleRateLimitClear(data);
495
495
  break;
496
496
  case 'model_download_progress':
497
- this._handleModelDownloadProgress(data.progress);
497
+ this._handleModelDownloadProgress(data.progress || data);
498
498
  break;
499
499
  default:
500
500
  break;
@@ -1188,9 +1188,10 @@ class AgentGUIClient {
1188
1188
  const tTitle = hasRenderer && block.input ? StreamingRenderer.getToolTitle(tn, block.input) : '';
1189
1189
  const iconHtml = hasRenderer && this.renderer ? `<span class="folded-tool-icon">${this.renderer.getToolIcon(tn)}</span>` : '';
1190
1190
  const typeClass = hasRenderer && this.renderer ? this.renderer._getBlockTypeClass('tool_use') : 'block-type-tool_use';
1191
+ const toolColorClass = hasRenderer && this.renderer ? this.renderer._getToolColorClass(tn) : 'tool-color-default';
1191
1192
  const nextBlock = blocks[blockIdx + 1];
1192
1193
  const resultClass = nextBlock?.type === 'tool_result' ? (nextBlock.is_error ? 'has-error' : 'has-success') : '';
1193
- html += `<details class="block-tool-use folded-tool ${typeClass} ${resultClass}"><summary class="folded-tool-bar">${iconHtml}<span class="folded-tool-name">${this.escapeHtml(dName)}</span>${tTitle ? `<span class="folded-tool-desc">${this.escapeHtml(tTitle)}</span>` : ''}</summary>${inputHtml}`;
1194
+ html += `<details class="block-tool-use folded-tool ${typeClass} ${toolColorClass} ${resultClass}"><summary class="folded-tool-bar">${iconHtml}<span class="folded-tool-name">${this.escapeHtml(dName)}</span>${tTitle ? `<span class="folded-tool-desc">${this.escapeHtml(tTitle)}</span>` : ''}</summary>${inputHtml}`;
1194
1195
  pendingToolUseClose = true;
1195
1196
  } else if (block.type === 'tool_result') {
1196
1197
  const content = typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
@@ -2027,16 +2028,16 @@ class AgentGUIClient {
2027
2028
 
2028
2029
  _handleModelDownloadProgress(progress) {
2029
2030
  this._modelDownloadProgress = progress;
2030
-
2031
- if (progress.error) {
2031
+
2032
+ if (progress.status === 'failed' || progress.error) {
2032
2033
  this._modelDownloadInProgress = false;
2033
- console.error('[Models] Download error:', progress.error);
2034
+ console.error('[Models] Download error:', progress.error || progress.status);
2034
2035
  this._updateConnectionIndicator(this.wsManager?.latency?.quality || 'unknown');
2035
2036
  if (window._voiceProgressDialog) {
2036
2037
  window._voiceProgressDialog.close();
2037
2038
  window._voiceProgressDialog = null;
2038
2039
  }
2039
- const errorMsg = 'Failed to download voice models: ' + progress.error;
2040
+ const errorMsg = 'Failed to download voice models: ' + (progress.error || 'unknown error');
2040
2041
  if (window.UIDialog) {
2041
2042
  window.UIDialog.alert(errorMsg, 'Download Error');
2042
2043
  } else {
@@ -2044,8 +2045,8 @@ class AgentGUIClient {
2044
2045
  }
2045
2046
  return;
2046
2047
  }
2047
-
2048
- if (progress.done) {
2048
+
2049
+ if (progress.done || progress.status === 'completed') {
2049
2050
  this._modelDownloadInProgress = false;
2050
2051
  console.log('[Models] Download complete');
2051
2052
  this._updateConnectionIndicator(this.wsManager?.latency?.quality || 'unknown');
@@ -2066,8 +2067,8 @@ class AgentGUIClient {
2066
2067
  }
2067
2068
  return;
2068
2069
  }
2069
-
2070
- if (progress.started || progress.downloading) {
2070
+
2071
+ if (progress.started || progress.downloading || progress.status === 'downloading' || progress.status === 'connecting') {
2071
2072
  this._modelDownloadInProgress = true;
2072
2073
  this._updateConnectionIndicator(this.wsManager?.latency?.quality || 'unknown');
2073
2074
 
@@ -2075,13 +2076,26 @@ class AgentGUIClient {
2075
2076
  window.__showVoiceDownloadProgress();
2076
2077
  }
2077
2078
 
2078
- if (window._voiceProgressDialog && progress.totalBytes > 0) {
2079
- var pct = Math.round((progress.totalDownloaded / progress.totalBytes) * 100);
2080
- var mb = Math.round(progress.totalBytes / 1024 / 1024);
2081
- var downloaded = Math.round((progress.totalDownloaded || 0) / 1024 / 1024);
2082
- window._voiceProgressDialog.update(pct, 'Downloading ' + downloaded + 'MB / ' + mb + 'MB');
2083
- } else if (window._voiceProgressDialog && progress.file) {
2084
- window._voiceProgressDialog.update(0, 'Loading ' + progress.file + '...');
2079
+ if (window._voiceProgressDialog) {
2080
+ let displayText = 'Downloading models...';
2081
+
2082
+ if (progress.status === 'connecting') {
2083
+ displayText = 'Connecting to ' + (progress.currentGateway || 'gateway') + '...';
2084
+ } else if (progress.totalBytes > 0) {
2085
+ const downloaded = (progress.bytesDownloaded || 0) / 1024 / 1024;
2086
+ const total = progress.totalBytes / 1024 / 1024;
2087
+ const speed = progress.downloadSpeed ? (progress.downloadSpeed / 1024 / 1024).toFixed(2) : '0';
2088
+ const eta = progress.eta ? Math.ceil(progress.eta) + 's' : '...';
2089
+ const retryInfo = progress.retryCount > 0 ? ` (retry ${progress.retryCount})` : '';
2090
+
2091
+ displayText = `Downloading ${downloaded.toFixed(1)}MB / ${total.toFixed(1)}MB @ ${speed}MB/s (ETA: ${eta})${retryInfo}`;
2092
+ } else if (progress.file) {
2093
+ displayText = 'Loading ' + progress.file + '...';
2094
+ } else if (progress.completedFiles && progress.totalFiles) {
2095
+ displayText = `Downloaded ${progress.completedFiles}/${progress.totalFiles} files`;
2096
+ }
2097
+
2098
+ window._voiceProgressDialog.update(progress.percentComplete || 0, displayText);
2085
2099
  }
2086
2100
  }
2087
2101
  }
@@ -422,6 +422,12 @@ class StreamingRenderer {
422
422
  return validTypes.includes(blockType) ? `block-type-${blockType}` : 'block-type-generic';
423
423
  }
424
424
 
425
+ _getToolColorClass(toolName) {
426
+ const n = (toolName || '').replace(/^mcp__[^_]+__/, '').toLowerCase();
427
+ const map = { read: 'read', write: 'write', edit: 'edit', bash: 'bash', glob: 'glob', grep: 'grep', webfetch: 'web', websearch: 'web', todowrite: 'todo', task: 'task', notebookedit: 'edit' };
428
+ return `tool-color-${map[n] || 'default'}`;
429
+ }
430
+
425
431
  containsHtmlTags(text) {
426
432
  const htmlPattern = /<(?:div|table|section|article|ul|ol|dl|nav|header|footer|main|aside|figure|details|summary|h[1-6]|p|blockquote|pre|code|span|strong|em|a|img|br|hr|li|td|tr|th|thead|tbody|tfoot)\b[^>]*>/i;
427
433
  return htmlPattern.test(text);
@@ -737,6 +743,7 @@ class StreamingRenderer {
737
743
  details.className = 'block-tool-use folded-tool';
738
744
  if (block.id) details.dataset.toolUseId = block.id;
739
745
  details.classList.add(this._getBlockTypeClass('tool_use'));
746
+ details.classList.add(this._getToolColorClass(toolName));
740
747
  const summary = document.createElement('summary');
741
748
  summary.className = 'folded-tool-bar';
742
749
  const displayName = this.getToolUseDisplayName(toolName);
@@ -1625,6 +1632,8 @@ class StreamingRenderer {
1625
1632
  const fileName = event.path ? event.path.split('/').pop() : 'unknown';
1626
1633
  const details = document.createElement('details');
1627
1634
  details.className = 'block-tool-use folded-tool';
1635
+ details.classList.add(this._getBlockTypeClass('tool_use'));
1636
+ details.classList.add(this._getToolColorClass('Read'));
1628
1637
  details.dataset.eventId = event.id || '';
1629
1638
  details.dataset.eventType = 'file_read';
1630
1639
  const summary = document.createElement('summary');
@@ -1656,6 +1665,8 @@ class StreamingRenderer {
1656
1665
  const fileName = event.path ? event.path.split('/').pop() : 'unknown';
1657
1666
  const details = document.createElement('details');
1658
1667
  details.className = 'block-tool-use folded-tool';
1668
+ details.classList.add(this._getBlockTypeClass('tool_use'));
1669
+ details.classList.add(this._getToolColorClass('Write'));
1659
1670
  details.dataset.eventId = event.id || '';
1660
1671
  details.dataset.eventType = 'file_write';
1661
1672
  const summary = document.createElement('summary');
@@ -1719,6 +1730,8 @@ class StreamingRenderer {
1719
1730
 
1720
1731
  const details = document.createElement('details');
1721
1732
  details.className = 'block-tool-use folded-tool';
1733
+ details.classList.add(this._getBlockTypeClass('tool_use'));
1734
+ details.classList.add(this._getToolColorClass('Bash'));
1722
1735
  details.dataset.eventId = event.id || '';
1723
1736
  details.dataset.eventType = 'command_execute';
1724
1737
  const summary = document.createElement('summary');
@@ -0,0 +1,223 @@
1
+ import https from 'https';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import os from 'os';
5
+
6
+ const testDir = path.join(os.tmpdir(), 'test-download-progress');
7
+ if (!fs.existsSync(testDir)) {
8
+ fs.mkdirSync(testDir, { recursive: true });
9
+ }
10
+
11
+ const GATEWAYS = [
12
+ 'https://ipfs.io',
13
+ 'https://gateway.pinata.cloud',
14
+ 'https://cloudflare-ipfs.com',
15
+ ];
16
+
17
+ function downloadWithProgress(url, destination, onProgress = null) {
18
+ let bytesDownloaded = 0;
19
+ let totalBytes = 0;
20
+ let lastProgressTime = Date.now();
21
+ let lastProgressBytes = 0;
22
+ const speeds = [];
23
+ let retryCount = 0;
24
+ let gatewayIndex = 0;
25
+
26
+ const emitProgress = () => {
27
+ const now = Date.now();
28
+ const deltaTime = (now - lastProgressTime) / 1000;
29
+ const deltaBytes = bytesDownloaded - lastProgressBytes;
30
+ const speed = deltaTime > 0 ? Math.round(deltaBytes / deltaTime) : 0;
31
+
32
+ if (speed > 0) {
33
+ speeds.push(speed);
34
+ if (speeds.length > 10) speeds.shift();
35
+ }
36
+
37
+ const avgSpeed = speeds.length > 0 ? Math.round(speeds.reduce((a, b) => a + b, 0) / speeds.length) : 0;
38
+ const eta = avgSpeed > 0 && totalBytes > bytesDownloaded ? Math.round((totalBytes - bytesDownloaded) / avgSpeed) : 0;
39
+
40
+ if (onProgress) {
41
+ onProgress({
42
+ bytesDownloaded,
43
+ bytesRemaining: Math.max(0, totalBytes - bytesDownloaded),
44
+ totalBytes,
45
+ downloadSpeed: avgSpeed,
46
+ eta,
47
+ retryCount,
48
+ currentGateway: url,
49
+ status: bytesDownloaded >= totalBytes ? 'completed' : 'downloading',
50
+ percentComplete: totalBytes > 0 ? Math.round((bytesDownloaded / totalBytes) * 100) : 0,
51
+ timestamp: now
52
+ });
53
+ }
54
+
55
+ lastProgressTime = now;
56
+ lastProgressBytes = bytesDownloaded;
57
+ };
58
+
59
+ return new Promise((resolve, reject) => {
60
+ const dir = path.dirname(destination);
61
+ if (!fs.existsSync(dir)) {
62
+ fs.mkdirSync(dir, { recursive: true });
63
+ }
64
+
65
+ const attemptDownload = (gateway) => {
66
+ if (onProgress) {
67
+ onProgress({
68
+ bytesDownloaded: 0,
69
+ bytesRemaining: 0,
70
+ totalBytes: 0,
71
+ downloadSpeed: 0,
72
+ eta: 0,
73
+ retryCount,
74
+ currentGateway: gateway,
75
+ status: 'connecting',
76
+ percentComplete: 0,
77
+ timestamp: Date.now()
78
+ });
79
+ }
80
+
81
+ https.get(gateway, { timeout: 30000 }, (res) => {
82
+ if ([301, 302, 307, 308].includes(res.statusCode)) {
83
+ const location = res.headers.location;
84
+ if (location) return attemptDownload(location);
85
+ }
86
+
87
+ if (res.statusCode !== 200) {
88
+ res.resume();
89
+ if (gatewayIndex < GATEWAYS.length - 1) {
90
+ retryCount++;
91
+ gatewayIndex++;
92
+ return setTimeout(() => attemptDownload(GATEWAYS[gatewayIndex]), 1000 * Math.pow(2, Math.min(retryCount, 3)));
93
+ }
94
+ return reject(new Error(`HTTP ${res.statusCode}`));
95
+ }
96
+
97
+ totalBytes = parseInt(res.headers['content-length'], 10) || 0;
98
+ bytesDownloaded = 0;
99
+ lastProgressBytes = 0;
100
+ lastProgressTime = Date.now();
101
+
102
+ const file = fs.createWriteStream(destination);
103
+ let lastEmit = Date.now();
104
+
105
+ res.on('data', (chunk) => {
106
+ bytesDownloaded += chunk.length;
107
+ const now = Date.now();
108
+ if (now - lastEmit >= 200) {
109
+ emitProgress();
110
+ lastEmit = now;
111
+ }
112
+ });
113
+
114
+ res.on('end', () => {
115
+ emitProgress();
116
+ file.destroy();
117
+ resolve({ destination, bytesDownloaded, success: true });
118
+ });
119
+
120
+ res.on('error', (err) => {
121
+ file.destroy();
122
+ fs.unlink(destination, () => {});
123
+ if (gatewayIndex < GATEWAYS.length - 1) {
124
+ retryCount++;
125
+ gatewayIndex++;
126
+ return setTimeout(() => attemptDownload(GATEWAYS[gatewayIndex]), 1000 * Math.pow(2, Math.min(retryCount, 3)));
127
+ }
128
+ reject(err);
129
+ });
130
+
131
+ file.on('error', (err) => {
132
+ res.destroy();
133
+ fs.unlink(destination, () => {});
134
+ if (gatewayIndex < GATEWAYS.length - 1) {
135
+ retryCount++;
136
+ gatewayIndex++;
137
+ return setTimeout(() => attemptDownload(GATEWAYS[gatewayIndex]), 1000 * Math.pow(2, Math.min(retryCount, 3)));
138
+ }
139
+ reject(err);
140
+ });
141
+
142
+ res.pipe(file);
143
+ }).on('timeout', () => {
144
+ if (gatewayIndex < GATEWAYS.length - 1) {
145
+ retryCount++;
146
+ gatewayIndex++;
147
+ return setTimeout(() => attemptDownload(GATEWAYS[gatewayIndex]), 1000 * Math.pow(2, Math.min(retryCount, 3)));
148
+ }
149
+ reject(new Error('Download timeout'));
150
+ }).on('error', (err) => {
151
+ if (gatewayIndex < GATEWAYS.length - 1) {
152
+ retryCount++;
153
+ gatewayIndex++;
154
+ return setTimeout(() => attemptDownload(GATEWAYS[gatewayIndex]), 1000 * Math.pow(2, Math.min(retryCount, 3)));
155
+ }
156
+ reject(err);
157
+ });
158
+ };
159
+
160
+ attemptDownload(GATEWAYS[0]);
161
+ });
162
+ }
163
+
164
+ let progressCount = 0;
165
+ let lastPrintTime = Date.now();
166
+ let minInterval = Infinity;
167
+ let maxInterval = 0;
168
+ const progressIntervals = [];
169
+
170
+ console.log('Starting download progress tracking test...\n');
171
+
172
+ downloadWithProgress(
173
+ 'https://www.w3.org/WAI/WCAG21/Techniques/pdf/pdf-files/table-example.pdf',
174
+ path.join(testDir, 'test-file.pdf'),
175
+ (progress) => {
176
+ progressCount++;
177
+ const now = Date.now();
178
+ const interval = now - lastPrintTime;
179
+
180
+ if (progressCount > 1) {
181
+ progressIntervals.push(interval);
182
+ minInterval = Math.min(minInterval, interval);
183
+ maxInterval = Math.max(maxInterval, interval);
184
+ }
185
+
186
+ console.log(`[${progressCount}] Progress Update:
187
+ Status: ${progress.status}
188
+ Downloaded: ${(progress.bytesDownloaded / 1024).toFixed(1)}KB / ${(progress.totalBytes / 1024).toFixed(1)}KB
189
+ Speed: ${(progress.downloadSpeed / 1024).toFixed(2)}MB/s
190
+ ETA: ${progress.eta}s
191
+ Complete: ${progress.percentComplete}%
192
+ Retry Count: ${progress.retryCount}
193
+ Gateway: ${progress.currentGateway}
194
+ Interval: ${interval}ms\n`);
195
+
196
+ lastPrintTime = now;
197
+ }
198
+ ).then((result) => {
199
+ const avgInterval = progressIntervals.length > 0 ? progressIntervals.reduce((a, b) => a + b, 0) / progressIntervals.length : 0;
200
+ console.log('\n=== Download Complete ===');
201
+ console.log(`Result: ${JSON.stringify(result, null, 2)}`);
202
+ console.log(`\nProgress Tracking Statistics:
203
+ Total Updates: ${progressCount}
204
+ Interval Range: ${minInterval}ms - ${maxInterval}ms
205
+ Average Interval: ${avgInterval.toFixed(0)}ms
206
+ Expected Interval: 200ms (should be 100-500ms range)`);
207
+
208
+ if (avgInterval >= 100 && avgInterval <= 500) {
209
+ console.log(' Status: PASS - Progress interval within acceptable range');
210
+ } else {
211
+ console.log(' Status: FAIL - Progress interval outside acceptable range');
212
+ }
213
+
214
+ if (fs.existsSync(path.join(testDir, 'test-file.pdf'))) {
215
+ const stat = fs.statSync(path.join(testDir, 'test-file.pdf'));
216
+ console.log(`\nDownloaded file size: ${(stat.size / 1024).toFixed(1)}KB`);
217
+ }
218
+
219
+ process.exit(0);
220
+ }).catch((err) => {
221
+ console.error('Download failed:', err.message);
222
+ process.exit(1);
223
+ });
@@ -0,0 +1,147 @@
1
+ import { WebSocketServer } from 'ws';
2
+ import http from 'http';
3
+ import path from 'path';
4
+ import os from 'os';
5
+
6
+ const PORT = 8899;
7
+ const server = http.createServer();
8
+ const wss = new WebSocketServer({ server, clientTracking: true });
9
+
10
+ let broadcastedMessages = [];
11
+ let clientConnected = false;
12
+
13
+ wss.on('connection', (ws) => {
14
+ clientConnected = true;
15
+ console.log('[WS] Client connected');
16
+
17
+ ws.on('message', (data) => {
18
+ const msg = JSON.parse(data);
19
+ console.log('[WS] Client sent:', msg.type);
20
+ });
21
+
22
+ ws.on('close', () => {
23
+ clientConnected = false;
24
+ console.log('[WS] Client disconnected');
25
+ });
26
+ });
27
+
28
+ function broadcastSync(event) {
29
+ if (wss.clients.size === 0) return;
30
+ const data = JSON.stringify(event);
31
+ broadcastedMessages.push(event);
32
+ for (const client of wss.clients) {
33
+ if (client.readyState === 1) {
34
+ client.send(data);
35
+ }
36
+ }
37
+ }
38
+
39
+ function broadcastModelProgress(progress) {
40
+ const broadcastData = {
41
+ type: 'model_download_progress',
42
+ modelId: progress.type || 'unknown',
43
+ bytesDownloaded: progress.bytesDownloaded || 0,
44
+ bytesRemaining: progress.bytesRemaining || 0,
45
+ totalBytes: progress.totalBytes || 0,
46
+ downloadSpeed: progress.downloadSpeed || 0,
47
+ eta: progress.eta || 0,
48
+ retryCount: progress.retryCount || 0,
49
+ currentGateway: progress.currentGateway || '',
50
+ status: progress.status || (progress.done ? 'completed' : progress.downloading ? 'downloading' : 'paused'),
51
+ percentComplete: progress.percentComplete || 0,
52
+ completedFiles: progress.completedFiles || 0,
53
+ totalFiles: progress.totalFiles || 0,
54
+ timestamp: Date.now(),
55
+ ...progress
56
+ };
57
+ broadcastSync(broadcastData);
58
+ }
59
+
60
+ server.listen(PORT, () => {
61
+ console.log(`[Server] Listening on ws://localhost:${PORT}`);
62
+
63
+ setTimeout(() => {
64
+ console.log('\n[Test] Simulating model download progress...\n');
65
+
66
+ broadcastModelProgress({
67
+ type: 'stt',
68
+ started: true,
69
+ downloading: true,
70
+ completedFiles: 0,
71
+ totalFiles: 10
72
+ });
73
+
74
+ setTimeout(() => {
75
+ broadcastModelProgress({
76
+ type: 'stt',
77
+ bytesDownloaded: 5242880,
78
+ bytesRemaining: 20971520,
79
+ totalBytes: 26214400,
80
+ downloadSpeed: 1048576,
81
+ eta: 20,
82
+ retryCount: 0,
83
+ currentGateway: 'https://huggingface.co/',
84
+ status: 'downloading',
85
+ percentComplete: 20,
86
+ completedFiles: 2,
87
+ totalFiles: 10
88
+ });
89
+
90
+ setTimeout(() => {
91
+ broadcastModelProgress({
92
+ type: 'stt',
93
+ bytesDownloaded: 15728640,
94
+ bytesRemaining: 10485760,
95
+ totalBytes: 26214400,
96
+ downloadSpeed: 2097152,
97
+ eta: 5,
98
+ retryCount: 0,
99
+ currentGateway: 'https://huggingface.co/',
100
+ status: 'downloading',
101
+ percentComplete: 60,
102
+ completedFiles: 6,
103
+ totalFiles: 10
104
+ });
105
+
106
+ setTimeout(() => {
107
+ broadcastModelProgress({
108
+ type: 'stt',
109
+ started: true,
110
+ done: true,
111
+ downloading: false,
112
+ completedFiles: 10,
113
+ totalFiles: 10,
114
+ status: 'completed'
115
+ });
116
+
117
+ setTimeout(() => {
118
+ console.log('\n[Test] Broadcasting complete. Results:\n');
119
+ console.log(`Broadcasted messages: ${broadcastedMessages.length}`);
120
+ console.log(`Client connected: ${clientConnected}`);
121
+
122
+ console.log('\nMessage types:');
123
+ broadcastedMessages.forEach((msg, idx) => {
124
+ console.log(` [${idx + 1}] Type: ${msg.type}`);
125
+ console.log(` Status: ${msg.status}`);
126
+ console.log(` Complete: ${msg.percentComplete || msg.completedFiles}%`);
127
+ console.log(` Speed: ${msg.downloadSpeed ? (msg.downloadSpeed / 1024 / 1024).toFixed(2) + 'MB/s' : 'N/A'}`);
128
+ console.log(` ETA: ${msg.eta || 0}s`);
129
+ });
130
+
131
+ const requiredFields = ['modelId', 'bytesDownloaded', 'bytesRemaining', 'downloadSpeed', 'eta', 'retryCount', 'currentGateway', 'status'];
132
+ const allFieldsPresent = broadcastedMessages.every(msg =>
133
+ requiredFields.every(field => field in msg)
134
+ );
135
+
136
+ console.log(`\nAll required fields present: ${allFieldsPresent ? 'PASS' : 'FAIL'}`);
137
+ console.log(`Message count >= 3: ${broadcastedMessages.length >= 3 ? 'PASS' : 'FAIL'}`);
138
+
139
+ server.close(() => {
140
+ process.exit(allFieldsPresent && broadcastedMessages.length >= 3 ? 0 : 1);
141
+ });
142
+ }, 500);
143
+ }, 500);
144
+ }, 500);
145
+ }, 500);
146
+ }, 500);
147
+ });