Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion docs/wiki/Knowledge Base.md
Original file line number Diff line number Diff line change
Expand Up @@ -542,7 +542,31 @@ Questions are budgeted per day to keep the shared service affordable. Anonymous

### What does the assistant send, and where?

Only the question you type and a conversation id go to the project's help gateway at ask.azgaarsfmg.com; nothing from your map or browser is sent. Answers are drawn from the wiki and Discord knowledge. Signing in with Discord stores a token in this browser, and 'Sign out' removes it. Questions are kept for 90 days to improve the documentation; the 'Policy' link at the bottom of the panel opens the full details at https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Policy
Only the question you type and a conversation id go to the project's help gateway at ask.azgaarsfmg.com; nothing from your map or browser is sent. Answers are drawn from the wiki and Discord knowledge. Signing in with Discord stores a token in this browser, and 'Sign out' removes it. Questions are kept for 90 days to improve the documentation; the 'Policy' link at the bottom of the panel opens the full details at https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Policy. This is the 'Help' tab; the 'This map' tab is a different thing and is covered below

### Can the assistant answer questions about my own map? What is the 'This map' tab?

Yes, in the assistant panel's second tab. 'Help' explains how the Generator works and never sees your map; 'This map' reads the world you have open and answers questions about it, such as which states have no ports or how the land is split between biomes. It runs on your own AI provider and your own API key, so it works on any copy of the Generator, including self-hosted ones. You can also open it straight from Tools -> Assistant

### Do I need an API key for the assistant?

Only for the 'This map' tab. 'Help' is free and needs nothing. 'This map' talks to an AI provider of your choice, so it needs a key from that provider, or a local server such as Ollama, which needs no key at all. Open the panel's settings with the cog at the bottom right of the panel, pick a provider, pick a model, and paste the key. It is stored in this browser only and is sent only to that provider

### Which AI providers and models can the assistant use?

Anthropic, OpenAI, Mistral, Qwen, DeepSeek, and any OpenAI-compatible local server (Ollama, llama.cpp, LM Studio). Choose the provider first in the settings drawer and the model list narrows to that provider's models. If your key can use models newer than the built-in list, they are fetched from the provider and added automatically. Bigger models reason better and cost more; you pay the provider directly

### Can the AI change my map? Can it write my notes?

It can write notes, and nothing else. Open the Notes Editor on any element and press the robot button, or switch to 'This map' while the editor is open, and ask for a description, a rewrite or a change of tone. The note updates in the editor and in the notes box as soon as the assistant writes it. Every edit shows in the chat with an Undo button that restores the previous text. The rest of the map is read-only to the assistant

### How do I undo something the assistant wrote?

Each edit appears in the transcript as its own line with an Undo button next to it; pressing it puts the note back exactly as it was, and removes the note entirely if the assistant had created it. The button greys out once used. Undo stays available for as long as the chat is kept, so you can reverse an edit after asking further questions

### How much does the 'This map' assistant cost?

You pay your chosen provider for what you send; the project charges nothing and runs no server for it. The line under the message box shows the tokens spent on the current chat. Every earlier turn is re-sent with each new message, so a long chat costs more per message than a fresh one — the New chat button in the panel's title bar starts a fresh one

### How to make a river?

Expand Down
12 changes: 7 additions & 5 deletions docs/wiki/Ollama-text-generation.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,12 @@ If you are running FMG locally, you don't need this step. But if you want Ollama
## Step 4: Start the server

1. Open Command Prompt/Terminal and type `ollama serve`. **Leave this window open** — Ollama is now running.
2. Open Fantasy Map Generator and open the AI text generator with the robot button in the Notes editor. Select **ollama (local models)** from the model list.
3. In the key field, type the model name: `llama3.2` (or whatever model you downloaded). This field is a model name, not an API key.
4. Update the prompt, optionally adjust the temperature, and click **Generate**
2. Open Fantasy Map Generator and open the Assistant: the robot button in the Notes editor, or _Tools → Assistant_. Switch to the **This map** tab.
3. Open the settings with the cog at the bottom right of the panel and choose **Local** as the provider.
4. Leave the model as **custom model…** and type the model name in the field beside the server address: `llama3.2`, or whatever `ollama list` reports. Installed models that Ollama reports are also offered in the model list directly.
5. Leave the API key empty — a local server needs none — and send your message.

