mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-18 18:11:24 +01:00
Add Claude AI support (#1165)
* feat: ai generator - add support for claude * feat: ai generator - add claude support * refactor: clean up API calls --------- Co-authored-by: Azgaar <maxganiev@yandex.com>
This commit is contained in:
parent
ca8e723006
commit
f5dff64e33
2 changed files with 137 additions and 52 deletions
|
|
@ -8109,6 +8109,7 @@
|
||||||
<script defer src="modules/ui/burg-editor.js?v=1.102.00"></script>
|
<script defer src="modules/ui/burg-editor.js?v=1.102.00"></script>
|
||||||
<script defer src="modules/ui/units-editor.js?v=1.104.0"></script>
|
<script defer src="modules/ui/units-editor.js?v=1.104.0"></script>
|
||||||
<script defer src="modules/ui/notes-editor.js?v=1.99.06"></script>
|
<script defer src="modules/ui/notes-editor.js?v=1.99.06"></script>
|
||||||
|
<script defer src="modules/ui/ai-generator.js?v=1.105.23"></script>
|
||||||
<script defer src="modules/ui/ai-generator.js?v=1.105.22"></script>
|
<script defer src="modules/ui/ai-generator.js?v=1.105.22"></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.105.20"></script>
|
<script defer src="modules/ui/zones-editor.js?v=1.105.20"></script>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const GPT_MODELS = ["gpt-4o-mini", "chatgpt-4o-latest", "gpt-4o", "gpt-4-turbo", "gpt-4", "gpt-3.5-turbo"];
|
const LLMS = ["gpt-4o-mini", "chatgpt-4o-latest", "gpt-4o", "gpt-4-turbo", "gpt-4", "gpt-3.5-turbo", "claude-3-opus-20240229", "claude-3-sonnet-20240229", "claude-3-haiku-20240307", "claude-3-5-sonnet-20240620"];
|
||||||
const SYSTEM_MESSAGE = "I'm working on my fantasy map.";
|
const SYSTEM_MESSAGE = "I'm working on my fantasy map.";
|
||||||
|
|
||||||
function generateWithAi(defaultPrompt, onApply) {
|
function generateWithAi(defaultPrompt, onApply) {
|
||||||
|
|
@ -23,12 +23,28 @@ function generateWithAi(defaultPrompt, onApply) {
|
||||||
Close: function () {
|
Close: function () {
|
||||||
$(this).dialog("close");
|
$(this).dialog("close");
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
open: function() {
|
||||||
|
initialize();
|
||||||
|
},
|
||||||
|
close: function() {
|
||||||
|
const helpLink = byId("aiGeneratorKey").nextElementSibling;
|
||||||
|
helpLink.removeEventListener("mouseover", showDataTip);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (modules.generateWithAi) return;
|
if (modules.generateWithAi) return;
|
||||||
modules.generateWithAi = true;
|
modules.generateWithAi = true;
|
||||||
|
|
||||||
|
function initialize() {
|
||||||
|
byId("aiGeneratorModel").addEventListener("change", function(e) {
|
||||||
|
updateKeyHelp(e.target.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
updateValues();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function updateValues() {
|
function updateValues() {
|
||||||
byId("aiGeneratorResult").value = "";
|
byId("aiGeneratorResult").value = "";
|
||||||
byId("aiGeneratorPrompt").value = defaultPrompt;
|
byId("aiGeneratorPrompt").value = defaultPrompt;
|
||||||
|
|
@ -37,13 +53,34 @@ function generateWithAi(defaultPrompt, onApply) {
|
||||||
|
|
||||||
const select = byId("aiGeneratorModel");
|
const select = byId("aiGeneratorModel");
|
||||||
select.options.length = 0;
|
select.options.length = 0;
|
||||||
GPT_MODELS.forEach(model => select.options.add(new Option(model, model)));
|
LLMS.forEach(model => select.options.add(new Option(model, model)));
|
||||||
select.value = localStorage.getItem("fmg-ai-model") || GPT_MODELS[0];
|
select.value = localStorage.getItem("fmg-ai-model") || LLMS[0];
|
||||||
|
|
||||||
|
updateKeyHelp(select.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateKeyHelp(model) {
|
||||||
|
const keyInput = byId("aiGeneratorKey");
|
||||||
|
const helpLink = keyInput.nextElementSibling;
|
||||||
|
|
||||||
|
helpLink.removeEventListener("mouseover", showDataTip);
|
||||||
|
|
||||||
|
if (model.includes("claude")) {
|
||||||
|
keyInput.placeholder = "Enter Anthropic API key";
|
||||||
|
helpLink.href = "https://console.anthropic.com/account/keys";
|
||||||
|
helpLink.dataset.tip = "Get the key at Anthropic's website. The key will be stored in your browser and send to Anthropic API directly. The Map Generator doesn't store the key or any generated data";
|
||||||
|
} else {
|
||||||
|
keyInput.placeholder = "Enter OpenAI API key";
|
||||||
|
helpLink.href = "https://platform.openai.com/account/api-keys";
|
||||||
|
helpLink.dataset.tip = "Get the key at OpenAI website. The key will be stored in your browser and send to OpenAI API directly. The Map Generator doesn't store the key or any generated data";
|
||||||
|
}
|
||||||
|
|
||||||
|
helpLink.addEventListener("mouseover", showDataTip);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generate(button) {
|
async function generate(button) {
|
||||||
const key = byId("aiGeneratorKey").value;
|
const key = byId("aiGeneratorKey").value;
|
||||||
if (!key) return tip("Please enter an OpenAI API key", true, "error", 4000);
|
if (!key) return tip("Please enter an API key", true, "error", 4000);
|
||||||
localStorage.setItem("fmg-ai-kl", key);
|
localStorage.setItem("fmg-ai-kl", key);
|
||||||
|
|
||||||
const model = byId("aiGeneratorModel").value;
|
const model = byId("aiGeneratorModel").value;
|
||||||
|
|
@ -65,6 +102,47 @@ function generateWithAi(defaultPrompt, onApply) {
|
||||||
resultArea.value = "";
|
resultArea.value = "";
|
||||||
resultArea.disabled = true;
|
resultArea.disabled = true;
|
||||||
|
|
||||||
|
if (model.includes("claude")) {
|
||||||
|
await generateWithClaude(key, model, prompt, temperature, resultArea);
|
||||||
|
} else {
|
||||||
|
await generateWithGPT(key, model, prompt, temperature, resultArea);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return tip(error.message, true, "error", 4000);
|
||||||
|
} finally {
|
||||||
|
button.disabled = false;
|
||||||
|
byId("aiGeneratorResult").disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateWithClaude(key, model, prompt, temperature, resultArea) {
|
||||||
|
const baseUrl = "https://api.anthropic.com/v1/messages";
|
||||||
|
const response = await fetch(baseUrl, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"x-api-key": key,
|
||||||
|
"anthropic-version": "2023-06-01",
|
||||||
|
"anthropic-dangerous-direct-browser-access": "true"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model,
|
||||||
|
messages: [{role: "user", content: prompt}],
|
||||||
|
stream: true,
|
||||||
|
max_tokens: 4096,
|
||||||
|
temperature
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const json = await response.json();
|
||||||
|
throw new Error(json?.error?.message || "Failed to generate with Claude");
|
||||||
|
}
|
||||||
|
|
||||||
|
await handleStream(response, resultArea, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateWithGPT(key, model, prompt, temperature, resultArea) {
|
||||||
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -77,16 +155,20 @@ function generateWithAi(defaultPrompt, onApply) {
|
||||||
{role: "system", content: SYSTEM_MESSAGE},
|
{role: "system", content: SYSTEM_MESSAGE},
|
||||||
{role: "user", content: prompt}
|
{role: "user", content: prompt}
|
||||||
],
|
],
|
||||||
temperature: temperature,
|
temperature,
|
||||||
stream: true // Enable streaming
|
stream: true
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
throw new Error(json?.error?.message || "Failed to generate");
|
throw new Error(json?.error?.message || "Failed to generate with GPT");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await handleStream(response, resultArea, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleStream(response, resultArea, isClaude) {
|
||||||
const reader = response.body.getReader();
|
const reader = response.body.getReader();
|
||||||
const decoder = new TextDecoder("utf-8");
|
const decoder = new TextDecoder("utf-8");
|
||||||
let buffer = "";
|
let buffer = "";
|
||||||
|
|
@ -100,24 +182,26 @@ function generateWithAi(defaultPrompt, onApply) {
|
||||||
|
|
||||||
for (let i = 0; i < lines.length - 1; i++) {
|
for (let i = 0; i < lines.length - 1; i++) {
|
||||||
const line = lines[i].trim();
|
const line = lines[i].trim();
|
||||||
if (line.startsWith("data: ") && line !== "data: [DONE]") {
|
if (line.startsWith("data: ") && (!isClaude && line !== "data: [DONE]")) {
|
||||||
try {
|
try {
|
||||||
const jsonData = JSON.parse(line.slice(6));
|
const jsonData = JSON.parse(line.slice(6));
|
||||||
const content = jsonData.choices[0].delta.content;
|
const content = isClaude
|
||||||
|
? jsonData.delta?.text
|
||||||
|
: jsonData.choices[0].delta.content;
|
||||||
|
|
||||||
if (content) resultArea.value += content;
|
if (content) resultArea.value += content;
|
||||||
} catch (jsonError) {
|
} catch (jsonError) {
|
||||||
console.warn("Failed to parse JSON:", jsonError, "Line:", line);
|
console.warn(
|
||||||
|
`Failed to parse ${isClaude ? "Claude" : "OpenAI"} JSON:`,
|
||||||
|
jsonError,
|
||||||
|
"Line:",
|
||||||
|
line
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer = lines[lines.length - 1];
|
buffer = lines[lines.length - 1];
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
return tip(error.message, true, "error", 4000);
|
|
||||||
} finally {
|
|
||||||
button.disabled = false;
|
|
||||||
byId("aiGeneratorResult").disabled = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue