diff --git a/index.html b/index.html index b0eaf33f..77fa6b10 100644 --- a/index.html +++ b/index.html @@ -4964,29 +4964,21 @@ >Model: - Temperature: - - + + Temperature: + Key: - - + + data-tip="Open provider's website to get the API key there. Note: the Map Genenerator doesn't store the key or any generated data" + /> @@ -8098,7 +8090,7 @@ - + @@ -8141,7 +8133,7 @@ - + diff --git a/modules/dynamic/auto-update.js b/modules/dynamic/auto-update.js index 50e6ed92..10a9a375 100644 --- a/modules/dynamic/auto-update.js +++ b/modules/dynamic/auto-update.js @@ -972,5 +972,9 @@ export function resolveVersionConflicts(mapVersion) { .attr("letter-spacing", null) .attr("fill", null) .attr("stroke", null); + + // pole can be missing for some states/provinces + BurgsAndStates.getPoles(); + Provinces.getPoles(); } } diff --git a/modules/dynamic/editors/cultures-editor.js b/modules/dynamic/editors/cultures-editor.js index b8e43608..dee807db 100644 --- a/modules/dynamic/editors/cultures-editor.js +++ b/modules/dynamic/editors/cultures-editor.js @@ -345,10 +345,13 @@ function cultureChangeName() { } function cultureRegenerateName() { - const culture = +this.parentNode.dataset.id; - const name = Names.getCultureShort(culture); + const cultureId = +this.parentNode.dataset.id; + const base = pack.cultures[cultureId].base; + if (!nameBases[base]) return tip("Namesbase is not defined, please select a valid namesbase", false, "error", 5000); + + const name = Names.getCultureShort(cultureId); this.parentNode.querySelector("input.cultureName").value = name; - pack.cultures[culture].name = name; + pack.cultures[cultureId].name = name; } function cultureChangeExpansionism() { @@ -494,12 +497,15 @@ function cultureRegenerateBurgs() { if (customization === 4) return; const cultureId = +this.parentNode.dataset.id; - const cBurgs = pack.burgs.filter(b => b.culture === cultureId && !b.lock); - cBurgs.forEach(b => { + const base = pack.cultures[cultureId].base; + if (!nameBases[base]) return tip("Namesbase is not defined, please select a valid namesbase", false, "error", 5000); + + const cultureBurgs = pack.burgs.filter(b => b.culture === cultureId && !b.removed && !b.lock); + cultureBurgs.forEach(b => { b.name = Names.getCulture(cultureId); labels.select("[data-id='" + b.i + "']").text(b.name); }); - tip(`Names for ${cBurgs.length} burgs are regenerated`, false, "success"); + tip(`Names for ${cultureBurgs.length} burgs are regenerated`, false, "success"); } function removeCulture(cultureId) { @@ -849,14 +855,15 @@ async function uploadCulturesData() { this.value = ""; const csv = await file.text(); const data = d3.csvParse(csv, d => ({ - i: +d.Id, name: d.Name, + i: +d.Id, color: d.Color, expansionism: +d.Expansionism, type: d.Type, population: +d.Population, emblemsShape: d["Emblems Shape"], - origins: d.Origins + origins: d.Origins, + namesbase: d.Namesbase })); const {cultures, cells} = pack; @@ -883,7 +890,7 @@ async function uploadCulturesData() { culture.i ); } else { - current = {i: cultures.length, center: ra(populated), area: 0, cells: 0, origin: 0, rural: 0, urban: 0}; + current = {i: cultures.length, center: ra(populated), area: 0, cells: 0, origins: [0], rural: 0, urban: 0}; cultures.push(current); } @@ -903,6 +910,10 @@ async function uploadCulturesData() { else current.type = "Generic"; } + culture.origins = current.i ? restoreOrigins(culture.origins || "") : [null]; + current.shield = shapes.includes(culture.emblemsShape) ? culture.emblemsShape : "heater"; + current.base = nameBases.findIndex(n => n.name == culture.namesbase); // can be -1 if namesbase is not found + function restoreOrigins(originsString) { const originNames = originsString .replaceAll('"', "") @@ -918,12 +929,6 @@ async function uploadCulturesData() { current.origins = originIds.filter(id => id !== null); if (!current.origins.length) current.origins = [0]; } - - culture.origins = current.i ? restoreOrigins(culture.origins || "") : [null]; - current.shield = shapes.includes(culture.emblemsShape) ? culture.emblemsShape : "heater"; - - const nameBaseIndex = nameBases.findIndex(n => n.name == culture.namesbase); - current.base = nameBaseIndex === -1 ? 0 : nameBaseIndex; } cultures.filter(c => c.removed).forEach(c => removeCulture(c.i)); diff --git a/modules/dynamic/supporters.js b/modules/dynamic/supporters.js index 3ca64da0..cf5c9cc7 100644 --- a/modules/dynamic/supporters.js +++ b/modules/dynamic/supporters.js @@ -583,4 +583,9 @@ James Benware FortunesFaded breadsticks Murderbits -Ben Jones`; +Ben Jones +Marco Faltracco +L +silentArtifact +Keith Potter +Morgan Gilbert`; diff --git a/modules/io/load.js b/modules/io/load.js index 05819ce7..8e05a798 100644 --- a/modules/io/load.js +++ b/modules/io/load.js @@ -471,7 +471,7 @@ async function parseLoadedData(data, mapVersion) { { // dynamically import and run auto-update script - const {resolveVersionConflicts} = await import("../dynamic/auto-update.js?v=1.105.10"); + const {resolveVersionConflicts} = await import("../dynamic/auto-update.js?v=1.105.24"); resolveVersionConflicts(mapVersion); } diff --git a/modules/ui/ai-generator.js b/modules/ui/ai-generator.js index daa8cde6..734f1246 100644 --- a/modules/ui/ai-generator.js +++ b/modules/ui/ai-generator.js @@ -1,8 +1,114 @@ "use strict"; -const GPT_MODELS = ["gpt-4o-mini", "chatgpt-4o-latest", "gpt-4o", "gpt-4-turbo", "gpt-4", "gpt-3.5-turbo"]; +const PROVIDERS = { + openai: { + keyLink: "https://platform.openai.com/account/api-keys", + generate: generateWithOpenAI + }, + anthropic: { + keyLink: "https://console.anthropic.com/account/keys", + generate: generateWithAnthropic + } +}; + +const DEFAULT_MODEL = "gpt-4o-mini"; + +const MODELS = { + "gpt-4o-mini": "openai", + "chatgpt-4o-latest": "openai", + "gpt-4o": "openai", + "gpt-4-turbo": "openai", + "o1-preview": "openai", + "o1-mini": "openai", + "claude-3-5-haiku-latest": "anthropic", + "claude-3-5-sonnet-latest": "anthropic", + "claude-3-opus-latest": "anthropic" +}; + const SYSTEM_MESSAGE = "I'm working on my fantasy map."; +async function generateWithOpenAI({key, model, prompt, temperature, onContent}) { + const headers = { + "Content-Type": "application/json", + Authorization: `Bearer ${key}` + }; + + const messages = [ + {role: "system", content: SYSTEM_MESSAGE}, + {role: "user", content: prompt} + ]; + + const response = await fetch("https://api.openai.com/v1/chat/completions", { + method: "POST", + headers, + body: JSON.stringify({model, messages, temperature, stream: true}) + }); + + const getContent = json => { + const content = json.choices?.[0]?.delta?.content; + if (content) onContent(content); + }; + + await handleStream(response, getContent); +} + +async function generateWithAnthropic({key, model, prompt, temperature, onContent}) { + const headers = { + "Content-Type": "application/json", + "x-api-key": key, + "anthropic-version": "2023-06-01", + "anthropic-dangerous-direct-browser-access": "true" + }; + + const messages = [{role: "user", content: prompt}]; + + const response = await fetch("https://api.anthropic.com/v1/messages", { + method: "POST", + headers, + body: JSON.stringify({model, system: SYSTEM_MESSAGE, messages, temperature, max_tokens: 4096, stream: true}) + }); + + const getContent = json => { + const content = json.delta?.text; + if (content) onContent(content); + }; + + await handleStream(response, getContent); +} + +async function handleStream(response, getContent) { + if (!response.ok) { + const json = await response.json(); + throw new Error(json?.error?.message || "Failed to generate"); + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder("utf-8"); + let buffer = ""; + + while (true) { + const {done, value} = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, {stream: true}); + const lines = buffer.split("\n"); + + for (let i = 0; i < lines.length - 1; i++) { + const line = lines[i].trim(); + if (line.startsWith("data: ") && line !== "data: [DONE]") { + try { + const json = JSON.parse(line.slice(6)); + getContent(json); + } catch (jsonError) { + ERROR && console.error(`Failed to parse JSON:`, jsonError, `Line: ${line}`); + } + } + } + + buffer = lines.at(-1); + } +} + function generateWithAi(defaultPrompt, onApply) { updateValues(); @@ -29,90 +135,53 @@ function generateWithAi(defaultPrompt, onApply) { if (modules.generateWithAi) return; modules.generateWithAi = true; + byId("aiGeneratorKeyHelp").on("click", function (e) { + const model = byId("aiGeneratorModel").value; + const provider = MODELS[model]; + openURL(PROVIDERS[provider].keyLink); + }); + function updateValues() { byId("aiGeneratorResult").value = ""; byId("aiGeneratorPrompt").value = defaultPrompt; - byId("aiGeneratorKey").value = localStorage.getItem("fmg-ai-kl") || ""; - byId("aiGeneratorTemperature").value = localStorage.getItem("fmg-ai-temperature") || "1.2"; + byId("aiGeneratorTemperature").value = localStorage.getItem("fmg-ai-temperature") || "1"; 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]; + Object.keys(MODELS).forEach(model => select.options.add(new Option(model, model))); + select.value = localStorage.getItem("fmg-ai-model"); + if (!select.value || !MODELS[select.value]) select.value = DEFAULT_MODEL; + + const provider = MODELS[select.value]; + byId("aiGeneratorKey").value = localStorage.getItem(`fmg-ai-kl-${provider}`) || ""; } 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); + if (!key) return tip("Please enter an API key", true, "error", 4000); const model = byId("aiGeneratorModel").value; if (!model) return tip("Please select a model", true, "error", 4000); localStorage.setItem("fmg-ai-model", model); + const provider = MODELS[model]; + localStorage.setItem(`fmg-ai-kl-${provider}`, key); + const prompt = byId("aiGeneratorPrompt").value; if (!prompt) return tip("Please enter a prompt", true, "error", 4000); - const temperature = parseFloat(byId("aiGeneratorTemperature").value); - if (isNaN(temperature) || temperature < 0 || temperature > 2) { - return tip("Temperature must be a number between 0 and 2", true, "error", 4000); - } - localStorage.setItem("fmg-ai-temperature", temperature.toString()); + const temperature = byId("aiGeneratorTemperature").valueAsNumber; + if (isNaN(temperature)) return tip("Temperature must be a number", true, "error", 4000); + localStorage.setItem("fmg-ai-temperature", temperature); try { button.disabled = true; const resultArea = byId("aiGeneratorResult"); - resultArea.value = ""; resultArea.disabled = true; + resultArea.value = ""; + const onContent = content => (resultArea.value += content); - 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: temperature, - stream: true // Enable streaming - }) - }); - - if (!response.ok) { - const json = await response.json(); - throw new Error(json?.error?.message || "Failed to generate"); - } - - const reader = response.body.getReader(); - const decoder = new TextDecoder("utf-8"); - let buffer = ""; - - while (true) { - const {done, value} = await reader.read(); - if (done) break; - - buffer += decoder.decode(value, {stream: true}); - const lines = buffer.split("\n"); - - for (let i = 0; i < lines.length - 1; i++) { - const line = lines[i].trim(); - if (line.startsWith("data: ") && line !== "data: [DONE]") { - try { - const jsonData = JSON.parse(line.slice(6)); - const content = jsonData.choices[0].delta.content; - if (content) resultArea.value += content; - } catch (jsonError) { - console.warn("Failed to parse JSON:", jsonError, "Line:", line); - } - } - } - - buffer = lines[lines.length - 1]; - } + await PROVIDERS[provider].generate({key, model, prompt, temperature, onContent}); } catch (error) { return tip(error.message, true, "error", 4000); } finally { diff --git a/modules/ui/editors.js b/modules/ui/editors.js index 13380155..a7df5ca3 100644 --- a/modules/ui/editors.js +++ b/modules/ui/editors.js @@ -1255,7 +1255,7 @@ async function editStates() { async function editCultures() { if (customization) return; - const Editor = await import("../dynamic/editors/cultures-editor.js?v=1.105.11"); + const Editor = await import("../dynamic/editors/cultures-editor.js?v=1.105.23"); Editor.open(); } diff --git a/versioning.js b/versioning.js index fa8da7ec..afd2433e 100644 --- a/versioning.js +++ b/versioning.js @@ -13,7 +13,7 @@ * Example: 1.102.2 -> Major version 1, Minor version 102, Patch version 2 */ -const VERSION = "1.105.22"; +const VERSION = "1.106.00"; if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format or parsing function"); {