add prompt history as suggested in #87

Signed-off-by: Victor Seiji Hariki <victorseijih@gmail.com>
This commit is contained in:
Victor Seiji Hariki 2022-12-12 18:25:32 -03:00
parent 34b995998b
commit 0a73687556
14 changed files with 526 additions and 81 deletions

View file

@ -331,13 +331,48 @@ input#host {
/* Prompt Fields */
.content.prompt {
display: flex;
align-items: stretch;
}
.content.prompt > .inputs {
width: 200px;
}
div.prompt-wrapper {
display: flex;
width: calc(100%);
overflow: visible;
}
div.prompt-wrapper > textarea {
div.prompt-wrapper > * {
flex-shrink: 0;
}
div.prompt-wrapper textarea {
margin: 0;
padding: 0;
border-radius: 0;
border: none;
z-index: 1;
}
div.prompt-wrapper:not(:first-child) textarea {
border-top: 1px solid black;
}
div.prompt-wrapper > textarea {
box-sizing: border-box;
width: calc(100% - 20px);
padding: 2px;
transition-duration: 200ms;
resize: vertical;
}
@ -346,6 +381,198 @@ div.prompt-wrapper > textarea:focus {
width: 700px;
}
div.prompt-wrapper > .prompt-indicator {
display: flex;
cursor: help;
width: 20px;
}
div.prompt-wrapper:first-child > .prompt-indicator {
border-top-left-radius: 5px;
}
div.prompt-wrapper:last-child > .prompt-indicator {
border-bottom-left-radius: 5px;
}
div.prompt-wrapper > .prompt-indicator.positive {
background-color: #484;
}
div.prompt-wrapper > .prompt-indicator.negative {
background-color: #844;
}
div.prompt-wrapper > .prompt-indicator.styles {
background-color: #448;
}
div.prompt-wrapper > .prompt-indicator::after {
content: "";
display: block;
margin: auto;
width: 16px;
height: 16px;
background-color: var(--c-text);
mask-size: contain;
-webkit-mask-size: contain;
}
div.prompt-wrapper > .prompt-indicator.positive::after {
mask-image: url("/res/icons/plus-square.svg");
-webkit-mask-image: url("/res/icons/plus-square.svg");
}
div.prompt-wrapper > .prompt-indicator.negative::after {
mask-image: url("/res/icons/minus-square.svg");
-webkit-mask-image: url("/res/icons/minus-square.svg");
}
div.prompt-wrapper > .prompt-indicator.styles::after {
mask-image: url("/res/icons/library.svg");
-webkit-mask-image: url("/res/icons/library.svg");
}
.prompt-history-wrapper {
position: relative;
flex-shrink: 0;
width: 20px;
}
.prompt-history-container {
display: flex;
position: absolute;
top: 0;
left: 0;
height: 100%;
}
#prompt-history {
width: 0px;
height: 100%;
transition-duration: 200ms;
background-color: #1e1e50;
}
#prompt-history.expanded {
width: 300px;
}
#prompt-history .entry {
display: flex;
align-items: stretch;
justify-content: stretch;
border: 1px #fff3;
height: 25px;
}
#prompt-history.expanded .entry > button {
padding: 2px;
padding-left: 5px;
}
#prompt-history .entry > button {
flex: 1;
cursor: pointer;
margin: 0;
border: 0;
padding: 0;
color: var(--c-text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition-duration: 100ms;
}
#prompt-history .entry:hover > button:not(:hover) {
flex-grow: 0;
flex-shrink: 1;
flex-basis: 20%;
width: 20%;
}
#prompt-history .entry > button.prompt {
background-color: #484;
}
#prompt-history .entry > button.negative {
background-color: #844;
}
#prompt-history .entry > button.styles {
background-color: #448;
}
#prompt-history .entry > button:hover {
filter: brightness(115%);
backdrop-filter: brightness(115%);
}
#prompt-history .entry > button:active {
filter: brightness(150%);
backdrop-filter: brightness(150%);
}
button.prompt-history-btn {
cursor: pointer;
border-radius: 0;
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
background-color: #1e1e50;
margin: 0;
padding: 0;
border: 0;
width: 20px;
}
button.prompt-history-btn::after {
content: "";
display: block;
margin: auto;
width: 16px;
height: 16px;
background-color: var(--c-text);
mask-size: contain;
-webkit-mask-size: contain;
mask-image: url("/res/icons/history.svg");
-webkit-mask-image: url("/res/icons/history.svg");
}
button.prompt-history-btn:hover {
filter: brightness(115%);
}
button.prompt-history-btn:active {
filter: brightness(150%);
}
/* Style Field */
select > .style-select-option {
position: relative;

View file

@ -51,19 +51,31 @@
</label>
<!-- Prompts section -->
<button type="button" class="collapsible">Prompts</button>
<div class="content">
<label for="prompt">Prompt:</label>
<br />
<div class="prompt-wrapper">
<textarea id="prompt"></textarea>
<div class="content prompt">
<div class="inputs">
<div class="prompt-wrapper">
<div class="prompt-indicator positive" title="Prompt"></div>
<textarea id="prompt" class="expandable"></textarea>
</div>
<div class="prompt-wrapper">
<div
class="prompt-indicator negative"
title="Negative Prompt"></div>
<textarea id="negPrompt" class="expandable"></textarea>
</div>
<div class="prompt-wrapper">
<div class="prompt-indicator styles" title="Styles"></div>
<div id="style-ac-mselect" style="flex-shrink: 1"></div>
</div>
</div>
<label for="negPrompt">Negative prompt:</label>
<div class="prompt-wrapper">
<textarea id="negPrompt"></textarea>
<div class="prompt-history-wrapper">
<div class="prompt-history-container">
<div id="prompt-history" class="expanded"></div>
<button
id="prompt-history-btn"
class="prompt-history-btn"></button>
</div>
</div>
<label for="styleSelect">Styles:</label>
<div id="style-ac-mselect"></div>
<!-- <hr /> -->
</div>
<!-- SD section -->
<button type="button" class="collapsible">
@ -286,6 +298,7 @@
<!-- Base Libs -->
<script src="js/lib/util.js" type="text/javascript"></script>
<script src="js/lib/events.js" type="text/javascript"></script>
<script src="js/lib/input.js" type="text/javascript"></script>
<script src="js/lib/layers.js" type="text/javascript"></script>
<script src="js/lib/commands.js" type="text/javascript"></script>
@ -298,7 +311,9 @@
type="text/javascript"></script>
<!-- Content -->
<script src="js/prompt.js" type="text/javascript"></script>
<script src="js/index.js" type="text/javascript"></script>
<script src="js/ui/floating/history.js" type="text/javascript"></script>
<script src="js/ui/floating/layers.js" type="text/javascript"></script>

View file

@ -96,20 +96,6 @@ function startup() {
};
});
const promptEl = document.getElementById("prompt");
promptEl.oninput = () => {
stableDiffusionData.prompt = promptEl.value;
promptEl.title = promptEl.value;
localStorage.setItem("prompt", stableDiffusionData.prompt);
};
const negPromptEl = document.getElementById("negPrompt");
negPromptEl.oninput = () => {
stableDiffusionData.negative_prompt = negPromptEl.value;
negPromptEl.title = negPromptEl.value;
localStorage.setItem("neg_prompt", stableDiffusionData.negative_prompt);
};
drawBackground();
changeMaskBlur();
changeSmoothRendering();
@ -447,12 +433,6 @@ const makeSlider = (
});
};
const styleAutoComplete = createAutoComplete(
"Style",
document.getElementById("style-ac-mselect"),
{multiple: true}
);
const modelAutoComplete = createAutoComplete(
"Model",
document.getElementById("models-ac-select")
@ -784,49 +764,6 @@ async function getConfig() {
}
}
async function getStyles() {
/** @type {HTMLSelectElement} */
var styleSelect = document.getElementById("styleSelect");
var url = document.getElementById("host").value + "/sdapi/v1/prompt-styles";
try {
const response = await fetch(url);
/** @type {{name: string, prompt: string, negative_prompt: string}[]} */
const data = await response.json();
/** @type {string[]} */
let stored = null;
try {
stored = JSON.parse(localStorage.getItem("promptStyle"));
// doesn't seem to throw a syntaxerror if the localstorage item simply doesn't exist?
if (stored == null) stored = [];
} catch (e) {
stored = [];
}
styleAutoComplete.options = data.map((style) => ({
name: style.name,
value: style.name,
title: `prompt: ${style.prompt}\nnegative: ${style.negative_prompt}`,
}));
styleAutoComplete.onchange.on(({value}) => {
let selected = [];
if (value.find((v) => v === "None")) {
styleAutoComplete.value = [];
} else {
selected = value;
}
stableDiffusionData.styles = selected;
localStorage.setItem("promptStyle", JSON.stringify(selected));
});
styleAutoComplete.value = stored;
localStorage.setItem("promptStyle", JSON.stringify(stored));
} catch (e) {
console.warn("[index] Failed to fetch prompt styles");
console.warn(e);
}
}
function changeStyles() {
/** @type {HTMLSelectElement} */
const styleSelectEl = document.getElementById("styleSelect");
@ -958,10 +895,6 @@ function loadSettings() {
);
// set the values into the UI
document.getElementById("prompt").value = String(_prompt);
document.getElementById("prompt").title = String(_prompt);
document.getElementById("negPrompt").value = String(_negprompt);
document.getElementById("negPrompt").title = String(_negprompt);
document.getElementById("maskBlur").value = Number(_mask_blur);
document.getElementById("seed").value = Number(_seed);
document.getElementById("cbxHRFix").checked = Boolean(_enable_hr);

View file

@ -48,7 +48,7 @@ for (var i = 0; i < coll.length; i++) {
if (active) content.style.maxHeight = content.scrollHeight + "px";
});
Array.from(content.children).forEach((child) => {
Array.from(content.querySelectorAll("*")).forEach((child) => {
observer.observe(child);
});
@ -62,6 +62,20 @@ for (var i = 0; i < coll.length; i++) {
});
}
/**
* Prompt history setup
*/
const _promptHistoryEl = document.getElementById("prompt-history");
const _promptHistoryBtn = document.getElementById("prompt-history-btn");
_promptHistoryEl.addEventListener("mouseleave", () => {
_promptHistoryEl.classList.remove("expanded");
});
_promptHistoryBtn.addEventListener("click", () =>
_promptHistoryEl.classList.toggle("expanded")
);
/**
* Settings overlay setup
*/

5
js/lib/events.js Normal file
View file

@ -0,0 +1,5 @@
const events = makeReadOnly({
tool: {
dream: new Observer(),
},
});

View file

@ -80,6 +80,33 @@ const guid = (size = 3) => {
return id;
};
/**
* Returns a hash code from a string
*
* From https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript
*
* @param {String} str The string to hash
* @return {Number} A 32bit integer
*/
const hashCode = (str, seed = 0) => {
let h1 = 0xdeadbeef ^ seed,
h2 = 0x41c6ce57 ^ seed;
for (let i = 0, ch; i < str.length; i++) {
ch = str.charCodeAt(i);
h1 = Math.imul(h1 ^ ch, 2654435761);
h2 = Math.imul(h2 ^ ch, 1597334677);
}
h1 =
Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^
Math.imul(h2 ^ (h2 >>> 13), 3266489909);
h2 =
Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^
Math.imul(h1 ^ (h1 >>> 13), 3266489909);
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
};
/**
* Assigns defaults to an option object passed to the function.
*

189
js/prompt.js Normal file
View file

@ -0,0 +1,189 @@
/**
* This file is for processing prompt/negative prompt and prompt style data
*/
// Prompt Style Element
const styleSelectElement = createAutoComplete(
"Style",
document.getElementById("style-ac-mselect"),
{multiple: true}
);
// Function to get styles from AUTOMATIC1111 webui
async function getStyles() {
var url = document.getElementById("host").value + "/sdapi/v1/prompt-styles";
try {
const response = await fetch(url);
/** @type {{name: string, prompt: string, negative_prompt: string}[]} */
const data = await response.json();
/** @type {string[]} */
let stored = null;
try {
stored = JSON.parse(localStorage.getItem("promptStyle"));
// doesn't seem to throw a syntaxerror if the localstorage item simply doesn't exist?
if (stored == null) stored = [];
} catch (e) {
stored = [];
}
styleSelectElement.options = data.map((style) => ({
name: style.name,
value: style.name,
title: `prompt: ${style.prompt}\nnegative: ${style.negative_prompt}`,
}));
styleSelectElement.onchange.on(({value}) => {
let selected = [];
if (value.find((v) => v === "None")) {
styleSelectElement.value = [];
} else {
selected = value;
}
stableDiffusionData.styles = selected;
localStorage.setItem("promptStyle", JSON.stringify(selected));
});
styleSelectElement.value = stored;
localStorage.setItem("promptStyle", JSON.stringify(stored));
} catch (e) {
console.warn("[index] Failed to fetch prompt styles");
console.warn(e);
}
}
(async () => {
// Default configurations
const defaultPrompt =
"ocean floor scientific expedition, underwater wildlife";
const defaultNegativePrompt =
"people, person, humans, human, divers, diver, glitch, error, text, watermark, bad quality, blurry";
// Prompt Elements
const promptEl = document.getElementById("prompt");
const negativePromptEl = document.getElementById("negPrompt");
// Add prompt change handlers
promptEl.oninput = () => {
stableDiffusionData.prompt = promptEl.value;
promptEl.title = promptEl.value;
localStorage.setItem("prompt", stableDiffusionData.prompt);
};
negativePromptEl.oninput = () => {
stableDiffusionData.negative_prompt = negativePromptEl.value;
negativePromptEl.title = negativePromptEl.value;
localStorage.setItem("neg_prompt", stableDiffusionData.negative_prompt);
};
// Load from local storage if set
const promptDefaultValue = localStorage.getItem("prompt") || defaultPrompt;
const negativePromptDefaultValue =
localStorage.getItem("neg_prompt") || defaultNegativePrompt;
promptEl.value = promptEl.title = promptDefaultValue;
negativePromptEl.value = negativePromptEl.title = negativePromptDefaultValue;
/**
* Prompt History
*/
// Get history-related elements
const promptHistoryEl = document.getElementById("prompt-history");
// History
const history = [];
function syncPromptHistory() {
const historyCopy = Array.from(history);
historyCopy.reverse();
for (let i = 0; i < historyCopy.length; i++) {
const historyItem = historyCopy[i];
const id = `prompt-history-${historyItem.id}`;
if (promptHistoryEl.querySelector(`#${id}`)) break;
const historyEntry = document.createElement("div");
historyEntry.classList.add("entry");
historyEntry.id = id;
historyEntry.title = `prompt: ${historyItem.prompt}\nnegative: ${
historyItem.negative
}\nstyles: ${historyItem.styles.join(", ")}`;
// Compare with previous
const samePrompt =
i !== historyCopy.length - 1 &&
historyItem.prompt === historyCopy[i + 1].prompt;
const sameNegativePrompt =
i !== historyCopy.length - 1 &&
historyItem.negative === historyCopy[i + 1].negative;
const sameStyles =
i !== historyCopy.length - 1 &&
historyItem.styles.length === historyCopy[i + 1].styles.length &&
!historyItem.styles.some(
(v, index) => v !== historyCopy[i + 1].styles[index]
);
const prompt = historyItem.prompt;
const negative = historyItem.negative;
const styles = historyItem.styles;
const promptBtn = document.createElement("button");
promptBtn.classList.add("prompt");
promptBtn.addEventListener("click", () => {
stableDiffusionData.prompt = prompt;
promptEl.title = prompt;
promptEl.value = prompt;
localStorage.setItem("prompt", prompt);
});
promptBtn.textContent = (samePrompt ? "= " : "") + prompt;
const negativeBtn = document.createElement("button");
negativeBtn.classList.add("negative");
negativeBtn.addEventListener("click", () => {
stableDiffusionData.negative_prompt = negative;
negativePromptEl.title = negative;
negativePromptEl.value = negative;
localStorage.setItem("neg_prompt", negative);
});
negativeBtn.textContent = (sameNegativePrompt ? "= " : "") + negative;
const stylesBtn = document.createElement("button");
stylesBtn.classList.add("styles");
stylesBtn.textContent = (sameStyles ? "= " : "") + styles.join(", ");
stylesBtn.addEventListener("click", () => {
styleSelectElement.value = styles;
});
historyEntry.appendChild(promptBtn);
historyEntry.appendChild(negativeBtn);
historyEntry.appendChild(stylesBtn);
promptHistoryEl.insertBefore(historyEntry, promptHistoryEl.firstChild);
}
}
// Listen for dreaming to add to history
events.tool.dream.on((message) => {
const {event} = message;
if (event === "generate") {
const {prompt, negative_prompt, styles} = message.request;
const hash = hashCode(
`p: ${prompt}, n: ${negative_prompt}, s: ${JSON.stringify(styles)}`
);
if (
!history[history.length - 1] ||
history[history.length - 1].hash !== hash
)
history.push({
id: guid(),
hash,
prompt,
negative: negative_prompt,
styles,
});
}
syncPromptHistory();
});
})();

