@@ -8037,7 +8064,8 @@
-
+
+
diff --git a/modules/ui/ai-generator.js b/modules/ui/ai-generator.js
new file mode 100644
index 00000000..a0dbc0a6
--- /dev/null
+++ b/modules/ui/ai-generator.js
@@ -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;
+ }
+ }
+}
diff --git a/modules/ui/notes-editor.js b/modules/ui/notes-editor.js
index becf4fa0..378cec3c 100644
--- a/modules/ui/notes-editor.js
+++ b/modules/ui/notes-editor.js
@@ -55,6 +55,7 @@ function editNotes(id, name) {
byId("notesLegend").addEventListener("blur", updateLegend);
byId("notesPin").addEventListener("click", toggleNotesPin);
byId("notesFocus").addEventListener("click", validateHighlightElement);
+ byId("notesGenerateWithAi").addEventListener("click", openAiGenerator);
byId("notesDownload").addEventListener("click", downloadLegends);
byId("notesUpload").addEventListener("click", () => legendsToLoad.click());
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() {
const notesData = JSON.stringify(notes);
const name = getFileName("Notes") + ".txt";
diff --git a/versioning.js b/versioning.js
index 368a4924..4ed43f47 100644
--- a/versioning.js
+++ b/versioning.js
@@ -28,6 +28,8 @@ const version = "1.99.06"; // generator version, update each time
Latest changes:
+ - Notes Editor: on-demand AI text generation
+ - New style preset: Dark Seas
- New routes generation algorithm
- Routes overview tool
- Configurable longitude
@@ -41,8 +43,6 @@ const version = "1.99.06"; // generator version, update each time
- Auto-load of the last saved map is now optional (see Onload behavior in Options)
- New label placement algorithm for states
- North and South Poles temperature can be set independently
- - More than 70 new heraldic charges
- - Multi-color heraldic charges support
Join our Discord server and Reddit community to ask questions, share maps, discuss the Generator and Worlbuilding, report bugs and propose new features.