From 92ed011876ff1b121f6cbd68c376b7bb4176632f Mon Sep 17 00:00:00 2001
From: Svornost <11434-svornost@users.noreply.gitgud.io>
Date: Mon, 9 Oct 2023 22:25:04 -0400
Subject: [PATCH] Add per-slave prompt customization to the Customize tab in
 Slave Interact.

---
 src/art/genAI/buildPrompt.js       |  1 +
 src/art/genAI/posturePromptPart.js |  4 ++
 src/interaction/siCustom.js        | 98 +++++++++++++++++++++++++++++-
 src/js/SlaveState.js               | 19 ++++++
 4 files changed, 120 insertions(+), 2 deletions(-)

diff --git a/src/art/genAI/buildPrompt.js b/src/art/genAI/buildPrompt.js
index a1015ba089f..5e6fff9992e 100644
--- a/src/art/genAI/buildPrompt.js
+++ b/src/art/genAI/buildPrompt.js
@@ -30,6 +30,7 @@ function buildPrompt(slave) {
 		new App.Art.GenAI.PiercingsPromptPart(slave),
 		new App.Art.GenAI.HealthPromptPart(slave),
 		new App.Art.GenAI.PubicHairPromptPart(slave),
+		new App.Art.GenAI.CustomPromptPart(slave),
 	];
 	return new App.Art.GenAI.Prompt(prompts);
 }
diff --git a/src/art/genAI/posturePromptPart.js b/src/art/genAI/posturePromptPart.js
index 9653828fe56..4ec86fef09a 100644
--- a/src/art/genAI/posturePromptPart.js
+++ b/src/art/genAI/posturePromptPart.js
@@ -3,6 +3,10 @@ App.Art.GenAI.PosturePromptPart = class PosturePromptPart extends App.Art.GenAI.
 	 * @returns {string}
 	 */
 	positive() {
+		if (this.slave.custom.aiPrompts?.pose) {
+			return this.slave.custom.aiPrompts.pose;
+		}
+
 		let devotionPart;
 		if (this.slave.devotion < -50) {
 			devotionPart = `standing, from side, arms crossed`;
diff --git a/src/interaction/siCustom.js b/src/interaction/siCustom.js
index 21fb5d5ff2e..7f2dc02dd5b 100644
--- a/src/interaction/siCustom.js
+++ b/src/interaction/siCustom.js
@@ -18,6 +18,7 @@ App.UI.SlaveInteract.custom = function(slave, refresh) {
 		customSlaveImage(),
 		customHairImage(),
 		artSeed(),
+		aiPrompts(),
 	);
 
 	App.UI.DOM.appendNewElement("h3", el, `Names`);
@@ -710,11 +711,104 @@ App.UI.SlaveInteract.custom = function(slave, refresh) {
 				button
 			));
 		}
