tmp/webapp-docs/frontend/components/ProviderGear.js

/**
 * The header provider-settings gear — a small gear button next to the provider picker that opens a
 * popover holding an **accordion** of the chosen providers' own controls (`ProviderBox`): one
 * section per role — Image (always), Text (when a rewrite AI is set), and Upscale (when an upscaler
 * is set). This keeps every provider's knobs (model / size / sampler / negative prompt / …) out of
 * the main prompt area while staying one click from the provider selection. Each section header
 * shows that provider's label + tier and toggles its body open/closed.
 * @module gui/components/ProviderGear
 */
import { useState } from "react";
import { useIntl, defineMessages } from "react-intl";
import { getProvider } from "../lib/providers/index.js";
import ProviderBox from "./ProviderBox.jsx";
import { jsxDEV as _jsxDEV, Fragment as _Fragment } from "react/jsx-dev-runtime";
const msgs = defineMessages({
  tierApi: {
    id: "providerGear.tier.api",
    defaultMessage: "image API"
  },
  tierSyntax: {
    id: "providerGear.tier.syntax",
    defaultMessage: "copy-prompt"
  },
  tierPlain: {
    id: "providerGear.tier.plain",
    defaultMessage: "plain text"
  },
  settings: {
    id: "providerGear.settings",
    defaultMessage: "Provider settings"
  },
  close: {
    id: "providerGear.close",
    defaultMessage: "close"
  },
  none: {
    id: "providerGear.none",
    defaultMessage: "No provider selected — pick one in the Providers menu to see its settings."
  },
  roleImage: {
    id: "providerGear.role.image",
    defaultMessage: "Image"
  },
  roleText: {
    id: "providerGear.role.text",
    defaultMessage: "Text"
  },
  roleUpscale: {
    id: "providerGear.role.upscale",
    defaultMessage: "Upscale"
  },
  noTextSettings: {
    id: "providerGear.noTextSettings",
    defaultMessage: "This text AI uses its built-in model — no extra settings here."
  },
  noUpscaleSettings: {
    id: "providerGear.noUpscaleSettings",
    defaultMessage: "No separate upscale settings — this provider reuses its image settings above."
  }
});

/**
 * The gear cog icon.
 * @returns {JSX.Element}
 */
function GearIcon() {
  return /*#__PURE__*/_jsxDEV("svg", {
    viewBox: "0 0 24 24",
    width: "16",
    height: "16",
    fill: "none",
    stroke: "currentColor",
    strokeWidth: "2",
    strokeLinecap: "round",
    strokeLinejoin: "round",
    "aria-hidden": "true",
    children: [/*#__PURE__*/_jsxDEV("circle", {
      cx: "12",
      cy: "12",
      r: "3"
    }, void 0, false), /*#__PURE__*/_jsxDEV("path", {
      d: "M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"
    }, void 0, false)]
  }, void 0, true);
}

/**
 * One accordion section: a provider role's header (label + tier) over its collapsible `ProviderBox`.
 * @param {object} props
 * @param {string} props.role Localized role name (Image / Text / Upscale).
 * @param {object} props.provider The provider manifest.
 * @param {boolean} props.open Whether the section body is expanded.
 * @param {Function} props.onToggle Toggle this section.
 * @param {object} props.settings The current settings.
 * @param {Function} props.setSettings Update the settings.
 * @returns {JSX.Element}
 */
function GearSection({
  roleId,
  role,
  provider,
  open,
  onToggle,
  settings,
  setSettings
}) {
  const intl = useIntl();
  const tierLabel = intl.formatMessage(provider.tier === "api" ? msgs.tierApi : provider.tier === "syntax" ? msgs.tierSyntax : msgs.tierPlain);
  // A provider has ONE settings schema — its native one (image-generation for image providers,
  // upscale params for upscale-only enhancers). So only render it for the role that schema serves:
  //   • image   → the provider's image-gen schema (correct).
  //   • text    → a rewrite AI has no applicable settings (it uses a fixed chat model) → note.
  //   • upscale → only upscale-ONLY providers carry an upscale schema; a dual image+upscale provider's
  //               schema is image-gen, edited under its Image section → note.
  // This stops the old behaviour of showing image-gen knobs (model/sampler/steps) under Text/Upscale.
  const showSchema = roleId === "image" || roleId === "upscale" && provider.upscaleOnly;
  return /*#__PURE__*/_jsxDEV("div", {
    className: `gear-acc-item${open ? " on" : ""}`,
    children: [/*#__PURE__*/_jsxDEV("button", {
      className: "gear-acc-head",
      onClick: onToggle,
      "aria-expanded": open,
      children: [/*#__PURE__*/_jsxDEV("span", {
        className: "gear-acc-caret",
        "aria-hidden": "true",
        children: "▸"
      }, void 0, false), /*#__PURE__*/_jsxDEV("span", {
        className: "gear-acc-role",
        children: role
      }, void 0, false), /*#__PURE__*/_jsxDEV("span", {
        className: "gear-acc-name",
        children: provider.label
      }, void 0, false), /*#__PURE__*/_jsxDEV("span", {
        className: "provider-tag",
        children: tierLabel
      }, void 0, false)]
    }, void 0, true), open && /*#__PURE__*/_jsxDEV("div", {
      className: "gear-acc-body",
      children: showSchema ? /*#__PURE__*/_jsxDEV(ProviderBox, {
        settings: settings,
        setSettings: setSettings,
        providerId: provider.id
      }, void 0, false) : /*#__PURE__*/_jsxDEV("p", {
        className: "hint provider-controls-empty",
        children: intl.formatMessage(roleId === "text" ? msgs.noTextSettings : msgs.noUpscaleSettings)
      }, void 0, false)
    }, void 0, false)]
  }, void 0, true);
}

