/**
* The Home composer — a focused two-pane prompt workspace. The left pane is the
* building-block cloud (keywords, lists, expansions, blocks); the right
* pane hosts the reusable {@link PromptComposer} prompt box (with all its bells and
* whistles) above the generated-prompt list.
*
* Image generation is back (per the active provider: api-tier renders into the Gallery;
* the syntax tier copies a formatted prompt). Still removed (see
* notes/plans/removed-pending-readd.md): presets and the Normal/Anime style toggle (the anime
* word lists mix SFW + explicit adult tags and need a proper split first).
* @module gui/components/Home
*/
import { useEffect, useRef, useState } from "react";
import { useIntl, defineMessages } from "react-intl";
import { generatePrompt, renderWrapperPart, expandPrompt } from "../lib/promptEngine.js";
import { shouldReflectSeed } from "../lib/home/seed.js";
import { buildRoll } from "../lib/home/buildRoll.js";
import { getDefaultWrapper } from "../lib/wrapperStore.js";
import { getProvider } from "../lib/providers/index.js";
import { flattenForProvider } from "../lib/useProvider.js";
import { softLockedForNsfw } from "../lib/contentPolicy.js";
import { dialog } from "../lib/dialog.js";
import PromptComposer from "./PromptComposer.jsx";
import PromptResult from "./PromptResult.jsx";
import BlockPalette from "./home/BlockPalette.jsx";
import { BlocksIcon, TrashIcon } from "./icons.jsx";
import { useImageBatches } from "../lib/home/useImageBatches.js";
import { jsxDEV as _jsxDEV } from "react/jsx-dev-runtime";
const msgs = defineMessages({
promptsHeading: {
id: "home.promptsHeading",
defaultMessage: "Prompts"
},
generatedCount: {
id: "home.generatedCount",
defaultMessage: "{count} generated · {provider}"
},
clearAllTitle: {
id: "home.clearAllTitle",
defaultMessage: "Clear all results"
},
clearAll: {
id: "home.clearAll",
defaultMessage: "Clear all"
},
example: {
id: "home.example",
defaultMessage: "Example:"
},
nsfwProceed: {
id: "home.nsfwProceed",
defaultMessage: "NSFW mode is on. Generate with {provider} anyway?"
},
openBlocks: {
id: "home.openBlocks",
defaultMessage: "Building blocks"
},
openBlocksAria: {
id: "home.openBlocksAria",
defaultMessage: "Open building blocks"
}
});
/**
* The compose workspace.
* @param {object} props
* @param {object} props.settings The current settings.
* @param {Function} props.setSettings Update the settings.
* @param {Function} [props.onOpenImage] Open a generated image (by served path) in the single view.
* @returns {JSX.Element}
*/
export default function Home({
settings,
setSettings,
onOpenImage
}) {
const intl = useIntl();
const [paletteOpen, setPaletteOpen] = useState(false); // phone: building-block drawer open?
// Hover tooltip for a building block: its label, description (piped from the v3 file /
// sidecar), and a LIVE example output that re-rolls while the pointer rests on the chip.
const [tip, setTip] = useState(null); // { token, label, description, x, y }
const [tipEx, setTipEx] = useState("");
const composerRef = useRef(null); // the PromptComposer — palette inserts reach its active editor
// --- Active image provider (selection lives in settings; knobs are per-provider) ---
const provider = getProvider(settings.provider);
const [providerFmt, setProviderFmt] = useState(null); // syntax/plain tier: prompt formatter
const [providerDefaults, setProviderDefaults] = useState({});
useEffect(() => {
let alive = true;
const p = getProvider(settings.provider);
Promise.resolve(p?.loadSettings ? p.loadSettings() : {
defaults: {}
}).then(m => alive && setProviderDefaults(m?.defaults || {}));
if (p?.loadFormat) p.loadFormat().then(f => alive && setProviderFmt(() => f));else setProviderFmt(null);
return () => {
alive = false;
};
}, [settings.provider]);
// The flat settings an adapter/formatter reads: app settings + this provider's namespaced
// params (under its schema defaults) + the dialect mode.
const flat = flattenForProvider(settings, providerDefaults);
const canGenerateImages = provider?.tier === "api" && !!provider?.loadGenerate;
// The generated-prompt list and its image-batch lifecycle (run a batch + the rewrite passes,
// and remove/clear images/batches/all) live in their own hook; Home just drives them.
const {
prompts,
setPrompts,
nextId,
imgError,
makeBatch,
removeImage,
removeBatch,
clearImages,
clearAll
} = useImageBatches({
settings,
provider,
flat
});
const settingsRef = useRef(settings);
settingsRef.current = settings;
// Phone: dismiss the building-block drawer on Escape (the scrim handles tap-away). Client-only,
// so it never runs during the prerender.
useEffect(() => {
if (!paletteOpen) return undefined;
const onKey = e => {
if (e.key === "Escape") setPaletteOpen(false);
};
document.addEventListener("keydown", onKey);
return () => document.removeEventListener("keydown", onKey);
}, [paletteOpen]);
// The palette inserts a building block into the composer's active editor (prompt or negative).
function insert(token) {
composerRef.current?.insert(token);
}
// --- Building-block hover tooltip (label + description + a refreshing example) ---
const showTip = (item, e) => setTip({
token: item.token,
label: item.label,
description: item.description,
x: e.clientX,
y: e.clientY
});
const moveTip = e => setTip(t => t ? {
...t,
x: e.clientX,
y: e.clientY
} : t);
const hideTip = () => setTip(null);
// While a tip is shown, expand its token into a fresh example, re-rolling on an interval.
// Examples are rendered WITHOUT the auto fx/artists framing so they show just the block.
const tipToken = tip?.token;
useEffect(() => {
if (!tipToken) {
setTipEx("");
return undefined;
}
const roll = () => {
try {
setTipEx(expandPrompt(tipToken, {
...settingsRef.current,
autoAddFx: false,
autoAddArtists: false
}));
} catch {
setTipEx("");
}
};
roll();
const id = setInterval(roll, 1400);
return () => clearInterval(id);
}, [tipToken]);
// Generate from the effective prompt text (supplied by the composer). Frame each prompt with the
// active wrapper, roll the batch, and auto-fire an image batch per prompt (api providers only).
// Errors propagate to the composer, which surfaces them inline.
async function buildPrompts(text) {
const w = !settings.wrapperName || settings.wrapperName === "Default" ? getDefaultWrapper() : settings.wrapper ?? getDefaultWrapper();
const {
prompts: out,
rollSeed
} = buildRoll({
settings,
text,
wrapper: w,
mode: flat.mode,
deps: {
renderWrapperPart,
generatePrompt,
nextId
}
});
// A new roll ADDS to the list, newest on top (Clear all / per-prompt clear to remove).
setPrompts(prev => [...out, ...prev]);
if (shouldReflectSeed(settings, rollSeed)) setSettings({
...settings,
promptSeed: rollSeed
});
// Auto-render: kick off an image batch for each new prompt (api providers only). If the chosen
// provider is safe-for-work-only and NSFW mode is on, ask once before sending.
const nsfwOk = !softLockedForNsfw(provider, settings.includeAdult) || (await dialog.confirm({
message: intl.formatMessage(msgs.nsfwProceed, {
provider: provider?.label
})
}));
if (canGenerateImages && nsfwOk) out.forEach(p => makeBatch(p.id, p.text, p.dpl));
}
// Copy the prompt. For a syntax/plain provider (e.g. Midjourney) copy the FORMATTED prompt
// (with the provider's --params), which is the whole point of the syntax tier.
function copyPrompt(p) {
const text = provider?.tier !== "api" && providerFmt ? providerFmt(p, flat) : p;
navigator.clipboard?.writeText(text).catch(() => {});
}
return /*#__PURE__*/_jsxDEV("div", {
className: `workspace home${paletteOpen ? " palette-open" : ""}`,
children: [/*#__PURE__*/_jsxDEV(BlockPalette, {
includeAdult: settings.includeAdult,
onInsert: insert,
onShowTip: showTip,
onMoveTip: moveTip,
onHideTip: hideTip,
onClose: () => setPaletteOpen(false)
}, void 0, false), /*#__PURE__*/_jsxDEV("div", {
className: "palette-scrim",
onClick: () => setPaletteOpen(false),
"aria-hidden": "true"
}, void 0, false), /*#__PURE__*/_jsxDEV("button", {
type: "button",
className: "palette-trigger",
onClick: () => setPaletteOpen(true),
"aria-controls": "block-palette",
"aria-expanded": paletteOpen,
"aria-label": intl.formatMessage(msgs.openBlocksAria),
title: intl.formatMessage(msgs.openBlocks),
children: /*#__PURE__*/_jsxDEV(BlocksIcon, {}, void 0, false)
}, void 0, false), /*#__PURE__*/_jsxDEV("div", {
className: "main-col",
children: [/*#__PURE__*/_jsxDEV(PromptComposer, {
ref: composerRef,
settings: settings,
setSettings: setSettings,
onGenerate: buildPrompts
}, void 0, false), imgError && /*#__PURE__*/_jsxDEV("p", {
className: "error",
role: "alert",
children: imgError
}, void 0, false), prompts.length > 0 && /*#__PURE__*/_jsxDEV("section", {
className: "card results-card",
children: [/*#__PURE__*/_jsxDEV("div", {
className: "results-head",
children: [/*#__PURE__*/_jsxDEV("h2", {
children: intl.formatMessage(msgs.promptsHeading)
}, void 0, false), /*#__PURE__*/_jsxDEV("div", {
className: "results-head-right",
children: [/*#__PURE__*/_jsxDEV("span", {
className: "count",
children: intl.formatMessage(msgs.generatedCount, {
count: prompts.length,
provider: provider?.label
})
}, void 0, false), /*#__PURE__*/_jsxDEV("button", {
className: "clear-all-btn",
onClick: clearAll,
title: intl.formatMessage(msgs.clearAllTitle),
"aria-label": intl.formatMessage(msgs.clearAll),
children: /*#__PURE__*/_jsxDEV(TrashIcon, {}, void 0, false)
}, void 0, false)]
}, void 0, true)]
}, void 0, true), /*#__PURE__*/_jsxDEV("ul", {
className: "prompts",
children: prompts.map((p, i) => /*#__PURE__*/_jsxDEV(PromptResult, {
prompt: p,
index: i,
number: prompts.length - i,
settings: settings,
canGenerate: canGenerateImages,
onGenerate: makeBatch,
onCopy: copyPrompt,
onRemoveImage: removeImage,
onRemoveBatch: removeBatch,
onClearImages: clearImages,
onImageClick: onOpenImage
}, p.id, false))
}, void 0, false)]
}, void 0, true)]
}, void 0, true), tip && /*#__PURE__*/_jsxDEV("div", {
className: "block-tip",
style: (() => {
const vw = typeof window !== "undefined" ? window.innerWidth : 9999;
const vh = typeof window !== "undefined" ? window.innerHeight : 9999;
const left = Math.max(8, Math.min(tip.x + 16, vw - 360));
// If the pointer is in the lower part of the screen, anchor the tip's bottom above it.
return tip.y > vh * 0.6 ? {
left,
bottom: vh - tip.y + 18,
maxHeight: tip.y - 16
} : {
left,
top: tip.y + 18,
maxHeight: vh - tip.y - 28
};
})(),
role: "tooltip",
children: [/*#__PURE__*/_jsxDEV("div", {
className: "block-tip-name",
children: [/*#__PURE__*/_jsxDEV("span", {
className: "block-tip-label",
children: tip.label
}, void 0, false), /*#__PURE__*/_jsxDEV("code", {
className: "block-tip-token",
children: tip.token
}, void 0, false)]
}, void 0, true), tip.description && /*#__PURE__*/_jsxDEV("div", {
className: "block-tip-desc",
children: tip.description
}, void 0, false), tipEx && /*#__PURE__*/_jsxDEV("div", {
className: "block-tip-ex",
children: [/*#__PURE__*/_jsxDEV("span", {
className: "block-tip-ex-label",
children: intl.formatMessage(msgs.example)
}, void 0, false), " ", tipEx]
}, void 0, true)]
}, void 0, true)]
}, void 0, true);
}