+		return frag;
+	}
+
+	function aiPrompts() {
+		function posePrompt() {
+			let el = document.createElement('p');
+			el.append(`Override ${his} pose: `);
+			el.appendChild(
+				App.UI.DOM.makeTextBox(
+					slave.custom.aiPrompts.pose,
+					v => {
+						slave.custom.aiPrompts.pose = v;
+						$(promptDiv).empty().append(genAIPrompt());
+					}
+				)
+			);
+
+			let choices = document.createElement('div');
+			choices.className = "choices";
+			choices.appendChild(App.UI.DOM.makeElement('span', ` This prompt will replace the default body pose prompts. Example: 'kneeling, arms behind back'`, 'note'));
+			el.appendChild(choices);
+			return el;
+		}
+
+		function positivePrompt() {
+			let el = document.createElement('p');
+			el.append(`Add positive prompts: `);
+			el.appendChild(
+				App.UI.DOM.makeTextBox(
+					slave.custom.aiPrompts.positive,
+					v => {
+						slave.custom.aiPrompts.positive = v;
+						$(promptDiv).empty().append(genAIPrompt());
+					}
+				)
+			);
+
+			let choices = document.createElement('div');
+			choices.className = "choices";
+			choices.appendChild(App.UI.DOM.makeElement('span', ` Prompts specified here will be appended to the end of the dynamic positive prompt; specify things you want to see in the rendered image.`, 'note'));
+			el.appendChild(choices);
+			return el;
+		}
+
+		function negativePrompt() {
+			let el = document.createElement('p');
+			el.append(`Add negative prompts: `);
+			el.appendChild(
+				App.UI.DOM.makeTextBox(
+					slave.custom.aiPrompts.negative,
+					v => {
+						slave.custom.aiPrompts.negative = v;
+						$(promptDiv).empty().append(genAIPrompt());
+					}
+				)
+			);
+
+			let choices = document.createElement('div');
+			choices.className = "choices";
+			choices.appendChild(App.UI.DOM.makeElement('span', ` Prompts specified here will be appended to the end of the dynamic negative prompt; specify things you don't want to see in the rendered image.`, 'note'));
+			el.appendChild(choices);
+			return el;
+		}
+
+		const frag = new DocumentFragment();
 
 		// Debug information for AI art, or prompt suggestions for custom images
-		if ((V.imageChoice === 6 && V.debugMode === 1) || (V.seeCustomImagesOnly && V.aiCustomImagePrompts)) {
-			frag.append(genAIPrompt());
+		const promptDiv = App.UI.DOM.makeElement('div');
+		if ((V.imageChoice === 6 && (V.debugMode === 1 || slave.custom.aiPrompts)) || (V.seeCustomImagesOnly && V.aiCustomImagePrompts)) {
+			promptDiv.append(genAIPrompt());
+		} else if (V.imageChoice === 6) {
+			promptDiv.append(App.UI.DOM.link("Show AI Prompts", f => {
+				$(promptDiv).empty().append(genAIPrompt());
+			}));
+		}
+		frag.append(promptDiv);
+
+		// Custom prompt parts
+		const customDiv = App.UI.DOM.makeElement('div');
+		if (V.imageChoice === 6 || (V.seeCustomImagesOnly && V.aiCustomImagePrompts)) {
+			if (slave.custom.aiPrompts) {
+				customDiv.append(
+					posePrompt(),
+					positivePrompt(),
+					negativePrompt(),
+				);
+				customDiv.append(App.UI.DOM.link("Disable Prompt Customization", f => {
+					delete slave.custom.aiPrompts;
+					refresh();
+				}));
+			} else {
+				customDiv.append(App.UI.DOM.link("Customize AI Prompts", f => {
+					slave.custom.aiPrompts = new App.Entity.SlaveCustomAIPrompts();
+					refresh();
+				}));
+			}
 		}
+		frag.append(customDiv);
 		return frag;
 	}
 
diff --git a/src/js/SlaveState.js b/src/js/SlaveState.js
index e22a6d40131..1fbff2dd119 100644
--- a/src/js/SlaveState.js
+++ b/src/js/SlaveState.js
@@ -351,6 +351,20 @@ App.Entity.SlaveActionsCountersState = class {
 	}
 };
 
+/**
+ * Encapsulates custom AI prompts
+ */
+App.Entity.SlaveCustomAIPrompts = class SlaveCustomAIPrompts {
+	constructor() {
+		/** replaces the slave's posture prompts with a custom string for user-specified poses */
+		this.pose = "";
+		/** manually adds to the dynamic positive prompt string */
+		this.positive = "";
+		/** manually adds to the dynamic negative prompt string */
+		this.negative = "";
+	}
+};
+
 /**
  * Encapsulates various custom properties, set by users
  */
@@ -389,6 +403,11 @@ App.Entity.SlaveCustomAddonsState = class SlaveCustomAddonsState {
 		 * @type {number}
 		 */
 		this.aiImageId = null;
+		/**
+		 * custom AI prompts; may be null or absent
+		 * @type {App.Entity.SlaveCustomAIPrompts}
+		 */
+		this.aiPrompts = null;
 	}
 };
 
-- 
GitLab