/**
* One generated prompt and its image batches. Each "Generate images" press adds a batch beneath
* the prompt. Per image: open in the OS default app, reveal in Explorer, or remove (with an option
* to delete from disk). A batch and the whole prompt can be cleared too. Clicking an image opens it
* in the single view.
*
* The prompt text and its source DPL / original are all **click-to-copy** (no separate copy button).
* Hovering shows the full text; hovering the **DPL** also shows a live example that re-rolls every
* second (so you can see the range of what that DPL produces).
* @module gui/components/PromptResult
*/
import { memo, useEffect, useRef, useState } from "react";
import { useIntl, defineMessages } from "react-intl";
import { isOutputFile, openImageFile, revealImageFile, openImageInNewTab } from "../lib/output.js";
import { expandPrompt } from "../lib/promptEngine.js";
import { TrashIcon } from "./icons.jsx";
import { jsxDEV as _jsxDEV, Fragment as _Fragment } from "react/jsx-dev-runtime";
const msgs = defineMessages({
resultAlt: {
id: "promptResult.resultAlt",
defaultMessage: "result"
},
dplTitle: {
id: "promptResult.dplTitle",
defaultMessage: "The DPL this was rolled from — click to copy"
},
example: {
id: "promptResult.example",
defaultMessage: "Example:"
},
originalTitle: {
id: "promptResult.originalTitle",
defaultMessage: "Original prompt (before auto-fix) — click to copy\n\n{original}"
},
copyTitle: {
id: "promptResult.copyTitle",
defaultMessage: "Click to copy\n\n{text}"
},
genTitle: {
id: "promptResult.genTitle",
defaultMessage: "Generate a batch of images for this prompt"
},
moreImages: {
id: "promptResult.moreImages",
defaultMessage: "More images"
},
genImages: {
id: "promptResult.genImages",
defaultMessage: "Generate images"
},
clearTitle: {
id: "promptResult.clearTitle",
defaultMessage: "Clear this prompt's images (asks about deleting from disk)"
},
clear: {
id: "promptResult.clear",
defaultMessage: "clear"
},
batch: {
id: "promptResult.batch",
defaultMessage: "Batch {n}"
},
rendering: {
id: "promptResult.rendering",
defaultMessage: "rendering…"
},
removeBatch: {
id: "promptResult.removeBatch",
defaultMessage: "remove batch"
},
renderingAria: {
id: "promptResult.renderingAria",
defaultMessage: "rendering"
},
openSingle: {
id: "promptResult.openSingle",
defaultMessage: "Open in the single view"
},
openNewTab: {
id: "promptResult.openNewTab",
defaultMessage: "Open in a new tab"
},
openDefault: {
id: "promptResult.openDefault",
defaultMessage: "Open in default app"
},
reveal: {
id: "promptResult.reveal",
defaultMessage: "Reveal in file explorer"
},
removeImage: {
id: "promptResult.removeImage",
defaultMessage: "Remove image"
}
});
/**
* A result image that fades in once it has actually loaded.
* @param {object} props `{ src }`.
* @returns {JSX.Element}
*/
function ResultImage({
src
}) {
const intl = useIntl();
const [loaded, setLoaded] = useState(false);
return /*#__PURE__*/_jsxDEV("img", {
src: src,
alt: intl.formatMessage(msgs.resultAlt),
className: `result-img${loaded ? " loaded" : ""}`,
loading: "lazy",
onLoad: () => setLoaded(true)
}, void 0, false);
}
/**
* The source-DPL line: click-to-copy, with a hover tooltip showing the full DPL and a concrete
* example that re-rolls every second (the same live preview the building-block chips use).
* @param {object} props `{ dpl, settings }`.
* @returns {JSX.Element}
*/
function DplHoverCode({
dpl,
settings
}) {
const intl = useIntl();
const [hover, setHover] = useState(false);
const [ex, setEx] = useState("");
const settingsRef = useRef(settings);
settingsRef.current = settings;
useEffect(() => {
if (!hover) {
setEx("");
return undefined;
}
const roll = () => {
try {
setEx(expandPrompt(dpl, {
...settingsRef.current,
autoAddFx: false,
autoAddArtists: false
}));
} catch {
setEx("");
}
};
roll();
const id = setInterval(roll, 1000);
return () => clearInterval(id);
}, [hover, dpl]);
return /*#__PURE__*/_jsxDEV("span", {
className: "dpl-hover",
onMouseEnter: () => setHover(true),
onMouseLeave: () => setHover(false),
children: [/*#__PURE__*/_jsxDEV("code", {
className: "prompt-dpl",
title: intl.formatMessage(msgs.dplTitle),
onClick: () => navigator.clipboard?.writeText(dpl).catch(() => {}),
children: dpl
}, void 0, false), hover && /*#__PURE__*/_jsxDEV("div", {
className: "dpl-hover-tip",
role: "tooltip",
children: [/*#__PURE__*/_jsxDEV("div", {
className: "dpl-hover-full",
children: dpl
}, void 0, false), ex && /*#__PURE__*/_jsxDEV("div", {
className: "dpl-hover-ex",
children: [/*#__PURE__*/_jsxDEV("span", {
className: "dpl-hover-ex-label",
children: intl.formatMessage(msgs.example)
}, void 0, false), " ", ex]
}, void 0, true)]
}, void 0, true)]
}, void 0, true);
}
const ImageIcon = () => /*#__PURE__*/_jsxDEV("svg", {
width: "15",
height: "15",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
strokeWidth: "2",
strokeLinecap: "round",
strokeLinejoin: "round",
"aria-hidden": "true",
children: [/*#__PURE__*/_jsxDEV("rect", {
x: "3",
y: "3",
width: "18",
height: "18",
rx: "2",
ry: "2"
}, void 0, false), /*#__PURE__*/_jsxDEV("circle", {
cx: "8.5",
cy: "8.5",
r: "1.5"
}, void 0, false), /*#__PURE__*/_jsxDEV("polyline", {
points: "21 15 16 10 5 21"
}, void 0, false)]
}, void 0, true);
/**
* @param {object} props
* @param {object} props.prompt `{ id, text, batches: [{ id, busy, images }] }`.
* @param {number} props.index Zero-based index in the list.
* @param {object} props.settings The generation settings (for the DPL example tooltip).
* @param {boolean} props.canGenerate Whether the active provider can render images.
* @param {Function} props.onGenerate `(promptId)`.
* @param {Function} props.onCopy `(text)`.
* @param {Function} props.onRemoveImage `(promptId, batchId, img)`.
* @param {Function} props.onRemoveBatch `(promptId, batchId)`.
* @param {Function} props.onClearImages `(promptId)`.
* @param {Function} [props.onImageClick] `(img)` — open the image in the single view.
* @returns {JSX.Element}
*/
function PromptResult({
prompt,
index,
number,
settings,
canGenerate,
onGenerate,
onCopy,
onRemoveImage,
onRemoveBatch,
onClearImages,
onImageClick
}) {
const intl = useIntl();
const hasImages = prompt.batches.some(b => b.images.length);
return /*#__PURE__*/_jsxDEV("li", {
className: "prompt-result",
children: [/*#__PURE__*/_jsxDEV("div", {
className: "prompt-line",
children: [/*#__PURE__*/_jsxDEV("span", {
className: "idx",
children: String(number ?? index + 1).padStart(2, "0")
}, void 0, false), /*#__PURE__*/_jsxDEV("div", {
className: "prompt-main",
children: [prompt.dpl && /*#__PURE__*/_jsxDEV(DplHoverCode, {
dpl: prompt.dpl,
settings: settings
}, void 0, false), prompt.original && /*#__PURE__*/_jsxDEV("code", {
className: "prompt-dpl prompt-original",
title: intl.formatMessage(msgs.originalTitle, {
original: prompt.original
}),
onClick: () => navigator.clipboard?.writeText(prompt.original).catch(() => {}),
children: prompt.original
}, void 0, false), /*#__PURE__*/_jsxDEV("span", {
className: "prompt-text",
title: intl.formatMessage(msgs.copyTitle, {
text: prompt.text
}),
onClick: () => onCopy(prompt.text),
children: prompt.text
}, void 0, false)]
}, void 0, true), /*#__PURE__*/_jsxDEV("div", {
className: "prompt-actions",
children: [canGenerate && /*#__PURE__*/_jsxDEV("button", {
className: "gen-btn",
onClick: () => onGenerate(prompt.id),
title: intl.formatMessage(msgs.genTitle),
children: [/*#__PURE__*/_jsxDEV(ImageIcon, {}, void 0, false), /*#__PURE__*/_jsxDEV("span", {
className: "gen-btn-label",
children: intl.formatMessage(prompt.batches.length ? msgs.moreImages : msgs.genImages)
}, void 0, false)]
}, void 0, true), hasImages && /*#__PURE__*/_jsxDEV("button", {
className: "copy-mini clear-icon-btn",
title: intl.formatMessage(msgs.clearTitle),
"aria-label": intl.formatMessage(msgs.clearTitle),
onClick: () => onClearImages(prompt.id),
children: /*#__PURE__*/_jsxDEV(TrashIcon, {}, void 0, false)
}, void 0, false)]
}, void 0, true)]
}, void 0, true), prompt.batches.map((b, bi) => /*#__PURE__*/_jsxDEV("div", {
className: "batch",
children: [/*#__PURE__*/_jsxDEV("div", {
className: "batch-head",
children: [/*#__PURE__*/_jsxDEV("span", {
className: "batch-label",
children: intl.formatMessage(msgs.batch, {
n: prompt.batches.length - bi
})
}, void 0, false), b.busy ? /*#__PURE__*/_jsxDEV("span", {
className: "batch-status",
children: intl.formatMessage(msgs.rendering)
}, void 0, false) : b.images.length > 0 && /*#__PURE__*/_jsxDEV("button", {
className: "link-btn",
onClick: () => onRemoveBatch(prompt.id, b.id),
children: intl.formatMessage(msgs.removeBatch)
}, void 0, false)]
}, void 0, true), b.busy ? /*#__PURE__*/_jsxDEV("div", {
className: "gallery",
children: Array.from({
length: b.count || 1
}).map((_, i) => /*#__PURE__*/_jsxDEV("figure", {
className: "skeleton-fig",
children: /*#__PURE__*/_jsxDEV("div", {
className: "img-skeleton",
"aria-label": intl.formatMessage(msgs.renderingAria)
}, void 0, false)
}, i, false))
}, void 0, false) : /*#__PURE__*/_jsxDEV("div", {
className: "gallery",
children: b.images.map(img => /*#__PURE__*/_jsxDEV("figure", {
children: [/*#__PURE__*/_jsxDEV("a", {
href: img,
target: "_blank",
rel: "noreferrer",
title: intl.formatMessage(onImageClick ? msgs.openSingle : msgs.openNewTab),
onClick: e => {
e.preventDefault();
if (onImageClick) onImageClick(img);else openImageInNewTab(img);
},
children: /*#__PURE__*/_jsxDEV(ResultImage, {
src: img
}, void 0, false)
}, void 0, false), /*#__PURE__*/_jsxDEV("div", {
className: "img-actions",
children: [isOutputFile(img) && /*#__PURE__*/_jsxDEV(_Fragment, {
children: [/*#__PURE__*/_jsxDEV("button", {
title: intl.formatMessage(msgs.openDefault),
onClick: () => openImageFile(img),
children: "↗"
}, void 0, false), /*#__PURE__*/_jsxDEV("button", {
title: intl.formatMessage(msgs.reveal),
onClick: () => revealImageFile(img),
children: "⌖"
}, void 0, false)]
}, void 0, true), /*#__PURE__*/_jsxDEV("button", {
title: intl.formatMessage(msgs.removeImage),
onClick: () => onRemoveImage(prompt.id, b.id, img),
children: "✕"
}, void 0, false)]
}, void 0, true)]
}, img, true))
}, void 0, false)]
}, b.id, true))]
}, void 0, true);
}
/**
* Re-render a row only when its own data changes. Crucially, an image batch resolving elsewhere
* updates just that prompt's object (the `setPrompts` maps keep the SAME reference for every other
* row), so at 1000 rows a single image landing re-renders one `<li>`, not the whole list. The
* handler props are intentionally excluded from the compare: they act via `promptId` + functional
* `setPrompts` + a live prompts ref, so a retained closure stays correct even if its identity is
* stale.
* @param {object} prev Previous props.
* @param {object} next Next props.
* @returns {boolean} True when the row can skip re-rendering.
*/
function sameRow(prev, next) {
return prev.prompt === next.prompt && prev.index === next.index && prev.number === next.number && prev.canGenerate === next.canGenerate && prev.settings === next.settings && prev.onImageClick === next.onImageClick;
}
export default /*#__PURE__*/memo(PromptResult, sameRow);