AI generation for notes (#1112)

* feat: ai generation for notes

* feat - ai generation -default to gpt-4o-mini

* feat - ai generation - change update text

* feat - ai generation - improve prompt

---------

Co-authored-by: Azgaar <azgaar.fmg@yandex.com>
This commit is contained in:
Azgaar 2024-08-23 18:08:50 +02:00 committed by GitHub
parent 1f280133be
commit 6df54d1ef6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 139 additions and 4 deletions

View file

@ -253,7 +253,7 @@
.icon-coa:before {content:'\f3ed'; font-size: .9em; color: #999;} /* '' */ .icon-coa:before {content:'\f3ed'; font-size: .9em; color: #999;} /* '' */
.icon-half:before {font-weight: bold;content:'½';} .icon-half:before {font-weight: bold;content:'½';}
.icon-voice:before {content:'🔊';} .icon-voice:before {content:'🔊';}
.icon-robot:before {content:'🤖';}
.icon-die:before {content:'🎲';} .icon-die:before {content:'🎲';}
.icon-button-die:before {content:'🎲'; padding-right: .4em;} .icon-button-die:before {content:'🎲'; padding-right: .4em;}
.icon-button-power:before {content:'💪'; padding-right: .6em;} .icon-button-power:before {content:'💪'; padding-right: .6em;}

View file

@ -4921,6 +4921,7 @@
<div id="notesLegend" contenteditable="true"></div> <div id="notesLegend" contenteditable="true"></div>
<div style="margin-top: 0.3em"> <div style="margin-top: 0.3em">
<button id="notesFocus" data-tip="Focus on selected object" class="icon-target"></button> <button id="notesFocus" data-tip="Focus on selected object" class="icon-target"></button>
<button id="notesGenerateWithAi" data-tip="Generate note with AI" class="icon-robot"></button>
<button <button
id="notesPin" id="notesPin"
data-tip="Toggle notes box dispay: hide or do not hide the box on mouse move" data-tip="Toggle notes box dispay: hide or do not hide the box on mouse move"
@ -4932,6 +4933,32 @@
</div> </div>
</div> </div>
<div id="aiGenerator" class="dialog stable" style="display: none">
<div style="display: flex; flex-direction: column; gap: 0.3em; width: 100%">
<textarea id="aiGeneratorResult" placeholder="Generated text will appear here" cols="30" rows="10"></textarea>
<textarea id="aiGeneratorPrompt" placeholder="Type a prompt here" cols="30" rows="5"></textarea>
<div style="display: flex; align-items: center; gap: 1em">
<label for="aiGeneratorModel"
>Model:
<select id="aiGeneratorModel"></select>
</label>
<label for="aiGeneratorKey"
>Key:
<input id="aiGeneratorKey" placeholder="Enter OpenAI API key" class="icon-key" />
<a
href="https://platform.openai.com/account/api-keys"
target="_blank"
rel="noreferrer"
class="icon-help-circled"
style="text-decoration: none"
data-tip="Get the key at OpenAI website. The key will be stored in your browser and send to OpenAI API directly. The Map Genenerator doesn't store the key or any generated data"
></a>
</label>
</div>
</div>
</div>
<div id="emblemEditor" class="dialog stable" style="display: none"> <div id="emblemEditor" class="dialog stable" style="display: none">
<svg viewBox="0 0 200 200"><use id="emblemImage"></use></svg> <svg viewBox="0 0 200 200"><use id="emblemImage"></use></svg>
<div id="emblemBody"> <div id="emblemBody">
@ -8037,7 +8064,8 @@
<script defer src="modules/ui/relief-editor.js?v=1.99.00"></script> <script defer src="modules/ui/relief-editor.js?v=1.99.00"></script>
<script defer src="modules/ui/burg-editor.js?v=1.99.05"></script> <script defer src="modules/ui/burg-editor.js?v=1.99.05"></script>
<script defer src="modules/ui/units-editor.js?v=1.99.05"></script> <script defer src="modules/ui/units-editor.js?v=1.99.05"></script>
<script defer src="modules/ui/notes-editor.js?v=1.99.00"></script> <script defer src="modules/ui/notes-editor.js?v=1.99.06"></script>
<script defer src="modules/ui/ai-generator.js?v=1.99.06"></script>
<script defer src="modules/ui/diplomacy-editor.js?v=1.99.00"></script> <script defer src="modules/ui/diplomacy-editor.js?v=1.99.00"></script>
<script defer src="modules/ui/zones-editor.js?v=1.99.00"></script> <script defer src="modules/ui/zones-editor.js?v=1.99.00"></script>
<script defer src="modules/ui/burgs-overview.js?v=1.99.05"></script> <script defer src="modules/ui/burgs-overview.js?v=1.99.05"></script>

View file

@ -0,0 +1,87 @@
"use strict";
const GPT_MODELS = ["gpt-4o-mini", "chatgpt-4o-latest", "gpt-4o", "gpt-4-turbo", "gpt-4", "gpt-3.5-turbo"];
const SYSTEM_MESSAGE = "I'm working on my fantasy map.";
function geneateWithAi(defaultPrompt, onApply) {
updateValues();
$("#aiGenerator").dialog({
title: "AI Text Generator",
position: {my: "center", at: "center", of: "svg"},
resizable: false,
buttons: {
Generate: function (e) {
generate(e.target);
},
Apply: function () {
const result = byId("aiGeneratorResult").value;
if (!result) return tip("No result to apply", true, "error", 4000);
onApply(result);
$(this).dialog("close");
},
Close: function () {
$(this).dialog("close");
}
}
});
if (modules.geneateWithAi) return;
modules.geneateWithAi = true;
function updateValues() {
byId("aiGeneratorResult").value = "";
byId("aiGeneratorPrompt").value = defaultPrompt;
byId("aiGeneratorKey").value = localStorage.getItem("fmg-ai-kl") || "";
const select = byId("aiGeneratorModel");
select.options.length = 0;
GPT_MODELS.forEach(model => select.options.add(new Option(model, model)));
select.value = localStorage.getItem("fmg-ai-model") || GPT_MODELS[0];
}
async function generate(button) {
const key = byId("aiGeneratorKey").value;
if (!key) return tip("Please enter an OpenAI API key", true, "error", 4000);
localStorage.setItem("fmg-ai-kl", key);
const model = byId("aiGeneratorModel").value;
if (!model) return tip("Please select a model", true, "error", 4000);
localStorage.setItem("fmg-ai-model", model);
const prompt = byId("aiGeneratorPrompt").value;
if (!prompt) return tip("Please enter a prompt", true, "error", 4000);
try {
button.disabled = true;
byId("aiGeneratorResult").disabled = true;
const response = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {"Content-Type": "application/json", Authorization: `Bearer ${key}`},
body: JSON.stringify({
model,
messages: [
{role: "system", content: SYSTEM_MESSAGE},
{role: "user", content: prompt}
],
temperature: 1.2
})
});
if (!response.ok) {
const json = await response.json();
throw new Error(json?.error?.message || "Failed to generate");
}
const {choices} = await response.json();
const result = choices[0].message.content;
byId("aiGeneratorResult").value = result;
} catch (error) {
return tip(error.message, true, "error", 4000);
} finally {
button.disabled = false;
byId("aiGeneratorResult").disabled = false;
}
}
}

