agentgui 1.0.525 → 1.0.527
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/lib/speech.js +48 -1
- package/package.json +1 -1
- package/server.js +20 -2
- package/static/css/tools-popup.css +133 -33
- package/static/index.html +13 -2
- package/static/js/tools-manager.js +133 -12
- package/static/js/voice.js +43 -10
- package/static/tools-ui-test.html +551 -0
package/lib/speech.js
CHANGED
|
@@ -1,4 +1,51 @@
|
|
|
1
1
|
import { createRequire } from 'module';
|
|
2
2
|
const require = createRequire(import.meta.url);
|
|
3
3
|
const speech = require('webtalk/speech');
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
const ttsMemCache = new Map();
|
|
6
|
+
const TTS_CACHE_MAX_BYTES = 10 * 1024 * 1024;
|
|
7
|
+
let ttsCacheBytes = 0;
|
|
8
|
+
|
|
9
|
+
function ttsCacheKey(text, voiceId) {
|
|
10
|
+
return (voiceId || 'default') + ':' + text;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function ttsCacheGet(key) {
|
|
14
|
+
return ttsMemCache.get(key) || null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function ttsCacheSet(key, wav) {
|
|
18
|
+
if (ttsMemCache.has(key)) return;
|
|
19
|
+
const size = wav ? wav.length : 0;
|
|
20
|
+
while (ttsCacheBytes + size > TTS_CACHE_MAX_BYTES && ttsMemCache.size > 0) {
|
|
21
|
+
const oldest = ttsMemCache.keys().next().value;
|
|
22
|
+
const old = ttsMemCache.get(oldest);
|
|
23
|
+
ttsCacheBytes -= old ? old.length : 0;
|
|
24
|
+
ttsMemCache.delete(oldest);
|
|
25
|
+
}
|
|
26
|
+
ttsMemCache.set(key, wav);
|
|
27
|
+
ttsCacheBytes += size;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function synthesizeWithCache(text, voiceId) {
|
|
31
|
+
const key = ttsCacheKey(text, voiceId);
|
|
32
|
+
const cached = ttsCacheGet(key);
|
|
33
|
+
if (cached) return cached;
|
|
34
|
+
const wav = await speech.synthesize(text, voiceId);
|
|
35
|
+
ttsCacheSet(key, wav);
|
|
36
|
+
return wav;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const transcribe = speech.transcribe;
|
|
40
|
+
export const synthesize = synthesizeWithCache;
|
|
41
|
+
export const synthesizeStream = speech.synthesizeStream;
|
|
42
|
+
export const getSTT = speech.getSTT;
|
|
43
|
+
export const getStatus = speech.getStatus;
|
|
44
|
+
export const getVoices = speech.getVoices;
|
|
45
|
+
export const preloadTTS = speech.preloadTTS;
|
|
46
|
+
export { ttsCacheKey, ttsCacheGet, ttsCacheSet };
|
|
47
|
+
export const splitSentences = speech.splitSentences;
|
|
48
|
+
export const resetSTTError = speech.resetSTTError;
|
|
49
|
+
export const clearCorruptedSTTCache = speech.clearCorruptedSTTCache;
|
|
50
|
+
export const getSttOptions = speech.getSttOptions;
|
|
51
|
+
export const VOICE_DIRS = speech.VOICE_DIRS;
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -64,7 +64,7 @@ const voiceCacheManager = {
|
|
|
64
64
|
this.generating.set(cacheKey, true);
|
|
65
65
|
try {
|
|
66
66
|
const speech = await getSpeech();
|
|
67
|
-
const audioBlob = await speech.
|
|
67
|
+
const audioBlob = await speech.synthesize(text, 'default');
|
|
68
68
|
const saved = queries.saveVoiceCache(conversationId, text, audioBlob);
|
|
69
69
|
const totalSize = queries.getVoiceCacheSize(conversationId);
|
|
70
70
|
if (totalSize > this.maxCacheSize) {
|
|
@@ -240,14 +240,15 @@ function flushTTSaccumulator(key, conversationId, sessionId) {
|
|
|
240
240
|
}
|
|
241
241
|
}
|
|
242
242
|
if (voices.size === 0) return;
|
|
243
|
-
const cacheKey = speech.ttsCacheKey(text, vid);
|
|
244
243
|
for (const vid of voices) {
|
|
244
|
+
const cacheKey = speech.ttsCacheKey(text, vid);
|
|
245
245
|
const cached = speech.ttsCacheGet(cacheKey);
|
|
246
246
|
if (cached) {
|
|
247
247
|
pushTTSAudio(cacheKey, cached, conversationId, sessionId, vid);
|
|
248
248
|
continue;
|
|
249
249
|
}
|
|
250
250
|
speech.synthesize(text, vid).then(wav => {
|
|
251
|
+
if (speech.ttsCacheSet) speech.ttsCacheSet(cacheKey, wav);
|
|
251
252
|
pushTTSAudio(cacheKey, wav, conversationId, sessionId, vid);
|
|
252
253
|
}).catch(() => {});
|
|
253
254
|
}
|
|
@@ -2880,6 +2881,23 @@ const server = http.createServer(async (req, res) => {
|
|
|
2880
2881
|
return;
|
|
2881
2882
|
}
|
|
2882
2883
|
|
|
2884
|
+
if (pathOnly.startsWith('/api/tts-cache/') && req.method === 'GET') {
|
|
2885
|
+
const cacheKey = decodeURIComponent(pathOnly.slice('/api/tts-cache/'.length));
|
|
2886
|
+
try {
|
|
2887
|
+
const speech = await getSpeech();
|
|
2888
|
+
const cached = speech.ttsCacheGet(cacheKey);
|
|
2889
|
+
if (cached) {
|
|
2890
|
+
res.writeHead(200, { 'Content-Type': 'audio/wav', 'Content-Length': cached.length, 'Cache-Control': 'public, max-age=3600' });
|
|
2891
|
+
res.end(cached);
|
|
2892
|
+
} else {
|
|
2893
|
+
sendJSON(req, res, 404, { error: 'not cached' });
|
|
2894
|
+
}
|
|
2895
|
+
} catch (err) {
|
|
2896
|
+
sendJSON(req, res, 500, { error: err.message });
|
|
2897
|
+
}
|
|
2898
|
+
return;
|
|
2899
|
+
}
|
|
2900
|
+
|
|
2883
2901
|
if (pathOnly === '/api/speech-status' && req.method === 'GET') {
|
|
2884
2902
|
try {
|
|
2885
2903
|
const { getStatus } = await getSpeech();
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
background: var(--color-bg-secondary);
|
|
41
41
|
border-radius: 1rem;
|
|
42
42
|
width: 90%;
|
|
43
|
-
max-width:
|
|
43
|
+
max-width: 600px;
|
|
44
44
|
max-height: 80vh;
|
|
45
45
|
display: flex;
|
|
46
46
|
flex-direction: column;
|
|
@@ -49,21 +49,91 @@
|
|
|
49
49
|
font-family: system-ui, -apple-system, sans-serif;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
@media (max-width: 768px) {
|
|
53
|
+
.tools-popup-content {
|
|
54
|
+
max-width: 90vw;
|
|
55
|
+
width: 100%;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@media (max-width: 480px) {
|
|
60
|
+
.tools-popup-content {
|
|
61
|
+
max-width: 95vw;
|
|
62
|
+
width: 100%;
|
|
63
|
+
max-height: 85vh;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.tools-popup-header {
|
|
67
|
+
flex-direction: column;
|
|
68
|
+
align-items: flex-start;
|
|
69
|
+
gap: 0.5rem;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.tools-popup-header-controls {
|
|
73
|
+
width: 100%;
|
|
74
|
+
flex-wrap: wrap;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
52
78
|
.tools-popup-header {
|
|
53
79
|
display: flex;
|
|
54
80
|
justify-content: space-between;
|
|
55
81
|
align-items: center;
|
|
56
|
-
padding:
|
|
82
|
+
padding: 0.875rem 1.25rem;
|
|
57
83
|
flex-shrink: 0;
|
|
58
84
|
border-bottom: 1px solid var(--color-border);
|
|
85
|
+
gap: 0.75rem;
|
|
59
86
|
}
|
|
60
87
|
|
|
61
88
|
.tools-popup-header h2 {
|
|
62
89
|
margin: 0;
|
|
63
|
-
font-size:
|
|
90
|
+
font-size: 1rem;
|
|
64
91
|
font-weight: 700;
|
|
65
92
|
}
|
|
66
93
|
|
|
94
|
+
.tools-popup-header-controls {
|
|
95
|
+
display: flex;
|
|
96
|
+
align-items: center;
|
|
97
|
+
gap: 0.75rem;
|
|
98
|
+
flex-shrink: 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.tools-voice-toggle {
|
|
102
|
+
display: flex;
|
|
103
|
+
align-items: center;
|
|
104
|
+
gap: 0.5rem;
|
|
105
|
+
font-size: 0.8rem;
|
|
106
|
+
cursor: pointer;
|
|
107
|
+
user-select: none;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.tools-voice-toggle input {
|
|
111
|
+
width: 0.875rem;
|
|
112
|
+
height: 0.875rem;
|
|
113
|
+
cursor: pointer;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.tools-voice-selector {
|
|
117
|
+
padding: 0.35rem 0.5rem;
|
|
118
|
+
font-size: 0.75rem;
|
|
119
|
+
border-radius: 0.3rem;
|
|
120
|
+
border: 1px solid var(--color-border);
|
|
121
|
+
background: var(--color-bg-primary);
|
|
122
|
+
color: var(--color-text-primary);
|
|
123
|
+
cursor: pointer;
|
|
124
|
+
transition: border-color 0.2s;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.tools-voice-selector:hover {
|
|
128
|
+
border-color: var(--color-primary);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.tools-voice-selector:focus {
|
|
132
|
+
outline: none;
|
|
133
|
+
border-color: var(--color-primary);
|
|
134
|
+
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
|
|
135
|
+
}
|
|
136
|
+
|
|
67
137
|
.tools-popup-close {
|
|
68
138
|
background: none;
|
|
69
139
|
border: none;
|
|
@@ -83,9 +153,21 @@
|
|
|
83
153
|
flex: 1;
|
|
84
154
|
overflow-y: auto;
|
|
85
155
|
padding: 1rem;
|
|
86
|
-
display:
|
|
87
|
-
|
|
88
|
-
gap:
|
|
156
|
+
display: grid;
|
|
157
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
158
|
+
gap: 1rem;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
@media (max-width: 768px) {
|
|
162
|
+
.tools-popup-scroll {
|
|
163
|
+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
@media (max-width: 480px) {
|
|
168
|
+
.tools-popup-scroll {
|
|
169
|
+
grid-template-columns: 1fr;
|
|
170
|
+
}
|
|
89
171
|
}
|
|
90
172
|
|
|
91
173
|
.tools-popup-scroll::-webkit-scrollbar {
|
|
@@ -106,43 +188,50 @@
|
|
|
106
188
|
}
|
|
107
189
|
|
|
108
190
|
.tool-item {
|
|
109
|
-
padding:
|
|
191
|
+
padding: 0.75rem;
|
|
110
192
|
border: 1px solid var(--color-border);
|
|
111
193
|
border-radius: 0.5rem;
|
|
112
194
|
background: var(--color-bg-primary);
|
|
113
195
|
display: flex;
|
|
114
196
|
flex-direction: column;
|
|
115
|
-
gap: 0.
|
|
197
|
+
gap: 0.375rem;
|
|
116
198
|
transition: border-color 0.2s, background-color 0.2s;
|
|
199
|
+
min-height: 120px;
|
|
200
|
+
justify-content: space-between;
|
|
117
201
|
}
|
|
118
202
|
|
|
119
203
|
.tool-item:hover {
|
|
120
204
|
border-color: var(--color-primary);
|
|
121
205
|
background: var(--color-bg-secondary);
|
|
206
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
122
207
|
}
|
|
123
208
|
|
|
124
209
|
.tool-header {
|
|
125
210
|
display: flex;
|
|
126
|
-
align-items:
|
|
127
|
-
gap: 0.
|
|
211
|
+
align-items: flex-start;
|
|
212
|
+
gap: 0.5rem;
|
|
128
213
|
justify-content: space-between;
|
|
214
|
+
flex-wrap: wrap;
|
|
129
215
|
}
|
|
130
216
|
|
|
131
217
|
.tool-name {
|
|
132
218
|
font-weight: 600;
|
|
133
|
-
font-size: 0.
|
|
219
|
+
font-size: 0.9rem;
|
|
134
220
|
flex: 1;
|
|
221
|
+
word-break: break-word;
|
|
135
222
|
}
|
|
136
223
|
|
|
137
224
|
.tool-status-indicator {
|
|
138
225
|
display: inline-flex;
|
|
139
226
|
align-items: center;
|
|
140
|
-
gap: 0.
|
|
141
|
-
font-size: 0.
|
|
142
|
-
padding: 0.
|
|
227
|
+
gap: 0.25rem;
|
|
228
|
+
font-size: 0.7rem;
|
|
229
|
+
padding: 0.2rem 0.4rem;
|
|
143
230
|
border-radius: 0.25rem;
|
|
144
231
|
background: var(--color-bg-tertiary);
|
|
145
232
|
color: var(--color-text-secondary);
|
|
233
|
+
white-space: nowrap;
|
|
234
|
+
flex-shrink: 0;
|
|
146
235
|
}
|
|
147
236
|
|
|
148
237
|
.tool-status-indicator.installed {
|
|
@@ -179,24 +268,28 @@
|
|
|
179
268
|
}
|
|
180
269
|
|
|
181
270
|
.tool-details {
|
|
182
|
-
font-size: 0.
|
|
271
|
+
font-size: 0.7rem;
|
|
183
272
|
color: var(--color-text-secondary);
|
|
184
|
-
display:
|
|
273
|
+
display: none;
|
|
185
274
|
align-items: center;
|
|
186
275
|
gap: 0.5rem;
|
|
276
|
+
line-height: 1.3;
|
|
277
|
+
max-height: 2.6em;
|
|
278
|
+
overflow: hidden;
|
|
187
279
|
}
|
|
188
280
|
|
|
189
281
|
.tool-progress-container {
|
|
190
282
|
display: flex;
|
|
191
283
|
flex-direction: column;
|
|
192
|
-
gap: 0.
|
|
284
|
+
gap: 0.15rem;
|
|
285
|
+
margin-top: 0.25rem;
|
|
193
286
|
}
|
|
194
287
|
|
|
195
288
|
.tool-progress-bar {
|
|
196
289
|
width: 100%;
|
|
197
|
-
height: 0.
|
|
290
|
+
height: 0.25rem;
|
|
198
291
|
background: var(--color-bg-tertiary);
|
|
199
|
-
border-radius: 0.
|
|
292
|
+
border-radius: 0.125rem;
|
|
200
293
|
overflow: hidden;
|
|
201
294
|
}
|
|
202
295
|
|
|
@@ -205,30 +298,31 @@
|
|
|
205
298
|
background: linear-gradient(90deg, #3b82f6, #2563eb);
|
|
206
299
|
width: 0%;
|
|
207
300
|
transition: width 0.3s ease;
|
|
208
|
-
border-radius: 0.
|
|
301
|
+
border-radius: 0.125rem;
|
|
209
302
|
}
|
|
210
303
|
|
|
211
304
|
.tool-progress-text {
|
|
212
|
-
font-size: 0.
|
|
305
|
+
font-size: 0.65rem;
|
|
213
306
|
color: var(--color-text-secondary);
|
|
214
307
|
}
|
|
215
308
|
|
|
216
309
|
.tool-actions {
|
|
217
310
|
display: flex;
|
|
218
|
-
gap: 0.
|
|
311
|
+
gap: 0.4rem;
|
|
219
312
|
flex-wrap: wrap;
|
|
313
|
+
margin-top: 0.25rem;
|
|
220
314
|
}
|
|
221
315
|
|
|
222
316
|
.tool-btn {
|
|
223
|
-
padding: 0.
|
|
317
|
+
padding: 0.35rem 0.65rem;
|
|
224
318
|
border: none;
|
|
225
|
-
border-radius: 0.
|
|
319
|
+
border-radius: 0.3rem;
|
|
226
320
|
cursor: pointer;
|
|
227
|
-
font-size: 0.
|
|
321
|
+
font-size: 0.7rem;
|
|
228
322
|
font-weight: 600;
|
|
229
323
|
transition: all 0.2s;
|
|
230
324
|
flex: 1;
|
|
231
|
-
min-width:
|
|
325
|
+
min-width: 60px;
|
|
232
326
|
white-space: nowrap;
|
|
233
327
|
}
|
|
234
328
|
|
|
@@ -259,12 +353,15 @@
|
|
|
259
353
|
}
|
|
260
354
|
|
|
261
355
|
.tool-error-message {
|
|
262
|
-
font-size: 0.
|
|
356
|
+
font-size: 0.65rem;
|
|
263
357
|
color: #ef4444;
|
|
264
358
|
background: rgba(239, 68, 68, 0.1);
|
|
265
|
-
padding: 0.
|
|
359
|
+
padding: 0.375rem;
|
|
266
360
|
border-radius: 0.25rem;
|
|
267
361
|
border-left: 2px solid #ef4444;
|
|
362
|
+
overflow: hidden;
|
|
363
|
+
text-overflow: ellipsis;
|
|
364
|
+
max-height: 1.5em;
|
|
268
365
|
}
|
|
269
366
|
|
|
270
367
|
.tools-popup-footer {
|
|
@@ -318,18 +415,21 @@
|
|
|
318
415
|
.tool-versions {
|
|
319
416
|
display: flex;
|
|
320
417
|
flex-direction: column;
|
|
321
|
-
gap: 0.
|
|
322
|
-
font-size: 0.
|
|
418
|
+
gap: 0.15rem;
|
|
419
|
+
font-size: 0.65rem;
|
|
323
420
|
color: var(--color-text-secondary);
|
|
324
|
-
background:
|
|
325
|
-
padding: 0
|
|
421
|
+
background: transparent;
|
|
422
|
+
padding: 0;
|
|
326
423
|
border-radius: 0.25rem;
|
|
424
|
+
line-height: 1.3;
|
|
327
425
|
}
|
|
328
426
|
|
|
329
427
|
.tool-version-item {
|
|
330
428
|
display: flex;
|
|
331
429
|
align-items: center;
|
|
332
|
-
gap: 0.
|
|
430
|
+
gap: 0.25rem;
|
|
431
|
+
overflow: hidden;
|
|
432
|
+
text-overflow: ellipsis;
|
|
333
433
|
}
|
|
334
434
|
|
|
335
435
|
.tool-version-item strong {
|
package/static/index.html
CHANGED
|
@@ -3125,8 +3125,19 @@
|
|
|
3125
3125
|
<div class="tools-popup" id="toolsPopup">
|
|
3126
3126
|
<div class="tools-popup-content" onclick="event.stopPropagation()">
|
|
3127
3127
|
<div class="tools-popup-header">
|
|
3128
|
-
<
|
|
3129
|
-
|
|
3128
|
+
<div style="display: flex; align-items: center; gap: 0.75rem; flex: 1;">
|
|
3129
|
+
<h2 style="margin: 0; font-size: 1rem;">Tools & Extensions</h2>
|
|
3130
|
+
</div>
|
|
3131
|
+
<div class="tools-popup-header-controls" style="display: flex; align-items: center; gap: 0.75rem; flex-shrink: 0;">
|
|
3132
|
+
<label class="tools-voice-toggle" style="display: flex; align-items: center; gap: 0.5rem; font-size: 0.85rem; cursor: pointer; user-select: none;">
|
|
3133
|
+
<input type="checkbox" id="toolsAutoSpeakToggle" style="width: 1rem; height: 1rem; cursor: pointer;">
|
|
3134
|
+
<span>Auto-speak</span>
|
|
3135
|
+
</label>
|
|
3136
|
+
<select class="tools-voice-selector" id="toolsVoiceSelector" style="padding: 0.4rem 0.6rem; font-size: 0.8rem; border-radius: 0.3rem; border: 1px solid var(--color-border); background: var(--color-bg-primary); color: var(--color-text-primary); cursor: pointer;">
|
|
3137
|
+
<option value="default">Voice</option>
|
|
3138
|
+
</select>
|
|
3139
|
+
<button class="tools-popup-close" onclick="document.getElementById('toolsPopup').classList.remove('open')" style="background: none; border: none; color: var(--color-text-secondary); font-size: 1.5rem; cursor: pointer; padding: 0; line-height: 1; transition: color 0.2s;">×</button>
|
|
3140
|
+
</div>
|
|
3130
3141
|
</div>
|
|
3131
3142
|
<div class="tools-popup-scroll"></div>
|
|
3132
3143
|
<div class="tools-popup-footer">
|
|
@@ -13,9 +13,96 @@
|
|
|
13
13
|
if (!btn.contains(e.target) && !popup.contains(e.target)) closePopup();
|
|
14
14
|
});
|
|
15
15
|
window.addEventListener('ws-message', onWsMessage);
|
|
16
|
+
|
|
17
|
+
// Initialize voice controls
|
|
18
|
+
initVoiceControls();
|
|
16
19
|
refresh();
|
|
17
20
|
}
|
|
18
21
|
|
|
22
|
+
function initVoiceControls() {
|
|
23
|
+
var autoSpeakToggle = document.getElementById('toolsAutoSpeakToggle');
|
|
24
|
+
var voiceSelector = document.getElementById('toolsVoiceSelector');
|
|
25
|
+
|
|
26
|
+
if (!autoSpeakToggle || !voiceSelector) return;
|
|
27
|
+
|
|
28
|
+
var savedAutoSpeak = localStorage.getItem('toolsAutoSpeak') === 'true';
|
|
29
|
+
autoSpeakToggle.checked = savedAutoSpeak;
|
|
30
|
+
|
|
31
|
+
window.addEventListener('ws-message', function(e) {
|
|
32
|
+
var data = e.detail;
|
|
33
|
+
if (data && data.type === 'voice_list') updateVoiceSelector(data.voices);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
function trySubscribeVsManager() {
|
|
37
|
+
if (window.wsManager && window.wsManager.subscribeToVoiceList) {
|
|
38
|
+
window.wsManager.subscribeToVoiceList(updateVoiceSelector);
|
|
39
|
+
} else {
|
|
40
|
+
var BASE = window.__BASE_URL || '';
|
|
41
|
+
fetch(BASE + '/api/voices').then(function(r) { return r.json(); }).then(function(d) {
|
|
42
|
+
if (d.ok && Array.isArray(d.voices)) updateVoiceSelector(d.voices);
|
|
43
|
+
}).catch(function() {});
|
|
44
|
+
setTimeout(function() {
|
|
45
|
+
if (window.wsManager && window.wsManager.subscribeToVoiceList) {
|
|
46
|
+
window.wsManager.subscribeToVoiceList(updateVoiceSelector);
|
|
47
|
+
}
|
|
48
|
+
}, 2000);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
trySubscribeVsManager();
|
|
52
|
+
|
|
53
|
+
autoSpeakToggle.addEventListener('change', function() {
|
|
54
|
+
localStorage.setItem('toolsAutoSpeak', this.checked);
|
|
55
|
+
if (window.voiceModule) window.voiceModule.setAutoSpeak(this.checked);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
voiceSelector.addEventListener('change', function() {
|
|
59
|
+
localStorage.setItem('toolsVoice', this.value);
|
|
60
|
+
if (window.voiceModule) window.voiceModule.setVoice(this.value);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function updateVoiceSelector(voices) {
|
|
65
|
+
var voiceSelector = document.getElementById('toolsVoiceSelector');
|
|
66
|
+
if (!voiceSelector || !voices || !Array.isArray(voices)) return;
|
|
67
|
+
|
|
68
|
+
var currentValue = voiceSelector.value || localStorage.getItem('toolsVoice') || 'default';
|
|
69
|
+
voiceSelector.innerHTML = '';
|
|
70
|
+
|
|
71
|
+
var builtIn = voices.filter(function(v) { return !v.isCustom; });
|
|
72
|
+
var custom = voices.filter(function(v) { return v.isCustom; });
|
|
73
|
+
|
|
74
|
+
if (builtIn.length) {
|
|
75
|
+
var grp1 = document.createElement('optgroup');
|
|
76
|
+
grp1.label = 'Built-in Voices';
|
|
77
|
+
builtIn.forEach(function(voice) {
|
|
78
|
+
var opt = document.createElement('option');
|
|
79
|
+
opt.value = voice.id;
|
|
80
|
+
var parts = [];
|
|
81
|
+
if (voice.gender && voice.gender !== 'custom') parts.push(voice.gender);
|
|
82
|
+
if (voice.accent && voice.accent !== 'custom') parts.push(voice.accent);
|
|
83
|
+
opt.textContent = voice.name + (parts.length ? ' (' + parts.join(', ') + ')' : '');
|
|
84
|
+
grp1.appendChild(opt);
|
|
85
|
+
});
|
|
86
|
+
voiceSelector.appendChild(grp1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (custom.length) {
|
|
90
|
+
var grp2 = document.createElement('optgroup');
|
|
91
|
+
grp2.label = 'Custom Voices';
|
|
92
|
+
custom.forEach(function(voice) {
|
|
93
|
+
var opt = document.createElement('option');
|
|
94
|
+
opt.value = voice.id;
|
|
95
|
+
opt.textContent = voice.name;
|
|
96
|
+
grp2.appendChild(opt);
|
|
97
|
+
});
|
|
98
|
+
voiceSelector.appendChild(grp2);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (voiceSelector.querySelector('option[value="' + currentValue + '"]')) {
|
|
102
|
+
voiceSelector.value = currentValue;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
19
106
|
function refresh() {
|
|
20
107
|
fetchTools();
|
|
21
108
|
}
|
|
@@ -141,10 +228,23 @@
|
|
|
141
228
|
popup.classList.remove('open');
|
|
142
229
|
}
|
|
143
230
|
|
|
231
|
+
function isAutoSpeakOn() {
|
|
232
|
+
var toggle = document.getElementById('toolsAutoSpeakToggle');
|
|
233
|
+
return toggle ? toggle.checked : false;
|
|
234
|
+
}
|
|
235
|
+
|
|
144
236
|
function onWsMessage(e) {
|
|
145
237
|
var data = e.detail;
|
|
146
238
|
if (!data) return;
|
|
147
239
|
|
|
240
|
+
if (data.type === 'streaming_progress' && data.block && data.block.type === 'text' && data.block.text) {
|
|
241
|
+
if (isAutoSpeakOn() && (!data.blockRole || data.blockRole === 'assistant')) {
|
|
242
|
+
if (window.voiceModule && typeof window.voiceModule.speakText === 'function') {
|
|
243
|
+
window.voiceModule.speakText(data.block.text);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
148
248
|
if (data.type === 'tools_update_started') {
|
|
149
249
|
var updateTools = data.tools || [];
|
|
150
250
|
updateTools.forEach(function(toolId) {
|
|
@@ -200,42 +300,41 @@
|
|
|
200
300
|
if (!scroll) return;
|
|
201
301
|
|
|
202
302
|
if (tools.length === 0) {
|
|
203
|
-
scroll.innerHTML = '<div class="tool-empty-state"><div class="tool-empty-state-icon">⚙️</div><div class="tool-empty-state-text">No tools available</div></div>';
|
|
303
|
+
scroll.innerHTML = '<div class="tool-empty-state" style="grid-column: 1 / -1;"><div class="tool-empty-state-icon">⚙️</div><div class="tool-empty-state-text">No tools available</div></div>';
|
|
204
304
|
return;
|
|
205
305
|
}
|
|
206
306
|
|
|
207
307
|
scroll.innerHTML = tools.map(function(tool) {
|
|
208
308
|
var statusClass = getStatusClass(tool);
|
|
209
309
|
var isInstalling = tool.status === 'installing' || tool.status === 'updating';
|
|
210
|
-
var hasAction = !tool.installed || tool.hasUpdate || tool.status === 'failed';
|
|
211
310
|
var versionInfo = '';
|
|
212
311
|
if (tool.installedVersion || tool.publishedVersion) {
|
|
213
312
|
versionInfo = '<div class="tool-versions">';
|
|
214
313
|
if (tool.installedVersion) {
|
|
215
|
-
versionInfo += '<span class="tool-version-item">
|
|
314
|
+
versionInfo += '<span class="tool-version-item">v' + esc(tool.installedVersion) + '</span>';
|
|
216
315
|
}
|
|
217
|
-
if (tool.publishedVersion) {
|
|
218
|
-
versionInfo += '<span class="tool-version-item">
|
|
316
|
+
if (tool.publishedVersion && tool.installedVersion !== tool.publishedVersion) {
|
|
317
|
+
versionInfo += '<span class="tool-version-item">(v' + esc(tool.publishedVersion) + ' available)</span>';
|
|
219
318
|
}
|
|
220
319
|
versionInfo += '</div>';
|
|
221
320
|
}
|
|
222
321
|
|
|
223
322
|
return '<div class="tool-item">' +
|
|
323
|
+
'<div style="display: flex; flex-direction: column; gap: 0.3rem;">' +
|
|
224
324
|
'<div class="tool-header">' +
|
|
225
325
|
'<span class="tool-name">' + esc(tool.id) + '</span>' +
|
|
226
|
-
'
|
|
326
|
+
'</div>' +
|
|
327
|
+
'<div class="tool-status-indicator ' + statusClass + '">' +
|
|
227
328
|
'<span class="tool-status-dot"></span>' +
|
|
228
329
|
'<span>' + getStatusText(tool) + '</span>' +
|
|
229
|
-
'</span>' +
|
|
230
330
|
'</div>' +
|
|
231
331
|
versionInfo +
|
|
232
|
-
(tool.description ? '<div class="tool-details">' + esc(tool.description) + '</div>' : '') +
|
|
233
332
|
(isInstalling && tool.progress !== undefined ?
|
|
234
333
|
'<div class="tool-progress-container">' +
|
|
235
334
|
'<div class="tool-progress-bar"><div class="tool-progress-fill" style="width:' + Math.min(tool.progress, 100) + '%"></div></div>' +
|
|
236
|
-
'<div class="tool-progress-text">' + Math.floor(tool.progress) + '%</div>' +
|
|
237
335
|
'</div>' : '') +
|
|
238
|
-
(tool.error_message ? '<div class="tool-error-message">Error: ' + esc(tool.error_message.substring(0,
|
|
336
|
+
(tool.error_message ? '<div class="tool-error-message">Error: ' + esc(tool.error_message.substring(0, 40)) + '</div>' : '') +
|
|
337
|
+
'</div>' +
|
|
239
338
|
'<div class="tool-actions">' +
|
|
240
339
|
(tool.status === 'not_installed' ?
|
|
241
340
|
'<button class="tool-btn tool-btn-primary" onclick="window.toolsManager.install(\'' + tool.id + '\')" ' + (operationInProgress.has(tool.id) ? 'disabled' : '') + '>Install</button>' :
|
|
@@ -243,7 +342,7 @@
|
|
|
243
342
|
'<button class="tool-btn tool-btn-primary" onclick="window.toolsManager.update(\'' + tool.id + '\')" ' + (operationInProgress.has(tool.id) ? 'disabled' : '') + '>Update</button>' :
|
|
244
343
|
tool.status === 'failed' ?
|
|
245
344
|
'<button class="tool-btn tool-btn-primary" onclick="window.toolsManager.install(\'' + tool.id + '\')" ' + (operationInProgress.has(tool.id) ? 'disabled' : '') + '>Retry</button>' :
|
|
246
|
-
'<button class="tool-btn tool-btn-secondary" onclick="window.toolsManager.refresh()" ' + (isRefreshing ? 'disabled' : '') + '
|
|
345
|
+
'<button class="tool-btn tool-btn-secondary" onclick="window.toolsManager.refresh()" ' + (isRefreshing ? 'disabled' : '') + '>✓</button>'
|
|
247
346
|
) +
|
|
248
347
|
'</div>' +
|
|
249
348
|
'</div>';
|
|
@@ -306,6 +405,28 @@
|
|
|
306
405
|
},
|
|
307
406
|
install: function(toolId) { install(toolId); },
|
|
308
407
|
update: function(toolId) { update(toolId); },
|
|
309
|
-
updateAll: function() { updateAll(); }
|
|
408
|
+
updateAll: function() { updateAll(); },
|
|
409
|
+
getAutoSpeak: function() {
|
|
410
|
+
var toggle = document.getElementById('toolsAutoSpeakToggle');
|
|
411
|
+
return toggle ? toggle.checked : false;
|
|
412
|
+
},
|
|
413
|
+
getVoice: function() {
|
|
414
|
+
var selector = document.getElementById('toolsVoiceSelector');
|
|
415
|
+
return selector ? selector.value : 'default';
|
|
416
|
+
},
|
|
417
|
+
setAutoSpeak: function(value) {
|
|
418
|
+
var toggle = document.getElementById('toolsAutoSpeakToggle');
|
|
419
|
+
if (toggle) {
|
|
420
|
+
toggle.checked = value;
|
|
421
|
+
localStorage.setItem('toolsAutoSpeak', value);
|
|
422
|
+
}
|
|
423
|
+
},
|
|
424
|
+
setVoice: function(value) {
|
|
425
|
+
var selector = document.getElementById('toolsVoiceSelector');
|
|
426
|
+
if (selector && Array.from(selector.options).some(opt => opt.value === value)) {
|
|
427
|
+
selector.value = value;
|
|
428
|
+
localStorage.setItem('toolsVoice', value);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
310
431
|
};
|
|
311
432
|
})();
|
package/static/js/voice.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
var _lastVoiceBlockText = null;
|
|
19
19
|
var _lastVoiceBlockTime = 0;
|
|
20
20
|
var _voiceBreakNext = false;
|
|
21
|
-
var selectedVoiceId = localStorage.getItem('voice-
|
|
21
|
+
var selectedVoiceId = localStorage.getItem('gmgui-voice-selection') || 'default';
|
|
22
22
|
var ttsAudioCache = new Map();
|
|
23
23
|
var TTS_CLIENT_CACHE_MAX = 50;
|
|
24
24
|
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
function setupVoiceSelector() {
|
|
34
34
|
var selector = document.getElementById('voiceSelector');
|
|
35
35
|
if (!selector) return;
|
|
36
|
-
var saved = localStorage.getItem('voice-
|
|
36
|
+
var saved = localStorage.getItem('gmgui-voice-selection');
|
|
37
37
|
if (saved) selectedVoiceId = saved;
|
|
38
38
|
if (window.wsManager) {
|
|
39
39
|
window.wsManager.subscribeToVoiceList(function(voices) {
|
|
@@ -66,8 +66,8 @@
|
|
|
66
66
|
});
|
|
67
67
|
selector.appendChild(grp2);
|
|
68
68
|
}
|
|
69
|
-
if (
|
|
70
|
-
selector.value =
|
|
69
|
+
if (selectedVoiceId && selector.querySelector('option[value="' + selectedVoiceId + '"]')) {
|
|
70
|
+
selector.value = selectedVoiceId;
|
|
71
71
|
}
|
|
72
72
|
});
|
|
73
73
|
return;
|
|
@@ -104,15 +104,15 @@
|
|
|
104
104
|
});
|
|
105
105
|
selector.appendChild(grp2);
|
|
106
106
|
}
|
|
107
|
-
if (
|
|
108
|
-
selector.value =
|
|
107
|
+
if (selectedVoiceId && selector.querySelector('option[value="' + selectedVoiceId + '"]')) {
|
|
108
|
+
selector.value = selectedVoiceId;
|
|
109
109
|
}
|
|
110
110
|
})
|
|
111
111
|
.catch(function(err) { console.error('[Voice] Failed to load voices:', err); });
|
|
112
112
|
}
|
|
113
113
|
selector.addEventListener('change', function() {
|
|
114
114
|
selectedVoiceId = selector.value;
|
|
115
|
-
localStorage.setItem('voice-
|
|
115
|
+
localStorage.setItem('gmgui-voice-selection', selectedVoiceId);
|
|
116
116
|
sendVoiceToServer();
|
|
117
117
|
});
|
|
118
118
|
}
|
|
@@ -211,14 +211,14 @@
|
|
|
211
211
|
function setupTTSToggle() {
|
|
212
212
|
var toggle = document.getElementById('voiceTTSToggle');
|
|
213
213
|
if (toggle) {
|
|
214
|
-
var saved = localStorage.getItem('
|
|
214
|
+
var saved = localStorage.getItem('gmgui-auto-speak');
|
|
215
215
|
if (saved !== null) {
|
|
216
216
|
ttsEnabled = saved === 'true';
|
|
217
217
|
toggle.checked = ttsEnabled;
|
|
218
218
|
}
|
|
219
219
|
toggle.addEventListener('change', function() {
|
|
220
220
|
ttsEnabled = toggle.checked;
|
|
221
|
-
localStorage.setItem('
|
|
221
|
+
localStorage.setItem('gmgui-auto-speak', ttsEnabled);
|
|
222
222
|
if (!ttsEnabled) stopSpeaking();
|
|
223
223
|
});
|
|
224
224
|
}
|
|
@@ -352,6 +352,10 @@
|
|
|
352
352
|
|
|
353
353
|
function speak(text) {
|
|
354
354
|
if (!ttsEnabled) return;
|
|
355
|
+
speakDirect(text);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function speakDirect(text) {
|
|
355
359
|
var clean = text.replace(/<[^>]*>/g, '').trim();
|
|
356
360
|
if (!clean) return;
|
|
357
361
|
var parts = [];
|
|
@@ -946,10 +950,39 @@
|
|
|
946
950
|
return text.replace(/[&<>"']/g, function(c) { return map[c]; });
|
|
947
951
|
}
|
|
948
952
|
|
|
953
|
+
function getAutoSpeak() {
|
|
954
|
+
return ttsEnabled;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function setAutoSpeak(value) {
|
|
958
|
+
ttsEnabled = Boolean(value);
|
|
959
|
+
localStorage.setItem('gmgui-auto-speak', ttsEnabled);
|
|
960
|
+
var toggle = document.getElementById('voiceTTSToggle');
|
|
961
|
+
if (toggle) toggle.checked = ttsEnabled;
|
|
962
|
+
if (!ttsEnabled) stopSpeaking();
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function getVoice() {
|
|
966
|
+
return selectedVoiceId;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function setVoice(voiceId) {
|
|
970
|
+
selectedVoiceId = String(voiceId);
|
|
971
|
+
localStorage.setItem('gmgui-voice-selection', selectedVoiceId);
|
|
972
|
+
var selector = document.getElementById('voiceSelector');
|
|
973
|
+
if (selector) selector.value = selectedVoiceId;
|
|
974
|
+
sendVoiceToServer();
|
|
975
|
+
}
|
|
976
|
+
|
|
949
977
|
window.voiceModule = {
|
|
950
978
|
activate: activate,
|
|
951
979
|
deactivate: deactivate,
|
|
952
|
-
handleBlock: handleVoiceBlock
|
|
980
|
+
handleBlock: handleVoiceBlock,
|
|
981
|
+
getAutoSpeak: getAutoSpeak,
|
|
982
|
+
setAutoSpeak: setAutoSpeak,
|
|
983
|
+
getVoice: getVoice,
|
|
984
|
+
setVoice: setVoice,
|
|
985
|
+
speakText: speakDirect
|
|
953
986
|
};
|
|
954
987
|
|
|
955
988
|
if (document.readyState === 'loading') {
|
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
|
|
2
|
+
<!DOCTYPE html>
|
|
3
|
+
<html lang="en">
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<title>Tools UI Redesign Test - Compact Grid Layout</title>
|
|
8
|
+
<style>
|
|
9
|
+
* { box-sizing: border-box; }
|
|
10
|
+
|
|
11
|
+
:root {
|
|
12
|
+
--color-primary: #3b82f6;
|
|
13
|
+
--color-primary-dark: #1e40af;
|
|
14
|
+
--color-bg-primary: #ffffff;
|
|
15
|
+
--color-bg-secondary: #f9fafb;
|
|
16
|
+
--color-bg-tertiary: #f3f4f6;
|
|
17
|
+
--color-text-primary: #111827;
|
|
18
|
+
--color-text-secondary: #6b7280;
|
|
19
|
+
--color-border: #e5e7eb;
|
|
20
|
+
--color-success: #10b981;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
html.dark {
|
|
24
|
+
--color-bg-primary: #111827;
|
|
25
|
+
--color-bg-secondary: #1f2937;
|
|
26
|
+
--color-bg-tertiary: #374151;
|
|
27
|
+
--color-text-primary: #f9fafb;
|
|
28
|
+
--color-text-secondary: #d1d5db;
|
|
29
|
+
--color-border: #374151;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
body {
|
|
33
|
+
margin: 0;
|
|
34
|
+
padding: 20px;
|
|
35
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
36
|
+
background: var(--color-bg-secondary);
|
|
37
|
+
color: var(--color-text-primary);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.test-container {
|
|
41
|
+
max-width: 1000px;
|
|
42
|
+
margin: 0 auto;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
h1 {
|
|
46
|
+
font-size: 2rem;
|
|
47
|
+
margin-bottom: 10px;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.description {
|
|
51
|
+
color: var(--color-text-secondary);
|
|
52
|
+
margin-bottom: 30px;
|
|
53
|
+
font-size: 0.95rem;
|
|
54
|
+
line-height: 1.5;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.demo-section {
|
|
58
|
+
margin-bottom: 40px;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.section-title {
|
|
62
|
+
font-size: 1.25rem;
|
|
63
|
+
font-weight: 600;
|
|
64
|
+
margin-bottom: 15px;
|
|
65
|
+
border-bottom: 2px solid var(--color-primary);
|
|
66
|
+
padding-bottom: 8px;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* Tools popup styles */
|
|
70
|
+
.tools-popup-content {
|
|
71
|
+
background: var(--color-bg-secondary);
|
|
72
|
+
border-radius: 1rem;
|
|
73
|
+
width: 100%;
|
|
74
|
+
max-width: 600px;
|
|
75
|
+
display: flex;
|
|
76
|
+
flex-direction: column;
|
|
77
|
+
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.1);
|
|
78
|
+
color: var(--color-text-primary);
|
|
79
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
80
|
+
overflow: hidden;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.tools-popup-header {
|
|
84
|
+
display: flex;
|
|
85
|
+
justify-content: space-between;
|
|
86
|
+
align-items: center;
|
|
87
|
+
padding: 0.875rem 1.25rem;
|
|
88
|
+
flex-shrink: 0;
|
|
89
|
+
border-bottom: 1px solid var(--color-border);
|
|
90
|
+
gap: 0.75rem;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.tools-popup-header h2 {
|
|
94
|
+
margin: 0;
|
|
95
|
+
font-size: 1rem;
|
|
96
|
+
font-weight: 700;
|
|
97
|
+
flex: 1;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.tools-popup-header-controls {
|
|
101
|
+
display: flex;
|
|
102
|
+
align-items: center;
|
|
103
|
+
gap: 0.75rem;
|
|
104
|
+
flex-shrink: 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.tools-voice-toggle {
|
|
108
|
+
display: flex;
|
|
109
|
+
align-items: center;
|
|
110
|
+
gap: 0.5rem;
|
|
111
|
+
font-size: 0.8rem;
|
|
112
|
+
cursor: pointer;
|
|
113
|
+
user-select: none;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.tools-voice-toggle input {
|
|
117
|
+
width: 0.875rem;
|
|
118
|
+
height: 0.875rem;
|
|
119
|
+
cursor: pointer;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.tools-voice-selector {
|
|
123
|
+
padding: 0.35rem 0.5rem;
|
|
124
|
+
font-size: 0.75rem;
|
|
125
|
+
border-radius: 0.3rem;
|
|
126
|
+
border: 1px solid var(--color-border);
|
|
127
|
+
background: var(--color-bg-primary);
|
|
128
|
+
color: var(--color-text-primary);
|
|
129
|
+
cursor: pointer;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.tools-popup-scroll {
|
|
133
|
+
flex: 1;
|
|
134
|
+
overflow-y: auto;
|
|
135
|
+
padding: 1rem;
|
|
136
|
+
display: grid;
|
|
137
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
138
|
+
gap: 1rem;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
@media (max-width: 768px) {
|
|
142
|
+
.tools-popup-scroll {
|
|
143
|
+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
@media (max-width: 480px) {
|
|
148
|
+
.tools-popup-scroll {
|
|
149
|
+
grid-template-columns: 1fr;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.tools-popup-header {
|
|
153
|
+
flex-direction: column;
|
|
154
|
+
align-items: flex-start;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.tools-popup-header-controls {
|
|
158
|
+
width: 100%;
|
|
159
|
+
flex-wrap: wrap;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.tool-item {
|
|
164
|
+
padding: 0.75rem;
|
|
165
|
+
border: 1px solid var(--color-border);
|
|
166
|
+
border-radius: 0.5rem;
|
|
167
|
+
background: var(--color-bg-primary);
|
|
168
|
+
display: flex;
|
|
169
|
+
flex-direction: column;
|
|
170
|
+
gap: 0.375rem;
|
|
171
|
+
transition: border-color 0.2s, background-color 0.2s;
|
|
172
|
+
min-height: 120px;
|
|
173
|
+
justify-content: space-between;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.tool-item:hover {
|
|
177
|
+
border-color: var(--color-primary);
|
|
178
|
+
background: var(--color-bg-secondary);
|
|
179
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.tool-header {
|
|
183
|
+
display: flex;
|
|
184
|
+
align-items: flex-start;
|
|
185
|
+
gap: 0.5rem;
|
|
186
|
+
justify-content: space-between;
|
|
187
|
+
flex-wrap: wrap;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.tool-name {
|
|
191
|
+
font-weight: 600;
|
|
192
|
+
font-size: 0.9rem;
|
|
193
|
+
flex: 1;
|
|
194
|
+
word-break: break-word;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.tool-status-indicator {
|
|
198
|
+
display: inline-flex;
|
|
199
|
+
align-items: center;
|
|
200
|
+
gap: 0.25rem;
|
|
201
|
+
font-size: 0.7rem;
|
|
202
|
+
padding: 0.2rem 0.4rem;
|
|
203
|
+
border-radius: 0.25rem;
|
|
204
|
+
background: var(--color-bg-tertiary);
|
|
205
|
+
color: var(--color-text-secondary);
|
|
206
|
+
white-space: nowrap;
|
|
207
|
+
flex-shrink: 0;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.tool-status-indicator.installed {
|
|
211
|
+
background: rgba(16, 185, 129, 0.15);
|
|
212
|
+
color: #10b981;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.tool-status-indicator.needs_update {
|
|
216
|
+
background: rgba(245, 158, 11, 0.15);
|
|
217
|
+
color: #f59e0b;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.tool-status-indicator.not_installed {
|
|
221
|
+
background: rgba(107, 114, 128, 0.15);
|
|
222
|
+
color: #9ca3af;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.tool-status-dot {
|
|
226
|
+
width: 0.375rem;
|
|
227
|
+
height: 0.375rem;
|
|
228
|
+
border-radius: 50%;
|
|
229
|
+
background: currentColor;
|
|
230
|
+
display: inline-block;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.tool-versions {
|
|
234
|
+
display: flex;
|
|
235
|
+
flex-direction: column;
|
|
236
|
+
gap: 0.15rem;
|
|
237
|
+
font-size: 0.65rem;
|
|
238
|
+
color: var(--color-text-secondary);
|
|
239
|
+
line-height: 1.3;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.tool-version-item {
|
|
243
|
+
display: flex;
|
|
244
|
+
align-items: center;
|
|
245
|
+
gap: 0.25rem;
|
|
246
|
+
overflow: hidden;
|
|
247
|
+
text-overflow: ellipsis;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.tool-actions {
|
|
251
|
+
display: flex;
|
|
252
|
+
gap: 0.4rem;
|
|
253
|
+
flex-wrap: wrap;
|
|
254
|
+
margin-top: 0.25rem;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.tool-btn {
|
|
258
|
+
padding: 0.35rem 0.65rem;
|
|
259
|
+
border: none;
|
|
260
|
+
border-radius: 0.3rem;
|
|
261
|
+
cursor: pointer;
|
|
262
|
+
font-size: 0.7rem;
|
|
263
|
+
font-weight: 600;
|
|
264
|
+
transition: all 0.2s;
|
|
265
|
+
flex: 1;
|
|
266
|
+
min-width: 60px;
|
|
267
|
+
white-space: nowrap;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.tool-btn-primary {
|
|
271
|
+
background: var(--color-primary);
|
|
272
|
+
color: white;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.tool-btn-primary:hover {
|
|
276
|
+
background: var(--color-primary-dark);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.tool-progress-bar {
|
|
280
|
+
width: 100%;
|
|
281
|
+
height: 0.25rem;
|
|
282
|
+
background: var(--color-bg-tertiary);
|
|
283
|
+
border-radius: 0.125rem;
|
|
284
|
+
overflow: hidden;
|
|
285
|
+
margin-top: 0.25rem;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.tool-progress-fill {
|
|
289
|
+
height: 100%;
|
|
290
|
+
background: linear-gradient(90deg, #3b82f6, #2563eb);
|
|
291
|
+
width: 60%;
|
|
292
|
+
border-radius: 0.125rem;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.feature-list {
|
|
296
|
+
display: grid;
|
|
297
|
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
298
|
+
gap: 15px;
|
|
299
|
+
margin: 20px 0;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.feature-item {
|
|
303
|
+
padding: 12px;
|
|
304
|
+
background: var(--color-bg-primary);
|
|
305
|
+
border-radius: 0.5rem;
|
|
306
|
+
border-left: 3px solid var(--color-primary);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.feature-item strong {
|
|
310
|
+
color: var(--color-primary);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.size-demo {
|
|
314
|
+
display: flex;
|
|
315
|
+
gap: 10px;
|
|
316
|
+
margin: 20px 0;
|
|
317
|
+
flex-wrap: wrap;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.size-demo-item {
|
|
321
|
+
text-align: center;
|
|
322
|
+
padding: 10px;
|
|
323
|
+
background: var(--color-bg-primary);
|
|
324
|
+
border-radius: 0.5rem;
|
|
325
|
+
border: 1px solid var(--color-border);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.size-label {
|
|
329
|
+
font-size: 0.8rem;
|
|
330
|
+
color: var(--color-text-secondary);
|
|
331
|
+
margin-bottom: 5px;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.toggle-dark {
|
|
335
|
+
position: fixed;
|
|
336
|
+
top: 20px;
|
|
337
|
+
right: 20px;
|
|
338
|
+
padding: 0.5rem 1rem;
|
|
339
|
+
background: var(--color-primary);
|
|
340
|
+
color: white;
|
|
341
|
+
border: none;
|
|
342
|
+
border-radius: 0.5rem;
|
|
343
|
+
cursor: pointer;
|
|
344
|
+
font-weight: 600;
|
|
345
|
+
}
|
|
346
|
+
</style>
|
|
347
|
+
</head>
|
|
348
|
+
<body>
|
|
349
|
+
<button class="toggle-dark" onclick="document.documentElement.classList.toggle('dark')">Toggle Dark</button>
|
|
350
|
+
|
|
351
|
+
<div class="test-container">
|
|
352
|
+
<h1>Tools UI Redesign - Compact Grid Layout</h1>
|
|
353
|
+
<div class="description">
|
|
354
|
+
This test page demonstrates the new compact single-card tools UI with integrated voice controls.
|
|
355
|
+
The tools are arranged in a responsive 2x2 grid that adapts to smaller screens.
|
|
356
|
+
</div>
|
|
357
|
+
|
|
358
|
+
<div class="demo-section">
|
|
359
|
+
<div class="section-title">New Features</div>
|
|
360
|
+
<div class="feature-list">
|
|
361
|
+
<div class="feature-item">
|
|
362
|
+
<strong>Compact Grid</strong><br>
|
|
363
|
+
All 4 tools fit in a 2x2 grid that's responsive
|
|
364
|
+
</div>
|
|
365
|
+
<div class="feature-item">
|
|
366
|
+
<strong>Integrated Voice</strong><br>
|
|
367
|
+
Auto-speak toggle and voice selector in header
|
|
368
|
+
</div>
|
|
369
|
+
<div class="feature-item">
|
|
370
|
+
<strong>Efficient Sizing</strong><br>
|
|
371
|
+
Tool cards ~120px height, optimized buttons
|
|
372
|
+
</div>
|
|
373
|
+
<div class="feature-item">
|
|
374
|
+
<strong>Responsive Design</strong><br>
|
|
375
|
+
Adapts from 2 columns to 1 on mobile
|
|
376
|
+
</div>
|
|
377
|
+
</div>
|
|
378
|
+
</div>
|
|
379
|
+
|
|
380
|
+
<div class="demo-section">
|
|
381
|
+
<div class="section-title">Live Preview</div>
|
|
382
|
+
<div style="display: flex; gap: 20px; margin-bottom: 30px; flex-wrap: wrap;">
|
|
383
|
+
<div>
|
|
384
|
+
<div class="size-label">Desktop (600px)</div>
|
|
385
|
+
<div class="tools-popup-content">
|
|
386
|
+
<div class="tools-popup-header">
|
|
387
|
+
<div style="display: flex; align-items: center; gap: 0.75rem; flex: 1;">
|
|
388
|
+
<h2 style="margin: 0; font-size: 1rem;">Tools & Extensions</h2>
|
|
389
|
+
</div>
|
|
390
|
+
<div class="tools-popup-header-controls">
|
|
391
|
+
<label class="tools-voice-toggle">
|
|
392
|
+
<input type="checkbox" checked>
|
|
393
|
+
<span>Auto-speak</span>
|
|
394
|
+
</label>
|
|
395
|
+
<select class="tools-voice-selector">
|
|
396
|
+
<option>Voice</option>
|
|
397
|
+
<option>en-US</option>
|
|
398
|
+
</select>
|
|
399
|
+
<button style="background: none; border: none; color: var(--color-text-secondary); font-size: 1.5rem; cursor: pointer; padding: 0; line-height: 1;">×</button>
|
|
400
|
+
</div>
|
|
401
|
+
</div>
|
|
402
|
+
<div class="tools-popup-scroll">
|
|
403
|
+
<div class="tool-item">
|
|
404
|
+
<div style="display: flex; flex-direction: column; gap: 0.3rem;">
|
|
405
|
+
<div class="tool-header">
|
|
406
|
+
<span class="tool-name">gm-cc</span>
|
|
407
|
+
</div>
|
|
408
|
+
<div class="tool-status-indicator installed">
|
|
409
|
+
<span class="tool-status-dot"></span>
|
|
410
|
+
<span>Up-to-date</span>
|
|
411
|
+
</div>
|
|
412
|
+
<div class="tool-versions">
|
|
413
|
+
<span class="tool-version-item">v1.2.5</span>
|
|
414
|
+
</div>
|
|
415
|
+
</div>
|
|
416
|
+
<div class="tool-actions">
|
|
417
|
+
<button class="tool-btn tool-btn-primary">✓</button>
|
|
418
|
+
</div>
|
|
419
|
+
</div>
|
|
420
|
+
|
|
421
|
+
<div class="tool-item">
|
|
422
|
+
<div style="display: flex; flex-direction: column; gap: 0.3rem;">
|
|
423
|
+
<div class="tool-header">
|
|
424
|
+
<span class="tool-name">gm-oc</span>
|
|
425
|
+
</div>
|
|
426
|
+
<div class="tool-status-indicator needs_update">
|
|
427
|
+
<span class="tool-status-dot"></span>
|
|
428
|
+
<span>Update avail</span>
|
|
429
|
+
</div>
|
|
430
|
+
<div class="tool-versions">
|
|
431
|
+
<span class="tool-version-item">v1.0.2</span>
|
|
432
|
+
<span class="tool-version-item">(v1.0.3 available)</span>
|
|
433
|
+
</div>
|
|
434
|
+
</div>
|
|
435
|
+
<div class="tool-actions">
|
|
436
|
+
<button class="tool-btn tool-btn-primary">Update</button>
|
|
437
|
+
</div>
|
|
438
|
+
</div>
|
|
439
|
+
|
|
440
|
+
<div class="tool-item">
|
|
441
|
+
<div style="display: flex; flex-direction: column; gap: 0.3rem;">
|
|
442
|
+
<div class="tool-header">
|
|
443
|
+
<span class="tool-name">gm-gc</span>
|
|
444
|
+
</div>
|
|
445
|
+
<div class="tool-status-indicator installed">
|
|
446
|
+
<span class="tool-status-dot"></span>
|
|
447
|
+
<span>Up-to-date</span>
|
|
448
|
+
</div>
|
|
449
|
+
<div class="tool-versions">
|
|
450
|
+
<span class="tool-version-item">v2.1.0</span>
|
|
451
|
+
</div>
|
|
452
|
+
</div>
|
|
453
|
+
<div class="tool-actions">
|
|
454
|
+
<button class="tool-btn tool-btn-primary">✓</button>
|
|
455
|
+
</div>
|
|
456
|
+
</div>
|
|
457
|
+
|
|
458
|
+
<div class="tool-item">
|
|
459
|
+
<div style="display: flex; flex-direction: column; gap: 0.3rem;">
|
|
460
|
+
<div class="tool-header">
|
|
461
|
+
<span class="tool-name">gm-kilo</span>
|
|
462
|
+
</div>
|
|
463
|
+
<div class="tool-status-indicator not_installed">
|
|
464
|
+
<span class="tool-status-dot"></span>
|
|
465
|
+
<span>Not instal</span>
|
|
466
|
+
</div>
|
|
467
|
+
<div class="tool-versions">
|
|
468
|
+
</div>
|
|
469
|
+
</div>
|
|
470
|
+
<div class="tool-actions">
|
|
471
|
+
<button class="tool-btn tool-btn-primary">Install</button>
|
|
472
|
+
</div>
|
|
473
|
+
</div>
|
|
474
|
+
</div>
|
|
475
|
+
</div>
|
|
476
|
+
</div>
|
|
477
|
+
|
|
478
|
+
<div>
|
|
479
|
+
<div class="size-label">Installing State</div>
|
|
480
|
+
<div class="tools-popup-content" style="max-width: 300px;">
|
|
481
|
+
<div class="tools-popup-header">
|
|
482
|
+
<h2 style="margin: 0; font-size: 1rem;">Tools & Extensions</h2>
|
|
483
|
+
<button style="background: none; border: none; color: var(--color-text-secondary); font-size: 1.5rem; cursor: pointer; padding: 0; line-height: 1;">×</button>
|
|
484
|
+
</div>
|
|
485
|
+
<div class="tools-popup-scroll">
|
|
486
|
+
<div class="tool-item">
|
|
487
|
+
<div style="display: flex; flex-direction: column; gap: 0.3rem;">
|
|
488
|
+
<div class="tool-header">
|
|
489
|
+
<span class="tool-name">gm-kilo</span>
|
|
490
|
+
</div>
|
|
491
|
+
<div class="tool-status-indicator" style="background: rgba(59, 130, 246, 0.15); color: #3b82f6;">
|
|
492
|
+
<span class="tool-status-dot"></span>
|
|
493
|
+
<span>Installing</span>
|
|
494
|
+
</div>
|
|
495
|
+
</div>
|
|
496
|
+
<div class="tool-actions">
|
|
497
|
+
<button class="tool-btn tool-btn-primary" disabled>Installing</button>
|
|
498
|
+
</div>
|
|
499
|
+
</div>
|
|
500
|
+
</div>
|
|
501
|
+
</div>
|
|
502
|
+
</div>
|
|
503
|
+
</div>
|
|
504
|
+
</div>
|
|
505
|
+
|
|
506
|
+
<div class="demo-section">
|
|
507
|
+
<div class="section-title">Size Comparison</div>
|
|
508
|
+
<div class="size-demo">
|
|
509
|
+
<div class="size-demo-item">
|
|
510
|
+
<div class="size-label">Tool Card Height</div>
|
|
511
|
+
<div style="font-size: 0.9rem; font-weight: 600; color: var(--color-primary);">120px min</div>
|
|
512
|
+
</div>
|
|
513
|
+
<div class="size-demo-item">
|
|
514
|
+
<div class="size-label">Tool Item Padding</div>
|
|
515
|
+
<div style="font-size: 0.9rem; font-weight: 600; color: var(--color-primary);">0.75rem</div>
|
|
516
|
+
</div>
|
|
517
|
+
<div class="size-demo-item">
|
|
518
|
+
<div class="size-label">Button Height</div>
|
|
519
|
+
<div style="font-size: 0.9rem; font-weight: 600; color: var(--color-primary);">28px</div>
|
|
520
|
+
</div>
|
|
521
|
+
<div class="size-demo-item">
|
|
522
|
+
<div class="size-label">Grid Gap</div>
|
|
523
|
+
<div style="font-size: 0.9rem; font-weight: 600; color: var(--color-primary);">1rem</div>
|
|
524
|
+
</div>
|
|
525
|
+
</div>
|
|
526
|
+
</div>
|
|
527
|
+
|
|
528
|
+
<div class="demo-section">
|
|
529
|
+
<div class="section-title">Responsive Behavior</div>
|
|
530
|
+
<div class="feature-list">
|
|
531
|
+
<div class="feature-item">
|
|
532
|
+
<strong>Desktop (>768px)</strong><br>
|
|
533
|
+
2x2 grid with 200px min-width per column
|
|
534
|
+
</div>
|
|
535
|
+
<div class="feature-item">
|
|
536
|
+
<strong>Tablet (481-768px)</strong><br>
|
|
537
|
+
Auto-fit with 150px min-width columns
|
|
538
|
+
</div>
|
|
539
|
+
<div class="feature-item">
|
|
540
|
+
<strong>Mobile (<480px)</strong><br>
|
|
541
|
+
Single column layout, full width
|
|
542
|
+
</div>
|
|
543
|
+
<div class="feature-item">
|
|
544
|
+
<strong>Header on Mobile</strong><br>
|
|
545
|
+
Flex-direction column for better spacing
|
|
546
|
+
</div>
|
|
547
|
+
</div>
|
|
548
|
+
</div>
|
|
549
|
+
</div>
|
|
550
|
+
</body>
|
|
551
|
+
</html>
|