/**
* @file
* @brief Framework-agnostic prompt engine: createEngine(loader) runs the same pipeline as the CLI over a prompt string. Notes: notes/systems/core-engine.md.
*/
// The framework-agnostic prompt engine.
//
// `createEngine(loader)` returns an engine that runs the prompt-module pipeline
// (the same stages and order as the Node CLI) over a prompt string. All data
// access — expansions, lists, dynamic prompts — goes through the injected
// `loader`, so the identical engine runs in Node (fs + createRequire loader) and
// in the browser (Vite import.meta.glob loader). See notes/plans/web-migration.md.
//
// Loader interface:
// readExpansion(name) -> string | null
// readListLines(name) -> string[] | null
// listNames() -> string[]
// loadDynamicPrompt(key) -> { default, full?, suggestion_exclude? } | null
//
// The pure stages (prompt-salt, cleanup) and the random* helpers are imported
// and reused directly — only the file/plugin access is reimplemented behind the
// loader, so there is no duplicated prompt logic.
import baseSettings from "../settings.js";
import promptSalt from "../prompt-modules/prompt-salt.js";
import cleanup from "../prompt-modules/cleanup.js";
import { makeExpansionStage } from "./stages/expansion.js";
import { makeDynamicPromptStage } from "./stages/dynamicPrompt.js";
import { makeListStage } from "./stages/list.js";
import { createListStore } from "./listStore.js";
const DEFAULT_ORDER = [
"expansion",
"dynamic-prompt",
"expansion",
"dynamic-prompt",
"prompt-salt",
"list",
"cleanup",
];
/**
* Create a framework-agnostic prompt engine that runs the same pipeline as the CLI.
* @param {object} loader Data-access loader (Node fs or browser glob): `readExpansion`,
* `readListLines`, `listNames`, `loadDynamicPrompt`.
* @returns {{expand: Function, generate: Function, generateMany: Function}} The engine API.
*/
export function createEngine(loader) {
const store = createListStore(loader);
const stages = {
expansion: makeExpansionStage(loader),
"dynamic-prompt": makeDynamicPromptStage(loader),
"prompt-salt": promptSalt,
list: makeListStage(store),
cleanup,
};
/**
* Run the prompt-module pipeline (in `settings.promptModules` order) over a prompt.
* @param {string} prompt The seed prompt.
* @param {object} settings The merged settings.
* @param {object} imageSettings Per-generation image-settings scratch.
* @param {object} upscaleSettings Per-generation upscale-settings scratch.
* @returns {string} The fully expanded prompt.
*/
function expand(prompt, settings, imageSettings, upscaleSettings) {
const order = settings.promptModules || DEFAULT_ORDER;
for (const name of order) {
const stage = stages[name];
if (!stage) continue;
prompt = stage(prompt, settings, imageSettings, upscaleSettings);
}
// Drop stray carriage returns, like the CLI does after the pipeline.
return prompt.replaceAll("\r", "");
}
// Generate a single prompt. Defaults from settings.js are merged under the
// caller's settings so every field the stages read is present, and a shallow
// copy is used so per-generation mutations (auto-fx toggles, etc.) don't leak.
/**
* Generate a single prompt from default settings merged under the caller's overrides.
* @param {object} [userSettings] Settings overrides (e.g. `{ prompt, mode }`).
* @returns {string} One generated prompt.
*/
function generate(userSettings = {}) {
store.reset();
const settings = { ...baseSettings, ...userSettings };
const imageSettings = {};
const upscaleSettings = {};
return expand(settings.prompt ?? "{#random-words}", settings, imageSettings, upscaleSettings);
}
/**
* Generate `userSettings.promptCount` prompts (minimum 1).
* @param {object} [userSettings] Settings overrides.
* @returns {string[]} The generated prompts.
*/
function generateMany(userSettings = {}) {
const count = Math.max(1, Number(userSettings.promptCount) || 1);
return Array.from({ length: count }, () => generate(userSettings));
}
return { expand, generate, generateMany };
}