Compare commits

...

12 Commits

Author SHA1 Message Date
Dominic Neuburg
fc53278c60
Focus message composer when it becomes available (#822)
Some checks failed
Build and Release / Determine run mode (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Has been cancelled
Build and Release / Prepare & create release (push) Has been cancelled
Build and Release / Read metadata (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Has been cancelled
Build and Release / Publish release (push) Has been cancelled
2026-06-23 09:02:40 +02:00
Thorsten Sommer
2acb6f2a57
Updated README.md with v26.6.2 release details (#820)
Some checks failed
Build and Release / Determine run mode (push) Has been cancelled
Build and Release / Read metadata (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Has been cancelled
Build and Release / Prepare & create release (push) Has been cancelled
Build and Release / Publish release (push) Has been cancelled
2026-06-21 20:44:44 +02:00
Thorsten Sommer
5af616f565
Enhanced settings manager with versioned backups and migrations (#819)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-06-21 18:46:21 +02:00
Thorsten Sommer
6d48252db3
Prepared release v26.6.2 (#818)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-06-21 16:19:17 +02:00
Thorsten Sommer
64e91ff4ff
Added support for organization-managed chat defaults (#817)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-06-21 15:59:23 +02:00
Thorsten Sommer
dddb40096d
Added support for organization-trusted providers (#816)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-06-21 15:16:37 +02:00
Thorsten Sommer
e65110a142
Added support for organization-managed provider confidence settings (#815)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-06-21 11:52:02 +02:00
Thorsten Sommer
5045da3a91
Added support for organization-managed introduction texts (#814)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-06-20 20:28:22 +02:00
Thorsten Sommer
e04879fd7f
Added a read-only view for managed profiles and chat templates (#813)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-06-20 17:06:43 +02:00
Thorsten Sommer
fc7197ec93
Added compatibility shim for the Qdrant Edge migration (#812)
Some checks are pending
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2026-06-20 16:11:24 +02:00
Thorsten Sommer
c3bf2563cd
Fixed self-hosted provider API key handling (#811)
Some checks are pending
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Publish release (push) Blocked by required conditions
2026-06-20 15:55:09 +02:00
Thorsten Sommer
24952e796e
Updated README.md (#810)
Some checks failed
Build and Release / Determine run mode (push) Has been cancelled
Build and Release / Read metadata (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Has been cancelled
Build and Release / Prepare & create release (push) Has been cancelled
Build and Release / Publish release (push) Has been cancelled
2026-06-11 21:06:48 +02:00
105 changed files with 2628 additions and 731 deletions

View File

@ -112,12 +112,16 @@ Plugins can configure:
- Chat templates
- etc.
When adding configuration options, update:
- `app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs`: In method `TryProcessConfiguration` register new options.
- `app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs`: In method `LoadAll` check for leftover configuration.
- The corresponding data class in `app/MindWork AI Studio/Settings/DataModel/` to call `ManagedConfiguration.Register(...)`, when adding config options (in contrast to complex config. objects)
- `app/MindWork AI Studio/Tools/PluginSystem/PluginConfigurationObject.cs` for parsing logic of complex configuration objects.
- `app/MindWork AI Studio/Plugins/configuration/plugin.lua` to document the new configuration option.
Configuration plugins provide three kinds of values:
- **Managed settings:** simple values such as booleans, numbers, strings, enums, lists, or sets handled through `ManagedConfiguration`. These values may be locked or used as organization defaults.
- **Managed configuration objects:** complex Lua tables that are persisted into `SettingsManager.ConfigurationData`, implement `IConfigurationObject`, and are cleaned up through `PluginConfigurationObject.CleanLeftOverConfigurationObjects(...)`. Examples include providers, profiles, chat templates, data sources, and document analysis policies.
- **Live plugin content:** complex Lua tables that implement `ILivePluginContent` and are read live from running plugins instead of being persisted to `ConfigurationData`. Examples include `MANDATORY_INFOS` and `INTRODUCTIONS`. If live plugin content creates persistent side data, add a dedicated cleanup path for that side data, like mandatory-info acceptances.
When adding configuration plugin capabilities:
- For managed settings, update the corresponding data class in `app/MindWork AI Studio/Settings/DataModel/` to call `ManagedConfiguration.Register(...)`, process the setting in `PluginConfiguration.TryProcessConfiguration`, and check for leftover managed configuration in `PluginFactory.Loading.LoadAll`.
- For managed configuration objects, update `PluginConfigurationObject.cs` and `PluginConfigurationObjectType.cs`, persist them in the appropriate `ConfigurationData` collection, and add cleanup via `PluginConfigurationObject.CleanLeftOverConfigurationObjects(...)`.
- For live plugin content, add a data type implementing `ILivePluginContent`, parse it in `PluginConfiguration`, expose it through `PluginFactory`, and add any required cleanup only for persistent side data.
- Always document the new capability in `app/MindWork AI Studio/Plugins/configuration/plugin.lua`.
## RAG (Retrieval-Augmented Generation)
@ -196,6 +200,7 @@ Multi-level confidence scheme allows users to control which providers see which
- **Encryption** - Initialized before Rust service is marked ready
- **Message Bus** - Singleton event bus for cross-component communication inside the .NET app
- **Naming conventions** - Constants, enum members, and `static readonly` fields use `UPPER_SNAKE_CASE` such as `MY_CONSTANT`.
- **Compatibility shims** - Temporary fallback or read-repair code must be documented in `documentation/compatibility-shims/` with an introduced date, remove-after date, code references, and removal checklist. Add a short code comment near the shim that references the document and remove-after date. Check this folder before adding similar fallback logic, and do not extend expired shims without explicit maintainer direction. Do not use this process for permanent settings schema migrations; those belong in `app/MindWork AI Studio/Settings/SettingsMigrations.cs`.
- **Empty lines** - Avoid adding extra empty lines at the end of files.
## Changelogs

View File

@ -78,6 +78,8 @@ Since March 2025: We have started developing the plugin system. There will be la
</h3>
</summary>
- v26.6.2: Expanded enterprise configuration options with chat defaults, custom introduction panels, trust settings for data security, and managed confidence levels; added auto-backups for app settings & the possibility to view managed profiles and chat templates.
- v26.6.1: Increased enterprise configuration capacity for large organizations, broader Flatpak deployment support, startup and Linux package diagnostics, chat search across all workspaces, improved workspace workflows, better model discovery for self-hosted llama.cpp providers, and fixes for profile and chat template updates, workspace naming, and startup behavior.
- v26.5.5: Released voice recording and transcription for all users; added support for multiple chats running at the same time, export options for profiles, chat templates, and ERI data sources, organization-managed ERI servers, and configurable request timeouts; upgraded the native runtime to Tauri v2.
- v26.4.1: Added support for the latest AI models, assistant plugins, a slide planner assistant, a prompt optimization assistant, math rendering in chats, and a configurable start page; released the document analysis assistant and improved enterprise deployment, chat performance, file attachments, and reliability across voice recording, logging, and provider validation.
- v26.2.2: Added Qdrant as a building block for our local RAG preview, added an embedding test option to validate embedding providers, and improved enterprise and configuration plugins with preselected providers, additive preview features, support for multiple configurations, and more reliable synchronization.
@ -88,8 +90,6 @@ Since March 2025: We have started developing the plugin system. There will be la
- v0.9.46: Released our plugin system, a German language plugin, early support for enterprise environments, and configuration plugins. Additionally, we added the Pandoc integration for future data processing and file generation.
- v0.9.45: Added chat templates to AI Studio, allowing you to create and use a library of system prompts for your chats.
- v0.9.44: Added PDF import to the text summarizer, translation, and legal check assistants, allowing you to import PDF files and use them as input for the assistants.
- v0.9.40: Added support for the `o4` models from OpenAI. Also, we added Alibaba Cloud & Hugging Face as LLM providers.
- v0.9.39: Added the plugin system as a preview feature.
</details>

View File

@ -53,6 +53,9 @@ public sealed partial class CollectI18NKeysCommand
foreach (var filePath in allFiles)
{
counter++;
if(!this.IsSupportedSourceFile(filePath))
continue;
if(filePath.StartsWith(binPath, StringComparison.OrdinalIgnoreCase))
continue;
@ -68,6 +71,9 @@ public sealed partial class CollectI18NKeysCommand
continue;
var ns = this.DetermineNamespace(filePath);
if(ns is null)
throw new InvalidOperationException($"Could not determine the namespace for I18N source file '{filePath}'.");
var fileInfo = new FileInfo(filePath);
var name = this.DetermineTypeName(filePath)
@ -204,6 +210,10 @@ public sealed partial class CollectI18NKeysCommand
return matches;
}
private bool IsSupportedSourceFile(string filePath) =>
filePath.EndsWith(".cs", StringComparison.OrdinalIgnoreCase) ||
filePath.EndsWith(".razor", StringComparison.OrdinalIgnoreCase);
private string? DetermineNamespace(string filePath)
{
@ -302,10 +312,10 @@ public sealed partial class CollectI18NKeysCommand
return match.Groups[1].Value;
}
[GeneratedRegex("""@namespace\s+([a-zA-Z0-9_.]+)""")]
[GeneratedRegex("""(?m)^\s*@namespace\s+([a-zA-Z0-9_.]+)""")]
private static partial Regex BlazorNamespaceRegex();
[GeneratedRegex("""namespace\s+([a-zA-Z0-9_.]+)""")]
[GeneratedRegex("""(?m)^\s*namespace\s+([a-zA-Z0-9_.]+)\s*[;{]""")]
private static partial Regex CSharpNamespaceRegex();
[GeneratedRegex("""\bpartial\s+(?:class|struct|interface|record(?:\s+(?:class|struct))?)\s+([A-Za-z_][A-Za-z0-9_]*)""")]

View File

@ -153,7 +153,7 @@
</MudButton>
}
@if (this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence)
@if (this.SettingsManager.ConfigurationData.Confidence.ShowProviderConfidence)
{
<ConfidenceInfo Mode="PopoverTriggerMode.BUTTON" LLMProvider="@this.ProviderSettings.UsedLLMProvider"/>
}

View File

@ -174,7 +174,7 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
private string TB(string fallbackEN) => this.T(fallbackEN, typeof(AssistantBase<TSettings>).Namespace, nameof(AssistantBase<TSettings>));
private string SubmitButtonStyle => this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence ? this.ProviderSettings.UsedLLMProvider.GetConfidence(this.SettingsManager).StyleBorder(this.SettingsManager) : string.Empty;
private string SubmitButtonStyle => this.SettingsManager.ConfigurationData.Confidence.ShowProviderConfidence ? this.ProviderSettings.UsedLLMProvider.GetConfidence(this.SettingsManager).StyleBorder(this.SettingsManager) : string.Empty;
private IReadOnlyList<Tools.Components> VisibleSendToAssistants => Enum.GetValues<AIStudio.Tools.Components>()
.Where(this.CanSendToAssistant)

View File

@ -439,10 +439,10 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
private ConfidenceLevel GetPolicyMinimumConfidenceLevel()
{
var minimumLevel = ConfidenceLevel.NONE;
var llmSettings = this.SettingsManager.ConfigurationData.LLMProviders;
var enforceGlobalMinimumConfidence = llmSettings is { EnforceGlobalMinimumConfidence: true, GlobalMinimumConfidence: not ConfidenceLevel.NONE and not ConfidenceLevel.UNKNOWN };
var confidenceSettings = this.SettingsManager.ConfigurationData.Confidence;
var enforceGlobalMinimumConfidence = confidenceSettings is { EnforceGlobalMinimumConfidence: true, GlobalMinimumConfidence: not ConfidenceLevel.NONE and not ConfidenceLevel.UNKNOWN };
if (enforceGlobalMinimumConfidence)
minimumLevel = llmSettings.GlobalMinimumConfidence;
minimumLevel = confidenceSettings.GlobalMinimumConfidence;
if (this.selectedPolicy is not null && this.selectedPolicy.MinimumProviderConfidence > minimumLevel)
minimumLevel = this.selectedPolicy.MinimumProviderConfidence;

View File

@ -2791,6 +2791,54 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T922066419"]
-- Administration settings are not visible
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T929143445"] = "Administration settings are not visible"
-- Show provider's confidence level?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T1052533048"] = "Show provider's confidence level?"
-- Choose the scheme that best suits you and your organization. Do you trust any western provider? Or only providers from the USA or exclusively European providers? Then choose the appropriate scheme. Alternatively, you can assign the confidence levels to each provider yourself.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T1081931329"] = "Choose the scheme that best suits you and your organization. Do you trust any western provider? Or only providers from the USA or exclusively European providers? Then choose the appropriate scheme. Alternatively, you can assign the confidence levels to each provider yourself."
-- Provider Confidence
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T1453422580"] = "Provider Confidence"
-- When enabled, you can enforce a minimum confidence level for all features in AI Studio. This way, you can make sure only trustworthy providers are used.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T1499004705"] = "When enabled, you can enforce a minimum confidence level for all features in AI Studio. This way, you can make sure only trustworthy providers are used."
-- When enabled, we show you the confidence level for the selected provider in the app. This helps you assess where you are sending your data at any time. Example: are you currently working with sensitive data? Then choose a particularly trustworthy provider, etc.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T1505516304"] = "When enabled, we show you the confidence level for the selected provider in the app. This helps you assess where you are sending your data at any time. Example: are you currently working with sensitive data? Then choose a particularly trustworthy provider, etc."
-- No, please hide the confidence level
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T1628475119"] = "No, please hide the confidence level"
-- Description
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T1725856265"] = "Description"
-- Confidence Level
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T2492230131"] = "Confidence Level"
-- No, do not enforce a minimum confidence level
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T3642102079"] = "No, do not enforce a minimum confidence level"
-- Select a confidence scheme
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T4144206465"] = "Select a confidence scheme"
-- Do you want to enforce an global minimum confidence level?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T4211873175"] = "Do you want to enforce an global minimum confidence level?"
-- Yes, enforce a minimum confidence level
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T458854917"] = "Yes, enforce a minimum confidence level"
-- Not yet configured
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T48051324"] = "Not yet configured"
-- Do you want to always see how trustworthy your providers are? This way, you stay in control of which provider you send your data to. You can choose a common schema or configure the trust levels for each provider yourself.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T700839804"] = "Do you want to always see how trustworthy your providers are? This way, you stay in control of which provider you send your data to. You can choose a common schema or configure the trust levels for each provider yourself."
-- Yes, show me the confidence level
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T853225204"] = "Yes, show me the confidence level"
-- Provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T900237532"] = "Provider"
-- Embedding Result
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1387042335"] = "Embedding Result"
@ -2845,6 +2893,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T32678
-- Close
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3448155331"] = "Close"
-- This embedding provider is trusted by your organization for data source security checks. Local data can be sent to it without security warnings.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3459188215"] = "This embedding provider is trusted by your organization for data source security checks. Local data can be sent to it without security warnings."
-- Actions
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3865031940"] = "Actions"
@ -2887,21 +2938,12 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERBASE::T336
-- Export API Key?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERBASE::T4010580285"] = "Export API Key?"
-- Show provider's confidence level?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T1052533048"] = "Show provider's confidence level?"
-- This provider is trusted by your organization for data source security checks.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T1298650849"] = "This provider is trusted by your organization for data source security checks."
-- Delete
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T1469573738"] = "Delete"
-- When enabled, we show you the confidence level for the selected provider in the app. This helps you assess where you are sending your data at any time. Example: are you currently working with sensitive data? Then choose a particularly trustworthy provider, etc.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T1505516304"] = "When enabled, we show you the confidence level for the selected provider in the app. This helps you assess where you are sending your data at any time. Example: are you currently working with sensitive data? Then choose a particularly trustworthy provider, etc."
-- No, please hide the confidence level
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T1628475119"] = "No, please hide the confidence level"
-- Description
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T1725856265"] = "Description"
-- Uses the provider-configured model
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T1760715963"] = "Uses the provider-configured model"
@ -2917,27 +2959,12 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T186876
-- Are you sure you want to delete the provider '{0}'?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T2031310917"] = "Are you sure you want to delete the provider '{0}'?"
-- Do you want to always be able to recognize how trustworthy your LLM providers are? This way, you keep control over which provider you send your data to. You have two options for this: Either you choose a common schema, or you configure the trust levels for each LLM provider yourself.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T2082904277"] = "Do you want to always be able to recognize how trustworthy your LLM providers are? This way, you keep control over which provider you send your data to. You have two options for this: Either you choose a common schema, or you configure the trust levels for each LLM provider yourself."
-- Model
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T2189814010"] = "Model"
-- Choose the scheme that best suits you and your life. Do you trust any western provider? Or only providers from the USA or exclusively European providers? Then choose the appropriate scheme. Alternatively, you can assign the confidence levels to each provider yourself.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T2283885378"] = "Choose the scheme that best suits you and your life. Do you trust any western provider? Or only providers from the USA or exclusively European providers? Then choose the appropriate scheme. Alternatively, you can assign the confidence levels to each provider yourself."
-- LLM Provider Confidence
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T2349972795"] = "LLM Provider Confidence"
-- What we call a provider is the combination of an LLM provider such as OpenAI and a model like GPT-4o. You can configure as many providers as you want. This way, you can use the appropriate model for each task. As an LLM provider, you can also choose local providers. However, to use this app, you must configure at least one provider.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T2460361126"] = "What we call a provider is the combination of an LLM provider such as OpenAI and a model like GPT-4o. You can configure as many providers as you want. This way, you can use the appropriate model for each task. As an LLM provider, you can also choose local providers. However, to use this app, you must configure at least one provider."
-- Confidence Level
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T2492230131"] = "Confidence Level"
-- When enabled, you can enforce a minimum confidence level for all LLM providers. This way, you can ensure that only trustworthy providers are used.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T281063702"] = "When enabled, you can enforce a minimum confidence level for all LLM providers. This way, you can ensure that only trustworthy providers are used."
-- Instance Name
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T2842060373"] = "Instance Name"
@ -2959,36 +2986,15 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T334643
-- This provider is managed by your organization.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T3415927576"] = "This provider is managed by your organization."
-- LLM Provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T3612415205"] = "LLM Provider"
-- No, do not enforce a minimum confidence level
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T3642102079"] = "No, do not enforce a minimum confidence level"
-- Actions
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T3865031940"] = "Actions"
-- Select a confidence scheme
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T4144206465"] = "Select a confidence scheme"
-- Do you want to enforce an app-wide minimum confidence level?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T4258968041"] = "Do you want to enforce an app-wide minimum confidence level?"
-- Delete LLM Provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T4269256234"] = "Delete LLM Provider"
-- Yes, enforce a minimum confidence level
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T458854917"] = "Yes, enforce a minimum confidence level"
-- Not yet configured
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T48051324"] = "Not yet configured"
-- Open Dashboard
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T78223861"] = "Open Dashboard"
-- Yes, show me the confidence level
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T853225204"] = "Yes, show me the confidence level"
-- Provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T900237532"] = "Provider"
@ -3037,6 +3043,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T42
-- With the support of transcription models, MindWork AI Studio can convert human speech into text. This is useful, for example, when you need to dictate text. You can choose from dedicated transcription models, but not multimodal LLMs (large language models) that can handle both speech and text. The configuration of multimodal models is done in the 'Configure providers' section.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T584860404"] = "With the support of transcription models, MindWork AI Studio can convert human speech into text. This is useful, for example, when you need to dictate text. You can choose from dedicated transcription models, but not multimodal LLMs (large language models) that can handle both speech and text. The configuration of multimodal models is done in the 'Configure providers' section."
-- This transcription provider is trusted by your organization for data source security checks.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T601264181"] = "This transcription provider is trusted by your organization for data source security checks."
-- This transcription provider is managed by your organization.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T756131076"] = "This transcription provider is managed by your organization."
@ -3514,6 +3523,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3227981830"] = "Using s
-- Add a message
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3372872324"] = "Add a message"
-- Close
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3448155331"] = "Close"
-- Unsupported content type
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3570316759"] = "Unsupported content type"
@ -4294,6 +4306,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROFILEDIALOG::T3243902394"] = "The profile
-- Profile Name
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROFILEDIALOG::T3392578705"] = "Profile Name"
-- Close
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROFILEDIALOG::T3448155331"] = "Close"
-- Please enter what the LLM should know about you and/or what actions it should take.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROFILEDIALOG::T3708405102"] = "Please enter what the LLM should know about you and/or what actions it should take."
@ -4813,6 +4828,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T14695
-- Add Chat Template
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1548314416"] = "Add Chat Template"
-- View
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1582017048"] = "View"
-- Note: This advanced feature is designed for users familiar with prompt engineering concepts. Furthermore, you have to make sure yourself that your chosen provider supports the use of assistant prompts.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1909110760"] = "Note: This advanced feature is designed for users familiar with prompt engineering concepts. Furthermore, you have to make sure yourself that your chosen provider supports the use of assistant prompts."
@ -4852,6 +4870,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T38650
-- Delete Chat Template
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T4025180906"] = "Delete Chat Template"
-- View Chat Template
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T4042112076"] = "View Chat Template"
-- Export Chat Template
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T491504763"] = "Export Chat Template"
@ -5260,6 +5281,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T143353473
-- Delete
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T1469573738"] = "Delete"
-- View
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T1582017048"] = "View"
-- Your Profiles
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T2378610256"] = "Your Profiles"
@ -5284,6 +5308,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T405841465
-- Store personal data about yourself in various profiles so that the AIs know your personal context. This saves you from having to explain your context each time, for example, in every chat. When you have different roles, you can create a profile for each role.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T4125557797"] = "Store personal data about yourself in various profiles so that the AIs know your personal context. This saves you from having to explain your context each time, for example, in every chat. When you have different roles, you can create a profile for each role."
-- View Profile
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T4219233997"] = "View Profile"
-- Add Profile
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T4248067241"] = "Add Profile"
@ -5815,12 +5842,21 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T3288132732"] = "P
-- Cancel
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T900713019"] = "Cancel"
-- Reason
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1093747001"] = "Reason"
-- Settings
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1258653480"] = "Settings"
-- Your settings file does not contain a settings-format version. Changes in this session will not be saved to avoid overwriting your settings. Please check for updates or contact support.
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1378304679"] = "Your settings file does not contain a settings-format version. Changes in this session will not be saved to avoid overwriting your settings. Please check for updates or contact support."
-- Home
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1391791790"] = "Home"
-- AI Studio found the current settings format but could not load it safely. Changes in this session will not be saved. Please check for updates or contact support.
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1497084127"] = "AI Studio found the current settings format but could not load it safely. Changes in this session will not be saved. Please check for updates or contact support."
-- Are you sure you want to leave the chat page? All unsaved changes will be lost.
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1563130494"] = "Are you sure you want to leave the chat page? All unsaved changes will be lost."
@ -5830,12 +5866,21 @@ UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1614176092"] = "Assistants"
-- Update
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1847791252"] = "Update"
-- Check for updates
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1890416390"] = "Check for updates"
-- Your settings were created by a newer AI Studio version. Changes in this session will not be saved. Please install or start the latest available update.
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1988273622"] = "Your settings were created by a newer AI Studio version. Changes in this session will not be saved. Please install or start the latest available update."
-- Leave Chat Page
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2124749705"] = "Leave Chat Page"
-- Plugins
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2222816203"] = "Plugins"
-- AI Studio cannot safely save settings in this session. Please check for updates or contact support.
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2382622618"] = "AI Studio cannot safely save settings in this session. Please check for updates or contact support."
-- An update to version {0} is available.
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2800137365"] = "An update to version {0} is available."
@ -5845,6 +5890,9 @@ UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2864211629"] = "Please wait for
-- Supporters
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2929332068"] = "Supporters"
-- AI Studio could not read your settings file. Changes in this session will not be saved to avoid overwriting recoverable settings. Please check for updates or contact support.
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2936083926"] = "AI Studio could not read your settings file. Changes in this session will not be saved to avoid overwriting recoverable settings. Please check for updates or contact support."
-- Writer
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2979224202"] = "Writer"
@ -5857,6 +5905,9 @@ UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T4256323669"] = "Information"
-- Chat
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T578410699"] = "Chat"
-- AI Studio does not recognize your settings-format version. Changes in this session will not be saved to avoid overwriting your settings. Please check for updates or contact support.
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T915412625"] = "AI Studio does not recognize your settings-format version. Changes in this session will not be saved to avoid overwriting your settings. Please check for updates or contact support."
-- Get coding and debugging support from an LLM.
UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1243850917"] = "Get coding and debugging support from an LLM."
@ -6040,6 +6091,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T144565305"] = "The app requires minimal
-- You only pay for what you use, which can be cheaper than monthly subscription services like ChatGPT Plus, especially if used infrequently. But beware, here be dragons: For extremely intensive usage, the API costs can be significantly higher. Unfortunately, providers currently do not offer a way to display current costs in the app. Therefore, check your account with the respective provider to see how your costs are developing. When available, use prepaid and set a cost limit.
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T149711988"] = "You only pay for what you use, which can be cheaper than monthly subscription services like ChatGPT Plus, especially if used infrequently. But beware, here be dragons: For extremely intensive usage, the API costs can be significantly higher. Unfortunately, providers currently do not offer a way to display current costs in the app. Therefore, check your account with the respective provider to see how your costs are developing. When available, use prepaid and set a cost limit."
-- Version
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1573770551"] = "Version"
-- Assistants
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1614176092"] = "Assistants"
@ -7870,6 +7924,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T25964655
-- Failed to store the secret data due to an API issue.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T1110203516"] = "Failed to store the secret data due to an API issue."
-- Failed to store the API key due to an API issue.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T1704298921"] = "Failed to store the API key due to an API issue."
-- Failed to delete the secret data due to an API issue.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T2303057928"] = "Failed to delete the secret data due to an API issue."

View File

@ -1,4 +1,5 @@
using AIStudio.Provider.SelfHosted;
using AIStudio.Provider;
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
namespace AIStudio.Chat;
@ -33,12 +34,13 @@ public static class ChatThreadExtensions
return true;
//
// Is the provider self-hosted?
// Is the provider trusted for data-source security checks?
//
var isSelfHostedProvider = provider switch
var settingsManager = Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>();
var isTrustedProvider = provider switch
{
ProviderSelfHosted => true,
AIStudio.Settings.Provider p => p.IsSelfHosted,
IProvider p => p.IsTrustedForDataSourceSecurityChecks(settingsManager),
AIStudio.Settings.Provider p => p.IsTrustedForDataSourceSecurityChecks(settingsManager),
_ => false,
};
@ -46,12 +48,12 @@ public static class ChatThreadExtensions
//
// Check the chat data security against the selected provider:
//
return isSelfHostedProvider switch
return isTrustedProvider switch
{
// The provider is self-hosted -- we can use any data source:
// The provider is trusted -- we can use any data source:
true => true,
// The provider is not self-hosted -- it depends on the data security of the chat thread:
// The provider is not trusted -- it depends on the data security of the chat thread:
false => chatThread.DataSecurity is not DataSourceSecurity.SELF_HOSTED,
};
}

View File

@ -52,29 +52,42 @@
}
else
{
<MudStack Row="true" AlignItems="AlignItems.Center" StretchItems="StretchItems.None" Wrap="Wrap.Wrap">
<MudText Typo="Typo.body1" Inline="true">
@T("Drag and drop files into the marked area or click here to attach documents: ")
</MudText>
<MudButton
Variant="Variant.Filled"
StartIcon="@Icons.Material.Filled.Add"
Color="Color.Primary"
OnClick="@(() => this.AddFilesManually())"
Style="vertical-align: top; margin-top: -2px;"
Size="Size.Small">
@T("Add file")
</MudButton>
</MudStack>
@if (!this.Disabled)
{
<MudStack Row="true" AlignItems="AlignItems.Center" StretchItems="StretchItems.None" Wrap="Wrap.Wrap">
<MudText Typo="Typo.body1" Inline="true">
@T("Drag and drop files into the marked area or click here to attach documents: ")
</MudText>
<MudButton
Variant="Variant.Filled"
StartIcon="@Icons.Material.Filled.Add"
Color="Color.Primary"
OnClick="@(() => this.AddFilesManually())"
Style="vertical-align: top; margin-top: -2px;"
Size="Size.Small">
@T("Add file")
</MudButton>
</MudStack>
}
<div @onmouseenter="@this.OnMouseEnter" @onmouseleave="@this.OnMouseLeave">
<MudPaper Height="20em" Outlined="true" Class="@this.dragClass" Style="overflow-y: auto;">
@foreach (var fileAttachment in this.DocumentPaths)
{
<MudChip T="string" Color="Color.Dark" Text="@fileAttachment.FileName" tabindex="-1" Icon="@Icons.Material.Filled.Search" OnClick="@(() => this.InvestigateFile(fileAttachment))" OnClose="@(() => this.RemoveDocument(fileAttachment))"/>
@if (this.Disabled)
{
<MudChip T="string" Color="Color.Dark" Text="@fileAttachment.FileName" tabindex="-1" Icon="@Icons.Material.Filled.Search" OnClick="@(() => this.InvestigateFile(fileAttachment))"/>
}
else
{
<MudChip T="string" Color="Color.Dark" Text="@fileAttachment.FileName" tabindex="-1" Icon="@Icons.Material.Filled.Search" OnClick="@(() => this.InvestigateFile(fileAttachment))" OnClose="@(() => this.RemoveDocument(fileAttachment))"/>
}
}
</MudPaper>
</div>
<MudButton OnClick="@(async () => await this.ClearAllFiles())" Variant="Variant.Filled" Color="Color.Info" Class="mt-2" StartIcon="@Icons.Material.Filled.Delete">
@T("Clear file list")
</MudButton>
@if (!this.Disabled)
{
<MudButton OnClick="@(async () => await this.ClearAllFiles())" Variant="Variant.Filled" Color="Color.Info" Class="mt-2" StartIcon="@Icons.Material.Filled.Delete">
@T("Clear file list")
</MudButton>
}
}

View File

@ -14,16 +14,16 @@ using DialogOptions = Dialogs.DialogOptions;
public partial class AttachDocuments : MSGComponentBase
{
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(AttachDocuments).Namespace, nameof(AttachDocuments));
[Parameter]
public string Name { get; set; } = string.Empty;
/// <summary>
/// On which layer to register the drop area. Higher layers have priority over lower layers.
/// </summary>
[Parameter]
public int Layer { get; set; }
/// <summary>
/// When true, pause catching dropped files. Default is false.
/// </summary>
@ -38,16 +38,19 @@ public partial class AttachDocuments : MSGComponentBase
[Parameter]
public Func<HashSet<FileAttachment>, Task> OnChange { get; set; } = _ => Task.CompletedTask;
/// <summary>
/// Catch all documents that are hovered over the AI Studio window and not only over the drop zone.
/// Catch all documents that are hovered over the AI Studio window and not only over the drop zone.
/// </summary>
[Parameter]
[Parameter]
public bool CatchAllDocuments { get; set; }
[Parameter]
public bool UseSmallForm { get; set; }
[Parameter]
public bool Disabled { get; set; }
/// <summary>
/// When true, validate media file types before attaching. Default is true. That means that
/// the user cannot attach unsupported media file types when the provider or model does not
@ -56,16 +59,16 @@ public partial class AttachDocuments : MSGComponentBase
/// </summary>
[Parameter]
public bool ValidateMediaFileTypes { get; set; } = true;
[Parameter]
public AIStudio.Settings.Provider? Provider { get; set; }
[Inject]
private ILogger<AttachDocuments> Logger { get; set; } = null!;
[Inject]
private RustService RustService { get; init; } = null!;
[Inject]
private IDialogService DialogService { get; init; } = null!;
@ -74,17 +77,17 @@ public partial class AttachDocuments : MSGComponentBase
private const Placement TOOLBAR_TOOLTIP_PLACEMENT = Placement.Top;
private static readonly string DROP_FILES_HERE_TEXT = TB("Drop files here to attach them.");
private uint numDropAreasAboveThis;
private bool isComponentHovered;
private bool isDraggingOver;
#region Overrides of MSGComponentBase
protected override async Task OnInitializedAsync()
{
this.ApplyFilters([], [ Event.TAURI_EVENT_RECEIVED, Event.REGISTER_FILE_DROP_AREA, Event.UNREGISTER_FILE_DROP_AREA ]);
// Register this drop area:
await this.MessageBus.SendMessage(this, Event.REGISTER_FILE_DROP_AREA, this.Layer);
await base.OnInitializedAsync();
@ -92,6 +95,9 @@ public partial class AttachDocuments : MSGComponentBase
protected override async Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
{
if (this.Disabled && triggeredEvent == Event.TAURI_EVENT_RECEIVED)
return;
switch (triggeredEvent)
{
case Event.REGISTER_FILE_DROP_AREA when sendingComponent != this:
@ -111,7 +117,7 @@ public partial class AttachDocuments : MSGComponentBase
{
if(this.numDropAreasAboveThis > 0)
this.numDropAreasAboveThis--;
if(this.numDropAreasAboveThis is 0)
this.PauseCatchingDrops = false;
}
@ -122,40 +128,40 @@ public partial class AttachDocuments : MSGComponentBase
case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.FILE_DROP_HOVERED }:
if(this.PauseCatchingDrops)
return;
if(!this.isComponentHovered && !this.CatchAllDocuments)
{
this.Logger.LogDebug("Attach documents component '{Name}' is not hovered, ignoring file drop hovered event.", this.Name);
return;
}
this.isDraggingOver = true;
this.SetDragClass();
this.StateHasChanged();
break;
case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.FILE_DROP_CANCELED }:
if(this.PauseCatchingDrops)
return;
this.isDraggingOver = false;
this.StateHasChanged();
break;
case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.WINDOW_NOT_FOCUSED }:
if(this.PauseCatchingDrops)
return;
this.isDraggingOver = false;
this.isComponentHovered = false;
this.ClearDragClass();
this.StateHasChanged();
break;
case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.FILE_DROP_DROPPED, Payload: var paths }:
if(this.PauseCatchingDrops)
return;
if(!this.isComponentHovered && !this.CatchAllDocuments)
{
this.Logger.LogDebug("Attach documents component '{Name}' is not hovered, ignoring file drop dropped event.", this.Name);
@ -197,11 +203,14 @@ public partial class AttachDocuments : MSGComponentBase
#endregion
private const string DEFAULT_DRAG_CLASS = "relative rounded-lg border-2 border-dashed pa-4 mt-4 mud-width-full mud-height-full";
private string dragClass = DEFAULT_DRAG_CLASS;
private async Task AddFilesManually()
{
if (this.Disabled)
return;
// Ensure that Pandoc is installed and ready:
var pandocState = await this.PandocAvailabilityService.EnsureAvailabilityAsync(
showSuccessMessage: false,
@ -228,43 +237,49 @@ public partial class AttachDocuments : MSGComponentBase
this.DocumentPaths.Add(FileAttachment.FromPath(selectedFilePath));
}
await this.DocumentPathsChanged.InvokeAsync(this.DocumentPaths);
await this.OnChange(this.DocumentPaths);
}
private async Task OpenAttachmentsDialog()
{
if (this.Disabled)
return;
this.DocumentPaths = await ReviewAttachmentsDialog.OpenDialogAsync(this.DialogService, this.DocumentPaths);
}
private async Task ClearAllFiles()
{
if (this.Disabled)
return;
this.DocumentPaths.Clear();
await this.DocumentPathsChanged.InvokeAsync(this.DocumentPaths);
await this.OnChange(this.DocumentPaths);
}
private void SetDragClass() => this.dragClass = $"{DEFAULT_DRAG_CLASS} mud-border-primary border-4";
private void ClearDragClass() => this.dragClass = DEFAULT_DRAG_CLASS;
private void OnMouseEnter(EventArgs _)
{
if(this.PauseCatchingDrops)
if(this.Disabled || this.PauseCatchingDrops)
return;
this.Logger.LogDebug("Attach documents component '{Name}' is hovered.", this.Name);
this.isComponentHovered = true;
this.SetDragClass();
this.StateHasChanged();
}
private void OnMouseLeave(EventArgs _)
{
if(this.PauseCatchingDrops)
if(this.Disabled || this.PauseCatchingDrops)
return;
this.Logger.LogDebug("Attach documents component '{Name}' is no longer hovered.", this.Name);
this.isComponentHovered = false;
this.ClearDragClass();
@ -273,6 +288,9 @@ public partial class AttachDocuments : MSGComponentBase
private async Task RemoveDocument(FileAttachment fileAttachment)
{
if (this.Disabled)
return;
this.DocumentPaths.Remove(fileAttachment);
await this.DocumentPathsChanged.InvokeAsync(this.DocumentPaths);

View File

@ -13,6 +13,7 @@ public partial class Changelog
public static readonly Log[] LOGS =
[
new (242, "v26.6.2, build 242 (2026-06-21 14:07 UTC)", "v26.6.2.md"),
new (241, "v26.6.1, build 241 (2026-06-11 13:49 UTC)", "v26.6.1.md"),
new (240, "v26.5.5, build 240 (2026-05-25 18:52 UTC)", "v26.5.5.md"),
new (239, "v26.5.4, build 239 (2026-05-13 11:58 UTC)", "v26.5.4.md"),

View File

@ -129,7 +129,7 @@
<DataSourceSelection @ref="@this.dataSourceSelectionComponent" PopoverTriggerMode="PopoverTriggerMode.BUTTON" LLMProvider="@this.Provider" DataSourceOptions="@this.GetCurrentDataSourceOptions()" DataSourceOptionsChanged="@(async options => await this.SetCurrentDataSourceOptions(options))" DataSourcesAISelected="@this.GetAgentSelectedDataSources()"/>
}
@if (this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence)
@if (this.SettingsManager.ConfigurationData.Confidence.ShowProviderConfidence)
{
<ConfidenceInfo Mode="PopoverTriggerMode.ICON" LLMProvider="@this.Provider.UsedLLMProvider"/>
}

View File

@ -69,6 +69,9 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
private bool mustLoadChat;
private LoadChat loadChat;
private bool autoSaveEnabled;
private bool previousInputForbidden = true;
private Guid lastSeenChatId = Guid.Empty;
private AIStudio.Settings.Provider lastSeenProvider = AIStudio.Settings.Provider.NONE;
private string currentWorkspaceName = string.Empty;
private Guid currentWorkspaceId = Guid.Empty;
private Guid currentChatThreadId = Guid.Empty;
@ -287,12 +290,25 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
this.StateHasChanged();
}
}
var inputForbidden = this.IsInputForbidden();
if (!inputForbidden && this.previousInputForbidden)
await this.inputField.FocusAsync();
this.previousInputForbidden = inputForbidden;
await base.OnAfterRenderAsync(firstRender);
}
protected override async Task OnParametersSetAsync()
{
var incomingChatId = this.ChatThread?.ChatId ?? Guid.Empty;
if (incomingChatId != this.lastSeenChatId || this.Provider != this.lastSeenProvider)
{
this.lastSeenChatId = incomingChatId;
this.lastSeenProvider = this.Provider;
this.previousInputForbidden = true;
}
await this.ApplyLoadedChatParameterAsync();
await this.SyncForegroundChatAsync();
await base.OnParametersSetAsync();
@ -435,9 +451,9 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
private string TooltipAddChatToWorkspace => string.Format(T("Start new chat in workspace '{0}'"), this.currentWorkspaceName);
private string UserInputStyle => this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence ? this.Provider.UsedLLMProvider.GetConfidence(this.SettingsManager).SetColorStyle(this.SettingsManager) : string.Empty;
private string UserInputClass => this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence ? "confidence-border" : string.Empty;
private string UserInputStyle => this.SettingsManager.ConfigurationData.Confidence.ShowProviderConfidence ? this.Provider.UsedLLMProvider.GetConfidence(this.SettingsManager).SetColorStyle(this.SettingsManager) : string.Empty;
private string UserInputClass => this.SettingsManager.ConfigurationData.Confidence.ShowProviderConfidence ? "confidence-border" : string.Empty;
private void ApplyStandardDataSourceOptions()
{
@ -503,6 +519,36 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
this.currentChatTemplate = this.SettingsManager.GetChatTemplateById(this.currentChatTemplate.Id);
}
private async Task RefreshChatSelectionsAfterConfigurationChange()
{
var previousProvider = this.Provider;
var previousChatTemplate = this.currentChatTemplate;
var chatProviderId = this.ChatThread?.SelectedProvider;
this.Provider = this.SettingsManager.GetChatProviderForLoadedChat(chatProviderId);
if (this.Provider != previousProvider)
await this.ProviderChanged.InvokeAsync(this.Provider);
if (this.ChatThread is null)
{
this.currentProfile = this.SettingsManager.GetPreselectedProfile(Tools.Components.CHAT);
this.currentChatTemplate = this.SettingsManager.GetPreselectedChatTemplate(Tools.Components.CHAT);
}
else
{
this.currentProfile = string.IsNullOrWhiteSpace(this.ChatThread.SelectedProfile)
? this.SettingsManager.GetProfileById(this.currentProfile.Id)
: this.SettingsManager.GetProfileById(this.ChatThread.SelectedProfile);
this.currentChatTemplate = string.IsNullOrWhiteSpace(this.ChatThread.SelectedChatTemplate)
? this.SettingsManager.GetChatTemplateById(this.currentChatTemplate.Id)
: this.SettingsManager.GetChatTemplateById(this.ChatThread.SelectedChatTemplate);
}
if (!this.ComposerState.HasUserDraft && previousChatTemplate != this.currentChatTemplate)
this.ComposerState.ApplyTemplate(this.currentChatTemplate);
}
private IReadOnlyList<DataSourceAgentSelected> GetAgentSelectedDataSources()
{
if (this.ChatThread is null)
@ -1082,11 +1128,8 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
break;
case Event.CONFIGURATION_CHANGED:
var previousChatTemplate = this.currentChatTemplate;
this.RefreshCurrentProfileAndChatTemplate();
if (!this.ComposerState.HasUserDraft && previousChatTemplate != this.currentChatTemplate)
this.ComposerState.ApplyTemplate(this.currentChatTemplate);
case Event.PLUGINS_RELOADED:
await this.RefreshChatSelectionsAfterConfigurationChange();
this.StateHasChanged();
break;
@ -1097,7 +1140,10 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
{
this.ChatThread = this.AIJobService.TryGetLiveChatThread(snapshot.SubjectId) ?? this.ChatThread;
if (!snapshot.IsActive)
{
this.hasUnsavedChanges = false;
this.previousInputForbidden = true;
}
this.StateHasChanged();
}

View File

@ -6,7 +6,7 @@
<ActivatorContent>
@if (this.CurrentChatTemplate != ChatTemplate.NO_CHAT_TEMPLATE)
{
<MudButton IconSize="Size.Large" StartIcon="@Icons.Material.Filled.RateReview" IconColor="Color.Default">
<MudButton IconSize="Size.Large" StartIcon="@this.ChatTemplateIcon(this.CurrentChatTemplate)" IconColor="Color.Default">
@this.CurrentChatTemplate.GetSafeName()
</MudButton>
}
@ -22,7 +22,7 @@
<MudDivider/>
@foreach (var chatTemplate in this.SettingsManager.ConfigurationData.ChatTemplates.GetAllChatTemplates())
{
<MudMenuItem Icon="@Icons.Material.Filled.RateReview" OnClick="@(async () => await this.SelectionChanged(chatTemplate))">
<MudMenuItem Icon="@this.ChatTemplateIcon(chatTemplate)" OnClick="@(async () => await this.SelectionChanged(chatTemplate))">
@chatTemplate.GetSafeName()
</MudMenuItem>
}

View File

@ -11,13 +11,13 @@ public partial class ChatTemplateSelection : MSGComponentBase
{
[Parameter]
public ChatTemplate CurrentChatTemplate { get; set; } = ChatTemplate.NO_CHAT_TEMPLATE;
[Parameter]
public bool CanChatThreadBeUsedForTemplate { get; set; }
[Parameter]
public ChatThread? CurrentChatThread { get; set; }
[Parameter]
public EventCallback<ChatTemplate> CurrentChatTemplateChanged { get; set; }
@ -26,24 +26,42 @@ public partial class ChatTemplateSelection : MSGComponentBase
[Parameter]
public string MarginRight { get; set; } = string.Empty;
[Inject]
private IDialogService DialogService { get; init; } = null!;
private string MarginClass => $"{this.MarginLeft} {this.MarginRight}";
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
this.ApplyFilters([], [ Event.CONFIGURATION_CHANGED ]);
await base.OnInitializedAsync();
}
#endregion
private string ChatTemplateIcon(ChatTemplate chatTemplate)
{
if (chatTemplate.IsEnterpriseConfiguration)
return Icons.Material.Filled.Business;
return Icons.Material.Filled.RateReview;
}
private async Task SelectionChanged(ChatTemplate chatTemplate)
{
this.CurrentChatTemplate = chatTemplate;
await this.CurrentChatTemplateChanged.InvokeAsync(chatTemplate);
}
private async Task OpenSettingsDialog()
{
var dialogParameters = new DialogParameters();
await this.DialogService.ShowAsync<SettingsDialogChatTemplate>(T("Open Chat Template Options"), dialogParameters, DialogOptions.FULLSCREEN);
}
private async Task CreateNewChatTemplateFromChat()
{
var dialogParameters = new DialogParameters<SettingsDialogChatTemplate>
@ -53,4 +71,16 @@ public partial class ChatTemplateSelection : MSGComponentBase
};
await this.DialogService.ShowAsync<SettingsDialogChatTemplate>(T("Open Chat Template Options"), dialogParameters, DialogOptions.FULLSCREEN);
}
#region Overrides of MSGComponentBase
protected override Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
{
if (triggeredEvent is Event.CONFIGURATION_CHANGED or Event.PLUGINS_RELOADED)
this.StateHasChanged();
return Task.CompletedTask;
}
#endregion
}

View File

@ -41,9 +41,9 @@ public partial class ConfigurationMinConfidenceSelection : MSGComponentBase
if (this.SelectedValue() is ConfidenceLevel.NONE)
return ConfidenceLevel.NONE;
if(this.RestrictToGlobalMinimumConfidence && this.SettingsManager.ConfigurationData.LLMProviders.EnforceGlobalMinimumConfidence)
if(this.RestrictToGlobalMinimumConfidence && this.SettingsManager.ConfigurationData.Confidence.EnforceGlobalMinimumConfidence)
{
var minimumLevel = this.SettingsManager.ConfigurationData.LLMProviders.GlobalMinimumConfidence;
var minimumLevel = this.SettingsManager.ConfigurationData.Confidence.GlobalMinimumConfidence;
if(this.SelectedValue() < minimumLevel)
return minimumLevel;
}

View File

@ -37,6 +37,16 @@ public partial class ProfileSelection : MSGComponentBase
private string ToolTipText => this.Disabled ? this.DisabledText : this.defaultToolTipText;
private string MarginClass => $"{this.MarginLeft} {this.MarginRight}";
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
this.ApplyFilters([], [ Event.CONFIGURATION_CHANGED ]);
await base.OnInitializedAsync();
}
#endregion
private string ProfileIcon(Profile profile)
{
@ -57,4 +67,16 @@ public partial class ProfileSelection : MSGComponentBase
var dialogParameters = new DialogParameters();
await this.DialogService.ShowAsync<SettingsDialogProfiles>(T("Open Profile Options"), dialogParameters, DialogOptions.FULLSCREEN);
}
#region Overrides of MSGComponentBase
protected override Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
{
if (triggeredEvent is Event.CONFIGURATION_CHANGED or Event.PLUGINS_RELOADED)
this.StateHasChanged();
return Task.CompletedTask;
}
#endregion
}

View File

@ -25,6 +25,16 @@ public partial class ProviderSelection : MSGComponentBase
[Inject]
private ILogger<ProviderSelection> Logger { get; init; } = null!;
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
this.ApplyFilters([], [ Event.CONFIGURATION_CHANGED ]);
await base.OnInitializedAsync();
}
#endregion
private async Task SelectionChanged(AIStudio.Settings.Provider provider)
{
@ -62,4 +72,16 @@ public partial class ProviderSelection : MSGComponentBase
break;
}
}
#region Overrides of MSGComponentBase
protected override Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
{
if (triggeredEvent is Event.CONFIGURATION_CHANGED or Event.PLUGINS_RELOADED)
this.StateHasChanged();
return Task.CompletedTask;
}
#endregion
}

View File

@ -0,0 +1,60 @@
@using AIStudio.Provider
@using AIStudio.Settings
@inherits SettingsPanelBase
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Security" HeaderText="@T("Provider Confidence")">
<MudText Typo="Typo.h4" Class="mb-3">
@T("Provider Confidence")
</MudText>
<MudJustifiedText Class="mb-3">
@T("Do you want to always see how trustworthy your providers are? This way, you stay in control of which provider you send your data to. You can choose a common schema or configure the trust levels for each provider yourself.")
</MudJustifiedText>
<ConfigurationOption OptionDescription="@T("Do you want to enforce an global minimum confidence level?")" LabelOn="@T("Yes, enforce a minimum confidence level")" LabelOff="@T("No, do not enforce a minimum confidence level")" State="@(() => this.SettingsManager.ConfigurationData.Confidence.EnforceGlobalMinimumConfidence)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.Confidence.EnforceGlobalMinimumConfidence = updatedState)" OptionHelp="@T("When enabled, you can enforce a minimum confidence level for all features in AI Studio. This way, you can make sure only trustworthy providers are used.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.Confidence, x => x.EnforceGlobalMinimumConfidence, out var meta) && meta.IsLocked"/>
@if(this.SettingsManager.ConfigurationData.Confidence.EnforceGlobalMinimumConfidence)
{
<ConfigurationMinConfidenceSelection RestrictToGlobalMinimumConfidence="@false" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Confidence.GlobalMinimumConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Confidence.GlobalMinimumConfidence = selectedValue)" IsLocked="() => ManagedConfiguration.TryGet(x => x.Confidence, x => x.GlobalMinimumConfidence, out var meta) && meta.IsLocked"/>
}
<ConfigurationOption OptionDescription="@T("Show provider's confidence level?")" LabelOn="@T("Yes, show me the confidence level")" LabelOff="@T("No, please hide the confidence level")" State="@(() => this.SettingsManager.ConfigurationData.Confidence.ShowProviderConfidence)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.Confidence.ShowProviderConfidence = updatedState)" OptionHelp="@T("When enabled, we show you the confidence level for the selected provider in the app. This helps you assess where you are sending your data at any time. Example: are you currently working with sensitive data? Then choose a particularly trustworthy provider, etc.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.Confidence, x => x.ShowProviderConfidence, out var meta) && meta.IsLocked"/>
@if (this.SettingsManager.ConfigurationData.Confidence.ShowProviderConfidence)
{
<ConfigurationSelect OptionDescription="@T("Select a confidence scheme")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Confidence.ConfidenceScheme)" Data="@ConfigurationSelectDataFactory.GetConfidenceSchemesData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Confidence.ConfidenceScheme = selectedValue)" OptionHelp="@T("Choose the scheme that best suits you and your organization. Do you trust any western provider? Or only providers from the USA or exclusively European providers? Then choose the appropriate scheme. Alternatively, you can assign the confidence levels to each provider yourself.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.Confidence, x => x.ConfidenceScheme, out var meta) && meta.IsLocked"/>
@if (this.SettingsManager.ConfigurationData.Confidence.ConfidenceScheme is ConfidenceSchemes.CUSTOM)
{
<MudTable Items="@(Enum.GetValues<LLMProviders>().Where(x => x is not LLMProviders.NONE))" Hover="@true" Class="border-dashed border rounded-lg">
<ColGroup>
<col style="width: 12em;"/>
<col/>
<col style="width: 22em;"/>
</ColGroup>
<HeaderContent>
<MudTh>@T("Provider")</MudTh>
<MudTh>@T("Description")</MudTh>
<MudTh>@T("Confidence Level")</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd Style="vertical-align: top;">
@context.ToName()
</MudTd>
<MudTd>
<MudMarkdown Value="@context.GetConfidence(this.SettingsManager).Description" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/>
</MudTd>
<MudTd Style="vertical-align: top;">
<MudMenu StartIcon="@Icons.Material.Filled.Security" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="@this.GetCurrentConfidenceLevelName(context)" Variant="Variant.Filled" Style="@this.SetCurrentConfidenceLevelColorStyle(context)" Disabled="@this.IsCustomConfidenceSchemeLocked()">
@foreach (var confidenceLevel in Enum.GetValues<ConfidenceLevel>().OrderBy(n => n))
{
if(confidenceLevel is ConfidenceLevel.NONE or ConfidenceLevel.UNKNOWN)
continue;
<MudMenuItem OnClick="@(async () => await this.ChangeCustomConfidenceLevel(context, confidenceLevel))">
@confidenceLevel.GetName()
</MudMenuItem>
}
</MudMenu>
</MudTd>
</RowTemplate>
</MudTable>
}
}
</ExpansionPanel>

View File

@ -0,0 +1,38 @@
using AIStudio.Provider;
using AIStudio.Settings;
namespace AIStudio.Components.Settings;
public partial class SettingsPanelConfidence : SettingsPanelBase
{
private string GetCurrentConfidenceLevelName(LLMProviders llmProvider)
{
if (this.SettingsManager.ConfigurationData.Confidence.CustomConfidenceScheme.TryGetValue(llmProvider, out var level))
return level.GetName();
return T("Not yet configured");
}
private string SetCurrentConfidenceLevelColorStyle(LLMProviders llmProvider)
{
if (this.SettingsManager.ConfigurationData.Confidence.CustomConfidenceScheme.TryGetValue(llmProvider, out var level))
return $"background-color: {level.GetColor(this.SettingsManager)};";
return $"background-color: {ConfidenceLevel.UNKNOWN.GetColor(this.SettingsManager)};";
}
private bool IsCustomConfidenceSchemeLocked()
{
return ManagedConfiguration.TryGet(x => x.Confidence, x => x.CustomConfidenceScheme, out var meta) && meta.IsLocked;
}
private async Task ChangeCustomConfidenceLevel(LLMProviders llmProvider, ConfidenceLevel level)
{
if (this.IsCustomConfidenceSchemeLocked())
return;
this.SettingsManager.ConfigurationData.Confidence.CustomConfidenceScheme[llmProvider] = level;
await this.SettingsManager.StoreSettings();
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
}
}

View File

@ -1,4 +1,5 @@
@using AIStudio.Provider
@using AIStudio.Settings
@using AIStudio.Settings.DataModel
@inherits SettingsPanelProviderBase
@ -39,6 +40,12 @@
<MudTd>
<MudStack Row="true" Class="mb-2 mt-2" Spacing="1" Wrap="Wrap.Wrap">
@if (context.IsTrustedByConfiguration(this.SettingsManager))
{
<MudTooltip Text="@T("This embedding provider is trusted by your organization for data source security checks. Local data can be sent to it without security warnings.")">
<MudIconButton Color="Color.Success" Icon="@Icons.Material.Filled.VerifiedUser" Disabled="true"/>
</MudTooltip>
}
@if (context.IsEnterpriseConfiguration)
{
<MudTooltip Text="@T("This embedding provider is managed by your organization.")">

View File

@ -31,6 +31,12 @@
<MudTd>@this.GetLLMProviderModelName(context)</MudTd>
<MudTd>
<MudStack Row="true" Class="mb-2 mt-2" Spacing="1" Wrap="Wrap.Wrap">
@if (context.IsTrustedByConfiguration(this.SettingsManager))
{
<MudTooltip Text="@T("This provider is trusted by your organization for data source security checks.")">
<MudIconButton Color="Color.Success" Icon="@Icons.Material.Filled.VerifiedUser" Disabled="true"/>
</MudTooltip>
}
@if (context.IsEnterpriseConfiguration)
{
<MudTooltip Text="@T("This provider is managed by your organization.")">
@ -68,59 +74,4 @@
}
<LockableButton Text="@T("Add Provider")" IsLocked="@(() => !this.SettingsManager.ConfigurationData.App.AllowUserToAddProvider)" Icon="@Icons.Material.Filled.AddRoad" OnClickAsync="@this.AddLLMProvider" Class="mt-3" />
<MudText Typo="Typo.h4" Class="mb-3">
@T("LLM Provider Confidence")
</MudText>
<MudJustifiedText Class="mb-3">
@T("Do you want to always be able to recognize how trustworthy your LLM providers are? This way, you keep control over which provider you send your data to. You have two options for this: Either you choose a common schema, or you configure the trust levels for each LLM provider yourself.")
</MudJustifiedText>
<ConfigurationOption OptionDescription="@T("Do you want to enforce an app-wide minimum confidence level?")" LabelOn="@T("Yes, enforce a minimum confidence level")" LabelOff="@T("No, do not enforce a minimum confidence level")" State="@(() => this.SettingsManager.ConfigurationData.LLMProviders.EnforceGlobalMinimumConfidence)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.LLMProviders.EnforceGlobalMinimumConfidence = updatedState)" OptionHelp="@T("When enabled, you can enforce a minimum confidence level for all LLM providers. This way, you can ensure that only trustworthy providers are used.")"/>
@if(this.SettingsManager.ConfigurationData.LLMProviders.EnforceGlobalMinimumConfidence)
{
<ConfigurationMinConfidenceSelection RestrictToGlobalMinimumConfidence="@false" SelectedValue="@(() => this.SettingsManager.ConfigurationData.LLMProviders.GlobalMinimumConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.LLMProviders.GlobalMinimumConfidence = selectedValue)"/>
}
<ConfigurationOption OptionDescription="@T("Show provider's confidence level?")" LabelOn="@T("Yes, show me the confidence level")" LabelOff="@T("No, please hide the confidence level")" State="@(() => this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence = updatedState)" OptionHelp="@T("When enabled, we show you the confidence level for the selected provider in the app. This helps you assess where you are sending your data at any time. Example: are you currently working with sensitive data? Then choose a particularly trustworthy provider, etc.")"/>
@if (this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence)
{
<ConfigurationSelect OptionDescription="@T("Select a confidence scheme")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.LLMProviders.ConfidenceScheme)" Data="@ConfigurationSelectDataFactory.GetConfidenceSchemesData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.LLMProviders.ConfidenceScheme = selectedValue)" OptionHelp="@T("Choose the scheme that best suits you and your life. Do you trust any western provider? Or only providers from the USA or exclusively European providers? Then choose the appropriate scheme. Alternatively, you can assign the confidence levels to each provider yourself.")"/>
@if (this.SettingsManager.ConfigurationData.LLMProviders.ConfidenceScheme is ConfidenceSchemes.CUSTOM)
{
<MudTable Items="@(Enum.GetValues<LLMProviders>().Where(x => x is not LLMProviders.NONE))" Hover="@true" Class="border-dashed border rounded-lg">
<ColGroup>
<col style="width: 12em;"/>
<col/>
<col style="width: 22em;"/>
</ColGroup>
<HeaderContent>
<MudTh>@T("LLM Provider")</MudTh>
<MudTh>@T("Description")</MudTh>
<MudTh>@T("Confidence Level")</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd Style="vertical-align: top;">
@context.ToName()
</MudTd>
<MudTd>
<MudMarkdown Value="@context.GetConfidence(this.SettingsManager).Description" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/>
</MudTd>
<MudTd Style="vertical-align: top;">
<MudMenu StartIcon="@Icons.Material.Filled.Security" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="@this.GetCurrentConfidenceLevelName(context)" Variant="Variant.Filled" Style="@this.SetCurrentConfidenceLevelColorStyle(context)">
@foreach (var confidenceLevel in Enum.GetValues<ConfidenceLevel>().OrderBy(n => n))
{
if(confidenceLevel is ConfidenceLevel.NONE or ConfidenceLevel.UNKNOWN)
continue;
<MudMenuItem OnClick="@(async () => await this.ChangeCustomConfidenceLevel(context, confidenceLevel))">
@confidenceLevel.GetName()
</MudMenuItem>
}
</MudMenu>
</MudTd>
</RowTemplate>
</MudTable>
}
}
</ExpansionPanel>

View File

@ -1,7 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using AIStudio.Dialogs;
using AIStudio.Provider;
using AIStudio.Settings;
using Microsoft.AspNetCore.Components;
@ -166,25 +165,4 @@ public partial class SettingsPanelProviders : SettingsPanelProviderBase
await this.AvailableLLMProvidersChanged.InvokeAsync(this.AvailableLLMProviders);
}
private string GetCurrentConfidenceLevelName(LLMProviders llmProvider)
{
if (this.SettingsManager.ConfigurationData.LLMProviders.CustomConfidenceScheme.TryGetValue(llmProvider, out var level))
return level.GetName();
return T("Not yet configured");
}
private string SetCurrentConfidenceLevelColorStyle(LLMProviders llmProvider)
{
if (this.SettingsManager.ConfigurationData.LLMProviders.CustomConfidenceScheme.TryGetValue(llmProvider, out var level))
return $"background-color: {level.GetColor(this.SettingsManager)};";
return $"background-color: {ConfidenceLevel.UNKNOWN.GetColor(this.SettingsManager)};";
}
private async Task ChangeCustomConfidenceLevel(LLMProviders llmProvider, ConfidenceLevel level)
{
this.SettingsManager.ConfigurationData.LLMProviders.CustomConfidenceScheme[llmProvider] = level;
await this.SettingsManager.StoreSettings();
}
}

View File

@ -1,4 +1,5 @@
@using AIStudio.Provider
@using AIStudio.Settings
@using AIStudio.Settings.DataModel
@inherits SettingsPanelProviderBase
@ -35,6 +36,12 @@
<MudTd>
<MudStack Row="true" Class="mb-2 mt-2" Spacing="1" Wrap="Wrap.Wrap">
@if (context.IsTrustedByConfiguration(this.SettingsManager))
{
<MudTooltip Text="@T("This transcription provider is trusted by your organization for data source security checks.")">
<MudIconButton Color="Color.Success" Icon="@Icons.Material.Filled.VerifiedUser" Disabled="true"/>
</MudTooltip>
}
@if (context.IsEnterpriseConfiguration)
{
<MudTooltip Text="@T("This transcription provider is managed by your organization.")">

View File

@ -10,7 +10,7 @@
<MudJustifiedText Class="mb-3" Typo="Typo.body1">
@T("The name of the chat template is mandatory. Each chat template must have a unique name.")
</MudJustifiedText>
<MudForm @ref="@this.form" @bind-IsValid="@this.dataIsValid" @bind-Errors="@this.dataIssues">
@* ReSharper disable once CSharpWarnings::CS8974 *@
<MudTextField
@ -26,9 +26,10 @@
AdornmentColor="Color.Info"
Validation="@this.ValidateName"
Variant="Variant.Outlined"
ReadOnly="@this.IsReadOnly"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
/>
<MudText Typo="Typo.h6" Class="mb-3 mt-3">
@T("System Prompt")
</MudText>
@ -47,16 +48,17 @@
Class="mb-3"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
HelperText="@T("Tell the AI your system prompt.")"
ReadOnly="@this.IsReadOnly"
/>
<MudJustifiedText Class="mb-3" Typo="Typo.body1">
@T("Are you unsure which system prompt to use? You might start with the default system prompt that AI Studio uses for all chats.")
</MudJustifiedText>
<MudButton Class="mb-3" Color="Color.Default" OnClick="@this.UseDefaultSystemPrompt" StartIcon="@Icons.Material.Filled.ListAlt" Variant="Variant.Filled">
<MudButton Class="mb-3" Color="Color.Default" OnClick="@this.UseDefaultSystemPrompt" StartIcon="@Icons.Material.Filled.ListAlt" Variant="Variant.Filled" Disabled="@this.IsReadOnly">
@T("Use the default system prompt")
</MudButton>
<ReadFileContent Text="@T("Load system prompt from file")" @bind-FileContent="@this.DataSystemPrompt"/>
<ReadFileContent Text="@T("Load system prompt from file")" @bind-FileContent="@this.DataSystemPrompt" Disabled="@this.IsReadOnly"/>
<MudText Typo="Typo.h6" Class="mb-3 mt-6">
@T("Predefined User Input")
</MudText>
@ -77,6 +79,7 @@
Class="mb-3"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
HelperText="@T("Tell the AI your predefined user input.")"
ReadOnly="@this.IsReadOnly"
/>
<MudText Typo="Typo.h6" Class="mb-3 mt-6">
@ -92,6 +95,7 @@
UseSmallForm="false"
CatchAllDocuments="true"
ValidateMediaFileTypes="false"
Disabled="@this.IsReadOnly"
/>
<MudText Typo="Typo.h6" Class="mb-3 mt-6">
@ -100,8 +104,8 @@
<MudJustifiedText Class="mb-3" Typo="Typo.body1">
@T("Using some chat templates in tandem with profiles might cause issues. Therefore, you might prohibit the usage of profiles here.")
</MudJustifiedText>
<MudTextSwitch @bind-Value="@this.AllowProfileUsage" Color="Color.Primary" Label="@T("Allow the use of profiles together with this chat template?")" LabelOn="@T("Yes, allow profiles when using this template")" LabelOff="@T("No, prohibit profile use for this template")" />
<MudTextSwitch @bind-Value="@this.AllowProfileUsage" Color="Color.Primary" Label="@T("Allow the use of profiles together with this chat template?")" LabelOn="@T("Yes, allow profiles when using this template")" LabelOff="@T("No, prohibit profile use for this template")" Disabled="@this.IsReadOnly" />
<MudText Typo="Typo.h6" Class="mb-3 mt-6">
@T("Example Conversation")
</MudText>
@ -129,18 +133,18 @@
case ContentText textContent:
<MudTextField AutoGrow="true" Value="@textContent.Text" Placeholder="@T("Enter a message")" ReadOnly="true" Variant="Variant.Text" Validation="@this.ValidateExampleTextMessage"/>
break;
case ContentImage { SourceType: ContentImageSource.URL or ContentImageSource.LOCAL_PATH } imageContent:
<MudImage Src="@imageContent.Source" Alt="@T("Image content")" Fluid="true" />
break;
default:
@T("Unsupported content type")
break;
}
</MudTd>
<MudTd>
@if (!this.isInlineEditOnGoing)
@if (!this.isInlineEditOnGoing && !this.IsReadOnly)
{
<MudStack Row="true" Class="mb-2 mt-2" Wrap="Wrap.Wrap">
<MudTooltip Text="@T("Add a new message below")">
@ -153,22 +157,29 @@
</RowTemplate>
<RowEditingTemplate>
<MudTd>
<MudSelect Label="@T("Role")" @bind-Value="@context.Role" Required="true">
@foreach (var role in ChatRoles.ChatTemplateRoles())
{
<MudSelectItem Value="@role">
@role.ToChatTemplateName()
</MudSelectItem>
}
</MudSelect>
@if (this.IsReadOnly)
{
@context.Role.ToChatTemplateName()
}
else
{
<MudSelect Label="@T("Role")" @bind-Value="@context.Role" Required="true">
@foreach (var role in ChatRoles.ChatTemplateRoles())
{
<MudSelectItem Value="@role">
@role.ToChatTemplateName()
</MudSelectItem>
}
</MudSelect>
}
</MudTd>
<MudTd>
@switch(context.Content)
{
case ContentText textContent:
<MudTextField AutoGrow="true" @bind-Value="@textContent.Text" Label="@T("The message")" Required="true" Immediate="true" Placeholder="@T("Enter a message")"/>
<MudTextField AutoGrow="true" @bind-Value="@textContent.Text" Label="@T("The message")" Required="true" Immediate="true" Placeholder="@T("Enter a message")" ReadOnly="@this.IsReadOnly"/>
break;
default:
<MudText Typo="Typo.body2">
@T("Only text content is supported in the editing mode yet.")
@ -182,8 +193,8 @@
</PagerContent>
</MudTable>
</MudForm>
@if (!this.isInlineEditOnGoing)
@if (!this.isInlineEditOnGoing && !this.IsReadOnly)
{
<MudButton Class="mb-6" Color="Color.Primary" OnClick="@this.AddMessageToEnd" StartIcon="@Icons.Material.Filled.Add" Variant="Variant.Filled">
@T("Add a message")
@ -193,22 +204,31 @@
<Issues IssuesData="@this.dataIssues"/>
</DialogContent>
<DialogActions>
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">
@T("Cancel")
</MudButton>
@if (!this.isInlineEditOnGoing)
@if (this.IsReadOnly)
{
<MudButton OnClick="@this.Store" Variant="Variant.Filled" Color="Color.Primary">
@if (this.IsEditing)
{
@T("Update")
}
else
{
@T("Add")
}
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">
@T("Close")
</MudButton>
}
else
{
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">
@T("Cancel")
</MudButton>
@if (!this.isInlineEditOnGoing)
{
<MudButton OnClick="@this.Store" Variant="Variant.Filled" Color="Color.Primary">
@if (this.IsEditing)
{
@T("Update")
}
else
{
@T("Add")
}
</MudButton>
}
}
</DialogActions>
</MudDialog>

View File

@ -16,37 +16,40 @@ public partial class ChatTemplateDialog : MSGComponentBase
/// </summary>
[Parameter]
public uint DataNum { get; set; }
/// <summary>
/// The chat template's ID.
/// </summary>
[Parameter]
public string DataId { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// The chat template name chosen by the user.
/// </summary>
[Parameter]
public string DataName { get; set; } = string.Empty;
/// <summary>
/// What is the system prompt?
/// </summary>
[Parameter]
public string DataSystemPrompt { get; set; } = string.Empty;
/// <summary>
/// What is the predefined user prompt?
/// </summary>
[Parameter]
public string PredefinedUserPrompt { get; set; } = string.Empty;
/// <summary>
/// Should the dialog be in editing mode?
/// </summary>
[Parameter]
public bool IsEditing { get; init; }
[Parameter]
public bool IsReadOnly { get; init; }
[Parameter]
public IReadOnlyCollection<ContentBlock> ExampleConversation { get; init; } = [];
@ -55,23 +58,23 @@ public partial class ChatTemplateDialog : MSGComponentBase
[Parameter]
public bool AllowProfileUsage { get; set; } = true;
[Parameter]
[Parameter]
public bool CreateFromExistingChatThread { get; set; }
[Parameter]
[Parameter]
public ChatThread? ExistingChatThread { get; set; }
[Inject]
private ILogger<ChatTemplateDialog> Logger { get; init; } = null!;
private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
/// <summary>
/// The list of used chat template names. We need this to check for uniqueness.
/// </summary>
private List<string> UsedNames { get; set; } = [];
private bool dataIsValid;
private List<ContentBlock> dataExampleConversation = [];
private HashSet<FileAttachment> fileAttachments = [];
@ -80,20 +83,20 @@ public partial class ChatTemplateDialog : MSGComponentBase
private bool isInlineEditOnGoing;
private ContentBlock? messageEntryBeforeEdit;
// We get the form reference from Blazor code to validate it manually:
private MudForm form = null!;
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
// Configure the spellchecking for the instance name input:
this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES);
// Load the used instance names:
this.UsedNames = this.SettingsManager.ConfigurationData.ChatTemplates.Select(x => x.Name.ToLowerInvariant()).ToList();
// When editing, we need to load the data:
if(this.IsEditing)
{
@ -108,7 +111,7 @@ public partial class ChatTemplateDialog : MSGComponentBase
this.dataExampleConversation = this.ExistingChatThread.Blocks.Select(n => n.DeepClone(true)).ToList();
this.DataName = this.ExistingChatThread.Name;
}
await base.OnInitializedAsync();
}
@ -118,7 +121,7 @@ public partial class ChatTemplateDialog : MSGComponentBase
// We don't want to show validation errors when the user opens the dialog.
if(!this.IsEditing && firstRender)
this.form.ResetValidation();
await base.OnAfterRenderAsync(firstRender);
}
@ -128,28 +131,34 @@ public partial class ChatTemplateDialog : MSGComponentBase
{
Num = this.DataNum,
Id = this.DataId,
Name = this.DataName,
SystemPrompt = this.DataSystemPrompt,
PredefinedUserPrompt = this.PredefinedUserPrompt,
ExampleConversation = this.dataExampleConversation,
FileAttachments = this.fileAttachments.Select(attachment => attachment.Normalize()).ToList(),
AllowProfileUsage = this.AllowProfileUsage,
EnterpriseConfigurationPluginId = Guid.Empty,
IsEnterpriseConfiguration = false,
};
private void RemoveMessage(ContentBlock item)
{
if (this.IsReadOnly)
return;
this.dataExampleConversation.Remove(item);
}
private void AddMessageToEnd()
{
if (this.IsReadOnly)
return;
var newEntry = new ContentBlock
{
Role = this.dataExampleConversation.Count is 0 ? ChatRole.USER : this.dataExampleConversation.Last().Role.SelectNextRoleForTemplate(),
Role = this.dataExampleConversation.Count is 0 ? ChatRole.USER : this.dataExampleConversation.Last().Role.SelectNextRoleForTemplate(),
Content = new ContentText(),
ContentType = ContentType.TEXT,
HideFromUser = true,
@ -161,6 +170,9 @@ public partial class ChatTemplateDialog : MSGComponentBase
private void AddMessageBelow(ContentBlock currentItem)
{
if (this.IsReadOnly)
return;
var insertedEntry = new ContentBlock
{
Role = this.dataExampleConversation.Count is 0 ? ChatRole.USER : this.dataExampleConversation.Last().Role.SelectNextRoleForTemplate(),
@ -169,7 +181,7 @@ public partial class ChatTemplateDialog : MSGComponentBase
HideFromUser = true,
Time = DateTimeOffset.Now,
};
// The rest of the method remains the same:
var index = this.dataExampleConversation.IndexOf(currentItem);
if (index >= 0)
@ -177,71 +189,83 @@ public partial class ChatTemplateDialog : MSGComponentBase
else
this.dataExampleConversation.Add(insertedEntry);
}
private void BackupItem(object? element)
{
if (this.IsReadOnly)
return;
this.isInlineEditOnGoing = true;
this.messageEntryBeforeEdit = element switch
{
ContentBlock block => block.DeepClone(),
_ => null,
};
this.StateHasChanged();
}
private void ResetItem(object? element)
{
if (this.IsReadOnly)
return;
this.isInlineEditOnGoing = false;
switch (element)
{
case ContentBlock block:
if (this.messageEntryBeforeEdit is null)
return; // No backup to restore from
block.Content = this.messageEntryBeforeEdit.Content?.DeepClone();
block.Role = this.messageEntryBeforeEdit.Role;
break;
}
this.StateHasChanged();
}
private void CommitInlineEdit(object? element)
{
if (this.IsReadOnly)
return;
this.isInlineEditOnGoing = false;
this.StateHasChanged();
}
private async Task Store()
{
if (this.IsReadOnly)
return;
await this.form.Validate();
// When the data is not valid, we don't store it:
if (!this.dataIsValid)
return;
// When an inline edit is ongoing, we cannot store the data:
if (this.isInlineEditOnGoing)
return;
// Use the data model to store the chat template.
// We just return this data to the parent component:
var addedChatTemplateSettings = this.CreateChatTemplateSettings();
if(this.IsEditing)
this.Logger.LogInformation($"Edited chat template '{addedChatTemplateSettings.Name}'.");
else
this.Logger.LogInformation($"Created chat template '{addedChatTemplateSettings.Name}'.");
this.MudDialog.Close(DialogResult.Ok(addedChatTemplateSettings));
}
private string? ValidateExampleTextMessage(string message)
{
if (string.IsNullOrWhiteSpace(message))
return T("Please enter a message for the example conversation.");
return null;
}
@ -249,20 +273,23 @@ public partial class ChatTemplateDialog : MSGComponentBase
{
if (string.IsNullOrWhiteSpace(name))
return T("Please enter a name for the chat template.");
if (name.Length > 40)
return T("The chat template name must not exceed 40 characters.");
// The instance name must be unique:
var lowerName = name.ToLowerInvariant();
if (lowerName != this.dataEditingPreviousName && this.UsedNames.Contains(lowerName))
return T("The chat template name must be unique; the chosen name is already in use.");
return null;
}
private void UseDefaultSystemPrompt()
{
if (this.IsReadOnly)
return;
this.DataSystemPrompt = SystemPrompts.DEFAULT;
}

View File

@ -96,7 +96,7 @@ public partial class DataSourceLocalDirectoryDialog : MSGComponentBase
#endregion
private bool SelectedCloudEmbedding => !this.SettingsManager.ConfigurationData.EmbeddingProviders.FirstOrDefault(x => x.Id == this.dataEmbeddingId)?.IsSelfHosted ?? false;
private bool SelectedCloudEmbedding => !(this.SettingsManager.ConfigurationData.EmbeddingProviders.FirstOrDefault(x => x.Id == this.dataEmbeddingId)?.IsTrustedForDataSourceSecurityChecks(this.SettingsManager) ?? false);
private DataSourceLocalDirectory CreateDataSource() => new()
{

View File

@ -56,7 +56,7 @@ public partial class DataSourceLocalDirectoryInfoDialog : MSGComponentBase, IAsy
private bool IsOperationInProgress { get; set; } = true;
private bool IsCloudEmbedding => !this.embeddingProvider.IsSelfHosted;
private bool IsCloudEmbedding => !this.embeddingProvider.IsTrustedForDataSourceSecurityChecks(this.SettingsManager);
private bool IsDirectoryAvailable => this.directoryInfo.Exists;

View File

@ -96,7 +96,7 @@ public partial class DataSourceLocalFileDialog : MSGComponentBase
#endregion
private bool SelectedCloudEmbedding => !this.SettingsManager.ConfigurationData.EmbeddingProviders.FirstOrDefault(x => x.Id == this.dataEmbeddingId)?.IsSelfHosted ?? false;
private bool SelectedCloudEmbedding => !(this.SettingsManager.ConfigurationData.EmbeddingProviders.FirstOrDefault(x => x.Id == this.dataEmbeddingId)?.IsTrustedForDataSourceSecurityChecks(this.SettingsManager) ?? false);
private DataSourceLocalFile CreateDataSource() => new()
{

View File

@ -28,7 +28,7 @@ public partial class DataSourceLocalFileInfoDialog : MSGComponentBase
private EmbeddingProvider embeddingProvider = EmbeddingProvider.NONE;
private FileInfo fileInfo = null!;
private bool IsCloudEmbedding => !this.embeddingProvider.IsSelfHosted;
private bool IsCloudEmbedding => !this.embeddingProvider.IsTrustedForDataSourceSecurityChecks(this.SettingsManager);
private bool IsFileAvailable => this.fileInfo.Exists;

View File

@ -203,7 +203,7 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
#region Implementation of ISecretId
public string SecretId => this.DataLLMProvider.ToName();
public string SecretId => this.DataLLMProvider.ToSecretId();
public string SecretName => this.DataName;

View File

@ -27,6 +27,7 @@
AdornmentColor="Color.Info"
Validation="@this.ValidateName"
Variant="Variant.Outlined"
ReadOnly="@this.IsReadOnly"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
/>
@ -44,8 +45,9 @@
MaxLines="12"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
HelperText="@T("Tell the AI something about yourself. What is your profession? How experienced are you in this profession? Which technologies do you like?")"
ReadOnly="@this.IsReadOnly"
/>
<ReadFileContent @bind-FileContent="@this.DataNeedToKnow"/>
<ReadFileContent @bind-FileContent="@this.DataNeedToKnow" Disabled="@this.IsReadOnly"/>
<MudTextField
T="string"
@ -62,8 +64,9 @@
Class="mt-10"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
HelperText="@T("Tell the AI what you want it to do for you. What are your goals or are you trying to achieve? Like having the AI address you informally.")"
ReadOnly="@this.IsReadOnly"
/>
<ReadFileContent @bind-FileContent="@this.DataActions"/>
<ReadFileContent @bind-FileContent="@this.DataActions" Disabled="@this.IsReadOnly"/>
<MudJustifiedText Typo="Typo.body2" Class="mb-3 mt-3">
@T("Please be aware that your profile info becomes part of the system prompt. This means it uses up context space — the “memory” the LLM uses to understand and respond to your request. If your profile is extremely long, the LLM may struggle to focus on your actual task.")
@ -73,18 +76,27 @@
<Issues IssuesData="@this.dataIssues"/>
</DialogContent>
<DialogActions>
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">
@T("Cancel")
</MudButton>
<MudButton OnClick="@this.Store" Variant="Variant.Filled" Color="Color.Primary">
@if(this.IsEditing)
{
@T("Update")
}
else
{
@T("Add")
}
</MudButton>
@if (this.IsReadOnly)
{
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">
@T("Close")
</MudButton>
}
else
{
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">
@T("Cancel")
</MudButton>
<MudButton OnClick="@this.Store" Variant="Variant.Filled" Color="Color.Primary">
@if(this.IsEditing)
{
@T("Update")
}
else
{
@T("Add")
}
</MudButton>
}
</DialogActions>
</MudDialog>

View File

@ -15,19 +15,19 @@ public partial class ProfileDialog : MSGComponentBase
/// </summary>
[Parameter]
public uint DataNum { get; set; }
/// <summary>
/// The profile's ID.
/// </summary>
[Parameter]
public string DataId { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// The profile name chosen by the user.
/// </summary>
[Parameter]
public string DataName { get; set; } = string.Empty;
/// <summary>
/// What should the LLM know about you?
/// </summary>
@ -39,27 +39,30 @@ public partial class ProfileDialog : MSGComponentBase
/// </summary>
[Parameter]
public string DataActions { get; set; } = string.Empty;
/// <summary>
/// Should the dialog be in editing mode?
/// </summary>
[Parameter]
public bool IsEditing { get; init; }
[Parameter]
public bool IsReadOnly { get; init; }
[Inject]
private ILogger<ProviderDialog> Logger { get; init; } = null!;
private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
/// <summary>
/// The list of used profile names. We need this to check for uniqueness.
/// </summary>
private List<string> UsedNames { get; set; } = [];
private bool dataIsValid;
private string[] dataIssues = [];
private string dataEditingPreviousName = string.Empty;
// We get the form reference from Blazor code to validate it manually:
private MudForm form = null!;
@ -70,7 +73,7 @@ public partial class ProfileDialog : MSGComponentBase
Name = this.DataName,
NeedToKnow = this.DataNeedToKnow,
Actions = this.DataActions,
EnterpriseConfigurationPluginId = Guid.Empty,
IsEnterpriseConfiguration = false,
};
@ -81,16 +84,16 @@ public partial class ProfileDialog : MSGComponentBase
{
// Configure the spellchecking for the instance name input:
this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES);
// Load the used instance names:
this.UsedNames = this.SettingsManager.ConfigurationData.Profiles.Select(x => x.Name.ToLowerInvariant()).ToList();
// When editing, we need to load the data:
if(this.IsEditing)
{
this.dataEditingPreviousName = this.DataName.ToLowerInvariant();
}
await base.OnInitializedAsync();
}
@ -100,37 +103,40 @@ public partial class ProfileDialog : MSGComponentBase
// We don't want to show validation errors when the user opens the dialog.
if(!this.IsEditing && firstRender)
this.form.ResetValidation();
await base.OnAfterRenderAsync(firstRender);
}
#endregion
private async Task Store()
{
if (this.IsReadOnly)
return;
await this.form.Validate();
// When the data is not valid, we don't store it:
if (!this.dataIsValid)
return;
// Use the data model to store the profile.
// We just return this data to the parent component:
var addedProfileSettings = this.CreateProfileSettings();
if(this.IsEditing)
this.Logger.LogInformation($"Edited profile '{addedProfileSettings.Name}'.");
else
this.Logger.LogInformation($"Created profile '{addedProfileSettings.Name}'.");
this.MudDialog.Close(DialogResult.Ok(addedProfileSettings));
}
private string? ValidateNeedToKnow(string text)
{
if (string.IsNullOrWhiteSpace(this.DataNeedToKnow) && string.IsNullOrWhiteSpace(this.DataActions))
return T("Please enter what the LLM should know about you and/or what actions it should take.");
return null;
}
@ -138,7 +144,7 @@ public partial class ProfileDialog : MSGComponentBase
{
if (string.IsNullOrWhiteSpace(this.DataNeedToKnow) && string.IsNullOrWhiteSpace(this.DataActions))
return T("Please enter what the LLM should know about you and/or what actions it should take.");
return null;
}
@ -146,15 +152,15 @@ public partial class ProfileDialog : MSGComponentBase
{
if (string.IsNullOrWhiteSpace(name))
return T("Please enter a profile name.");
if (name.Length > 40)
return T("The profile name must not exceed 40 characters.");
// The instance name must be unique:
var lowerName = name.ToLowerInvariant();
if (lowerName != this.dataEditingPreviousName && this.UsedNames.Contains(lowerName))
return T("The profile name must be unique; the chosen name is already in use.");
return null;
}

View File

@ -231,7 +231,7 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
#region Implementation of ISecretId
public string SecretId => this.DataLLMProvider.ToName();
public string SecretId => this.DataLLMProvider.ToSecretId();
public string SecretName => this.DataInstanceName;

View File

@ -65,6 +65,9 @@ public abstract class SettingsDialogBase : MSGComponentBase
switch (triggeredEvent)
{
case Event.CONFIGURATION_CHANGED:
case Event.PLUGINS_RELOADED:
this.UpdateProviders();
this.UpdateEmbeddingProviders();
this.StateHasChanged();
break;
}

View File

@ -16,10 +16,10 @@
<ConfigurationSelect OptionDescription="@T("Provider selection when loading a chat and sending assistant results to chat")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Chat.LoadingProviderBehavior)" Data="@ConfigurationSelectDataFactory.GetLoadingChatProviderBehavior()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Chat.LoadingProviderBehavior = selectedValue)" OptionHelp="@T("Control how the LLM provider for loaded chats is selected and when assistant results are sent to chat.")"/>
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
<ConfigurationOption OptionDescription="@T("Preselect chat options?")" LabelOn="@T("Chat options are preselected")" LabelOff="@T("No chat options are preselected")" State="@(() => this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.Chat.PreselectOptions = updatedState)" OptionHelp="@T("When enabled, you can preselect chat options. This is might be useful when you prefer a specific provider.")"/>
<ConfigurationProviderSelection Component="Components.CHAT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Chat.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Chat.PreselectedProvider = selectedValue)"/>
<ConfigurationSelect OptionDescription="@T("Preselect a profile")" Disabled="@(() => !this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" SelectedValue="@(() => ProfilePreselection.FromStoredValue(this.SettingsManager.ConfigurationData.Chat.PreselectedProfile))" Data="@ConfigurationSelectDataFactory.GetComponentProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Chat.PreselectedProfile = selectedValue)" OptionHelp="@T("Choose whether chats should use the app default profile, no profile, or a specific profile.")"/>
<ConfigurationSelect OptionDescription="@T("Preselect one of your chat templates?")" Disabled="@(() => !this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Chat.PreselectedChatTemplate)" Data="@ConfigurationSelectDataFactory.GetChatTemplatesData(this.SettingsManager.ConfigurationData.ChatTemplates)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Chat.PreselectedChatTemplate = selectedValue)" OptionHelp="@T("Would you like to set one of your chat templates as the default for chats?")"/>
<ConfigurationOption OptionDescription="@T("Preselect chat options?")" LabelOn="@T("Chat options are preselected")" LabelOff="@T("No chat options are preselected")" State="@(() => this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.Chat.PreselectOptions = updatedState)" OptionHelp="@T("When enabled, you can preselect chat options. This is might be useful when you prefer a specific provider.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.Chat, x => x.PreselectOptions, out var meta) && meta.IsLocked"/>
<ConfigurationProviderSelection Component="Components.CHAT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Chat.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Chat.PreselectedProvider = selectedValue)" IsLocked="() => ManagedConfiguration.TryGet(x => x.Chat, x => x.PreselectedProvider, out var meta) && meta.IsLocked"/>
<ConfigurationSelect OptionDescription="@T("Preselect a profile")" Disabled="@(() => !this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" SelectedValue="@(() => ProfilePreselection.FromStoredValue(this.SettingsManager.ConfigurationData.Chat.PreselectedProfile))" Data="@ConfigurationSelectDataFactory.GetComponentProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Chat.PreselectedProfile = selectedValue)" OptionHelp="@T("Choose whether chats should use the app default profile, no profile, or a specific profile.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.Chat, x => x.PreselectedProfile, out var meta) && meta.IsLocked"/>
<ConfigurationSelect OptionDescription="@T("Preselect one of your chat templates?")" Disabled="@(() => !this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Chat.PreselectedChatTemplate)" Data="@ConfigurationSelectDataFactory.GetChatTemplatesData(this.SettingsManager.ConfigurationData.ChatTemplates)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Chat.PreselectedChatTemplate = selectedValue)" OptionHelp="@T("Would you like to set one of your chat templates as the default for chats?")" IsLocked="() => ManagedConfiguration.TryGet(x => x.Chat, x => x.PreselectedChatTemplate, out var meta) && meta.IsLocked"/>
</MudPaper>
@if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager))

View File

@ -33,9 +33,14 @@
<MudTd>
@if (context.IsEnterpriseConfiguration)
{
<MudTooltip Text="@T("This template is managed by your organization.")">
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Business" Disabled="true"/>
</MudTooltip>
<MudStack Row="true" Class="mb-2 mt-2" Wrap="Wrap.Wrap">
<MudTooltip Text="@T("This template is managed by your organization.")">
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Business" Disabled="true"/>
</MudTooltip>
<MudTooltip Text="@T("View")">
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Visibility" OnClick="@(() => this.ViewChatTemplate(context))"/>
</MudTooltip>
</MudStack>
}
else
{

View File

@ -6,24 +6,24 @@ namespace AIStudio.Dialogs.Settings;
public partial class SettingsDialogChatTemplate : SettingsDialogBase
{
[Parameter]
[Parameter]
public bool CreateTemplateFromExistingChatThread { get; set; }
[Parameter]
public ChatThread? ExistingChatThread { get; set; }
#region Overrides of ComponentBase
/// <inheritdoc />
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
if (this.CreateTemplateFromExistingChatThread)
if (this.CreateTemplateFromExistingChatThread)
await this.AddChatTemplate();
}
#endregion
private async Task AddChatTemplate()
{
var dialogParameters = new DialogParameters<ChatTemplateDialog>
@ -41,21 +41,21 @@ public partial class SettingsDialogChatTemplate : SettingsDialogBase
var dialogResult = await dialogReference.Result;
if (dialogResult is null || dialogResult.Canceled)
return;
var addedChatTemplate = (ChatTemplate)dialogResult.Data!;
addedChatTemplate = addedChatTemplate with { Num = this.SettingsManager.ConfigurationData.NextChatTemplateNum++ };
this.SettingsManager.ConfigurationData.ChatTemplates.Add(addedChatTemplate);
await this.SettingsManager.StoreSettings();
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
}
private async Task EditChatTemplate(ChatTemplate chatTemplate)
{
if (chatTemplate == ChatTemplate.NO_CHAT_TEMPLATE || chatTemplate.IsEnterpriseConfiguration)
return;
var dialogParameters = new DialogParameters<ChatTemplateDialog>
{
{ x => x.DataNum, chatTemplate.Num },
@ -68,34 +68,53 @@ public partial class SettingsDialogChatTemplate : SettingsDialogBase
{ x => x.FileAttachments, chatTemplate.FileAttachments },
{ x => x.AllowProfileUsage, chatTemplate.AllowProfileUsage },
};
var dialogReference = await this.DialogService.ShowAsync<ChatTemplateDialog>(T("Edit Chat Template"), dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
if (dialogResult is null || dialogResult.Canceled)
return;
var editedChatTemplate = (ChatTemplate)dialogResult.Data!;
this.SettingsManager.ConfigurationData.ChatTemplates[this.SettingsManager.ConfigurationData.ChatTemplates.IndexOf(chatTemplate)] = editedChatTemplate;
await this.SettingsManager.StoreSettings();
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
}
private async Task ViewChatTemplate(ChatTemplate chatTemplate)
{
var dialogParameters = new DialogParameters<ChatTemplateDialog>
{
{ x => x.DataNum, chatTemplate.Num },
{ x => x.DataId, chatTemplate.Id },
{ x => x.DataName, chatTemplate.Name },
{ x => x.DataSystemPrompt, chatTemplate.SystemPrompt },
{ x => x.PredefinedUserPrompt, chatTemplate.PredefinedUserPrompt },
{ x => x.IsEditing, true },
{ x => x.IsReadOnly, true },
{ x => x.ExampleConversation, chatTemplate.ExampleConversation },
{ x => x.FileAttachments, chatTemplate.FileAttachments },
{ x => x.AllowProfileUsage, chatTemplate.AllowProfileUsage },
};
await this.DialogService.ShowAsync<ChatTemplateDialog>(T("View Chat Template"), dialogParameters, DialogOptions.FULLSCREEN);
}
private async Task DeleteChatTemplate(ChatTemplate chatTemplate)
{
var dialogParameters = new DialogParameters<ConfirmDialog>
{
{ x => x.Message, string.Format(T("Are you sure you want to delete the chat template '{0}'?"), chatTemplate.Name) },
};
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>(T("Delete Chat Template"), dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
if (dialogResult is null || dialogResult.Canceled)
return;
this.SettingsManager.ConfigurationData.ChatTemplates.Remove(chatTemplate);
await this.SettingsManager.StoreSettings();
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
}

View File

@ -32,9 +32,14 @@
<MudTd>
@if (context.IsEnterpriseConfiguration)
{
<MudTooltip Text="@T("This profile is managed by your organization.")">
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Business" Disabled="true"/>
</MudTooltip>
<MudStack Row="true" Class="mb-2 mt-2" Wrap="Wrap.Wrap">
<MudTooltip Text="@T("This profile is managed by your organization.")">
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Business" Disabled="true"/>
</MudTooltip>
<MudTooltip Text="@T("View")">
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Visibility" OnClick="() => this.ViewProfile(context)"/>
</MudTooltip>
</MudStack>
}
else
{

View File

@ -10,21 +10,21 @@ public partial class SettingsDialogProfiles : SettingsDialogBase
{
{ x => x.IsEditing, false },
};
var dialogReference = await this.DialogService.ShowAsync<ProfileDialog>(T("Add Profile"), dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
if (dialogResult is null || dialogResult.Canceled)
return;
var addedProfile = (Profile)dialogResult.Data!;
addedProfile = addedProfile with { Num = this.SettingsManager.ConfigurationData.NextProfileNum++ };
this.SettingsManager.ConfigurationData.Profiles.Add(addedProfile);
await this.SettingsManager.StoreSettings();
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
}
private async Task EditProfile(Profile profile)
{
var dialogParameters = new DialogParameters<ProfileDialog>
@ -36,19 +36,35 @@ public partial class SettingsDialogProfiles : SettingsDialogBase
{ x => x.DataActions, profile.Actions },
{ x => x.IsEditing, true },
};
var dialogReference = await this.DialogService.ShowAsync<ProfileDialog>(T("Edit Profile"), dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
if (dialogResult is null || dialogResult.Canceled)
return;
var editedProfile = (Profile)dialogResult.Data!;
this.SettingsManager.ConfigurationData.Profiles[this.SettingsManager.ConfigurationData.Profiles.IndexOf(profile)] = editedProfile;
await this.SettingsManager.StoreSettings();
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
}
private async Task ViewProfile(Profile profile)
{
var dialogParameters = new DialogParameters<ProfileDialog>
{
{ x => x.DataNum, profile.Num },
{ x => x.DataId, profile.Id },
{ x => x.DataName, profile.Name },
{ x => x.DataNeedToKnow, profile.NeedToKnow },
{ x => x.DataActions, profile.Actions },
{ x => x.IsEditing, true },
{ x => x.IsReadOnly, true },
};
await this.DialogService.ShowAsync<ProfileDialog>(T("View Profile"), dialogParameters, DialogOptions.FULLSCREEN);
}
private async Task ExportProfile(Profile profile)
{
if (!this.SettingsManager.ConfigurationData.App.ShowAdminSettings)
@ -68,15 +84,15 @@ public partial class SettingsDialogProfiles : SettingsDialogBase
{
{ x => x.Message, string.Format(T("Are you sure you want to delete the profile '{0}'?"), profile.Name) },
};
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>(T("Delete Profile"), dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
if (dialogResult is null || dialogResult.Canceled)
return;
this.SettingsManager.ConfigurationData.Profiles.Remove(profile);
await this.SettingsManager.StoreSettings();
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
}
}

View File

@ -218,7 +218,7 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId
#region Implementation of ISecretId
public string SecretId => this.DataLLMProvider.ToName();
public string SecretId => this.DataLLMProvider.ToSecretId();
public string SecretName => this.DataName;

View File

@ -58,6 +58,7 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
private MudThemeProvider themeProvider = null!;
private bool useDarkMode;
private bool startupCompleted;
private bool settingsWriteProtectionWarningShown;
private readonly SemaphoreSlim mandatoryInfoDialogSemaphore = new(1, 1);
private IReadOnlyCollection<NavBarItem> navItems = [];
@ -127,6 +128,39 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
#endregion
private void ShowSettingsWriteProtectionWarning()
{
if(!this.SettingsManager.SettingsWriteBlocked || this.settingsWriteProtectionWarningShown)
return;
this.settingsWriteProtectionWarningShown = true;
var reason = this.SettingsManager.SettingsWriteBlockReason;
var message = reason switch
{
SettingsWriteBlockReason.VERSION_NEWER_THAN_APP => T("Your settings were created by a newer AI Studio version. Changes in this session will not be saved. Please install or start the latest available update."),
SettingsWriteBlockReason.VERSION_MISSING => T("Your settings file does not contain a settings-format version. Changes in this session will not be saved to avoid overwriting your settings. Please check for updates or contact support."),
SettingsWriteBlockReason.VERSION_UNKNOWN => T("AI Studio does not recognize your settings-format version. Changes in this session will not be saved to avoid overwriting your settings. Please check for updates or contact support."),
SettingsWriteBlockReason.FILE_UNREADABLE => T("AI Studio could not read your settings file. Changes in this session will not be saved to avoid overwriting recoverable settings. Please check for updates or contact support."),
SettingsWriteBlockReason.CURRENT_VERSION_INVALID => T("AI Studio found the current settings format but could not load it safely. Changes in this session will not be saved. Please check for updates or contact support."),
_ => T("AI Studio cannot safely save settings in this session. Please check for updates or contact support."),
};
message = $"{message} {T("Reason")}: {reason}";
this.Snackbar.Add(message, Severity.Warning, config =>
{
config.Icon = Icons.Material.Filled.WarningAmber;
config.IconSize = Size.Large;
config.VisibleStateDuration = 32_000;
config.HideTransitionDuration = 600;
config.Action = T("Check for updates");
config.ActionVariant = Variant.Filled;
config.OnClick = async _ =>
{
await this.MessageBus.SendMessage<bool>(this, Event.USER_SEARCH_FOR_UPDATE);
};
});
}
#region Implementation of ILang
/// <inheritdoc />
@ -276,6 +310,7 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
case Event.PLUGINS_RELOADED:
this.Lang = await this.SettingsManager.GetActiveLanguagePlugin();
I18N.Init(this.Lang);
this.ShowSettingsWriteProtectionWarning();
this.LoadNavItems();
await this.InvokeAsync(this.StateHasChanged);

View File

@ -8,39 +8,52 @@
</MudText>
<InnerScrolling>
<MudExpansionPanels Class="mb-3" MultiExpansion="@false">
<MudExpansionPanels @key="@this.expansionPanelsRenderKey" Class="mb-3" MultiExpansion="@false">
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.MenuBook" HeaderText="@T("Introduction")" IsExpanded="@true">
<MudText Typo="Typo.h5" Class="mb-3">
@T("Welcome to MindWork AI Studio!")
</MudText>
<MudText Typo="Typo.body1" Class="mb-3" Style="text-align: justify; hyphens: auto;">
@T("Thank you for considering MindWork AI Studio for your AI needs. This app is designed to help you harness the power of Large Language Models (LLMs). Please note that this app doesn't come with an integrated LLM. Instead, you will need to bring an API key from a suitable provider.")
</MudText>
<MudText Typo="Typo.body1" Class="mb-3">
@T("Here's what makes MindWork AI Studio stand out:")
</MudText>
<MudTextList Icon="@Icons.Material.Filled.CheckCircle" Clickable="@true" Items="@this.itemsAdvantages" Class="mb-3"/>
<MudText Typo="Typo.body1" Class="mb-3">
@T("We hope you enjoy using MindWork AI Studio to bring your AI projects to life!")
</MudText>
</ExpansionPanel>
@if (this.SettingsManager.ConfigurationData.App.ShowIntroduction)
{
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.MenuBook" HeaderText="@T("Introduction")" IsExpanded="@this.IsPanelExpanded(PANEL_ID_BUILT_IN_INTRODUCTION)" ExpandedChanged="@(isExpanded => this.SetPanelExpanded(PANEL_ID_BUILT_IN_INTRODUCTION, isExpanded))">
<MudText Typo="Typo.h5" Class="mb-3">
@T("Welcome to MindWork AI Studio!")
</MudText>
<MudText Typo="Typo.body1" Class="mb-3" Style="text-align: justify; hyphens: auto;">
@T("Thank you for considering MindWork AI Studio for your AI needs. This app is designed to help you harness the power of Large Language Models (LLMs). Please note that this app doesn't come with an integrated LLM. Instead, you will need to bring an API key from a suitable provider.")
</MudText>
<MudText Typo="Typo.body1" Class="mb-3">
@T("Here's what makes MindWork AI Studio stand out:")
</MudText>
<MudTextList Icon="@Icons.Material.Filled.CheckCircle" Clickable="@true" Items="@this.itemsAdvantages" Class="mb-3"/>
<MudText Typo="Typo.body1" Class="mb-3">
@T("We hope you enjoy using MindWork AI Studio to bring your AI projects to life!")
</MudText>
</ExpansionPanel>
}
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.EventNote" HeaderText="@T("Last Changelog")">
@foreach (var introduction in this.introductions)
{
<ExpansionPanel @key="@introduction.Id" HeaderIcon="@Icons.Material.Filled.Info" HeaderText="@introduction.Title" IsExpanded="@this.IsPanelExpanded(IntroductionPanelId(introduction))" ExpandedChanged="@(isExpanded => this.SetPanelExpanded(IntroductionPanelId(introduction), isExpanded))">
<MudText Typo="Typo.body2" Class="mb-3">
@T("Version"): @introduction.VersionText
</MudText>
<MudJustifiedMarkdown Value="@introduction.Markdown" />
</ExpansionPanel>
}
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.EventNote" HeaderText="@T("Last Changelog")" IsExpanded="@this.IsPanelExpanded(PANEL_ID_LAST_CHANGELOG)" ExpandedChanged="@(isExpanded => this.SetPanelExpanded(PANEL_ID_LAST_CHANGELOG, isExpanded))">
<MudMarkdown Value="@this.LastChangeContent" Props="Markdown.DefaultConfig" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/>
</ExpansionPanel>
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Lightbulb" HeaderText="@T("Vision")">
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Lightbulb" HeaderText="@T("Vision")" IsExpanded="@this.IsPanelExpanded(PANEL_ID_VISION)" ExpandedChanged="@(isExpanded => this.SetPanelExpanded(PANEL_ID_VISION, isExpanded))">
<Vision/>
</ExpansionPanel>
@if (this.SettingsManager.ConfigurationData.App.ShowQuickStartGuide)
{
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.RocketLaunch" HeaderText="@T("Quick Start Guide")">
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.RocketLaunch" HeaderText="@T("Quick Start Guide")" IsExpanded="@this.IsPanelExpanded(PANEL_ID_QUICK_START_GUIDE)" ExpandedChanged="@(isExpanded => this.SetPanelExpanded(PANEL_ID_QUICK_START_GUIDE, isExpanded))">
<MudMarkdown Props="Markdown.DefaultConfig" Value="@QUICK_START_GUIDE" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/>
</ExpansionPanel>
}
</MudExpansionPanels>
</InnerScrolling>
</div>
</div>

View File

@ -1,5 +1,6 @@
using AIStudio.Components;
using AIStudio.Settings.DataModel;
using AIStudio.Tools.PluginSystem;
using Microsoft.AspNetCore.Components;
@ -18,13 +19,25 @@ public partial class Home : MSGComponentBase
private string LastChangeContent { get; set; } = string.Empty;
private TextItem[] itemsAdvantages = [];
private List<DataIntroduction> introductions = [];
private string expandedPanelId = string.Empty;
private int expansionPanelsRenderKey;
private const string PANEL_ID_BUILT_IN_INTRODUCTION = "built-in-introduction";
private const string PANEL_ID_LAST_CHANGELOG = "last-changelog";
private const string PANEL_ID_VISION = "vision";
private const string PANEL_ID_QUICK_START_GUIDE = "quick-start-guide";
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
this.ApplyFilters([], [ Event.CONFIGURATION_CHANGED ]);
await base.OnInitializedAsync();
this.InitializeAdvantagesItems();
this.RefreshIntroductionPanels();
this.EnsureDefaultExpandedPanel();
// Read the last change content asynchronously
// without blocking the UI thread:
@ -69,10 +82,14 @@ public partial class Home : MSGComponentBase
{
case Event.PLUGINS_RELOADED:
this.InitializeAdvantagesItems();
this.RefreshIntroductionPanels();
this.EnsureDefaultExpandedPanel();
await this.InvokeAsync(this.StateHasChanged);
break;
case Event.CONFIGURATION_CHANGED:
this.RefreshIntroductionPanels();
this.EnsureDefaultExpandedPanel();
await this.InvokeAsync(this.StateHasChanged);
break;
}
@ -80,6 +97,42 @@ public partial class Home : MSGComponentBase
#endregion
private void RefreshIntroductionPanels()
{
this.introductions = PluginFactory.GetIntroductions().ToList();
}
private string GetDefaultExpandedPanelId()
{
if (this.SettingsManager.ConfigurationData.App.ShowIntroduction)
return PANEL_ID_BUILT_IN_INTRODUCTION;
var firstIntroduction = this.introductions.FirstOrDefault();
return firstIntroduction is not null
? IntroductionPanelId(firstIntroduction)
: PANEL_ID_LAST_CHANGELOG;
}
private void EnsureDefaultExpandedPanel()
{
this.expandedPanelId = this.GetDefaultExpandedPanelId();
this.expansionPanelsRenderKey++;
}
private bool IsPanelExpanded(string panelId) => string.Equals(this.expandedPanelId, panelId, StringComparison.Ordinal);
private Task SetPanelExpanded(string panelId, bool isExpanded)
{
if (isExpanded)
this.expandedPanelId = panelId;
else if (this.IsPanelExpanded(panelId))
this.expandedPanelId = string.Empty;
return Task.CompletedTask;
}
private static string IntroductionPanelId(DataIntroduction introduction) => $"introduction:{introduction.Id}";
private async Task ReadLastChangeAsync()
{
var latest = Changelog.LOGS.MaxBy(n => n.Build);

View File

@ -8,6 +8,7 @@
<InnerScrolling>
<MudExpansionPanels Class="mb-3" MultiExpansion="@false">
<SettingsPanelConfidence/>
<SettingsPanelProviders @bind-AvailableLLMProviders="@this.availableLLMProviders"/>
@if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager))

View File

@ -207,6 +207,9 @@ CONFIG["SETTINGS"] = {}
-- Configure whether the quick start guide is shown on the welcome page.
-- CONFIG["SETTINGS"]["DataApp.ShowQuickStartGuide"] = false
-- Configure whether the built-in introduction is shown on the welcome page.
-- CONFIG["SETTINGS"]["DataApp.ShowIntroduction"] = false
-- Configure the user permission to add providers:
-- CONFIG["SETTINGS"]["DataApp.AllowUserToAddProvider"] = false
@ -235,6 +238,32 @@ CONFIG["SETTINGS"] = {}
-- Please note: using an empty string ("") will lock the preselected profile selection, even though no valid preselected profile is found.
-- CONFIG["SETTINGS"]["DataApp.PreselectedProfile"] = "00000000-0000-0000-0000-000000000000"
-- Configure chat-specific preselected options.
-- This must be enabled for the chat-specific provider, profile, and chat template to take effect.
-- CONFIG["SETTINGS"]["DataChat.PreselectOptions"] = true
--
-- Configure the preselected provider for chats.
-- It must be one of the provider IDs defined in CONFIG["LLM_PROVIDERS"].
-- CONFIG["SETTINGS"]["DataChat.PreselectedProvider"] = "00000000-0000-0000-0000-000000000000"
--
-- Configure the preselected profile for chats.
-- It must be one of the profile IDs defined in CONFIG["PROFILES"].
-- Please note: using an empty string ("") means chats will use the app default profile.
-- Please note: using "00000000-0000-0000-0000-000000000000" means chats will use no profile.
-- CONFIG["SETTINGS"]["DataChat.PreselectedProfile"] = "00000000-0000-0000-0000-000000000000"
--
-- Configure the preselected chat template for chats.
-- It must be one of the chat template IDs defined in CONFIG["CHAT_TEMPLATES"].
-- Please note: using an empty string ("") or "00000000-0000-0000-0000-000000000000" means chats will use no chat template.
-- CONFIG["SETTINGS"]["DataChat.PreselectedChatTemplate"] = "00000000-0000-0000-0000-000000000000"
--
-- Allow users to change any configured chat default locally.
-- Allowed values are: true, false
-- CONFIG["SETTINGS"]["DataChat.PreselectOptions.AllowUserOverride"] = true
-- CONFIG["SETTINGS"]["DataChat.PreselectedProvider.AllowUserOverride"] = true
-- CONFIG["SETTINGS"]["DataChat.PreselectedProfile.AllowUserOverride"] = true
-- CONFIG["SETTINGS"]["DataChat.PreselectedChatTemplate.AllowUserOverride"] = true
-- Configure the transcription provider for voice-to-text functionality.
-- It must be one of the transcription provider IDs defined in CONFIG["TRANSCRIPTION_PROVIDERS"].
-- Without a selected transcription provider, dictation and transcription features will be disabled.
@ -284,6 +313,66 @@ CONFIG["SETTINGS"] = {}
-- CONFIG["SETTINGS"]["DataApp.ExternalHttpCustomRootCertificateBundlePath"] = "/path/in/sandbox/company-root-cas.pem"
-- CONFIG["SETTINGS"]["DataApp.ExternalHttpCustomRootCertificateAllowedHosts"] = { "*.intra.example.org", "eri.example.org" }
-- Configure provider confidence settings.
-- These settings apply to LLM providers, embedding providers, and transcription providers.
--
-- Configure a predefined confidence scheme.
-- Allowed values are: TRUST_ALL, TRUST_USA_EUROPE, TRUST_USA, TRUST_EUROPE, TRUST_ASIA, LOCAL_TRUST_ONLY, CUSTOM
-- CONFIG["SETTINGS"]["DataConfidence.ConfidenceScheme"] = "TRUST_EUROPE"
--
-- Configure whether users can still change the confidence scheme locally.
-- Allowed values are: true, false
-- When set to true, the configured confidence scheme becomes the organization default,
-- but users can still choose another scheme in the app settings.
-- CONFIG["SETTINGS"]["DataConfidence.ConfidenceScheme.AllowUserOverride"] = true
--
-- Configure whether confidence levels are shown in the UI.
-- CONFIG["SETTINGS"]["DataConfidence.ShowProviderConfidence"] = true
--
-- Configure an app-wide minimum confidence level.
-- Allowed values are: NONE, VERY_LOW, LOW, MODERATE, MEDIUM, HIGH
-- CONFIG["SETTINGS"]["DataConfidence.EnforceGlobalMinimumConfidence"] = true
-- CONFIG["SETTINGS"]["DataConfidence.GlobalMinimumConfidence"] = "MEDIUM"
--
-- Configure whether users can change the app-wide minimum confidence level locally.
-- CONFIG["SETTINGS"]["DataConfidence.EnforceGlobalMinimumConfidence.AllowUserOverride"] = false
-- CONFIG["SETTINGS"]["DataConfidence.GlobalMinimumConfidence.AllowUserOverride"] = false
--
-- Configure a custom confidence scheme.
-- This is used when DataConfidence.ConfidenceScheme is set to CUSTOM.
-- Allowed provider keys are: OPEN_AI, ANTHROPIC, MISTRAL, GOOGLE, X, DEEP_SEEK, ALIBABA_CLOUD,
-- PERPLEXITY, OPEN_ROUTER, FIREWORKS, GROQ, HUGGINGFACE, SELF_HOSTED, HELMHOLTZ, GWDG
-- Allowed confidence values are: UNTRUSTED, VERY_LOW, LOW, MODERATE, MEDIUM, HIGH
-- CONFIG["SETTINGS"]["DataConfidence.CustomConfidenceScheme"] = {
-- ["OPEN_AI"] = "MODERATE",
-- ["ANTHROPIC"] = "MODERATE",
-- ["MISTRAL"] = "HIGH",
-- ["GOOGLE"] = "LOW",
-- ["X"] = "LOW",
-- ["DEEP_SEEK"] = "LOW",
-- ["ALIBABA_CLOUD"] = "LOW",
-- ["PERPLEXITY"] = "MODERATE",
-- ["OPEN_ROUTER"] = "MODERATE",
-- ["FIREWORKS"] = "MODERATE",
-- ["GROQ"] = "MODERATE",
-- ["HUGGINGFACE"] = "MODERATE",
-- ["SELF_HOSTED"] = "HIGH",
-- ["HELMHOLTZ"] = "HIGH",
-- ["GWDG"] = "HIGH",
-- }
--
-- Configure whether users can change the custom confidence scheme locally.
-- CONFIG["SETTINGS"]["DataConfidence.CustomConfidenceScheme.AllowUserOverride"] = false
--
-- Configure provider instances trusted by your organization for data-source security checks.
-- These IDs may refer to LLM providers, embedding providers, or transcription providers
-- defined in this configuration. Trusted providers are treated like self-hosted providers
-- only for data-source security checks and related local data warnings.
-- CONFIG["SETTINGS"]["DataSourceSecuritySettings.TrustedProviderIds"] = {
-- "00000000-0000-0000-0000-000000000000",
-- "00000000-0000-0000-0000-000000000001",
-- }
-- Example chat templates for this configuration:
CONFIG["CHAT_TEMPLATES"] = {}
@ -336,6 +425,26 @@ CONFIG["CHAT_TEMPLATES"] = {}
-- }
-- }
-- Introduction texts shown as expansion panels on the welcome page:
CONFIG["INTRODUCTIONS"] = {}
-- An example introduction:
-- CONFIG["INTRODUCTIONS"][#CONFIG["INTRODUCTIONS"]+1] = {
-- ["Id"] = "00000000-0000-0000-0000-000000000000",
-- ["Title"] = "Welcome to Your Organization's AI Studio",
-- ["Version"] = "1",
-- ["Index"] = 1,
-- ["Markdown"] = [===[
-- ## Getting Started
--
-- This AI Studio installation is managed by your organization.
-- Please use the preconfigured providers and follow your internal
-- AI usage guidelines.
--
-- Further information is available in the [internal wiki](https://example.org/wiki).
-- ]===]
-- }
-- Mandatory infos that users must explicitly accept before using AI Studio:
-- AI Studio asks users again when Version, Title, or Markdown change.
-- Changing Version additionally allows the UI to communicate that a new version is available.

View File

@ -2793,6 +2793,54 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T922066419"]
-- Administration settings are not visible
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T929143445"] = "Die Optionen für die Administration sind nicht sichtbar."
-- Show provider's confidence level?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T1052533048"] = "Anzeigen, wie sicher der Anbieter ist?"
-- Choose the scheme that best suits you and your organization. Do you trust any western provider? Or only providers from the USA or exclusively European providers? Then choose the appropriate scheme. Alternatively, you can assign the confidence levels to each provider yourself.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T1081931329"] = "Wählen Sie das Schema, das am besten zu Ihnen und Ihrer Organisation passt. Vertrauen Sie irgendeinem westlichen Anbieter? Oder nur Anbietern aus den USA oder ausschließlich europäischen Anbietern? Wählen Sie dann das passende Schema. Alternativ können Sie auch die Vertrauensstufen für jeden Anbieter eigenständig festlegen."
-- Provider Confidence
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T1453422580"] = "Vertrauen in die Anbieter"
-- When enabled, you can enforce a minimum confidence level for all features in AI Studio. This way, you can make sure only trustworthy providers are used.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T1499004705"] = "Wenn aktiviert, können Sie für alle Funktionen in AI Studio ein minimales Vertrauensniveau festlegen. So können Sie sicherstellen, dass nur vertrauenswürdige Anbieter verwendet werden."
-- When enabled, we show you the confidence level for the selected provider in the app. This helps you assess where you are sending your data at any time. Example: are you currently working with sensitive data? Then choose a particularly trustworthy provider, etc.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T1505516304"] = "Wenn aktiviert, zeigen wir Ihnen in der App das Vertrauensniveau für den ausgewählten Anbieter an. So können Sie jederzeit einschätzen, wohin Ihre Daten gesendet werden. Beispiel: Arbeiten Sie gerade mit sensiblen Daten? Dann wählen Sie einen besonders vertrauenswürdigen Anbieter usw."
-- No, please hide the confidence level
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T1628475119"] = "Nein, bitte das Vertrauensniveau ausblenden"
-- Description
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T1725856265"] = "Beschreibung"
-- Confidence Level
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T2492230131"] = "Vertrauensniveau"
-- No, do not enforce a minimum confidence level
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T3642102079"] = "Nein, kein Mindestvertrauensniveau erzwingen"
-- Select a confidence scheme
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T4144206465"] = "Wählen Sie ein Vertrauensschema aus"
-- Do you want to enforce an global minimum confidence level?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T4211873175"] = "Möchten Sie ein globales Mindestvertrauensniveau festlegen?"
-- Yes, enforce a minimum confidence level
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T458854917"] = "Ja, ein Mindestvertrauensniveau erzwingen"
-- Not yet configured
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T48051324"] = "Noch nicht konfiguriert"
-- Do you want to always see how trustworthy your providers are? This way, you stay in control of which provider you send your data to. You can choose a common schema or configure the trust levels for each provider yourself.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T700839804"] = "Möchten Sie immer sehen, wie vertrauenswürdig Ihre Anbieter sind? So behalten Sie die Kontrolle darüber, an welchen Anbieter Sie Ihre Daten senden. Sie können ein gängiges Schema wählen oder die Vertrauensstufen für jeden Anbieter selbst festlegen."
-- Yes, show me the confidence level
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T853225204"] = "Ja, zeige mir das Vertrauensniveau"
-- Provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T900237532"] = "Anbieter"
-- Embedding Result
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1387042335"] = "Einbettungsergebnis"
@ -2847,6 +2895,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T32678
-- Close
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3448155331"] = "Schließen"
-- This embedding provider is trusted by your organization for data source security checks. Local data can be sent to it without security warnings.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3459188215"] = "Ihre Organisation vertraut diesem Anbieter von Einbettungen bei der Sicherheitsprüfung von Datenquellen. Lokale Daten können ohne Sicherheitswarnungen an diesen gesendet werden."
-- Actions
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3865031940"] = "Aktionen"
@ -2889,21 +2940,12 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERBASE::T336
-- Export API Key?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERBASE::T4010580285"] = "API-Schlüssel exportieren?"
-- Show provider's confidence level?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T1052533048"] = "Anzeigen, wie sicher sich der Anbieter ist?"
-- This provider is trusted by your organization for data source security checks.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T1298650849"] = "Ihre Organisation vertraut diesem Anbieter bei der Sicherheitsprüfung von Datenquellen."
-- Delete
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T1469573738"] = "Löschen"
-- When enabled, we show you the confidence level for the selected provider in the app. This helps you assess where you are sending your data at any time. Example: are you currently working with sensitive data? Then choose a particularly trustworthy provider, etc.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T1505516304"] = "Wenn diese Option aktiviert ist, zeigen wir Ihnen das Vertrauensniveau des ausgewählten Anbieters in der App an. So können Sie jederzeit einschätzen, wohin ihre Daten gesendet werden. Beispiel: Arbeiten Sie gerade mit sensiblen Daten? Dann wählen Sie einen besonders vertrauenswürdigen Anbieter usw."
-- No, please hide the confidence level
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T1628475119"] = "Nein, bitte verbergen Sie das Vertrauensniveau."
-- Description
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T1725856265"] = "Beschreibung"
-- Uses the provider-configured model
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T1760715963"] = "Verwendet das vom Anbieter konfigurierte Modell"
@ -2919,27 +2961,12 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T186876
-- Are you sure you want to delete the provider '{0}'?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T2031310917"] = "Möchten Sie den Anbieter „{0}“ wirklich löschen?"
-- Do you want to always be able to recognize how trustworthy your LLM providers are? This way, you keep control over which provider you send your data to. You have two options for this: Either you choose a common schema, or you configure the trust levels for each LLM provider yourself.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T2082904277"] = "Möchten Sie immer erkennen können, wie vertrauenswürdig ihre LLM-Anbieter sind? So behalten Sie die Kontrolle darüber, an welchen Anbieter Sie ihre Daten senden. Dafür haben Sie zwei Möglichkeiten: Entweder wählen Sie ein vorkonfiguriertes Schema, oder Sie konfigurieren die Vertrauensstufen für jeden LLM-Anbieter selbst."
-- Model
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T2189814010"] = "Modell"
-- Choose the scheme that best suits you and your life. Do you trust any western provider? Or only providers from the USA or exclusively European providers? Then choose the appropriate scheme. Alternatively, you can assign the confidence levels to each provider yourself.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T2283885378"] = "Wählen Sie das Schema, das am besten zu Ihnen und ihren Umständen passt. Vertrauen Sie einem westlichen Anbieter? Oder nur Anbietern aus den USA oder ausschließlich europäischen Anbietern? Dann wählen Sie das passende Schema aus. Alternativ können Sie auch die Vertrauensstufen für jeden Anbieter eigenständig festlegen."
-- LLM Provider Confidence
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T2349972795"] = "Vertrauenswürdigkeit in LLM-Anbieter"
-- What we call a provider is the combination of an LLM provider such as OpenAI and a model like GPT-4o. You can configure as many providers as you want. This way, you can use the appropriate model for each task. As an LLM provider, you can also choose local providers. However, to use this app, you must configure at least one provider.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T2460361126"] = "Was wir als „Anbieter“ bezeichnen, ist die Kombination aus einem LLM-Anbieter wie OpenAI und einem Modell wie GPT-4o. Sie können beliebig viele Anbieter einrichten. So können Sie für jede Aufgabe das passende Modell nutzen. Als LLM-Anbieter können Sie auch lokale Anbieter auswählen. Um diese App zu verwenden, müssen Sie jedoch mindestens einen Anbieter konfigurieren."
-- Confidence Level
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T2492230131"] = "Vertrauensniveau"
-- When enabled, you can enforce a minimum confidence level for all LLM providers. This way, you can ensure that only trustworthy providers are used.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T281063702"] = "Wenn aktiviert, können Sie ein minimales Vertrauensniveau für alle LLM-Anbieter festlegen. So stellen Sie sicher, dass nur vertrauenswürdige Anbieter verwendet werden."
-- Instance Name
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T2842060373"] = "Instanzname"
@ -2961,36 +2988,15 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T334643
-- This provider is managed by your organization.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T3415927576"] = "Dieser Anbieter wird von ihrer Organisation verwaltet."
-- LLM Provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T3612415205"] = "LLM-Anbieter"
-- No, do not enforce a minimum confidence level
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T3642102079"] = "Nein, kein Mindestvertrauensniveau erzwingen"
-- Actions
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T3865031940"] = "Aktionen"
-- Select a confidence scheme
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T4144206465"] = "Wählen Sie ein Vertrauensschema aus"
-- Do you want to enforce an app-wide minimum confidence level?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T4258968041"] = "Möchten Sie ein appweites Mindestvertrauensniveau festlegen?"
-- Delete LLM Provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T4269256234"] = "LLM-Anbieter löschen"
-- Yes, enforce a minimum confidence level
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T458854917"] = "Ja, ein Mindestvertrauensniveau erzwingen"
-- Not yet configured
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T48051324"] = "Noch nicht konfiguriert"
-- Open Dashboard
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T78223861"] = "Dashboard öffnen"
-- Yes, show me the confidence level
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T853225204"] = "Ja, zeige mir das Vertrauensniveau"
-- Provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T900237532"] = "Anbieter"
@ -3039,6 +3045,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T42
-- With the support of transcription models, MindWork AI Studio can convert human speech into text. This is useful, for example, when you need to dictate text. You can choose from dedicated transcription models, but not multimodal LLMs (large language models) that can handle both speech and text. The configuration of multimodal models is done in the 'Configure LLM providers' section.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T584860404"] = "Mit Unterstützung von Modellen für Transkriptionen kann MindWork AI Studio menschliche Sprache in Text umwandeln. Das ist zum Beispiel hilfreich, wenn Sie Texte diktieren möchten. Sie können aus speziellen Modellen für Transkriptionen wählen, jedoch nicht aus multimodalen LLMs (Large Language Models), die sowohl Sprache als auch Text verarbeiten können. Die Einrichtung multimodaler Modelle erfolgt im Abschnitt „Anbieter für LLMs konfigurieren“."
-- This transcription provider is trusted by your organization for data source security checks.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T601264181"] = "Ihre Organisation vertraut diesem Anbieter für Transkriptionen bei der Sicherheitsprüfung von Datenquellen."
-- This transcription provider is managed by your organization.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T756131076"] = "Dieser Anbieter für Transkriptionen wird von Ihrer Organisation verwaltet."
@ -3516,6 +3525,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3227981830"] = "Die gle
-- Add a message
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3372872324"] = "Nachricht hinzufügen"
-- Close
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3448155331"] = "Schließen"
-- Unsupported content type
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3570316759"] = "Nicht unterstützter Inhaltstyp"
@ -4296,6 +4308,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROFILEDIALOG::T3243902394"] = "Der Profilna
-- Profile Name
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROFILEDIALOG::T3392578705"] = "Profilname"
-- Close
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROFILEDIALOG::T3448155331"] = "Schließen"
-- Please enter what the LLM should know about you and/or what actions it should take.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROFILEDIALOG::T3708405102"] = "Bitte geben Sie ein, was das LLM über Sie wissen sollte und/oder welche Aktionen es ausführen soll."
@ -4815,6 +4830,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T14695
-- Add Chat Template
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1548314416"] = "Chat-Vorlage hinzufügen"
-- View
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1582017048"] = "Anzeigen"
-- Note: This advanced feature is designed for users familiar with prompt engineering concepts. Furthermore, you have to make sure yourself that your chosen provider supports the use of assistant prompts.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1909110760"] = "Hinweis: Diese fortgeschrittene Funktion richtet sich an Nutzer, die mit den Grundlagen des Prompt Engineerings vertraut sind. Außerdem müssen Sie selbst sicherstellen, dass Ihr gewählter Anbieter die Verwendung von Assistenten-Prompts unterstützt."
@ -4854,6 +4872,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T38650
-- Delete Chat Template
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T4025180906"] = "Chat-Vorlage löschen"
-- View Chat Template
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T4042112076"] = "Chat-Vorlage anzeigen"
-- Export Chat Template
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T491504763"] = "Chat-Vorlage exportieren"
@ -5262,6 +5283,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T143353473
-- Delete
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T1469573738"] = "Löschen"
-- View
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T1582017048"] = "Anzeigen"
-- Your Profiles
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T2378610256"] = "Ihre Profile"
@ -5286,6 +5310,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T405841465
-- Store personal data about yourself in various profiles so that the AIs know your personal context. This saves you from having to explain your context each time, for example, in every chat. When you have different roles, you can create a profile for each role.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T4125557797"] = "Speichern Sie persönliche Daten über sich in verschiedenen Profilen, damit die KIs ihren persönlichen Kontext kennen. So müssen Sie den Kontext nicht jedes Mal erneut erklären, zum Beispiel in jedem Chat. Wenn Sie verschiedene Rollen haben, können Sie für jede Rolle ein eigenes Profil anlegen."
-- View Profile
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T4219233997"] = "Profil anzeigen"
-- Add Profile
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T4248067241"] = "Profil hinzufügen"
@ -5817,12 +5844,21 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T3288132732"] = "B
-- Cancel
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T900713019"] = "Abbrechen"
-- Reason
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1093747001"] = "Begründung"
-- Settings
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1258653480"] = "Einstellungen"
-- Your settings file does not contain a settings-format version. Changes in this session will not be saved to avoid overwriting your settings. Please check for updates or contact support.
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1378304679"] = "Ihre Einstellungsdatei enthält keine Versionsangabe des Einstellungsformats. Änderungen in dieser Sitzung werden nicht gespeichert, um ein Überschreiben Ihrer Einstellungen zu vermeiden. Bitte suchen Sie nach Updates oder wenden Sie sich an den Support."
-- Home
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1391791790"] = "Startseite"
-- AI Studio found the current settings format but could not load it safely. Changes in this session will not be saved. Please check for updates or contact support.
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1497084127"] = "AI Studio hat das aktuelle Einstellungsformat gefunden, konnte es jedoch nicht sicher laden. Änderungen in dieser Sitzung werden nicht gespeichert. Bitte suchen Sie nach Updates oder wenden Sie sich an den Support."
-- Are you sure you want to leave the chat page? All unsaved changes will be lost.
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1563130494"] = "Sind Sie sicher, dass Sie die Chat-Seite verlassen möchten? Alle nicht gespeicherten Änderungen gehen verloren."
@ -5832,12 +5868,21 @@ UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1614176092"] = "Assistenten"
-- Update
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1847791252"] = "Aktualisieren"
-- Check for updates
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1890416390"] = "Nach Updates suchen"
-- Your settings were created by a newer AI Studio version. Changes in this session will not be saved. Please install or start the latest available update.
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1988273622"] = "Ihre Einstellungen wurden mit einer neueren Version von AI Studio erstellt. Änderungen in dieser Sitzung werden nicht gespeichert. Bitte installieren oder starten Sie das neueste verfügbare Update."
-- Leave Chat Page
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2124749705"] = "Chat-Seite verlassen"
-- Plugins
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2222816203"] = "Plugins"
-- AI Studio cannot safely save settings in this session. Please check for updates or contact support.
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2382622618"] = "AI Studio kann die Einstellungen in dieser Sitzung nicht sicher speichern. Bitte suchen Sie nach Updates oder wenden Sie sich an den Support."
-- An update to version {0} is available.
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2800137365"] = "Ein Update auf Version {0} ist verfügbar."
@ -5847,6 +5892,9 @@ UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2864211629"] = "Bitte warten Sie
-- Supporters
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2929332068"] = "Unterstützer"
-- AI Studio could not read your settings file. Changes in this session will not be saved to avoid overwriting recoverable settings. Please check for updates or contact support.
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2936083926"] = "AI Studio konnte Ihre Einstellungsdatei nicht lesen. Änderungen in dieser Sitzung werden nicht gespeichert, um ein Überschreiben wiederherstellbarer Einstellungen zu vermeiden. Bitte suchen Sie nach Updates oder wenden Sie sich an den Support."
-- Writing
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2979224202"] = "Schreiben"
@ -5859,6 +5907,9 @@ UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T4256323669"] = "Information"
-- Chat
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T578410699"] = "Chat"
-- AI Studio does not recognize your settings-format version. Changes in this session will not be saved to avoid overwriting your settings. Please check for updates or contact support.
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T915412625"] = "AI Studio erkennt die Version Ihres Einstellungsformats nicht. Änderungen in dieser Sitzung werden nicht gespeichert, um zu verhindern, dass Ihre Einstellungen überschrieben werden. Bitte suchen Sie nach Updates oder wenden Sie sich an den Support."
-- Get coding and debugging support from an LLM.
UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1243850917"] = "Erhalten Sie Unterstützung beim Programmieren und Debuggen durch ein KI-Modell."
@ -6042,6 +6093,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T144565305"] = "Die App benötigt nur we
-- You only pay for what you use, which can be cheaper than monthly subscription services like ChatGPT Plus, especially if used infrequently. But beware, here be dragons: For extremely intensive usage, the API costs can be significantly higher. Unfortunately, providers currently do not offer a way to display current costs in the app. Therefore, check your account with the respective provider to see how your costs are developing. When available, use prepaid and set a cost limit.
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T149711988"] = "Sie zahlen nur für das, was Sie tatsächlich nutzen das kann günstiger sein als monatliche Abos wie ChatGPT Plus, vor allem bei gelegentlicher Nutzung. Aber Vorsicht: Bei sehr intensiver Nutzung können die API-Kosten deutlich höher ausfallen. Leider bieten die Anbieter derzeit keine Möglichkeit, die aktuellen Kosten direkt in der App anzuzeigen. Prüfen Sie deshalb regelmäßig Ihr Konto beim jeweiligen Anbieter, um ihre Ausgaben im Blick zu behalten. Nutzen Sie, wenn möglich, Prepaid-Optionen und legen Sie ein Ausgabenlimit fest."
-- Version
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1573770551"] = "Version"
-- Assistants
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1614176092"] = "Assistenten"
@ -7872,6 +7926,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T25964655
-- Failed to store the secret data due to an API issue.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T1110203516"] = "Fehler beim Speichern der geheimen Daten aufgrund eines API-Problems."
-- Failed to store the API key due to an API issue.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T1704298921"] = "Fehler beim Speichern des API-Schlüssels aufgrund eines API-Problems."
-- Failed to delete the secret data due to an API issue.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T2303057928"] = "Das Löschen der geheimen Daten ist aufgrund eines API-Problems fehlgeschlagen."

View File

@ -2793,6 +2793,54 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T922066419"]
-- Administration settings are not visible
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T929143445"] = "Administration settings are not visible"
-- Show provider's confidence level?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T1052533048"] = "Show provider's confidence level?"
-- Choose the scheme that best suits you and your organization. Do you trust any western provider? Or only providers from the USA or exclusively European providers? Then choose the appropriate scheme. Alternatively, you can assign the confidence levels to each provider yourself.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T1081931329"] = "Choose the scheme that best suits you and your organization. Do you trust any western provider? Or only providers from the USA or exclusively European providers? Then choose the appropriate scheme. Alternatively, you can assign the confidence levels to each provider yourself."
-- Provider Confidence
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T1453422580"] = "Provider Confidence"
-- When enabled, you can enforce a minimum confidence level for all features in AI Studio. This way, you can make sure only trustworthy providers are used.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T1499004705"] = "When enabled, you can enforce a minimum confidence level for all features in AI Studio. This way, you can make sure only trustworthy providers are used."
-- When enabled, we show you the confidence level for the selected provider in the app. This helps you assess where you are sending your data at any time. Example: are you currently working with sensitive data? Then choose a particularly trustworthy provider, etc.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T1505516304"] = "When enabled, we show you the confidence level for the selected provider in the app. This helps you assess where you are sending your data at any time. Example: are you currently working with sensitive data? Then choose a particularly trustworthy provider, etc."
-- No, please hide the confidence level
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T1628475119"] = "No, please hide the confidence level"
-- Description
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T1725856265"] = "Description"
-- Confidence Level
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T2492230131"] = "Confidence Level"
-- No, do not enforce a minimum confidence level
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T3642102079"] = "No, do not enforce a minimum confidence level"
-- Select a confidence scheme
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T4144206465"] = "Select a confidence scheme"
-- Do you want to enforce an global minimum confidence level?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T4211873175"] = "Do you want to enforce an global minimum confidence level?"
-- Yes, enforce a minimum confidence level
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T458854917"] = "Yes, enforce a minimum confidence level"
-- Not yet configured
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T48051324"] = "Not yet configured"
-- Do you want to always see how trustworthy your providers are? This way, you stay in control of which provider you send your data to. You can choose a common schema or configure the trust levels for each provider yourself.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T700839804"] = "Do you want to always see how trustworthy your providers are? This way, you stay in control of which provider you send your data to. You can choose a common schema or configure the trust levels for each provider yourself."
-- Yes, show me the confidence level
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T853225204"] = "Yes, show me the confidence level"
-- Provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELCONFIDENCE::T900237532"] = "Provider"
-- Embedding Result
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1387042335"] = "Embedding Result"
@ -2847,6 +2895,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T32678
-- Close
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3448155331"] = "Close"
-- This embedding provider is trusted by your organization for data source security checks. Local data can be sent to it without security warnings.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3459188215"] = "This embedding provider is trusted by your organization for data source security checks. Local data can be sent to it without security warnings."
-- Actions
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3865031940"] = "Actions"
@ -2889,21 +2940,12 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERBASE::T336
-- Export API Key?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERBASE::T4010580285"] = "Export API Key?"
-- Show provider's confidence level?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T1052533048"] = "Show provider's confidence level?"
-- This provider is trusted by your organization for data source security checks.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T1298650849"] = "This provider is trusted by your organization for data source security checks."
-- Delete
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T1469573738"] = "Delete"
-- When enabled, we show you the confidence level for the selected provider in the app. This helps you assess where you are sending your data at any time. Example: are you currently working with sensitive data? Then choose a particularly trustworthy provider, etc.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T1505516304"] = "When enabled, we show you the confidence level for the selected provider in the app. This helps you assess where you are sending your data at any time. Example: are you currently working with sensitive data? Then choose a particularly trustworthy provider, etc."
-- No, please hide the confidence level
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T1628475119"] = "No, please hide the confidence level"
-- Description
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T1725856265"] = "Description"
-- Uses the provider-configured model
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T1760715963"] = "Uses the provider-configured model"
@ -2919,27 +2961,12 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T186876
-- Are you sure you want to delete the provider '{0}'?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T2031310917"] = "Are you sure you want to delete the provider '{0}'?"
-- Do you want to always be able to recognize how trustworthy your LLM providers are? This way, you keep control over which provider you send your data to. You have two options for this: Either you choose a common schema, or you configure the trust levels for each LLM provider yourself.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T2082904277"] = "Do you want to always be able to recognize how trustworthy your LLM providers are? This way, you keep control over which provider you send your data to. You have two options for this: Either you choose a common schema, or you configure the trust levels for each LLM provider yourself."
-- Model
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T2189814010"] = "Model"
-- Choose the scheme that best suits you and your life. Do you trust any western provider? Or only providers from the USA or exclusively European providers? Then choose the appropriate scheme. Alternatively, you can assign the confidence levels to each provider yourself.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T2283885378"] = "Choose the scheme that best suits you and your life. Do you trust any western provider? Or only providers from the USA or exclusively European providers? Then choose the appropriate scheme. Alternatively, you can assign the confidence levels to each provider yourself."
-- LLM Provider Confidence
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T2349972795"] = "LLM Provider Confidence"
-- What we call a provider is the combination of an LLM provider such as OpenAI and a model like GPT-4o. You can configure as many providers as you want. This way, you can use the appropriate model for each task. As an LLM provider, you can also choose local providers. However, to use this app, you must configure at least one provider.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T2460361126"] = "What we call a provider is the combination of an LLM provider such as OpenAI and a model like GPT-4o. You can configure as many providers as you want. This way, you can use the appropriate model for each task. As an LLM provider, you can also choose local providers. However, to use this app, you must configure at least one provider."
-- Confidence Level
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T2492230131"] = "Confidence Level"
-- When enabled, you can enforce a minimum confidence level for all LLM providers. This way, you can ensure that only trustworthy providers are used.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T281063702"] = "When enabled, you can enforce a minimum confidence level for all LLM providers. This way, you can ensure that only trustworthy providers are used."
-- Instance Name
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T2842060373"] = "Instance Name"
@ -2961,36 +2988,15 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T334643
-- This provider is managed by your organization.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T3415927576"] = "This provider is managed by your organization."
-- LLM Provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T3612415205"] = "LLM Provider"
-- No, do not enforce a minimum confidence level
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T3642102079"] = "No, do not enforce a minimum confidence level"
-- Actions
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T3865031940"] = "Actions"
-- Select a confidence scheme
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T4144206465"] = "Select a confidence scheme"
-- Do you want to enforce an app-wide minimum confidence level?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T4258968041"] = "Do you want to enforce an app-wide minimum confidence level?"
-- Delete LLM Provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T4269256234"] = "Delete LLM Provider"
-- Yes, enforce a minimum confidence level
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T458854917"] = "Yes, enforce a minimum confidence level"
-- Not yet configured
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T48051324"] = "Not yet configured"
-- Open Dashboard
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T78223861"] = "Open Dashboard"
-- Yes, show me the confidence level
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T853225204"] = "Yes, show me the confidence level"
-- Provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T900237532"] = "Provider"
@ -3039,6 +3045,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T42
-- With the support of transcription models, MindWork AI Studio can convert human speech into text. This is useful, for example, when you need to dictate text. You can choose from dedicated transcription models, but not multimodal LLMs (large language models) that can handle both speech and text. The configuration of multimodal models is done in the 'Configure LLM providers' section.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T584860404"] = "With the support of transcription models, MindWork AI Studio can convert human speech into text. This is useful, for example, when you need to dictate text. You can choose from dedicated transcription models, but not multimodal LLMs (large language models) that can handle both speech and text. The configuration of multimodal models is done in the 'Configure LLM providers' section."
-- This transcription provider is trusted by your organization for data source security checks.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T601264181"] = "This transcription provider is trusted by your organization for data source security checks."
-- This transcription provider is managed by your organization.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T756131076"] = "This transcription provider is managed by your organization."
@ -3516,6 +3525,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3227981830"] = "Using s
-- Add a message
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3372872324"] = "Add a message"
-- Close
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3448155331"] = "Close"
-- Unsupported content type
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3570316759"] = "Unsupported content type"
@ -4296,6 +4308,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROFILEDIALOG::T3243902394"] = "The profile
-- Profile Name
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROFILEDIALOG::T3392578705"] = "Profile Name"
-- Close
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROFILEDIALOG::T3448155331"] = "Close"
-- Please enter what the LLM should know about you and/or what actions it should take.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROFILEDIALOG::T3708405102"] = "Please enter what the LLM should know about you and/or what actions it should take."
@ -4815,6 +4830,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T14695
-- Add Chat Template
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1548314416"] = "Add Chat Template"
-- View
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1582017048"] = "View"
-- Note: This advanced feature is designed for users familiar with prompt engineering concepts. Furthermore, you have to make sure yourself that your chosen provider supports the use of assistant prompts.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1909110760"] = "Note: This advanced feature is designed for users familiar with prompt engineering concepts. Furthermore, you have to make sure yourself that your chosen provider supports the use of assistant prompts."
@ -4854,6 +4872,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T38650
-- Delete Chat Template
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T4025180906"] = "Delete Chat Template"
-- View Chat Template
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T4042112076"] = "View Chat Template"
-- Export Chat Template
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T491504763"] = "Export Chat Template"
@ -5262,6 +5283,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T143353473
-- Delete
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T1469573738"] = "Delete"
-- View
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T1582017048"] = "View"
-- Your Profiles
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T2378610256"] = "Your Profiles"
@ -5286,6 +5310,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T405841465
-- Store personal data about yourself in various profiles so that the AIs know your personal context. This saves you from having to explain your context each time, for example, in every chat. When you have different roles, you can create a profile for each role.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T4125557797"] = "Store personal data about yourself in various profiles so that the AIs know your personal context. This saves you from having to explain your context each time, for example, in every chat. When you have different roles, you can create a profile for each role."
-- View Profile
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T4219233997"] = "View Profile"
-- Add Profile
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T4248067241"] = "Add Profile"
@ -5817,12 +5844,21 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T3288132732"] = "P
-- Cancel
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T900713019"] = "Cancel"
-- Reason
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1093747001"] = "Reason"
-- Settings
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1258653480"] = "Settings"
-- Your settings file does not contain a settings-format version. Changes in this session will not be saved to avoid overwriting your settings. Please check for updates or contact support.
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1378304679"] = "Your settings file does not contain a settings-format version. Changes in this session will not be saved to avoid overwriting your settings. Please check for updates or contact support."
-- Home
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1391791790"] = "Home"
-- AI Studio found the current settings format but could not load it safely. Changes in this session will not be saved. Please check for updates or contact support.
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1497084127"] = "AI Studio found the current settings format but could not load it safely. Changes in this session will not be saved. Please check for updates or contact support."
-- Are you sure you want to leave the chat page? All unsaved changes will be lost.
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1563130494"] = "Are you sure you want to leave the chat page? All unsaved changes will be lost."
@ -5832,12 +5868,21 @@ UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1614176092"] = "Assistants"
-- Update
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1847791252"] = "Update"
-- Check for updates
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1890416390"] = "Check for updates"
-- Your settings were created by a newer AI Studio version. Changes in this session will not be saved. Please install or start the latest available update.
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1988273622"] = "Your settings were created by a newer AI Studio version. Changes in this session will not be saved. Please install or start the latest available update."
-- Leave Chat Page
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2124749705"] = "Leave Chat Page"
-- Plugins
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2222816203"] = "Plugins"
-- AI Studio cannot safely save settings in this session. Please check for updates or contact support.
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2382622618"] = "AI Studio cannot safely save settings in this session. Please check for updates or contact support."
-- An update to version {0} is available.
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2800137365"] = "An update to version {0} is available."
@ -5847,6 +5892,9 @@ UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2864211629"] = "Please wait for
-- Supporters
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2929332068"] = "Supporters"
-- AI Studio could not read your settings file. Changes in this session will not be saved to avoid overwriting recoverable settings. Please check for updates or contact support.
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2936083926"] = "AI Studio could not read your settings file. Changes in this session will not be saved to avoid overwriting recoverable settings. Please check for updates or contact support."
-- Writing
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2979224202"] = "Writing"
@ -5859,6 +5907,9 @@ UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T4256323669"] = "Information"
-- Chat
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T578410699"] = "Chat"
-- AI Studio does not recognize your settings-format version. Changes in this session will not be saved to avoid overwriting your settings. Please check for updates or contact support.
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T915412625"] = "AI Studio does not recognize your settings-format version. Changes in this session will not be saved to avoid overwriting your settings. Please check for updates or contact support."
-- Get coding and debugging support from an LLM.
UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1243850917"] = "Get coding and debugging support from an LLM."
@ -6042,6 +6093,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T144565305"] = "The app requires minimal
-- You only pay for what you use, which can be cheaper than monthly subscription services like ChatGPT Plus, especially if used infrequently. But beware, here be dragons: For extremely intensive usage, the API costs can be significantly higher. Unfortunately, providers currently do not offer a way to display current costs in the app. Therefore, check your account with the respective provider to see how your costs are developing. When available, use prepaid and set a cost limit.
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T149711988"] = "You only pay for what you use, which can be cheaper than monthly subscription services like ChatGPT Plus, especially if used infrequently. But beware, here be dragons: For extremely intensive usage, the API costs can be significantly higher. Unfortunately, providers currently do not offer a way to display current costs in the app. Therefore, check your account with the respective provider to see how your costs are developing. When available, use prepaid and set a cost limit."
-- Version
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1573770551"] = "Version"
-- Assistants
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1614176092"] = "Assistants"
@ -7872,6 +7926,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T25964655
-- Failed to store the secret data due to an API issue.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T1110203516"] = "Failed to store the secret data due to an API issue."
-- Failed to store the API key due to an API issue.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T1704298921"] = "Failed to store the API key due to an API issue."
-- Failed to delete the secret data due to an API issue.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T2303057928"] = "Failed to delete the secret data due to an API issue."

View File

@ -13,7 +13,7 @@ public sealed class ProviderAlibabaCloud() : BaseProvider(LLMProviders.ALIBABA_C
#region Implementation of IProvider
/// <inheritdoc />
public override string Id => LLMProviders.ALIBABA_CLOUD.ToName();
public override string Id => LLMProviders.ALIBABA_CLOUD.ToSecretId();
/// <inheritdoc />
public override string InstanceName { get; set; } = "AlibabaCloud";

View File

@ -15,7 +15,7 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, n
#region Implementation of IProvider
/// <inheritdoc />
public override string Id => LLMProviders.ANTHROPIC.ToName();
public override string Id => LLMProviders.ANTHROPIC.ToSecretId();
/// <inheritdoc />
public override string InstanceName { get; set; } = "Anthropic";

View File

@ -77,6 +77,9 @@ public abstract class BaseProvider : IProvider, ISecretId
/// <inheritdoc />
public abstract string Id { get; }
/// <inheritdoc />
public string ConfiguredProviderId { get; init; } = string.Empty;
/// <inheritdoc />
public abstract string InstanceName { get; set; }

View File

@ -13,7 +13,7 @@ public sealed class ProviderDeepSeek() : BaseProvider(LLMProviders.DEEP_SEEK, ne
#region Implementation of IProvider
/// <inheritdoc />
public override string Id => LLMProviders.DEEP_SEEK.ToName();
public override string Id => LLMProviders.DEEP_SEEK.ToSecretId();
/// <inheritdoc />
public override string InstanceName { get; set; } = "DeepSeek";

View File

@ -13,7 +13,7 @@ public class ProviderFireworks() : BaseProvider(LLMProviders.FIREWORKS, new Uri(
#region Implementation of IProvider
/// <inheritdoc />
public override string Id => LLMProviders.FIREWORKS.ToName();
public override string Id => LLMProviders.FIREWORKS.ToSecretId();
/// <inheritdoc />
public override string InstanceName { get; set; } = "Fireworks.ai";

View File

@ -13,7 +13,7 @@ public sealed class ProviderGWDG() : BaseProvider(LLMProviders.GWDG, new Uri("ht
#region Implementation of IProvider
/// <inheritdoc />
public override string Id => LLMProviders.GWDG.ToName();
public override string Id => LLMProviders.GWDG.ToSecretId();
/// <inheritdoc />
public override string InstanceName { get; set; } = "GWDG SAIA";

View File

@ -15,7 +15,7 @@ public class ProviderGoogle() : BaseProvider(LLMProviders.GOOGLE, new Uri("https
#region Implementation of IProvider
/// <inheritdoc />
public override string Id => LLMProviders.GOOGLE.ToName();
public override string Id => LLMProviders.GOOGLE.ToSecretId();
/// <inheritdoc />
public override string InstanceName { get; set; } = "Google Gemini";

View File

@ -13,7 +13,7 @@ public class ProviderGroq() : BaseProvider(LLMProviders.GROQ, new Uri("https://a
#region Implementation of IProvider
/// <inheritdoc />
public override string Id => LLMProviders.GROQ.ToName();
public override string Id => LLMProviders.GROQ.ToSecretId();
/// <inheritdoc />
public override string InstanceName { get; set; } = "Groq";

View File

@ -15,7 +15,7 @@ public sealed class ProviderHelmholtz() : BaseProvider(LLMProviders.HELMHOLTZ, n
#region Implementation of IProvider
/// <inheritdoc />
public override string Id => LLMProviders.HELMHOLTZ.ToName();
public override string Id => LLMProviders.HELMHOLTZ.ToSecretId();
/// <inheritdoc />
public override string InstanceName { get; set; } = "Helmholtz Blablador";

View File

@ -18,7 +18,7 @@ public sealed class ProviderHuggingFace : BaseProvider
#region Implementation of IProvider
/// <inheritdoc />
public override string Id => LLMProviders.HUGGINGFACE.ToName();
public override string Id => LLMProviders.HUGGINGFACE.ToSecretId();
/// <inheritdoc />
public override string InstanceName { get; set; } = "HuggingFace";

View File

@ -18,6 +18,11 @@ public interface IProvider
/// </summary>
public string Id { get; }
/// <summary>
/// The ID of the configured provider instance.
/// </summary>
public string ConfiguredProviderId { get; }
/// <summary>
/// The provider's instance name. Useful for multiple instances of the same provider,
/// e.g., to distinguish between different OpenAI API keys.

View File

@ -29,6 +29,10 @@ public static class LLMProvidersExtensions
/// <summary>
/// Returns the human-readable name of the provider.
/// </summary>
/// <remarks>
/// This value is UI text and may be localized. Do not use it for persisted IDs, secret namespaces,
/// or other stable identifiers.
/// </remarks>
/// <param name="llmProvider">The provider.</param>
/// <returns>The human-readable name of the provider.</returns>
public static string ToName(this LLMProviders llmProvider) => llmProvider switch
@ -56,6 +60,41 @@ public static class LLMProvidersExtensions
_ => TB("Unknown"),
};
/// <summary>
/// Returns the stable secret namespace for the provider.
/// </summary>
/// <remarks>
/// These values are used for OS keyring namespaces. They must never be localized or changed without
/// an explicit migration for existing API keys.
/// </remarks>
/// <param name="llmProvider">The provider.</param>
/// <returns>The stable secret namespace for the provider.</returns>
public static string ToSecretId(this LLMProviders llmProvider) => llmProvider switch
{
LLMProviders.NONE => "No provider selected",
LLMProviders.OPEN_AI => "OpenAI",
LLMProviders.ANTHROPIC => "Anthropic",
LLMProviders.MISTRAL => "Mistral",
LLMProviders.GOOGLE => "Google",
LLMProviders.X => "xAI",
LLMProviders.DEEP_SEEK => "DeepSeek",
LLMProviders.ALIBABA_CLOUD => "Alibaba Cloud",
LLMProviders.PERPLEXITY => "Perplexity",
LLMProviders.OPEN_ROUTER => "OpenRouter",
LLMProviders.GROQ => "Groq",
LLMProviders.FIREWORKS => "Fireworks.ai",
LLMProviders.HUGGINGFACE => "Hugging Face",
LLMProviders.SELF_HOSTED => "Self-hosted",
LLMProviders.HELMHOLTZ => "Helmholtz Blablador",
LLMProviders.GWDG => "GWDG SAIA",
_ => "Unknown",
};
/// <summary>
/// Get a provider's confidence.
@ -186,7 +225,7 @@ public static class LLMProvidersExtensions
/// <returns>The provider instance.</returns>
public static IProvider CreateProvider(this AIStudio.Settings.Provider providerSettings)
{
return providerSettings.UsedLLMProvider.CreateProvider(providerSettings.InstanceName, providerSettings.Host, providerSettings.Hostname, providerSettings.Model, providerSettings.HFInferenceProvider, providerSettings.AdditionalJsonApiParameters, providerSettings.IsEnterpriseConfiguration);
return providerSettings.UsedLLMProvider.CreateProvider(providerSettings.InstanceName, providerSettings.Host, providerSettings.Hostname, providerSettings.Model, providerSettings.HFInferenceProvider, providerSettings.Id, providerSettings.AdditionalJsonApiParameters, providerSettings.IsEnterpriseConfiguration);
}
/// <summary>
@ -196,7 +235,7 @@ public static class LLMProvidersExtensions
/// <returns>The provider instance.</returns>
public static IProvider CreateProvider(this EmbeddingProvider embeddingProviderSettings)
{
return embeddingProviderSettings.UsedLLMProvider.CreateProvider(embeddingProviderSettings.Name, embeddingProviderSettings.Host, embeddingProviderSettings.Hostname, embeddingProviderSettings.Model, HFInferenceProvider.NONE, isEnterpriseConfiguration: embeddingProviderSettings.IsEnterpriseConfiguration);
return embeddingProviderSettings.UsedLLMProvider.CreateProvider(embeddingProviderSettings.Name, embeddingProviderSettings.Host, embeddingProviderSettings.Hostname, embeddingProviderSettings.Model, HFInferenceProvider.NONE, configuredProviderId: embeddingProviderSettings.Id, isEnterpriseConfiguration: embeddingProviderSettings.IsEnterpriseConfiguration);
}
/// <summary>
@ -206,33 +245,33 @@ public static class LLMProvidersExtensions
/// <returns>The provider instance.</returns>
public static IProvider CreateProvider(this TranscriptionProvider transcriptionProviderSettings)
{
return transcriptionProviderSettings.UsedLLMProvider.CreateProvider(transcriptionProviderSettings.Name, transcriptionProviderSettings.Host, transcriptionProviderSettings.Hostname, transcriptionProviderSettings.Model, HFInferenceProvider.NONE, isEnterpriseConfiguration: transcriptionProviderSettings.IsEnterpriseConfiguration);
return transcriptionProviderSettings.UsedLLMProvider.CreateProvider(transcriptionProviderSettings.Name, transcriptionProviderSettings.Host, transcriptionProviderSettings.Hostname, transcriptionProviderSettings.Model, HFInferenceProvider.NONE, configuredProviderId: transcriptionProviderSettings.Id, isEnterpriseConfiguration: transcriptionProviderSettings.IsEnterpriseConfiguration);
}
private static IProvider CreateProvider(this LLMProviders provider, string instanceName, Host host, string hostname, Model model, HFInferenceProvider inferenceProvider, string expertProviderApiParameter = "", bool isEnterpriseConfiguration = false)
private static IProvider CreateProvider(this LLMProviders provider, string instanceName, Host host, string hostname, Model model, HFInferenceProvider inferenceProvider, string configuredProviderId = "", string expertProviderApiParameter = "", bool isEnterpriseConfiguration = false)
{
try
{
return provider switch
{
LLMProviders.OPEN_AI => new ProviderOpenAI { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.ANTHROPIC => new ProviderAnthropic { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.MISTRAL => new ProviderMistral { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.GOOGLE => new ProviderGoogle { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.X => new ProviderX { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.DEEP_SEEK => new ProviderDeepSeek { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.ALIBABA_CLOUD => new ProviderAlibabaCloud { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.PERPLEXITY => new ProviderPerplexity { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.OPEN_ROUTER => new ProviderOpenRouter { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.OPEN_AI => new ProviderOpenAI { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.ANTHROPIC => new ProviderAnthropic { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.MISTRAL => new ProviderMistral { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.GOOGLE => new ProviderGoogle { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.X => new ProviderX { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.DEEP_SEEK => new ProviderDeepSeek { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.ALIBABA_CLOUD => new ProviderAlibabaCloud { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.PERPLEXITY => new ProviderPerplexity { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.OPEN_ROUTER => new ProviderOpenRouter { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.GROQ => new ProviderGroq { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.FIREWORKS => new ProviderFireworks { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.HUGGINGFACE => new ProviderHuggingFace(inferenceProvider, model) { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.GROQ => new ProviderGroq { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.FIREWORKS => new ProviderFireworks { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.HUGGINGFACE => new ProviderHuggingFace(inferenceProvider, model) { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.SELF_HOSTED => new ProviderSelfHosted(host, hostname) { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.SELF_HOSTED => new ProviderSelfHosted(host, hostname) { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.HELMHOLTZ => new ProviderHelmholtz { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.GWDG => new ProviderGWDG { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.HELMHOLTZ => new ProviderHelmholtz { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
LLMProviders.GWDG => new ProviderGWDG { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration },
_ => new NoProvider(),
};

View File

@ -13,7 +13,7 @@ public sealed class ProviderMistral() : BaseProvider(LLMProviders.MISTRAL, new U
#region Implementation of IProvider
/// <inheritdoc />
public override string Id => LLMProviders.MISTRAL.ToName();
public override string Id => LLMProviders.MISTRAL.ToSecretId();
/// <inheritdoc />
public override string InstanceName { get; set; } = "Mistral";

View File

@ -13,6 +13,8 @@ public class NoProvider : IProvider
public string Id => "none";
public string ConfiguredProviderId => string.Empty;
public string InstanceName { get; set; } = "None";
/// <inheritdoc />

View File

@ -22,7 +22,7 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur
#region Implementation of IProvider
/// <inheritdoc />
public override string Id => LLMProviders.OPEN_AI.ToName();
public override string Id => LLMProviders.OPEN_AI.ToSecretId();
/// <inheritdoc />
public override string InstanceName { get; set; } = "OpenAI";

View File

@ -17,7 +17,7 @@ public sealed class ProviderOpenRouter() : BaseProvider(LLMProviders.OPEN_ROUTER
#region Implementation of IProvider
/// <inheritdoc />
public override string Id => LLMProviders.OPEN_ROUTER.ToName();
public override string Id => LLMProviders.OPEN_ROUTER.ToSecretId();
/// <inheritdoc />
public override string InstanceName { get; set; } = "OpenRouter";

View File

@ -22,7 +22,7 @@ public sealed class ProviderPerplexity() : BaseProvider(LLMProviders.PERPLEXITY,
#region Implementation of IProvider
/// <inheritdoc />
public override string Id => LLMProviders.PERPLEXITY.ToName();
public override string Id => LLMProviders.PERPLEXITY.ToSecretId();
/// <inheritdoc />
public override string InstanceName { get; set; } = "Perplexity";

View File

@ -18,7 +18,7 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide
#region Implementation of IProvider
/// <inheritdoc />
public override string Id => LLMProviders.SELF_HOSTED.ToName();
public override string Id => LLMProviders.SELF_HOSTED.ToSecretId();
/// <inheritdoc />
public override string InstanceName { get; set; } = "Self-hosted";

View File

@ -13,7 +13,7 @@ public sealed class ProviderX() : BaseProvider(LLMProviders.X, new Uri("https://
#region Implementation of IProvider
/// <inheritdoc />
public override string Id => LLMProviders.X.ToName();
public override string Id => LLMProviders.X.ToSecretId();
/// <inheritdoc />
public override string InstanceName { get; set; } = "xAI";

View File

@ -271,8 +271,8 @@ public static class ConfigurationSelectDataFactory
public static IEnumerable<ConfigurationSelectData<ConfidenceLevel>> GetConfidenceLevelsData(SettingsManager settingsManager, bool restrictToGlobalMinimum = false)
{
var minimumLevel = ConfidenceLevel.NONE;
if(restrictToGlobalMinimum && settingsManager.ConfigurationData.LLMProviders is { EnforceGlobalMinimumConfidence: true, GlobalMinimumConfidence: not ConfidenceLevel.NONE and not ConfidenceLevel.UNKNOWN })
minimumLevel = settingsManager.ConfigurationData.LLMProviders.GlobalMinimumConfidence;
if(restrictToGlobalMinimum && settingsManager.ConfigurationData.Confidence is { EnforceGlobalMinimumConfidence: true, GlobalMinimumConfidence: not ConfidenceLevel.NONE and not ConfidenceLevel.UNKNOWN })
minimumLevel = settingsManager.ConfigurationData.Confidence.GlobalMinimumConfidence;
foreach (var level in Enum.GetValues<ConfidenceLevel>())
{

View File

@ -11,7 +11,7 @@ public sealed class Data
/// The version of the settings file. Allows us to upgrade the settings
/// when a new version is available.
/// </summary>
public Version Version { get; init; } = Version.V5;
public Version Version { get; init; } = Version.V6;
/// <summary>
/// List of configured providers.
@ -19,9 +19,14 @@ public sealed class Data
public List<Provider> Providers { get; init; } = [];
/// <summary>
/// Settings concerning the LLM providers.
/// Settings concerning confidence levels.
/// </summary>
public DataLLMProviders LLMProviders { get; init; } = new();
public DataConfidence Confidence { get; init; } = new(x => x.Confidence);
/// <summary>
/// Settings concerning data source security checks.
/// </summary>
public DataSourceSecuritySettings DataSourceSecurity { get; init; } = new(x => x.DataSourceSecurity);
/// <summary>
/// A collection of embedding providers configured.
@ -100,7 +105,7 @@ public sealed class Data
public DataApp App { get; init; } = new(x => x.App);
public DataChat Chat { get; init; } = new();
public DataChat Chat { get; init; } = new(x => x.Chat);
public DataWorkspace Workspace { get; init; } = new();

View File

@ -57,6 +57,11 @@ public sealed class DataApp(Expression<Func<Data, DataApp>>? configSelection = n
/// </summary>
public StartPage StartPage { get; set; } = ManagedConfiguration.Register(configSelection, n => n.StartPage, StartPage.HOME);
/// <summary>
/// Should the built-in introduction be visible on the home page?
/// </summary>
public bool ShowIntroduction { get; set; } = ManagedConfiguration.Register(configSelection, n => n.ShowIntroduction, true);
/// <summary>
/// Should the quick start guide be visible on the home page?
/// </summary>

View File

@ -1,7 +1,16 @@
using System.Linq.Expressions;
namespace AIStudio.Settings.DataModel;
public sealed class DataChat
public sealed class DataChat(Expression<Func<Data, DataChat>>? configSelection = null)
{
/// <summary>
/// The default constructor for the JSON deserializer.
/// </summary>
public DataChat() : this(null)
{
}
/// <summary>
/// Shortcuts to send the input to the AI.
/// </summary>
@ -25,22 +34,22 @@ public sealed class DataChat
/// <summary>
/// Preselect any chat options?
/// </summary>
public bool PreselectOptions { get; set; }
public bool PreselectOptions { get; set; } = ManagedConfiguration.Register(configSelection, n => n.PreselectOptions, false);
/// <summary>
/// Should we preselect a provider for the chat?
/// </summary>
public string PreselectedProvider { get; set; } = string.Empty;
public string PreselectedProvider { get; set; } = ManagedConfiguration.Register(configSelection, n => n.PreselectedProvider, string.Empty);
/// <summary>
/// Preselect a profile?
/// </summary>
public string PreselectedProfile { get; set; } = string.Empty;
public string PreselectedProfile { get; set; } = ManagedConfiguration.Register(configSelection, n => n.PreselectedProfile, string.Empty);
/// <summary>
/// Preselect a chat template?
/// </summary>
public string PreselectedChatTemplate { get; set; } = string.Empty;
public string PreselectedChatTemplate { get; set; } = ManagedConfiguration.Register(configSelection, n => n.PreselectedChatTemplate, string.Empty);
/// <summary>
/// Should we preselect data sources options for a created chat?

View File

@ -0,0 +1,40 @@
using System.Linq.Expressions;
using AIStudio.Provider;
namespace AIStudio.Settings.DataModel;
public sealed class DataConfidence(Expression<Func<Data, DataConfidence>>? configSelection = null)
{
/// <summary>
/// The default constructor for the JSON deserializer.
/// </summary>
public DataConfidence() : this(null)
{
}
/// <summary>
/// Should we enforce a global minimum confidence level?
/// </summary>
public bool EnforceGlobalMinimumConfidence { get; set; } = ManagedConfiguration.Register(configSelection, n => n.EnforceGlobalMinimumConfidence, false);
/// <summary>
/// The global minimum confidence level to enforce.
/// </summary>
public ConfidenceLevel GlobalMinimumConfidence { get; set; } = ManagedConfiguration.Register(configSelection, n => n.GlobalMinimumConfidence, ConfidenceLevel.NONE);
/// <summary>
/// Should we show the provider confidence level?
/// </summary>
public bool ShowProviderConfidence { get; set; } = ManagedConfiguration.Register(configSelection, n => n.ShowProviderConfidence, true);
/// <summary>
/// Which confidence scheme to use.
/// </summary>
public ConfidenceSchemes ConfidenceScheme { get; set; } = ManagedConfiguration.Register(configSelection, n => n.ConfidenceScheme, ConfidenceSchemes.TRUST_ALL);
/// <summary>
/// Provide custom confidence levels for each provider family.
/// </summary>
public Dictionary<LLMProviders, ConfidenceLevel> CustomConfidenceScheme { get; set; } = ManagedConfiguration.Register(configSelection, n => n.CustomConfidenceScheme, []);
}

View File

@ -0,0 +1,87 @@
using AIStudio.Tools.PluginSystem;
using Lua;
namespace AIStudio.Settings.DataModel;
public sealed record DataIntroduction : ILivePluginContent
{
private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger<DataIntroduction>();
/// <summary>
/// The stable ID of the introduction.
/// </summary>
public string Id { get; private init; } = string.Empty;
/// <summary>
/// The ID of the enterprise configuration plugin that provides this introduction.
/// </summary>
public Guid EnterpriseConfigurationPluginId { get; private init; } = Guid.Empty;
/// <summary>
/// The title shown to the user.
/// </summary>
public string Title { get; private init; } = string.Empty;
/// <summary>
/// The configured version string shown to the user.
/// </summary>
public string VersionText { get; private init; } = string.Empty;
/// <summary>
/// The sort index used on the home page.
/// </summary>
public int Index { get; private init; } = 1;
/// <summary>
/// The Markdown content shown to the user.
/// </summary>
public string Markdown { get; private init; } = string.Empty;
public static bool TryParseConfiguration(int idx, LuaTable table, Guid configPluginId, out DataIntroduction introduction)
{
introduction = new DataIntroduction();
if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead<string>(out var idText) || !Guid.TryParse(idText, out var id))
{
LOG.LogWarning("The configured introduction {IntroductionIndex} does not contain a valid ID. The ID must be a valid GUID.", idx);
return false;
}
if (!table.TryGetValue("Title", out var titleValue) || !titleValue.TryRead<string>(out var title) || string.IsNullOrWhiteSpace(title))
{
LOG.LogWarning("The configured introduction {IntroductionIndex} does not contain a valid Title field.", idx);
return false;
}
if (!table.TryGetValue("Version", out var versionValue) || !versionValue.TryRead<string>(out var versionText) || string.IsNullOrWhiteSpace(versionText))
{
LOG.LogWarning("The configured introduction {IntroductionIndex} does not contain a valid Version field.", idx);
return false;
}
if (!table.TryGetValue("Markdown", out var markdownValue) || !markdownValue.TryRead<string>(out var markdown) || string.IsNullOrWhiteSpace(markdown))
{
LOG.LogWarning("The configured introduction {IntroductionIndex} does not contain a valid Markdown field.", idx);
return false;
}
var index = 1;
if (table.TryGetValue("Index", out var indexValue) && !indexValue.TryRead(out index))
{
LOG.LogWarning("The configured introduction {IntroductionIndex} does not contain a valid Index field. The Index must be an integer.", idx);
return false;
}
introduction = new DataIntroduction
{
Id = id.ToString(),
Title = title,
VersionText = versionText,
Index = index,
Markdown = AIStudio.Tools.Markdown.RemoveSharedIndentation(markdown),
EnterpriseConfigurationPluginId = configPluginId,
};
return true;
}
}

View File

@ -1,11 +1,13 @@
using System.Security.Cryptography;
using System.Text;
using AIStudio.Tools.PluginSystem;
using Lua;
namespace AIStudio.Settings.DataModel;
public sealed record DataMandatoryInfo
public sealed record DataMandatoryInfo : ILivePluginContent
{
private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger<DataMandatoryInfo>();

View File

@ -1,8 +1,8 @@
using AIStudio.Provider;
namespace AIStudio.Settings.DataModel;
namespace AIStudio.Settings.DataModel.PreviousModels;
public sealed class DataLLMProviders
public sealed class DataLLMProvidersV5
{
/// <summary>
/// Should we enforce a global minimum confidence level?
@ -25,7 +25,7 @@ public sealed class DataLLMProviders
public ConfidenceSchemes ConfidenceScheme { get; set; } = ConfidenceSchemes.TRUST_ALL;
/// <summary>
/// Provide custom confidence levels for each LLM provider.
/// Provide custom confidence levels for each provider family.
/// </summary>
public Dictionary<LLMProviders, ConfidenceLevel> CustomConfidenceScheme { get; set; } = new();
}

View File

@ -16,7 +16,7 @@ public sealed class DataV4
/// <summary>
/// Settings concerning the LLM providers.
/// </summary>
public DataLLMProviders LLMProviders { get; init; } = new();
public DataLLMProvidersV5 LLMProviders { get; init; } = new();
/// <summary>
/// List of configured profiles.

View File

@ -0,0 +1,149 @@
using AIStudio.Tools.PluginSystem.Assistants;
namespace AIStudio.Settings.DataModel.PreviousModels;
public sealed class DataV5
{
/// <summary>
/// The version of the settings file. Allows us to upgrade the settings
/// when a new version is available.
/// </summary>
public Version Version { get; init; } = Version.V5;
/// <summary>
/// List of configured providers.
/// </summary>
public List<AIStudio.Settings.Provider> Providers { get; init; } = [];
/// <summary>
/// Settings concerning the LLM providers.
/// </summary>
public DataLLMProvidersV5 LLMProviders { get; init; } = new();
/// <summary>
/// A collection of embedding providers configured.
/// </summary>
public List<EmbeddingProvider> EmbeddingProviders { get; init; } = [];
/// <summary>
/// A collection of speech providers configured.
/// </summary>
public List<TranscriptionProvider> TranscriptionProviders { get; init; } = [];
/// <summary>
/// A collection of data sources configured.
/// </summary>
public List<IDataSource> DataSources { get; set; } = [];
/// <summary>
/// List of configured profiles.
/// </summary>
public List<Profile> Profiles { get; init; } = [];
/// <summary>
/// List of configured chat templates.
/// </summary>
public List<ChatTemplate> ChatTemplates { get; init; } = [];
/// <summary>
/// List of enabled plugins.
/// </summary>
public List<Guid> EnabledPlugins { get; set; } = [];
/// <summary>
/// Metadata for managed settings that use a plugin-provided editable default.
/// </summary>
public Dictionary<string, ManagedEditableDefaultState> ManagedEditableDefaults { get; set; } = [];
/// <summary>
/// Cached audit results for assistant plugins.
/// </summary>
public List<PluginAssistantAudit> AssistantPluginAudits { get; set; } = [];
/// <summary>
/// The next provider number to use.
/// </summary>
public uint NextProviderNum { get; set; } = 1;
/// <summary>
/// The next embedding provider number to use.
/// </summary>
public uint NextEmbeddingNum { get; set; } = 1;
/// <summary>
/// The next transcription provider number to use.
/// </summary>
public uint NextTranscriptionNum { get; set; } = 1;
/// <summary>
/// The next data source number to use.
/// </summary>
public uint NextDataSourceNum { get; set; } = 1;
/// <summary>
/// The next profile number to use.
/// </summary>
public uint NextProfileNum { get; set; } = 1;
/// <summary>
/// The next chat template number to use.
/// </summary>
public uint NextChatTemplateNum { get; set; } = 1;
/// <summary>
/// The next document analysis policy number to use.
/// </summary>
public uint NextDocumentAnalysisPolicyNum { get; set; } = 1;
public DataApp App { get; init; } = new(x => x.App);
public DataChat Chat { get; init; } = new();
public DataWorkspace Workspace { get; init; } = new();
public DataIconFinder IconFinder { get; init; } = new();
public DataTranslation Translation { get; init; } = new();
public DataCoding Coding { get; init; } = new();
public DataERI ERI { get; init; } = new();
public DataDocumentAnalysis DocumentAnalysis { get; init; } = new();
public DataMandatoryInformation MandatoryInformation { get; init; } = new();
public DataTextSummarizer TextSummarizer { get; init; } = new();
public DataTextContentCleaner TextContentCleaner { get; init; } = new();
public DataAgentDataSourceSelection AgentDataSourceSelection { get; init; } = new();
public DataAgentRetrievalContextValidation AgentRetrievalContextValidation { get; init; } = new();
public DataAssistantPluginAudit AssistantPluginAudit { get; init; } = new(x => x.AssistantPluginAudit);
public DataAgenda Agenda { get; init; } = new();
public DataGrammarSpelling GrammarSpelling { get; init; } = new();
public DataRewriteImprove RewriteImprove { get; init; } = new();
public DataPromptOptimizer PromptOptimizer { get; init; } = new();
public DataEMail EMail { get; init; } = new();
public DataSlideBuilder SlideBuilder { get; init; } = new();
public DataLegalCheck LegalCheck { get; init; } = new();
public DataSynonyms Synonyms { get; init; } = new();
public DataMyTasks MyTasks { get; init; } = new();
public DataJobPostings JobPostings { get; init; } = new();
public DataBiasOfTheDay BiasOfTheDay { get; init; } = new();
public DataI18N I18N { get; init; } = new();
}

View File

@ -0,0 +1,20 @@
using System.Linq.Expressions;
using AIStudio.Settings.DataModel;
namespace AIStudio.Settings;
public sealed class DataSourceSecuritySettings(Expression<Func<Data, DataSourceSecuritySettings>>? configSelection = null)
{
/// <summary>
/// The default constructor for the JSON deserializer.
/// </summary>
public DataSourceSecuritySettings() : this(null)
{
}
/// <summary>
/// Provider instance IDs trusted by an organization for data-source security checks.
/// </summary>
public HashSet<string> TrustedProviderIds { get; set; } = ManagedConfiguration.Register(configSelection, n => n.TrustedProviderIds, []);
}

View File

@ -0,0 +1,52 @@
using AIStudio.Provider;
namespace AIStudio.Settings;
public static class DataSourceSecurityTrustExtensions
{
public static bool IsTrustedForDataSourceSecurityChecks(this Provider provider, SettingsManager settingsManager)
{
if (provider == Provider.NONE)
return false;
return provider.IsSelfHosted || provider.IsTrustedByConfiguration(settingsManager);
}
public static bool IsTrustedForDataSourceSecurityChecks(this EmbeddingProvider provider, SettingsManager settingsManager)
{
if (provider == EmbeddingProvider.NONE)
return false;
return provider.IsSelfHosted || provider.IsTrustedByConfiguration(settingsManager);
}
public static bool IsTrustedForDataSourceSecurityChecks(this TranscriptionProvider provider, SettingsManager settingsManager)
{
if (provider == TranscriptionProvider.NONE)
return false;
return provider.IsSelfHosted || provider.IsTrustedByConfiguration(settingsManager);
}
public static bool IsTrustedForDataSourceSecurityChecks(this IProvider provider, SettingsManager settingsManager)
{
if (provider is NoProvider)
return false;
return provider.Provider is LLMProviders.SELF_HOSTED || IsTrustedProviderId(provider.ConfiguredProviderId, settingsManager);
}
public static bool IsTrustedByConfiguration(this Provider provider, SettingsManager settingsManager) => IsTrustedProviderId(provider.Id, settingsManager);
public static bool IsTrustedByConfiguration(this EmbeddingProvider provider, SettingsManager settingsManager) => IsTrustedProviderId(provider.Id, settingsManager);
public static bool IsTrustedByConfiguration(this TranscriptionProvider provider, SettingsManager settingsManager) => IsTrustedProviderId(provider.Id, settingsManager);
private static bool IsTrustedProviderId(string providerId, SettingsManager settingsManager)
{
if (string.IsNullOrWhiteSpace(providerId))
return false;
return settingsManager.ConfigurationData.DataSourceSecurity.TrustedProviderIds.Any(id => string.Equals(id, providerId, StringComparison.OrdinalIgnoreCase));
}
}

View File

@ -44,7 +44,7 @@ public sealed record EmbeddingProvider(
/// <inheritdoc />
[JsonIgnore]
public string SecretId => this.IsEnterpriseConfiguration ? $"{ISecretId.ENTERPRISE_KEY_PREFIX}::{this.UsedLLMProvider.ToName()}" : this.UsedLLMProvider.ToName();
public string SecretId => this.IsEnterpriseConfiguration ? $"{ISecretId.ENTERPRISE_KEY_PREFIX}::{this.UsedLLMProvider.ToSecretId()}" : this.UsedLLMProvider.ToSecretId();
/// <inheritdoc />
[JsonIgnore]
@ -125,7 +125,7 @@ public sealed record EmbeddingProvider(
{
// Queue the API key for storage in the OS keyring:
PendingEnterpriseApiKeys.Add(new(
$"{ISecretId.ENTERPRISE_KEY_PREFIX}::{usedLLMProvider.ToName()}",
$"{ISecretId.ENTERPRISE_KEY_PREFIX}::{usedLLMProvider.ToSecretId()}",
name,
decryptedApiKey,
SecretStoreType.EMBEDDING_PROVIDER));

View File

@ -38,14 +38,14 @@ public static partial class ManagedConfiguration
//
// Handle configured enum values
//
// Check if that configuration was registered:
if(!TryGet(configSelection, propertyExpression, out var configMeta))
return false;
var successful = false;
var configuredValue = configMeta.Default;
// Step 1 -- try to read the Lua value out of the Lua table:
if(settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredEnumValue))
{
@ -60,7 +60,7 @@ public static partial class ManagedConfiguration
}
}
}
if(dryRun)
return successful;
@ -98,14 +98,14 @@ public static partial class ManagedConfiguration
//
// Handle configured ISpanParsable values
//
// Check if that configuration was registered:
if(!TryGet(configSelection, propertyExpression, out var configMeta))
return false;
var successful = false;
var configuredValue = configMeta.Default;
// Step 1 -- try to read the Lua value out of the Lua table:
if (settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredLuaValue))
{
@ -119,7 +119,7 @@ public static partial class ManagedConfiguration
successful = true;
}
}
// Step 2b -- try to read the Lua value:
if(configuredLuaValue.TryRead<TValue>(out var configuredLuaValueInstance))
{
@ -135,7 +135,7 @@ public static partial class ManagedConfiguration
var managedMode = ReadManagedConfigurationMode(propertyExpression, settings);
return HandleParsedScalarValue(configPluginId, dryRun, successful, configMeta, configuredValue, managedMode, settingName);
}
/// <summary>
/// Attempts to process the configuration settings from a Lua table for string values.
/// </summary>
@ -189,14 +189,14 @@ public static partial class ManagedConfiguration
//
// Handle configured string values
//
// Check if that configuration was registered:
if(!TryGet(configSelection, propertyExpression, out var configMeta))
return false;
var successful = false;
var configuredValue = configMeta.Default;
// Step 1 -- try to read the Lua value out of the Lua table:
if(settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredTextValue))
{
@ -210,7 +210,7 @@ public static partial class ManagedConfiguration
successful = Guid.TryParse(configuredText, out var id);
configuredValue = successful ? id.ToString().ToLowerInvariant() : configuredText;
break;
// Case: the read string is just a string:
case string:
configuredValue = configuredText;
@ -219,7 +219,7 @@ public static partial class ManagedConfiguration
}
}
}
var settingName = SettingName(propertyExpression);
var managedMode = ReadManagedConfigurationMode(propertyExpression, settings);
return HandleParsedScalarValue(configPluginId, dryRun, successful, configMeta, configuredValue, managedMode, settingName);
@ -273,13 +273,13 @@ public static partial class ManagedConfiguration
// Determine the length of the Lua table and prepare a list to hold the parsed values:
var len = valueTable.ArrayLength;
var list = new List<TValue>(len);
// Iterate over each entry in the Lua table:
for (var index = 1; index <= len; index++)
{
// Retrieve the Lua value at the current index:
var value = valueTable[index];
// Step 2a -- try to read the Lua value as a string:
if (value.Type is LuaValueType.String && value.TryRead<string>(out var configuredLuaValueText))
{
@ -304,7 +304,7 @@ public static partial class ManagedConfiguration
}
// ReSharper restore MethodOverloadWithOptionalParameter
/// <summary>
/// Attempts to process the configuration settings from a Lua table for enum list types.
/// </summary>
@ -333,14 +333,14 @@ public static partial class ManagedConfiguration
//
// Handle configured enum lists
//
// Check if that configuration was registered:
if(!TryGet(configSelection, propertyExpression, out var configMeta))
return false;
var successful = false;
var configuredValue = configMeta.Default;
// Step 1 -- try to read the Lua value (we expect a table) out of the Lua table:
if (settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredLuaList) &&
configuredLuaList.Type is LuaValueType.Table &&
@ -349,13 +349,13 @@ public static partial class ManagedConfiguration
// Determine the length of the Lua table and prepare a list to hold the parsed values:
var len = valueTable.ArrayLength;
var list = new List<TValue>(len);
// Iterate over each entry in the Lua table:
for (var index = 1; index <= len; index++)
{
// Retrieve the Lua value at the current index:
var value = valueTable[index];
// Step 2 -- try to read the Lua value as a string:
if (value.Type is LuaValueType.String && value.TryRead<string>(out var configuredLuaValueText))
{
@ -364,17 +364,17 @@ public static partial class ManagedConfiguration
list.Add((TValue)configuredEnum);
}
}
configuredValue = list;
successful = true;
}
if(dryRun)
return successful;
return HandleParsedValue(configPluginId, dryRun, successful, configMeta, configuredValue);
}
/// <summary>
/// Attempts to process the configuration settings from a Lua table for string list types.
/// </summary>
@ -400,14 +400,14 @@ public static partial class ManagedConfiguration
//
// Handle configured string lists
//
// Check if that configuration was registered:
if(!TryGet(configSelection, propertyExpression, out var configMeta))
return false;
var successful = false;
var configuredValue = configMeta.Default;
// Step 1 -- try to read the Lua value (we expect a table) out of the Lua table:
if (settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredLuaList) &&
configuredLuaList.Type is LuaValueType.Table &&
@ -416,25 +416,25 @@ public static partial class ManagedConfiguration
// Determine the length of the Lua table and prepare a list to hold the parsed values:
var len = valueTable.ArrayLength;
var list = new List<string>(len);
// Iterate over each entry in the Lua table:
for (var index = 1; index <= len; index++)
{
// Retrieve the Lua value at the current index:
var value = valueTable[index];
// Step 2 -- try to read the Lua value as a string:
if (value.Type is LuaValueType.String && value.TryRead<string>(out var configuredLuaValueText))
list.Add(configuredLuaValueText);
}
configuredValue = list;
successful = true;
}
if(dryRun)
return successful;
return HandleParsedValue(configPluginId, dryRun, successful, configMeta, configuredValue);
}
@ -486,13 +486,13 @@ public static partial class ManagedConfiguration
// Determine the length of the Lua table and prepare a set to hold the parsed values:
var len = valueTable.ArrayLength;
var set = new HashSet<TValue>(len);
// Iterate over each entry in the Lua table:
for (var index = 1; index <= len; index++)
{
// Retrieve the Lua value at the current index:
var value = valueTable[index];
// Step 2a -- try to read the Lua value as a string:
if (value.Type is LuaValueType.String && value.TryRead<string>(out var configuredLuaValueText))
{
@ -517,7 +517,7 @@ public static partial class ManagedConfiguration
}
// ReSharper restore MethodOverloadWithOptionalParameter
/// <summary>
/// Attempts to process the configuration settings from a Lua table for enum set types.
/// </summary>
@ -546,14 +546,14 @@ public static partial class ManagedConfiguration
//
// Handle configured enum sets
//
// Check if that configuration was registered:
if(!TryGet(configSelection, propertyExpression, out var configMeta))
return false;
var successful = false;
var configuredValue = configMeta.Default;
// Step 1 -- try to read the Lua value (we expect a table) out of the Lua table:
if (settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredLuaList) &&
configuredLuaList.Type is LuaValueType.Table &&
@ -562,13 +562,13 @@ public static partial class ManagedConfiguration
// Determine the length of the Lua table and prepare a set to hold the parsed values:
var len = valueTable.ArrayLength;
var set = new HashSet<TValue>(len);
// Iterate over each entry in the Lua table:
for (var index = 1; index <= len; index++)
{
// Retrieve the Lua value at the current index:
var value = valueTable[index];
// Step 2 -- try to read the Lua value as a string:
if (value.Type is LuaValueType.String && value.TryRead<string>(out var configuredLuaValueText))
{
@ -577,14 +577,14 @@ public static partial class ManagedConfiguration
set.Add((TValue)configuredEnum);
}
}
configuredValue = set;
successful = true;
}
if(dryRun)
return successful;
return HandleParsedValue(configPluginId, dryRun, successful, configMeta, configuredValue);
}
@ -629,13 +629,13 @@ public static partial class ManagedConfiguration
// Determine the length of the Lua table and prepare a set to hold the parsed values:
var len = valueTable.ArrayLength;
var set = new HashSet<TValue>(len);
// Iterate over each entry in the Lua table:
for (var index = 1; index <= len; index++)
{
// Retrieve the Lua value at the current index:
var value = valueTable[index];
// Step 2 -- try to read the Lua value as a string:
if (value.Type is LuaValueType.String && value.TryRead<string>(out var configuredLuaValueText))
{
@ -671,7 +671,7 @@ public static partial class ManagedConfiguration
return successful;
}
/// <summary>
/// Attempts to process the configuration settings from a Lua table for string set types.
/// </summary>
@ -697,14 +697,14 @@ public static partial class ManagedConfiguration
//
// Handle configured string sets
//
// Check if that configuration was registered:
if(!TryGet(configSelection, propertyExpression, out var configMeta))
return false;
var successful = false;
var configuredValue = configMeta.Default;
// Step 1 -- try to read the Lua value (we expect a table) out of the Lua table:
if (settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredLuaList) &&
configuredLuaList.Type is LuaValueType.Table &&
@ -713,28 +713,28 @@ public static partial class ManagedConfiguration
// Determine the length of the Lua table and prepare a set to hold the parsed values:
var len = valueTable.ArrayLength;
var set = new HashSet<string>(len);
// Iterate over each entry in the Lua table:
for (var index = 1; index <= len; index++)
{
// Retrieve the Lua value at the current index:
var value = valueTable[index];
// Step 2 -- try to read the Lua value as a string:
if (value.Type is LuaValueType.String && value.TryRead<string>(out var configuredLuaValueText))
set.Add(configuredLuaValueText);
}
configuredValue = set;
successful = true;
}
if(dryRun)
return successful;
return HandleParsedValue(configPluginId, dryRun, successful, configMeta, configuredValue);
}
/// <summary>
/// Attempts to process the configuration settings from a Lua table for string dictionary types.
/// </summary>
@ -760,14 +760,14 @@ public static partial class ManagedConfiguration
//
// Handle configured string dictionaries (both keys and values are strings)
//
// Check if that configuration was registered:
if(!TryGet(configSelection, propertyExpression, out var configMeta))
return false;
var successful = false;
var configuredValue = configMeta.Default;
// Step 1 -- try to read the Lua value (we expect a table) out of the Lua table:
if (settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredLuaList) &&
configuredLuaList.Type is LuaValueType.Table &&
@ -778,7 +778,7 @@ public static partial class ManagedConfiguration
var len = valueTable.HashMapCount;
if (len > 0)
configuredValue.Clear();
// In order to iterate over all key-value pairs in the Lua table, we have to use TryGetNext.
// Thus, we initialize the previous key variable to Nil and keep calling TryGetNext until
// there are no more pairs:
@ -787,25 +787,101 @@ public static partial class ManagedConfiguration
{
// Update the previous key for the next iteration:
previousKey = pair.Key;
// Try to read both the key and the value as strings:
var hadKey = pair.Key.TryRead<string>(out var key);
var hadValue = pair.Value.TryRead<string>(out var value);
// If both key and value were read successfully, add them to the dictionary:
if (hadKey && hadValue)
configuredValue[key] = value;
}
successful = true;
}
if(dryRun)
return successful;
return HandleParsedValue(configPluginId, dryRun, successful, configMeta, configuredValue);
}
/// <summary>
/// Attempts to process the configuration settings from a Lua table for enum dictionary types.
/// </summary>
/// <remarks>
/// When the configuration is successfully processed, it updates the configuration metadata with the configured value.
/// Furthermore, it applies the configured managed state to the provided configuration plugin ID.
/// The setting's value is set to the configured value when locked or when the editable default should apply.
/// </remarks>
/// <param name="configPluginId">The ID of the related configuration plugin.</param>
/// <param name="settings">The Lua table containing the settings to process.</param>
/// <param name="configSelection">The expression to select the configuration class.</param>
/// <param name="propertyExpression">The expression to select the property within the configuration class.</param>
/// <param name="dryRun">When true, the method will not apply any changes but only check if the configuration can be read.</param>
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
/// <typeparam name="TKey">The enum type of the dictionary keys.</typeparam>
/// <typeparam name="TValue">The enum type of the dictionary values.</typeparam>
/// <returns>True when the configuration was successfully processed, otherwise false.</returns>
public static bool TryProcessConfiguration<TClass, TKey, TValue>(
Expression<Func<Data, TClass>> configSelection,
Expression<Func<TClass, Dictionary<TKey, TValue>>> propertyExpression,
Guid configPluginId,
LuaTable settings,
bool dryRun)
where TKey : struct, Enum
where TValue : struct, Enum
{
//
// Handle configured enum dictionaries (Lua keys and values are strings)
//
// Check if that configuration was registered:
if(!TryGet(configSelection, propertyExpression, out var configMeta))
return false;
var successful = false;
var configuredValue = new Dictionary<TKey, TValue>(configMeta.Default);
// Step 1 -- try to read the Lua value (we expect a table) out of the Lua table:
if (settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredLuaList) &&
configuredLuaList.Type is LuaValueType.Table &&
configuredLuaList.TryRead<LuaTable>(out var valueTable))
{
configuredValue.Clear();
// In order to iterate over all key-value pairs in the Lua table, we have to use TryGetNext.
// Thus, we initialize the previous key variable to Nil and keep calling TryGetNext until
// there are no more pairs:
var previousKey = LuaValue.Nil;
while(valueTable.TryGetNext(previousKey, out var pair))
{
// Update the previous key for the next iteration:
previousKey = pair.Key;
// Try to read both the key and the value as strings:
var hadKey = pair.Key.TryRead<string>(out var keyText);
var hadValue = pair.Value.TryRead<string>(out var valueText);
// If both key and value were read successfully, parse and add them to the dictionary:
if (hadKey
&& hadValue
&& Enum.TryParse<TKey>(keyText, true, out var key)
&& Enum.TryParse<TValue>(valueText, true, out var value))
configuredValue[key] = value;
}
successful = true;
}
if(dryRun)
return successful;
var settingName = SettingName(propertyExpression);
var managedMode = ReadManagedConfigurationMode(propertyExpression, settings);
return HandleParsedScalarValue(configPluginId, dryRun, successful, configMeta, configuredValue, managedMode, settingName);
}
/// <summary>
/// Handles the parsed configuration value based on whether the parsing was successful and whether it's a dry run.
/// </summary>
@ -826,14 +902,14 @@ public static partial class ManagedConfiguration
{
if(dryRun)
return successful;
switch (successful)
{
case true:
//
// Case: the setting was configured, and we could read the value successfully.
//
// Set the configured value and lock the configuration:
configMeta.SetValue(configuredValue);
configMeta.LockConfiguration(configPluginId);
@ -852,7 +928,7 @@ public static partial class ManagedConfiguration
//
configMeta.ResetLockedConfiguration();
break;
case false:
//
// Case: the setting was not configured, or we could not read the value successfully.
@ -946,8 +1022,12 @@ public static partial class ManagedConfiguration
{
null => string.Empty,
string text => text,
System.Collections.IDictionary dictionary => string.Join(";", dictionary.Keys
.Cast<object>()
.OrderBy(key => key.ToString(), StringComparer.Ordinal)
.Select(key => $"{key}:{dictionary[key]}")),
IFormattable formattable => formattable.ToString(null, CultureInfo.InvariantCulture),
_ => value.ToString() ?? string.Empty,
};
}

View File

@ -66,13 +66,13 @@ public static partial class ManagedConfiguration
// we ignore the register call and return the default value:
if(configSelection is null)
return defaultValue;
var configPath = Path(configSelection, propertyExpression);
// If the metadata already exists for this configuration path, we return the default value:
if (METADATA.ContainsKey(configPath))
return defaultValue;
// Not registered yet, so we register it now:
METADATA[configPath] = new ConfigMeta<TClass, string>(configSelection, propertyExpression)
{
@ -104,13 +104,13 @@ public static partial class ManagedConfiguration
// we ignore the register call and return the default value:
if(configSelection is null)
return [defaultValue];
var configPath = Path(configSelection, propertyExpression);
// If the metadata already exists for this configuration path, we return the default value:
if (METADATA.ContainsKey(configPath))
return [defaultValue];
// Not registered yet, so we register it now:
METADATA[configPath] = new ConfigMeta<TClass, IList<TValue>>(configSelection, propertyExpression)
{
@ -142,13 +142,13 @@ public static partial class ManagedConfiguration
// we ignore the register call and return the default value:
if(configSelection is null)
return [..defaultValues];
var configPath = Path(configSelection, propertyExpression);
// If the metadata already exists for this configuration path, we return the default value:
if (METADATA.ContainsKey(configPath))
return [..defaultValues];
// Not registered yet, so we register it now:
METADATA[configPath] = new ConfigMeta<TClass, IList<TValue>>(configSelection, propertyExpression)
{
@ -179,13 +179,13 @@ public static partial class ManagedConfiguration
// we ignore the register call and return the default value:
if (configSelection is null)
return [defaultValue];
var configPath = Path(configSelection, propertyExpression);
// If the metadata already exists for this configuration path, we return the default value:
if (METADATA.ContainsKey(configPath))
return [defaultValue];
// Not registered yet, so we register it now:
METADATA[configPath] = new ConfigMeta<TClass, ISet<TValue>>(configSelection, propertyExpression)
{
@ -217,13 +217,13 @@ public static partial class ManagedConfiguration
// we ignore the register call and return the default value:
if (configSelection is null)
return [..defaultValues];
var configPath = Path(configSelection, propertyExpression);
// If the metadata already exists for this configuration path, we return the default value:
if (METADATA.ContainsKey(configPath))
return [..defaultValues];
// Not registered yet, so we register it now:
METADATA[configPath] = new ConfigMeta<TClass, ISet<TValue>>(configSelection, propertyExpression)
{
@ -268,7 +268,48 @@ public static partial class ManagedConfiguration
{
Default = defaultValues,
};
return defaultValues;
}
/// <summary>
/// Registers a configuration setting with a default dictionary of enum key-value pairs.
/// </summary>
/// <remarks>
/// When the method is invoked with a null configSelection, the configuration path
/// is ignored, and the specified default values are returned without registration.
/// </remarks>
/// <param name="configSelection">The expression that selects the configuration class from the root Data model.</param>
/// <param name="propertyExpression">The expression to select the property within the configuration class.</param>
/// <param name="defaultValues">The default dictionary of values to use when the setting is not configured.</param>
/// <typeparam name="TClass">The type of the configuration class from which the property is selected.</typeparam>
/// <typeparam name="TKey">The enum type of the dictionary keys.</typeparam>
/// <typeparam name="TValue">The enum type of the dictionary values.</typeparam>
/// <returns>A dictionary containing the default values.</returns>
public static Dictionary<TKey, TValue> Register<TClass, TKey, TValue>(
Expression<Func<Data, TClass>>? configSelection,
Expression<Func<TClass, Dictionary<TKey, TValue>>> propertyExpression,
Dictionary<TKey, TValue> defaultValues)
where TKey : struct, Enum
where TValue : struct, Enum
{
// When called from the JSON deserializer by using the standard constructor,
// we ignore the register call and return the default value:
if (configSelection is null)
return new();
var configPath = Path(configSelection, propertyExpression);
// If the metadata already exists for this configuration path, we return the default value:
if (METADATA.ContainsKey(configPath))
return defaultValues;
// Not registered yet, so we register it now:
METADATA[configPath] = new ConfigMeta<TClass, Dictionary<TKey, TValue>>(configSelection, propertyExpression)
{
Default = defaultValues,
};
return defaultValues;
}
}

View File

@ -231,6 +231,44 @@ public static partial class ManagedConfiguration
return false;
}
/// <summary>
/// Attempts to retrieve the configuration metadata for an enum dictionary-based setting.
/// </summary>
/// <remarks>
/// When no configuration metadata is found, it returns a NoConfig instance with the default
/// value set to an empty dictionary. This allows the caller to handle the absence of configuration
/// gracefully. In such cases, the return value of the method will be false.
/// </remarks>
/// <param name="configSelection">The expression to select the configuration class.</param>
/// <param name="propertyExpression">The expression to select the property within the
/// configuration class.</param>
/// <param name="configMeta">The output parameter that will hold the configuration metadata
/// if found.</param>
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
/// <typeparam name="TKey">The enum type of the dictionary keys.</typeparam>
/// <typeparam name="TValue">The enum type of the dictionary values.</typeparam>
/// <returns>True if the configuration metadata was found, otherwise false.</returns>
public static bool TryGet<TClass, TKey, TValue>(
Expression<Func<Data, TClass>> configSelection,
Expression<Func<TClass, Dictionary<TKey, TValue>>> propertyExpression,
out ConfigMeta<TClass, Dictionary<TKey, TValue>> configMeta)
where TKey : struct, Enum
where TValue : struct, Enum
{
var configPath = Path(configSelection, propertyExpression);
if (METADATA.TryGetValue(configPath, out var value) && value is ConfigMeta<TClass, Dictionary<TKey, TValue>> meta)
{
configMeta = meta;
return true;
}
configMeta = new NoConfig<TClass, Dictionary<TKey, TValue>>(configSelection, propertyExpression)
{
Default = new Dictionary<TKey, TValue>(),
};
return false;
}
/// <summary>
/// Checks if a configuration setting is left over from a configuration plugin that is no longer available.
/// If the configuration setting is locked and managed by a configuration plugin that is not available,
@ -399,6 +437,42 @@ public static partial class ManagedConfiguration
return false;
}
public static bool IsConfigurationLeftOver<TClass, TKey, TValue>(
Expression<Func<Data, TClass>> configSelection,
Expression<Func<TClass, Dictionary<TKey, TValue>>> propertyExpression,
IEnumerable<IAvailablePlugin> availablePlugins)
where TKey : struct, Enum
where TValue : struct, Enum
{
if (!TryGet(configSelection, propertyExpression, out var configMeta))
return false;
if (configMeta.ManagedMode is ManagedConfigurationMode.EDITABLE_DEFAULT)
{
var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.EditableDefaultByConfigPluginId);
if (plugin is null)
{
configMeta.ClearEditableDefaultConfiguration();
ClearEditableDefaultState(SettingName(propertyExpression));
return true;
}
return false;
}
if (configMeta.LockedByConfigPluginId == Guid.Empty || !configMeta.IsLocked)
return false;
var lockedPlugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.LockedByConfigPluginId);
if (lockedPlugin is null)
{
configMeta.ResetLockedConfiguration();
return true;
}
return false;
}
private static string Path<TClass, TValue>(Expression<Func<Data, TClass>> configSelection, Expression<Func<TClass, TValue>> propertyExpression)
{

View File

@ -72,7 +72,7 @@ public sealed record Provider(
/// <inheritdoc />
[JsonIgnore]
public string SecretId => this.IsEnterpriseConfiguration ? $"{ISecretId.ENTERPRISE_KEY_PREFIX}::{this.UsedLLMProvider.ToName()}" : this.UsedLLMProvider.ToName();
public string SecretId => this.IsEnterpriseConfiguration ? $"{ISecretId.ENTERPRISE_KEY_PREFIX}::{this.UsedLLMProvider.ToSecretId()}" : this.UsedLLMProvider.ToSecretId();
/// <inheritdoc />
[JsonIgnore]
@ -182,7 +182,7 @@ public sealed record Provider(
{
// Queue the API key for storage in the OS keyring:
PendingEnterpriseApiKeys.Add(new(
$"{ISecretId.ENTERPRISE_KEY_PREFIX}::{usedLLMProvider.ToName()}",
$"{ISecretId.ENTERPRISE_KEY_PREFIX}::{usedLLMProvider.ToSecretId()}",
instanceName,
decryptedApiKey,
SecretStoreType.LLM_PROVIDER));

View File

@ -17,6 +17,11 @@ namespace AIStudio.Settings;
public sealed class SettingsManager
{
private const string SETTINGS_FILENAME = "settings.json";
private const Version CURRENT_SETTINGS_VERSION = Version.V6;
private readonly record struct SettingsVersionReadResult(Version Version, SettingsWriteBlockReason FailureReason);
private readonly record struct CurrentSettingsReadResult(Data? SettingsData, SettingsWriteBlockReason FailureReason);
private static readonly JsonSerializerOptions JSON_OPTIONS = new()
{
@ -62,6 +67,16 @@ public sealed class SettingsManager
/// </summary>
public bool HasCompletedInitialSettingsLoad { get; private set; }
/// <summary>
/// Indicates why settings writes are blocked for the current session.
/// </summary>
public SettingsWriteBlockReason SettingsWriteBlockReason { get; private set; } = SettingsWriteBlockReason.NONE;
/// <summary>
/// Indicates that settings writes are blocked for the current session.
/// </summary>
public bool SettingsWriteBlocked => this.SettingsWriteBlockReason is not SettingsWriteBlockReason.NONE;
/// <summary>
/// The configuration data.
/// </summary>
@ -87,6 +102,7 @@ public sealed class SettingsManager
/// <returns>A (migrated) settings snapshot, or null if it could not be read.</returns>
public async Task<Data?> TryReadSettingsSnapshot()
{
this.SettingsWriteBlockReason = SettingsWriteBlockReason.NONE;
if(!this.IsSetUp)
{
this.logger.LogWarning("Cannot load settings, because the configuration is not set up yet.");
@ -100,38 +116,175 @@ public sealed class SettingsManager
return null;
}
// We read the `"Version": "V3"` line to determine the version of the settings file:
await foreach (var line in File.ReadLinesAsync(settingsPath))
var settingsVersion = await this.TryReadSettingsVersion(settingsPath);
if(settingsVersion.FailureReason is not SettingsWriteBlockReason.NONE)
{
if (!line.Contains("""
"Version":
"""))
continue;
this.BlockSettingsWrites(settingsVersion.FailureReason, "The settings file version could not be identified. Settings writes are blocked to avoid overwriting newer or unreadable settings.");
return await this.TryReadCurrentVersionBackupSnapshotForBlockedSettings();
}
// Extract the version from the line:
var settingsVersionText = line.Split('"')[3];
if(settingsVersion.Version > CURRENT_SETTINGS_VERSION)
{
this.BlockSettingsWrites(SettingsWriteBlockReason.VERSION_NEWER_THAN_APP, $"The settings file uses the newer version '{settingsVersion.Version}'. Settings writes are blocked to avoid overwriting newer settings.");
return await this.TryReadCurrentVersionBackupSnapshotForBlockedSettings();
}
// Parse the version:
Enum.TryParse(settingsVersionText, out Version settingsVersion);
if(settingsVersion is Version.UNKNOWN)
Data? settingsData;
if(settingsVersion.Version < CURRENT_SETTINGS_VERSION)
{
settingsData = await this.TryReadCurrentVersionBackupSnapshot();
if(settingsData is not null)
{
this.logger.LogError("Unknown version of the settings file found.");
return new();
this.PrepareLoadedSettings(settingsData);
await this.StoreSettingsSnapshot(settingsData, settingsPath);
await this.StoreCurrentVersionBackup(settingsData);
this.logger.LogInformation($"Restored settings from the '{GetBackupSettingsFilename(CURRENT_SETTINGS_VERSION)}' backup file.");
return settingsData;
}
var settingsData = SettingsMigrations.Migrate(this.logger, settingsVersion, await File.ReadAllTextAsync(settingsPath), JSON_OPTIONS);
//
// We filter the enabled preview features based on the preview visibility.
// This is necessary when the app starts up: some preview features may have
// been disabled or released from the last time the app was started.
//
settingsData.App.EnabledPreviewFeatures = settingsData.App.PreviewVisibility.FilterPreviewFeatures(settingsData.App.EnabledPreviewFeatures);
this.logger.LogInformation("No valid current-version settings backup was found. Migrating the settings file.");
settingsData = SettingsMigrations.Migrate(this.logger, settingsVersion.Version, await File.ReadAllTextAsync(settingsPath), JSON_OPTIONS);
this.PrepareLoadedSettings(settingsData);
await this.StoreSettingsSnapshot(settingsData, settingsPath);
await this.StoreCurrentVersionBackup(settingsData);
return settingsData;
}
this.logger.LogError("Failed to read the version of the settings file.");
return new();
var currentSettings = await this.TryDeserializeCurrentSettings(settingsPath, "settings file");
if(currentSettings.FailureReason is not SettingsWriteBlockReason.NONE)
{
this.BlockSettingsWrites(currentSettings.FailureReason, "The current settings file could not be safely loaded. Settings writes are blocked to avoid overwriting recoverable settings.");
return await this.TryReadCurrentVersionBackupSnapshotForBlockedSettings();
}
settingsData = currentSettings.SettingsData!;
this.PrepareLoadedSettings(settingsData);
await this.StoreCurrentVersionBackup(settingsData);
return settingsData;
}
private async Task<SettingsVersionReadResult> TryReadSettingsVersion(string settingsPath)
{
try
{
await using var settingsStream = File.OpenRead(settingsPath);
using var settingsDocument = await JsonDocument.ParseAsync(settingsStream);
if(!settingsDocument.RootElement.TryGetProperty("Version", out var versionElement))
{
this.logger.LogError($"Failed to read the version of the settings file '{settingsPath}'.");
return new(Version.UNKNOWN, SettingsWriteBlockReason.VERSION_MISSING);
}
if(versionElement.ValueKind is JsonValueKind.String && versionElement.GetString() is { } versionText)
{
if(Enum.TryParse(versionText, out Version stringVersion) && Enum.IsDefined(stringVersion) && stringVersion is not Version.UNKNOWN)
return new(stringVersion, SettingsWriteBlockReason.NONE);
if(versionText.StartsWith('V') && int.TryParse(versionText[1..], out var futureVersion) && futureVersion > (int)CURRENT_SETTINGS_VERSION)
return new((Version)futureVersion, SettingsWriteBlockReason.NONE);
if(int.TryParse(versionText, out var numericStringVersion) && numericStringVersion > (int)CURRENT_SETTINGS_VERSION)
return new((Version)numericStringVersion, SettingsWriteBlockReason.NONE);
}
if(versionElement.ValueKind is JsonValueKind.Number && versionElement.TryGetInt32(out var numericVersion) && numericVersion > (int)Version.UNKNOWN && (Enum.IsDefined(typeof(Version), numericVersion) || numericVersion > (int)CURRENT_SETTINGS_VERSION))
return new((Version)numericVersion, SettingsWriteBlockReason.NONE);
}
catch(Exception e)
{
this.logger.LogError(e, $"Failed to read the version of the settings file '{settingsPath}'.");
return new(Version.UNKNOWN, SettingsWriteBlockReason.FILE_UNREADABLE);
}
return new(Version.UNKNOWN, SettingsWriteBlockReason.VERSION_UNKNOWN);
}
private async Task<Data?> TryReadCurrentVersionBackupSnapshot()
{
var backupSettingsPath = GetBackupSettingsPath(CURRENT_SETTINGS_VERSION);
if(!File.Exists(backupSettingsPath))
{
this.logger.LogInformation($"The settings backup file '{backupSettingsPath}' does not exist.");
return null;
}
var backupVersion = await this.TryReadSettingsVersion(backupSettingsPath);
if(backupVersion.FailureReason is not SettingsWriteBlockReason.NONE)
{
this.logger.LogWarning($"The settings backup file '{backupSettingsPath}' could not be used because its version could not be identified. Reason: '{backupVersion.FailureReason}'.");
return null;
}
if(backupVersion.Version != CURRENT_SETTINGS_VERSION)
{
this.logger.LogWarning($"The settings backup file '{backupSettingsPath}' uses version '{backupVersion.Version}' instead of '{CURRENT_SETTINGS_VERSION}'.");
return null;
}
var backupSettings = await this.TryDeserializeCurrentSettings(backupSettingsPath, "settings backup file");
if(backupSettings.FailureReason is not SettingsWriteBlockReason.NONE)
{
this.logger.LogWarning($"The settings backup file '{backupSettingsPath}' could not be used. Reason: '{backupSettings.FailureReason}'.");
return null;
}
return backupSettings.SettingsData;
}
private async Task<Data?> TryReadCurrentVersionBackupSnapshotForBlockedSettings()
{
var settingsData = await this.TryReadCurrentVersionBackupSnapshot();
if(settingsData is null)
{
this.logger.LogWarning($"No valid current-version settings backup was found while settings writes are blocked. Reason: '{this.SettingsWriteBlockReason}'.");
return null;
}
this.PrepareLoadedSettings(settingsData);
this.logger.LogWarning($"Loaded settings from the '{GetBackupSettingsFilename(CURRENT_SETTINGS_VERSION)}' backup file while settings writes remain blocked. Reason: '{this.SettingsWriteBlockReason}'.");
return settingsData;
}
private async Task<CurrentSettingsReadResult> TryDeserializeCurrentSettings(string settingsPath, string sourceDescription)
{
try
{
var settingsData = JsonSerializer.Deserialize<Data>(await File.ReadAllTextAsync(settingsPath), JSON_OPTIONS);
if(settingsData is null)
{
this.logger.LogError($"Failed to parse the {sourceDescription} '{settingsPath}'.");
return new(null, SettingsWriteBlockReason.CURRENT_VERSION_INVALID);
}
if(settingsData.Version != CURRENT_SETTINGS_VERSION)
{
this.logger.LogError($"The {sourceDescription} '{settingsPath}' uses version '{settingsData.Version}' instead of '{CURRENT_SETTINGS_VERSION}'.");
return new(null, SettingsWriteBlockReason.CURRENT_VERSION_INVALID);
}
return new(settingsData, SettingsWriteBlockReason.NONE);
}
catch(Exception e)
{
this.logger.LogError(e, $"Failed to parse the {sourceDescription} '{settingsPath}'.");
return new(null, SettingsWriteBlockReason.FILE_UNREADABLE);
}
}
private void BlockSettingsWrites(SettingsWriteBlockReason reason, string message)
{
this.SettingsWriteBlockReason = reason;
this.logger.LogError($"{message} Reason: '{reason}'.");
}
private void PrepareLoadedSettings(Data settingsData)
{
//
// We filter the enabled preview features based on the preview visibility.
// This is necessary when the app starts up: some preview features may have
// been disabled or released from the last time the app was started.
//
settingsData.App.EnabledPreviewFeatures = settingsData.App.PreviewVisibility.FilterPreviewFeatures(settingsData.App.EnabledPreviewFeatures);
}
/// <summary>
@ -145,19 +298,48 @@ public sealed class SettingsManager
return;
}
if(this.SettingsWriteBlocked)
{
this.logger.LogWarning($"Cannot store settings, because settings writes are blocked. Reason: '{this.SettingsWriteBlockReason}'.");
return;
}
var settingsPath = Path.Combine(ConfigDirectory!, SETTINGS_FILENAME);
await this.StoreSettingsSnapshot(this.ConfigurationData, settingsPath);
await this.StoreCurrentVersionBackup(this.ConfigurationData);
}
private static string GetBackupSettingsFilename(Version version) => $"settings.{version.ToString().ToLowerInvariant()}.json";
private static string GetBackupSettingsPath(Version version) => Path.Combine(ConfigDirectory!, GetBackupSettingsFilename(version));
private async Task StoreCurrentVersionBackup(Data settingsData)
{
if(settingsData.Version != CURRENT_SETTINGS_VERSION)
{
this.logger.LogWarning($"Skipping settings backup because the settings version '{settingsData.Version}' is not the current version '{CURRENT_SETTINGS_VERSION}'.");
return;
}
var backupSettingsPath = GetBackupSettingsPath(CURRENT_SETTINGS_VERSION);
await this.StoreSettingsSnapshot(settingsData, backupSettingsPath);
this.logger.LogInformation($"Stored the settings backup file '{backupSettingsPath}'.");
}
private async Task StoreSettingsSnapshot(Data settingsData, string settingsPath)
{
if(!Directory.Exists(ConfigDirectory))
{
this.logger.LogInformation("Creating the configuration directory.");
Directory.CreateDirectory(ConfigDirectory!);
}
var settingsJson = JsonSerializer.Serialize(this.ConfigurationData, JSON_OPTIONS);
var settingsJson = JsonSerializer.Serialize(settingsData, JSON_OPTIONS);
var tempFile = Path.GetTempFileName();
await File.WriteAllTextAsync(tempFile, settingsJson);
File.Move(tempFile, settingsPath, true);
this.logger.LogInformation("Stored the settings to the file system.");
this.logger.LogInformation($"Stored the settings to '{settingsPath}'.");
}
public void InjectSpellchecking(Dictionary<string, object?> attributes) => attributes["spellcheck"] = this.ConfigurationData.App.EnableSpellchecking ? "true" : "false";
@ -165,9 +347,9 @@ public sealed class SettingsManager
public ConfidenceLevel GetMinimumConfidenceLevel(Tools.Components component)
{
var minimumLevel = ConfidenceLevel.NONE;
var enforceGlobalMinimumConfidence = this.ConfigurationData.LLMProviders is { EnforceGlobalMinimumConfidence: true, GlobalMinimumConfidence: not ConfidenceLevel.NONE and not ConfidenceLevel.UNKNOWN };
var enforceGlobalMinimumConfidence = this.ConfigurationData.Confidence is { EnforceGlobalMinimumConfidence: true, GlobalMinimumConfidence: not ConfidenceLevel.NONE and not ConfidenceLevel.UNKNOWN };
if (enforceGlobalMinimumConfidence)
minimumLevel = this.ConfigurationData.LLMProviders.GlobalMinimumConfidence;
minimumLevel = this.ConfigurationData.Confidence.GlobalMinimumConfidence;
var componentMinimumLevel = component.MinimumConfidence(this);
if (componentMinimumLevel > minimumLevel)
@ -402,7 +584,7 @@ public sealed class SettingsManager
if(llmProvider is LLMProviders.NONE)
return ConfidenceLevel.NONE;
switch (this.ConfigurationData.LLMProviders.ConfidenceScheme)
switch (this.ConfigurationData.Confidence.ConfidenceScheme)
{
case ConfidenceSchemes.TRUST_ALL:
return llmProvider switch
@ -462,7 +644,7 @@ public sealed class SettingsManager
};
case ConfidenceSchemes.CUSTOM:
return this.ConfigurationData.LLMProviders.CustomConfidenceScheme.GetValueOrDefault(llmProvider, ConfidenceLevel.UNKNOWN);
return this.ConfigurationData.Confidence.CustomConfidenceScheme.GetValueOrDefault(llmProvider, ConfidenceLevel.UNKNOWN);
default:
return ConfidenceLevel.UNKNOWN;
@ -485,4 +667,4 @@ public sealed class SettingsManager
// Return the full name of the property, including the class name:
return $"{typeof(TIn).Name}.{memberExpr.Member.Name}";
}
}
}

View File

@ -24,7 +24,8 @@ public static class SettingsMigrations
configV1 = MigrateV1ToV2(logger, configV1);
configV1 = MigrateV2ToV3(logger, configV1);
var configV14 = MigrateV3ToV4(logger, configV1);
return MigrateV4ToV5(logger, configV14);
var configV15 = MigrateV4ToV5(logger, configV14);
return MigrateV5ToV6(logger, configV15);
case Version.V2:
var configV2 = JsonSerializer.Deserialize<DataV1V3>(configData, jsonOptions);
@ -36,7 +37,8 @@ public static class SettingsMigrations
configV2 = MigrateV2ToV3(logger, configV2);
var configV24 = MigrateV3ToV4(logger, configV2);
return MigrateV4ToV5(logger, configV24);
var configV25 = MigrateV4ToV5(logger, configV24);
return MigrateV5ToV6(logger, configV25);
case Version.V3:
var configV3 = JsonSerializer.Deserialize<DataV1V3>(configData, jsonOptions);
@ -47,8 +49,9 @@ public static class SettingsMigrations
}
var configV34 = MigrateV3ToV4(logger, configV3);
return MigrateV4ToV5(logger, configV34);
var configV35 = MigrateV4ToV5(logger, configV34);
return MigrateV5ToV6(logger, configV35);
case Version.V4:
var configV4 = JsonSerializer.Deserialize<DataV4>(configData, jsonOptions);
if (configV4 is null)
@ -57,18 +60,29 @@ public static class SettingsMigrations
return new();
}
return MigrateV4ToV5(logger, configV4);
var configV45 = MigrateV4ToV5(logger, configV4);
return MigrateV5ToV6(logger, configV45);
default:
logger.LogInformation("No configuration migration is needed.");
var configV5 = JsonSerializer.Deserialize<Data>(configData, jsonOptions);
case Version.V5:
var configV5 = JsonSerializer.Deserialize<DataV5>(configData, jsonOptions);
if (configV5 is null)
{
logger.LogError("Failed to parse the v4 configuration. Using default values.");
logger.LogError("Failed to parse the v5 configuration. Using default values.");
return new();
}
return configV5;
return MigrateV5ToV6(logger, configV5);
default:
logger.LogInformation("No configuration migration is needed.");
var configV6 = JsonSerializer.Deserialize<Data>(configData, jsonOptions);
if (configV6 is null)
{
logger.LogError("Failed to parse the v6 configuration. Using default values.");
return new();
}
return configV6;
}
}
@ -83,9 +97,9 @@ public static class SettingsMigrations
return new()
{
Version = Version.V2,
Providers = previousData.Providers.Select(provider => provider with { IsSelfHosted = false, Hostname = string.Empty }).ToList(),
EnableSpellchecking = previousData.EnableSpellchecking,
IsSavingEnergy = previousData.IsSavingEnergy,
NextProviderNum = previousData.NextProviderNum,
@ -93,7 +107,7 @@ public static class SettingsMigrations
UpdateInterval = previousData.UpdateInterval,
};
}
private static DataV1V3 MigrateV2ToV3(ILogger<SettingsManager> logger, DataV1V3 previousData)
{
//
@ -109,7 +123,7 @@ public static class SettingsMigrations
{
if(provider.IsSelfHosted)
return provider with { Host = Host.LM_STUDIO };
return provider with { Host = Host.NONE };
}).ToList(),
@ -129,14 +143,14 @@ public static class SettingsMigrations
// Summary:
// We grouped the settings into different categories.
//
logger.LogInformation("Migrating from v3 to v4...");
return new()
{
Version = Version.V4,
Providers = previousConfig.Providers,
NextProviderNum = previousConfig.NextProviderNum,
App = new(x => x.App)
{
EnableSpellchecking = previousConfig.EnableSpellchecking,
@ -144,27 +158,27 @@ public static class SettingsMigrations
UpdateInterval = previousConfig.UpdateInterval,
NavigationBehavior = previousConfig.NavigationBehavior,
},
Chat = new()
{
ShortcutSendBehavior = previousConfig.ShortcutSendBehavior,
PreselectOptions = previousConfig.PreselectChatOptions,
PreselectedProvider = previousConfig.PreselectedChatProvider,
},
Workspace = new()
{
StorageBehavior = previousConfig.WorkspaceStorageBehavior,
StorageTemporaryMaintenancePolicy = previousConfig.WorkspaceStorageTemporaryMaintenancePolicy,
},
IconFinder = new()
{
PreselectOptions = previousConfig.PreselectIconOptions,
PreselectedProvider = previousConfig.PreselectedIconProvider,
PreselectedSource = previousConfig.PreselectedIconSource,
},
Translation = new()
{
PreselectLiveTranslation = previousConfig.PreselectLiveTranslation,
@ -177,7 +191,7 @@ public static class SettingsMigrations
PreselectContentCleanerAgent = previousConfig.PreselectContentCleanerAgentForTranslation,
PreselectWebContentReader = previousConfig.PreselectWebContentReaderForTranslation,
},
Coding = new()
{
PreselectOptions = previousConfig.PreselectCodingOptions,
@ -186,7 +200,7 @@ public static class SettingsMigrations
PreselectedOtherProgrammingLanguage = previousConfig.PreselectedCodingOtherLanguage,
PreselectCompilerMessages = previousConfig.PreselectCodingCompilerMessages,
},
TextSummarizer = new()
{
PreselectOptions = previousConfig.PreselectTextSummarizerOptions,
@ -199,7 +213,7 @@ public static class SettingsMigrations
PreselectContentCleanerAgent = previousConfig.PreselectContentCleanerAgentForTextSummarizer,
PreselectWebContentReader = previousConfig.PreselectWebContentReaderForTextSummarizer,
},
TextContentCleaner = new()
{
PreselectAgentOptions = previousConfig.PreselectAgentTextContentCleanerOptions,
@ -207,14 +221,14 @@ public static class SettingsMigrations
},
};
}
private static Data MigrateV4ToV5(ILogger<SettingsManager> logger, DataV4 previousConfig)
private static DataV5 MigrateV4ToV5(ILogger<SettingsManager> logger, DataV4 previousConfig)
{
//
// Summary:
// We renamed the LLM provider enum.
//
logger.LogInformation("Migrating from v4 to v5...");
return new()
{
@ -241,4 +255,68 @@ public static class SettingsMigrations
MyTasks = previousConfig.MyTasks,
};
}
private static Data MigrateV5ToV6(ILogger<SettingsManager> logger, DataV5 previousConfig)
{
//
// Summary:
// We moved confidence settings out of LLM provider settings.
//
logger.LogInformation("Migrating from v5 to v6...");
return new()
{
Version = Version.V6,
Providers = previousConfig.Providers,
Confidence = new(x => x.Confidence)
{
EnforceGlobalMinimumConfidence = previousConfig.LLMProviders.EnforceGlobalMinimumConfidence,
GlobalMinimumConfidence = previousConfig.LLMProviders.GlobalMinimumConfidence,
ShowProviderConfidence = previousConfig.LLMProviders.ShowProviderConfidence,
ConfidenceScheme = previousConfig.LLMProviders.ConfidenceScheme,
CustomConfidenceScheme = previousConfig.LLMProviders.CustomConfidenceScheme,
},
EmbeddingProviders = previousConfig.EmbeddingProviders,
TranscriptionProviders = previousConfig.TranscriptionProviders,
DataSources = previousConfig.DataSources,
Profiles = previousConfig.Profiles,
ChatTemplates = previousConfig.ChatTemplates,
EnabledPlugins = previousConfig.EnabledPlugins,
ManagedEditableDefaults = previousConfig.ManagedEditableDefaults,
AssistantPluginAudits = previousConfig.AssistantPluginAudits,
NextProviderNum = previousConfig.NextProviderNum,
NextEmbeddingNum = previousConfig.NextEmbeddingNum,
NextTranscriptionNum = previousConfig.NextTranscriptionNum,
NextDataSourceNum = previousConfig.NextDataSourceNum,
NextProfileNum = previousConfig.NextProfileNum,
NextChatTemplateNum = previousConfig.NextChatTemplateNum,
NextDocumentAnalysisPolicyNum = previousConfig.NextDocumentAnalysisPolicyNum,
App = previousConfig.App,
Chat = previousConfig.Chat,
Workspace = previousConfig.Workspace,
IconFinder = previousConfig.IconFinder,
Translation = previousConfig.Translation,
Coding = previousConfig.Coding,
ERI = previousConfig.ERI,
DocumentAnalysis = previousConfig.DocumentAnalysis,
MandatoryInformation = previousConfig.MandatoryInformation,
TextSummarizer = previousConfig.TextSummarizer,
TextContentCleaner = previousConfig.TextContentCleaner,
AgentDataSourceSelection = previousConfig.AgentDataSourceSelection,
AgentRetrievalContextValidation = previousConfig.AgentRetrievalContextValidation,
AssistantPluginAudit = previousConfig.AssistantPluginAudit,
Agenda = previousConfig.Agenda,
GrammarSpelling = previousConfig.GrammarSpelling,
RewriteImprove = previousConfig.RewriteImprove,
PromptOptimizer = previousConfig.PromptOptimizer,
EMail = previousConfig.EMail,
SlideBuilder = previousConfig.SlideBuilder,
LegalCheck = previousConfig.LegalCheck,
Synonyms = previousConfig.Synonyms,
MyTasks = previousConfig.MyTasks,
JobPostings = previousConfig.JobPostings,
BiasOfTheDay = previousConfig.BiasOfTheDay,
I18N = previousConfig.I18N,
};
}
}

View File

@ -0,0 +1,11 @@
namespace AIStudio.Settings;
public enum SettingsWriteBlockReason
{
NONE,
VERSION_MISSING,
VERSION_UNKNOWN,
VERSION_NEWER_THAN_APP,
FILE_UNREADABLE,
CURRENT_VERSION_INVALID,
}

View File

@ -44,7 +44,7 @@ public sealed record TranscriptionProvider(
/// <inheritdoc />
[JsonIgnore]
public string SecretId => this.IsEnterpriseConfiguration ? $"{ISecretId.ENTERPRISE_KEY_PREFIX}::{this.UsedLLMProvider.ToName()}" : this.UsedLLMProvider.ToName();
public string SecretId => this.IsEnterpriseConfiguration ? $"{ISecretId.ENTERPRISE_KEY_PREFIX}::{this.UsedLLMProvider.ToSecretId()}" : this.UsedLLMProvider.ToSecretId();
/// <inheritdoc />
[JsonIgnore]
@ -125,7 +125,7 @@ public sealed record TranscriptionProvider(
{
// Queue the API key for storage in the OS keyring:
PendingEnterpriseApiKeys.Add(new(
$"{ISecretId.ENTERPRISE_KEY_PREFIX}::{usedLLMProvider.ToName()}",
$"{ISecretId.ENTERPRISE_KEY_PREFIX}::{usedLLMProvider.ToSecretId()}",
name,
decryptedApiKey,
SecretStoreType.TRANSCRIPTION_PROVIDER));

View File

@ -13,4 +13,5 @@ public enum Version
V3,
V4,
V5,
V6,
}

View File

@ -0,0 +1,18 @@
namespace AIStudio.Tools.PluginSystem;
/// <summary>
/// Represents complex content from a configuration plugin that is read live from
/// running plugins and is not persisted to the settings data model.
/// </summary>
public interface ILivePluginContent
{
/// <summary>
/// The stable ID of the live plugin content.
/// </summary>
public string Id { get; }
/// <summary>
/// The ID of the enterprise configuration plugin that provides this content.
/// </summary>
public Guid EnterpriseConfigurationPluginId { get; }
}

View File

@ -14,6 +14,7 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
private List<PluginConfigurationObject> configObjects = [];
private List<DataMandatoryInfo> mandatoryInfos = [];
private List<DataIntroduction> introductions = [];
/// <summary>
/// The list of configuration objects. Configuration objects are, e.g., providers or chat templates.
@ -22,9 +23,16 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
/// <summary>
/// The list of mandatory infos provided by this configuration plugin.
/// Mandatory infos are live plugin content and are not persisted to ConfigurationData.
/// </summary>
public IReadOnlyList<DataMandatoryInfo> MandatoryInfos => this.mandatoryInfos;
/// <summary>
/// The list of introductions provided by this configuration plugin.
/// Introductions are live plugin content and are not persisted to ConfigurationData.
/// </summary>
public IReadOnlyList<DataIntroduction> Introductions => this.introductions;
/// <summary>
/// True/false when explicitly configured in the plugin, otherwise null.
/// </summary>
@ -130,6 +138,7 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
{
this.configObjects.Clear();
this.mandatoryInfos.Clear();
this.introductions.Clear();
// Ensure that the main CONFIG table exists and is a valid Lua table:
if (!this.State.Environment["CONFIG"].TryRead<LuaTable>(out var mainTable))
@ -154,6 +163,9 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
// Config: what should be the start page?
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.StartPage, this.Id, settingsTable, dryRun);
// Config: show built-in introduction on the home page?
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.ShowIntroduction, this.Id, settingsTable, dryRun);
// Config: show quick start guide on the home page?
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.ShowQuickStartGuide, this.Id, settingsTable, dryRun);
@ -182,6 +194,16 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.ExternalHttpCustomRootCertificatesEnabled, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.ExternalHttpCustomRootCertificateBundlePath, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.ExternalHttpCustomRootCertificateAllowedHosts, this.Id, settingsTable, dryRun);
// Config: provider confidence settings
ManagedConfiguration.TryProcessConfiguration(x => x.Confidence, x => x.EnforceGlobalMinimumConfidence, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.Confidence, x => x.GlobalMinimumConfidence, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.Confidence, x => x.ShowProviderConfidence, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.Confidence, x => x.ConfidenceScheme, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.Confidence, x => x.CustomConfidenceScheme, this.Id, settingsTable, dryRun);
// Config: data source security settings
ManagedConfiguration.TryProcessConfiguration(x => x.DataSourceSecurity, x => x.TrustedProviderIds, this.Id, settingsTable, dryRun);
// Handle configured LLM providers:
PluginConfigurationObject.TryParse(PluginConfigurationObjectType.LLM_PROVIDER, x => x.Providers, x => x.NextProviderNum, mainTable, this.Id, ref this.configObjects, dryRun);
@ -206,6 +228,9 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
// Handle configured mandatory infos:
this.TryReadMandatoryInfos(mainTable);
// Handle configured introductions:
this.TryReadIntroductions(mainTable);
// Config: preselected provider?
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.PreselectedProvider, Guid.Empty, this.Id, settingsTable, dryRun);
@ -213,6 +238,12 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
// Config: preselected profile?
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.PreselectedProfile, Guid.Empty, this.Id, settingsTable, dryRun);
// Config: preselected chat options?
ManagedConfiguration.TryProcessConfiguration(x => x.Chat, x => x.PreselectOptions, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.Chat, x => x.PreselectedProvider, Guid.Empty, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.Chat, x => x.PreselectedProfile, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.Chat, x => x.PreselectedChatTemplate, this.Id, settingsTable, dryRun);
// Config: transcription provider?
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.UseTranscriptionProvider, Guid.Empty, this.Id, settingsTable, dryRun);
@ -240,4 +271,25 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
LOG.LogWarning("The table 'MANDATORY_INFOS' entry at index {Index} does not contain a valid mandatory info (config plugin id: {ConfigPluginId}).", i, this.Id);
}
}
private void TryReadIntroductions(LuaTable mainTable)
{
if (!mainTable.TryGetValue("INTRODUCTIONS", out var introductionsValue) || !introductionsValue.TryRead<LuaTable>(out var introductionsTable))
return;
for (var i = 1; i <= introductionsTable.ArrayLength; i++)
{
var luaIntroductionValue = introductionsTable[i];
if (!luaIntroductionValue.TryRead<LuaTable>(out var luaIntroductionTable))
{
LOG.LogWarning("The table 'INTRODUCTIONS' entry at index {Index} is not a valid table (config plugin id: {ConfigPluginId}).", i, this.Id);
continue;
}
if (DataIntroduction.TryParseConfiguration(i, luaIntroductionTable, this.Id, out var introduction))
this.introductions.Add(introduction);
else
LOG.LogWarning("The table 'INTRODUCTIONS' entry at index {Index} does not contain a valid introduction (config plugin id: {ConfigPluginId}).", i, this.Id);
}
}
}

View File

@ -201,6 +201,19 @@ public static partial class PluginFactory
// Check for a preselected profile:
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.PreselectedProfile, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
// Check for preselected chat options:
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Chat, x => x.PreselectOptions, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Chat, x => x.PreselectedProvider, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Chat, x => x.PreselectedProfile, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Chat, x => x.PreselectedChatTemplate, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
// Check for the update interval:
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.UpdateInterval, AVAILABLE_PLUGINS))
@ -214,6 +227,10 @@ public static partial class PluginFactory
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.StartPage, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
// Check for the built-in introduction visibility:
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.ShowIntroduction, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
// Check for the quick start guide visibility:
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.ShowQuickStartGuide, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
@ -263,6 +280,26 @@ public static partial class PluginFactory
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.ExternalHttpCustomRootCertificateAllowedHosts, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
// Check provider confidence settings:
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Confidence, x => x.EnforceGlobalMinimumConfidence, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Confidence, x => x.GlobalMinimumConfidence, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Confidence, x => x.ShowProviderConfidence, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Confidence, x => x.ConfidenceScheme, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Confidence, x => x.CustomConfidenceScheme, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
// Check data source security settings:
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.DataSourceSecurity, x => x.TrustedProviderIds, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
// Check if audit is required before it can be activated
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.AssistantPluginAudit, x => x.RequireAuditBeforeActivation, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;

View File

@ -136,4 +136,13 @@ public static partial class PluginFactory
.SelectMany(plugin => plugin.MandatoryInfos)
.ToList();
}
public static IReadOnlyList<DataIntroduction> GetIntroductions()
{
return RUNNING_PLUGINS
.OfType<PluginConfiguration>()
.SelectMany(plugin => plugin.Introductions)
.OrderBy(introduction => introduction.Index)
.ToList();
}
}

View File

@ -1,6 +1,5 @@
using AIStudio.Assistants.ERI;
using AIStudio.Provider;
using AIStudio.Provider.SelfHosted;
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using AIStudio.Tools.ERIClient;
@ -43,7 +42,7 @@ public sealed class DataSourceService
return new([], []);
}
return await this.GetDataSources(selectedLLMProvider.IsSelfHosted, previousSelectedDataSources);
return await this.GetDataSources(selectedLLMProvider.IsTrustedForDataSourceSecurityChecks(this.settingsManager), previousSelectedDataSources);
}
/// <summary>
@ -66,10 +65,10 @@ public sealed class DataSourceService
return new([], []);
}
return await this.GetDataSources(selectedLLMProvider is ProviderSelfHosted, previousSelectedDataSources);
return await this.GetDataSources(selectedLLMProvider.IsTrustedForDataSourceSecurityChecks(this.settingsManager), previousSelectedDataSources);
}
private async Task<AllowedSelectedDataSources> GetDataSources(bool usingSelfHostedProvider, IReadOnlyCollection<IDataSource>? previousSelectedDataSources = null)
private async Task<AllowedSelectedDataSources> GetDataSources(bool usingTrustedProvider, IReadOnlyCollection<IDataSource>? previousSelectedDataSources = null)
{
var allDataSources = this.settingsManager.ConfigurationData.DataSources;
var filteredDataSources = new List<IDataSource>(allDataSources.Count);
@ -78,7 +77,7 @@ public sealed class DataSourceService
// Start all checks in parallel:
foreach (var source in allDataSources)
tasks.Add(this.CheckOneDataSource(source, usingSelfHostedProvider));
tasks.Add(this.CheckOneDataSource(source, usingTrustedProvider));
// Wait for all checks and collect the results:
foreach (var task in tasks)
@ -95,7 +94,7 @@ public sealed class DataSourceService
return new(filteredDataSources, filteredSelectedDataSources);
}
private async Task<IDataSource?> CheckOneDataSource(IDataSource source, bool usingSelfHostedProvider)
private async Task<IDataSource?> CheckOneDataSource(IDataSource source, bool usingTrustedProvider)
{
//
// Unfortunately, we have to live-check any ERI source for its security requirements.
@ -137,10 +136,10 @@ public sealed class DataSourceService
case DataSourceSecurity.ALLOW_ANY:
//
// Case: The data source allows any provider type. We want to use a self-hosted provider.
// Case: The data source allows any provider type. We want to use a trusted provider.
// There is no issue with this source. Accept it.
//
if(usingSelfHostedProvider)
if(usingTrustedProvider)
return source;
//
@ -151,13 +150,13 @@ public sealed class DataSourceService
return source;
//
// Case: The ERI source requires a self-hosted provider. This misconfiguration happens
// Case: The ERI source requires a self-hosted or organization-trusted provider. This misconfiguration happens
// when the ERI server operator changes the security requirements. The ERI server
// operator owns the data -- we have to respect their rules. We skip this source.
//
if (eriSourceRequirements is { AllowedProviderType: ProviderType.SELF_HOSTED })
{
this.logger.LogWarning($"The ERI source '{source.Name}' (id={source.Id}) requires a self-hosted provider. We skip this source.");
this.logger.LogWarning($"The ERI source '{source.Name}' (id={source.Id}) requires a self-hosted or organization-trusted provider. We skip this source.");
return null;
}
@ -171,22 +170,22 @@ public sealed class DataSourceService
//
// Case: Missing rules. We skip this source. Better safe than sorry.
//
this.logger.LogDebug($"The ERI source '{source.Name}' (id={source.Id}) was filtered out due to missing rules.");
this.logger.LogWarning($"The ERI source '{source.Name}' (id={source.Id}) was filtered out due to missing rules.");
return null;
//
// Case: The data source requires a self-hosted provider. We want to use a self-hosted provider.
// Case: The data source requires a trusted provider. We want to use a trusted provider.
// There is no issue with this source. Accept it.
//
case DataSourceSecurity.SELF_HOSTED when usingSelfHostedProvider:
case DataSourceSecurity.SELF_HOSTED when usingTrustedProvider:
return source;
//
// Case: The data source requires a self-hosted provider. We want to use a cloud provider.
// Case: The data source requires a trusted provider. We want to use an untrusted provider.
// We skip this source.
//
case DataSourceSecurity.SELF_HOSTED when !usingSelfHostedProvider:
this.logger.LogWarning($"The data source '{source.Name}' (id={source.Id}) requires a self-hosted provider. We skip this source.");
case DataSourceSecurity.SELF_HOSTED when !usingTrustedProvider:
this.logger.LogWarning($"The data source '{source.Name}' (id={source.Id}) requires a self-hosted or organization-trusted provider. We skip this source.");
return null;
//

View File

@ -4,6 +4,23 @@ namespace AIStudio.Tools.Services;
public sealed partial class RustService
{
private const string SELF_HOSTED_SECRET_ID = "Self-hosted";
// Temporary compatibility shim until 2026-12-19:
// documentation/compatibility-shims/2026-06-self-hosted-secret-id.md
private const string LEGACY_SELF_HOSTED_SECRET_ID_DE = "Selbst gehostet";
private static string APIKey(SecretStoreType storeType, ISecretId secretId) => $"{storeType.Prefix()}::{secretId.SecretId}::{secretId.SecretName}::api_key";
private static IEnumerable<string> LegacySelfHostedAPIKeys(ISecretId secretId, SecretStoreType storeType)
{
if (secretId.SecretId == SELF_HOSTED_SECRET_ID)
yield return $"{storeType.Prefix()}::{LEGACY_SELF_HOSTED_SECRET_ID_DE}::{secretId.SecretName}::api_key";
if (secretId.SecretId == $"{ISecretId.ENTERPRISE_KEY_PREFIX}::{SELF_HOSTED_SECRET_ID}")
yield return $"{storeType.Prefix()}::{ISecretId.ENTERPRISE_KEY_PREFIX}::{LEGACY_SELF_HOSTED_SECRET_ID_DE}::{secretId.SecretName}::api_key";
}
/// <summary>
/// Try to get the API key for the given secret ID.
/// </summary>
@ -13,24 +30,55 @@ public sealed partial class RustService
/// <returns>The requested secret.</returns>
public async Task<RequestedSecret> GetAPIKey(ISecretId secretId, SecretStoreType storeType, bool isTrying = false)
{
var prefix = storeType.Prefix();
var secretRequest = new SelectSecretRequest($"{prefix}::{secretId.SecretId}::{secretId.SecretName}::api_key", Environment.UserName, isTrying);
var secretKey = APIKey(storeType, secretId);
var legacySecretKeys = LegacySelfHostedAPIKeys(secretId, storeType).ToList();
var secret = await this.GetAPIKeyByKey(secretKey, isTrying || legacySecretKeys.Count > 0);
if (secret.Success)
{
foreach (var legacySecretKey in legacySecretKeys)
await this.DeleteAPIKeyByKey(legacySecretKey, isTrying: true);
return secret;
}
foreach (var legacySecretKey in legacySecretKeys)
{
var legacySecret = await this.GetAPIKeyByKey(legacySecretKey, isTrying: true);
if (!legacySecret.Success)
continue;
this.logger!.LogInformation($"Migrating legacy self-hosted API key namespace '{legacySecretKey}' to '{secretKey}'.");
var migrationResult = await this.StoreEncryptedAPIKeyByKey(secretKey, legacySecret.Secret);
if (migrationResult.Success)
await this.DeleteAPIKeyByKey(legacySecretKey, isTrying: true);
else
this.logger!.LogWarning($"Failed to migrate legacy self-hosted API key namespace '{legacySecretKey}' to '{secretKey}': '{migrationResult.Issue}'");
return legacySecret;
}
if (!isTrying)
this.logger!.LogError($"Failed to get the API key for '{secretKey}': '{secret.Issue}'");
return secret;
}
private async Task<RequestedSecret> GetAPIKeyByKey(string secretKey, bool isTrying)
{
var secretRequest = new SelectSecretRequest(secretKey, Environment.UserName, isTrying);
var result = await this.http.PostAsJsonAsync("/secrets/get", secretRequest, this.jsonRustSerializerOptions);
if (!result.IsSuccessStatusCode)
{
if(!isTrying)
this.logger!.LogError($"Failed to get the API key for '{prefix}::{secretId.SecretId}::{secretId.SecretName}::api_key' due to an API issue: '{result.StatusCode}'");
this.logger!.LogError($"Failed to get the API key for '{secretKey}' due to an API issue: '{result.StatusCode}'");
return new RequestedSecret(false, new EncryptedText(string.Empty), TB("Failed to get the API key due to an API issue."));
}
var secret = await result.Content.ReadFromJsonAsync<RequestedSecret>(this.jsonRustSerializerOptions);
if (!secret.Success && !isTrying)
this.logger!.LogError($"Failed to get the API key for '{prefix}::{secretId.SecretId}::{secretId.SecretName}::api_key': '{secret.Issue}'");
if (secret.Success)
this.logger!.LogDebug($"Successfully retrieved the API key for '{prefix}::{secretId.SecretId}::{secretId.SecretName}::api_key'.");
this.logger!.LogDebug($"Successfully retrieved the API key for '{secretKey}'.");
else if (isTrying)
this.logger!.LogDebug($"No API key configured for '{prefix}::{secretId.SecretId}::{secretId.SecretName}::api_key' (try mode): '{secret.Issue}'");
this.logger!.LogDebug($"No API key configured for '{secretKey}' (try mode): '{secret.Issue}'");
return secret;
}
@ -44,21 +92,34 @@ public sealed partial class RustService
/// <returns>The store secret response.</returns>
public async Task<StoreSecretResponse> SetAPIKey(ISecretId secretId, string key, SecretStoreType storeType)
{
var prefix = storeType.Prefix();
var encryptedKey = await this.encryptor!.Encrypt(key);
var request = new StoreSecretRequest($"{prefix}::{secretId.SecretId}::{secretId.SecretName}::api_key", Environment.UserName, encryptedKey);
var secretKey = APIKey(storeType, secretId);
var state = await this.StoreEncryptedAPIKeyByKey(secretKey, encryptedKey);
if (state.Success)
{
foreach (var legacySecretKey in LegacySelfHostedAPIKeys(secretId, storeType))
await this.DeleteAPIKeyByKey(legacySecretKey, isTrying: true);
}
return state;
}
private async Task<StoreSecretResponse> StoreEncryptedAPIKeyByKey(string secretKey, EncryptedText encryptedKey)
{
var request = new StoreSecretRequest(secretKey, Environment.UserName, encryptedKey);
var result = await this.http.PostAsJsonAsync("/secrets/store", request, this.jsonRustSerializerOptions);
if (!result.IsSuccessStatusCode)
{
this.logger!.LogError($"Failed to store the API key for '{prefix}::{secretId.SecretId}::{secretId.SecretName}::api_key' due to an API issue: '{result.StatusCode}'");
return new StoreSecretResponse(false, TB("Failed to get the API key due to an API issue."));
this.logger!.LogError($"Failed to store the API key for '{secretKey}' due to an API issue: '{result.StatusCode}'");
return new StoreSecretResponse(false, TB("Failed to store the API key due to an API issue."));
}
var state = await result.Content.ReadFromJsonAsync<StoreSecretResponse>(this.jsonRustSerializerOptions);
if (!state.Success)
this.logger!.LogError($"Failed to store the API key for '{prefix}::{secretId.SecretId}::{secretId.SecretName}::api_key': '{state.Issue}'");
this.logger!.LogError($"Failed to store the API key for '{secretKey}': '{state.Issue}'");
else
this.logger!.LogDebug($"Successfully stored the API key for '{secretKey}'.");
this.logger!.LogDebug($"Successfully stored the API key for '{prefix}::{secretId.SecretId}::{secretId.SecretName}::api_key'.");
return state;
}
@ -70,18 +131,35 @@ public sealed partial class RustService
/// <returns>The delete secret response.</returns>
public async Task<DeleteSecretResponse> DeleteAPIKey(ISecretId secretId, SecretStoreType storeType)
{
var prefix = storeType.Prefix();
var request = new SelectSecretRequest($"{prefix}::{secretId.SecretId}::{secretId.SecretName}::api_key", Environment.UserName, false);
var deleteResult = await this.DeleteAPIKeyByKey(APIKey(storeType, secretId));
if (!deleteResult.Success)
return deleteResult;
foreach (var legacySecretKey in LegacySelfHostedAPIKeys(secretId, storeType))
{
var legacyDeleteResult = await this.DeleteAPIKeyByKey(legacySecretKey, isTrying: true);
if (!legacyDeleteResult.Success)
return legacyDeleteResult;
deleteResult = deleteResult with { WasEntryFound = deleteResult.WasEntryFound || legacyDeleteResult.WasEntryFound };
}
return deleteResult;
}
private async Task<DeleteSecretResponse> DeleteAPIKeyByKey(string secretKey, bool isTrying = false)
{
var request = new SelectSecretRequest(secretKey, Environment.UserName, false);
var result = await this.http.PostAsJsonAsync("/secrets/delete", request, this.jsonRustSerializerOptions);
if (!result.IsSuccessStatusCode)
{
this.logger!.LogError($"Failed to delete the API key for secret ID '{secretId.SecretId}' due to an API issue: '{result.StatusCode}'");
this.logger!.LogError($"Failed to delete the API key for '{secretKey}' due to an API issue: '{result.StatusCode}'");
return new DeleteSecretResponse{Success = false, WasEntryFound = false, Issue = TB("Failed to delete the API key due to an API issue.")};
}
var state = await result.Content.ReadFromJsonAsync<DeleteSecretResponse>(this.jsonRustSerializerOptions);
if (!state.Success)
this.logger!.LogError($"Failed to delete the API key for secret ID '{secretId.SecretId}': '{state.Issue}'");
if (!state.Success && !isTrying)
this.logger!.LogError($"Failed to delete the API key for '{secretKey}': '{state.Issue}'");
return state;
}

View File

@ -1 +1,11 @@
# v26.6.2, build 242 (2026-06-xx xx:xx UTC)
# v26.6.2, build 242 (2026-06-21 14:07 UTC)
- Added a read-only view for organization-managed profiles and chat templates, so users can inspect the content while the organization remains in control of changes.
- Added support for organization-managed chat defaults. Configuration plugins can now preselect the chat provider, profile, and chat template, either as locked values or editable defaults.
- Added support for organization-managed introduction texts on the home page. Configuration plugins can now add custom Markdown introductions and hide the built-in introduction. Thanks, Harald, for the feedback about the built-in introduction text.
- Added support for organization-managed provider confidence settings. Configuration plugins can now set confidence presets, custom confidence schemes, and an app-wide minimum confidence level.
- Added support for organization-trusted providers in data source security checks. Configuration plugins can now mark specific provider instances as trusted for data source usage and local embedding warnings.
- Changed provider confidence settings to appear in their own settings panel, because they apply to LLM, embedding, and transcription providers.
- Fixed chat provider, profile, and template selections not updating live after configuration plugins were changed.
- Fixed organization-managed chat templates not showing the correct icon in the chat template selection menu.
- Fixed personal settings sometimes being lost after a settings-format upgrade when an older app version was started again. AI Studio now keeps versioned settings backups, restores the latest compatible backup when needed, and warns users when settings cannot be saved safely.
- Fixed self-hosted provider API keys sometimes being stored under a localized name. AI Studio now uses a stable key name, keeps correct entries working, and automatically migrates known localized entries for LLM, transcription, and embedding providers. Organizations using configuration plugins do not need to change their plugins; affected users who still see an invalid API key warning should open the provider, transcription, or embedding settings and update the API key once. Thanks, Tim & Eric, for the detailed bug report and testing help.

View File

@ -0,0 +1,2 @@
# v26.6.3, build 243 (2026-06-xx xx:xx UTC)
- Improved the chat experience by automatically focusing the message composer again when it becomes available. Thanks, Dominic Neuburg (`donework`), for the contribution.

View File

@ -0,0 +1,26 @@
# Qdrant Edge Migration
- Status: Active
- Introduced: 2026-06-02
- Remove after: 2026-12-02
- Code references:
- `runtime/src/qdrant_edge_database.rs`
## User Impact
Older installations may still contain Qdrant server sidecar binaries or directories after upgrading to a release that uses Qdrant Edge.
Without this shim, obsolete Qdrant server files could remain in application data or bundled resource locations even though AI Studio no longer starts or uses the separate Qdrant server process.
## Compatibility Behavior
When Qdrant Edge starts, AI Studio checks known previous Qdrant server sidecar locations and attempts to remove obsolete `qdrant` and `qdrant_test` files or directories.
On Windows and macOS production installations, AI Studio also checks the executable directory for old `qdrant.exe` or `qdrant` sidecar binaries. Missing paths are ignored, and failed cleanup attempts are logged without blocking Qdrant Edge startup.
## Removal Checklist
- Remove `remove_obsolete_qdrant_sidecar_files`.
- Remove `remove_obsolete_qdrant_path` if it has no other callers.
- Remove the startup cleanup call from `start_qdrant_edge_database`.
- Update this document's status to `Removed`.

View File

@ -0,0 +1,30 @@
# Self-Hosted Provider Secret ID
- Status: Active
- Introduced: 2026-06-19
- Remove after: 2026-12-19
- Code references:
- `app/MindWork AI Studio/Tools/Services/RustService.APIKeys.cs`
- `app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs`
## User Impact
Some self-hosted provider API keys were stored under a localized OS keyring namespace. In German installations this could produce entries using `Selbst gehostet`, while the fixed canonical namespace is `Self-hosted`.
Without this shim, affected users may see an invalid or missing API key warning until they manually enter the key again.
## Compatibility Behavior
AI Studio uses `Self-hosted` as the canonical secret namespace. For a limited time, API key reads, writes, and deletes also consider the known German legacy namespace `Selbst gehostet`.
When a legacy entry is found, AI Studio stores the same encrypted API key under the canonical namespace and deletes the legacy entry. If the canonical entry already exists, AI Studio also attempts to delete the known legacy alias.
This applies to LLM provider, embedding provider, and transcription provider API keys, including enterprise configuration plugin namespaces.
## Removal Checklist
- Remove `LEGACY_SELF_HOSTED_SECRET_ID_DE`.
- Remove `LegacySelfHostedAPIKeys`.
- Remove legacy lookup, migration, and cleanup calls from API key read, write, and delete paths.
- Keep `LLMProvidersExtensions.ToSecretId()` and the canonical `Self-hosted` namespace.
- Update this document's status to `Removed`.

View File

@ -0,0 +1,44 @@
# Compatibility Shims
Compatibility shims are temporary fallback paths that keep older installations, settings, secrets, plugin data, or external integrations working while users move to a newer release.
Use this folder for short-lived compatibility code such as legacy aliases, read-repair logic, temporary import fallbacks, or cleanup paths. Do not use it for permanent settings schema migrations; those belong in `app/MindWork AI Studio/Settings/SettingsMigrations.cs`.
Every compatibility shim must have:
- A Markdown file in this folder.
- A clear status.
- An introduced date.
- A remove-after date.
- Code references.
- A short explanation of user impact.
- The compatibility behavior.
- A removal checklist.
- A short code comment near the shim that references the Markdown file and remove-after date.
## Template
```md
# Short Title
- Status: Active
- Introduced: YYYY-MM-DD
- Remove after: YYYY-MM-DD
- Code references:
- path/to/file.cs
## User Impact
Describe who needs this compatibility path and what breaks without it.
## Compatibility Behavior
Describe the temporary fallback, alias, read-repair, or cleanup behavior.
## Removal Checklist
- Remove the temporary constants, fallback branches, aliases, or cleanup paths.
- Remove or update tests and static checks that mention the shim.
- Update this document's status to `Removed`.
- Add a changelog entry if removing the shim is user-visible.
```

Some files were not shown because too many files have changed in this diff Show More