**Important:** Fantasy Map Generator connects to Ollama at `http://localhost:11434/api/generate`. This should work automatically by default. If you need to change the connection address, modify the endpoint in `src/controllers/ai-generator.ts` in a local source build.
**Important:** the Assistant talks to Ollama through its OpenAI-compatible endpoint at `http://localhost:11434/v1`. That is the default in the server field, so it should work as it stands. If Ollama runs on another machine or port, put its address there, and allow the Generator's origin on the Ollama side with `OLLAMA_ORIGINS`.

That's It! You can now generate text using your local AI model.

Expand All @@ -92,7 +93,8 @@ That's It! You can now generate text using your local AI model.
**If it doesn't work:**
- Check that `ollama serve` is still running in your command prompt/terminal
- Try typing `ollama list` to see if your model downloaded correctly
- Make sure the model name you typed in the key field matches exactly what `ollama list` reports
- Make sure the model name you typed matches exactly what `ollama list` reports
- Smaller local models often cannot write the scripts the assistant uses to read your map. If answers come back empty or malformed, try a larger model
- If you use the hosted version of FMG, double-check `OLLAMA_ORIGINS` includes `https://azgaar.github.io`

## Other providers
Expand Down
26 changes: 22 additions & 4 deletions docs/wiki/Policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,17 @@ These are off by default. Using one means sending data to that third party under

