mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-18 02:01:22 +01:00
feat: ai - claude support
This commit is contained in:
parent
f5dff64e33
commit
eb48bfa537
2 changed files with 136 additions and 163 deletions
28
index.html
28
index.html
|
|
@ -4951,29 +4951,18 @@
|
|||
>Model:
|
||||
<select id="aiGeneratorModel"></select>
|
||||
</label>
|
||||
<label for="aiGeneratorTemperature"
|
||||
>Temperature:
|
||||
<input id="aiGeneratorTemperature" type="number" min="0" max="2" placeholder="1.2" class="icon-key" />
|
||||
<a
|
||||
href="https://platform.openai.com/docs/api-reference/chat/create#chat-create-temperature"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="icon-help-circled"
|
||||
style="text-decoration: none"
|
||||
data-tip="Between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic."
|
||||
></a>
|
||||
<label for="aiGeneratorTemperature" data-tip="Temperature controls response randomness; higher values mean more creativity, lower values mean more predictability">
|
||||
Temperature:
|
||||
<input id="aiGeneratorTemperature" type="number" min="-1" max="2" step=".1" class="icon-key" />
|
||||
</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"
|
||||
<input id="aiGeneratorKey" placeholder="Enter API key" class="icon-key" />
|
||||
<button
|
||||
id="aiGeneratorKeyHelp"
|
||||
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>
|
||||
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"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -8109,7 +8098,6 @@
|
|||
<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/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/diplomacy-editor.js?v=1.99.00"></script>
|
||||
<script defer src="modules/ui/zones-editor.js?v=1.105.20"></script>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,114 @@
|
|||
"use strict";
|
||||
|
||||
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 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();
|
||||
|
||||
|
|
@ -23,90 +129,59 @@ function generateWithAi(defaultPrompt, onApply) {
|
|||
Close: function () {
|
||||
$(this).dialog("close");
|
||||
}
|
||||
},
|
||||
open: function() {
|
||||
initialize();
|
||||
},
|
||||
close: function() {
|
||||
const helpLink = byId("aiGeneratorKey").nextElementSibling;
|
||||
helpLink.removeEventListener("mouseover", showDataTip);
|
||||
}
|
||||
});
|
||||
|
||||
if (modules.generateWithAi) return;
|
||||
modules.generateWithAi = true;
|
||||
|
||||
function initialize() {
|
||||
byId("aiGeneratorModel").addEventListener("change", function(e) {
|
||||
updateKeyHelp(e.target.value);
|
||||
});
|
||||
|
||||
updateValues();
|
||||
}
|
||||
|
||||
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;
|
||||
LLMS.forEach(model => select.options.add(new Option(model, model)));
|
||||
select.value = localStorage.getItem("fmg-ai-model") || LLMS[0];
|
||||
|
||||
updateKeyHelp(select.value);
|
||||
}
|
||||
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;
|
||||
|
||||
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);
|
||||
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 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 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);
|
||||
|
||||
if (model.includes("claude")) {
|
||||
await generateWithClaude(key, model, prompt, temperature, resultArea);
|
||||
} else {
|
||||
await generateWithGPT(key, model, prompt, temperature, resultArea);
|
||||
}
|
||||
await PROVIDERS[provider].generate({key, model, prompt, temperature, onContent});
|
||||
} catch (error) {
|
||||
return tip(error.message, true, "error", 4000);
|
||||
} finally {
|
||||
|
|
@ -114,94 +189,4 @@ function generateWithAi(defaultPrompt, onApply) {
|
|||
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", {
|
||||
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,
|
||||
stream: true
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const json = await response.json();
|
||||
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 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: ") && (!isClaude && line !== "data: [DONE]")) {
|
||||
try {
|
||||
const jsonData = JSON.parse(line.slice(6));
|
||||
const content = isClaude
|
||||
? jsonData.delta?.text
|
||||
: jsonData.choices[0].delta.content;
|
||||
|
||||
if (content) resultArea.value += content;
|
||||
} catch (jsonError) {
|
||||
console.warn(
|
||||
`Failed to parse ${isClaude ? "Claude" : "OpenAI"} JSON:`,
|
||||
jsonError,
|
||||
"Line:",
|
||||
line
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buffer = lines[lines.length - 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue