@mooncompany/uplink-chat 0.5.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.
Potentially problematic release.
This version of @mooncompany/uplink-chat might be problematic. Click here for more details.
- package/LICENSE +21 -0
- package/README.md +185 -0
- package/bin/uplink.js +279 -0
- package/middleware/error-handler.js +69 -0
- package/package.json +93 -0
- package/public/css/agents.36b98c0f.css +1469 -0
- package/public/css/agents.css +1469 -0
- package/public/css/app.a6a7f8f5.css +2731 -0
- package/public/css/app.css +2731 -0
- package/public/css/artifacts.css +444 -0
- package/public/css/commands.css +55 -0
- package/public/css/connection.css +131 -0
- package/public/css/dashboard.css +233 -0
- package/public/css/developer.css +328 -0
- package/public/css/files.css +123 -0
- package/public/css/markdown.css +156 -0
- package/public/css/message-actions.css +278 -0
- package/public/css/mobile.css +614 -0
- package/public/css/panels-unified.css +483 -0
- package/public/css/premium.css +415 -0
- package/public/css/realtime.css +189 -0
- package/public/css/satellites.css +401 -0
- package/public/css/shortcuts.css +185 -0
- package/public/css/split-view.4def0262.css +673 -0
- package/public/css/split-view.css +673 -0
- package/public/css/theme-generator.css +391 -0
- package/public/css/themes.css +387 -0
- package/public/css/timestamps.css +54 -0
- package/public/css/variables.css +78 -0
- package/public/dist/bundle.b55050c4.js +15757 -0
- package/public/favicon.svg +24 -0
- package/public/img/agents/ada.png +0 -0
- package/public/img/agents/clarice.png +0 -0
- package/public/img/agents/dennis-nedry.png +0 -0
- package/public/img/agents/elliot-alderson.png +0 -0
- package/public/img/agents/main.png +0 -0
- package/public/img/agents/scotty.png +0 -0
- package/public/img/agents/top-flight-security.png +0 -0
- package/public/index.html +1083 -0
- package/public/js/agents-data.js +234 -0
- package/public/js/agents-ui.js +72 -0
- package/public/js/agents.js +1525 -0
- package/public/js/app.js +79 -0
- package/public/js/appearance-settings.js +111 -0
- package/public/js/artifacts.js +432 -0
- package/public/js/audio-queue.js +168 -0
- package/public/js/bootstrap.js +54 -0
- package/public/js/chat.js +1211 -0
- package/public/js/commands.js +581 -0
- package/public/js/connection-api.js +121 -0
- package/public/js/connection.js +1231 -0
- package/public/js/context-tracker.js +271 -0
- package/public/js/core.js +172 -0
- package/public/js/dashboard.js +452 -0
- package/public/js/developer.js +432 -0
- package/public/js/encryption.js +124 -0
- package/public/js/errors.js +122 -0
- package/public/js/event-bus.js +77 -0
- package/public/js/fetch-utils.js +171 -0
- package/public/js/file-handler.js +229 -0
- package/public/js/files.js +352 -0
- package/public/js/gateway-chat.js +538 -0
- package/public/js/logger.js +112 -0
- package/public/js/markdown.js +190 -0
- package/public/js/message-actions.js +431 -0
- package/public/js/message-renderer.js +288 -0
- package/public/js/missed-messages.js +235 -0
- package/public/js/mobile-debug.js +95 -0
- package/public/js/notifications.js +367 -0
- package/public/js/offline-queue.js +178 -0
- package/public/js/onboarding.js +543 -0
- package/public/js/panels.js +156 -0
- package/public/js/premium.js +412 -0
- package/public/js/realtime-voice.js +844 -0
- package/public/js/satellite-sync.js +256 -0
- package/public/js/satellite-ui.js +175 -0
- package/public/js/satellites.js +1516 -0
- package/public/js/settings.js +1087 -0
- package/public/js/shortcuts.js +381 -0
- package/public/js/split-chat.js +1234 -0
- package/public/js/split-resize.js +211 -0
- package/public/js/splitview.js +340 -0
- package/public/js/storage.js +408 -0
- package/public/js/streaming-handler.js +324 -0
- package/public/js/stt-settings.js +316 -0
- package/public/js/theme-generator.js +661 -0
- package/public/js/themes.js +164 -0
- package/public/js/timestamps.js +198 -0
- package/public/js/tts-settings.js +575 -0
- package/public/js/ui.js +267 -0
- package/public/js/update-notifier.js +143 -0
- package/public/js/utils/constants.js +165 -0
- package/public/js/utils/sanitize.js +93 -0
- package/public/js/utils/sse-parser.js +195 -0
- package/public/js/voice.js +883 -0
- package/public/manifest.json +58 -0
- package/public/moon_texture.jpg +0 -0
- package/public/sw.js +221 -0
- package/public/three.min.js +6 -0
- package/server/channel.js +529 -0
- package/server/chat.js +270 -0
- package/server/config-store.js +362 -0
- package/server/config.js +159 -0
- package/server/context.js +131 -0
- package/server/gateway-commands.js +211 -0
- package/server/gateway-proxy.js +318 -0
- package/server/index.js +22 -0
- package/server/logger.js +89 -0
- package/server/middleware/auth.js +188 -0
- package/server/middleware.js +218 -0
- package/server/openclaw-discover.js +308 -0
- package/server/premium/index.js +156 -0
- package/server/premium/license.js +140 -0
- package/server/realtime/bridge.js +837 -0
- package/server/realtime/index.js +349 -0
- package/server/realtime/tts-stream.js +446 -0
- package/server/routes/agents.js +564 -0
- package/server/routes/artifacts.js +174 -0
- package/server/routes/chat.js +311 -0
- package/server/routes/config-settings.js +345 -0
- package/server/routes/config.js +603 -0
- package/server/routes/files.js +307 -0
- package/server/routes/index.js +18 -0
- package/server/routes/media.js +451 -0
- package/server/routes/missed-messages.js +107 -0
- package/server/routes/premium.js +75 -0
- package/server/routes/push.js +156 -0
- package/server/routes/satellite.js +406 -0
- package/server/routes/status.js +251 -0
- package/server/routes/stt.js +35 -0
- package/server/routes/voice.js +260 -0
- package/server/routes/webhooks.js +203 -0
- package/server/routes.js +206 -0
- package/server/runtime-config.js +336 -0
- package/server/share.js +305 -0
- package/server/stt/faster-whisper.js +72 -0
- package/server/stt/groq.js +51 -0
- package/server/stt/index.js +196 -0
- package/server/stt/openai.js +49 -0
- package/server/sync.js +244 -0
- package/server/tailscale-https.js +175 -0
- package/server/tts.js +646 -0
- package/server/update-checker.js +172 -0
- package/server/utils/filename.js +129 -0
- package/server/utils.js +147 -0
- package/server/watchdog.js +318 -0
- package/server/websocket/broadcast.js +359 -0
- package/server/websocket/connections.js +339 -0
- package/server/websocket/index.js +215 -0
- package/server/websocket/routing.js +277 -0
- package/server/websocket/sync.js +102 -0
- package/server.js +404 -0
- package/utils/detect-tool-usage.js +93 -0
- package/utils/errors.js +158 -0
- package/utils/html-escape.js +84 -0
- package/utils/id-sanitize.js +94 -0
- package/utils/response.js +130 -0
- package/utils/with-retry.js +105 -0
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
// ============================================
|
|
2
|
+
// TTS SETTINGS MODULE
|
|
3
|
+
// Text-to-Speech configuration and voice selection
|
|
4
|
+
// ============================================
|
|
5
|
+
|
|
6
|
+
// DOM elements
|
|
7
|
+
let ttsProviderSelect, ttsProviderDesc;
|
|
8
|
+
let audioToggle;
|
|
9
|
+
let elevenLabsKeyInput, elevenLabsSaveBtn, elevenLabsKeyStatus;
|
|
10
|
+
let elevenLabsVoiceRow, elevenLabsVoiceSelect;
|
|
11
|
+
let openaiKeyInput, openaiKeySaveBtn, openaiKeyStatus;
|
|
12
|
+
let openaiTtsVoiceSelect, openaiTtsModelSelect;
|
|
13
|
+
let edgeTtsVoiceSelect;
|
|
14
|
+
let localTtsUrlInput, localTtsSaveBtn;
|
|
15
|
+
|
|
16
|
+
function init() {
|
|
17
|
+
ttsProviderSelect = document.getElementById('ttsProviderSelect');
|
|
18
|
+
ttsProviderDesc = document.getElementById('ttsProviderDesc');
|
|
19
|
+
audioToggle = document.getElementById('audioToggle');
|
|
20
|
+
elevenLabsKeyInput = document.getElementById('elevenLabsKeyInput');
|
|
21
|
+
elevenLabsSaveBtn = document.getElementById('elevenLabsSaveBtn');
|
|
22
|
+
elevenLabsKeyStatus = document.getElementById('elevenLabsKeyStatus');
|
|
23
|
+
elevenLabsVoiceRow = document.getElementById('elevenLabsVoiceRow');
|
|
24
|
+
elevenLabsVoiceSelect = document.getElementById('elevenLabsVoiceSelect');
|
|
25
|
+
openaiKeyInput = document.getElementById('openaiKeyInput');
|
|
26
|
+
openaiKeySaveBtn = document.getElementById('openaiKeySaveBtn');
|
|
27
|
+
openaiKeyStatus = document.getElementById('openaiKeyStatus');
|
|
28
|
+
openaiTtsVoiceSelect = document.getElementById('openaiTtsVoiceSelect');
|
|
29
|
+
openaiTtsModelSelect = document.getElementById('openaiTtsModelSelect');
|
|
30
|
+
edgeTtsVoiceSelect = document.getElementById('edgeTtsVoiceSelect');
|
|
31
|
+
localTtsUrlInput = document.getElementById('localTtsUrlInput');
|
|
32
|
+
localTtsSaveBtn = document.getElementById('localTtsSaveBtn');
|
|
33
|
+
|
|
34
|
+
setupEvents();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function setupEvents() {
|
|
38
|
+
// Audio toggle
|
|
39
|
+
audioToggle?.addEventListener('click', () => {
|
|
40
|
+
const core = window.UplinkCore;
|
|
41
|
+
if (core) {
|
|
42
|
+
core.audioResponses = !core.audioResponses;
|
|
43
|
+
audioToggle.classList.toggle('on', core.audioResponses);
|
|
44
|
+
audioToggle.setAttribute('aria-checked', core.audioResponses ? 'true' : 'false');
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Audio toggle keyboard handler
|
|
49
|
+
audioToggle?.addEventListener('keydown', (e) => {
|
|
50
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
51
|
+
e.preventDefault();
|
|
52
|
+
audioToggle.click();
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// TTS Provider change
|
|
57
|
+
ttsProviderSelect?.addEventListener('change', async () => {
|
|
58
|
+
const newProvider = ttsProviderSelect.value;
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const response = await fetch('/api/config', {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers: { 'Content-Type': 'application/json' },
|
|
64
|
+
body: JSON.stringify({ ttsProvider: newProvider })
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
throw new Error(`HTTP ${response.status}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Update description based on provider
|
|
72
|
+
if (ttsProviderDesc) {
|
|
73
|
+
const descriptions = {
|
|
74
|
+
'none': 'Voice synthesis disabled',
|
|
75
|
+
'elevenlabs': 'High-quality cloud TTS (requires API key)',
|
|
76
|
+
'openai': 'Cloud TTS (uses your OpenAI API key)',
|
|
77
|
+
'local': 'GPU-accelerated local TTS (custom voice)',
|
|
78
|
+
'edge': 'Free Microsoft TTS (no API key needed)',
|
|
79
|
+
'piper': 'Fast local TTS (requires piper install)',
|
|
80
|
+
};
|
|
81
|
+
ttsProviderDesc.textContent = descriptions[newProvider] || 'Voice synthesis service';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Show config panel for the selected provider
|
|
85
|
+
updateProviderConfigVisibility(newProvider);
|
|
86
|
+
|
|
87
|
+
showToast('TTS provider updated', 'success');
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error('Failed to update TTS provider:', error);
|
|
90
|
+
showToast('Failed to update TTS provider', 'error');
|
|
91
|
+
// Revert selection
|
|
92
|
+
fetchServerConfig();
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// ElevenLabs API key save
|
|
97
|
+
elevenLabsSaveBtn?.addEventListener('click', saveElevenLabsKey);
|
|
98
|
+
elevenLabsKeyInput?.addEventListener('keypress', (e) => {
|
|
99
|
+
if (e.key === 'Enter') saveElevenLabsKey();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// ElevenLabs voice selection
|
|
103
|
+
elevenLabsVoiceSelect?.addEventListener('change', async () => {
|
|
104
|
+
const voiceId = elevenLabsVoiceSelect.value;
|
|
105
|
+
const voiceName = elevenLabsVoiceSelect.options[elevenLabsVoiceSelect.selectedIndex]?.text;
|
|
106
|
+
|
|
107
|
+
if (!voiceId) return;
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const response = await fetch('/api/config/elevenlabs-voice', {
|
|
111
|
+
method: 'POST',
|
|
112
|
+
headers: { 'Content-Type': 'application/json' },
|
|
113
|
+
body: JSON.stringify({ voiceId, voiceName })
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
if (response.ok) {
|
|
117
|
+
showToast(`Voice set to ${voiceName}`, 'success');
|
|
118
|
+
} else {
|
|
119
|
+
throw new Error('Failed to save voice');
|
|
120
|
+
}
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.error('Failed to set voice:', error);
|
|
123
|
+
showToast('Failed to set voice', 'error');
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// OpenAI API key save
|
|
128
|
+
openaiKeySaveBtn?.addEventListener('click', saveOpenAIKey);
|
|
129
|
+
openaiKeyInput?.addEventListener('keypress', (e) => {
|
|
130
|
+
if (e.key === 'Enter') saveOpenAIKey();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// OpenAI TTS voice/model change
|
|
134
|
+
openaiTtsVoiceSelect?.addEventListener('change', async () => {
|
|
135
|
+
try {
|
|
136
|
+
const response = await fetch('/api/config/openai-tts', {
|
|
137
|
+
method: 'POST',
|
|
138
|
+
headers: { 'Content-Type': 'application/json' },
|
|
139
|
+
body: JSON.stringify({ voice: openaiTtsVoiceSelect.value })
|
|
140
|
+
});
|
|
141
|
+
if (response.ok) {
|
|
142
|
+
showToast(`OpenAI voice set to ${openaiTtsVoiceSelect.value}`, 'success');
|
|
143
|
+
} else {
|
|
144
|
+
const err = await response.json();
|
|
145
|
+
showToast(err.error || 'Failed to save', 'error');
|
|
146
|
+
}
|
|
147
|
+
} catch (e) {
|
|
148
|
+
showToast('Failed to save OpenAI voice', 'error');
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
openaiTtsModelSelect?.addEventListener('change', async () => {
|
|
153
|
+
try {
|
|
154
|
+
const response = await fetch('/api/config/openai-tts', {
|
|
155
|
+
method: 'POST',
|
|
156
|
+
headers: { 'Content-Type': 'application/json' },
|
|
157
|
+
body: JSON.stringify({ model: openaiTtsModelSelect.value })
|
|
158
|
+
});
|
|
159
|
+
if (response.ok) {
|
|
160
|
+
const labels = { 'tts-1': 'Standard', 'tts-1-hd': 'HD', 'gpt-4o-mini-tts': 'GPT-4o Mini' };
|
|
161
|
+
showToast(`OpenAI model set to ${labels[openaiTtsModelSelect.value] || openaiTtsModelSelect.value}`, 'success');
|
|
162
|
+
} else {
|
|
163
|
+
const err = await response.json();
|
|
164
|
+
showToast(err.error || 'Failed to save', 'error');
|
|
165
|
+
}
|
|
166
|
+
} catch (e) {
|
|
167
|
+
showToast('Failed to save OpenAI model', 'error');
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Edge TTS voice change
|
|
172
|
+
edgeTtsVoiceSelect?.addEventListener('change', async () => {
|
|
173
|
+
const voice = edgeTtsVoiceSelect.value;
|
|
174
|
+
if (!voice) return;
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
const response = await fetch('/api/config/edge-voice', {
|
|
178
|
+
method: 'POST',
|
|
179
|
+
headers: { 'Content-Type': 'application/json' },
|
|
180
|
+
body: JSON.stringify({ voice })
|
|
181
|
+
});
|
|
182
|
+
if (response.ok) {
|
|
183
|
+
showToast(`Edge voice set to ${edgeTtsVoiceSelect.options[edgeTtsVoiceSelect.selectedIndex]?.text || voice}`, 'success');
|
|
184
|
+
} else {
|
|
185
|
+
showToast('Failed to save Edge voice', 'error');
|
|
186
|
+
}
|
|
187
|
+
} catch (e) {
|
|
188
|
+
showToast('Failed to save Edge voice', 'error');
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// XTTS URL save
|
|
193
|
+
localTtsSaveBtn?.addEventListener('click', async () => {
|
|
194
|
+
const url = localTtsUrlInput?.value.trim();
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
const response = await fetch('/api/config/local-tts', {
|
|
198
|
+
method: 'POST',
|
|
199
|
+
headers: { 'Content-Type': 'application/json' },
|
|
200
|
+
body: JSON.stringify({ url: url || '' })
|
|
201
|
+
});
|
|
202
|
+
if (response.ok) {
|
|
203
|
+
showToast(url ? 'XTTS server URL saved' : 'XTTS URL cleared', 'success');
|
|
204
|
+
} else {
|
|
205
|
+
const err = await response.json();
|
|
206
|
+
showToast(err.error || 'Failed to save', 'error');
|
|
207
|
+
}
|
|
208
|
+
} catch (e) {
|
|
209
|
+
showToast('Failed to save XTTS URL', 'error');
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Show config panel for the active provider, hide all others
|
|
216
|
+
*/
|
|
217
|
+
function updateProviderConfigVisibility(provider) {
|
|
218
|
+
const settingsPanel = document.getElementById('settingsPanel');
|
|
219
|
+
|
|
220
|
+
// Hide all provider config panels
|
|
221
|
+
const allConfigs = settingsPanel?.querySelectorAll('.tts-provider-config') || [];
|
|
222
|
+
allConfigs.forEach(el => { el.style.display = 'none'; });
|
|
223
|
+
|
|
224
|
+
// Show the active provider's config
|
|
225
|
+
const activeConfig = document.getElementById(`ttsConfig-${provider}`);
|
|
226
|
+
if (activeConfig) {
|
|
227
|
+
activeConfig.style.display = 'block';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Provider-specific init
|
|
231
|
+
if (provider === 'elevenlabs') {
|
|
232
|
+
loadElevenLabsVoices();
|
|
233
|
+
} else if (provider === 'edge') {
|
|
234
|
+
loadEdgeTTSVoices();
|
|
235
|
+
} else if (provider === 'openai') {
|
|
236
|
+
loadOpenAITTSStatus();
|
|
237
|
+
} else if (provider === 'piper') {
|
|
238
|
+
loadPiperStatus();
|
|
239
|
+
} else if (provider === 'local') {
|
|
240
|
+
loadXTTSStatus();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function saveElevenLabsKey() {
|
|
245
|
+
const apiKey = elevenLabsKeyInput?.value.trim();
|
|
246
|
+
|
|
247
|
+
if (!apiKey) {
|
|
248
|
+
if (elevenLabsKeyStatus) {
|
|
249
|
+
elevenLabsKeyStatus.textContent = 'Please enter an API key';
|
|
250
|
+
elevenLabsKeyStatus.style.color = 'var(--error-color, #ef4444)';
|
|
251
|
+
}
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (elevenLabsKeyStatus) {
|
|
256
|
+
elevenLabsKeyStatus.textContent = 'Validating...';
|
|
257
|
+
elevenLabsKeyStatus.style.color = 'var(--text-muted)';
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
const response = await fetch('/api/config/elevenlabs-key', {
|
|
262
|
+
method: 'POST',
|
|
263
|
+
headers: { 'Content-Type': 'application/json' },
|
|
264
|
+
body: JSON.stringify({ apiKey })
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const data = await response.json();
|
|
268
|
+
|
|
269
|
+
if (data.valid) {
|
|
270
|
+
if (elevenLabsKeyStatus) {
|
|
271
|
+
elevenLabsKeyStatus.textContent = `✓ Key saved (${data.subscription} tier)`;
|
|
272
|
+
elevenLabsKeyStatus.style.color = 'var(--success-color, #4ade80)';
|
|
273
|
+
}
|
|
274
|
+
if (elevenLabsKeyInput) {
|
|
275
|
+
elevenLabsKeyInput.value = ''; // Clear input after save
|
|
276
|
+
elevenLabsKeyInput.placeholder = '••••••••••••••••';
|
|
277
|
+
}
|
|
278
|
+
showToast('ElevenLabs API key saved', 'success');
|
|
279
|
+
|
|
280
|
+
// Load available voices
|
|
281
|
+
loadElevenLabsVoices();
|
|
282
|
+
} else {
|
|
283
|
+
if (elevenLabsKeyStatus) {
|
|
284
|
+
elevenLabsKeyStatus.textContent = data.error || 'Invalid API key';
|
|
285
|
+
elevenLabsKeyStatus.style.color = 'var(--error-color, #ef4444)';
|
|
286
|
+
}
|
|
287
|
+
showToast('Invalid API key', 'error');
|
|
288
|
+
}
|
|
289
|
+
} catch (error) {
|
|
290
|
+
console.error('Failed to save ElevenLabs key:', error);
|
|
291
|
+
if (elevenLabsKeyStatus) {
|
|
292
|
+
elevenLabsKeyStatus.textContent = 'Failed to validate key';
|
|
293
|
+
elevenLabsKeyStatus.style.color = 'var(--error-color, #ef4444)';
|
|
294
|
+
}
|
|
295
|
+
showToast('Failed to save API key', 'error');
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function loadElevenLabsVoices() {
|
|
300
|
+
if (!elevenLabsVoiceSelect) return;
|
|
301
|
+
|
|
302
|
+
elevenLabsVoiceSelect.innerHTML = '<option value="">Loading voices...</option>';
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
const response = await fetch('/api/config/elevenlabs-voices');
|
|
306
|
+
const data = await response.json();
|
|
307
|
+
|
|
308
|
+
if (data.error || !data.voices?.length) {
|
|
309
|
+
elevenLabsVoiceSelect.innerHTML = '<option value="">No voices available</option>';
|
|
310
|
+
if (elevenLabsVoiceRow) elevenLabsVoiceRow.style.display = 'none';
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Get current voice from config
|
|
315
|
+
const configRes = await fetch('/api/config');
|
|
316
|
+
const config = await configRes.json();
|
|
317
|
+
const currentVoiceId = config.elevenLabsVoiceId;
|
|
318
|
+
|
|
319
|
+
// Build options
|
|
320
|
+
elevenLabsVoiceSelect.innerHTML = data.voices.map(v =>
|
|
321
|
+
`<option value="${v.id}" ${v.id === currentVoiceId ? 'selected' : ''}>${v.name}</option>`
|
|
322
|
+
).join('');
|
|
323
|
+
|
|
324
|
+
if (elevenLabsVoiceRow) elevenLabsVoiceRow.style.display = 'flex';
|
|
325
|
+
} catch (error) {
|
|
326
|
+
console.error('Failed to load voices:', error);
|
|
327
|
+
elevenLabsVoiceSelect.innerHTML = '<option value="">Failed to load voices</option>';
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async function loadOpenAITTSStatus() {
|
|
332
|
+
try {
|
|
333
|
+
const res = await fetch('/api/config/tts-status');
|
|
334
|
+
const data = await res.json();
|
|
335
|
+
const info = data.openai;
|
|
336
|
+
|
|
337
|
+
if (info?.hasKey) {
|
|
338
|
+
if (openaiKeyStatus) {
|
|
339
|
+
openaiKeyStatus.textContent = '✓ API key configured';
|
|
340
|
+
openaiKeyStatus.style.color = 'var(--success-color, #4ade80)';
|
|
341
|
+
}
|
|
342
|
+
if (openaiKeyInput) openaiKeyInput.placeholder = '••••••••••••••••';
|
|
343
|
+
} else {
|
|
344
|
+
if (openaiKeyStatus) {
|
|
345
|
+
openaiKeyStatus.textContent = 'Enter your OpenAI API key';
|
|
346
|
+
openaiKeyStatus.style.color = '';
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Sync dropdowns with current config
|
|
351
|
+
if (openaiTtsVoiceSelect && info?.voice) {
|
|
352
|
+
openaiTtsVoiceSelect.value = info.voice;
|
|
353
|
+
}
|
|
354
|
+
if (openaiTtsModelSelect && info?.model) {
|
|
355
|
+
openaiTtsModelSelect.value = info.model;
|
|
356
|
+
}
|
|
357
|
+
} catch (e) {
|
|
358
|
+
console.error('Failed to load OpenAI TTS status:', e);
|
|
359
|
+
if (openaiKeyStatus) {
|
|
360
|
+
openaiKeyStatus.textContent = 'Failed to check status';
|
|
361
|
+
openaiKeyStatus.style.color = 'var(--error-color, #ef4444)';
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function saveOpenAIKey() {
|
|
367
|
+
const apiKey = openaiKeyInput?.value.trim();
|
|
368
|
+
|
|
369
|
+
if (!apiKey) {
|
|
370
|
+
if (openaiKeyStatus) {
|
|
371
|
+
openaiKeyStatus.textContent = 'Please enter an API key';
|
|
372
|
+
openaiKeyStatus.style.color = 'var(--error-color, #ef4444)';
|
|
373
|
+
}
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (openaiKeyStatus) {
|
|
378
|
+
openaiKeyStatus.textContent = 'Validating...';
|
|
379
|
+
openaiKeyStatus.style.color = 'var(--text-muted)';
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
try {
|
|
383
|
+
const response = await fetch('/api/config/openai-key', {
|
|
384
|
+
method: 'POST',
|
|
385
|
+
headers: { 'Content-Type': 'application/json' },
|
|
386
|
+
body: JSON.stringify({ apiKey })
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
const data = await response.json();
|
|
390
|
+
|
|
391
|
+
if (data.valid) {
|
|
392
|
+
if (openaiKeyStatus) {
|
|
393
|
+
openaiKeyStatus.textContent = '✓ API key saved';
|
|
394
|
+
openaiKeyStatus.style.color = 'var(--success-color, #4ade80)';
|
|
395
|
+
}
|
|
396
|
+
if (openaiKeyInput) {
|
|
397
|
+
openaiKeyInput.value = '';
|
|
398
|
+
openaiKeyInput.placeholder = '••••••••••••••••';
|
|
399
|
+
}
|
|
400
|
+
showToast('OpenAI API key saved', 'success');
|
|
401
|
+
} else {
|
|
402
|
+
if (openaiKeyStatus) {
|
|
403
|
+
openaiKeyStatus.textContent = data.error || 'Invalid API key';
|
|
404
|
+
openaiKeyStatus.style.color = 'var(--error-color, #ef4444)';
|
|
405
|
+
}
|
|
406
|
+
showToast('Invalid API key', 'error');
|
|
407
|
+
}
|
|
408
|
+
} catch (error) {
|
|
409
|
+
console.error('Failed to save OpenAI key:', error);
|
|
410
|
+
if (openaiKeyStatus) {
|
|
411
|
+
openaiKeyStatus.textContent = 'Failed to validate key';
|
|
412
|
+
openaiKeyStatus.style.color = 'var(--error-color, #ef4444)';
|
|
413
|
+
}
|
|
414
|
+
showToast('Failed to save API key', 'error');
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async function loadEdgeTTSVoices() {
|
|
419
|
+
const statusDesc = document.getElementById('edgeTtsStatusDesc');
|
|
420
|
+
const statusDot = document.getElementById('edgeTtsStatusDot');
|
|
421
|
+
const voiceRow = document.getElementById('edgeTtsVoiceRow');
|
|
422
|
+
|
|
423
|
+
try {
|
|
424
|
+
const res = await fetch('/api/config/tts-status');
|
|
425
|
+
const data = await res.json();
|
|
426
|
+
const info = data.edge;
|
|
427
|
+
|
|
428
|
+
if (!info?.installed) {
|
|
429
|
+
if (statusDesc) statusDesc.textContent = 'Not installed — run: npm install node-edge-tts';
|
|
430
|
+
if (statusDot) statusDot.className = 'status-indicator disconnected';
|
|
431
|
+
if (voiceRow) voiceRow.style.display = 'none';
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (statusDesc) statusDesc.textContent = 'Installed — free, no API key needed';
|
|
436
|
+
if (statusDot) statusDot.className = 'status-indicator connected';
|
|
437
|
+
|
|
438
|
+
// Load voice list
|
|
439
|
+
if (edgeTtsVoiceSelect) {
|
|
440
|
+
edgeTtsVoiceSelect.innerHTML = '<option value="">Loading voices...</option>';
|
|
441
|
+
|
|
442
|
+
const voicesRes = await fetch('/api/config/edge-voices');
|
|
443
|
+
const voicesData = await voicesRes.json();
|
|
444
|
+
|
|
445
|
+
if (voicesData.voices?.length) {
|
|
446
|
+
edgeTtsVoiceSelect.innerHTML = voicesData.voices.map(v =>
|
|
447
|
+
`<option value="${v.shortName}" ${v.shortName === info.voice ? 'selected' : ''}>${v.name} (${v.gender})</option>`
|
|
448
|
+
).join('');
|
|
449
|
+
if (voiceRow) voiceRow.style.display = 'flex';
|
|
450
|
+
} else {
|
|
451
|
+
edgeTtsVoiceSelect.innerHTML = '<option value="">No voices available</option>';
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
} catch (e) {
|
|
455
|
+
console.error('Failed to load Edge TTS voices:', e);
|
|
456
|
+
if (statusDesc) statusDesc.textContent = 'Failed to check status';
|
|
457
|
+
if (statusDot) statusDot.className = 'status-indicator disconnected';
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
async function loadPiperStatus() {
|
|
462
|
+
const statusDesc = document.getElementById('piperStatusDesc');
|
|
463
|
+
const statusDot = document.getElementById('piperStatusDot');
|
|
464
|
+
|
|
465
|
+
try {
|
|
466
|
+
const res = await fetch('/api/config/tts-status');
|
|
467
|
+
const data = await res.json();
|
|
468
|
+
const info = data.piper;
|
|
469
|
+
|
|
470
|
+
if (info?.configured) {
|
|
471
|
+
if (statusDesc) statusDesc.textContent = 'Configured via environment (PIPER_MODEL)';
|
|
472
|
+
if (statusDot) statusDot.className = 'status-indicator connected';
|
|
473
|
+
} else {
|
|
474
|
+
if (statusDesc) statusDesc.textContent = 'Not configured — set PIPER_MODEL in .env';
|
|
475
|
+
if (statusDot) statusDot.className = 'status-indicator disconnected';
|
|
476
|
+
}
|
|
477
|
+
} catch (e) {
|
|
478
|
+
console.error('Failed to load Piper status:', e);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async function loadXTTSStatus() {
|
|
483
|
+
try {
|
|
484
|
+
const res = await fetch('/api/config/tts-status');
|
|
485
|
+
const data = await res.json();
|
|
486
|
+
const info = data.local;
|
|
487
|
+
|
|
488
|
+
if (localTtsUrlInput && info?.url) {
|
|
489
|
+
localTtsUrlInput.value = info.url;
|
|
490
|
+
}
|
|
491
|
+
} catch (e) {
|
|
492
|
+
console.error('Failed to load XTTS status:', e);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
async function fetchServerConfig() {
|
|
497
|
+
try {
|
|
498
|
+
const response = await fetch('/api/config');
|
|
499
|
+
if (!response.ok) return;
|
|
500
|
+
|
|
501
|
+
const config = await response.json();
|
|
502
|
+
|
|
503
|
+
if (ttsProviderSelect && config.ttsProvider) {
|
|
504
|
+
ttsProviderSelect.value = config.ttsProvider;
|
|
505
|
+
|
|
506
|
+
// Enable/disable Edge TTS based on availability
|
|
507
|
+
const edgeOption = document.getElementById('edgeTtsOption');
|
|
508
|
+
if (edgeOption) {
|
|
509
|
+
if (config.edgeTtsAvailable) {
|
|
510
|
+
edgeOption.disabled = false;
|
|
511
|
+
edgeOption.textContent = 'Edge TTS (Free)';
|
|
512
|
+
} else {
|
|
513
|
+
edgeOption.disabled = true;
|
|
514
|
+
edgeOption.textContent = 'Edge TTS (not installed)';
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Update description
|
|
519
|
+
if (ttsProviderDesc) {
|
|
520
|
+
const descriptions = {
|
|
521
|
+
'none': 'Voice synthesis disabled',
|
|
522
|
+
'elevenlabs': 'High-quality cloud TTS (requires API key)',
|
|
523
|
+
'openai': 'OpenAI TTS (requires API key)',
|
|
524
|
+
'local': 'GPU-accelerated local TTS (custom voice)',
|
|
525
|
+
'edge': 'Free Microsoft TTS (user-installed)',
|
|
526
|
+
'piper': 'Fast local TTS (user-installed)'
|
|
527
|
+
};
|
|
528
|
+
ttsProviderDesc.textContent = descriptions[config.ttsProvider] || 'Voice synthesis service';
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Show config panel for active provider
|
|
532
|
+
updateProviderConfigVisibility(config.ttsProvider);
|
|
533
|
+
|
|
534
|
+
// Update ElevenLabs key status
|
|
535
|
+
if (config.hasElevenLabsKey && elevenLabsKeyStatus) {
|
|
536
|
+
elevenLabsKeyStatus.textContent = '✓ API key configured';
|
|
537
|
+
elevenLabsKeyStatus.style.color = 'var(--success-color, #4ade80)';
|
|
538
|
+
if (elevenLabsKeyInput) elevenLabsKeyInput.placeholder = '••••••••••••••••';
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
} catch (error) {
|
|
542
|
+
console.warn('TTS Settings: Failed to fetch server config', error);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function showToast(message, type = 'success') {
|
|
547
|
+
if (window.UplinkSettings?.showToast) {
|
|
548
|
+
window.UplinkSettings.showToast(message, type);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function applyState() {
|
|
553
|
+
const core = window.UplinkCore;
|
|
554
|
+
if (!core) return;
|
|
555
|
+
|
|
556
|
+
if (audioToggle) {
|
|
557
|
+
audioToggle.classList.toggle('on', core.audioResponses);
|
|
558
|
+
audioToggle.setAttribute('aria-checked', core.audioResponses ? 'true' : 'false');
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
fetchServerConfig();
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Expose API
|
|
565
|
+
export const UplinkTTSSettings = {
|
|
566
|
+
init,
|
|
567
|
+
applyState,
|
|
568
|
+
fetchServerConfig,
|
|
569
|
+
updateProviderConfigVisibility
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
import { UplinkCore } from './core.js';
|
|
573
|
+
|
|
574
|
+
// Backward compat: assign to window
|
|
575
|
+
window.UplinkTTSSettings = UplinkTTSSettings;
|