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
@@ -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."
|
||||
|
||||
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 106 KiB |
BIN
docs/img/0.39.0/api-keys-anthropic.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
docs/img/0.39.0/api-keys-cohere.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
docs/img/0.39.0/api-keys-elevenlabs.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
docs/img/0.39.0/api-keys-google.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
docs/img/0.39.0/api-keys-groq.png
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
docs/img/0.39.0/api-keys-mistral.png
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
docs/img/0.39.0/api-keys-openai.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
docs/img/0.39.0/api-keys-openrouter.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
docs/img/0.39.0/app-settings-api-keys.png
Normal file
|
After Width: | Height: | Size: 102 KiB |
BIN
docs/img/0.39.0/app-settings-appearance-messages.png
Normal file
|
After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 108 KiB |
BIN
docs/img/0.39.0/app-settings-creator-content-classification.png
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
docs/img/0.39.0/app-settings-creator-perspective-presets.png
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
docs/img/0.39.0/app-settings-env-variables.png
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
docs/img/0.39.0/app-settings-gameplay.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
docs/img/0.39.0/app-settings-player-character.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
docs/img/0.39.0/app-settings-presets-embeddings.png
Normal file
|
After Width: | Height: | Size: 113 KiB |
BIN
docs/img/0.39.0/app-settings-presets-inference.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
docs/img/0.39.0/app-settings-presets-system-prompts.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
docs/img/0.39.0/app-settings-search.png
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
docs/img/0.39.0/inference-presets-1.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
docs/img/0.39.0/inference-presets-custom-group-1.png
Normal file
|
After Width: | Height: | Size: 99 KiB |
BIN
docs/img/0.39.0/inference-presets-custom-group-2.png
Normal file
|
After Width: | Height: | Size: 102 KiB |
@@ -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
|
||||
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
|
||||
You can manage your available embeddings through the application settings.
|
||||
|
||||

|
||||
|
||||
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).
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
Then click `Save` to save your changes.
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
Then click `Save` to save your changes.
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
Click `Save` to save your changes.
|
||||
|
||||
|
||||
@@ -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`.
|
||||
|
||||

|
||||
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.
|
||||
|
||||

|
||||

|
||||
|
||||
Then click `Save` to save your changes.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
Then click `Save` to save your changes.
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
Then click `Save` to save your changes.
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
Then click `Save` to save your changes.
|
||||
@@ -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.
|
||||
|
||||

|
||||

|
||||
|
||||
Finally click **Save** to store the credentials.
|
||||
@@ -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
|
||||
|
||||

|
||||

|
||||
|
||||
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:
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
# :material-application-outline: Application
|
||||
# :material-connection: Connections
|
||||
|
||||

|
||||
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.
|
||||

|
||||
|
||||
## 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"
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||
@@ -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
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||

|
||||
|
||||
### Adding a preset
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
The **Game** group holds general game behavior and the default player character.
|
||||
|
||||
## :material-cog: General
|
||||
|
||||

|
||||

|
||||
|
||||
##### 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
|
||||
|
||||

|
||||
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
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||

|
||||
|
||||
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
|
||||
|
||||

|
||||

|
||||
|
||||
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
|
||||
|
||||

|
||||

|
||||
|
||||
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).
|
||||
|
||||
|
||||
@@ -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-level overrides apply to every client unless that client has its own override for the same prompt kind.
|
||||
|
||||
|
||||
@@ -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 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.
|
||||
|
||||

|
||||

|
||||
|
||||
!!! 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`.
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
The new group will be added and automatically selected for editing.
|
||||
|
||||

|
||||

|
||||
|
||||
Once you have adjusted the presets to your liking you can save the group by clicking the :material-content-save: **Save** button.
|
||||
|
||||
|
||||
@@ -12,11 +12,11 @@ The client should appear in the clients list. If you haven't setup Anthropic bef
|
||||
|
||||

|
||||
|
||||
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/).
|
||||
|
||||

|
||||

|
||||
|
||||
Click `Save` and after a moment the client should have a green dot next to it, indicating that it is ready to go.
|
||||
|
||||
|
||||
@@ -12,11 +12,11 @@ The client should appear in the clients list. If you haven't setup Cohere before
|
||||
|
||||

|
||||
|
||||
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/).
|
||||
|
||||

|
||||

|
||||
|
||||
Click `Save` and after a moment the client should have a green dot next to it, indicating that it is ready to go.
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ The client should appear in the clients list. If you haven't set up DeepSeek bef
|
||||
|
||||

|
||||
|
||||
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/).
|
||||
|
||||
|
||||
@@ -12,11 +12,11 @@ The client should appear in the clients list. If you haven't setup Google before
|
||||
|
||||

|
||||
|
||||
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/).
|
||||
|
||||

|
||||

|
||||
|
||||
Click `Save` and after a moment the client should have a green dot next to it, indicating that it is ready to go.
|
||||
|
||||
|
||||
@@ -12,11 +12,11 @@ The client should appear in the clients list. If you haven't setup Groq before,
|
||||
|
||||

|
||||
|
||||
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/).
|
||||
|
||||

|
||||

|
||||
|
||||
Click `Save` and after a moment the client should have a green dot next to it, indicating that it is ready to go.
|
||||
|
||||
|
||||
@@ -12,11 +12,11 @@ The client should appear in the clients list. If you haven't setup Mistral.ai be
|
||||
|
||||

|
||||
|
||||
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/).
|
||||
|
||||

|
||||

|
||||
|
||||
Click `Save` and after a moment the client should have a green dot next to it, indicating that it is ready to go.
|
||||
|
||||
|
||||
@@ -12,11 +12,11 @@ The client should appear in the clients list. If you haven't setup OpenAI before
|
||||
|
||||

|
||||
|
||||
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/).
|
||||
|
||||

|
||||

|
||||
|
||||
Click `Save` and after a moment the client should have a green dot next to it, indicating that it is ready to go.
|
||||
|
||||
|
||||
@@ -12,11 +12,11 @@ The client should appear in the clients list. If you haven't set up OpenRouter b
|
||||
|
||||

|
||||
|
||||
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/).
|
||||
|
||||

|
||||

|
||||
|
||||
Click `Save` and after a moment the client should have a red dot next to it, saying that there is no model loaded.
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
Then click the `APPLICATION` tab and then the `RUNPOD` category. Here you can add your RunPod API key.
|
||||
|
||||

|
||||
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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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 1 – API Key (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 2 – Vertex AI service account (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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
401
talemate_frontend/src/components/AppSettings.vue
Normal 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>
|
||||
86
talemate_frontend/src/components/AppSettingsApiKeys.vue
Normal 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 1 – API Key (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 2 – Vertex AI service account (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>
|
||||
100
talemate_frontend/src/components/AppSettingsEnvVariables.vue
Normal 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>
|
||||
44
talemate_frontend/src/components/AppSettingsGameplay.vue
Normal 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>
|
||||
85
talemate_frontend/src/components/AppSettingsMenu.vue
Normal 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>
|
||||
19
talemate_frontend/src/components/AppSettingsPageHeader.vue
Normal 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>
|
||||
@@ -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>
|
||||
55
talemate_frontend/src/components/AppSettingsStringList.vue
Normal 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>
|
||||
@@ -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;
|
||||
|
||||
313
talemate_frontend/src/utils/appSettingsRegistry.js
Normal 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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||