View file

@ -55,6 +55,7 @@ function editNotes(id, name) {
byId("notesLegend").addEventListener("blur", updateLegend); byId("notesLegend").addEventListener("blur", updateLegend);
byId("notesPin").addEventListener("click", toggleNotesPin); byId("notesPin").addEventListener("click", toggleNotesPin);
byId("notesFocus").addEventListener("click", validateHighlightElement); byId("notesFocus").addEventListener("click", validateHighlightElement);
byId("notesGenerateWithAi").addEventListener("click", openAiGenerator);
byId("notesDownload").addEventListener("click", downloadLegends); byId("notesDownload").addEventListener("click", downloadLegends);
byId("notesUpload").addEventListener("click", () => legendsToLoad.click()); byId("notesUpload").addEventListener("click", () => legendsToLoad.click());
byId("legendsToLoad").addEventListener("change", function () { byId("legendsToLoad").addEventListener("change", function () {
@ -143,6 +144,25 @@ function editNotes(id, name) {
}); });
} }
function openAiGenerator() {
const note = notes.find(note => note.id === notesSelect.value);
let prompt = `Respond with description. Use simple dry language. Invent facts, names and details. Split to paragraphs and format to HTML. Remove h tags, remove markdown.`;
if (note?.name) prompt += ` Name: ${note.name}.`;
if (note?.legend) prompt += ` Data: ${note.legend}`;
const onApply = result => {
notesLegend.innerHTML = result;
if (note) {
note.legend = result;
updateNotesBox(note);
if (window.tinymce) tinymce.activeEditor.setContent(note.legend);
}
};
geneateWithAi(prompt, onApply);
}
function downloadLegends() { function downloadLegends() {
const notesData = JSON.stringify(notes); const notesData = JSON.stringify(notes);
const name = getFileName("Notes") + ".txt"; const name = getFileName("Notes") + ".txt";

View file

@ -28,6 +28,8 @@ const version = "1.99.06"; // generator version, update each time
<ul> <ul>
<strong>Latest changes:</strong> <strong>Latest changes:</strong>
<li>Notes Editor: on-demand AI text generation</li>
<li>New style preset: Dark Seas</li>
<li>New routes generation algorithm</li> <li>New routes generation algorithm</li>
<li>Routes overview tool</li> <li>Routes overview tool</li>
<li>Configurable longitude</li> <li>Configurable longitude</li>
@ -41,8 +43,6 @@ const version = "1.99.06"; // generator version, update each time
<li>Auto-load of the last saved map is now optional (see <i>Onload behavior</i> in Options)</li> <li>Auto-load of the last saved map is now optional (see <i>Onload behavior</i> in Options)</li>
<li>New label placement algorithm for states</li> <li>New label placement algorithm for states</li>
<li>North and South Poles temperature can be set independently</li> <li>North and South Poles temperature can be set independently</li>
<li>More than 70 new heraldic charges</li>
<li>Multi-color heraldic charges support</li>
</ul> </ul>
<p>Join our <a href="${discord}" target="_blank">Discord server</a> and <a href="${reddit}" target="_blank">Reddit community</a> to ask questions, share maps, discuss the Generator and Worlbuilding, report bugs and propose new features.</p> <p>Join our <a href="${discord}" target="_blank">Discord server</a> and <a href="${reddit}" target="_blank">Reddit community</a> to ask questions, share maps, discuss the Generator and Worlbuilding, report bugs and propose new features.</p>