* **Dropbox** — only if you sign in from _Save/Load → Cloud_, and only the map files you choose to store there.
* **Google Translate** — only if you switch the interface language; the page text is then processed by Google.
* **AI text generation** (_Tools → AI generator_) — your prompt goes directly from your browser to the provider you pick (OpenAI, Anthropic, or a local Ollama instance). The API key you enter is kept in this browser's `localStorage` and is sent only to that provider. The project never sees the prompt or the key.
* **AI text generation** (the Assistant's _This map_ tab, described below) — your messages go directly from your browser to the provider you pick. The API key you enter is kept in this browser's `localStorage` and is sent only to that provider. The project never sees the messages or the key.

## Azgaar Assistant

The Assistant answers questions about using the Generator. It talks to the project's help gateway at `ask.azgaarsfmg.com`, which is the only FMG server involved anywhere in the tool.
The Assistant panel holds two separate chats, and they are worth telling apart because only one of them ever sees your map.

**What is sent.** The question you type and a conversation id. Nothing from your map, your files or your browser is sent — the Assistant cannot see the world you are working on and cannot describe it back to you.
### Help

**Help** answers questions about using the Generator. It talks to the project's help gateway at `ask.azgaarsfmg.com`, which is the only FMG server involved anywhere in the tool.

**What is sent.** The question you type and a conversation id. Nothing from your map, your files or your browser is sent — Help cannot see the world you are working on and cannot describe it back to you.

**How long questions are kept.** Questions and the answers given to them are retained for **90 days**, then deleted. They are read only to fix wrong answers and to find gaps in the wiki. They are not published, shared or used to identify anyone.

Expand All @@ -35,7 +39,21 @@ The Assistant answers questions about using the Generator. It talks to the proje

**Availability.** The gateway only accepts requests from the official site. On a self-hosted copy the panel says so and offers the wiki instead.

**Turning it off.** _Options → Generator settings → Azgaar assistant → Hide_ removes the button and the panel. Nothing is sent when you do not ask a question.
### This map

**This map** is the other tab, and it is the opposite arrangement: it uses **your** AI provider and **your** API key, and it does read the map you have open. The project runs no server for it and sees none of it.

**What is sent, and to whom.** Your messages go from your browser straight to the provider you choose in the panel's settings drawer — Anthropic, OpenAI, Mistral, Qwen, DeepSeek, or a local server such as Ollama that you run yourself. To answer a question the assistant writes small scripts that read the open map, and **what those scripts return travels back to that provider as part of the conversation**: burg names, state statistics, the text of a note, whatever the question needed. Treat it as you would pasting that material into any AI chat. Nothing goes to FMG, and a local server keeps everything on your own machine.

**Your key.** It is stored in this browser's `localStorage`, one slot per provider, and is sent only to that provider. Clearing site data removes it.

**What it may change.** Notes, and only notes, through a single tool. It cannot edit anything else on the map. Every edit appears in the transcript with an **Undo** that puts the note back as it was, and the Generator's own save files are untouched until you save.

**Chats.** Conversations are kept in this browser's `localStorage` so you can return to them, with the oldest dropped as the space fills. Each is tied to the map it was about. The drawer lists them and deletes them; the **New chat** button in the title bar starts a fresh one.

**Cost.** You pay your provider directly for what you send. The panel shows the tokens spent on the current chat, and a fresh chat costs less than a long one because every earlier turn is re-sent with each message.

**Turning it off.** _Options → Generator settings → Azgaar assistant → Hide_ removes the button and the panel. Nothing is sent from either tab when you do not ask a question, and This map sends nothing at all until you enter a key.

## Signing in with Discord

Expand Down
2 changes: 1 addition & 1 deletion docs/wiki/Quick-Start-Tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ Generator settings:
* _Transparency_: opacity of the control panes.
* _Autosave interval_: number of minutes the map should be auto-saved to browser memory. Set to `0` to disable the autosave.
* _Onload behavior_: define what should be done when Generator is opened: a new map generated or a previously saved map auto-opened.
* _Azgaar assistant_: show or hide the help bubble in the bottom right corner, which answers questions about using the Generator.
* _Azgaar assistant_: show or hide the assistant button in the bottom right corner. Its _Help_ tab answers questions about using the Generator; its _This map_ tab reads the world you have open and writes your notes, using your own AI provider and key.
* _Speaker voice_: select the voice used to speak burg and other names. Voice synthesis is provided by the browser.
* _Emblem shape_: defines shield shape used during emblems generation.
* _Zoom extent_: minimal and maximal zoom levels. Click on the button on the right to restore the default values.
Expand Down
2 changes: 1 addition & 1 deletion nix/package.nix
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ buildNpmPackage (finalAttrs: {
jq '(.version, .packages."".version) |= "0.0.0"' \
${../package-lock.json} > $out/package-lock.json
'';
hash = "sha256-xxsEz/ZI9ymRVgLlU9W50NBFz/tAFg5tHIx4x8+KeDs=";
hash = "sha256-fBsSd7CZ4GzQ2Oh4j+HVGqWsi6ozezW7Y1joJ+TMw4o=";
};

# the lock file is hashed with its version flattened, so the copy npm checks must match
Expand Down
33 changes: 33 additions & 0 deletions src/controllers/help-assistant-map.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,39 @@ describe("map panel", () => {
expect(el("helpMapLog").querySelector(".helpAssistantMsg.user")).toBeNull();
});

it("lists providers separately and narrows the model list to the one chosen", () => {
mountMapPanel(el("host"));
const provider = el<HTMLSelectElement>("helpMapProvider");
const model = el<HTMLSelectElement>("helpMapModel");

// the stored model decides which provider starts selected
expect(provider.value).toBe("anthropic");
expect([...model.options].map(option => option.value)).toEqual([
"claude-sonnet-5",
"claude-opus-4-8",
"claude-haiku-4-5"
]);
expect(model.value).toBe("claude-sonnet-5");
expect([...provider.options].map(option => option.value)).toContain("mistral");

provider.value = "mistral";
provider.dispatchEvent(new Event("change"));
expect([...model.options].map(option => option.value)).toEqual(["mistral-small-latest", "mistral-medium-latest"]);
expect(model.value).toBe("mistral-small-latest");
expect(el("helpMapStatusModel").textContent).toContain("mistral-small-latest");
});

it("shows the local server fields only for the local provider", () => {
mountMapPanel(el("host"));
expect(el("helpMapLocal").hidden).toBe(true);

const provider = el<HTMLSelectElement>("helpMapProvider");
provider.value = "local";
provider.dispatchEvent(new Event("change"));
expect(el("helpMapLocal").hidden).toBe(false);
expect(el("helpMapStatusModel").textContent).toContain("local model");
});

it("toggles the drawer from the gear and the status model button", () => {
mountMapPanel(el("host"));
el<HTMLButtonElement>("helpMapSettings").click();
Expand Down
47 changes: 31 additions & 16 deletions src/controllers/help-assistant-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ function panelHtml(): string {
<select id="helpMapConversation" title="Switch between chats. Each one is sent in full with every message, so a fresh one costs less"></select>
<button id="helpMapDelete" type="button" class="icon-trash" title="Delete this chat" aria-label="Delete this chat"></button>
</label>
<label>
<span>Provider</span>
<select id="helpMapProvider" title="Who runs the model. Each provider keeps its own key"></select>
</label>
<label>
<span>Model</span>
<select id="helpMapModel" title="Bigger models reason better and cost more"></select>
Expand Down Expand Up @@ -219,13 +223,24 @@ function setInitialValues(): void {
PROVIDERS.forEach(provider => {
registerModels(provider.id, cachedModels(provider.id));
});
buildModelSelect();

const select = ensureEl<HTMLSelectElement>("helpMapModel");
const providerSelect = ensureEl<HTMLSelectElement>("helpMapProvider");
providerSelect.replaceChildren();
providerSelect.append(...PROVIDERS.map(provider => new Option(provider.label, provider.id)));

// the stored model decides the provider, not the other way round: it is the only thing persisted
const stored = localStorage.getItem(MODEL_STORAGE) ?? "";
select.value = isKnownModel(stored) ? stored : DEFAULT_MODEL;
const model = isKnownModel(stored) ? stored : DEFAULT_MODEL;
providerSelect.value = providerOf(model).id;
buildModelSelect();
ensureEl<HTMLSelectElement>("helpMapModel").value = model;

select.addEventListener("change", () => {
providerSelect.addEventListener("change", () => {
buildModelSelect(); // falls to the provider's first model
loadKeyForModel();
void refreshModels();
});
ensureEl("helpMapModel").addEventListener("change", () => {
loadKeyForModel();
void refreshModels();
});
Expand All @@ -243,28 +258,28 @@ function isKnownModel(model: string): boolean {
}
}

// One provider's models only: the flat list across every provider was too long to pick from
function buildModelSelect(): void {
const providerId = ensureEl<HTMLSelectElement>("helpMapProvider").value;
const provider = PROVIDERS.find(candidate => candidate.id === providerId) ?? PROVIDERS[0];
const select = ensureEl<HTMLSelectElement>("helpMapModel");
const previous = select.value;
select.replaceChildren(); // options.length = 0 would leave the old optgroup shells behind
PROVIDERS.forEach(provider => {
const group = document.createElement("optgroup");
group.label = provider.label;
mergeModels(provider.models, cachedModels(provider.id)).forEach(model => {
group.append(new Option(model === LOCAL_MODEL ? "custom model…" : model, model));
});
select.append(group);
select.replaceChildren();
mergeModels(provider.models, cachedModels(provider.id)).forEach(model => {
select.append(new Option(model === LOCAL_MODEL ? "custom model…" : model, model));
});
if (previous && isKnownModel(previous)) select.value = previous;
// keep the choice when the list is only being refreshed, otherwise take the first model
const keep = [...select.options].some(option => option.value === previous);
select.value = keep ? previous : (select.options[0]?.value ?? "");
}

// Ask the selected provider what its key can actually use, so new models appear without a release
async function refreshModels(): Promise<void> {
const provider = providerOf(ensureEl<HTMLSelectElement>("helpMapModel").value);
const providerId = ensureEl<HTMLSelectElement>("helpMapProvider").value;
const key = ensureEl<HTMLInputElement>("helpMapKey").value;
if (provider.id !== "local" && !key) return;
if (providerId !== "local" && !key) return;
try {
await listModels(provider.id, key);
await listModels(providerId as (typeof PROVIDERS)[number]["id"], key);
if (document.getElementById("helpMapModel")) buildModelSelect();
} catch {
// unreachable server or bad key: the curated list stands
Expand Down
4 changes: 3 additions & 1 deletion src/controllers/help-assistant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,9 @@ function renderDialog(): void {
// its stylesheet rides along with it instead of sitting in the global sheet
const styles = /* html */ `
<style>
#helpAssistant.ui-dialog-content { display: flex; flex-direction: column; gap: .5em; overflow: hidden; padding: .6em .7em .5em; font-family: var(--sans-serif); }
/* .ui-dialog turns selection off so dialogs drag cleanly; an answer is there to be copied,
so the body opts back in and only the titlebar keeps the drag behaviour */
#helpAssistant.ui-dialog-content { display: flex; flex-direction: column; gap: .5em; overflow: hidden; padding: .6em .7em .5em; font-family: var(--sans-serif); user-select: text; }
#helpAssistant > div { width: auto; }
.ui-dialog-titlebar .helpAssistantNewChat { font-size: .62em; }

Expand Down
Loading