/**
* A compact BYOK key field for one provider — used per-row inside the header Providers dropdown
* (once for the image provider, once for the text/rewrite provider). It targets the provider given
* by `providerId` (falling back to the active image provider) and only appears when that provider
* needs a key. The key is held in memory for the session only; the in-field Save icon persists it
* to this browser and the Clear icon removes a saved one. The "Get a key" link and an info tooltip
* are kept here too. Renders nothing for local / no-key / "none" providers.
* @module gui/components/ApiKeyField
*/
import { useEffect, useState } from "react";
import { useIntl, defineMessages } from "react-intl";
import { getProvider } from "../lib/providers/index.js";
import { getSessionKey, setSessionKey } from "../lib/sessionKeys.js";
import { metaFor } from "../lib/providerMeta.js";
import { dialog } from "../lib/dialog.js";
import { jsxDEV as _jsxDEV } from "react/jsx-dev-runtime";
const msgs = defineMessages({
infoTip: {
id: "apiKey.infoTip",
defaultMessage: "Your key for this provider. Kept in memory for this session only — click the save icon to store it in this browser."
},
ariaKey: {
id: "apiKey.aria",
defaultMessage: "{provider} API key",
description: "aria-label for the key input (provider = provider name)"
},
placeholder: {
id: "apiKey.placeholder",
defaultMessage: "API key — not saved unless you save it"
},
confirmSave: {
id: "apiKey.confirmSave",
defaultMessage: "Save this API key in your browser? It will persist on this device until you clear it."
},
confirmClear: {
id: "apiKey.confirmClear",
defaultMessage: "Remove the saved API key from this browser?"
},
saveUpdateTitle: {
id: "apiKey.saveUpdateTitle",
defaultMessage: "Update the saved key in this browser"
},
saveTitle: {
id: "apiKey.saveTitle",
defaultMessage: "Save key in this browser"
},
saveAria: {
id: "apiKey.saveAria",
defaultMessage: "Save API key"
},
clearTitle: {
id: "apiKey.clearTitle",
defaultMessage: "Remove the saved key from this browser"
},
clearAria: {
id: "apiKey.clearAria",
defaultMessage: "Clear saved API key"
},
getKey: {
id: "apiKey.getKey",
defaultMessage: "Get a key ↗"
}
});
/**
* A small info "i" with a tooltip.
* @param {object} props `{ text }`.
* @returns {(JSX.Element|null)}
*/
function InfoTip({
text
}) {
if (!text) return null;
return /*#__PURE__*/_jsxDEV("span", {
className: "info-tip",
title: text,
"aria-label": text,
role: "img",
children: "i"
}, void 0, false);
}
/**
* A provider's API-key field.
* @param {object} props
* @param {object} props.settings The current settings.
* @param {Function} props.setSettings Update the settings.
* @param {string} [props.providerId] The provider whose key to edit (defaults to the active image
* provider). `"none"` / falsy renders nothing.
* @returns {(JSX.Element|null)}
*/
export default function ApiKeyField({
settings,
setSettings,
providerId
}) {
const intl = useIntl();
const id = providerId ?? settings.provider;
const provider = id && id !== "none" ? getProvider(id) : null;
const pid = provider?.id;
const [keyInput, setKeyInput] = useState("");
// Show the session key when the provider changes, falling back to the saved one.
useEffect(() => {
setKeyInput(getSessionKey(pid) || settings.keys?.[pid] || "");
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pid]);
if (!provider || !provider.needsKey) return null;
const saved = !!settings.keys?.[pid];
const persisted = settings.keys?.[pid] || "";
const keyUrl = metaFor(pid).keyUrl;
const onKeyInput = val => {
setKeyInput(val);
setSessionKey(pid, val); // in memory only — not persisted until Save
};
const saveKey = async () => {
if (!keyInput) return;
if (await dialog.confirm({
message: intl.formatMessage(msgs.confirmSave)
})) setSettings(s => ({
...s,
keys: {
...s.keys,
[pid]: keyInput
}
}));
};
const clearKey = async () => {
if (!(await dialog.confirm({
message: intl.formatMessage(msgs.confirmClear)
}))) return;
setSettings(s => {
const keys = {
...s.keys
};
delete keys[pid];
return {
...s,
keys
};
});
};
// Save is redundant once the typed value already matches the saved one.
const canSave = !!keyInput && keyInput !== persisted;
return /*#__PURE__*/_jsxDEV("div", {
className: "hkey",
children: [/*#__PURE__*/_jsxDEV(InfoTip, {
text: intl.formatMessage(msgs.infoTip)
}, void 0, false), /*#__PURE__*/_jsxDEV("div", {
className: `hkey-input${saved ? " saved" : ""}`,
children: [/*#__PURE__*/_jsxDEV("input", {
type: "password",
autoComplete: "off",
"aria-label": intl.formatMessage(msgs.ariaKey, {
provider: provider.label
}),
placeholder: intl.formatMessage(msgs.placeholder),
value: keyInput,
onChange: e => onKeyInput(e.target.value)
}, void 0, false), /*#__PURE__*/_jsxDEV("button", {
type: "button",
className: "hkey-icon save",
onClick: saveKey,
disabled: !canSave,
title: intl.formatMessage(saved ? msgs.saveUpdateTitle : msgs.saveTitle),
"aria-label": intl.formatMessage(msgs.saveAria),
children: /*#__PURE__*/_jsxDEV("svg", {
viewBox: "0 0 24 24",
width: "15",
height: "15",
"aria-hidden": "true",
children: /*#__PURE__*/_jsxDEV("path", {
d: "M5 3h11l3 3v15H5V3z M8 3v5h7V3 M8 21v-7h8v7",
fill: "none",
stroke: "currentColor",
strokeWidth: "1.8",
strokeLinejoin: "round"
}, void 0, false)
}, void 0, false)
}, void 0, false), saved && /*#__PURE__*/_jsxDEV("button", {
type: "button",
className: "hkey-icon clear",
onClick: clearKey,
title: intl.formatMessage(msgs.clearTitle),
"aria-label": intl.formatMessage(msgs.clearAria),
children: /*#__PURE__*/_jsxDEV("svg", {
viewBox: "0 0 24 24",
width: "15",
height: "15",
"aria-hidden": "true",
children: /*#__PURE__*/_jsxDEV("path", {
d: "M6 6l12 12 M18 6L6 18",
fill: "none",
stroke: "currentColor",
strokeWidth: "1.8",
strokeLinecap: "round"
}, void 0, false)
}, void 0, false)
}, void 0, false)]
}, void 0, true), keyUrl && /*#__PURE__*/_jsxDEV("a", {
className: "key-link",
href: keyUrl,
target: "_blank",
rel: "noreferrer",
children: intl.formatMessage(msgs.getKey)
}, void 0, false)]
}, void 0, true);
}