View file

@ -120,6 +120,8 @@ const _generate = async (
bb,
drawEvery = 0.2 / request.n_iter
) => {
events.tool.dream.emit({event: "generate", request});
const requestCopy = JSON.parse(JSON.stringify(request));
// Block requests to identical areas

6
res/icons/history.svg Normal file
View file

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 3v5h5"></path>
<path d="M3.05 13A9 9 0 1 0 6 5.3L3 8"></path>
<path d="M12 7v5l4 2"></path>
</svg>

After

Width:  |  Height:  |  Size: 299 B

7
res/icons/library.svg Normal file
View file

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m16 6 4 14"></path>
<path d="M12 6v14"></path>
<path d="M8 8v12"></path>
<path d="M4 4v16"></path>
</svg>

After

Width:  |  Height:  |  Size: 305 B

View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<line x1="8" y1="12" x2="16" y2="12"></line>
</svg>

After

Width:  |  Height:  |  Size: 301 B

4
res/icons/minus.svg Normal file
View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>

After

Width:  |  Height:  |  Size: 236 B

View file

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<line x1="12" y1="8" x2="12" y2="16"></line>
<line x1="8" y1="12" x2="16" y2="12"></line>
</svg>

After

Width:  |  Height:  |  Size: 348 B

5
res/icons/plus.svg Normal file
View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>

After

Width:  |  Height:  |  Size: 283 B