/**
 * @param {object} props
 * @param {object} props.settings The current settings.
 * @param {Function} props.setSettings Update the settings.
 * @returns {(JSX.Element|null)}
 */
export default function ProviderGear({
  settings,
  setSettings
}) {
  const intl = useIntl();
  const [open, setOpen] = useState(false);
  // Which accordion sections are expanded — Image leads, expanded by default.
  const [expanded, setExpanded] = useState(() => new Set(["image"]));
  const toggle = id => setExpanded(prev => {
    const next = new Set(prev);
    if (next.has(id)) next.delete(id);else next.add(id);
    return next;
  });

  // Text / Upscale may be unset ("none"); the image provider is always set (Plain/"Unset" is real).
  const image = getProvider(settings.provider);
  const textId = settings.rewriteProvider && settings.rewriteProvider !== "none" ? settings.rewriteProvider : null;
  const upscaleId = settings.upscaleProvider && settings.upscaleProvider !== "none" ? settings.upscaleProvider : null;
  const text = textId ? getProvider(textId) : null;
  const upscale = upscaleId ? getProvider(upscaleId) : null;
  const sections = [image && {
    id: "image",
    role: intl.formatMessage(msgs.roleImage),
    provider: image
  }, text && {
    id: "text",
    role: intl.formatMessage(msgs.roleText),
    provider: text
  }, upscale && {
    id: "upscale",
    role: intl.formatMessage(msgs.roleUpscale),
    provider: upscale
  }].filter(Boolean);
  return /*#__PURE__*/_jsxDEV("div", {
    className: "field-menu-wrap provider-gear",
    children: [/*#__PURE__*/_jsxDEV("button", {
      className: `ps-gear${open ? " on" : ""}`,
      onClick: () => setOpen(o => !o),
      title: intl.formatMessage(msgs.settings),
      "aria-label": intl.formatMessage(msgs.settings),
      "aria-haspopup": "dialog",
      "aria-expanded": open,
      children: [/*#__PURE__*/_jsxDEV(GearIcon, {}, void 0, false), /*#__PURE__*/_jsxDEV("span", {
        className: "ctl-label",
        children: intl.formatMessage(msgs.settings)
      }, void 0, false)]
    }, void 0, true), open && /*#__PURE__*/_jsxDEV(_Fragment, {
      children: [/*#__PURE__*/_jsxDEV("div", {
        className: "gear-pop-scrim",
        onClick: () => setOpen(false)
      }, void 0, false), /*#__PURE__*/_jsxDEV("div", {
        className: "gear-pop provider-gear-pop",
        role: "dialog",
        "aria-label": intl.formatMessage(msgs.settings),
        children: [/*#__PURE__*/_jsxDEV("div", {
          className: "gear-pop-head",
          children: [/*#__PURE__*/_jsxDEV("span", {
            className: "gear-pop-title",
            children: intl.formatMessage(msgs.settings)
          }, void 0, false), /*#__PURE__*/_jsxDEV("button", {
            className: "link-btn",
            onClick: () => setOpen(false),
            children: intl.formatMessage(msgs.close)
          }, void 0, false)]
        }, void 0, true), /*#__PURE__*/_jsxDEV("div", {
          className: "gear-pop-body",
          children: sections.length ? /*#__PURE__*/_jsxDEV("div", {
            className: "gear-acc",
            children: sections.map(s => /*#__PURE__*/_jsxDEV(GearSection, {
              roleId: s.id,
              role: s.role,
              provider: s.provider,
              open: expanded.has(s.id),
              onToggle: () => toggle(s.id),
              settings: settings,
              setSettings: setSettings
            }, s.id, false))
          }, void 0, false) : /*#__PURE__*/_jsxDEV("p", {
            className: "hint provider-controls-empty",
            children: intl.formatMessage(msgs.none)
          }, void 0, false)
        }, void 0, false)]
      }, void 0, true)]
    }, void 0, true)]
  }, void 0, true);
}