Refactor the AppConfig UX: main-view Settings tab with sidebar navigation, search, and dirty tracking (#110)

* refactor: move application settings from modal to main-view Settings tab with sidebar nav, search, and dirty tracking (#108)

* fix: address settings-view review findings (system prompt defaults lost after save, dirty-on-type for prompt overrides, scoped embeddings busy-lock, external-change false positives)

* style: give settings pages a surface-colored canvas so reused editors blend like they did in the modal; anchor env variables in a card with empty state

* style: rename settings groups Creator->Storytelling and General/Gameplay->Game/General, distinct group icons; drop content-classification influence claim

* docs: refresh settings screenshots for the Storytelling and Game/General relabels

* fix: address PR review - decouple settings working copy from shared ws payload, clear saving flag on failed save, gate help-agent config writes on dirty state instead of open settings view, data-drive provider icons

* chore: drop dead null-guard on system-prompt-defaults binding (card is v-if gated on app_config)

* docs: update remaining pages describing the removed settings modal (apis/, client presets, outline, runpod note), fresh per-provider screenshots, help docs-index paths; reword system-prompts client hint

* docs: recapture per-provider API key screenshots as card element shots so each page shows its own provider
This commit is contained in:
veguAI
2026-07-20 00:55:37 +03:00
committed by GitHub
parent e3c1c231df
commit e54938d37b
94 changed files with 1364 additions and 1120 deletions

View File

@@ -13,6 +13,7 @@
- "Simplified Character Card Import: The character card import dialog gained an AI Generation section with individual toggles for each generation step — content context, description rewrite, attribute extraction, dialogue instructions, example dialogue, and story intent — plus Full/Minimal preset buttons to flip them all at once. Disabled steps fall back to the card's original data (the description and example dialogue import verbatim), so a minimal import needs no text generation at all and completes in a fraction of the time."
- "Scene Browser Landing Page: The home screen was rebuilt into a full-page landing view, replacing the sidebar load panel. A file-tree Scene Library lists scene projects with cover thumbnails, save counts and per-save metadata, plus an asset and node-module summary row per expanded project; long save lists collapse behind a 'Show all' row and the list is filterable with click-to-load. Individual scene files and character cards can be deleted from the tree, and an entire scene project (saves, assets, nodes, history) can be deleted behind a type-the-project-name confirmation. Character cards display their card image and only actual card files are listed. Scene and character card import moved to a prominent drag-and-drop dropzone next to a Create-new-scene card, with the quick-load recent scene cards kept on top."
improvements:
- "Application Settings Redesign: The application settings moved out of their cramped modal into the main view as an always-available Settings tab, matching the World Editor and Templates. The nested tabs-within-tabs layout is replaced by a single sidebar navigation grouped by topic (Game, Appearance, Connections, Presets, Storytelling), with a settings search that finds any setting by name — including individual API-key providers — and jumps straight to it, highlighting the target. All API keys now live on one scrollable page instead of ten separate per-provider pages. Edits are tracked against the stored configuration: an Unsaved-changes indicator with Save and Discard appears in the toolbar, unsaved edits survive switching to other tabs, and configuration changed elsewhere (another window, the help agent) while you are editing is flagged instead of silently overwriting or being overwritten. Existing deep links — for example the 'Set API key' action on a misconfigured client — land on the correct page as before."
- "Uniform Settings Framework: Agent settings and client settings now share one field-definition schema on the backend and one field renderer on the frontend. Client-specific settings gain the full widget set previously exclusive to agents (sliders, autocompletes, selects with rich choices, per-value notes) plus conditional visibility, and choice lists are delivered to the frontend in a single normalized shape."
- "MistralAI Client: Added a Concurrent Inference toggle so batch operations can dispatch multiple requests in parallel. Off by default."
- "Text-Generation-WebUI Client: Added an 'API handles prompt template' toggle. When enabled, requests go to the chat/completions API and text-generation-webui applies the model's prompt template; response pre-filling (coercion) keeps working via the API's continue_ mechanism, and reasoning-model thinking is captured automatically without needing a reasoning pattern. Off by default."

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

View File

@@ -32,13 +32,15 @@ So things like "switch the conversation format to Narrative", "lower the editor
Some things are intentionally out of reach:
- API keys and passwords are never shown to the help agent and it cannot change them - key management stays in the settings dialog.
- API keys and passwords are never shown to the help agent and it cannot change them - key management stays in the application settings (Connections → API Keys).
- Clients are read-only - client changes (model, context length, etc.) must be made in the client settings.
- Complex settings (tables, weight maps, template pickers) are read-only in chat and must be edited in their settings dialog.
- Complex settings (tables, weight maps, template pickers) are read-only in chat and must be edited in the settings interface directly.
!!! note "Open settings dialogs"
If the settings dialog of the agent you want to change is currently open, the help agent will ask you to close it first. The dialog holds its own copy of the settings - it would not show the change, and closing it could overwrite it.
Application settings work differently: having the Settings tab open is fine, and applied changes show up in it live. Only when the settings view has **unsaved edits** will the help agent ask you to save or discard them before it changes an application setting.
## Chats
Help conversations are multi-turn and you can keep several of them - use the chat selector at the top of the help drawer to switch, create, or delete chats. Chats persist across restarts (they are stored in `chats/help.json` in your Talemate directory, independent of any scene).
@@ -49,7 +51,7 @@ The most recent answer can be regenerated via the refresh button next to the mes
Each chat has a **Scene Aware** toggle. When enabled and a scene is loaded, the help agent can see the scene title, its characters, and recent scene progress, so you can ask scene-specific questions. When disabled the conversation is completely unaware of your scene.
The help agent also receives a small snapshot of what you are currently looking at in the interface (active tab, open panels, and any open client or agent settings dialog including its selected tab), so questions like "what does this setting do?" can be answered in context.
The help agent also receives a small snapshot of what you are currently looking at in the interface (active tab, open panels, the settings view's current page, and any open client or agent settings dialog including its selected tab), so questions like "what does this setting do?" can be answered in context.
## Non-blocking

View File

@@ -2,9 +2,7 @@
You can manage your available embeddings through the application settings.
![Open settings](/talemate/img/0.26.0/open-settings.png)
In the settings dialogue go to **:material-tune: Presets** and then **:material-cube-unfolded: Embeddings**.
Open the **:material-cog: Settings** tab in the top navigation and select **:material-cube-unfolded: Embeddings** under the **Presets** group.
!!! warning "INSTRUCTOR Embeddings Removed (0.35.0)"
INSTRUCTOR embeddings are no longer supported. If you were using INSTRUCTOR embeddings, your configuration has been automatically reset to use the default embedding model (all-MiniLM-L6-v2).

View File

@@ -15,7 +15,7 @@ Before configuring the Google backend, you need to obtain a Google API key:
Then configure it in Talemate:
1. Open Talemate Settings → Application → Google
1. Open Talemate Settings → Connections → API Keys → Google
2. Paste your Google API key in the "Google API Key" field
3. Save your changes

View File

@@ -15,7 +15,7 @@ Before configuring the OpenAI backend, you need to obtain an OpenAI API key:
Then configure it in Talemate:
1. Open Talemate Settings → Application → OpenAI API
1. Open Talemate Settings → Connections → API Keys → OpenAI
2. Paste your OpenAI API key in the "OpenAI API Key" field
3. Save your changes

View File

@@ -15,7 +15,7 @@ Before configuring the OpenRouter backend, you need to obtain an OpenRouter API
Then configure it in Talemate:
1. Open Talemate Settings → Application → OpenRouter API
1. Open Talemate Settings → Connections → API Keys → OpenRouter
2. Paste your OpenRouter API key in the "OpenRouter API Key" field
3. Save your changes

View File

@@ -4,10 +4,8 @@ Talemate can use Anthropic for text generation.
You need to have an API key for Anthropic and set it in the application config. You can manage your Anthropic API keys at [https://console.anthropic.com/settings/keys](https://console.anthropic.com/settings/keys)
Once you have your key open the Talemate settings and click the `APPLICATION` tab and then the `ANTHROPIC` category. Here you can add your Anthropic API key.
Once you have your key open the Talemate settings (the **Settings** tab in the top navigation), go to **Connections → API Keys** and scroll to (or search for) **Anthropic**. Here you can add your Anthropic API key.
![Open settings](/talemate/img/0.26.0/open-settings.png)
![Set Anthropic Api Key](/talemate/img/0.26.0/anthropic-settings.png)
![Set Anthropic API key](/talemate/img/0.39.0/api-keys-anthropic.png)
Then click `Save` to save your changes.

View File

@@ -4,10 +4,8 @@ Talemate can use Cohere for text generation.
You need to have an API key for Cohere and set it in the application config. You can manage your Cohere API keys at [https://dashboard.cohere.com/api-keys](https://dashboard.cohere.com/api-keys)
Once you have your key open the Talemate settings and click the `APPLICATION` tab and then the `COHERE` category. Here you can add your Cohere API key.
Once you have your key open the Talemate settings (the **Settings** tab in the top navigation), go to **Connections → API Keys** and scroll to (or search for) **Cohere**. Here you can add your Cohere API key.
![Open settings](/talemate/img/0.26.0/open-settings.png)
![Set Cohere Api Key](/talemate/img/0.26.0/cohere-settings.png)
![Set Cohere API key](/talemate/img/0.39.0/api-keys-cohere.png)
Then click `Save` to save your changes.

View File

@@ -3,11 +3,9 @@ To use ElevenLabs TTS with Talemate, follow these steps:
1. Visit [ElevenLabs](https://elevenlabs.com) and create an account if you don't already have one.
2. Click on your profile in the upper right corner of the Eleven Labs website to access your API key.
In Talemate open the settings and click the `APPLICATION` tab and then the `ELEVENLABS` category. Here you can add your ElevenLabs API key.
In Talemate open the settings and go to **Connections → API Keys** and scroll to (or search for) **ElevenLabs**. Here you can add your ElevenLabs API key.
![Open settings](/talemate/img/0.26.0/open-settings.png)
![Runpod settings](/talemate/img/0.26.0/elevenlabs-settings.png)
![Set ElevenLabs API key](/talemate/img/0.39.0/api-keys-elevenlabs.png)
Click `Save` to save your changes.

View File

@@ -6,9 +6,9 @@ Please follow their [instructions for setup](https://cloud.google.com/vertex-ai/
Once you have downloaded the credentials, copy the JSON file into the talemate directory. You can rename it to something that's easier to remember, like `creds.json`.
![Open settings](/talemate/img/0.26.0/open-settings.png)
In the Talemate settings (the **Settings** tab in the top navigation), go to **Connections → API Keys** and scroll to (or search for) **Google** to fill in the credentials path and location.
![Set Google creds](/talemate/img/0.26.0/google-settings.png)
![Set Google creds](/talemate/img/0.39.0/api-keys-google.png)
Then click `Save` to save your changes.

View File

@@ -4,10 +4,8 @@ Talemate can use Groq for text generation.
You need to have an API key for Groq and set it in the application config. You can manage your Groq API keys at [https://console.groq.com/keys](https://console.groq.com/keys)
Once you have your key open the Talemate settings and click the `APPLICATION` tab and then the `GROQ` category. Here you can add your Groq API key.
Once you have your key open the Talemate settings (the **Settings** tab in the top navigation), go to **Connections → API Keys** and scroll to (or search for) **groq**. Here you can add your Groq API key.
![Open settings](/talemate/img/0.26.0/open-settings.png)
![Set Groq Api Key](/talemate/img/0.26.0/groq-settings.png)
![Set groq API key](/talemate/img/0.39.0/api-keys-groq.png)
Then click `Save` to save your changes.

View File

@@ -4,10 +4,8 @@ Talemate can use Mistral.ai for text generation.
You need to have an API key for Mistral.ai and set it in the application config. You can manage your Mistral.ai API keys at [https://console.mistral.ai/api-keys/](https://console.mistral.ai/api-keys/)
Once you have your key open the Talemate settings and click the `APPLICATION` tab and then the `MISTRAL.AI` category. Here you can add your Mistral.ai API key.
Once you have your key open the Talemate settings (the **Settings** tab in the top navigation), go to **Connections → API Keys** and scroll to (or search for) **mistral.ai**. Here you can add your Mistral.ai API key.
![Open settings](/talemate/img/0.26.0/open-settings.png)
![Set Mistral.ai Api Key](/talemate/img/0.26.0/mistral-settings.png)
![Set mistral.ai API key](/talemate/img/0.39.0/api-keys-mistral.png)
Then click `Save` to save your changes.

View File

@@ -4,10 +4,8 @@ Talemate can use OpenAI for text, image and audio generation.
You need to have an API key for OpenAI and set it in the application config. You can manage your OpenAI API keys at [https://platform.openai.com/api-keys](https://platform.openai.com/api-keys)
Once you have your key open the Talemate settings and click the `APPLICATION` tab and then the `OPENAI` category. Here you can add your OpenAI API key.
Once you have your key open the Talemate settings (the **Settings** tab in the top navigation), go to **Connections → API Keys** and scroll to (or search for) **OpenAI**. Here you can add your OpenAI API key.
![Open settings](/talemate/img/0.26.0/open-settings.png)
![Set OpenAI Api Key](/talemate/img/0.26.0/openai-settings.png)
![Set OpenAI API key](/talemate/img/0.39.0/api-keys-openai.png)
Then click `Save` to save your changes.

View File

@@ -4,8 +4,8 @@ Talemate can use any model accessible through OpenRouter.
You need an OpenRouter API key and must set it in the application config. You can create and manage keys in your OpenRouter dashboard at [https://openrouter.ai/keys](https://openrouter.ai/keys).
Once you have generated a key open the Talemate settings, switch to the `APPLICATION` tab and then select the `OPENROUTER API` category. Paste your key in the **API Key** field.
Once you have generated a key open the Talemate settings (the **Settings** tab in the top navigation), go to **Connections → API Keys** and scroll to (or search for) **OpenRouter**. Paste your key in the **API Key** field.
![OpenRouter API settings](/talemate/img/0.31.0/openrouter-settings.png)
![Set OpenRouter API key](/talemate/img/0.39.0/api-keys-openrouter.png)
Finally click **Save** to store the credentials.

View File

@@ -1,12 +1,14 @@
# :material-palette-outline: Appearance
The Appearance settings let you customize how messages and visuals are displayed in your scene. These settings are divided into two tabs: **Messages** and **Visuals**.
The Appearance settings let you customize how messages and visuals are displayed in your scene. The **Appearance** group in the settings sidebar holds two pages: **Messages** and **Visuals**.
While you edit appearance settings, the scene view previews your changes live; they only become permanent when you save.
## :material-script: Messages
![App settings - Appearance - Messages](/talemate/img/0.38.0/app-settings-appearance-messages.png)
![App settings - Appearance - Messages](/talemate/img/0.39.0/app-settings-appearance-messages.png)
The Messages tab gives you control over how text appears in the scene feed. You can customize the styling for different message types and special text formatting.
The Messages page gives you control over how text appears in the scene feed. You can customize the styling for different message types and special text formatting.
### Message Types
@@ -75,7 +77,7 @@ To change a color:
![App settings - Appearance - Visuals](/talemate/img/0.39.0/app-settings-appearance-visuals.png)
The Visuals tab controls how images appear alongside messages in your scene feed. This includes character portraits, cards, scene illustrations, and scene backgrounds.
The Visuals page controls how images appear alongside messages in your scene feed. This includes character portraits, cards, scene illustrations, and scene backgrounds.
### Visual Types

View File

@@ -1,12 +1,16 @@
# :material-application-outline: Application
# :material-connection: Connections
![App settings - Application](/talemate/img/0.38.0/app-settings-application.png)
The **Connections** group of the application settings holds credentials for third party services and the environment variable store. Open it from the **:material-cog: Settings** tab in the top navigation.
Configure various API keys for integration with external services. (OpenAI, Anthropic, etc.)
## :material-key-variant: API Keys
Each external service has its own page in the sidebar. Select a service, paste your key or token, and save.
![App settings - API Keys](/talemate/img/0.39.0/app-settings-api-keys.png)
## HuggingFace Token
Configure API keys for integration with external services. (OpenAI, Anthropic, etc.)
All services live on a single page — scroll to the service, paste your key or token, and save. You can also type a service name into the sidebar's **Search settings** field to jump directly to its entry.
### HuggingFace Token
!!! info "Added in 0.38.0"
@@ -14,11 +18,23 @@ Some features download model weights from [Hugging Face](https://huggingface.co/
To download gated weights, add a HuggingFace access token:
1. Open the **HuggingFace** page under Application settings.
1. Find the **HuggingFace** entry on the API Keys page.
2. Create a token at [https://huggingface.co/settings/tokens](https://huggingface.co/settings/tokens). A read token is sufficient.
3. Paste it into the **HuggingFace Token** field and save.
The token is shared across Talemate. Once set here, any feature that needs to download gated weights (such as the [Pocket TTS](../agents/voice/pocket-tts.md) agent) will use it automatically. You can also set the same token directly in the Pocket TTS agent config — both fields point at the same setting.
!!! note "Accepting model terms"
A token only proves who you are. For gated models you still need to visit the model's page on Hugging Face and accept its terms once with the same account before the download will succeed.
A token only proves who you are. For gated models you still need to visit the model's page on Hugging Face and accept its terms once with the same account before the download will succeed.
## :material-variable: Environment Variables
!!! info "Added in 0.39.0"
![App settings - Environment Variables](/talemate/img/0.39.0/app-settings-env-variables.png)
Named values that are passed as environment variables to processes Talemate spawns — currently the [Pi Bridge](../clients/types/pi-bridge.md) client, where pi's `models.json` can reference them as `$NAME`.
Values are encrypted at rest in Talemate's configuration file. See [API key encryption](../api-key-encryption.md) for how encryption keys are managed.
To add a variable, enter a name (letters, digits and underscores; must not start with a digit) and a value in the bottom row and press ++enter++ or the plus button. Existing variables can have their value edited in place or be removed with the delete button; remember to save.

View File

@@ -1,21 +1,27 @@
# :material-cube-scan: Creator
# :material-feather: Storytelling
The **Creator** tab in the application settings holds shared building blocks that are offered as picker options elsewhere in the UI. Open it from **Settings** (the cogwheel in the top navigation) and switch to the **Creator** tab.
The **Storytelling** group in the application settings holds shared building blocks that are offered as picker options elsewhere in the UI. Open the **:material-cog: Settings** tab in the top navigation and find the **Storytelling** group in the sidebar.
Two sub-tabs live here:
Two pages live here:
- **Content Classification** — the list of content classification strings offered when you set up a scene.
- **Perspective Presets** — the list of narrative perspective / tense strings offered in the scene outline.
Both lists are global to your Talemate installation. Editing them does not change any existing scene — your scenes keep whatever value they were saved with. The lists only affect what appears as suggestions in the pickers next time you edit a field.
## Content Classification
![Storytelling settings with the Content Classification page selected](/talemate/img/0.39.0/app-settings-creator-content-classification.png)
Available content classification choices when generating characters or scenarios. Add and remove entries the same way as perspective presets below.
## Perspective Presets
!!! info "Added in 0.38.0"
The presets you add here show up in every **Perspective and tense** combobox in the scene outline (default plus the three per-speaker overrides). They are convenience suggestions, not constraints — you can always type a custom value into any of the four fields if no preset fits.
![Creator settings with the Perspective Presets sub-tab selected](/talemate/img/0.38.0/app-settings-creator-perspective-presets.png)
![Storytelling settings with the Perspective Presets page selected](/talemate/img/0.39.0/app-settings-creator-perspective-presets.png)
### Adding a preset

View File

@@ -1,7 +1,16 @@
# Game
# :material-gamepad-square: Game
Application settings open in the main view via the **:material-cog: Settings** tab in the top navigation (or the cogwheel icon on the right side of the top bar). Navigation lives in the left sidebar, grouped by topic, with a **search field** at the top that finds any setting by name and jumps straight to it.
Changes are collected as you edit — an **Unsaved changes** indicator with **Save** and **Discard** actions appears in the toolbar whenever your edits differ from the stored configuration. Nothing is applied until you save.
![App settings - search](/talemate/img/0.39.0/app-settings-search.png)
The **Game** group holds general game behavior and the default player character.
## :material-cog: General
![App settings - Game - General](/talemate/img/0.29.0/app-settings-game-general.png)
![App settings - Game - General](/talemate/img/0.39.0/app-settings-gameplay.png)
##### Auto save
@@ -23,10 +32,18 @@ When enabled, a horizontal bar appears above the scene tools showing which agent
This setting is enabled by default.
## :material-human-edit: Default character
##### Release GPU cache on scene load
![App settings - Game - Default Character](/talemate/img/0.29.0/app-settings-game-default-character.png)
When enabled, idle GPU memory reserved by local CUDA embeddings or TTS is handed back to the driver when switching scenes, so it doesn't pile up and eventually leave too little VRAM to load another scene. Disable only if you have VRAM to spare and would rather keep it reserved.
## :material-human-edit: Player Character
![App settings - Player Character](/talemate/img/0.39.0/app-settings-player-character.png)
Lets you manage a basic default character.
This is only relevant when loading scenes that do not come with a default character. (e.g., mostly from other application exports, like ST character cards.)
This character is used when a scene is loaded that does not define a player character. Mostly relevant when you load character-cards that aren't in the talemate scene format (e.g., ST character cards).
##### Add default character to blank talemate scenes
When creating a new scene, add the default player character to the scene.

View File

@@ -1,6 +1,6 @@
# :material-tune: Presets
Change inference parameters, embedding parameters and global system prompt overrides.
Change inference parameters, embedding parameters and global system prompt overrides. The **Presets** group in the settings sidebar holds three pages: **Inference**, **Embeddings** and **System Prompts**.
## :material-matrix: Inference
@@ -9,7 +9,7 @@ Change inference parameters, embedding parameters and global system prompt overr
This document will NOT explain what each setting does.
![App settings - Application](/talemate/img/0.29.0/app-settings-presets-inference.png)
![App settings - Presets - Inference](/talemate/img/0.39.0/app-settings-presets-inference.png)
If you're familiar with editing inference parameters from other similar applications, be aware that there is a significant difference in how TaleMate handles these settings.
@@ -27,7 +27,7 @@ These are all experimental and will probably change / get merged in the future.
## :material-cube-unfolded: Embeddings
![App settings - Application](/talemate/img/0.29.0/app-settings-presets-embeddings.png)
![App settings - Presets - Embeddings](/talemate/img/0.39.0/app-settings-presets-embeddings.png)
Allows you to add, remove and manage various embedding models for the memory agent to use via chromadb.
@@ -35,7 +35,7 @@ Allows you to add, remove and manage various embedding models for the memory age
## :material-text-box: System Prompts
![App settings - Application](/talemate/img/0.29.0/app-settings-presets-system-prompts.png)
![App settings - Presets - System Prompts](/talemate/img/0.39.0/app-settings-presets-system-prompts.png)
This panel lets you override the global system prompts for the entire application for each prompt kind (Conversation, Narration, Creation, and so on). Per-client overrides live on the **System Prompts** tab of each client's [configuration dialog](../clients/client-configuration.md).

View File

@@ -18,9 +18,9 @@ If a level is blank it falls through to the next one. An empty textarea counts a
### App-level overrides
Open **Settings** (the cogwheel in the top navigation), then go to the **Presets** tab and select **System Prompts**.
Open the **:material-cog: Settings** tab in the top navigation, then select **System Prompts** under the **Presets** group in the sidebar.
![App-wide System Prompt presets with the override list on the left](/talemate/img/0.29.0/app-settings-presets-system-prompts.png)
![App-wide System Prompt presets with the override list on the left](/talemate/img/0.39.0/app-settings-presets-system-prompts.png)
App-level overrides apply to every client unless that client has its own override for the same prompt kind.

View File

@@ -6,11 +6,9 @@
If you wish to alter the inference parameters sent with the generation requests for text-generation you can do in the settings interface.
![open settings](/talemate/img/0.26.0/open-settings.png)
Open the **Settings** tab in the top navigation, then select :material-matrix: **Inference** under the :material-tune: **Presets** group in the sidebar.
Navigate to the :material-tune: **Presets** tab then select the :material-matrix: **Inference** tab.
![selected preset](/talemate/img/0.38.0/inference-presets-1.png)
![selected preset](/talemate/img/0.39.0/inference-presets-1.png)
!!! warning
Not all clients support all parameters, and generally it is assumed that the client implementation handles the parameters in a sane way, especially if values are passed for all of them. All presets are used and will be selected depending on the action the agent is performing. If you don't know what these mean, it is recommended to leave them as they are.
@@ -139,7 +137,7 @@ Because this overwrites the values of all the other presets in the group, you wi
The **Reset** action restores the currently selected preset back to its default values. As of version 0.38.0 this action also asks for confirmation before resetting, so an accidental click no longer discards your changes.
Both actions only change the values in the editor. Remember to save the settings with the **Save** button at the bottom of the dialog for them to take effect.
Both actions only change the values in the editor. Remember to save the settings with the **Save** button in the settings toolbar for them to take effect.
## Preset Groups
@@ -147,12 +145,12 @@ Initially there is a `Default` group in which the presets are edited, but if you
To add a new group, type the title in to the **New Group Name** field in the upper right and press `Enter`.
![new group](/talemate/img/0.30.0/inference-presets-custom-group-1.png)
![new group](/talemate/img/0.39.0/inference-presets-custom-group-1.png)
The new group will be added and automatically selected for editing.
![new group](/talemate/img/0.30.0/inference-presets-custom-group-2.png)
![new group](/talemate/img/0.39.0/inference-presets-custom-group-2.png)
Once you have adjusted the presets to your liking you can save the group by clicking the :material-content-save: **Save** button.

View File

@@ -12,11 +12,11 @@ The client should appear in the clients list. If you haven't setup Anthropic bef
![Client anthropic no api key](/talemate/img/0.26.0/client-anthropic-no-api-key.png)
Click the `SET API KEY` button. This will open the api settings window where you can add your Anthropic API key.
Click the `SET API KEY` button. This will open the application settings on the API Keys page where you can add your Anthropic API key.
For additional instructions on obtaining and setting your Anthropic API key, see [Anthropic API instructions](/talemate/user-guide/apis/anthropic/).
![Anthropic settings](/talemate/img/0.26.0/anthropic-settings.png)
![Anthropic settings](/talemate/img/0.39.0/api-keys-anthropic.png)
Click `Save` and after a moment the client should have a green dot next to it, indicating that it is ready to go.

View File

@@ -12,11 +12,11 @@ The client should appear in the clients list. If you haven't setup Cohere before
![Client cohere no api key](/talemate/img/0.26.0/client-cohere-no-api-key.png)
Click the `SET API KEY` button. This will open the api settings window where you can add your Cohere API key.
Click the `SET API KEY` button. This will open the application settings on the API Keys page where you can add your Cohere API key.
For additional instructions on obtaining and setting your Cohere API key, see [Cohere API instructions](/talemate/user-guide/apis/cohere/).
![Cohere settings](/talemate/img/0.26.0/cohere-settings.png)
![Cohere settings](/talemate/img/0.39.0/api-keys-cohere.png)
Click `Save` and after a moment the client should have a green dot next to it, indicating that it is ready to go.

View File

@@ -12,7 +12,7 @@ The client should appear in the clients list. If you haven't set up DeepSeek bef
![Client deepseek no api key](/talemate/img/0.35.0/client-deepseek-no-api-key.png)
Click the `SET API KEY` button. This will open the API settings window where you can add your DeepSeek API key.
Click the `SET API KEY` button. This will open the application settings on the API Keys page where you can add your DeepSeek API key.
You can obtain an API key from the [DeepSeek Platform](https://platform.deepseek.com/).

View File

@@ -12,11 +12,11 @@ The client should appear in the clients list. If you haven't setup Google before
![Client google no api key](/talemate/img/0.26.0/client-google-creds-missing.png)
Click the `SETUP GOOGLE API CREDENTIALS` button. This will open the api settings window where you can add your Google API credentials.
Click the `SETUP GOOGLE API CREDENTIALS` button. This will open the application settings on the API Keys page where you can add your Google API credentials.
For additional instructions on obtaining and setting your Google API credentials, see [Google API instructions](/talemate/user-guide/apis/google/).
![Google settings](/talemate/img/0.26.0/google-settings.png)
![Google settings](/talemate/img/0.39.0/api-keys-google.png)
Click `Save` and after a moment the client should have a green dot next to it, indicating that it is ready to go.

View File

@@ -12,11 +12,11 @@ The client should appear in the clients list. If you haven't setup Groq before,
![Client groq no api key](/talemate/img/0.26.0/client-groq-no-api-key.png)
Click the `SET API KEY` button. This will open the api settings window where you can add your Groq API key.
Click the `SET API KEY` button. This will open the application settings on the API Keys page where you can add your Groq API key.
For additional instructions on obtaining and setting your Groq API key, see [Groq API instructions](/talemate/user-guide/apis/groq/).
![Groq settings](/talemate/img/0.26.0/groq-settings.png)
![Groq settings](/talemate/img/0.39.0/api-keys-groq.png)
Click `Save` and after a moment the client should have a green dot next to it, indicating that it is ready to go.

View File

@@ -12,11 +12,11 @@ The client should appear in the clients list. If you haven't setup Mistral.ai be
![Client mistral no api key](/talemate/img/0.26.0/client-mistral-no-api-key.png)
Click the `SET API KEY` button. This will open the api settings window where you can add your Mistral.ai API key.
Click the `SET API KEY` button. This will open the application settings on the API Keys page where you can add your Mistral.ai API key.
For additional instructions on obtaining and setting your Mistral.ai API key, see [Mistral.ai API instructions](/talemate/user-guide/apis/mistral/).
![Mistral.ai settings](/talemate/img/0.26.0/mistral-settings.png)
![Mistral.ai settings](/talemate/img/0.39.0/api-keys-mistral.png)
Click `Save` and after a moment the client should have a green dot next to it, indicating that it is ready to go.

View File

@@ -12,11 +12,11 @@ The client should appear in the clients list. If you haven't setup OpenAI before
![Client openai no api key](/talemate/img/0.26.0/client-openai-no-api-key.png)
Click the `SET API KEY` button. This will open the api settings window where you can add your OpenAI API key.
Click the `SET API KEY` button. This will open the application settings on the API Keys page where you can add your OpenAI API key.
For additional instructions on obtaining and setting your OpenAI API key, see [OpenAI API instructions](/talemate/user-guide/apis/openai/).
![OpenAI settings](/talemate/img/0.26.0/openai-settings.png)
![OpenAI settings](/talemate/img/0.39.0/api-keys-openai.png)
Click `Save` and after a moment the client should have a green dot next to it, indicating that it is ready to go.

View File

@@ -12,11 +12,11 @@ The client should appear in the clients list. If you haven't set up OpenRouter b
![Client openrouter no api key](/talemate/img/0.31.0/client-openrouter-no-api-key.png)
Click the `SET API KEY` button. This will open the API settings window where you can add your OpenRouter API key.
Click the `SET API KEY` button. This will open the application settings on the API Keys page where you can add your OpenRouter API key.
For additional instructions on obtaining and setting your OpenRouter API key, see [OpenRouter API instructions](/talemate/user-guide/apis/openrouter/).
![OpenRouter settings](/talemate/img/0.31.0/openrouter-settings.png)
![OpenRouter settings](/talemate/img/0.39.0/api-keys-openrouter.png)
Click `Save` and after a moment the client should have a red dot next to it, saying that there is no model loaded.

View File

@@ -29,7 +29,7 @@ As a convenience, an OpenRouter API key configured in Talemate's application set
### Environment variable store
Instead of keeping API keys for `models.json` providers in plaintext environment files, store them in Talemate: `Settings → Application → Environment Variables`. Values are encrypted at rest in Talemate's configuration and passed as environment variables to every pi process the client spawns, so `models.json` can reference them:
Instead of keeping API keys for `models.json` providers in plaintext environment files, store them in Talemate: `Settings → Connections → Environment Variables`. Values are encrypted at rest in Talemate's configuration and passed as environment variables to every pi process the client spawns, so `models.json` can reference them:
```json
{

View File

@@ -1,6 +1,8 @@
!!! note
These instructions have not been updated in a while and RunPod has changed somewhat. I will update this as soon as I can. The general idea should still be the same.
The RunPod settings category described below no longer exists in the current settings interface.
RunPod allows you to quickly set up and run text-generation-webui instances on powerful GPUs, remotely. If you want to run the significantly larger models (like 70B parameters) with reasonable speeds, this is probably the best way to do it.
### Get a RunPod API key and add it to the talemate config
@@ -9,11 +11,7 @@ You can manage your RunPod api keys at [https://www.runpod.io/console/user/setti
Once you have your key you can open the settings in talemate.
![Open settings](/talemate/img/0.26.0/open-settings.png)
Then click the `APPLICATION` tab and then the `RUNPOD` category. Here you can add your RunPod API key.
![Runpod settings](/talemate/img/0.26.0/runpod-api-key-setting.png)
Older versions provided a `RUNPOD` category in the application settings to add your RunPod API key.
Setting the runpod api key requires a restart of the backend, so make sure to save your changes and restart the backend.

View File

@@ -23,7 +23,7 @@ The `Content Classification` is used to set the tone and expectation of the gene
You can type in a value or pick something from the list.
!!! note
What is available in the list is controlled in the :material-cog: application settings under the `Creator` tab.
What is available in the list is controlled in the :material-cog: application settings under the **Storytelling** group.
##### Example
@@ -69,7 +69,7 @@ Each of the four fields is a combobox. You can:
- **Pick a preset** from the dropdown.
- **Type a custom value** — the field accepts any free-form text and your custom entry is saved with the scene whether or not it matches a preset.
The dropdown list is shared by all four fields and is managed in **Settings → Creator → Perspective Presets**. See [Creator settings](../../app-settings/creator.md#perspective-presets) for how to add, remove, or rename presets.
The dropdown list is shared by all four fields and is managed in **Settings → Storytelling → Perspective Presets**. See [Storytelling settings](../../app-settings/creator.md#perspective-presets) for how to add, remove, or rename presets.
#### The `{player_name}` placeholder
@@ -107,7 +107,7 @@ This table lists the presets that ship with Talemate, what each one means in pla
| `Second person, present tense.` | The narrator addresses an unnamed "you" — classic interactive-fiction voice. Present tense. | *"You walk up to the porch. The door looks older than the rest of the house."* |
| `Second person, present tense. Talking to {player_name}.` | Same as above, but the narrator explicitly knows the player character's name and may use it. | *"You walk up to the porch, Annabelle. The door looks older than the rest of the house."* |
You are not limited to these — type anything into the field and it will be passed to the AI verbatim. The presets are just shortcuts for the most common configurations. See [Creator settings → Perspective Presets](../../app-settings/creator.md#perspective-presets) for how to add your own.
You are not limited to these — type anything into the field and it will be passed to the AI verbatim. The presets are just shortcuts for the most common configurations. See [Storytelling settings → Perspective Presets](../../app-settings/creator.md#perspective-presets) for how to add your own.
#### Common patterns

View File

@@ -293,7 +293,12 @@ class HelpChatMixin:
open_agent_modal = ((ux_snapshot or {}).get("agent_settings_modal") or {}).get(
"agent"
)
open_app_settings = bool((ux_snapshot or {}).get("app_settings_modal"))
# the settings view lives in the main viewport now — merely having it
# open is fine (incoming config pushes are handled cleanly); only
# unsaved edits there conflict with a config write
app_settings_dirty = bool(
((ux_snapshot or {}).get("app_settings_modal") or {}).get("dirty")
)
async def read_agent_settings(agent: str):
return settings.read_agent_settings(agent)
@@ -317,7 +322,7 @@ class HelpChatMixin:
async def update_app_config(path: str, value):
return await settings.update_app_config(
path, value, open_app_settings=open_app_settings
path, value, app_settings_dirty=app_settings_dirty
)
async def read_clients():

View File

@@ -316,59 +316,62 @@
of plaintext keys, and recovery from lost/corrupted keys and Docker deployment considerations.'
- path: user-guide/apis/anthropic.md
title: Anthropic API Setup
summary: Set the Anthropic API key for text generation in Settings under the APPLICATION tab, ANTHROPIC category; includes
where to create keys on console.anthropic.com.
summary: Set the Anthropic API key for text generation in Settings under Connections, API Keys (scroll or search for Anthropic);
includes where to create keys on console.anthropic.com.
- path: user-guide/apis/cohere.md
title: Cohere API Setup
summary: Set the Cohere API key for text generation in Settings under the APPLICATION tab, COHERE category; includes where
to create keys on dashboard.cohere.com.
summary: Set the Cohere API key for text generation in Settings under Connections, API Keys (scroll or search for Cohere);
includes where to create keys on dashboard.cohere.com.
- path: user-guide/apis/elevenlabs.md
title: ElevenLabs
summary: Set the ElevenLabs API key for text-to-speech in Settings under the APPLICATION tab, ELEVENLABS category, then
configure the Voice agent to use it.
summary: Set the ElevenLabs API key for text-to-speech in Settings under Connections, API Keys (scroll or search for ElevenLabs),
then configure the Voice agent to use it.
- path: user-guide/apis/google.md
title: Google Cloud Setup
summary: 'Set up Google Cloud for Gemini via Vertex AI: create a project and service account, download the credentials JSON
into the talemate directory, and configure the credentials path and Google Cloud location in the APPLICATION settings.'
into the talemate directory, and configure the credentials path and Google Cloud location in Settings under Connections, API Keys (Google entry).'
- path: user-guide/apis/groq.md
title: Groq API Setup
summary: Set the Groq API key for text generation in Settings under the APPLICATION tab, GROQ category; includes where to
create keys on console.groq.com.
summary: Set the Groq API key for text generation in Settings under Connections, API Keys (scroll or search for groq); includes
where to create keys on console.groq.com.
- path: user-guide/apis/mistral.md
title: Mistral.ai API Setup
summary: Set the Mistral.ai API key for text generation in Settings under the APPLICATION tab, MISTRAL.AI category; includes
where to create keys on console.mistral.ai.
summary: Set the Mistral.ai API key for text generation in Settings under Connections, API Keys (scroll or search for mistral.ai);
includes where to create keys on console.mistral.ai.
- path: user-guide/apis/openai.md
title: OpenAI API Setup
summary: Set the OpenAI API key for text, image, and audio generation in Settings under the APPLICATION tab, OPENAI category;
includes where to create keys on platform.openai.com.
summary: Set the OpenAI API key for text, image, and audio generation in Settings under Connections, API Keys (scroll or search
for OpenAI); includes where to create keys on platform.openai.com.
- path: user-guide/apis/openrouter.md
title: OpenRouter API Setup
summary: Set the OpenRouter API key (from openrouter.ai/keys) in Settings under the APPLICATION tab, OPENROUTER API category,
to use any model available through OpenRouter.
summary: Set the OpenRouter API key (from openrouter.ai/keys) in Settings under Connections, API Keys (scroll or search for
OpenRouter), to use any model available through OpenRouter.
- path: user-guide/app-settings/appearance.md
title: Appearance
summary: 'Appearance settings for the scene feed: per-message-type styling (narrator, actor, director, time, context investigations)
with italic/bold/color/visibility, text markup styling for quotes, parentheses, brackets, emphasis, and entity highlights,
live preview, and Visuals-tab controls for avatar/card/scene-illustration cadence (Always/On Change/Never), size, and
live preview, and Visuals-page controls for avatar/card/scene-illustration cadence (Always/On Change/Never), size, and
auto-attach.'
- path: user-guide/app-settings/application.md
title: Application
summary: The Application settings tab where API keys for external services (OpenAI, Anthropic, etc.) are stored, including
the HuggingFace token (added 0.38.0) needed to download gated model weights such as the Pocket TTS voice-cloning model.
title: Connections
summary: The Connections group of the application settings, holding the single API Keys page where keys for external services
(OpenAI, Anthropic, etc.) are stored - including the HuggingFace token (added 0.38.0) needed to download gated model weights
such as the Pocket TTS voice-cloning model - and the Environment Variables store (added 0.39.0) for encrypted named values
passed to spawned processes like the Pi Bridge client.
- path: user-guide/app-settings/creator.md
title: Creator
summary: 'Creator settings tab holding global picker lists: Content Classification strings for scene setup and Perspective
title: Storytelling
summary: 'Storytelling settings group (formerly Creator) holding global picker lists: Content Classification strings for scene setup and Perspective
Presets (added 0.38.0) for the scene outline''s perspective/tense comboboxes, including the {player_name} placeholder
substitution and how to add or remove presets.'
- path: user-guide/app-settings/game.md
title: Game
summary: 'Game settings: auto save on game-loop completion, auto progress to the next character, max backscroll message
count, the agent activity bar toggle, and managing a default player character for imported scenes (e.g. ST character cards)
that lack one.'
summary: 'Overview of the redesigned settings view (0.39.0: main-view Settings tab with grouped sidebar navigation, settings
search, and explicit Save/Discard with unsaved-change tracking) plus the Game group: auto save on game-loop completion,
auto progress to the next character, max backscroll message count, the agent activity bar toggle, releasing GPU cache on
scene load, and managing a default player character for imported scenes (e.g. ST character cards) that lack one.'
- path: user-guide/app-settings/presets.md
title: Presets
summary: Presets settings tab covering inference parameter presets (Analytical, Conversation, and other per-action presets
summary: Presets settings group covering inference parameter presets (Analytical, Conversation, and other per-action presets
used by agents), embedding model management for the memory agent via chromadb, and the global system prompt override panel.
- path: user-guide/app-settings/system-prompts.md
title: System Prompt Overrides
@@ -601,8 +604,8 @@
size, and auto-attach.
- path: user-guide/integrations/runpod.md
title: RunPod
summary: 'Run text-generation-webui on rented RunPod GPUs: add the RunPod API key in APPLICATION settings (backend restart
required), deploy a pod with the ''RunPod TheBloke LLMs'' template named to include ''thebloke llms'' or ''textgen'' so
summary: 'Run text-generation-webui on rented RunPod GPUs: add the RunPod API key (the settings category for it no longer exists in current versions; backend restart
was required), deploy a pod with the ''RunPod TheBloke LLMs'' template named to include ''thebloke llms'' or ''textgen'' so
Talemate auto-discovers it, and load models via the pod''s web UI. Notes the instructions are somewhat outdated.'
- path: user-guide/interacting.md
title: Interacting with the scene

View File

@@ -575,22 +575,22 @@ def _coerce_config_leaf(current: Any, value: Any, path: str) -> Any:
async def update_app_config(
path: str, value: Any, open_app_settings: bool = False
path: str, value: Any, app_settings_dirty: bool = False
) -> dict | str:
"""Update one application config setting by dotted path.
The whole section is re-validated through its pydantic model, so invalid
values are rejected with the validation error. ``open_app_settings``
flags that the application settings dialog is open in the frontend -
writes are refused then, since the config push they trigger re-hydrates
the dialog and discards the user's unsaved edits.
values are rejected with the validation error. ``app_settings_dirty``
flags that the settings view in the frontend has unsaved edits - writes
are refused then, since the user's pending changes and the agent's write
would conflict (whichever saves last would overwrite the other). A merely
open settings view is fine: the frontend handles the config push cleanly.
"""
if open_app_settings:
if app_settings_dirty:
return (
"The application settings dialog is currently open in the interface - "
"applying this change now would discard any unsaved edits the user has "
"in it. Ask the user to close the dialog first, then apply the change "
"again."
"The application settings view has unsaved edits - applying this "
"change now would conflict with them. Ask the user to save or "
"discard their settings changes first, then apply the change again."
)
keys = [key for key in (path or "").strip().split(".") if key]

View File

@@ -1,793 +0,0 @@
<template>
<v-dialog v-model="dialog" scrollable max-width="960">
<v-card v-if="app_config !== null">
<v-card-title><v-icon class="mr-1">mdi-cog</v-icon>Settings</v-card-title>
<v-tabs color="primary" v-model="tab">
<v-tab value="game">
<v-icon start>mdi-gamepad-square</v-icon>
Game
</v-tab>
<v-tab value="appearance">
<v-icon start>mdi-palette-outline</v-icon>
Appearance
</v-tab>
<v-tab value="application">
<v-icon start>mdi-application</v-icon>
Application
</v-tab>
<v-tab value="presets">
<v-icon start>mdi-tune</v-icon>
Presets
</v-tab>
<v-tab value="creator">
<v-icon start>mdi-palette-outline</v-icon>
Creator
</v-tab>
</v-tabs>
<v-divider></v-divider>
<v-window v-model="tab" class="app-config-window">
<!-- GAME -->
<v-window-item value="game">
<div class="app-config-window-content">
<v-card flat>
<v-card-text>
<v-row>
<v-col cols="4">
<v-tabs v-model="gamePageSelected" color="primary" direction="vertical">
<v-tab v-for="(item, index) in navigation.game" :key="index" :value="item.value">
<v-icon class="mr-1">{{ item.icon }}</v-icon>
{{ item.title }}
</v-tab>
</v-tabs>
</v-col>
<v-col cols="8">
<div v-if="gamePageSelected === 'general'">
<v-alert color="white" variant="text" icon="mdi-cog" density="compact">
<v-alert-title>General</v-alert-title>
<div class="text-grey">
General game settings.
</div>
</v-alert>
<v-divider class="mb-2"></v-divider>
<v-row>
<v-col cols="12">
<v-checkbox color="primary" v-model="app_config.game.general.auto_save" label="Auto save" messages="Automatically save after each game-loop"></v-checkbox>
</v-col>
</v-row>
<v-row>
<v-col cols="12">
<v-checkbox color="primary" v-model="app_config.game.general.auto_progress" label="Auto progress" messages="AI automatically progresses after player turn."></v-checkbox>
</v-col>
</v-row>
<v-row>
<v-col cols="6">
<v-number-input v-model="app_config.game.general.max_backscroll" label="Max backscroll" messages="Maximum number of messages to keep in the scene backscroll"></v-number-input>
</v-col>
</v-row>
<v-row>
<v-col cols="12">
<v-checkbox color="primary" v-model="app_config.game.general.show_agent_activity_bar" label="Show agent activity bar" messages="Display active agent actions in a horizontal bar above scene controls"></v-checkbox>
</v-col>
</v-row>
<v-row>
<v-col cols="12">
<v-checkbox color="primary" v-model="app_config.game.general.release_gpu_cache_on_scene_load" label="Release GPU cache on scene load" messages="Hand idle GPU memory reserved by local CUDA embeddings/TTS back to the driver when switching scenes, so it doesn't pile up and eventually leave too little VRAM to load another scene. Disable only if you have VRAM to spare and would rather keep it reserved."></v-checkbox>
</v-col>
</v-row>
</div>
<div v-else-if="gamePageSelected === 'character'">
<v-alert color="white" variant="text" icon="mdi-human-edit" density="compact">
<v-alert-title>Default player character</v-alert-title>
<div class="text-grey">
This will be default player character that will be added to a scene if the scene does not come with a defined player character. Mostly relevant when you load character-cards that aren't in the talemate scene format.
</div>
</v-alert>
<v-divider class="mb-2"></v-divider>
<v-row>
<v-col cols="6">
<v-text-field v-model="app_config.game.default_player_character.name"
label="Name"></v-text-field>
</v-col>
<v-col cols="6">
<v-text-field v-model="app_config.game.default_player_character.gender"
label="Gender"></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12">
<v-textarea v-model="app_config.game.default_player_character.description"
auto-grow label="Description"></v-textarea>
</v-col>
</v-row>
<v-row>
<v-col cols="12">
<v-checkbox color="primary" v-model="app_config.game.general.add_default_character" label="Add default character to blank talemate scenes" messages="When creating a new scene, add the default player character to the scene."></v-checkbox>
</v-col>
</v-row>
</div>
</v-col>
</v-row>
</v-card-text>
</v-card>
</div>
</v-window-item>
<!-- APPEARANCE -->
<v-window-item value="appearance">
<div class="app-config-window-content">
<AppConfigAppearance
ref="appearance"
:immutableConfig="app_config"
:sceneActive="sceneActive"
@appearance-preview="onAppearancePreview"
></AppConfigAppearance>
</div>
</v-window-item>
<!-- APPLICATION -->
<v-window-item value="application">
<div class="app-config-window-content">
<v-card flat>
<v-card-text>
<v-row>
<v-col cols="4">
<v-list>
<v-list-subheader>Application</v-list-subheader>
<v-tabs v-model="applicationPageSelected" color="primary" direction="vertical" density="compact">
<v-tab v-for="(item, index) in navigation.application" :key="index" :value="item.value">
<v-icon class="mr-1">{{ item.icon }}</v-icon>
{{ item.title }}
</v-tab>
</v-tabs>
</v-list>
</v-col>
<v-col cols="8">
<!-- OPENAI API -->
<div v-if="applicationPageSelected === 'openai_api'">
<v-alert color="white" variant="text" icon="mdi-api" density="compact">
<v-alert-title>OpenAI</v-alert-title>
<div class="text-grey">
Configure your OpenAI API key here. You can get one from <a href="https://platform.openai.com/" target="_blank">https://platform.openai.com/</a>
</div>
</v-alert>
<v-divider class="mb-2"></v-divider>
<v-row>
<v-col cols="12">
<v-text-field type="password" v-model="app_config.openai.api_key"
label="OpenAI API Key"></v-text-field>
</v-col>
</v-row>
</div>
<!-- MISTRAL.AI API -->
<div v-if="applicationPageSelected === 'mistralai_api'">
<v-alert color="white" variant="text" icon="mdi-api" density="compact">
<v-alert-title>mistral.ai</v-alert-title>
<div class="text-grey">
Configure your mistral.ai API key here. You can get one from <a href="https://console.mistral.ai/api-keys/" target="_blank">https://console.mistral.ai/api-keys/</a>
</div>
</v-alert>
<v-divider class="mb-2"></v-divider>
<v-row>
<v-col cols="12">
<v-text-field type="password" v-model="app_config.mistralai.api_key"
label="mistral.ai API Key"></v-text-field>
</v-col>
</v-row>
</div>
<!-- ANTHROPIC API -->
<div v-if="applicationPageSelected === 'anthropic_api'">
<v-alert color="white" variant="text" icon="mdi-api" density="compact">
<v-alert-title>Anthropic</v-alert-title>
<div class="text-grey">
Configure your Anthropic API key here. You can get one from <a href="https://console.anthropic.com/settings/keys" target="_blank">https://console.anthropic.com/settings/keys</a>
</div>
</v-alert>
<v-divider class="mb-2"></v-divider>
<v-row>
<v-col cols="12">
<v-text-field type="password" v-model="app_config.anthropic.api_key"
label="Anthropic API Key"></v-text-field>
</v-col>
</v-row>
</div>
<!-- COHERE API -->
<div v-if="applicationPageSelected === 'cohere_api'">
<v-alert color="white" variant="text" icon="mdi-api" density="compact">
<v-alert-title>Cohere</v-alert-title>
<div class="text-grey">
Configure your Cohere API key here. You can get one from <a href="https://dashboard.cohere.com/api-keys" target="_blank">https://dashboard.cohere.com/api-keys</a>
</div>
</v-alert>
<v-divider class="mb-2"></v-divider>
<v-row>
<v-col cols="12">
<v-text-field type="password" v-model="app_config.cohere.api_key"
label="Cohere API Key"></v-text-field>
</v-col>
</v-row>
</div>
<!-- DEEPSEEK API -->
<div v-if="applicationPageSelected === 'deepseek_api'">
<v-alert color="white" variant="text" icon="mdi-api" density="compact">
<v-alert-title>DeepSeek</v-alert-title>
<div class="text-grey">
Configure your DeepSeek API key here. You can get one from <a href="https://platform.deepseek.com/" target="_blank">https://platform.deepseek.com/</a>
</div>
</v-alert>
<v-divider class="mb-2"></v-divider>
<v-row>
<v-col cols="12">
<v-text-field type="password" v-model="app_config.deepseek.api_key"
label="DeepSeek API Key"></v-text-field>
</v-col>
</v-row>
</div>
<!-- OPENROUTER API -->
<div v-if="applicationPageSelected === 'openrouter_api'">
<v-alert color="white" variant="text" icon="mdi-api" density="compact">
<v-alert-title>OpenRouter</v-alert-title>
<div class="text-grey">
Configure your OpenRouter API key here. You can get one from <a href="https://openrouter.ai/settings/keys" target="_blank">https://openrouter.ai/settings/keys</a>
</div>
</v-alert>
<v-divider class="mb-2"></v-divider>
<v-row>
<v-col cols="12">
<v-text-field type="password" v-model="app_config.openrouter.api_key"
label="OpenRouter API Key"></v-text-field>
</v-col>
</v-row>
</div>
<!-- GROQ API -->
<div v-if="applicationPageSelected === 'groq_api'">
<v-alert color="white" variant="text" icon="mdi-api" density="compact">
<v-alert-title>groq</v-alert-title>
<div class="text-grey">
Configure your GROQ API key here. You can get one from <a href="https://console.groq.com/keys" target="_blank">https://console.groq.com/keys</a>
</div>
</v-alert>
<v-divider class="mb-2"></v-divider>
<v-row>
<v-col cols="12">
<v-text-field type="password" v-model="app_config.groq.api_key"
label="GROQ API Key"></v-text-field>
</v-col>
</v-row>
</div>
<!-- GOOGLE API
THis adds fields for
gcloud_credentials_path
gcloud_project_id
gcloud_location
-->
<div v-if="applicationPageSelected === 'google_api'">
<v-alert color="white" variant="text" icon="mdi-google-cloud" density="compact">
<v-alert-title>Google</v-alert-title>
<div class="text-grey">
<p class="mb-2"><strong>Option&nbsp;1 API&nbsp;Key&nbsp;(recommended)</strong></p>
<p class="mb-1">Create a Google API key at <a href="https://aistudio.google.com/apikey" target="_blank">aistudio.google.com/apikey</a> and paste it in the field below. This is the quickest way to start using Gemini models.</p>
<v-divider class="my-4"></v-divider>
<p class="mb-2"><strong>Option&nbsp;2 Vertex&nbsp;AI service&nbsp;account&nbsp;(advanced)</strong></p>
<p class="mb-0">If you prefer using a full Google Cloud project, follow the setup guide <a href="https://cloud.google.com/vertex-ai/docs/start/client-libraries" target="_blank">here</a> to generate a service-account JSON credential file, then complete the legacy fields below.</p>
<p class="text-caption mt-1 text-muted">
If both are setup, the API key will be used.
</p>
</div>
</v-alert>
<v-divider class="mb-2"></v-divider>
<!-- API KEY -->
<v-row class="mb-4">
<v-col cols="12">
<v-text-field type="password"
v-model="app_config.google.api_key"
label="Google API Key"
messages="Paste your Google API key here. This is the easiest way to authenticate.">
</v-text-field>
</v-col>
</v-row>
<!-- Vertex AI (legacy) fields -->
<v-row>
<v-col cols="12">
<v-text-field v-model="app_config.google.gcloud_credentials_path"
label="Google Cloud Credentials Path" messages="Path to the service-account JSON credentials file on the machine running the Talemate backend."></v-text-field>
</v-col>
<v-col cols="6">
<v-combobox v-model="app_config.google.gcloud_location"
label="Google Cloud Location" :items="googleCloudLocations" messages="Pick something close to you" :return-object="false"></v-combobox>
</v-col>
</v-row>
</div>
<!-- ELEVENLABS API -->
<div v-if="applicationPageSelected === 'elevenlabs_api'">
<v-alert color="white" variant="text" icon="mdi-api" density="compact">
<v-alert-title>ElevenLabs</v-alert-title>
<div class="text-grey">
<p class="mb-1">Generate realistic speech with the most advanced AI voice model ever.</p>
Configure your ElevenLabs API key here. You can get one from <a href="https://elevenlabs.io/?from=partnerewing2048" target="_blank">https://elevenlabs.io</a> <span class="text-caption">(affiliate link)</span>
</div>
</v-alert>
<v-divider class="mb-2"></v-divider>
<v-row>
<v-col cols="12">
<v-text-field type="password" v-model="app_config.elevenlabs.api_key"
label="ElevenLabs API Key"></v-text-field>
</v-col>
</v-row>
</div>
<!-- ENVIRONMENT VARIABLES -->
<div v-if="applicationPageSelected === 'env_variables'">
<v-alert color="white" variant="text" icon="mdi-variable" density="compact">
<v-alert-title>Environment Variables</v-alert-title>
<div class="text-grey">
<p class="mb-1">Named values passed as environment variables to processes Talemate spawns — currently the <span class="text-primary">Pi Bridge</span> client, where pi's <code>models.json</code> can reference them as <code>$NAME</code>.</p>
Values are encrypted at rest in Talemate's configuration file.
</div>
</v-alert>
<v-divider class="mb-2"></v-divider>
<v-row v-for="name in envVariableNames" :key="name">
<v-col cols="5">
<v-text-field :model-value="name" label="Name" readonly density="compact"></v-text-field>
</v-col>
<v-col cols="6">
<v-text-field type="password" v-model="app_config.env[name]" label="Value" density="compact"></v-text-field>
</v-col>
<v-col cols="1" class="d-flex align-center">
<v-btn icon="mdi-close-circle-outline" variant="text" color="delete" size="small" @click="envVariableRemove(name)"></v-btn>
</v-col>
</v-row>
<v-row>
<v-col cols="5">
<v-text-field v-model="env_variable_name_input" label="Name" density="compact"
:rules="[validateEnvVariableName]" placeholder="MY_API_KEY"
@keyup.enter="envVariableAdd"></v-text-field>
</v-col>
<v-col cols="6">
<v-text-field type="password" v-model="env_variable_value_input" label="Value" density="compact"
@keyup.enter="envVariableAdd"></v-text-field>
</v-col>
<v-col cols="1" class="d-flex align-center">
<v-btn icon="mdi-plus-circle-outline" variant="text" color="primary" size="small"
:disabled="!envVariableAddValid" @click="envVariableAdd"></v-btn>
</v-col>
</v-row>
</div>
<!-- HUGGINGFACE API -->
<div v-if="applicationPageSelected === 'huggingface_api'">
<v-alert color="white" variant="text" icon="mdi-api" density="compact">
<v-alert-title>HuggingFace</v-alert-title>
<div class="text-grey">
<p class="mb-1">Access token used to download gated model weights (e.g. for Pocket TTS voice cloning).</p>
Create a token at <a href="https://huggingface.co/settings/tokens" target="_blank">https://huggingface.co/settings/tokens</a>. A read token is sufficient.
</div>
</v-alert>
<v-divider class="mb-2"></v-divider>
<v-row>
<v-col cols="12">
<v-text-field type="password" v-model="app_config.huggingface.api_key"
label="HuggingFace Token"></v-text-field>
</v-col>
</v-row>
</div>
</v-col>
</v-row>
</v-card-text>
</v-card>
</div>
</v-window-item>
<!-- PRESETS -->
<v-window-item value="presets">
<div class="app-config-window-content">
<AppConfigPresets
ref="presets"
:immutable-config="app_config"
:agentStatus="agentStatus"
:sceneActive="sceneActive"
:clientStatus="clientStatus"
></AppConfigPresets>
</div>
</v-window-item>
<!-- CREATOR -->
<v-window-item value="creator">
<div class="app-config-window-content">
<v-card flat>
<v-card-text>
<v-row>
<v-col cols="4">
<v-tabs v-model="creatorPageSelected" color="primary" direction="vertical">
<v-tab v-for="(item, index) in navigation.creator" :key="index" :value="item.value">
<v-icon class="mr-1">{{ item.icon }}</v-icon>
{{ item.title }}
</v-tab>
</v-tabs>
</v-col>
<v-col cols="8">
<div v-if="creatorPageSelected === 'content_context'">
<v-alert color="white" variant="text" icon="mdi-cube-scan" density="compact">
<v-alert-title>Content Classification</v-alert-title>
<div class="text-grey">
Available content classification choices when generating characters or scenarios. This can strongly influence the content that is generated.
</div>
</v-alert>
<v-divider class="mb-2"></v-divider>
<v-row>
<v-col cols="12">
<v-list density="compact">
<v-list-item v-for="(value, index) in app_config.creator.content_context" :key="index">
<v-list-item-title><v-icon color="red-darken-1" class="mr-2" @click="contentContextRemove(index)">mdi-close-box-outline</v-icon>{{ value }}</v-list-item-title>
</v-list-item>
</v-list>
<v-divider></v-divider>
<v-text-field v-model="content_context_input" label="Add content classification (Press enter to add)"
@keyup.enter="app_config.creator.content_context.push(content_context_input); content_context_input = ''"></v-text-field>
</v-col>
</v-row>
</div>
<div v-if="creatorPageSelected === 'perspective_presets'">
<v-alert color="white" variant="text" icon="mdi-eye-outline" density="compact">
<v-alert-title>Perspective Presets</v-alert-title>
<div class="text-grey">
Reusable narrative perspective / tense strings offered in the scene outline. Use <code>{player_name}</code> as a placeholder for the player character — it will be substituted at prompt render time.
</div>
</v-alert>
<v-divider class="mb-2"></v-divider>
<v-row>
<v-col cols="12">
<v-list density="compact">
<v-list-item v-for="(value, index) in app_config.creator.perspective_presets" :key="index">
<v-list-item-title><v-icon color="red-darken-1" class="mr-2" @click="perspectivePresetRemove(index)">mdi-close-box-outline</v-icon>{{ value }}</v-list-item-title>
</v-list-item>
</v-list>
<v-divider></v-divider>
<v-text-field v-model="perspective_preset_input" label="Add perspective preset (Press enter to add)"
@keyup.enter="perspectivePresetAdd()"></v-text-field>
</v-col>
</v-row>
</div>
</v-col>
</v-row>
</v-card-text>
</v-card>
</div>
</v-window-item>
</v-window>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="primary" text @click="saveConfig" prepend-icon="mdi-check-circle-outline">Save</v-btn>
</v-card-actions>
</v-card>
<v-card v-else>
<v-card-title>
<span class="headline">Configuration</span>
</v-card-title>
<v-card-text>
<v-progress-circular indeterminate="disable-shrink" color="primary" size="20"></v-progress-circular>
</v-card-text>
</v-card>
</v-dialog>
</template>
<script>
import AppConfigPresets from './AppConfigPresets.vue';
import AppConfigAppearance from './AppConfigAppearance.vue';
export default {
name: 'AppConfig',
components: {
AppConfigPresets,
AppConfigAppearance,
},
props: {
agentStatus: Object,
sceneActive: Boolean,
clientStatus: Object,
},
emits: [
'appearance-preview',
'appearance-preview-clear',
],
data() {
return {
tab: 'game',
dialog: false,
app_config: null,
content_context_input: '',
perspective_preset_input: '',
env_variable_name_input: '',
env_variable_value_input: '',
navigation: {
game: [
{title: 'General', icon: 'mdi-cog', value: 'general'},
{title: 'Default Character', icon: 'mdi-human-edit', value: 'character'},
],
appearance: [
{title: 'Scene', icon: 'mdi-script-text', value: 'scene'},
],
application: [
{title: 'Anthropic', icon: 'mdi-api', value: 'anthropic_api'},
{title: 'Cohere', icon: 'mdi-api', value: 'cohere_api'},
{title: 'DeepSeek', icon: 'mdi-api', value: 'deepseek_api'},
{title: 'ElevenLabs', icon: 'mdi-api', value: 'elevenlabs_api'},
{title: 'Environment Variables', icon: 'mdi-variable', value: 'env_variables'},
{title: 'Google', icon: 'mdi-api', value: 'google_api'},
{title: 'groq', icon: 'mdi-api', value: 'groq_api'},
{title: 'HuggingFace', icon: 'mdi-api', value: 'huggingface_api'},
{title: 'mistral.ai', icon: 'mdi-api', value: 'mistralai_api'},
{title: 'OpenAI', icon: 'mdi-api', value: 'openai_api'},
{title: 'OpenRouter', icon: 'mdi-api', value: 'openrouter_api'},
],
creator: [
{title: 'Content Classification', icon: 'mdi-cube-scan', value: 'content_context'},
{title: 'Perspective Presets', icon: 'mdi-eye-outline', value: 'perspective_presets'},
]
},
gamePageSelected: 'general',
applicationPageSelected: 'openai_api',
creatorPageSelected: 'content_context',
googleCloudLocations: [
{"value": 'us-central1', "title": 'US Central - Iowa'},
{"value": 'us-west4', "title": 'US West 4 - Las Vegas'},
{"value": 'us-east1', "title": 'US East 1 - South Carolina'},
{"value": 'us-east4', "title": 'US East 4 - Northern Virginia'},
{"value": 'us-west1', "title": 'US West 1 - Oregon'},
{"value": 'northamerica-northeast1', "title": 'North America Northeast 1 - Montreal'},
{"value": 'southamerica-east1', "title": 'South America East 1 - Sao Paulo'},
{"value": 'europe-west1', "title": 'Europe West 1 - Belgium'},
{"value": 'europe-north1', "title": 'Europe North 1 - Finland'},
{"value": 'europe-west3', "title": 'Europe West 3 - Frankfurt'},
{"value": 'europe-west2', "title": 'Europe West 2 - London'},
{"value": 'europe-southwest1', "title": 'Europe Southwest 1 - Zurich'},
{"value": 'europe-west8', "title": 'Europe West 8 - Netherlands'},
{"value": 'europe-west4', "title": 'Europe West 4 - London'},
{"value": 'europe-west9', "title": 'Europe West 9 - Stockholm'},
{"value": 'europe-central2', "title": 'Europe Central 2 - Warsaw'},
{"value": 'europe-west6', "title": 'Europe West 6 - Zurich'},
{"value": 'asia-east1', "title": 'Asia East 1 - Taiwan'},
{"value": 'asia-east2', "title": 'Asia East 2 - Hong Kong'},
{"value": 'asia-south1', "title": 'Asia South 1 - Mumbai'},
{"value": 'asia-northeast1', "title": 'Asia Northeast 1 - Tokyo'},
{"value": 'asia-northeast3', "title": 'Asia Northeast 3 - Seoul'},
{"value": 'asia-southeast1', "title": 'Asia Southeast 1 - Singapore'},
{"value": 'asia-southeast2', "title": 'Asia Southeast 2 - Jakarta'},
{"value": 'australia-southeast1', "title": 'Australia Southeast 1 - Sydney'},
{"value": 'australia-southeast2', "title": 'Australia Southeast 2 - Melbourne'},
{"value": 'me-west1', "title": 'Middle East West 1 - Dammam'},
{"value": 'asia-northeast2', "title": 'Asia Northeast 2 - Osaka'},
{"value": 'asia-northeast3', "title": 'Asia Northeast 3 - Seoul'},
{"value": 'asia-south1', "title": 'Asia South 1 - Mumbai'},
{"value": 'asia-southeast1', "title": 'Asia Southeast 1 - Singapore'},
{"value": 'asia-southeast2', "title": 'Asia Southeast 2 - Jakarta'}
].sort((a, b) => a.title.localeCompare(b.title))
}
},
inject: ['getWebsocket', 'registerMessageHandler', 'setWaitingForInput', 'requestSceneAssets', 'requestAppConfig'],
computed: {
envVariableNames() {
return Object.keys(this.app_config?.env || {}).sort();
},
envVariableAddValid() {
return this.validateEnvVariableName(this.env_variable_name_input) === true
&& this.env_variable_name_input.trim() !== ''
&& this.env_variable_value_input !== '';
},
},
watch: {
dialog(newVal, oldVal) {
// Clear preview whenever the dialog actually closes (including overlay/ESC close)
if (oldVal === true && newVal === false) {
this.$emit('appearance-preview-clear');
}
},
},
methods: {
uxSnapshot() {
// what the open app-settings dialog shows, for the help agent's UX snapshot
if(!this.dialog) return null;
return { tab: this.tab };
},
show(tab, page, item) {
this.requestAppConfig();
this.dialog = true;
if(tab) {
this.tab = tab;
if(page) {
if(this[tab + 'PageSelected'] !== undefined) {
this[tab + 'PageSelected'] = page;
} else {
this.$nextTick(() => {
console.log("SETTING SELECTION", {tab, page, item});
if(this.$refs[tab] && this.$refs[tab].setSelection) {
this.$refs[tab].setSelection(page);
this.$nextTick(() => {
if(item && this.$refs[tab].$refs[page] && this.$refs[tab].$refs[page].setSelection) {
this.$refs[tab].$refs[page].setSelection(item);
}
});
}
})
}
}
}
},
exit() {
this.dialog = false;
},
onAppearancePreview(previewConfig) {
// Re-emit appearance preview upward
this.$emit('appearance-preview', previewConfig);
},
contentContextRemove(index) {
this.app_config.creator.content_context.splice(index, 1);
},
perspectivePresetRemove(index) {
this.app_config.creator.perspective_presets.splice(index, 1);
},
validateEnvVariableName(value) {
const name = (value || '').trim();
if (!name) return true;
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
return 'Letters, digits and underscores only; must not start with a digit';
}
if (this.app_config?.env && name in this.app_config.env) {
return 'Already exists — edit its row above';
}
return true;
},
envVariableAdd() {
if (!this.envVariableAddValid) return;
const name = this.env_variable_name_input.trim();
if (!this.app_config.env) {
this.app_config.env = {};
}
this.app_config.env[name] = this.env_variable_value_input;
this.env_variable_name_input = '';
this.env_variable_value_input = '';
},
envVariableRemove(name) {
delete this.app_config.env[name];
},
perspectivePresetAdd() {
const value = (this.perspective_preset_input || '').trim();
if (!value) return;
if (!Array.isArray(this.app_config.creator.perspective_presets)) {
this.app_config.creator.perspective_presets = [];
}
this.app_config.creator.perspective_presets.push(value);
this.perspective_preset_input = '';
},
handleMessage(message) {
if (message.type == "app_config") {
this.app_config = message.data;
return;
}
if (message.type == 'config') {
if (message.action == 'save_complete') {
// exit() will emit appearance-preview-clear, no need to duplicate
this.exit();
}
}
},
sendRequest(data) {
data.type = 'config';
this.getWebsocket().send(JSON.stringify(data));
},
saveConfig() {
// check if presets component is present
if(this.$refs.presets) {
// update app_config.presets from $refs.presets.config
let inferenceConfig = this.$refs.presets.inference_config();
let inferenceGroupsConfig = this.$refs.presets.inference_groups_config();
let embeddingsConfig = this.$refs.presets.embeddings_config();
let systemPromptsConfig = this.$refs.presets.system_prompts_config();
if(inferenceConfig) {
this.app_config.presets.inference = inferenceConfig;
}
if(inferenceGroupsConfig) {
this.app_config.presets.inference_groups = inferenceGroupsConfig;
}
if(embeddingsConfig) {
this.app_config.presets.embeddings = embeddingsConfig;
}
if(systemPromptsConfig) {
this.app_config.system_prompts = systemPromptsConfig;
}
}
// check if appearance component is present
if(this.$refs.appearance) {
// update app_config.appearance from $refs.appearance.config
this.app_config.appearance = this.$refs.appearance.get_config();
}
console.log("SAVING", this.app_config);
this.sendRequest({
action: 'save',
config: this.app_config,
})
},
},
created() {
this.registerMessageHandler(this.handleMessage);
},
}
</script>
<style scoped>
.app-config-window-content {
overflow-y: auto;
overflow-x: hidden;
min-height: 0;
max-height: calc(100vh - 250px);
}
/* Style scrollbar when it appears */
.app-config-window-content::-webkit-scrollbar {
width: 8px;
}
.app-config-window-content::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.1);
}
.app-config-window-content::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.3);
border-radius: 4px;
}
.app-config-window-content::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.5);
}
</style>

View File

@@ -1,87 +0,0 @@
<template>
<v-tabs color="secondary" v-model="tab">
<v-tab v-for="t in tabs" :key="t.value" :value="t.value">
<v-icon start>{{ t.icon }}</v-icon>
{{ t.title }}
</v-tab>
</v-tabs>
<v-window v-model="tab">
<v-window-item value="scene">
<AppConfigAppearanceScene ref="scene" :immutableConfig="immutableConfig" :sceneActive="sceneActive" @changed="onChildChanged"></AppConfigAppearanceScene>
</v-window-item>
<v-window-item value="assets">
<AppConfigAppearanceAssets ref="assets" :immutableConfig="immutableConfig" :sceneActive="sceneActive" @changed="onChildChanged"></AppConfigAppearanceAssets>
</v-window-item>
</v-window>
</template>
<script>
import AppConfigAppearanceScene from './AppConfigAppearanceScene.vue';
import AppConfigAppearanceAssets from './AppConfigAppearanceAssets.vue';
export default {
name: 'AppConfigAppearance',
components: {
AppConfigAppearanceScene,
AppConfigAppearanceAssets,
},
props: {
immutableConfig: Object,
sceneActive: Boolean,
},
emits: [
'appearance-preview',
],
data() {
return {
tab: 'scene',
tabs: [
{ title: 'Messages', icon: 'mdi-script-text', value: 'scene' },
{ title: 'Visuals', icon: 'mdi-image-outline', value: 'assets' },
]
}
},
methods: {
get_config() {
let config = {
scene: this.immutableConfig.appearance.scene,
};
if(this.$refs.scene) {
config.scene = this.$refs.scene.config;
}
// Merge message_assets config from Assets component
if(this.$refs.assets && this.$refs.assets.get_config) {
const assetsConfig = this.$refs.assets.get_config();
if(assetsConfig) {
if(!config.scene) {
config.scene = {};
}
config.scene.message_assets = assetsConfig;
}
}
// Include auto_attach_assets from Assets component
if(this.$refs.assets && this.$refs.assets.get_auto_attach_assets) {
if(!config.scene) {
config.scene = {};
}
config.scene.auto_attach_assets = this.$refs.assets.get_auto_attach_assets();
}
// Include shared backdrop legibility settings from Assets component
if(this.$refs.assets && this.$refs.assets.get_backdrop_settings) {
if(!config.scene) {
config.scene = {};
}
Object.assign(config.scene, this.$refs.assets.get_backdrop_settings());
}
return config;
},
onChildChanged() {
// When any child component changes, emit preview with merged config
const previewConfig = this.get_config();
this.$emit('appearance-preview', previewConfig);
},
},
}
</script>

View File

@@ -1,97 +0,0 @@
<template>
<v-tabs color="secondary" v-model="tab" :disabled="busy">
<v-tab v-for="t in tabs" :key="t.value" :value="t.value">
<v-icon start>{{ t.icon }}</v-icon>
{{ t.title }}
</v-tab>
</v-tabs>
<v-window v-model="tab">
<v-window-item value="inference">
<AppConfigPresetsInference ref="inference" :immutableConfig="immutableConfig" @update="() => $emit('update', config)"></AppConfigPresetsInference>
</v-window-item>
<v-window-item value="embeddings">
<AppConfigPresetsEmbeddings
ref="embeddings"
@busy="() => busy = true"
@done="() => busy = false"
:clientStatus="clientStatus"
:memoryAgentStatus="agentStatus.memory || null" :immutableConfig="immutableConfig"
:sceneActive="sceneActive"
@update="() => $emit('update', config)"
></AppConfigPresetsEmbeddings>
</v-window-item>
<v-window-item value="system_prompts">
<AppConfigPresetsSystemPrompts
ref="system_prompts"
:immutableConfig="immutableConfig"
:system-prompt-defaults="immutableConfig ? immutableConfig.system_prompt_defaults : {}"
@update="() => $emit('update', config)"
></AppConfigPresetsSystemPrompts>
</v-window-item>
</v-window>
</template>
<script>
import AppConfigPresetsInference from './AppConfigPresetsInference.vue';
import AppConfigPresetsEmbeddings from './AppConfigPresetsEmbeddings.vue';
import AppConfigPresetsSystemPrompts from './AppConfigPresetsSystemPrompts.vue';
export default {
name: 'AppConfigPresets',
components: {
AppConfigPresetsInference,
AppConfigPresetsEmbeddings,
AppConfigPresetsSystemPrompts,
},
props: {
immutableConfig: Object,
agentStatus: Object,
sceneActive: Boolean,
clientStatus: Object,
},
emits: [
'update',
],
data() {
return {
tabs: [
{ title: 'Inference', icon: 'mdi-matrix', value: 'inference' },
{ title: 'Embeddings', icon: 'mdi-cube-unfolded', value: 'embeddings' },
{ title: 'System Prompts', icon: 'mdi-text-box', value: 'system_prompts' },
],
tab: 'inference',
busy: false,
}
},
methods: {
setSelection(tab) {
this.tab = tab;
console.log('setSelection (presets)', tab);
},
inference_config() {
if(this.$refs.inference) {
return this.$refs.inference.config.inference;
}
return null;
},
inference_groups_config() {
if(this.$refs.inference) {
return this.$refs.inference.config.inference_groups;
}
return null;
},
embeddings_config() {
if(this.$refs.embeddings) {
return this.$refs.embeddings.config.embeddings;
}
return null;
},
system_prompts_config() {
if(this.$refs.system_prompts) {
return this.$refs.system_prompts.config;
}
return null;
},
},
}
</script>

View File

@@ -33,7 +33,7 @@
rows="10"
auto-grow
clearable
@update:model-value="dropIfEmpty(selectedKey);"
@update:model-value="dropIfEmpty(selectedKey); $emit('update', {system_prompts: config})"
@blur="$emit('update', {system_prompts: config})"
:label="labelFromValue(selected[0], tab === 'decensor')"
></v-textarea>
@@ -67,7 +67,7 @@
These system prompts will only be used by this client.
</p>
<p class="text-caption text-grey">
You can specify global overrides in the <span class="text-primary"><v-icon>mdi-cog</v-icon> Settings</span> window.
You can specify global overrides in the <span class="text-primary"><v-icon>mdi-cog</v-icon> Settings</span> tab under <span class="text-primary">Presets System Prompts</span>.
</p>
</v-alert>
</v-col>

View File

@@ -0,0 +1,401 @@
<template>
<v-card variant="text" v-if="app_config !== null">
<v-toolbar rounded="md" density="compact" color="grey-darken-4" class="pl-2 mb-1 app-settings-toolbar">
<v-icon class="mr-2" color="primary">{{ currentPage?.icon || 'mdi-cog' }}</v-icon>
<span class="text-subtitle-2">{{ currentPage?.group }} <span class="text-muted">/</span> {{ currentPage?.title }}</span>
<v-spacer></v-spacer>
<v-chip v-if="externalChange" size="small" label color="warning" variant="text" prepend-icon="mdi-alert-circle-outline" class="mr-2">
Changed outside this view saving overwrites
<v-tooltip activator="parent" location="bottom" max-width="400">The configuration was changed elsewhere (another window or the help agent) while you have unsaved edits here. Discard to load the latest, or save to overwrite.</v-tooltip>
</v-chip>
<span v-if="dirty" class="text-muted text-caption mr-2">Unsaved changes.</span>
<v-btn v-if="dirty" color="muted" variant="text" prepend-icon="mdi-undo" @click="discard">Discard</v-btn>
<v-btn color="primary" variant="text" prepend-icon="mdi-check-circle-outline" :disabled="!dirty || embeddingsBusy" @click="saveConfig">Save</v-btn>
</v-toolbar>
<!-- surface-colored canvas: the reused editors were designed against the
old modal's card surface on the bare viewport background their
internal surfaces read as disconnected grey islands -->
<v-sheet class="app-settings-content pa-4" rounded="md">
<v-window v-model="page" :touch="false">
<v-window-item value="gameplay">
<AppSettingsGameplay :config="app_config" />
</v-window-item>
<v-window-item value="player-character">
<AppSettingsPlayerCharacter :config="app_config" />
</v-window-item>
<v-window-item value="appearance-messages">
<AppSettingsPageHeader title="Messages" icon="mdi-script-text">
Style the different message types in the scene view. Changes preview live in the scene while you edit.
</AppSettingsPageHeader>
<AppConfigAppearanceScene ref="appearance-messages" :immutableConfig="app_config" :sceneActive="sceneActive" @changed="onAppearanceChanged"></AppConfigAppearanceScene>
</v-window-item>
<v-window-item value="appearance-visuals">
<AppSettingsPageHeader title="Visuals" icon="mdi-image-outline">
Message visuals and scene backdrop behavior.
</AppSettingsPageHeader>
<AppConfigAppearanceAssets ref="appearance-visuals" :immutableConfig="app_config" :sceneActive="sceneActive" @changed="onAppearanceChanged"></AppConfigAppearanceAssets>
</v-window-item>
<v-window-item value="api-keys">
<AppSettingsApiKeys :config="app_config" />
</v-window-item>
<v-window-item value="env-variables">
<AppSettingsEnvVariables :config="app_config" />
</v-window-item>
<v-window-item value="presets-inference">
<AppConfigPresetsInference ref="presets-inference" :immutableConfig="app_config" @update="onPresetsChanged"></AppConfigPresetsInference>
</v-window-item>
<v-window-item value="presets-embeddings">
<AppConfigPresetsEmbeddings
ref="presets-embeddings"
@busy="() => busy = true"
@done="() => busy = false"
:clientStatus="clientStatus"
:memoryAgentStatus="agentStatus.memory || null"
:immutableConfig="app_config"
:sceneActive="sceneActive"
@update="onPresetsChanged"
></AppConfigPresetsEmbeddings>
</v-window-item>
<v-window-item value="presets-system-prompts">
<AppConfigPresetsSystemPrompts
ref="presets-system-prompts"
:immutableConfig="app_config"
:system-prompt-defaults="app_config.system_prompt_defaults"
@update="onPresetsChanged"
></AppConfigPresetsSystemPrompts>
</v-window-item>
<v-window-item value="content-classification">
<AppSettingsStringList
title="Content Classification"
icon="mdi-cube-scan"
input-label="Add content classification (Press enter to add)"
:list="app_config.creator.content_context"
@update:list="(list) => app_config.creator.content_context = list">
Available content classification choices when generating characters or scenarios.
</AppSettingsStringList>
</v-window-item>
<v-window-item value="perspective-presets">
<AppSettingsStringList
title="Perspective Presets"
icon="mdi-eye-outline"
input-label="Add perspective preset (Press enter to add)"
:list="app_config.creator.perspective_presets"
@update:list="(list) => app_config.creator.perspective_presets = list">
Reusable narrative perspective / tense strings offered in the scene outline. Use <code>{player_name}</code> as a placeholder for the player character it will be substituted at prompt render time.
</AppSettingsStringList>
</v-window-item>
</v-window>
</v-sheet>
</v-card>
<v-card v-else variant="text">
<v-card-text>
<v-progress-circular indeterminate="disable-shrink" color="primary" size="20"></v-progress-circular>
</v-card-text>
</v-card>
</template>
<script>
import AppSettingsPageHeader from './AppSettingsPageHeader.vue';
import AppSettingsGameplay from './AppSettingsGameplay.vue';
import AppSettingsPlayerCharacter from './AppSettingsPlayerCharacter.vue';
import AppSettingsApiKeys from './AppSettingsApiKeys.vue';
import AppSettingsEnvVariables from './AppSettingsEnvVariables.vue';
import AppSettingsStringList from './AppSettingsStringList.vue';
import AppConfigAppearanceScene from './AppConfigAppearanceScene.vue';
import AppConfigAppearanceAssets from './AppConfigAppearanceAssets.vue';
import AppConfigPresetsInference from './AppConfigPresetsInference.vue';
import AppConfigPresetsEmbeddings from './AppConfigPresetsEmbeddings.vue';
import AppConfigPresetsSystemPrompts from './AppConfigPresetsSystemPrompts.vue';
import { settingsPage, mapLegacyLocation } from '../utils/appSettingsRegistry.js';
export default {
name: 'AppSettings',
components: {
AppSettingsPageHeader,
AppSettingsGameplay,
AppSettingsPlayerCharacter,
AppSettingsApiKeys,
AppSettingsEnvVariables,
AppSettingsStringList,
AppConfigAppearanceScene,
AppConfigAppearanceAssets,
AppConfigPresetsInference,
AppConfigPresetsEmbeddings,
AppConfigPresetsSystemPrompts,
},
props: {
agentStatus: Object,
sceneActive: Boolean,
clientStatus: Object,
visible: Boolean,
},
emits: [
'appearance-preview',
'appearance-preview-clear',
'page-changed',
],
data() {
return {
page: 'gameplay',
app_config: null,
snapshot: null,
pendingConfig: null,
externalChange: false,
saving: false,
busy: false,
}
},
inject: ['getWebsocket', 'registerMessageHandler', 'requestAppConfig'],
computed: {
dirty() {
if (this.app_config === null || this.snapshot === null) {
return false;
}
return JSON.stringify(this.app_config) !== this.snapshot;
},
currentPage() {
return settingsPage(this.page);
},
embeddingsBusy() {
// the embeddings editor reports busy while the memory agent
// applies an embedding set — only block saving from that page
return this.busy && this.page === 'presets-embeddings';
},
},
watch: {
page(newPage) {
this.$emit('page-changed', newPage);
},
visible(isVisible) {
if (!isVisible) {
// leaving the settings tab — the scene should show the saved
// appearance again, not an unsaved preview
this.$emit('appearance-preview-clear');
} else {
if (this.app_config === null) {
this.requestAppConfig();
}
if (this.dirty) {
// returning with unsaved appearance edits — restore the preview
this.emitAppearancePreview();
}
}
},
},
methods: {
uxSnapshot() {
// what the settings view shows, for the help agent's UX snapshot.
// `dirty` gates help-agent config writes backend-side: only
// unsaved edits here are a conflict, an idle settings tab is not
if (!this.visible) return null;
return { tab: 'settings', page: this.page, dirty: this.dirty };
},
navigate(page, anchor = null, item = null) {
if (page) {
this.page = page;
}
this.$nextTick(() => {
// wait for the (lazily mounted) window item before selecting
// or scrolling
setTimeout(() => {
if (item) {
const ref = this.$refs[this.page];
if (ref && ref.setSelection) {
ref.setSelection(item);
}
}
if (anchor) {
this.flashAnchor(anchor);
}
}, 300);
});
},
// legacy openAppConfig(tab, page, item) entry point — still used by
// backend uxActions and older callers
openLegacy(tab, page, item = null) {
const location = mapLegacyLocation(tab, page, item);
if (location.page) {
this.navigate(location.page, location.anchor, location.item);
}
},
flashAnchor(anchor) {
const el = this.$el.querySelector(`[data-setting-anchor="${anchor}"]`);
if (!el) {
return;
}
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
el.classList.remove('setting-flash');
// restart the animation if it was already applied
void el.offsetWidth;
el.classList.add('setting-flash');
setTimeout(() => el.classList.remove('setting-flash'), 2500);
},
adoptConfig(config) {
// the app_config payload emitted alongside a save lacks
// system_prompt_defaults (only request_app_config and
// config-changed broadcasts include it) — carry it over
if (config.system_prompt_defaults === undefined && this.app_config?.system_prompt_defaults !== undefined) {
config = { ...config, system_prompt_defaults: this.app_config.system_prompt_defaults };
}
// deep-copy: ws payloads are parsed once and shared with every
// registered handler (TalemateApp.appConfig stores the same
// object) — editing an aliased working copy would leak unsaved
// edits app-wide and make Discard leave stale state behind
this.snapshot = JSON.stringify(config);
this.app_config = JSON.parse(this.snapshot);
this.pendingConfig = null;
this.externalChange = false;
},
discard() {
const restore = this.pendingConfig !== null ? this.pendingConfig : JSON.parse(this.snapshot);
this.adoptConfig(restore);
this.$emit('appearance-preview-clear');
},
mergeSubcomponentConfigs() {
// ref-based editors (presets, appearance) keep their own working
// state — fold it back into the config working copy.
// NOTE: the inference and embeddings editors shallow-copy their
// immutableConfig, so their nested preset objects ALIAS
// app_config.presets.* — edits there mutate the working copy
// directly (which is what drives dirty tracking for those pages),
// and the assignments below are no-ops for them. If those editors
// ever deep-copy, dirty tracking needs an explicit update event.
const inference = this.$refs['presets-inference'];
if (inference) {
this.app_config.presets.inference = inference.config.inference;
this.app_config.presets.inference_groups = inference.config.inference_groups;
}
const embeddings = this.$refs['presets-embeddings'];
if (embeddings) {
this.app_config.presets.embeddings = embeddings.config.embeddings;
}
const systemPrompts = this.$refs['presets-system-prompts'];
if (systemPrompts) {
this.app_config.system_prompts = systemPrompts.config;
}
const appearanceScene = this.$refs['appearance-messages'];
if (appearanceScene) {
this.app_config.appearance.scene = appearanceScene.config;
}
const appearanceAssets = this.$refs['appearance-visuals'];
if (appearanceAssets) {
if (!this.app_config.appearance.scene) {
this.app_config.appearance.scene = {};
}
if (appearanceAssets.get_config) {
this.app_config.appearance.scene.message_assets = appearanceAssets.get_config();
}
if (appearanceAssets.get_auto_attach_assets) {
this.app_config.appearance.scene.auto_attach_assets = appearanceAssets.get_auto_attach_assets();
}
if (appearanceAssets.get_backdrop_settings) {
Object.assign(this.app_config.appearance.scene, appearanceAssets.get_backdrop_settings());
}
}
},
onPresetsChanged() {
this.mergeSubcomponentConfigs();
},
onAppearanceChanged() {
this.mergeSubcomponentConfigs();
this.emitAppearancePreview();
},
emitAppearancePreview() {
this.$emit('appearance-preview', this.app_config.appearance);
},
handleMessage(message) {
if (message.type == "app_config") {
if (this.dirty) {
// don't clobber unsaved edits — stash the incoming config.
// Broadcasts fire on reconnects and unrelated saves too, so
// only flag an external change when the content actually
// differs from the baseline we're editing against.
this.pendingConfig = message.data;
if (!this.saving && JSON.stringify(message.data) !== this.snapshot) {
this.externalChange = true;
}
} else {
this.adoptConfig(message.data);
}
return;
}
if (message.type == 'config') {
if (message.action == 'operation_done' && message.error) {
// failed save (e.g. backend validation) — save_complete
// never arrives; a stuck saving flag would suppress
// external-change detection for the rest of the session
this.saving = false;
return;
}
if (message.action == 'save_complete') {
// adopt the server-normalized config broadcast alongside
// the save when available
if (this.pendingConfig !== null) {
this.adoptConfig(this.pendingConfig);
} else {
this.snapshot = JSON.stringify(this.app_config);
this.externalChange = false;
}
this.saving = false;
this.$emit('appearance-preview-clear');
}
}
},
sendRequest(data) {
data.type = 'config';
this.getWebsocket().send(JSON.stringify(data));
},
saveConfig() {
this.mergeSubcomponentConfigs();
this.saving = true;
this.sendRequest({
action: 'save',
config: this.app_config,
});
},
},
created() {
this.registerMessageHandler(this.handleMessage);
this.requestAppConfig();
},
}
</script>
<style scoped>
.app-settings-toolbar {
position: sticky;
top: 0;
z-index: 2;
}
.app-settings-content {
max-width: 1200px;
}
</style>
<style>
.setting-flash {
animation: setting-flash-pulse 2.5s ease-out;
border-radius: 4px;
}
@keyframes setting-flash-pulse {
0% {
background-color: rgba(var(--v-theme-primary), 0.25);
}
100% {
background-color: transparent;
}
}
</style>

View File

@@ -0,0 +1,86 @@
<template>
<AppSettingsPageHeader title="API Keys" icon="mdi-key-variant">
API keys and access tokens for third party services. Only fill in the services you use.
</AppSettingsPageHeader>
<v-card v-for="provider in providers" :key="provider.id" class="mb-3" variant="tonal" :data-setting-anchor="provider.id">
<v-card-title class="text-subtitle-1">
<v-icon start size="small">{{ provider.icon || 'mdi-api' }}</v-icon>
{{ provider.title }}
</v-card-title>
<v-card-text>
<div class="text-grey text-body-2 mb-2">
<p v-if="provider.description" class="mb-1">{{ provider.description }}</p>
<template v-if="provider.id === 'google_api'">
<p class="mb-2"><strong>Option&nbsp;1 API&nbsp;Key&nbsp;(recommended)</strong></p>
<p class="mb-1">Create a Google API key at <a href="https://aistudio.google.com/apikey" target="_blank">aistudio.google.com/apikey</a> and paste it in the field below. This is the quickest way to start using Gemini models.</p>
<p class="mb-2 mt-3"><strong>Option&nbsp;2 Vertex&nbsp;AI service&nbsp;account&nbsp;(advanced)</strong></p>
<p class="mb-0">If you prefer using a full Google Cloud project, follow the setup guide <a href="https://cloud.google.com/vertex-ai/docs/start/client-libraries" target="_blank">here</a> to generate a service-account JSON credential file, then complete the legacy fields below.</p>
<p class="text-caption mt-1 text-muted">
If both are setup, the API key will be used.
</p>
</template>
<template v-else>
Get your key from <a :href="provider.url" target="_blank">{{ provider.urlLabel || provider.url }}</a> <span v-if="provider.urlNote" class="text-caption">{{ provider.urlNote }}</span>
</template>
</div>
<v-text-field type="password" v-model="config[provider.section].api_key"
:label="provider.fieldLabel" density="compact" hide-details></v-text-field>
<template v-if="provider.id === 'google_api'">
<v-text-field v-model="config.google.gcloud_credentials_path" class="mt-3"
label="Google Cloud Credentials Path" density="compact" messages="Path to the service-account JSON credentials file on the machine running the Talemate backend."></v-text-field>
<v-combobox v-model="config.google.gcloud_location" class="mt-3"
label="Google Cloud Location" density="compact" :items="googleCloudLocations" messages="Pick something close to you" :return-object="false"></v-combobox>
</template>
</v-card-text>
</v-card>
</template>
<script>
import AppSettingsPageHeader from './AppSettingsPageHeader.vue';
import { API_PROVIDERS } from '../utils/appSettingsRegistry.js';
export default {
name: 'AppSettingsApiKeys',
components: {
AppSettingsPageHeader,
},
props: {
config: Object,
},
data() {
return {
providers: API_PROVIDERS,
googleCloudLocations: [
{"value": 'us-central1', "title": 'US Central - Iowa'},
{"value": 'us-west4', "title": 'US West 4 - Las Vegas'},
{"value": 'us-east1', "title": 'US East 1 - South Carolina'},
{"value": 'us-east4', "title": 'US East 4 - Northern Virginia'},
{"value": 'us-west1', "title": 'US West 1 - Oregon'},
{"value": 'northamerica-northeast1', "title": 'North America Northeast 1 - Montreal'},
{"value": 'southamerica-east1', "title": 'South America East 1 - Sao Paulo'},
{"value": 'europe-west1', "title": 'Europe West 1 - Belgium'},
{"value": 'europe-north1', "title": 'Europe North 1 - Finland'},
{"value": 'europe-west3', "title": 'Europe West 3 - Frankfurt'},
{"value": 'europe-west2', "title": 'Europe West 2 - London'},
{"value": 'europe-southwest1', "title": 'Europe Southwest 1 - Zurich'},
{"value": 'europe-west8', "title": 'Europe West 8 - Netherlands'},
{"value": 'europe-west4', "title": 'Europe West 4 - London'},
{"value": 'europe-west9', "title": 'Europe West 9 - Stockholm'},
{"value": 'europe-central2', "title": 'Europe Central 2 - Warsaw'},
{"value": 'europe-west6', "title": 'Europe West 6 - Zurich'},
{"value": 'asia-east1', "title": 'Asia East 1 - Taiwan'},
{"value": 'asia-east2', "title": 'Asia East 2 - Hong Kong'},
{"value": 'asia-south1', "title": 'Asia South 1 - Mumbai'},
{"value": 'asia-northeast1', "title": 'Asia Northeast 1 - Tokyo'},
{"value": 'asia-northeast3', "title": 'Asia Northeast 3 - Seoul'},
{"value": 'asia-southeast1', "title": 'Asia Southeast 1 - Singapore'},
{"value": 'asia-southeast2', "title": 'Asia Southeast 2 - Jakarta'},
{"value": 'australia-southeast1', "title": 'Australia Southeast 1 - Sydney'},
{"value": 'australia-southeast2', "title": 'Australia Southeast 2 - Melbourne'},
{"value": 'me-west1', "title": 'Middle East West 1 - Dammam'},
{"value": 'asia-northeast2', "title": 'Asia Northeast 2 - Osaka'},
].sort((a, b) => a.title.localeCompare(b.title)),
}
},
}
</script>

View File

@@ -0,0 +1,100 @@
<template>
<AppSettingsPageHeader title="Environment Variables" icon="mdi-variable">
<p class="mb-1">Named values passed as environment variables to processes Talemate spawns currently the <span class="text-primary">Pi Bridge</span> client, where pi's <code>models.json</code> can reference them as <code>$NAME</code>.</p>
Values are encrypted at rest in Talemate's configuration file.
</AppSettingsPageHeader>
<v-card variant="tonal">
<v-card-title class="text-subtitle-1">
<v-icon start size="small">mdi-variable</v-icon>
Variables
</v-card-title>
<v-card-text>
<div v-if="envVariableNames.length === 0" class="text-grey text-body-2 mb-4">
No environment variables defined yet add one below.
</div>
<v-row v-for="name in envVariableNames" :key="name" dense>
<v-col cols="5">
<v-text-field :model-value="name" label="Name" readonly density="compact" hide-details></v-text-field>
</v-col>
<v-col cols="6">
<v-text-field type="password" v-model="config.env[name]" label="Value" density="compact" hide-details></v-text-field>
</v-col>
<v-col cols="1" class="d-flex align-center">
<v-btn icon="mdi-close-circle-outline" variant="text" color="delete" size="small" @click="envVariableRemove(name)"></v-btn>
</v-col>
</v-row>
<v-divider v-if="envVariableNames.length > 0" class="my-4"></v-divider>
<v-row dense>
<v-col cols="5">
<v-text-field v-model="env_variable_name_input" label="Name" density="compact"
:rules="[validateEnvVariableName]" placeholder="MY_API_KEY"
@keyup.enter="envVariableAdd"></v-text-field>
</v-col>
<v-col cols="6">
<v-text-field type="password" v-model="env_variable_value_input" label="Value" density="compact"
@keyup.enter="envVariableAdd"></v-text-field>
</v-col>
<v-col cols="1" class="d-flex align-center">
<v-btn icon="mdi-plus-circle-outline" variant="text" color="primary" size="small"
:disabled="!envVariableAddValid" @click="envVariableAdd"></v-btn>
</v-col>
</v-row>
</v-card-text>
</v-card>
</template>
<script>
import AppSettingsPageHeader from './AppSettingsPageHeader.vue';
export default {
name: 'AppSettingsEnvVariables',
components: {
AppSettingsPageHeader,
},
props: {
config: Object,
},
data() {
return {
env_variable_name_input: '',
env_variable_value_input: '',
}
},
computed: {
envVariableNames() {
return Object.keys(this.config?.env || {}).sort();
},
envVariableAddValid() {
return this.validateEnvVariableName(this.env_variable_name_input) === true
&& this.env_variable_name_input.trim() !== ''
&& this.env_variable_value_input !== '';
},
},
methods: {
validateEnvVariableName(value) {
const name = (value || '').trim();
if (!name) return true;
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
return 'Letters, digits and underscores only; must not start with a digit';
}
if (this.config?.env && name in this.config.env) {
return 'Already exists — edit its row above';
}
return true;
},
envVariableAdd() {
if (!this.envVariableAddValid) return;
const name = this.env_variable_name_input.trim();
if (!this.config.env) {
this.config.env = {};
}
this.config.env[name] = this.env_variable_value_input;
this.env_variable_name_input = '';
this.env_variable_value_input = '';
},
envVariableRemove(name) {
delete this.config.env[name];
},
},
}
</script>

View File

@@ -0,0 +1,44 @@
<template>
<AppSettingsPageHeader title="General" icon="mdi-cog">
General game behavior.
</AppSettingsPageHeader>
<v-row>
<v-col cols="12" data-setting-anchor="auto_save">
<v-checkbox color="primary" v-model="config.game.general.auto_save" label="Auto save" messages="Automatically save after each game-loop"></v-checkbox>
</v-col>
</v-row>
<v-row>
<v-col cols="12" data-setting-anchor="auto_progress">
<v-checkbox color="primary" v-model="config.game.general.auto_progress" label="Auto progress" messages="AI automatically progresses after player turn."></v-checkbox>
</v-col>
</v-row>
<v-row>
<v-col cols="6" data-setting-anchor="max_backscroll">
<v-number-input v-model="config.game.general.max_backscroll" label="Max backscroll" messages="Maximum number of messages to keep in the scene backscroll"></v-number-input>
</v-col>
</v-row>
<v-row>
<v-col cols="12" data-setting-anchor="show_agent_activity_bar">
<v-checkbox color="primary" v-model="config.game.general.show_agent_activity_bar" label="Show agent activity bar" messages="Display active agent actions in a horizontal bar above scene controls"></v-checkbox>
</v-col>
</v-row>
<v-row>
<v-col cols="12" data-setting-anchor="release_gpu_cache_on_scene_load">
<v-checkbox color="primary" v-model="config.game.general.release_gpu_cache_on_scene_load" label="Release GPU cache on scene load" messages="Hand idle GPU memory reserved by local CUDA embeddings/TTS back to the driver when switching scenes, so it doesn't pile up and eventually leave too little VRAM to load another scene. Disable only if you have VRAM to spare and would rather keep it reserved."></v-checkbox>
</v-col>
</v-row>
</template>
<script>
import AppSettingsPageHeader from './AppSettingsPageHeader.vue';
export default {
name: 'AppSettingsGameplay',
components: {
AppSettingsPageHeader,
},
props: {
config: Object,
},
}
</script>

View File

@@ -0,0 +1,85 @@
<template>
<v-text-field
v-model="search"
density="compact"
variant="solo-filled"
flat
hide-details
clearable
placeholder="Search settings"
prepend-inner-icon="mdi-magnify"
class="ma-2"
></v-text-field>
<!-- search results -->
<v-list v-if="searchActive" density="compact" slim color="primary" selectable>
<v-list-subheader color="grey">
<v-icon color="primary" class="mr-1">mdi-magnify</v-icon>
Results
</v-list-subheader>
<v-list-item v-if="searchResults.length === 0">
<v-list-item-subtitle class="text-caption">No settings match "{{ search }}"</v-list-item-subtitle>
</v-list-item>
<v-list-item v-for="(result, index) in searchResults" :key="index"
:active="false"
@click="onSelectResult(result)">
<template v-slot:prepend>
<v-icon>{{ result.page.icon }}</v-icon>
</template>
<v-list-item-title>{{ result.entry ? result.entry.label : result.page.title }}</v-list-item-title>
<v-list-item-subtitle class="text-caption">{{ result.page.group }} · {{ result.page.title }}</v-list-item-subtitle>
</v-list-item>
</v-list>
<!-- navigation -->
<v-list v-else density="compact" slim color="primary" :selected="[page]" selectable @update:selected="onSelectPage">
<template v-for="group in groups" :key="group.title">
<v-list-subheader color="grey">
<v-icon color="primary" class="mr-1">{{ group.icon }}</v-icon>
{{ group.title }}
</v-list-subheader>
<v-list-item v-for="groupPage in group.pages" :key="groupPage.id" :value="groupPage.id" :prepend-icon="groupPage.icon">
<v-list-item-title>{{ groupPage.title }}</v-list-item-title>
</v-list-item>
</template>
</v-list>
</template>
<script>
import { SETTINGS_GROUPS, searchSettings } from '../utils/appSettingsRegistry.js';
export default {
name: 'AppSettingsMenu',
props: {
page: String,
},
emits: [
'navigate',
],
data() {
return {
groups: SETTINGS_GROUPS,
search: '',
}
},
computed: {
searchActive() {
return (this.search || '').trim() !== '';
},
searchResults() {
return searchSettings(this.search);
},
},
methods: {
onSelectPage(selection) {
if (selection.length === 0) {
return;
}
this.$emit('navigate', { page: selection[0], anchor: null });
},
onSelectResult(result) {
this.$emit('navigate', { page: result.page.id, anchor: result.entry ? result.entry.anchor : null });
},
},
}
</script>

View File

@@ -0,0 +1,19 @@
<template>
<v-alert color="white" variant="text" :icon="icon" density="compact">
<v-alert-title>{{ title }}</v-alert-title>
<div class="text-grey">
<slot></slot>
</div>
</v-alert>
<v-divider class="mb-2"></v-divider>
</template>
<script>
export default {
name: 'AppSettingsPageHeader',
props: {
title: String,
icon: String,
},
}
</script>

View File

@@ -0,0 +1,40 @@
<template>
<AppSettingsPageHeader title="Default player character" icon="mdi-human-edit">
This will be default player character that will be added to a scene if the scene does not come with a defined player character. Mostly relevant when you load character-cards that aren't in the talemate scene format.
</AppSettingsPageHeader>
<v-row>
<v-col cols="6">
<v-text-field v-model="config.game.default_player_character.name"
label="Name"></v-text-field>
</v-col>
<v-col cols="6">
<v-text-field v-model="config.game.default_player_character.gender"
label="Gender"></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12">
<v-textarea v-model="config.game.default_player_character.description"
auto-grow label="Description"></v-textarea>
</v-col>
</v-row>
<v-row>
<v-col cols="12" data-setting-anchor="add_default_character">
<v-checkbox color="primary" v-model="config.game.general.add_default_character" label="Add default character to blank talemate scenes" messages="When creating a new scene, add the default player character to the scene."></v-checkbox>
</v-col>
</v-row>
</template>
<script>
import AppSettingsPageHeader from './AppSettingsPageHeader.vue';
export default {
name: 'AppSettingsPlayerCharacter',
components: {
AppSettingsPageHeader,
},
props: {
config: Object,
},
}
</script>

View File

@@ -0,0 +1,55 @@
<template>
<AppSettingsPageHeader :title="title" :icon="icon">
<slot></slot>
</AppSettingsPageHeader>
<v-row>
<v-col cols="12">
<v-list density="compact">
<v-list-item v-for="(value, index) in list" :key="index">
<v-list-item-title><v-icon color="red-darken-1" class="mr-2" @click="remove(index)">mdi-close-box-outline</v-icon>{{ value }}</v-list-item-title>
</v-list-item>
</v-list>
<v-divider></v-divider>
<v-text-field v-model="input" :label="inputLabel"
@keyup.enter="add"></v-text-field>
</v-col>
</v-row>
</template>
<script>
import AppSettingsPageHeader from './AppSettingsPageHeader.vue';
export default {
name: 'AppSettingsStringList',
components: {
AppSettingsPageHeader,
},
props: {
title: String,
icon: String,
inputLabel: String,
list: Array,
},
emits: [
'update:list',
],
data() {
return {
input: '',
}
},
methods: {
add() {
const value = (this.input || '').trim();
if (!value) return;
this.$emit('update:list', [...(this.list || []), value]);
this.input = '';
},
remove(index) {
const updated = [...(this.list || [])];
updated.splice(index, 1);
this.$emit('update:list', updated);
},
},
}
</script>

View File

@@ -169,6 +169,9 @@
@clear-prompts="onClearPrompts"
/>
</v-tabs-window-item>
<v-tabs-window-item :transition="false" :reverse-transition="false" value="settings">
<AppSettingsMenu :page="appSettingsPage" @navigate="onAppSettingsNavigate" />
</v-tabs-window-item>
</v-tabs-window>
</v-navigation-drawer>
@@ -359,13 +362,24 @@
<v-tabs-window-item :transition="false" :reverse-transition="false" value="prompts">
<PromptsView :visible="tab === 'prompts'" :prompts="prompts" :agent-status="agentStatus" :app-config="appConfig" ref="promptsView" v-model:main-tab="promptsMainTab" @clear-prompts="onClearPrompts" />
</v-tabs-window-item>
<!-- SETTINGS -->
<v-tabs-window-item :transition="false" :reverse-transition="false" value="settings">
<AppSettings
ref="appSettings"
:agent-status="agentStatus"
:scene-active="sceneActive"
:client-status="clientStatus"
:visible="tab === 'settings'"
@appearance-preview="onAppearancePreview"
@appearance-preview-clear="onAppearancePreviewClear"
@page-changed="(page) => appSettingsPage = page" />
</v-tabs-window-item>
</v-tabs-window>
</v-container>
</v-main>
<AppConfig ref="appConfig" :agentStatus="agentStatus" :sceneActive="sceneActive" :clientStatus="clientStatus" @appearance-preview="onAppearancePreview" @appearance-preview-clear="onAppearancePreviewClear" />
<AgentActionOverrides ref="agentActionOverrides" :app-config="appConfig" :agent-status="agentStatus" />
<v-dialog v-model="worldEditorUnavailable" max-width="420">
@@ -417,7 +431,8 @@ import SceneMessages from './SceneMessages.vue';
import SceneMessageInput from './SceneMessageInput.vue';
import WorldState from './WorldState.vue';
import CoverImage from './CoverImage.vue';
import AppConfig from './AppConfig.vue';
import AppSettings from './AppSettings.vue';
import AppSettingsMenu from './AppSettingsMenu.vue';
import DebugTools from './DebugTools.vue';
import AudioQueue from './AudioQueue.vue';
import StatusNotification from './StatusNotification.vue';
@@ -460,7 +475,8 @@ export default {
SceneMessageInput,
WorldState,
CoverImage,
AppConfig,
AppSettings,
AppSettingsMenu,
DebugTools,
AudioQueue,
StatusNotification,
@@ -554,6 +570,15 @@ export default {
},
value: 'prompts'
},
{
title: () => { return 'Settings' },
condition: () => { return true },
icon: () => { return 'mdi-cog' },
click: () => {
// Settings tab clicked
},
value: 'settings'
},
{
title: () => { return 'Home' },
condition: () => { return true },
@@ -627,6 +652,8 @@ export default {
recentTemplates: [],
// Synced tab state between PromptsMenu and PromptsView
promptsMainTab: 'prompts',
// Synced page state between AppSettingsMenu and AppSettings
appSettingsPage: 'gameplay',
// Count of outdated prompt template overrides
promptsOutdatedCount: 0,
// Flag to ensure new-scene navigation only happens once per scene load
@@ -1477,7 +1504,7 @@ export default {
scene_environment: this.sceneActive ? this.scene?.environment : null,
client_settings_modal: this.$refs.aiClient?.uxSnapshot() || null,
agent_settings_modal: this.$refs.aiAgent?.uxSnapshot() || null,
app_settings_modal: this.$refs.appConfig?.uxSnapshot() || null,
app_settings_modal: this.$refs.appSettings?.uxSnapshot() || null,
app_ready: this.ready,
waiting_for_input: this.waitingForInput,
};
@@ -1627,7 +1654,17 @@ export default {
this.openAppConfig('appearance', 'scene');
},
openAppConfig(tab, page, item=null) {
this.$refs.appConfig.show(tab, page, item);
this.tab = 'settings';
this.$nextTick(() => {
if (tab && this.$refs.appSettings) {
this.$refs.appSettings.openLegacy(tab, page, item);
}
});
},
onAppSettingsNavigate({ page, anchor }) {
if (this.$refs.appSettings) {
this.$refs.appSettings.navigate(page, anchor);
}
},
uxErrorHandler(error) {
this.errorNotification = true;

View File

@@ -0,0 +1,313 @@
// Registry describing the application settings navigation and search index.
//
// Pages are flat (one level of sidebar navigation, grouped by subheaders).
// Each page carries `entries` — the individual settings it hosts — which feed
// the settings search in the drawer menu. An entry's optional `anchor` matches
// a `data-setting-anchor` attribute in the page markup so search results can
// scroll to and highlight the exact setting.
// API key providers rendered by AppSettingsApiKeys.vue. `id` doubles as the
// legacy deep-link page value (`openAppConfig('application', '<id>')`) and the
// scroll anchor on the API Keys page. `section` is the key in the app config.
export const API_PROVIDERS = [
{
id: 'anthropic_api',
section: 'anthropic',
title: 'Anthropic',
fieldLabel: 'Anthropic API Key',
url: 'https://console.anthropic.com/settings/keys',
keywords: ['claude'],
},
{
id: 'cohere_api',
section: 'cohere',
title: 'Cohere',
fieldLabel: 'Cohere API Key',
url: 'https://dashboard.cohere.com/api-keys',
keywords: ['command'],
},
{
id: 'deepseek_api',
section: 'deepseek',
title: 'DeepSeek',
fieldLabel: 'DeepSeek API Key',
url: 'https://platform.deepseek.com/',
keywords: [],
},
{
id: 'elevenlabs_api',
section: 'elevenlabs',
title: 'ElevenLabs',
fieldLabel: 'ElevenLabs API Key',
url: 'https://elevenlabs.io/?from=partnerewing2048',
urlLabel: 'https://elevenlabs.io',
urlNote: '(affiliate link)',
description: 'Generate realistic speech with the most advanced AI voice model ever.',
keywords: ['tts', 'voice', 'speech'],
},
{
id: 'google_api',
section: 'google',
title: 'Google',
fieldLabel: 'Google API Key',
url: 'https://aistudio.google.com/apikey',
icon: 'mdi-google-cloud',
keywords: ['gemini', 'vertex', 'gcloud'],
},
{
id: 'groq_api',
section: 'groq',
title: 'groq',
fieldLabel: 'GROQ API Key',
url: 'https://console.groq.com/keys',
keywords: [],
},
{
id: 'huggingface_api',
section: 'huggingface',
title: 'HuggingFace',
fieldLabel: 'HuggingFace Token',
url: 'https://huggingface.co/settings/tokens',
description: 'Access token used to download gated model weights (e.g. for Pocket TTS voice cloning). A read token is sufficient.',
keywords: ['token', 'gated', 'weights'],
},
{
id: 'mistralai_api',
section: 'mistralai',
title: 'mistral.ai',
fieldLabel: 'mistral.ai API Key',
url: 'https://console.mistral.ai/api-keys/',
keywords: ['mistral'],
},
{
id: 'openai_api',
section: 'openai',
title: 'OpenAI',
fieldLabel: 'OpenAI API Key',
url: 'https://platform.openai.com/',
keywords: ['gpt', 'dall-e', 'dalle'],
},
{
id: 'openrouter_api',
section: 'openrouter',
title: 'OpenRouter',
fieldLabel: 'OpenRouter API Key',
url: 'https://openrouter.ai/settings/keys',
keywords: [],
},
];
export const SETTINGS_GROUPS = [
{
title: 'Game',
icon: 'mdi-gamepad-square',
pages: [
{
// page id predates the rename to Game / General — it doubles
// as the legacy deep-link target, keep it stable
id: 'gameplay',
title: 'General',
icon: 'mdi-cog',
description: 'General game behavior.',
entries: [
{ label: 'Auto save', anchor: 'auto_save', keywords: ['save', 'automatic'] },
{ label: 'Auto progress', anchor: 'auto_progress', keywords: ['progress', 'ai turn'] },
{ label: 'Max backscroll', anchor: 'max_backscroll', keywords: ['messages', 'history', 'scroll'] },
{ label: 'Show agent activity bar', anchor: 'show_agent_activity_bar', keywords: ['activity', 'agents'] },
{ label: 'Release GPU cache on scene load', anchor: 'release_gpu_cache_on_scene_load', keywords: ['vram', 'cuda', 'memory', 'gpu'] },
],
},
{
id: 'player-character',
title: 'Player Character',
icon: 'mdi-human-edit',
description: 'Default player character for scenes that do not define one.',
entries: [
{ label: 'Name', keywords: ['player', 'character', 'default'] },
{ label: 'Gender', keywords: ['player', 'character'] },
{ label: 'Description', keywords: ['player', 'character'] },
{ label: 'Add default character to blank scenes', anchor: 'add_default_character', keywords: ['blank', 'new scene'] },
],
},
],
},
{
title: 'Appearance',
icon: 'mdi-palette-outline',
pages: [
{
id: 'appearance-messages',
title: 'Messages',
icon: 'mdi-script-text',
description: 'Message styles and colors in the scene view.',
entries: [
{ label: 'Message styles', keywords: ['color', 'italic', 'bold', 'narrator', 'character', 'director', 'font'] },
{ label: 'Quotes / emphasis styling', keywords: ['quotes', 'parentheses', 'brackets', 'emphasis', 'entities', 'highlight'] },
],
},
{
id: 'appearance-visuals',
title: 'Visuals',
icon: 'mdi-image-outline',
description: 'Message visuals and scene backdrop behavior.',
entries: [
{ label: 'Auto-attach visuals', keywords: ['images', 'illustration', 'attach'] },
{ label: 'Auto backdrop', keywords: ['backdrop', 'background', 'immersive'] },
{ label: 'Backdrop message panel opacity', keywords: ['backdrop', 'opacity', 'legibility', 'panel'] },
{ label: 'Backdrop message text shadow', keywords: ['backdrop', 'shadow', 'legibility'] },
],
},
],
},
{
title: 'Connections',
icon: 'mdi-connection',
pages: [
{
id: 'api-keys',
title: 'API Keys',
icon: 'mdi-key-variant',
description: 'API keys for third party services.',
entries: API_PROVIDERS.map((provider) => ({
label: provider.title,
anchor: provider.id,
keywords: ['api key', 'key', 'token', ...(provider.keywords || [])],
})),
},
{
id: 'env-variables',
title: 'Environment Variables',
icon: 'mdi-variable',
description: 'Named values passed to processes Talemate spawns.',
entries: [
{ label: 'Environment variables', keywords: ['env', 'secret', 'pi bridge', 'variable'] },
],
},
],
},
{
title: 'Presets',
icon: 'mdi-tune',
pages: [
{
id: 'presets-inference',
title: 'Inference',
icon: 'mdi-matrix',
description: 'Inference parameter presets and preset groups.',
entries: [
{ label: 'Inference presets', keywords: ['temperature', 'top_p', 'top_k', 'min_p', 'repetition penalty', 'sampling', 'preset group'] },
],
},
{
id: 'presets-embeddings',
title: 'Embeddings',
icon: 'mdi-cube-unfolded',
description: 'Embedding presets for the memory agent.',
entries: [
{ label: 'Embedding presets', keywords: ['memory', 'chromadb', 'vector', 'embeddings'] },
],
},
{
id: 'presets-system-prompts',
title: 'System Prompts',
icon: 'mdi-text-box',
description: 'Global system prompt overrides.',
entries: [
{ label: 'System prompts', keywords: ['prompt', 'roleplay', 'override', 'decensor'] },
],
},
],
},
{
title: 'Storytelling',
icon: 'mdi-feather',
pages: [
{
id: 'content-classification',
title: 'Content Classification',
icon: 'mdi-cube-scan',
description: 'Content classification choices for generation.',
entries: [
{ label: 'Content classification', keywords: ['content context', 'rating', 'classification'] },
],
},
{
id: 'perspective-presets',
title: 'Perspective Presets',
icon: 'mdi-eye-outline',
description: 'Reusable narrative perspective / tense strings.',
entries: [
{ label: 'Perspective presets', keywords: ['perspective', 'tense', 'pov', 'narrative'] },
],
},
],
},
];
export const SETTINGS_PAGES = SETTINGS_GROUPS.flatMap((group) =>
group.pages.map((page) => ({ ...page, group: group.title }))
);
export function settingsPage(pageId) {
return SETTINGS_PAGES.find((page) => page.id === pageId) || null;
}
// Maps legacy openAppConfig(tab, page, item) arguments — still used by backend
// uxActions and older callers — onto the flat page structure.
// Returns { page, anchor, item }.
export function mapLegacyLocation(tab, page, item = null) {
if (!tab) {
return { page: null, anchor: null, item: null };
}
switch (tab) {
case 'game':
return { page: page === 'character' ? 'player-character' : 'gameplay', anchor: null, item: null };
case 'appearance':
return { page: page === 'assets' ? 'appearance-visuals' : 'appearance-messages', anchor: null, item: null };
case 'application':
if (page === 'env_variables') {
return { page: 'env-variables', anchor: null, item: null };
}
return { page: 'api-keys', anchor: page || null, item: null };
case 'presets':
if (page === 'embeddings') {
return { page: 'presets-embeddings', anchor: null, item: item };
}
if (page === 'system_prompts') {
return { page: 'presets-system-prompts', anchor: null, item: item };
}
return { page: 'presets-inference', anchor: null, item: item };
case 'creator':
return { page: page === 'perspective_presets' ? 'perspective-presets' : 'content-classification', anchor: null, item: null };
case 'settings':
// already a new-style page id
return { page: page || null, anchor: null, item: item };
default:
return { page: null, anchor: null, item: null };
}
}
// Search over pages and their entries. Returns [{page, entry|null}] where a
// null entry means the page itself matched.
export function searchSettings(query) {
const q = (query || '').trim().toLowerCase();
if (!q) {
return [];
}
const results = [];
for (const page of SETTINGS_PAGES) {
const pageHaystack = `${page.title} ${page.group} ${page.description || ''}`.toLowerCase();
const matchedEntries = (page.entries || []).filter((entry) => {
const haystack = `${entry.label} ${(entry.keywords || []).join(' ')}`.toLowerCase();
return haystack.includes(q);
});
if (matchedEntries.length > 0) {
for (const entry of matchedEntries) {
results.push({ page, entry });
}
} else if (pageHaystack.includes(q)) {
results.push({ page, entry: null });
}
}
return results;
}

View File

@@ -948,14 +948,21 @@ async def test_update_agent_setting_flags(settings_env):
@pytest.mark.asyncio
async def test_update_app_config_refused_while_app_settings_open(settings_env):
async def test_update_app_config_refused_while_app_settings_dirty(settings_env):
result = await help_settings.update_app_config(
"game.general.auto_save", False, open_app_settings=True
"game.general.auto_save", False, app_settings_dirty=True
)
assert isinstance(result, str)
assert "application settings dialog" in result
assert "unsaved edits" in result
assert get_config().game.general.auto_save is True
# a merely open (clean) settings view does not block the write
result = await help_settings.update_app_config(
"game.general.auto_save", False, app_settings_dirty=False
)
assert result["applied"] is True
assert get_config().game.general.auto_save is False
def test_read_clients_unified_api_key(settings_env):
"""Clients resolving their key from an app-level unified path must not