clay-server 5.1.0-beta.2 → 5.1.0-beta.3
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/bin/cli.js +9 -91
- package/lib/builtin-mates.js +7 -7
- package/lib/daemon-settings.js +11 -0
- package/lib/daemon.js +8 -37
- package/lib/linear-store.js +35 -8
- package/lib/mate-identity-file.js +39 -0
- package/lib/mate-knowledge-sync.js +1 -1
- package/lib/mate-ready-creation.js +2 -1
- package/lib/mates-identity.js +4 -4
- package/lib/mates-prompts.js +4 -3
- package/lib/mates.js +12 -11
- package/lib/os-users.js +5 -16
- package/lib/project-connection.js +0 -7
- package/lib/project-debate.js +3 -2
- package/lib/project-filesystem.js +21 -39
- package/lib/project-mate-interaction.js +7 -6
- package/lib/project-server-settings.js +31 -0
- package/lib/project-sessions.js +57 -184
- package/lib/project.js +3 -3
- package/lib/public/app.js +6 -37
- package/lib/public/css/input.css +3 -1
- package/lib/public/css/menus.css +3 -0
- package/lib/public/css/overlays.css +7 -0
- package/lib/public/css/project-settings-modal.css +56 -0
- package/lib/public/css/server-settings-system.css +125 -0
- package/lib/public/css/session-github.css +25 -16
- package/lib/public/css/settings-ui.css +247 -22
- package/lib/public/css/shared-browser.css +7 -3
- package/lib/public/css/title-bar.css +5 -1
- package/lib/public/index.html +173 -447
- package/lib/public/modules/account-island.js +9 -7
- package/lib/public/modules/admin-settings-email.js +197 -0
- package/lib/public/modules/admin-settings-invitations.js +183 -0
- package/lib/public/modules/admin-settings-projects.js +198 -0
- package/lib/public/modules/admin-settings-ui.js +100 -0
- package/lib/public/modules/admin-settings-users.js +331 -0
- package/lib/public/modules/admin.js +19 -949
- package/lib/public/modules/app-dm.js +0 -2
- package/lib/public/modules/app-messages.js +13 -52
- package/lib/public/modules/app-rate-limit.js +70 -12
- package/lib/public/modules/avatar.js +6 -1
- package/lib/public/modules/context-sources.js +27 -18
- package/lib/public/modules/default-ai.js +14 -7
- package/lib/public/modules/header-tui-font.js +10 -20
- package/lib/public/modules/home-mate-avatar-editor.js +2 -2
- package/lib/public/modules/linear-settings.js +3 -2
- package/lib/public/modules/markdown.js +5 -6
- package/lib/public/modules/notification-settings.js +81 -0
- package/lib/public/modules/notifications.js +44 -28
- package/lib/public/modules/personal-settings-dialog.js +51 -0
- package/lib/public/modules/profile.js +15 -6
- package/lib/public/modules/project-settings-environment.js +106 -0
- package/lib/public/modules/project-settings.js +72 -628
- package/lib/public/modules/server-settings-controls.js +316 -0
- package/lib/public/modules/server-settings-environment.js +194 -0
- package/lib/public/modules/server-settings-navigation.js +39 -0
- package/lib/public/modules/server-settings-state.js +54 -0
- package/lib/public/modules/server-settings.js +116 -788
- package/lib/public/modules/server-storage-settings.js +84 -0
- package/lib/public/modules/session-github.js +2 -1
- package/lib/public/modules/session-presence-ui.js +1 -1
- package/lib/public/modules/settings-env-values.js +34 -0
- package/lib/public/modules/settings-select.js +121 -0
- package/lib/public/modules/shared-browser.js +22 -12
- package/lib/public/modules/stt-picker.js +14 -5
- package/lib/public/modules/terminal-prefs.js +22 -0
- package/lib/public/modules/theme.js +88 -52
- package/lib/public/modules/user-settings-account.js +318 -0
- package/lib/public/modules/user-settings-appearance.js +55 -0
- package/lib/public/modules/user-settings-editors.js +24 -0
- package/lib/public/modules/user-settings-integrations.js +104 -0
- package/lib/public/modules/user-settings-layout.js +80 -0
- package/lib/public/modules/user-settings-preferences.js +48 -0
- package/lib/public/modules/user-settings-profile.js +53 -0
- package/lib/public/modules/user-settings-themes.js +38 -0
- package/lib/public/modules/user-settings-voice.js +75 -0
- package/lib/public/modules/user-settings.js +53 -480
- package/lib/public/style.css +4 -0
- package/lib/sdk-message-processor.js +16 -4
- package/lib/sdk-quota-snapshot.js +37 -0
- package/lib/server-appearance-settings.js +71 -0
- package/lib/server-linear.js +14 -8
- package/lib/server-mate-instructions.js +2 -2
- package/lib/server-mates.js +2 -1
- package/lib/server-notification-settings.js +67 -0
- package/lib/server-settings.js +4 -0
- package/lib/server-storage.js +57 -0
- package/lib/server.js +10 -2
- package/lib/sessions.js +3 -4
- package/lib/startup-projects.js +40 -0
- package/lib/storage-native-walk.js +85 -0
- package/lib/storage-usage.js +144 -0
- package/lib/users-preferences.js +0 -74
- package/lib/users.js +0 -4
- package/lib/ws-schema.js +2 -4
- package/lib/yoke/acp-process-manager.js +2 -2
- package/lib/yoke/adapters/codex.js +12 -52
- package/lib/yoke/codex-app-server.js +2 -2
- package/lib/yoke/codex-model-catalog.js +52 -0
- package/lib/yoke/codex-rate-limits.js +26 -0
- package/package.json +3 -3
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { getWs } from './ws-ref.js';
|
|
3
|
+
import { getChatLayout, setChatLayout } from './theme.js';
|
|
4
|
+
|
|
5
|
+
export function preferenceStatus(id, text, error) {
|
|
6
|
+
var control = document.getElementById(id); if (!control) return;
|
|
7
|
+
var status = document.getElementById(id + '-receipt');
|
|
8
|
+
if (!status) {status = document.createElement('p');status.id=id+'-receipt';status.className='settings-status';status.setAttribute('role','status');control.closest('.settings-card').appendChild(status);}
|
|
9
|
+
status.textContent=text;status.dataset.state=error?'error':'';
|
|
10
|
+
}
|
|
11
|
+
async function saveToggle(id, url, apply) {
|
|
12
|
+
var input=document.getElementById(id);var want=input.checked;
|
|
13
|
+
input.disabled=true;preferenceStatus(id,'Saving…');
|
|
14
|
+
try {
|
|
15
|
+
var response=await fetch(url,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({enabled:want})});var data=await response.json();
|
|
16
|
+
if(!response.ok || !data.ok)throw new Error(data.error || 'Could not save.');
|
|
17
|
+
if(apply)apply(want);preferenceStatus(id,'Saved.');
|
|
18
|
+
} catch(error) {input.checked=!want;preferenceStatus(id,error.message+' Try again.',true);}
|
|
19
|
+
finally {input.disabled=false;}
|
|
20
|
+
}
|
|
21
|
+
export function initPersonalPreferences() {
|
|
22
|
+
document.getElementById('us-auto-continue').onchange=function(){saveToggle(this.id,'/api/user/auto-continue');};
|
|
23
|
+
|
|
24
|
+
document.querySelectorAll('#us-layout-switcher button').forEach(function(button){button.onclick=async function(){
|
|
25
|
+
var layout=button.dataset.layout;var controls=button.parentElement.querySelectorAll('button');controls.forEach(function(el){el.disabled=true;});preferenceStatus('us-layout-switcher','Saving…');
|
|
26
|
+
try {var response=await fetch('/api/user/chat-layout',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({layout:layout})});if(!response.ok)throw new Error('Could not save layout.');setChatLayout(layout);controls.forEach(function(el){el.classList.toggle('selected',el.dataset.layout===layout);el.setAttribute('aria-pressed',el.dataset.layout===layout);});preferenceStatus('us-layout-switcher','Saved. Applied to conversations in this view.');}
|
|
27
|
+
catch(error){preferenceStatus('us-layout-switcher',error.message+' Try again.',true);}
|
|
28
|
+
finally{controls.forEach(function(el){el.disabled=false;});}
|
|
29
|
+
};});
|
|
30
|
+
var textarea=document.getElementById('us-claude-allow-list');textarea.oninput=function(){store.set({userAllowDirty:true});};
|
|
31
|
+
document.getElementById('us-claude-allow-save').onclick=function(){
|
|
32
|
+
var ws=getWs();if(!ws || ws.readyState!==1){preferenceStatus('us-claude-allow-list','Disconnected. Your draft is retained.',true);return;}
|
|
33
|
+
this.disabled=true;preferenceStatus('us-claude-allow-list','Saving…');ws.send(JSON.stringify({type:'set_claude_user_allow_list',patterns:textarea.value.split('\n').map(function(line){return line.trim();}).filter(Boolean)}));
|
|
34
|
+
setTimeout(function(){var save=document.getElementById('us-claude-allow-save');if(save.disabled){save.disabled=false;preferenceStatus('us-claude-allow-list','No acknowledgement. Your draft is retained; reconnect and retry.',true);}},12000);
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function handlePersonalSettingsMessage(msg) {
|
|
38
|
+
if(msg.type==='claude_allow_list') {
|
|
39
|
+
store.set({userAllowSaved:msg.user || []});
|
|
40
|
+
if(store.get('userAllowDirty'))return;
|
|
41
|
+
}
|
|
42
|
+
if(msg.type==='set_claude_user_allow_list_result') {
|
|
43
|
+
document.getElementById('us-claude-allow-save').disabled=false;
|
|
44
|
+
if(msg.ok)store.set({userAllowDirty:msg.effective===false,userAllowSaved:msg.patterns || []});
|
|
45
|
+
var message=msg.ok ? (msg.effective===false ? 'Saved to your account, but not installed in Claude. Check filesystem access, then Save to retry.' : 'Saved. Applies on the next Claude invocation.') : msg.error || 'Save failed. Your draft is retained.';
|
|
46
|
+
preferenceStatus('us-claude-allow-list',message,!msg.ok || msg.effective===false);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { getProfile, saveProfileUpdates, uploadAvatarBlob, showAvatarPositioner } from './profile.js';
|
|
3
|
+
import { populateAccount } from './user-settings-account.js';
|
|
4
|
+
import { confirmSettingsAction } from './server-settings-state.js';
|
|
5
|
+
import { iconHtml, refreshIcons } from './icons.js';
|
|
6
|
+
|
|
7
|
+
export function initPersonalProfile() {
|
|
8
|
+
var hero = document.querySelector('#user-settings .us-account-hero');
|
|
9
|
+
var name = document.getElementById('us-account-name');
|
|
10
|
+
var edit = document.createElement('button'); edit.id = 'us-profile-edit'; edit.type = 'button'; edit.className = 'settings-btn-sm'; edit.textContent = 'Edit name'; hero.appendChild(edit);
|
|
11
|
+
var form = document.createElement('form'); form.hidden = true; form.className = 'us-profile-form';
|
|
12
|
+
form.innerHTML = '<label class="settings-label" for="us-profile-name">Display name</label><input id="us-profile-name" class="us-text-input" maxlength="100" required autocomplete="name"><div class="settings-form-actions"><button type="submit" class="settings-btn-sm">Save</button><button type="button" class="settings-btn-sm" data-cancel>Cancel</button></div><p role="status" class="settings-status"></p>';
|
|
13
|
+
name.after(form); var input = form.querySelector('input');
|
|
14
|
+
function close() { form.hidden = true; name.hidden = false; edit.hidden = false; edit.focus(); }
|
|
15
|
+
edit.onclick = function () {
|
|
16
|
+
if (!store.get('userProfileDirty')) input.value = getProfile().name || name.textContent;
|
|
17
|
+
form.hidden = false; name.hidden = true; edit.hidden = true; input.focus(); input.select();
|
|
18
|
+
};
|
|
19
|
+
input.oninput = function () { store.set({ userProfileDirty: true }); };
|
|
20
|
+
form.onsubmit = async function (event) {
|
|
21
|
+
event.preventDefault(); var save = form.querySelector('[type=submit]'); var status = form.querySelector('[role=status]');
|
|
22
|
+
if (!input.value.trim()) { status.textContent = 'Enter a display name.'; return; }
|
|
23
|
+
save.disabled = true; status.textContent = 'Saving…';
|
|
24
|
+
try { await saveProfileUpdates({ name: input.value.trim() }); store.set({ userProfileDirty: false }); close(); populateAccount(); }
|
|
25
|
+
catch (error) { status.textContent = error.message; status.dataset.state = 'error'; }
|
|
26
|
+
finally { save.disabled = false; }
|
|
27
|
+
};
|
|
28
|
+
form.querySelector('[data-cancel]').onclick = function () {
|
|
29
|
+
function discard() { store.set({ userProfileDirty: false }); close(); }
|
|
30
|
+
if (store.get('userProfileDirty')) confirmSettingsAction('Discard your unsaved name?', 'Discard', discard); else discard();
|
|
31
|
+
};
|
|
32
|
+
var visual = hero.querySelector('.us-account-avatar');
|
|
33
|
+
var avatar = document.createElement('button'); avatar.id = 'us-profile-avatar'; avatar.type = 'button'; avatar.className = 'us-avatar-edit'; avatar.setAttribute('aria-label', 'Change profile picture'); avatar.title = 'Change profile picture';
|
|
34
|
+
visual.before(avatar); avatar.appendChild(visual);
|
|
35
|
+
avatar.insertAdjacentHTML('beforeend', '<span class="us-avatar-edit-mark" aria-hidden="true">' + iconHtml('camera') + '</span>');
|
|
36
|
+
refreshIcons();
|
|
37
|
+
var file = document.createElement('input'); file.type = 'file'; file.accept = 'image/*'; file.hidden = true; hero.after(file);
|
|
38
|
+
var status = document.createElement('p'); status.className = 'settings-status'; status.setAttribute('role', 'status'); hero.after(status);
|
|
39
|
+
avatar.onclick = function () { file.click(); };
|
|
40
|
+
file.onchange = function () {
|
|
41
|
+
var selected = file.files[0]; if (!selected) return;
|
|
42
|
+
var url = URL.createObjectURL(selected); var image = new Image();
|
|
43
|
+
image.onload = function () {
|
|
44
|
+
showAvatarPositioner(image, url, function (blob) {
|
|
45
|
+
URL.revokeObjectURL(url); status.textContent = 'Uploading picture…';
|
|
46
|
+
uploadAvatarBlob(blob).then(function () { status.textContent = 'Picture updated.'; populateAccount(); avatar.focus(); })
|
|
47
|
+
.catch(function (error) { status.textContent = error.message; status.dataset.state = 'error'; });
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
image.onerror = function () { URL.revokeObjectURL(url); status.textContent = 'Could not read that image. Choose another file.'; };
|
|
51
|
+
image.src = url; file.value = '';
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { getThemes, retryAppearanceSave, getAppearanceChoices, selectAppearanceMode, selectAppearanceTheme, onThemeChange, importLegacyAppearance } from './theme.js';
|
|
3
|
+
import { enhanceSettingsSelects } from './settings-select.js';
|
|
4
|
+
|
|
5
|
+
export function initPersonalThemes(root) {
|
|
6
|
+
root.innerHTML = '<h3>Theme</h3><div class="us-preference-row"><label class="settings-label" for="us-theme-mode">Appearance</label><select id="us-theme-mode" class="settings-select"><option value="system">Follow system</option><option value="light">Light</option><option value="dark">Dark</option></select></div><p class="settings-hint">Choose a palette for each appearance. Switching light and dark keeps both choices.</p><div class="us-theme-pair"></div><p id="us-appearance-status" class="settings-status" role="status"></p>';
|
|
7
|
+
var mode = root.querySelector('select');
|
|
8
|
+
mode.onchange = function () { selectAppearanceMode(mode.value); };
|
|
9
|
+
['light', 'dark'].forEach(function (variant) {
|
|
10
|
+
var group = document.createElement('div'); group.className = 'us-theme-choice';
|
|
11
|
+
group.innerHTML = '<label class="settings-label" for="us-theme-' + variant + '">' + (variant === 'light' ? 'Light palette' : 'Dark palette') + '</label><select class="settings-select" id="us-theme-' + variant + '"></select><div class="us-theme-preview" aria-hidden="true"><div class="us-theme-preview-nav"></div><div class="us-theme-preview-content"><strong>Aa</strong><div class="us-theme-swatches"></div></div></div>';
|
|
12
|
+
group.querySelector('select').onchange = function () { selectAppearanceTheme(variant, this.value); };
|
|
13
|
+
root.querySelector('.us-theme-pair').appendChild(group);
|
|
14
|
+
});
|
|
15
|
+
function sync() {
|
|
16
|
+
var themes = getThemes(); var choices = getAppearanceChoices(); mode.value = choices.mode; mode.dispatchEvent(new Event('settings-sync'));
|
|
17
|
+
['light', 'dark'].forEach(function (variant) {
|
|
18
|
+
var select = root.querySelector('#us-theme-' + variant); select.replaceChildren();
|
|
19
|
+
Object.keys(themes).filter(function (id) { return themes[id].variant === variant; }).sort(function (a,b) { return themes[a].name.localeCompare(themes[b].name); }).forEach(function (id) {
|
|
20
|
+
var option = document.createElement('option'); option.value = id; option.textContent = themes[id].name; select.appendChild(option);
|
|
21
|
+
});
|
|
22
|
+
select.value = choices[variant + 'Theme']; select.dispatchEvent(new Event('settings-sync'));
|
|
23
|
+
var theme = themes[select.value]; if (!theme) return;
|
|
24
|
+
var preview = select.parentElement.querySelector('.us-theme-preview');
|
|
25
|
+
preview.style.background = '#' + theme.base00; preview.style.color = '#' + theme.base06;
|
|
26
|
+
preview.querySelector('.us-theme-preview-nav').style.background = '#' + theme.base01;
|
|
27
|
+
var swatches = preview.querySelector('.us-theme-swatches'); swatches.replaceChildren();
|
|
28
|
+
['base08','base09','base0A','base0B','base0D','base0E'].forEach(function (key) { var dot = document.createElement('span'); dot.style.background = '#' + theme[key]; swatches.appendChild(dot); });
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
var retry = document.createElement('button'); retry.type = 'button'; retry.className = 'settings-btn-sm'; retry.textContent = 'Retry save'; retry.hidden = !store.get('appearanceSaveError'); retry.onclick = retryAppearanceSave; root.appendChild(retry);
|
|
32
|
+
var legacy = document.createElement('button'); legacy.type = 'button'; legacy.className = 'settings-btn-sm'; legacy.textContent = "Import this browser's previous theme"; legacy.onclick = importLegacyAppearance; root.appendChild(legacy);
|
|
33
|
+
store.subscribe(function (state, previous) {
|
|
34
|
+
legacy.hidden = !state.legacyAppearanceAvailable; retry.hidden = !state.appearanceSaveError;
|
|
35
|
+
if (state.appearanceDraft !== previous.appearanceDraft || state.appearancePreference !== previous.appearancePreference) sync();
|
|
36
|
+
});
|
|
37
|
+
legacy.hidden = !store.get('legacyAppearanceAvailable'); onThemeChange(sync); sync(); enhanceSettingsSelects(root);
|
|
38
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { saveSpeechSettings, loadSpeechSettings } from './stt-settings.js';
|
|
3
|
+
import { confirmSettingsAction } from './server-settings-state.js';
|
|
4
|
+
|
|
5
|
+
export function initPersonalVoice() {
|
|
6
|
+
var root = document.getElementById('us-voice-settings');
|
|
7
|
+
root.innerHTML = '<h3>Voice providers</h3><p class="settings-hint">Manage your personal API keys here. Choose the voice model, transcription mode and language from the microphone menu in the composer. Browser speech needs no API key.</p><div class="us-voice-providers" aria-label="Voice providers"></div><div id="us-voice-connection" hidden><form id="us-voice-key-form"><label class="settings-label" for="us-voice-key">API key</label><input id="us-voice-key" class="us-text-input" type="password" autocomplete="off" spellcheck="false" maxlength="4096" required><p id="us-voice-privacy" class="settings-hint"></p><div class="settings-form-actions"><button type="submit" class="settings-btn-sm">Save key</button><button type="button" id="us-voice-key-cancel" class="settings-btn-sm">Cancel</button><a id="us-voice-get-key" target="_blank" rel="noopener noreferrer">Get API key ↗</a></div></form><button type="button" id="us-voice-remove" class="settings-btn-sm">Remove saved key</button></div><p class="settings-status" role="status" id="us-voice-status"></p><button type="button" class="settings-btn-sm" id="us-voice-retry" hidden>Reload providers</button>';
|
|
8
|
+
var list = root.querySelector('.us-voice-providers'); var editor = root.querySelector('#us-voice-connection');
|
|
9
|
+
var form = root.querySelector('form'); var key = form.querySelector('input'); var status = root.querySelector('[role=status]');
|
|
10
|
+
var remove = root.querySelector('#us-voice-remove'); var retry = root.querySelector('#us-voice-retry');
|
|
11
|
+
function sync() {
|
|
12
|
+
var settings = store.get('speechSettings'); var busy = !!store.get('speechSettingsSaving');
|
|
13
|
+
if (settings) {
|
|
14
|
+
var signature = settings.providers.map(function (provider) { return provider.id; }).join(',');
|
|
15
|
+
if (list.dataset.catalog !== signature) {
|
|
16
|
+
root.appendChild(editor); list.replaceChildren(); list.dataset.catalog = signature;
|
|
17
|
+
settings.providers.forEach(function (provider) {
|
|
18
|
+
var row = document.createElement('div'); row.className = 'us-voice-row'; row.dataset.voiceProvider = provider.id;
|
|
19
|
+
var controls = document.createElement('div'); controls.className = 'us-voice-row-controls';
|
|
20
|
+
var image = document.createElement('img'); image.className = 'us-voice-provider-logo'; image.src = provider.icon; image.alt = '';
|
|
21
|
+
var copy = document.createElement('div'); copy.className = 'us-voice-provider-copy';
|
|
22
|
+
var name = document.createElement('strong'); name.textContent = provider.name;
|
|
23
|
+
var state = document.createElement('small'); state.className = 'us-voice-key-state'; copy.appendChild(name); copy.appendChild(state);
|
|
24
|
+
var button = document.createElement('button'); button.type = 'button'; button.className = 'settings-btn-sm us-voice-key-action'; button.dataset.voiceKey = provider.id;
|
|
25
|
+
button.setAttribute('aria-controls','us-voice-connection');
|
|
26
|
+
button.onclick = function () { discardThen(function () { store.set({userVoiceKeyProvider:provider.id}); status.textContent = ''; sync(); key.focus(); }); };
|
|
27
|
+
controls.appendChild(image); controls.appendChild(copy); controls.appendChild(button); row.appendChild(controls); list.appendChild(row);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
list.querySelectorAll('[data-voice-provider]').forEach(function (row) {
|
|
31
|
+
var id = row.dataset.voiceProvider; var connected = settings.configured.indexOf(id) >= 0; var button = row.querySelector('[data-voice-key]');
|
|
32
|
+
row.querySelector('.us-voice-key-state').textContent = connected ? 'Key saved' : 'Not connected';
|
|
33
|
+
button.textContent = connected ? 'API key' : 'Connect'; button.disabled = busy;
|
|
34
|
+
button.setAttribute('aria-label', (connected ? 'Manage ' : 'Connect ') + row.querySelector('strong').textContent + ' API key');
|
|
35
|
+
button.setAttribute('aria-expanded',String(store.get('userVoiceKeyProvider') === id));
|
|
36
|
+
});
|
|
37
|
+
var provider = settings.providers.find(function (item) { return item.id === store.get('userVoiceKeyProvider'); });
|
|
38
|
+
editor.hidden = !provider;
|
|
39
|
+
if (provider) {
|
|
40
|
+
var connected = settings.configured.indexOf(provider.id) >= 0;
|
|
41
|
+
var row = Array.from(list.children).find(function (item) { return item.dataset.voiceProvider === provider.id; });
|
|
42
|
+
if (editor.parentElement !== row) row.appendChild(editor);
|
|
43
|
+
if (status.parentElement !== editor) editor.appendChild(status);
|
|
44
|
+
form.querySelector('label').textContent = provider.name + ' API key';
|
|
45
|
+
key.placeholder = connected ? 'Enter a replacement key' : 'Enter API key'; remove.hidden = !connected;
|
|
46
|
+
root.querySelector('#us-voice-privacy').textContent = 'Stored privately on your Clay server and shared by all ' + provider.name + ' voice models. Audio is sent to ' + provider.name + '; usage is billed to your account.';
|
|
47
|
+
root.querySelector('#us-voice-get-key').href = provider.url;
|
|
48
|
+
} else { if (editor.parentElement !== root) root.appendChild(editor); retry.before(status); }
|
|
49
|
+
} else list.textContent = 'Loading providers…';
|
|
50
|
+
editor.querySelectorAll('input,button').forEach(function (control) { control.disabled = busy || !settings; });
|
|
51
|
+
retry.hidden = !store.get('speechSettingsError'); retry.disabled = busy;
|
|
52
|
+
if (store.get('speechSettingsError')) { status.textContent = store.get('speechSettingsError'); status.dataset.state = 'error'; }
|
|
53
|
+
}
|
|
54
|
+
function discardThen(action) {
|
|
55
|
+
if (!key.value) { action(); return; }
|
|
56
|
+
confirmSettingsAction('Discard the unsaved voice API key?', 'Discard', function () { key.value = ''; store.set({userVoiceDirty:false}); action(); });
|
|
57
|
+
}
|
|
58
|
+
function closeEditor() {
|
|
59
|
+
var id = store.get('userVoiceKeyProvider'); key.value = ''; store.set({userVoiceKeyProvider:null,userVoiceDirty:false}); sync();
|
|
60
|
+
var button = root.querySelector('[data-voice-key="' + id + '"]'); if (button) button.focus();
|
|
61
|
+
}
|
|
62
|
+
async function save(value) {
|
|
63
|
+
var provider = store.get('userVoiceKeyProvider'); if (!provider) return;
|
|
64
|
+
status.textContent = 'Saving…'; status.dataset.state = '';
|
|
65
|
+
try { await saveSpeechSettings({provider:provider,key:value}); closeEditor(); status.textContent = value === null ? 'Key removed. Reconnect this provider before using its voice models.' : 'Key saved. Your voice model and language are unchanged.'; }
|
|
66
|
+
catch (error) { status.textContent = error.message; status.dataset.state = 'error'; sync(); }
|
|
67
|
+
}
|
|
68
|
+
key.oninput = function () { store.set({userVoiceDirty:!!key.value}); };
|
|
69
|
+
form.onsubmit = function (event) { event.preventDefault(); save(key.value); };
|
|
70
|
+
root.querySelector('#us-voice-key-cancel').onclick = function () { discardThen(function () { closeEditor(); status.textContent = ''; }); };
|
|
71
|
+
remove.onclick = function () { confirmSettingsAction('Remove this provider’s saved voice key? Models using it will require reconnection.', 'Remove key', function () { save(null); }); };
|
|
72
|
+
retry.onclick = loadSpeechSettings;
|
|
73
|
+
store.subscribe(function (state, previous) { if (state.speechSettings !== previous.speechSettings || state.speechSettingsSaving !== previous.speechSettingsSaving || state.speechSettingsError !== previous.speechSettingsError) sync(); });
|
|
74
|
+
sync();
|
|
75
|
+
}
|