Translate: Update API key management
| @@ -28,8 +28,14 @@ | ||
| 28 | 28 | <option value="free">Free</option> |
| 29 | 29 | <option value="pro">Pro</option> |
| 30 | 30 | </select> |
| 31 | 31 | <div id="translate_key_button" class="menu_button fa-solid fa-keymenu_button_icon margin0 manage-api-keys"></div> |
| 32 | 32 | <div id="translate_url_button"i class="menu_button fa-solid fa-link margin0key"></divi> |
| 33 | + <span>Key</span> | |
| 34 | + </div> | |
| 35 | + <div id="translate_url_button" class="menu_button menu_button_icon margin0 manage-api-keys"> | |
| 36 | + <i class="fa-solid fa-link"></i> | |
| 37 | + <span>URL</span> | |
| 38 | + </div> | |
| 33 | 39 | </div> |
| 34 | 40 | <label data-i18n="ext_translate_target_lang" for="translation_target_language">Target Language</label> |
| 35 | 41 | <select id="translation_target_language" name="target_language"></select> |
| @@ -10,9 +10,9 @@ import { | ||
| 10 | 10 | updateMessageBlock, |
| 11 | 11 | } from '../../../script.js'; |
| 12 | 12 | import { extension_settings, getContext, renderExtensionTemplateAsync } from '../../extensions.js'; |
| 13 | 13 | import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from '../../popup.js'; |
| 14 | 14 | import { updateReasoningUI } from '../../reasoning.js'; |
| 15 | 15 | import { findSecret, secret_state, writeSecret } from '../../secrets.js'; |
| 16 | 16 | import { SlashCommand } from '../../slash-commands/SlashCommand.js'; |
| 17 | 17 | import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; |
| 18 | 18 | import { enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; |
| @@ -152,9 +152,9 @@ const LOCAL_URL = ['libre', 'oneringtranslator', 'deeplx', 'lingva']; | ||
| 152 | 152 | function showKeysButton() { |
| 153 | 153 | const providerRequiresKey = KEY_REQUIRED.includes(extension_settings.translate.provider); |
| 154 | 154 | const providerOptionalUrl = LOCAL_URL.includes(extension_settings.translate.provider); |
| 155 | 155 | $('#translate_key_button').toggle(providerRequiresKey).data('key', extension_settings.translate.provider); |
| 156 | 156 | $('#translate_key_button').toggleClass('success', Boolean(secret_state[extension_settings.translate.provider])); |
| 157 | 157 | $('#translate_url_button').toggle(providerOptionalUrl).data('key', extension_settings.translate.provider + '_url'); |
| 158 | 158 | $('#translate_url_button').toggleClass('success', Boolean(secret_state[extension_settings.translate.provider + '_url'])); |
| 159 | 159 | $('#deepl_api_endpoint').toggle(extension_settings.translate.provider === 'deepl'); |
| 160 | 160 | } |
| @@ -749,63 +749,16 @@ jQuery(async () => { | ||
| 749 | 749 | saveSettingsDebounced(); |
| 750 | 750 | }); |
| 751 | 751 | $(document).on('click', '.mes_translate', onMessageTranslateClick); |
| 752 | - $('#translate_key_button').on('click', async () => { | |
| 753 | - const optionText = $('#translation_provider option:selected').text(); | |
| 754 | - const key = await callGenericPopup(`<h3>${optionText} API Key</h3>`, POPUP_TYPE.INPUT, '', { | |
| 755 | - customButtons: [{ | |
| 756 | - text: 'Remove Key', | |
| 757 | - appendAtEnd: true, | |
| 758 | - result: POPUP_RESULT.NEGATIVE, | |
| 759 | - action: async () => { | |
| 760 | - await writeSecret(extension_settings.translate.provider, ''); | |
| 761 | - toastr.success('API Key removed'); | |
| 762 | - $('#translate_key_button').toggleClass('success', !!secret_state[extension_settings.translate.provider]); | |
| 763 | - }, | |
| 764 | - }], | |
| 765 | - }); | |
| 766 | 752 | |
| 767 | - if (!key) { | |
| 753 | + [event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach((eventType) => { | |
| 768 | - return; | |
| 754 | + eventSource.on(eventType, (/** @type {string} */ key) => { | |
| 755 | + if (key === extension_settings.translate.provider) { | |
| 756 | + $('#translate_key_button').toggleClass('success', !!secret_state[extension_settings.translate.provider]); | |
| 769 | 757 | } |
| 770 | - | |
| 758 | + if (key === `${extension_settings.translate.provider}_url`) { | |
| 771 | - await writeSecret(extension_settings.translate.provider, String(key)); | |
| 759 | + $('#translate_url_button').toggleClass('success', !!secret_state[`${extension_settings.translate.provider}_url`]); | |
| 772 | - toastr.success('API Key saved'); | |
| 773 | - $('#translate_key_button').addClass('success'); | |
| 774 | - }); | |
| 775 | - $('#translate_url_button').on('click', async () => { | |
| 776 | - const optionText = $('#translation_provider option:selected').text(); | |
| 777 | - const exampleURLs = { | |
| 778 | - 'libre': 'http://127.0.0.1:5000/translate', | |
| 779 | - 'lingva': 'https://lingva.ml/api/v1', | |
| 780 | - 'oneringtranslator': 'http://127.0.0.1:4990/translate', | |
| 781 | - 'deeplx': 'http://127.0.0.1:1188/translate', | |
| 782 | - }; | |
| 783 | - const popupText = `<h3>${optionText} API URL</h3><i>Example: <tt>${String(exampleURLs[extension_settings.translate.provider])}</tt></i>`; | |
| 784 | - | |
| 785 | - const secretKey = extension_settings.translate.provider + '_url'; | |
| 786 | - const savedUrl = secret_state[secretKey] ? await findSecret(secretKey) : ''; | |
| 787 | - | |
| 788 | - const url = await callGenericPopup(popupText, POPUP_TYPE.INPUT, savedUrl, { | |
| 789 | - customButtons: [{ | |
| 790 | - text: 'Remove URL', | |
| 791 | - appendAtEnd: true, | |
| 792 | - result: POPUP_RESULT.NEGATIVE, | |
| 793 | - action: async () => { | |
| 794 | - await writeSecret(secretKey, ''); | |
| 795 | - toastr.success('API URL removed'); | |
| 796 | - $('#translate_url_button').toggleClass('success', !!secret_state[secretKey]); | |
| 797 | - }, | |
| 798 | - }], | |
| 799 | - }); | |
| 800 | - | |
| 801 | - if (!url) { | |
| 802 | - return; | |
| 803 | 760 | } |
| 804 | - | |
| 761 | + }); | |
| 805 | - await writeSecret(secretKey, String(url)); | |
| 806 | - | |
| 807 | - toastr.success('API URL saved'); | |
| 808 | - $('#translate_url_button').addClass('success'); | |
| 809 | 762 | }); |
| 810 | 763 | |
| 811 | 764 | loadSettings(); |
| @@ -23,10 +23,16 @@ export const SECRET_KEYS = { | ||
| 23 | 23 | OPENAI: 'api_key_openai', |
| 24 | 24 | NOVEL: 'api_key_novel', |
| 25 | 25 | CLAUDE: 'api_key_claude', |
| 26 | + DEEPL: 'deepl', | |
| 27 | + LIBRE: 'libre', | |
| 28 | + LIBRE_URL: 'libre_url', | |
| 29 | + LINGVA_URL: 'lingva_url', | |
| 26 | 30 | OPENROUTER: 'api_key_openrouter', |
| 27 | 31 | SCALE: 'api_key_scale', |
| 28 | 32 | AI21: 'api_key_ai21', |
| 29 | 33 | SCALE_COOKIE: 'scale_cookie', |
| 34 | + ONERING_URL: 'oneringtranslator_url', | |
| 35 | + DEEPLX_URL: 'deeplx_url', | |
| 30 | 36 | MAKERSUITE: 'api_key_makersuite', |
| 31 | 37 | VERTEXAI: 'api_key_vertexai', |
| 32 | 38 | SERPAPI: 'api_key_serpapi', |
| @@ -104,6 +110,12 @@ const FRIENDLY_NAMES = { | ||
| 104 | 110 | [SECRET_KEYS.FALAI]: 'FAL.AI', |
| 105 | 111 | [SECRET_KEYS.AZURE_TTS]: 'Azure TTS', |
| 106 | 112 | [SECRET_KEYS.AIMLAPI]: 'AI/ML API', |
| 113 | + [SECRET_KEYS.DEEPL]: 'DeepL', | |
| 114 | + [SECRET_KEYS.LIBRE]: 'LibreTranslate', | |
| 115 | + [SECRET_KEYS.LIBRE_URL]: 'LibreTranslate Endpoint (e.g. http://127.0.0.1:5000/translate)', | |
| 116 | + [SECRET_KEYS.LINGVA_URL]: 'Lingva Endpoint (e.g. https://lingva.ml/api/v1)', | |
| 117 | + [SECRET_KEYS.ONERING_URL]: 'OneRingTranslator Endpoint (e.g. http://127.0.0.1:4990/translate)', | |
| 118 | + [SECRET_KEYS.DEEPLX_URL]: 'DeepLX Endpoint (e.g. http://127.0.0.1:1188/translate)', | |
| 107 | 119 | }; |
| 108 | 120 | |
| 109 | 121 | const INPUT_MAP = { |
| @@ -162,11 +162,12 @@ export class SecretManager { | ||
| 162 | 162 | /** |
| 163 | 163 | * Masks a secret value with asterisks in the middle |
| 164 | 164 | * @param {string} value The secret value to mask |
| 165 | + * @param {string} key The secret key | |
| 165 | 166 | * @returns {string} A masked version of the value for peeking |
| 166 | 167 | */ |
| 167 | 168 | getMaskedValue(value, key) { |
| 168 | 169 | // No masking if exposure is allowed |
| 169 | 170 | if (allowKeysExposure || EXPORTABLE_KEYS.includes(key)) { |
| 170 | 171 | return value; |
| 171 | 172 | } |
| 172 | 173 | const threshold = 10; |
| @@ -340,7 +341,7 @@ export class SecretManager { | ||
| 340 | 341 | if (value && Array.isArray(value) && value.length > 0) { |
| 341 | 342 | state[key] = value.map(secret => ({ |
| 342 | 343 | id: secret.id, |
| 343 | 344 | value: this.getMaskedValue(secret.value, key), |
| 344 | 345 | label: secret.label, |
| 345 | 346 | active: secret.active, |
| 346 | 347 | })); |