/**
* The active image provider's *common* image knobs, surfaced inline at the bottom-left of the
* prompt box (next to the Prompts counter) instead of being buried in the provider gear: the
* batch count ("Images") and the output size (a Size / Ratio select, or a Width × Height pair —
* whichever the provider exposes). Capability-driven: it reads the provider's own settings schema
* and renders only the controls that exist, so providers without image knobs (plain text, the
* copy-prompt syntax providers like Midjourney) render nothing. Values are written to the same
* `providerParams[id]` namespace the provider gear (`ProviderBox`) uses, so the two stay in sync.
*
* Compact width (<=768px): **Images stays inline, but Size tucks behind a small cog popover** — on a
* narrow phone the Prompts + Images + Size(W×H) cluster would otherwise overflow the box's right edge.
* Prompts-only and Prompts+Images stay fully inline; only the wider Size control moves behind the cog.
* Desktop renders everything inline (unchanged) — the split is driven by the hydration-safe {@link useCompact}.
* @module gui/components/InlineImageControls
*/
import { useEffect, useState } from "react";
import { useIntl, defineMessages } from "react-intl";
import { getProvider } from "../lib/providers/index.js";
import { useProviderSettings } from "../lib/useProvider.js";
import { useCompact } from "../lib/useCompact.js";
import { GearIcon } from "./icons.jsx";
import { jsxDEV as _jsxDEV, Fragment as _Fragment } from "react/jsx-dev-runtime";
const msgs = defineMessages({
imagesTitle: {
id: "inlineImg.imagesTitle",
defaultMessage: "Images generated per prompt"
},
images: {
id: "inlineImg.images",
defaultMessage: "Images"
},
ratio: {
id: "inlineImg.ratio",
defaultMessage: "Ratio"
},
size: {
id: "inlineImg.size",
defaultMessage: "Size"
},
sizeTitle: {
id: "inlineImg.sizeTitle",
defaultMessage: "Output size — width × height"
},
sizeMenu: {
id: "inlineImg.sizeMenu",
defaultMessage: "Size settings"
},
width: {
id: "inlineImg.width",
defaultMessage: "Width"
},
height: {
id: "inlineImg.height",
defaultMessage: "Height"
}
});
// Keys a provider may use to express output size / aspect. The first matching field wins (rendered
// as its native control — select or text); only when there's none do we fall back to the
// imageWidth × imageHeight number pair. `ar` is Midjourney's aspect-ratio flag.
const SIZE_KEYS = ["size", "imageSize", "aspectRatio", "ar"];
/**
* Inline provider image controls (Images + Size).
* @param {object} props
* @param {object} props.settings The current settings.
* @param {Function} props.setSettings Update the settings.
* @returns {(JSX.Element|null)}
*/
export default function InlineImageControls({
settings,
setSettings
}) {
const intl = useIntl();
const provider = getProvider(settings.provider);
const pid = provider?.id;
const {
schema,
options
} = useProviderSettings(pid);
const compact = useCompact();
const [sizeOpen, setSizeOpen] = useState(false);
// Close the compact Size popover on Escape.
useEffect(() => {
if (!sizeOpen) return undefined;
const onKey = e => e.key === "Escape" && setSizeOpen(false);
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [sizeOpen]);
// Surface for any provider that exposes the relevant fields — the in-app generators (api tier)
// plus copy-prompt providers like Midjourney (its aspect ratio). Providers without these fields
// (plain text) render nothing.
if (!provider || !schema) return null;
const fields = schema.fields || [];
const defaults = schema.defaults || {};
const params = settings.providerParams?.[pid] || {};
const setParam = (key, value) => setSettings(s => ({
...s,
providerParams: {
...s.providerParams,
[pid]: {
...s.providerParams?.[pid],
[key]: value
}
}
}));
const batch = fields.find(f => f.key === "batchSize");
const sizeField = fields.find(f => SIZE_KEYS.includes(f.key));
const width = fields.find(f => f.key === "imageWidth");
const height = fields.find(f => f.key === "imageHeight");
if (!batch && !sizeField && !(width && height)) return null;
const sizeLabel = intl.formatMessage(sizeField && (sizeField.key === "aspectRatio" || sizeField.key === "ar") ? msgs.ratio : msgs.size);
const sizeOptions = sizeField ? sizeField.options || options[sizeField.optionsFrom] || [] : [];
// The Images batch-count control (always inline when present).
const imagesEl = batch && /*#__PURE__*/_jsxDEV("label", {
className: "field-count",
title: intl.formatMessage(msgs.imagesTitle),
children: [/*#__PURE__*/_jsxDEV("span", {
className: "field-count-label",
children: intl.formatMessage(msgs.images)
}, void 0, false), /*#__PURE__*/_jsxDEV("input", {
type: "number",
min: batch.min ?? 1,
max: batch.max,
value: params.batchSize ?? defaults.batchSize ?? 1,
onChange: e => setParam("batchSize", Math.max(1, Number(e.target.value) || 1)),
"aria-label": intl.formatMessage(msgs.imagesTitle)
}, void 0, false)]
}, void 0, true);
// The Size control — a Size/Ratio select, a text field, or the Width × Height pair.
const sizeEl = sizeField ? /*#__PURE__*/_jsxDEV("label", {
className: "field-count field-size",
title: sizeField.label,
children: [/*#__PURE__*/_jsxDEV("span", {
className: "field-count-label",
children: sizeLabel
}, void 0, false), sizeField.type === "select" ? /*#__PURE__*/_jsxDEV("select", {
className: "field-size-select",
value: params[sizeField.key] ?? defaults[sizeField.key] ?? "",
onChange: e => setParam(sizeField.key, e.target.value),
"aria-label": sizeField.label,
children: sizeOptions.map(o => {
const opt = typeof o === "string" ? {
value: o,
label: o
} : o;
return /*#__PURE__*/_jsxDEV("option", {
value: opt.value,
children: opt.label
}, opt.value, false);
})
}, void 0, false) : /*#__PURE__*/_jsxDEV("input", {
type: sizeField.type === "number" ? "number" : "text",
className: "field-size-text",
value: params[sizeField.key] ?? defaults[sizeField.key] ?? "",
onChange: e => setParam(sizeField.key, e.target.value),
"aria-label": sizeField.label,
placeholder: "1:1"
}, void 0, false)]
}, void 0, true) : width && height ? /*#__PURE__*/_jsxDEV("label", {
className: "field-count field-size",
title: intl.formatMessage(msgs.sizeTitle),
children: [/*#__PURE__*/_jsxDEV("span", {
className: "field-count-label",
children: intl.formatMessage(msgs.size)
}, void 0, false), /*#__PURE__*/_jsxDEV("span", {
className: "field-size-wh",
children: [/*#__PURE__*/_jsxDEV("input", {
type: "number",
min: width.min,
max: width.max,
step: width.step,
value: params.imageWidth ?? defaults.imageWidth ?? 512,
onChange: e => setParam("imageWidth", Number(e.target.value) || 0),
"aria-label": intl.formatMessage(msgs.width)
}, void 0, false), /*#__PURE__*/_jsxDEV("span", {
className: "field-size-x",
"aria-hidden": "true",
children: "×"
}, void 0, false), /*#__PURE__*/_jsxDEV("input", {
type: "number",
min: height.min,
max: height.max,
step: height.step,
value: params.imageHeight ?? defaults.imageHeight ?? 512,
onChange: e => setParam("imageHeight", Number(e.target.value) || 0),
"aria-label": intl.formatMessage(msgs.height)
}, void 0, false)]
}, void 0, true)]
}, void 0, true) : null;
return /*#__PURE__*/_jsxDEV(_Fragment, {
children: [imagesEl, sizeEl && !compact && sizeEl, sizeEl && compact && /*#__PURE__*/_jsxDEV("div", {
className: "size-cog",
children: [/*#__PURE__*/_jsxDEV("button", {
type: "button",
className: `size-cog-btn${sizeOpen ? " on" : ""}`,
"aria-haspopup": "dialog",
"aria-expanded": sizeOpen,
"aria-label": intl.formatMessage(msgs.sizeMenu),
title: intl.formatMessage(msgs.sizeMenu),
onClick: () => setSizeOpen(o => !o),
children: /*#__PURE__*/_jsxDEV(GearIcon, {}, void 0, false)
}, void 0, false), sizeOpen && /*#__PURE__*/_jsxDEV(_Fragment, {
children: [/*#__PURE__*/_jsxDEV("div", {
className: "size-cog-scrim",
onClick: () => setSizeOpen(false),
"aria-hidden": "true"
}, void 0, false), /*#__PURE__*/_jsxDEV("div", {
className: "size-cog-pop",
role: "dialog",
"aria-label": intl.formatMessage(msgs.sizeMenu),
children: sizeEl
}, void 0, false)]
}, void 0, true)]
}, void 0, true)]
}, void 0, true);
}