mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-18 02:01:22 +01:00
ollama implementation
This commit is contained in:
parent
a6f66e9828
commit
f4cb6dd29f
3 changed files with 200 additions and 63 deletions
|
|
@ -4978,14 +4978,16 @@
|
|||
Temperature:
|
||||
<input id="aiGeneratorTemperature" type="number" min="-1" max="2" step=".1" class="icon-key" />
|
||||
</label>
|
||||
<label for="aiGeneratorKey"
|
||||
>Key:
|
||||
<label for="aiGeneratorKey" >Key:
|
||||
<input id="aiGeneratorKey" placeholder="Enter API key" class="icon-key" />
|
||||
<button
|
||||
id="aiGeneratorKeyHelp"
|
||||
class="icon-help-circled"
|
||||
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"
|
||||
data-tip="Open provider's website to get the API key there. Note: the Map Generator doesn't store the key or any generated data"
|
||||
/>
|
||||
<div id="ollamaHint" style="display: none; font-size: 0.85em; color: #999; margin-top: 0.5em;">
|
||||
Using Ollama requires it to be running locally on your machine at http://localhost:11434
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@ const PROVIDERS = {
|
|||
anthropic: {
|
||||
keyLink: "https://console.anthropic.com/account/keys",
|
||||
generate: generateWithAnthropic
|
||||
},
|
||||
ollama: {
|
||||
keyLink: "https://ollama.com/library", // Link to Ollama model library
|
||||
generate: generateWithOllama
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -22,11 +26,17 @@ const MODELS = {
|
|||
"o1-mini": "openai",
|
||||
"claude-3-5-haiku-latest": "anthropic",
|
||||
"claude-3-5-sonnet-latest": "anthropic",
|
||||
"claude-3-opus-latest": "anthropic"
|
||||
"claude-3-opus-latest": "anthropic",
|
||||
"Ollama (enter model in key field)": "ollama" // Entry for Ollama
|
||||
};
|
||||
|
||||
const SYSTEM_MESSAGE = "I'm working on my fantasy map.";
|
||||
|
||||
// Initialize a flag for one-time setup if it doesn't exist
|
||||
if (typeof modules.generateWithAi_setupDone === 'undefined') {
|
||||
modules.generateWithAi_setupDone = false;
|
||||
}
|
||||
|
||||
async function generateWithOpenAI({key, model, prompt, temperature, onContent}) {
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
|
|
@ -49,7 +59,7 @@ async function generateWithOpenAI({key, model, prompt, temperature, onContent})
|
|||
if (content) onContent(content);
|
||||
};
|
||||
|
||||
await handleStream(response, getContent);
|
||||
await handleStream(response, getContent, "openai");
|
||||
}
|
||||
|
||||
async function generateWithAnthropic({key, model, prompt, temperature, onContent}) {
|
||||
|
|
@ -73,13 +83,60 @@ async function generateWithAnthropic({key, model, prompt, temperature, onContent
|
|||
if (content) onContent(content);
|
||||
};
|
||||
|
||||
await handleStream(response, getContent);
|
||||
await handleStream(response, getContent, "anthropic");
|
||||
}
|
||||
|
||||
async function handleStream(response, getContent) {
|
||||
async function generateWithOllama({key, model, prompt, temperature, onContent}) {
|
||||
// For Ollama, 'key' is the actual model name entered by the user.
|
||||
// 'model' is the value from the dropdown, e.g., "Ollama (enter model in key field)".
|
||||
const ollamaModelName = key;
|
||||
|
||||
const headers = {
|
||||
"Content-Type": "application/json"
|
||||
};
|
||||
|
||||
const body = {
|
||||
model: ollamaModelName,
|
||||
prompt: prompt,
|
||||
system: SYSTEM_MESSAGE,
|
||||
options: {
|
||||
temperature: temperature
|
||||
},
|
||||
stream: true
|
||||
};
|
||||
|
||||
const response = await fetch("http://localhost:11434/api/generate", {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
const getContent = json => {
|
||||
// Ollama streams JSON objects with a "response" field for content
|
||||
// and "done": true in the final message (which might have an empty response).
|
||||
if (json.response) {
|
||||
onContent(json.response);
|
||||
}
|
||||
};
|
||||
|
||||
await handleStream(response, getContent, "ollama");
|
||||
}
|
||||
|
||||
async function handleStream(response, getContent, providerType) {
|
||||
if (!response.ok) {
|
||||
const json = await response.json();
|
||||
throw new Error(json?.error?.message || "Failed to generate");
|
||||
let errorMessage = `Failed to generate (${response.status} ${response.statusText})`;
|
||||
try {
|
||||
const json = await response.json();
|
||||
if (providerType === "ollama" && json?.error) {
|
||||
errorMessage = json.error;
|
||||
} else {
|
||||
errorMessage = json?.error?.message || json?.error || `Failed to generate (${response.status} ${response.statusText})`;
|
||||
}
|
||||
} catch (e) {
|
||||
// Error message is already set, or parsing failed.
|
||||
ERROR && console.error("Failed to parse error response JSON:", e)
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
|
|
@ -95,12 +152,23 @@ async function handleStream(response, getContent) {
|
|||
|
||||
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}`);
|
||||
if (providerType === "ollama") {
|
||||
if (line) { // Ollama sends JSON objects directly, hopefully one per line
|
||||
try {
|
||||
const json = JSON.parse(line);
|
||||
getContent(json);
|
||||
} catch (jsonError) {
|
||||
ERROR && console.error(`Failed to parse JSON from Ollama:`, jsonError, `Line: ${line}`);
|
||||
}
|
||||
}
|
||||
} else { // Existing logic for OpenAI/Anthropic (SSE)
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -110,65 +178,60 @@ async function handleStream(response, getContent) {
|
|||
}
|
||||
|
||||
function generateWithAi(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.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() {
|
||||
// Helper function to update dialog UI elements
|
||||
function updateDialogElements() {
|
||||
byId("aiGeneratorResult").value = "";
|
||||
byId("aiGeneratorPrompt").value = defaultPrompt;
|
||||
byId("aiGeneratorTemperature").value = localStorage.getItem("fmg-ai-temperature") || "1";
|
||||
|
||||
const select = byId("aiGeneratorModel");
|
||||
const currentModelVal = select.value; // Preserve current selection if possible before clearing
|
||||
select.options.length = 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 storedModel = localStorage.getItem("fmg-ai-model");
|
||||
if (storedModel && MODELS[storedModel]) {
|
||||
select.value = storedModel;
|
||||
} else if (currentModelVal && MODELS[currentModelVal]) {
|
||||
select.value = currentModelVal;
|
||||
} else {
|
||||
select.value = DEFAULT_MODEL;
|
||||
}
|
||||
if (!select.value || !MODELS[select.value]) select.value = DEFAULT_MODEL; // Final fallback
|
||||
|
||||
const provider = MODELS[select.value];
|
||||
byId("aiGeneratorKey").value = localStorage.getItem(`fmg-ai-kl-${provider}`) || "";
|
||||
const keyInput = byId("aiGeneratorKey"); // Define keyInput here
|
||||
if (keyInput) { // Check if keyInput exists
|
||||
keyInput.value = localStorage.getItem(`fmg-ai-kl-${provider}`) || "";
|
||||
if (provider === "ollama") {
|
||||
keyInput.placeholder = "Enter Ollama model name (e.g., llama3)";
|
||||
} else {
|
||||
keyInput.placeholder = "Enter API Key";
|
||||
}
|
||||
} else {
|
||||
ERROR && console.error("AI Generator: Could not find 'aiGeneratorKey' element in updateDialogElements.");
|
||||
}
|
||||
}
|
||||
|
||||
async function generate(button) {
|
||||
// Async helper function for the "Generate" button
|
||||
async function doGenerate(button) {
|
||||
const key = byId("aiGeneratorKey").value;
|
||||
if (!key) return tip("Please enter an API key", true, "error", 4000);
|
||||
const modelValue = byId("aiGeneratorModel").value;
|
||||
const provider = MODELS[modelValue];
|
||||
|
||||
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];
|
||||
if (provider !== "ollama" && !key) {
|
||||
return tip("Please enter an API key", true, "error", 4000);
|
||||
}
|
||||
if (provider === "ollama" && !key) {
|
||||
return tip("Please enter the Ollama model name in the key field", true, "error", 4000);
|
||||
}
|
||||
if (!modelValue) return tip("Please select a model", true, "error", 4000);
|
||||
|
||||
localStorage.setItem("fmg-ai-model", modelValue);
|
||||
localStorage.setItem(`fmg-ai-kl-${provider}`, key);
|
||||
|
||||
const prompt = byId("aiGeneratorPrompt").value;
|
||||
if (!prompt) return tip("Please enter a prompt", true, "error", 4000);
|
||||
const promptText = byId("aiGeneratorPrompt").value;
|
||||
if (!promptText) return tip("Please enter a prompt", true, "error", 4000);
|
||||
|
||||
const temperature = byId("aiGeneratorTemperature").valueAsNumber;
|
||||
if (isNaN(temperature)) return tip("Temperature must be a number", true, "error", 4000);
|
||||
|
|
@ -179,14 +242,86 @@ function generateWithAi(defaultPrompt, onApply) {
|
|||
const resultArea = byId("aiGeneratorResult");
|
||||
resultArea.disabled = true;
|
||||
resultArea.value = "";
|
||||
const onContent = content => (resultArea.value += content);
|
||||
const onContentCallback = content => (resultArea.value += content);
|
||||
|
||||
await PROVIDERS[provider].generate({key, model, prompt, temperature, onContent});
|
||||
await PROVIDERS[provider].generate({key: key, model: modelValue, prompt: promptText, temperature, onContent: onContentCallback});
|
||||
} catch (error) {
|
||||
return tip(error.message, true, "error", 4000);
|
||||
tip(error.message, true, "error", 4000);
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
byId("aiGeneratorResult").disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
$("#aiGenerator").dialog({
|
||||
title: "AI Text Generator",
|
||||
position: {my: "center", at: "center", of: "svg"},
|
||||
resizable: false,
|
||||
width: Math.min(600, window.innerWidth - 20),
|
||||
modal: true,
|
||||
open: function() {
|
||||
// Perform one-time setup for event listeners if not already done
|
||||
if (!modules.generateWithAi_setupDone) {
|
||||
const keyHelpButton = byId("aiGeneratorKeyHelp");
|
||||
if (keyHelpButton) {
|
||||
keyHelpButton.addEventListener("click", function () {
|
||||
const modelValue = byId("aiGeneratorModel").value;
|
||||
const provider = MODELS[modelValue];
|
||||
if (provider === "ollama") {
|
||||
openURL(PROVIDERS.ollama.keyLink);
|
||||
} else if (provider && PROVIDERS[provider] && PROVIDERS[provider].keyLink) {
|
||||
openURL(PROVIDERS[provider].keyLink);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ERROR && console.error("AI Generator: Could not find 'aiGeneratorKeyHelp' element for event listener.");
|
||||
}
|
||||
|
||||
const modelSelect = byId("aiGeneratorModel");
|
||||
if (modelSelect) {
|
||||
modelSelect.addEventListener("change", function() {
|
||||
const newModelValue = this.value;
|
||||
const newProvider = MODELS[newModelValue];
|
||||
const keyInput = byId("aiGeneratorKey");
|
||||
if (keyInput) {
|
||||
if (newProvider === "ollama") {
|
||||
keyInput.placeholder = "Enter Ollama model name (e.g., llama3)";
|
||||
} else {
|
||||
keyInput.placeholder = "Enter API Key";
|
||||
}
|
||||
// Load the stored key for the newly selected provider
|
||||
keyInput.value = localStorage.getItem(`fmg-ai-kl-${newProvider}`) || "";
|
||||
} else {
|
||||
ERROR && console.error("AI Generator: Could not find 'aiGeneratorKey' element during model change listener.");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ERROR && console.error("AI Generator: Could not find 'aiGeneratorModel' element for event listener.");
|
||||
}
|
||||
modules.generateWithAi_setupDone = true;
|
||||
}
|
||||
|
||||
// Always update dialog elements when dialog is opened
|
||||
updateDialogElements();
|
||||
},
|
||||
buttons: {
|
||||
"Generate": function (e) {
|
||||
// The button passed to doGenerate is the DOM element itself, not the jQuery event object.
|
||||
doGenerate(e.currentTarget || 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");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Expose the generateWithAi function
|
||||
modules.generateWithAi = generateWithAi;
|
||||
window.generateWithAi = generateWithAi;
|
||||
|
|
|
|||
|
|
@ -55,7 +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("notesGenerateWithAi").addEventListener("click", openAiGenerator); // Ensure the original listener attachment is present and correct
|
||||
byId("notesDownload").addEventListener("click", downloadLegends);
|
||||
byId("notesUpload").addEventListener("click", () => legendsToLoad.click());
|
||||
byId("legendsToLoad").addEventListener("change", function () {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue