talktocursor 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INSTALL.md +249 -0
- package/README.md +177 -0
- package/build/config.js +82 -0
- package/build/index.js +166 -0
- package/build/settings-server.js +124 -0
- package/package.json +54 -0
- package/public/index.html +1574 -0
- package/scripts/auto-submit.py +394 -0
- package/scripts/silence_detector.py +146 -0
|
@@ -0,0 +1,1574 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Cursor TTS Settings</title>
|
|
7
|
+
<style>
|
|
8
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
|
+
|
|
10
|
+
:root {
|
|
11
|
+
--bg: #0f0f13;
|
|
12
|
+
--surface: #1a1a24;
|
|
13
|
+
--surface-hover: #22222e;
|
|
14
|
+
--border: #2a2a3a;
|
|
15
|
+
--border-focus: #6366f1;
|
|
16
|
+
--text: #e4e4ed;
|
|
17
|
+
--text-muted: #8888a0;
|
|
18
|
+
--primary: #6366f1;
|
|
19
|
+
--primary-hover: #818cf8;
|
|
20
|
+
--success: #22c55e;
|
|
21
|
+
--error: #ef4444;
|
|
22
|
+
--warning: #f59e0b;
|
|
23
|
+
--radius: 12px;
|
|
24
|
+
--radius-sm: 8px;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
body {
|
|
28
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', sans-serif;
|
|
29
|
+
background: var(--bg);
|
|
30
|
+
color: var(--text);
|
|
31
|
+
min-height: 100vh;
|
|
32
|
+
line-height: 1.6;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.container {
|
|
36
|
+
max-width: 640px;
|
|
37
|
+
margin: 0 auto;
|
|
38
|
+
padding: 48px 24px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
header {
|
|
42
|
+
text-align: center;
|
|
43
|
+
margin-bottom: 40px;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
header .icon {
|
|
47
|
+
font-size: 48px;
|
|
48
|
+
margin-bottom: 12px;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
header h1 {
|
|
52
|
+
font-size: 28px;
|
|
53
|
+
font-weight: 700;
|
|
54
|
+
letter-spacing: -0.5px;
|
|
55
|
+
margin-bottom: 6px;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
header p {
|
|
59
|
+
color: var(--text-muted);
|
|
60
|
+
font-size: 15px;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.card {
|
|
64
|
+
background: var(--surface);
|
|
65
|
+
border: 1px solid var(--border);
|
|
66
|
+
border-radius: var(--radius);
|
|
67
|
+
padding: 28px;
|
|
68
|
+
margin-bottom: 20px;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.card-title {
|
|
72
|
+
font-size: 16px;
|
|
73
|
+
font-weight: 600;
|
|
74
|
+
margin-bottom: 20px;
|
|
75
|
+
display: flex;
|
|
76
|
+
align-items: center;
|
|
77
|
+
gap: 8px;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.card-title .badge {
|
|
81
|
+
font-size: 11px;
|
|
82
|
+
font-weight: 500;
|
|
83
|
+
padding: 2px 8px;
|
|
84
|
+
border-radius: 99px;
|
|
85
|
+
text-transform: uppercase;
|
|
86
|
+
letter-spacing: 0.5px;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.badge-set {
|
|
90
|
+
background: rgba(34, 197, 94, 0.15);
|
|
91
|
+
color: var(--success);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.badge-missing {
|
|
95
|
+
background: rgba(239, 68, 68, 0.15);
|
|
96
|
+
color: var(--error);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.form-group {
|
|
100
|
+
margin-bottom: 20px;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.form-group:last-child {
|
|
104
|
+
margin-bottom: 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
label {
|
|
108
|
+
display: block;
|
|
109
|
+
font-size: 13px;
|
|
110
|
+
font-weight: 500;
|
|
111
|
+
color: var(--text-muted);
|
|
112
|
+
margin-bottom: 6px;
|
|
113
|
+
text-transform: uppercase;
|
|
114
|
+
letter-spacing: 0.5px;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.input-wrapper {
|
|
118
|
+
position: relative;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
input[type="text"],
|
|
122
|
+
input[type="password"],
|
|
123
|
+
select {
|
|
124
|
+
width: 100%;
|
|
125
|
+
padding: 10px 14px;
|
|
126
|
+
background: var(--bg);
|
|
127
|
+
border: 1px solid var(--border);
|
|
128
|
+
border-radius: var(--radius-sm);
|
|
129
|
+
color: var(--text);
|
|
130
|
+
font-size: 14px;
|
|
131
|
+
font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
|
|
132
|
+
transition: border-color 0.2s;
|
|
133
|
+
outline: none;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
input:focus, select:focus {
|
|
137
|
+
border-color: var(--border-focus);
|
|
138
|
+
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
select {
|
|
142
|
+
cursor: pointer;
|
|
143
|
+
appearance: none;
|
|
144
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%238888a0' viewBox='0 0 16 16'%3E%3Cpath d='M4.5 6l3.5 4 3.5-4z'/%3E%3C/svg%3E");
|
|
145
|
+
background-repeat: no-repeat;
|
|
146
|
+
background-position: right 12px center;
|
|
147
|
+
padding-right: 36px;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.toggle-visibility {
|
|
151
|
+
position: absolute;
|
|
152
|
+
right: 10px;
|
|
153
|
+
top: 50%;
|
|
154
|
+
transform: translateY(-50%);
|
|
155
|
+
background: none;
|
|
156
|
+
border: none;
|
|
157
|
+
color: var(--text-muted);
|
|
158
|
+
cursor: pointer;
|
|
159
|
+
padding: 4px;
|
|
160
|
+
font-size: 13px;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.toggle-visibility:hover {
|
|
164
|
+
color: var(--text);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.help-text {
|
|
168
|
+
font-size: 12px;
|
|
169
|
+
color: var(--text-muted);
|
|
170
|
+
margin-top: 6px;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.help-text a {
|
|
174
|
+
color: var(--primary);
|
|
175
|
+
text-decoration: none;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.help-text a:hover {
|
|
179
|
+
text-decoration: underline;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.btn-row {
|
|
183
|
+
display: flex;
|
|
184
|
+
gap: 10px;
|
|
185
|
+
margin-top: 24px;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
button {
|
|
189
|
+
padding: 10px 20px;
|
|
190
|
+
border-radius: var(--radius-sm);
|
|
191
|
+
font-size: 14px;
|
|
192
|
+
font-weight: 500;
|
|
193
|
+
cursor: pointer;
|
|
194
|
+
border: none;
|
|
195
|
+
transition: all 0.2s;
|
|
196
|
+
display: inline-flex;
|
|
197
|
+
align-items: center;
|
|
198
|
+
gap: 6px;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.btn-primary {
|
|
202
|
+
background: var(--primary);
|
|
203
|
+
color: white;
|
|
204
|
+
flex: 1;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.btn-primary:hover {
|
|
208
|
+
background: var(--primary-hover);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.btn-primary:disabled {
|
|
212
|
+
opacity: 0.5;
|
|
213
|
+
cursor: not-allowed;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.btn-secondary {
|
|
217
|
+
background: var(--surface-hover);
|
|
218
|
+
color: var(--text);
|
|
219
|
+
border: 1px solid var(--border);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.btn-secondary:hover {
|
|
223
|
+
background: var(--border);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.btn-secondary:disabled {
|
|
227
|
+
opacity: 0.5;
|
|
228
|
+
cursor: not-allowed;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.toast {
|
|
232
|
+
position: fixed;
|
|
233
|
+
bottom: 24px;
|
|
234
|
+
left: 50%;
|
|
235
|
+
transform: translateX(-50%) translateY(80px);
|
|
236
|
+
padding: 12px 24px;
|
|
237
|
+
border-radius: var(--radius-sm);
|
|
238
|
+
font-size: 14px;
|
|
239
|
+
font-weight: 500;
|
|
240
|
+
opacity: 0;
|
|
241
|
+
transition: all 0.3s ease;
|
|
242
|
+
z-index: 1000;
|
|
243
|
+
white-space: nowrap;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.toast.show {
|
|
247
|
+
transform: translateX(-50%) translateY(0);
|
|
248
|
+
opacity: 1;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.toast-success {
|
|
252
|
+
background: rgba(34, 197, 94, 0.15);
|
|
253
|
+
color: var(--success);
|
|
254
|
+
border: 1px solid rgba(34, 197, 94, 0.3);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.toast-error {
|
|
258
|
+
background: rgba(239, 68, 68, 0.15);
|
|
259
|
+
color: var(--error);
|
|
260
|
+
border: 1px solid rgba(239, 68, 68, 0.3);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.voices-grid {
|
|
264
|
+
display: grid;
|
|
265
|
+
grid-template-columns: 1fr;
|
|
266
|
+
gap: 8px;
|
|
267
|
+
max-height: 300px;
|
|
268
|
+
overflow-y: auto;
|
|
269
|
+
margin-top: 12px;
|
|
270
|
+
padding-right: 4px;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.voices-grid::-webkit-scrollbar {
|
|
274
|
+
width: 6px;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.voices-grid::-webkit-scrollbar-track {
|
|
278
|
+
background: transparent;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.voices-grid::-webkit-scrollbar-thumb {
|
|
282
|
+
background: var(--border);
|
|
283
|
+
border-radius: 3px;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.voice-item {
|
|
287
|
+
display: flex;
|
|
288
|
+
align-items: center;
|
|
289
|
+
justify-content: space-between;
|
|
290
|
+
padding: 10px 14px;
|
|
291
|
+
background: var(--bg);
|
|
292
|
+
border: 1px solid var(--border);
|
|
293
|
+
border-radius: var(--radius-sm);
|
|
294
|
+
cursor: pointer;
|
|
295
|
+
transition: all 0.15s;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.voice-item:hover {
|
|
299
|
+
border-color: var(--border-focus);
|
|
300
|
+
background: var(--surface-hover);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.voice-item.selected {
|
|
304
|
+
border-color: var(--primary);
|
|
305
|
+
background: rgba(99, 102, 241, 0.08);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.voice-item .voice-info {
|
|
309
|
+
display: flex;
|
|
310
|
+
flex-direction: column;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.voice-item .voice-name {
|
|
314
|
+
font-size: 14px;
|
|
315
|
+
font-weight: 500;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.voice-item .voice-meta {
|
|
319
|
+
font-size: 12px;
|
|
320
|
+
color: var(--text-muted);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.voice-item .voice-actions {
|
|
324
|
+
display: flex;
|
|
325
|
+
gap: 6px;
|
|
326
|
+
align-items: center;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.btn-icon {
|
|
330
|
+
width: 32px;
|
|
331
|
+
height: 32px;
|
|
332
|
+
padding: 0;
|
|
333
|
+
display: flex;
|
|
334
|
+
align-items: center;
|
|
335
|
+
justify-content: center;
|
|
336
|
+
background: var(--surface);
|
|
337
|
+
border: 1px solid var(--border);
|
|
338
|
+
border-radius: 6px;
|
|
339
|
+
color: var(--text-muted);
|
|
340
|
+
cursor: pointer;
|
|
341
|
+
transition: all 0.15s;
|
|
342
|
+
font-size: 16px;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.btn-icon:hover {
|
|
346
|
+
color: var(--text);
|
|
347
|
+
border-color: var(--text-muted);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.spinner {
|
|
351
|
+
display: inline-block;
|
|
352
|
+
width: 16px;
|
|
353
|
+
height: 16px;
|
|
354
|
+
border: 2px solid transparent;
|
|
355
|
+
border-top-color: currentColor;
|
|
356
|
+
border-radius: 50%;
|
|
357
|
+
animation: spin 0.6s linear infinite;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
@keyframes spin {
|
|
361
|
+
to { transform: rotate(360deg); }
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.loading-state {
|
|
365
|
+
text-align: center;
|
|
366
|
+
padding: 32px 0;
|
|
367
|
+
color: var(--text-muted);
|
|
368
|
+
display: flex;
|
|
369
|
+
flex-direction: column;
|
|
370
|
+
align-items: center;
|
|
371
|
+
gap: 12px;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
.empty-state {
|
|
375
|
+
text-align: center;
|
|
376
|
+
padding: 24px;
|
|
377
|
+
color: var(--text-muted);
|
|
378
|
+
font-size: 14px;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
.env-notice {
|
|
382
|
+
font-size: 12px;
|
|
383
|
+
color: var(--text-muted);
|
|
384
|
+
background: rgba(245, 158, 11, 0.08);
|
|
385
|
+
border: 1px solid rgba(245, 158, 11, 0.2);
|
|
386
|
+
border-radius: var(--radius-sm);
|
|
387
|
+
padding: 10px 14px;
|
|
388
|
+
margin-top: 16px;
|
|
389
|
+
line-height: 1.5;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.env-notice strong {
|
|
393
|
+
color: var(--warning);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.presets-grid {
|
|
397
|
+
display: grid;
|
|
398
|
+
grid-template-columns: repeat(3, 1fr);
|
|
399
|
+
gap: 10px;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.preset-btn {
|
|
403
|
+
padding: 12px 14px;
|
|
404
|
+
background: var(--bg);
|
|
405
|
+
border: 1px solid var(--border);
|
|
406
|
+
border-radius: var(--radius-sm);
|
|
407
|
+
cursor: pointer;
|
|
408
|
+
transition: all 0.15s;
|
|
409
|
+
display: flex;
|
|
410
|
+
flex-direction: column;
|
|
411
|
+
align-items: flex-start;
|
|
412
|
+
gap: 2px;
|
|
413
|
+
text-align: left;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.preset-btn:hover {
|
|
417
|
+
border-color: var(--border-focus);
|
|
418
|
+
background: var(--surface-hover);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
.preset-btn.active {
|
|
422
|
+
border-color: var(--primary);
|
|
423
|
+
background: rgba(99, 102, 241, 0.08);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
.preset-name {
|
|
427
|
+
font-size: 13px;
|
|
428
|
+
font-weight: 600;
|
|
429
|
+
color: var(--text);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
.preset-desc {
|
|
433
|
+
font-size: 11px;
|
|
434
|
+
color: var(--text-muted);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
.slider-group {
|
|
438
|
+
margin-bottom: 20px;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
.slider-group:last-of-type {
|
|
442
|
+
margin-bottom: 0;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
.slider-label {
|
|
446
|
+
display: flex;
|
|
447
|
+
justify-content: space-between;
|
|
448
|
+
align-items: center;
|
|
449
|
+
margin-bottom: 8px;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
.slider-label label {
|
|
453
|
+
margin-bottom: 0;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
.slider-value {
|
|
457
|
+
font-size: 14px;
|
|
458
|
+
font-weight: 600;
|
|
459
|
+
color: var(--primary);
|
|
460
|
+
font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
|
|
461
|
+
min-width: 36px;
|
|
462
|
+
text-align: right;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
input[type="range"] {
|
|
466
|
+
-webkit-appearance: none;
|
|
467
|
+
appearance: none;
|
|
468
|
+
width: 100%;
|
|
469
|
+
height: 6px;
|
|
470
|
+
background: var(--bg);
|
|
471
|
+
border: 1px solid var(--border);
|
|
472
|
+
border-radius: 3px;
|
|
473
|
+
outline: none;
|
|
474
|
+
cursor: pointer;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
input[type="range"]::-webkit-slider-thumb {
|
|
478
|
+
-webkit-appearance: none;
|
|
479
|
+
appearance: none;
|
|
480
|
+
width: 20px;
|
|
481
|
+
height: 20px;
|
|
482
|
+
border-radius: 50%;
|
|
483
|
+
background: var(--primary);
|
|
484
|
+
border: 2px solid var(--surface);
|
|
485
|
+
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
|
|
486
|
+
cursor: pointer;
|
|
487
|
+
transition: background 0.15s;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
input[type="range"]::-webkit-slider-thumb:hover {
|
|
491
|
+
background: var(--primary-hover);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
input[type="range"]::-moz-range-thumb {
|
|
495
|
+
width: 20px;
|
|
496
|
+
height: 20px;
|
|
497
|
+
border-radius: 50%;
|
|
498
|
+
background: var(--primary);
|
|
499
|
+
border: 2px solid var(--surface);
|
|
500
|
+
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
|
|
501
|
+
cursor: pointer;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
.slider-range-labels {
|
|
505
|
+
display: flex;
|
|
506
|
+
justify-content: space-between;
|
|
507
|
+
font-size: 11px;
|
|
508
|
+
color: var(--text-muted);
|
|
509
|
+
margin-top: 4px;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
.toggle-row {
|
|
513
|
+
display: flex;
|
|
514
|
+
align-items: center;
|
|
515
|
+
justify-content: space-between;
|
|
516
|
+
padding: 14px 0;
|
|
517
|
+
border-bottom: 1px solid var(--border);
|
|
518
|
+
margin-bottom: 16px;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
.toggle-row:last-child {
|
|
522
|
+
border-bottom: none;
|
|
523
|
+
margin-bottom: 0;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
.toggle-info {
|
|
527
|
+
display: flex;
|
|
528
|
+
flex-direction: column;
|
|
529
|
+
gap: 2px;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
.toggle-title {
|
|
533
|
+
font-size: 14px;
|
|
534
|
+
font-weight: 500;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
.toggle-desc {
|
|
538
|
+
font-size: 12px;
|
|
539
|
+
color: var(--text-muted);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
.switch {
|
|
543
|
+
position: relative;
|
|
544
|
+
width: 44px;
|
|
545
|
+
height: 24px;
|
|
546
|
+
flex-shrink: 0;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
.switch input {
|
|
550
|
+
opacity: 0;
|
|
551
|
+
width: 0;
|
|
552
|
+
height: 0;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
.switch-slider {
|
|
556
|
+
position: absolute;
|
|
557
|
+
cursor: pointer;
|
|
558
|
+
inset: 0;
|
|
559
|
+
background: var(--border);
|
|
560
|
+
border-radius: 24px;
|
|
561
|
+
transition: background 0.2s;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
.switch-slider::before {
|
|
565
|
+
content: '';
|
|
566
|
+
position: absolute;
|
|
567
|
+
width: 18px;
|
|
568
|
+
height: 18px;
|
|
569
|
+
left: 3px;
|
|
570
|
+
bottom: 3px;
|
|
571
|
+
background: var(--text);
|
|
572
|
+
border-radius: 50%;
|
|
573
|
+
transition: transform 0.2s;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
.switch input:checked + .switch-slider {
|
|
577
|
+
background: var(--primary);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
.switch input:checked + .switch-slider::before {
|
|
581
|
+
transform: translateX(20px);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
.auto-submit-details {
|
|
585
|
+
overflow: hidden;
|
|
586
|
+
transition: max-height 0.3s ease, opacity 0.3s ease;
|
|
587
|
+
max-height: 0;
|
|
588
|
+
opacity: 0;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
.auto-submit-details.open {
|
|
592
|
+
max-height: 1000px;
|
|
593
|
+
opacity: 1;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
.status-indicator {
|
|
597
|
+
display: inline-flex;
|
|
598
|
+
align-items: center;
|
|
599
|
+
gap: 6px;
|
|
600
|
+
font-size: 12px;
|
|
601
|
+
padding: 4px 10px;
|
|
602
|
+
border-radius: 99px;
|
|
603
|
+
font-weight: 500;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
.status-running {
|
|
607
|
+
background: rgba(34, 197, 94, 0.15);
|
|
608
|
+
color: var(--success);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
.status-stopped {
|
|
612
|
+
background: rgba(136, 136, 160, 0.15);
|
|
613
|
+
color: var(--text-muted);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
.status-dot {
|
|
617
|
+
width: 6px;
|
|
618
|
+
height: 6px;
|
|
619
|
+
border-radius: 50%;
|
|
620
|
+
background: currentColor;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
.copy-btn {
|
|
624
|
+
position: absolute;
|
|
625
|
+
right: 8px;
|
|
626
|
+
top: 50%;
|
|
627
|
+
transform: translateY(-50%);
|
|
628
|
+
background: var(--surface-hover);
|
|
629
|
+
border: 1px solid var(--border);
|
|
630
|
+
border-radius: 4px;
|
|
631
|
+
padding: 4px 6px;
|
|
632
|
+
cursor: pointer;
|
|
633
|
+
color: var(--text-muted);
|
|
634
|
+
display: flex;
|
|
635
|
+
align-items: center;
|
|
636
|
+
transition: all 0.15s;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
.copy-btn:hover {
|
|
640
|
+
background: var(--border);
|
|
641
|
+
color: var(--text);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
.copy-btn:active {
|
|
645
|
+
transform: translateY(-50%) scale(0.95);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
.reset-link {
|
|
649
|
+
font-size: 12px;
|
|
650
|
+
color: var(--text-muted);
|
|
651
|
+
background: none;
|
|
652
|
+
border: none;
|
|
653
|
+
cursor: pointer;
|
|
654
|
+
padding: 0;
|
|
655
|
+
text-decoration: underline;
|
|
656
|
+
display: inline;
|
|
657
|
+
font-weight: 400;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
.reset-link:hover {
|
|
661
|
+
color: var(--text);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
footer {
|
|
665
|
+
text-align: center;
|
|
666
|
+
margin-top: 40px;
|
|
667
|
+
color: var(--text-muted);
|
|
668
|
+
font-size: 13px;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
footer a {
|
|
672
|
+
color: var(--primary);
|
|
673
|
+
text-decoration: none;
|
|
674
|
+
}
|
|
675
|
+
</style>
|
|
676
|
+
</head>
|
|
677
|
+
<body>
|
|
678
|
+
<div class="container">
|
|
679
|
+
<header>
|
|
680
|
+
<div class="icon">🔊</div>
|
|
681
|
+
<h1>Cursor TTS Settings</h1>
|
|
682
|
+
<p>Configure your text-to-speech MCP server</p>
|
|
683
|
+
</header>
|
|
684
|
+
|
|
685
|
+
<!-- API Key Card -->
|
|
686
|
+
<div class="card">
|
|
687
|
+
<div class="card-title">
|
|
688
|
+
API Key
|
|
689
|
+
<span id="apiKeyBadge" class="badge badge-missing">Not Set</span>
|
|
690
|
+
</div>
|
|
691
|
+
|
|
692
|
+
<div class="form-group">
|
|
693
|
+
<label for="apiKey">ElevenLabs API Key</label>
|
|
694
|
+
<div class="input-wrapper">
|
|
695
|
+
<input type="password" id="apiKey" placeholder="sk_xxxxxxxxxxxxxxxxxxxxxxxx" autocomplete="off" spellcheck="false">
|
|
696
|
+
<button class="toggle-visibility" id="toggleKey" type="button">Show</button>
|
|
697
|
+
</div>
|
|
698
|
+
<div class="help-text">
|
|
699
|
+
Get your key from <a href="https://elevenlabs.io/app/settings/api-keys" target="_blank">elevenlabs.io/app/settings/api-keys</a>
|
|
700
|
+
</div>
|
|
701
|
+
</div>
|
|
702
|
+
|
|
703
|
+
<div class="btn-row">
|
|
704
|
+
<button class="btn-secondary" id="testBtn" type="button">
|
|
705
|
+
Test Key
|
|
706
|
+
</button>
|
|
707
|
+
<button class="btn-primary" id="saveKeyBtn" type="button">
|
|
708
|
+
Save API Key
|
|
709
|
+
</button>
|
|
710
|
+
</div>
|
|
711
|
+
</div>
|
|
712
|
+
|
|
713
|
+
<!-- Voice Card -->
|
|
714
|
+
<div class="card">
|
|
715
|
+
<div class="card-title">Voice</div>
|
|
716
|
+
|
|
717
|
+
<div class="form-group">
|
|
718
|
+
<label for="voiceId">Voice ID</label>
|
|
719
|
+
<input type="text" id="voiceId" placeholder="21m00Tcm4TlvDq8ikWAM" spellcheck="false">
|
|
720
|
+
<div class="help-text">
|
|
721
|
+
Browse voices at <a href="https://elevenlabs.io/app/voice-library" target="_blank">elevenlabs.io/app/voice-library</a>
|
|
722
|
+
</div>
|
|
723
|
+
</div>
|
|
724
|
+
|
|
725
|
+
<div class="btn-row">
|
|
726
|
+
<button class="btn-secondary" id="loadVoicesBtn" type="button">
|
|
727
|
+
Load My Voices
|
|
728
|
+
</button>
|
|
729
|
+
<button class="btn-primary" id="saveVoiceBtn" type="button">
|
|
730
|
+
Save Voice
|
|
731
|
+
</button>
|
|
732
|
+
</div>
|
|
733
|
+
|
|
734
|
+
<div id="voicesList" style="display: none;">
|
|
735
|
+
<div class="voices-grid" id="voicesGrid"></div>
|
|
736
|
+
</div>
|
|
737
|
+
</div>
|
|
738
|
+
|
|
739
|
+
<!-- Model Card -->
|
|
740
|
+
<div class="card">
|
|
741
|
+
<div class="card-title">Model</div>
|
|
742
|
+
|
|
743
|
+
<div class="form-group">
|
|
744
|
+
<label for="model">TTS Model</label>
|
|
745
|
+
<select id="model">
|
|
746
|
+
<option value="eleven_flash_v2_5">Flash v2.5 (fastest, recommended)</option>
|
|
747
|
+
<option value="eleven_flash_v2">Flash v2</option>
|
|
748
|
+
<option value="eleven_multilingual_v2">Multilingual v2 (best quality)</option>
|
|
749
|
+
<option value="eleven_turbo_v2_5">Turbo v2.5</option>
|
|
750
|
+
<option value="eleven_turbo_v2">Turbo v2</option>
|
|
751
|
+
<option value="eleven_monolingual_v1">Monolingual v1</option>
|
|
752
|
+
</select>
|
|
753
|
+
</div>
|
|
754
|
+
|
|
755
|
+
<div class="btn-row">
|
|
756
|
+
<button class="btn-primary" id="saveModelBtn" type="button" style="flex: none;">
|
|
757
|
+
Save Model
|
|
758
|
+
</button>
|
|
759
|
+
</div>
|
|
760
|
+
</div>
|
|
761
|
+
|
|
762
|
+
<!-- Voice Settings Card -->
|
|
763
|
+
<div class="card">
|
|
764
|
+
<div class="card-title">
|
|
765
|
+
Voice Settings
|
|
766
|
+
<button class="reset-link" id="resetSettingsBtn" type="button">Reset to defaults</button>
|
|
767
|
+
</div>
|
|
768
|
+
|
|
769
|
+
<div class="form-group" style="margin-bottom: 24px;">
|
|
770
|
+
<label style="margin-bottom: 10px;">Quick Presets</label>
|
|
771
|
+
<div class="presets-grid">
|
|
772
|
+
<button class="preset-btn" data-preset="default" type="button">
|
|
773
|
+
<span class="preset-name">Default</span>
|
|
774
|
+
<span class="preset-desc">Balanced</span>
|
|
775
|
+
</button>
|
|
776
|
+
<button class="preset-btn" data-preset="fast" type="button">
|
|
777
|
+
<span class="preset-name">Fast</span>
|
|
778
|
+
<span class="preset-desc">Quick & energetic</span>
|
|
779
|
+
</button>
|
|
780
|
+
<button class="preset-btn" data-preset="slow" type="button">
|
|
781
|
+
<span class="preset-name">Slow</span>
|
|
782
|
+
<span class="preset-desc">Clear & measured</span>
|
|
783
|
+
</button>
|
|
784
|
+
<button class="preset-btn" data-preset="expressive" type="button">
|
|
785
|
+
<span class="preset-name">Expressive</span>
|
|
786
|
+
<span class="preset-desc">Dynamic & varied</span>
|
|
787
|
+
</button>
|
|
788
|
+
<button class="preset-btn" data-preset="stable" type="button">
|
|
789
|
+
<span class="preset-name">Stable</span>
|
|
790
|
+
<span class="preset-desc">Consistent tone</span>
|
|
791
|
+
</button>
|
|
792
|
+
<button class="preset-btn" data-preset="dramatic" type="button">
|
|
793
|
+
<span class="preset-name">Dramatic</span>
|
|
794
|
+
<span class="preset-desc">Maximum style</span>
|
|
795
|
+
</button>
|
|
796
|
+
</div>
|
|
797
|
+
</div>
|
|
798
|
+
|
|
799
|
+
<div class="slider-group">
|
|
800
|
+
<div class="slider-label">
|
|
801
|
+
<label for="speed">Speed</label>
|
|
802
|
+
<span class="slider-value" id="speedValue">1.00</span>
|
|
803
|
+
</div>
|
|
804
|
+
<input type="range" id="speed" min="0.7" max="1.2" step="0.05" value="1.0">
|
|
805
|
+
<div class="slider-range-labels">
|
|
806
|
+
<span>0.7x (slow)</span>
|
|
807
|
+
<span>1.2x (fast)</span>
|
|
808
|
+
</div>
|
|
809
|
+
<div class="help-text">Controls how fast the speech is delivered. 1.0 is normal speed.</div>
|
|
810
|
+
</div>
|
|
811
|
+
|
|
812
|
+
<div class="slider-group">
|
|
813
|
+
<div class="slider-label">
|
|
814
|
+
<label for="stability">Stability</label>
|
|
815
|
+
<span class="slider-value" id="stabilityValue">0.50</span>
|
|
816
|
+
</div>
|
|
817
|
+
<input type="range" id="stability" min="0" max="1" step="0.05" value="0.5">
|
|
818
|
+
<div class="slider-range-labels">
|
|
819
|
+
<span>More variable</span>
|
|
820
|
+
<span>More stable</span>
|
|
821
|
+
</div>
|
|
822
|
+
<div class="help-text">Higher values make the voice more consistent; lower adds more expressiveness.</div>
|
|
823
|
+
</div>
|
|
824
|
+
|
|
825
|
+
<div class="slider-group">
|
|
826
|
+
<div class="slider-label">
|
|
827
|
+
<label for="similarityBoost">Similarity Boost</label>
|
|
828
|
+
<span class="slider-value" id="similarityBoostValue">0.75</span>
|
|
829
|
+
</div>
|
|
830
|
+
<input type="range" id="similarityBoost" min="0" max="1" step="0.05" value="0.75">
|
|
831
|
+
<div class="slider-range-labels">
|
|
832
|
+
<span>Low</span>
|
|
833
|
+
<span>High</span>
|
|
834
|
+
</div>
|
|
835
|
+
<div class="help-text">Boosts similarity to the original voice. Higher values use more compute and may increase latency.</div>
|
|
836
|
+
</div>
|
|
837
|
+
|
|
838
|
+
<div class="slider-group">
|
|
839
|
+
<div class="slider-label">
|
|
840
|
+
<label for="style">Style Exaggeration</label>
|
|
841
|
+
<span class="slider-value" id="styleValue">0.00</span>
|
|
842
|
+
</div>
|
|
843
|
+
<input type="range" id="style" min="0" max="1" step="0.05" value="0">
|
|
844
|
+
<div class="slider-range-labels">
|
|
845
|
+
<span>None</span>
|
|
846
|
+
<span>Exaggerated</span>
|
|
847
|
+
</div>
|
|
848
|
+
<div class="help-text">Amplifies the style of the original speaker. Only works with V2+ models. May increase latency.</div>
|
|
849
|
+
</div>
|
|
850
|
+
|
|
851
|
+
<div class="btn-row">
|
|
852
|
+
<button class="btn-primary" id="saveSettingsBtn" type="button" style="flex: none;">
|
|
853
|
+
Save Voice Settings
|
|
854
|
+
</button>
|
|
855
|
+
</div>
|
|
856
|
+
</div>
|
|
857
|
+
|
|
858
|
+
<!-- Auto-Listen Card -->
|
|
859
|
+
<div class="card">
|
|
860
|
+
<div class="card-title">Auto-Listen</div>
|
|
861
|
+
|
|
862
|
+
<div class="toggle-row">
|
|
863
|
+
<div class="toggle-info">
|
|
864
|
+
<span class="toggle-title">Enable Auto-Listen</span>
|
|
865
|
+
<span class="toggle-desc">Automatically start listening for voice input after task completion</span>
|
|
866
|
+
</div>
|
|
867
|
+
<label class="switch">
|
|
868
|
+
<input type="checkbox" id="autoListenEnabled">
|
|
869
|
+
<span class="switch-slider"></span>
|
|
870
|
+
</label>
|
|
871
|
+
</div>
|
|
872
|
+
|
|
873
|
+
<p style="color: var(--text-muted); font-size: 13px; margin-top: 16px; line-height: 1.5;">
|
|
874
|
+
When enabled, I'll automatically call the listen tool after completing tasks to trigger the Wispr voice loop.
|
|
875
|
+
Disable this if you prefer to manually start dictation.
|
|
876
|
+
</p>
|
|
877
|
+
</div>
|
|
878
|
+
|
|
879
|
+
<!-- Auto-Submit Card -->
|
|
880
|
+
<div class="card">
|
|
881
|
+
<div class="card-title">Auto-Submit</div>
|
|
882
|
+
|
|
883
|
+
<div class="toggle-row">
|
|
884
|
+
<div class="toggle-info">
|
|
885
|
+
<span class="toggle-title">Enable Auto-Submit</span>
|
|
886
|
+
<span class="toggle-desc">Automatically press Enter when dictation ends</span>
|
|
887
|
+
</div>
|
|
888
|
+
<label class="switch">
|
|
889
|
+
<input type="checkbox" id="autoSubmitEnabled">
|
|
890
|
+
<span class="switch-slider"></span>
|
|
891
|
+
</label>
|
|
892
|
+
</div>
|
|
893
|
+
|
|
894
|
+
<div class="auto-submit-details" id="autoSubmitDetails">
|
|
895
|
+
<div class="slider-group">
|
|
896
|
+
<div class="slider-label">
|
|
897
|
+
<label for="silenceDelay">Silence Delay</label>
|
|
898
|
+
<span class="slider-value" id="silenceDelayValue">3.0s</span>
|
|
899
|
+
</div>
|
|
900
|
+
<input type="range" id="silenceDelay" min="0.5" max="5" step="0.25" value="3.0">
|
|
901
|
+
<div class="slider-range-labels">
|
|
902
|
+
<span>0.5s (instant)</span>
|
|
903
|
+
<span>5s (patient)</span>
|
|
904
|
+
</div>
|
|
905
|
+
<div class="help-text">How long to wait after typing stops before pressing Enter. Increase if prompts are getting cut off.</div>
|
|
906
|
+
</div>
|
|
907
|
+
|
|
908
|
+
<div class="slider-group">
|
|
909
|
+
<div class="slider-label">
|
|
910
|
+
<label for="minTextLength">Min Text Length</label>
|
|
911
|
+
<span class="slider-value" id="minTextLengthValue">15</span>
|
|
912
|
+
</div>
|
|
913
|
+
<input type="range" id="minTextLength" min="5" max="50" step="5" value="15">
|
|
914
|
+
<div class="slider-range-labels">
|
|
915
|
+
<span>5 chars (sensitive)</span>
|
|
916
|
+
<span>50 chars (strict)</span>
|
|
917
|
+
</div>
|
|
918
|
+
<div class="help-text">Minimum characters in clipboard to count as dictation. Prevents accidental submits from short copy-paste actions.</div>
|
|
919
|
+
</div>
|
|
920
|
+
|
|
921
|
+
<div class="btn-row" style="margin-bottom: 16px;">
|
|
922
|
+
<button class="btn-primary" id="saveAutoSubmitBtn" type="button" style="flex: none;">
|
|
923
|
+
Save Auto-Submit Settings
|
|
924
|
+
</button>
|
|
925
|
+
</div>
|
|
926
|
+
|
|
927
|
+
<div class="env-notice" style="margin-top: 0;">
|
|
928
|
+
<strong>How it works:</strong> Monitors the text field via Accessibility API. Detects text from dictation, typing, or paste. Auto-submits when Cursor is active.
|
|
929
|
+
<br><br>
|
|
930
|
+
<strong>Setup:</strong> After enabling and saving, run in a terminal:
|
|
931
|
+
<div style="position: relative; margin-top: 6px;">
|
|
932
|
+
<code style="display: block; padding: 6px 10px 6px 10px; background: var(--bg); border-radius: 4px; font-size: 13px; padding-right: 45px;">npm run auto-submit</code>
|
|
933
|
+
<button class="copy-btn" onclick="copyCommand()" title="Copy command">
|
|
934
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
935
|
+
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
|
936
|
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
|
937
|
+
</svg>
|
|
938
|
+
</button>
|
|
939
|
+
</div>
|
|
940
|
+
<span style="display: block; margin-top: 6px;">Requires macOS Accessibility permissions (System Settings > Privacy > Accessibility).</span>
|
|
941
|
+
</div>
|
|
942
|
+
</div>
|
|
943
|
+
</div>
|
|
944
|
+
|
|
945
|
+
<!-- Wispr Voice Loop Card -->
|
|
946
|
+
<div class="card">
|
|
947
|
+
<div class="card-title">Wispr Voice Loop</div>
|
|
948
|
+
|
|
949
|
+
<div class="toggle-row">
|
|
950
|
+
<div class="toggle-info">
|
|
951
|
+
<span class="toggle-title">Enable Wispr Voice Loop</span>
|
|
952
|
+
<span class="toggle-desc">Hands-free conversational loop with automatic Wispr control</span>
|
|
953
|
+
</div>
|
|
954
|
+
<label class="switch">
|
|
955
|
+
<input type="checkbox" id="wisprLoopEnabled">
|
|
956
|
+
<span class="switch-slider"></span>
|
|
957
|
+
</label>
|
|
958
|
+
</div>
|
|
959
|
+
|
|
960
|
+
<div class="auto-submit-details" id="wisprLoopDetails">
|
|
961
|
+
<div class="slider-group">
|
|
962
|
+
<div class="slider-label">
|
|
963
|
+
<label for="ttsDelay">TTS Delay</label>
|
|
964
|
+
<span class="slider-value" id="ttsDelayValue">4.0s</span>
|
|
965
|
+
</div>
|
|
966
|
+
<input type="range" id="ttsDelay" min="1" max="8" step="0.5" value="4.0">
|
|
967
|
+
<div class="slider-range-labels">
|
|
968
|
+
<span>1s (quick)</span>
|
|
969
|
+
<span>8s (patient)</span>
|
|
970
|
+
</div>
|
|
971
|
+
<div class="help-text">How long to wait for TTS to finish before starting Wispr. Increase if TTS gets cut off.</div>
|
|
972
|
+
</div>
|
|
973
|
+
|
|
974
|
+
<div class="slider-group">
|
|
975
|
+
<div class="slider-label">
|
|
976
|
+
<label for="silenceThreshold">Silence Threshold</label>
|
|
977
|
+
<span class="slider-value" id="silenceThresholdValue">0.020</span>
|
|
978
|
+
</div>
|
|
979
|
+
<input type="range" id="silenceThreshold" min="0.005" max="0.1" step="0.005" value="0.02">
|
|
980
|
+
<div class="slider-range-labels">
|
|
981
|
+
<span>0.005 (sensitive)</span>
|
|
982
|
+
<span>0.1 (strict)</span>
|
|
983
|
+
</div>
|
|
984
|
+
<div class="help-text">RMS amplitude threshold for speech detection. Lower = more sensitive to quiet speech.</div>
|
|
985
|
+
</div>
|
|
986
|
+
|
|
987
|
+
<div class="slider-group">
|
|
988
|
+
<div class="slider-label">
|
|
989
|
+
<label for="silenceDuration">Silence Duration</label>
|
|
990
|
+
<span class="slider-value" id="silenceDurationValue">2.0s</span>
|
|
991
|
+
</div>
|
|
992
|
+
<input type="range" id="silenceDuration" min="0.5" max="5" step="0.25" value="2.0">
|
|
993
|
+
<div class="slider-range-labels">
|
|
994
|
+
<span>0.5s (quick)</span>
|
|
995
|
+
<span>5s (patient)</span>
|
|
996
|
+
</div>
|
|
997
|
+
<div class="help-text">How long of silence confirms you're done speaking. Increase if it cuts you off mid-sentence.</div>
|
|
998
|
+
</div>
|
|
999
|
+
|
|
1000
|
+
<div class="form-group">
|
|
1001
|
+
<label for="wisprHotkey">Wispr Hotkey</label>
|
|
1002
|
+
<input type="text" id="wisprHotkey" value="shift+ctrl" spellcheck="false">
|
|
1003
|
+
<div class="help-text">Hotkey combo that toggles Wispr recording (start/stop). Default: shift+ctrl</div>
|
|
1004
|
+
</div>
|
|
1005
|
+
|
|
1006
|
+
<div class="form-group">
|
|
1007
|
+
<label for="manualTriggerHotkey">Manual Trigger Hotkey</label>
|
|
1008
|
+
<input type="text" id="manualTriggerHotkey" value="ctrl+shift+l" spellcheck="false">
|
|
1009
|
+
<div class="help-text">Global hotkey to manually start the voice loop anytime. Default: ctrl+shift+l</div>
|
|
1010
|
+
</div>
|
|
1011
|
+
|
|
1012
|
+
<div class="btn-row" style="margin-bottom: 16px;">
|
|
1013
|
+
<button class="btn-primary" id="saveWisprLoopBtn" type="button" style="flex: none;">
|
|
1014
|
+
Save Wispr Loop Settings
|
|
1015
|
+
</button>
|
|
1016
|
+
</div>
|
|
1017
|
+
|
|
1018
|
+
<div class="env-notice" style="margin-top: 0;">
|
|
1019
|
+
<strong>How it works:</strong>
|
|
1020
|
+
<ol style="margin: 8px 0 0 20px; padding: 0;">
|
|
1021
|
+
<li>Agent finishes task and calls listen() tool</li>
|
|
1022
|
+
<li>Script waits for TTS to finish, then triggers Wispr</li>
|
|
1023
|
+
<li>Your mic is monitored for speech and silence</li>
|
|
1024
|
+
<li>When silence is detected, Wispr stops and pastes</li>
|
|
1025
|
+
<li>Auto-submit presses Enter, cycle repeats</li>
|
|
1026
|
+
</ol>
|
|
1027
|
+
<span style="display: block; margin-top: 8px;">Requires sounddevice Python package and PortAudio. See terminal for setup commands.</span>
|
|
1028
|
+
</div>
|
|
1029
|
+
</div>
|
|
1030
|
+
</div>
|
|
1031
|
+
|
|
1032
|
+
<!-- Info Notice -->
|
|
1033
|
+
<div class="env-notice">
|
|
1034
|
+
<strong>Note:</strong> Environment variables (<code>ELEVENLABS_API_KEY</code>, <code>ELEVENLABS_VOICE_ID</code>) take priority over these settings.
|
|
1035
|
+
After saving, restart Cursor for the MCP server to pick up the new config.
|
|
1036
|
+
</div>
|
|
1037
|
+
|
|
1038
|
+
<footer>
|
|
1039
|
+
cursor-tts-mcp · <a href="https://github.com" target="_blank">Documentation</a>
|
|
1040
|
+
</footer>
|
|
1041
|
+
</div>
|
|
1042
|
+
|
|
1043
|
+
<div class="toast" id="toast"></div>
|
|
1044
|
+
|
|
1045
|
+
<script>
|
|
1046
|
+
// Elements
|
|
1047
|
+
const apiKeyInput = document.getElementById('apiKey');
|
|
1048
|
+
const toggleKeyBtn = document.getElementById('toggleKey');
|
|
1049
|
+
const testBtn = document.getElementById('testBtn');
|
|
1050
|
+
const saveKeyBtn = document.getElementById('saveKeyBtn');
|
|
1051
|
+
const apiKeyBadge = document.getElementById('apiKeyBadge');
|
|
1052
|
+
const voiceIdInput = document.getElementById('voiceId');
|
|
1053
|
+
const loadVoicesBtn = document.getElementById('loadVoicesBtn');
|
|
1054
|
+
const saveVoiceBtn = document.getElementById('saveVoiceBtn');
|
|
1055
|
+
const voicesList = document.getElementById('voicesList');
|
|
1056
|
+
const voicesGrid = document.getElementById('voicesGrid');
|
|
1057
|
+
const modelSelect = document.getElementById('model');
|
|
1058
|
+
const saveModelBtn = document.getElementById('saveModelBtn');
|
|
1059
|
+
const speedSlider = document.getElementById('speed');
|
|
1060
|
+
const speedValue = document.getElementById('speedValue');
|
|
1061
|
+
const stabilitySlider = document.getElementById('stability');
|
|
1062
|
+
const stabilityValue = document.getElementById('stabilityValue');
|
|
1063
|
+
const similarityBoostSlider = document.getElementById('similarityBoost');
|
|
1064
|
+
const similarityBoostValue = document.getElementById('similarityBoostValue');
|
|
1065
|
+
const styleSlider = document.getElementById('style');
|
|
1066
|
+
const styleValue = document.getElementById('styleValue');
|
|
1067
|
+
const saveSettingsBtn = document.getElementById('saveSettingsBtn');
|
|
1068
|
+
const resetSettingsBtn = document.getElementById('resetSettingsBtn');
|
|
1069
|
+
const autoListenEnabled = document.getElementById('autoListenEnabled');
|
|
1070
|
+
const autoSubmitEnabled = document.getElementById('autoSubmitEnabled');
|
|
1071
|
+
const autoSubmitDetails = document.getElementById('autoSubmitDetails');
|
|
1072
|
+
const silenceDelaySlider = document.getElementById('silenceDelay');
|
|
1073
|
+
const silenceDelayValue = document.getElementById('silenceDelayValue');
|
|
1074
|
+
const minTextLengthSlider = document.getElementById('minTextLength');
|
|
1075
|
+
const minTextLengthValue = document.getElementById('minTextLengthValue');
|
|
1076
|
+
const saveAutoSubmitBtn = document.getElementById('saveAutoSubmitBtn');
|
|
1077
|
+
const wisprLoopEnabled = document.getElementById('wisprLoopEnabled');
|
|
1078
|
+
const wisprLoopDetails = document.getElementById('wisprLoopDetails');
|
|
1079
|
+
const ttsDelaySlider = document.getElementById('ttsDelay');
|
|
1080
|
+
const ttsDelayValue = document.getElementById('ttsDelayValue');
|
|
1081
|
+
const silenceThresholdSlider = document.getElementById('silenceThreshold');
|
|
1082
|
+
const silenceThresholdValue = document.getElementById('silenceThresholdValue');
|
|
1083
|
+
const silenceDurationSlider = document.getElementById('silenceDuration');
|
|
1084
|
+
const silenceDurationValue = document.getElementById('silenceDurationValue');
|
|
1085
|
+
const wisprHotkeyInput = document.getElementById('wisprHotkey');
|
|
1086
|
+
const manualTriggerHotkeyInput = document.getElementById('manualTriggerHotkey');
|
|
1087
|
+
const saveWisprLoopBtn = document.getElementById('saveWisprLoopBtn');
|
|
1088
|
+
const toastEl = document.getElementById('toast');
|
|
1089
|
+
|
|
1090
|
+
let currentPreviewAudio = null;
|
|
1091
|
+
|
|
1092
|
+
// Wire up slider value displays
|
|
1093
|
+
const sliders = [
|
|
1094
|
+
{ slider: speedSlider, display: speedValue },
|
|
1095
|
+
{ slider: stabilitySlider, display: stabilityValue },
|
|
1096
|
+
{ slider: similarityBoostSlider, display: similarityBoostValue },
|
|
1097
|
+
{ slider: styleSlider, display: styleValue },
|
|
1098
|
+
];
|
|
1099
|
+
sliders.forEach(({ slider, display }) => {
|
|
1100
|
+
slider.addEventListener('input', () => {
|
|
1101
|
+
display.textContent = parseFloat(slider.value).toFixed(2);
|
|
1102
|
+
});
|
|
1103
|
+
});
|
|
1104
|
+
|
|
1105
|
+
// Auto-submit toggle and sliders
|
|
1106
|
+
// Auto-listen toggle - save immediately on change
|
|
1107
|
+
autoListenEnabled.addEventListener('change', async () => {
|
|
1108
|
+
try {
|
|
1109
|
+
const res = await fetch('/api/config', {
|
|
1110
|
+
method: 'POST',
|
|
1111
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1112
|
+
body: JSON.stringify({ autoListen: autoListenEnabled.checked }),
|
|
1113
|
+
});
|
|
1114
|
+
const data = await res.json();
|
|
1115
|
+
|
|
1116
|
+
if (data.success) {
|
|
1117
|
+
showToast(autoListenEnabled.checked ? 'Auto-listen enabled' : 'Auto-listen disabled', 'success');
|
|
1118
|
+
}
|
|
1119
|
+
} catch (error) {
|
|
1120
|
+
showToast('Failed to save auto-listen setting', 'error');
|
|
1121
|
+
}
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
autoSubmitEnabled.addEventListener('change', () => {
|
|
1125
|
+
autoSubmitDetails.classList.toggle('open', autoSubmitEnabled.checked);
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
silenceDelaySlider.addEventListener('input', () => {
|
|
1129
|
+
silenceDelayValue.textContent = parseFloat(silenceDelaySlider.value).toFixed(1) + 's';
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
minTextLengthSlider.addEventListener('input', () => {
|
|
1133
|
+
minTextLengthValue.textContent = parseInt(minTextLengthSlider.value);
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
// Wispr loop toggle and sliders
|
|
1137
|
+
wisprLoopEnabled.addEventListener('change', () => {
|
|
1138
|
+
wisprLoopDetails.classList.toggle('open', wisprLoopEnabled.checked);
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
ttsDelaySlider.addEventListener('input', () => {
|
|
1142
|
+
ttsDelayValue.textContent = parseFloat(ttsDelaySlider.value).toFixed(1) + 's';
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
silenceThresholdSlider.addEventListener('input', () => {
|
|
1146
|
+
silenceThresholdValue.textContent = parseFloat(silenceThresholdSlider.value).toFixed(3);
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
silenceDurationSlider.addEventListener('input', () => {
|
|
1150
|
+
silenceDurationValue.textContent = parseFloat(silenceDurationSlider.value).toFixed(1) + 's';
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
// Voice settings presets
|
|
1154
|
+
const presets = {
|
|
1155
|
+
default: { speed: 1.0, stability: 0.5, similarityBoost: 0.75, style: 0.0 },
|
|
1156
|
+
fast: { speed: 1.15, stability: 0.6, similarityBoost: 0.75, style: 0.1 },
|
|
1157
|
+
slow: { speed: 0.85, stability: 0.7, similarityBoost: 0.8, style: 0.0 },
|
|
1158
|
+
expressive: { speed: 1.0, stability: 0.3, similarityBoost: 0.7, style: 0.3 },
|
|
1159
|
+
stable: { speed: 1.0, stability: 0.8, similarityBoost: 0.85, style: 0.0 },
|
|
1160
|
+
dramatic: { speed: 0.95, stability: 0.2, similarityBoost: 0.7, style: 0.6 },
|
|
1161
|
+
};
|
|
1162
|
+
|
|
1163
|
+
function applyPreset(presetName) {
|
|
1164
|
+
const preset = presets[presetName];
|
|
1165
|
+
if (!preset) return;
|
|
1166
|
+
|
|
1167
|
+
speedSlider.value = preset.speed;
|
|
1168
|
+
speedValue.textContent = preset.speed.toFixed(2);
|
|
1169
|
+
stabilitySlider.value = preset.stability;
|
|
1170
|
+
stabilityValue.textContent = preset.stability.toFixed(2);
|
|
1171
|
+
similarityBoostSlider.value = preset.similarityBoost;
|
|
1172
|
+
similarityBoostValue.textContent = preset.similarityBoost.toFixed(2);
|
|
1173
|
+
styleSlider.value = preset.style;
|
|
1174
|
+
styleValue.textContent = preset.style.toFixed(2);
|
|
1175
|
+
|
|
1176
|
+
// Update active state
|
|
1177
|
+
document.querySelectorAll('.preset-btn').forEach(btn => {
|
|
1178
|
+
btn.classList.toggle('active', btn.dataset.preset === presetName);
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1181
|
+
showToast(`Applied ${presetName} preset - click Save to apply`, 'success');
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// Preset button click handlers
|
|
1185
|
+
document.querySelectorAll('.preset-btn').forEach(btn => {
|
|
1186
|
+
btn.addEventListener('click', () => applyPreset(btn.dataset.preset));
|
|
1187
|
+
});
|
|
1188
|
+
|
|
1189
|
+
// Toggle API key visibility
|
|
1190
|
+
toggleKeyBtn.addEventListener('click', () => {
|
|
1191
|
+
const isPassword = apiKeyInput.type === 'password';
|
|
1192
|
+
apiKeyInput.type = isPassword ? 'text' : 'password';
|
|
1193
|
+
toggleKeyBtn.textContent = isPassword ? 'Hide' : 'Show';
|
|
1194
|
+
});
|
|
1195
|
+
|
|
1196
|
+
// Load current config on page load
|
|
1197
|
+
async function loadConfig() {
|
|
1198
|
+
try {
|
|
1199
|
+
const res = await fetch('/api/config');
|
|
1200
|
+
const data = await res.json();
|
|
1201
|
+
|
|
1202
|
+
if (data.apiKeySet) {
|
|
1203
|
+
apiKeyInput.placeholder = data.apiKey;
|
|
1204
|
+
apiKeyBadge.textContent = 'Set';
|
|
1205
|
+
apiKeyBadge.className = 'badge badge-set';
|
|
1206
|
+
} else {
|
|
1207
|
+
apiKeyBadge.textContent = 'Not Set';
|
|
1208
|
+
apiKeyBadge.className = 'badge badge-missing';
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
voiceIdInput.value = data.voiceId || '';
|
|
1212
|
+
modelSelect.value = data.model || 'eleven_flash_v2_5';
|
|
1213
|
+
|
|
1214
|
+
// Auto-listen setting
|
|
1215
|
+
if (data.autoListen !== undefined) {
|
|
1216
|
+
autoListenEnabled.checked = data.autoListen;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// Auto-submit settings
|
|
1220
|
+
if (data.autoSubmit) {
|
|
1221
|
+
const as = data.autoSubmit;
|
|
1222
|
+
autoSubmitEnabled.checked = !!as.enabled;
|
|
1223
|
+
autoSubmitDetails.classList.toggle('open', !!as.enabled);
|
|
1224
|
+
silenceDelaySlider.value = as.silenceDelay ?? 2.0;
|
|
1225
|
+
silenceDelayValue.textContent = parseFloat(silenceDelaySlider.value).toFixed(1) + 's';
|
|
1226
|
+
minTextLengthSlider.value = as.minTextLength ?? 10;
|
|
1227
|
+
minTextLengthValue.textContent = parseInt(minTextLengthSlider.value);
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// Voice settings sliders
|
|
1231
|
+
if (data.voiceSettings) {
|
|
1232
|
+
const vs = data.voiceSettings;
|
|
1233
|
+
speedSlider.value = vs.speed ?? 1.0;
|
|
1234
|
+
speedValue.textContent = parseFloat(speedSlider.value).toFixed(2);
|
|
1235
|
+
stabilitySlider.value = vs.stability ?? 0.5;
|
|
1236
|
+
stabilityValue.textContent = parseFloat(stabilitySlider.value).toFixed(2);
|
|
1237
|
+
similarityBoostSlider.value = vs.similarityBoost ?? 0.75;
|
|
1238
|
+
similarityBoostValue.textContent = parseFloat(similarityBoostSlider.value).toFixed(2);
|
|
1239
|
+
styleSlider.value = vs.style ?? 0.0;
|
|
1240
|
+
styleValue.textContent = parseFloat(styleSlider.value).toFixed(2);
|
|
1241
|
+
}
|
|
1242
|
+
} catch (error) {
|
|
1243
|
+
showToast('Failed to load config', 'error');
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
// Save API key
|
|
1248
|
+
saveKeyBtn.addEventListener('click', async () => {
|
|
1249
|
+
const apiKey = apiKeyInput.value.trim();
|
|
1250
|
+
if (!apiKey) {
|
|
1251
|
+
showToast('Please enter an API key', 'error');
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
saveKeyBtn.disabled = true;
|
|
1256
|
+
saveKeyBtn.innerHTML = '<span class="spinner"></span> Saving...';
|
|
1257
|
+
|
|
1258
|
+
try {
|
|
1259
|
+
const res = await fetch('/api/config', {
|
|
1260
|
+
method: 'POST',
|
|
1261
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1262
|
+
body: JSON.stringify({ apiKey }),
|
|
1263
|
+
});
|
|
1264
|
+
const data = await res.json();
|
|
1265
|
+
|
|
1266
|
+
if (data.success) {
|
|
1267
|
+
apiKeyInput.value = '';
|
|
1268
|
+
apiKeyInput.placeholder = data.config.apiKey;
|
|
1269
|
+
apiKeyBadge.textContent = 'Set';
|
|
1270
|
+
apiKeyBadge.className = 'badge badge-set';
|
|
1271
|
+
showToast('API key saved successfully', 'success');
|
|
1272
|
+
}
|
|
1273
|
+
} catch (error) {
|
|
1274
|
+
showToast('Failed to save API key', 'error');
|
|
1275
|
+
} finally {
|
|
1276
|
+
saveKeyBtn.disabled = false;
|
|
1277
|
+
saveKeyBtn.innerHTML = 'Save API Key';
|
|
1278
|
+
}
|
|
1279
|
+
});
|
|
1280
|
+
|
|
1281
|
+
// Test API key
|
|
1282
|
+
testBtn.addEventListener('click', async () => {
|
|
1283
|
+
const apiKey = apiKeyInput.value.trim();
|
|
1284
|
+
|
|
1285
|
+
testBtn.disabled = true;
|
|
1286
|
+
testBtn.innerHTML = '<span class="spinner"></span> Testing...';
|
|
1287
|
+
|
|
1288
|
+
try {
|
|
1289
|
+
const res = await fetch('/api/test', {
|
|
1290
|
+
method: 'POST',
|
|
1291
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1292
|
+
body: JSON.stringify({ apiKey: apiKey || undefined }),
|
|
1293
|
+
});
|
|
1294
|
+
const data = await res.json();
|
|
1295
|
+
|
|
1296
|
+
if (data.success) {
|
|
1297
|
+
showToast(data.message, 'success');
|
|
1298
|
+
} else {
|
|
1299
|
+
showToast(data.error, 'error');
|
|
1300
|
+
}
|
|
1301
|
+
} catch (error) {
|
|
1302
|
+
showToast('Test request failed', 'error');
|
|
1303
|
+
} finally {
|
|
1304
|
+
testBtn.disabled = false;
|
|
1305
|
+
testBtn.innerHTML = 'Test Key';
|
|
1306
|
+
}
|
|
1307
|
+
});
|
|
1308
|
+
|
|
1309
|
+
// Save voice
|
|
1310
|
+
saveVoiceBtn.addEventListener('click', async () => {
|
|
1311
|
+
const voiceId = voiceIdInput.value.trim();
|
|
1312
|
+
if (!voiceId) {
|
|
1313
|
+
showToast('Please enter a voice ID', 'error');
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
saveVoiceBtn.disabled = true;
|
|
1318
|
+
saveVoiceBtn.innerHTML = '<span class="spinner"></span> Saving...';
|
|
1319
|
+
|
|
1320
|
+
try {
|
|
1321
|
+
const res = await fetch('/api/config', {
|
|
1322
|
+
method: 'POST',
|
|
1323
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1324
|
+
body: JSON.stringify({ voiceId }),
|
|
1325
|
+
});
|
|
1326
|
+
const data = await res.json();
|
|
1327
|
+
|
|
1328
|
+
if (data.success) {
|
|
1329
|
+
showToast('Voice saved successfully', 'success');
|
|
1330
|
+
// Update selection in voices list if visible
|
|
1331
|
+
updateVoiceSelection(voiceId);
|
|
1332
|
+
}
|
|
1333
|
+
} catch (error) {
|
|
1334
|
+
showToast('Failed to save voice', 'error');
|
|
1335
|
+
} finally {
|
|
1336
|
+
saveVoiceBtn.disabled = false;
|
|
1337
|
+
saveVoiceBtn.innerHTML = 'Save Voice';
|
|
1338
|
+
}
|
|
1339
|
+
});
|
|
1340
|
+
|
|
1341
|
+
// Load voices
|
|
1342
|
+
loadVoicesBtn.addEventListener('click', async () => {
|
|
1343
|
+
loadVoicesBtn.disabled = true;
|
|
1344
|
+
loadVoicesBtn.innerHTML = '<span class="spinner"></span> Loading...';
|
|
1345
|
+
|
|
1346
|
+
try {
|
|
1347
|
+
const res = await fetch('/api/voices', {
|
|
1348
|
+
method: 'POST',
|
|
1349
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1350
|
+
body: JSON.stringify({}),
|
|
1351
|
+
});
|
|
1352
|
+
const data = await res.json();
|
|
1353
|
+
|
|
1354
|
+
if (data.error) {
|
|
1355
|
+
showToast(data.error, 'error');
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
renderVoices(data.voices);
|
|
1360
|
+
voicesList.style.display = 'block';
|
|
1361
|
+
} catch (error) {
|
|
1362
|
+
showToast('Failed to load voices', 'error');
|
|
1363
|
+
} finally {
|
|
1364
|
+
loadVoicesBtn.disabled = false;
|
|
1365
|
+
loadVoicesBtn.innerHTML = 'Load My Voices';
|
|
1366
|
+
}
|
|
1367
|
+
});
|
|
1368
|
+
|
|
1369
|
+
function renderVoices(voices) {
|
|
1370
|
+
const currentVoice = voiceIdInput.value.trim();
|
|
1371
|
+
|
|
1372
|
+
if (!voices.length) {
|
|
1373
|
+
voicesGrid.innerHTML = '<div class="empty-state">No voices found. Add voices in your ElevenLabs dashboard.</div>';
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
voicesGrid.innerHTML = voices.map(v => `
|
|
1378
|
+
<div class="voice-item ${v.id === currentVoice ? 'selected' : ''}" data-id="${v.id}">
|
|
1379
|
+
<div class="voice-info">
|
|
1380
|
+
<span class="voice-name">${escapeHtml(v.name)}</span>
|
|
1381
|
+
<span class="voice-meta">${escapeHtml(v.category || 'custom')} · ${v.id.slice(0, 12)}...</span>
|
|
1382
|
+
</div>
|
|
1383
|
+
<div class="voice-actions">
|
|
1384
|
+
${v.preview_url ? `<button class="btn-icon" title="Preview" onclick="event.stopPropagation(); previewVoice('${v.preview_url}')">▶</button>` : ''}
|
|
1385
|
+
<button class="btn-icon" title="Select" onclick="event.stopPropagation(); selectVoice('${v.id}')">✓</button>
|
|
1386
|
+
</div>
|
|
1387
|
+
</div>
|
|
1388
|
+
`).join('');
|
|
1389
|
+
|
|
1390
|
+
// Click to select
|
|
1391
|
+
voicesGrid.querySelectorAll('.voice-item').forEach(el => {
|
|
1392
|
+
el.addEventListener('click', () => selectVoice(el.dataset.id));
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
function selectVoice(id) {
|
|
1397
|
+
voiceIdInput.value = id;
|
|
1398
|
+
updateVoiceSelection(id);
|
|
1399
|
+
showToast('Voice selected - click Save Voice to apply', 'success');
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
function updateVoiceSelection(id) {
|
|
1403
|
+
voicesGrid.querySelectorAll('.voice-item').forEach(el => {
|
|
1404
|
+
el.classList.toggle('selected', el.dataset.id === id);
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
function previewVoice(url) {
|
|
1409
|
+
if (currentPreviewAudio) {
|
|
1410
|
+
currentPreviewAudio.pause();
|
|
1411
|
+
currentPreviewAudio = null;
|
|
1412
|
+
}
|
|
1413
|
+
currentPreviewAudio = new Audio(url);
|
|
1414
|
+
currentPreviewAudio.play().catch(() => {
|
|
1415
|
+
showToast('Could not play preview', 'error');
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// Save model
|
|
1420
|
+
saveModelBtn.addEventListener('click', async () => {
|
|
1421
|
+
saveModelBtn.disabled = true;
|
|
1422
|
+
saveModelBtn.innerHTML = '<span class="spinner"></span> Saving...';
|
|
1423
|
+
|
|
1424
|
+
try {
|
|
1425
|
+
const res = await fetch('/api/config', {
|
|
1426
|
+
method: 'POST',
|
|
1427
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1428
|
+
body: JSON.stringify({ model: modelSelect.value }),
|
|
1429
|
+
});
|
|
1430
|
+
const data = await res.json();
|
|
1431
|
+
|
|
1432
|
+
if (data.success) {
|
|
1433
|
+
showToast('Model saved successfully', 'success');
|
|
1434
|
+
}
|
|
1435
|
+
} catch (error) {
|
|
1436
|
+
showToast('Failed to save model', 'error');
|
|
1437
|
+
} finally {
|
|
1438
|
+
saveModelBtn.disabled = false;
|
|
1439
|
+
saveModelBtn.innerHTML = 'Save Model';
|
|
1440
|
+
}
|
|
1441
|
+
});
|
|
1442
|
+
|
|
1443
|
+
// Save voice settings
|
|
1444
|
+
saveSettingsBtn.addEventListener('click', async () => {
|
|
1445
|
+
saveSettingsBtn.disabled = true;
|
|
1446
|
+
saveSettingsBtn.innerHTML = '<span class="spinner"></span> Saving...';
|
|
1447
|
+
|
|
1448
|
+
const voiceSettings = {
|
|
1449
|
+
speed: parseFloat(speedSlider.value),
|
|
1450
|
+
stability: parseFloat(stabilitySlider.value),
|
|
1451
|
+
similarityBoost: parseFloat(similarityBoostSlider.value),
|
|
1452
|
+
style: parseFloat(styleSlider.value),
|
|
1453
|
+
};
|
|
1454
|
+
|
|
1455
|
+
try {
|
|
1456
|
+
const res = await fetch('/api/config', {
|
|
1457
|
+
method: 'POST',
|
|
1458
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1459
|
+
body: JSON.stringify({ voiceSettings }),
|
|
1460
|
+
});
|
|
1461
|
+
const data = await res.json();
|
|
1462
|
+
|
|
1463
|
+
if (data.success) {
|
|
1464
|
+
showToast('Voice settings saved successfully', 'success');
|
|
1465
|
+
}
|
|
1466
|
+
} catch (error) {
|
|
1467
|
+
showToast('Failed to save voice settings', 'error');
|
|
1468
|
+
} finally {
|
|
1469
|
+
saveSettingsBtn.disabled = false;
|
|
1470
|
+
saveSettingsBtn.innerHTML = 'Save Voice Settings';
|
|
1471
|
+
}
|
|
1472
|
+
});
|
|
1473
|
+
|
|
1474
|
+
// Reset voice settings to defaults
|
|
1475
|
+
resetSettingsBtn.addEventListener('click', () => {
|
|
1476
|
+
applyPreset('default');
|
|
1477
|
+
});
|
|
1478
|
+
|
|
1479
|
+
// Save auto-submit settings
|
|
1480
|
+
saveAutoSubmitBtn.addEventListener('click', async () => {
|
|
1481
|
+
saveAutoSubmitBtn.disabled = true;
|
|
1482
|
+
saveAutoSubmitBtn.innerHTML = '<span class="spinner"></span> Saving...';
|
|
1483
|
+
|
|
1484
|
+
const autoSubmit = {
|
|
1485
|
+
enabled: autoSubmitEnabled.checked,
|
|
1486
|
+
silenceDelay: parseFloat(silenceDelaySlider.value),
|
|
1487
|
+
minTextLength: parseInt(minTextLengthSlider.value),
|
|
1488
|
+
targetApp: 'Cursor',
|
|
1489
|
+
};
|
|
1490
|
+
|
|
1491
|
+
try {
|
|
1492
|
+
const res = await fetch('/api/config', {
|
|
1493
|
+
method: 'POST',
|
|
1494
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1495
|
+
body: JSON.stringify({ autoSubmit }),
|
|
1496
|
+
});
|
|
1497
|
+
const data = await res.json();
|
|
1498
|
+
|
|
1499
|
+
if (data.success) {
|
|
1500
|
+
showToast('Auto-submit settings saved', 'success');
|
|
1501
|
+
}
|
|
1502
|
+
} catch (error) {
|
|
1503
|
+
showToast('Failed to save auto-submit settings', 'error');
|
|
1504
|
+
} finally {
|
|
1505
|
+
saveAutoSubmitBtn.disabled = false;
|
|
1506
|
+
saveAutoSubmitBtn.innerHTML = 'Save Auto-Submit Settings';
|
|
1507
|
+
}
|
|
1508
|
+
});
|
|
1509
|
+
|
|
1510
|
+
// Toast notification
|
|
1511
|
+
let toastTimeout;
|
|
1512
|
+
function showToast(message, type = 'success') {
|
|
1513
|
+
clearTimeout(toastTimeout);
|
|
1514
|
+
toastEl.textContent = message;
|
|
1515
|
+
toastEl.className = `toast toast-${type} show`;
|
|
1516
|
+
toastTimeout = setTimeout(() => {
|
|
1517
|
+
toastEl.classList.remove('show');
|
|
1518
|
+
}, 3500);
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
function escapeHtml(str) {
|
|
1522
|
+
const div = document.createElement('div');
|
|
1523
|
+
div.textContent = str;
|
|
1524
|
+
return div.innerHTML;
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
// Save wispr loop settings
|
|
1528
|
+
saveWisprLoopBtn.addEventListener('click', async () => {
|
|
1529
|
+
saveWisprLoopBtn.disabled = true;
|
|
1530
|
+
saveWisprLoopBtn.innerHTML = '<span class="spinner"></span> Saving...';
|
|
1531
|
+
|
|
1532
|
+
const wisprLoop = {
|
|
1533
|
+
enabled: wisprLoopEnabled.checked,
|
|
1534
|
+
ttsDelay: parseFloat(ttsDelaySlider.value),
|
|
1535
|
+
silenceThreshold: parseFloat(silenceThresholdSlider.value),
|
|
1536
|
+
silenceDuration: parseFloat(silenceDurationSlider.value),
|
|
1537
|
+
wisprHotkey: wisprHotkeyInput.value.trim(),
|
|
1538
|
+
manualTriggerHotkey: manualTriggerHotkeyInput.value.trim(),
|
|
1539
|
+
};
|
|
1540
|
+
|
|
1541
|
+
try {
|
|
1542
|
+
const res = await fetch('/api/config', {
|
|
1543
|
+
method: 'POST',
|
|
1544
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1545
|
+
body: JSON.stringify({ wisprLoop }),
|
|
1546
|
+
});
|
|
1547
|
+
const data = await res.json();
|
|
1548
|
+
|
|
1549
|
+
if (data.success) {
|
|
1550
|
+
showToast('Wispr loop settings saved', 'success');
|
|
1551
|
+
}
|
|
1552
|
+
} catch (error) {
|
|
1553
|
+
showToast('Failed to save wispr loop settings', 'error');
|
|
1554
|
+
} finally {
|
|
1555
|
+
saveWisprLoopBtn.disabled = false;
|
|
1556
|
+
saveWisprLoopBtn.innerHTML = 'Save Wispr Loop Settings';
|
|
1557
|
+
}
|
|
1558
|
+
});
|
|
1559
|
+
|
|
1560
|
+
// Copy command to clipboard
|
|
1561
|
+
function copyCommand() {
|
|
1562
|
+
const command = 'npm run auto-submit';
|
|
1563
|
+
navigator.clipboard.writeText(command).then(() => {
|
|
1564
|
+
showToast('Command copied to clipboard', 'success');
|
|
1565
|
+
}).catch(() => {
|
|
1566
|
+
showToast('Failed to copy command', 'error');
|
|
1567
|
+
});
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
// Init
|
|
1571
|
+
loadConfig();
|
|
1572
|
+
</script>
|
|
1573
|
+
</body>
|
|
1574
|
+
</html>
|