diff --git a/AGENTS.md b/AGENTS.md index 48a25021..d559c62e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,7 +7,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co MindWork AI Studio is a cross-platform desktop application for interacting with Large Language Models (LLMs). The app uses a hybrid architecture combining a Rust Tauri runtime (for the native desktop shell) with a .NET Blazor Server web application (for the UI and business logic). **Key Architecture Points:** -- **Runtime:** Rust-based Tauri v1.8 application providing the native window, system integration, and IPC layer +- **Runtime:** Rust-based Tauri v2 application providing the native window, system integration, and IPC layer - **App:** .NET 9 Blazor Server application providing the UI and core functionality - **Communication:** The Rust runtime and .NET app communicate via HTTPS with TLS certificates generated at startup - **Providers:** Multi-provider architecture supporting OpenAI, Anthropic, Google, Mistral, Perplexity, self-hosted models, and others @@ -18,7 +18,7 @@ MindWork AI Studio is a cross-platform desktop application for interacting with ### Prerequisites - .NET 9 SDK - Rust toolchain (stable) -- Tauri v1.6.2 CLI: `cargo install --version 1.6.2 tauri-cli` +- Tauri v2 CLI - Tauri prerequisites (platform-specific dependencies) - **Note:** Development on Linux is discouraged due to complex Tauri dependencies that vary by distribution @@ -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) @@ -151,7 +155,7 @@ Multi-level confidence scheme allows users to control which providers see which ## Dependencies and Frameworks **Rust:** -- Tauri 1.8 - Desktop application framework +- Tauri 2 - Desktop application framework - Axum - HTTPS API server - tokio - Async runtime - keyring - OS keyring integration @@ -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 diff --git a/README.md b/README.md index 73cc6c8b..ec80e887 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,8 @@ Since March 2025: We have started developing the plugin system. There will be la +- 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. diff --git a/app/Build/Commands/CollectI18NKeysCommand.cs b/app/Build/Commands/CollectI18NKeysCommand.cs index d36e650a..760a018a 100644 --- a/app/Build/Commands/CollectI18NKeysCommand.cs +++ b/app/Build/Commands/CollectI18NKeysCommand.cs @@ -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_]*)""")] diff --git a/app/Build/Commands/Pdfium.cs b/app/Build/Commands/Pdfium.cs index 12348a4b..6593ec28 100644 --- a/app/Build/Commands/Pdfium.cs +++ b/app/Build/Commands/Pdfium.cs @@ -7,74 +7,95 @@ namespace Build.Commands; public static class Pdfium { - public static async Task InstallAsync(RID rid, string version) + private static readonly HttpClient CLIENT = new() + { + Timeout = TimeSpan.FromMinutes(5) + }; + + public static async Task InstallAsync(RID rid, string version, bool offline) { Console.Write($"- Installing Pdfium {version} for {rid.ToUserFriendlyName()} ..."); var cwd = Environment.GetRustRuntimeDirectory(); - var pdfiumTmpDownloadPath = Path.GetTempFileName(); - var pdfiumTmpExtractPath = Directory.CreateTempSubdirectory(); var pdfiumUrl = GetPdfiumDownloadUrl(rid, version); + var library = GetLibraryPath(rid); + var pdfiumLibTargetPath = Path.Join(cwd, "resources", "libraries", library.Filename); - // - // Download the file: - // - Console.Write(" downloading ..."); - using (var client = new HttpClient()) + if (offline) { - var response = await client.GetAsync(pdfiumUrl); - if (!response.IsSuccessStatusCode) + if (File.Exists(pdfiumLibTargetPath)) { - Console.WriteLine($" failed to download Pdfium {version} for {rid.ToUserFriendlyName()} from {pdfiumUrl}"); + Console.WriteLine(" offline mode enabled and library already exists, skipping download"); return; } - await using var fileStream = File.Create(pdfiumTmpDownloadPath); - await response.Content.CopyToAsync(fileStream); + Console.WriteLine($" failed because offline mode is enabled and '{pdfiumLibTargetPath}' does not exist"); + return; } - - // - // Extract the downloaded file: - // - Console.Write(" extracting ..."); - await using(var tgzStream = File.Open(pdfiumTmpDownloadPath, FileMode.Open, FileAccess.Read, FileShare.Read)) - { - await using var uncompressedStream = new GZipStream(tgzStream, CompressionMode.Decompress); - await TarFile.ExtractToDirectoryAsync(uncompressedStream, pdfiumTmpExtractPath.FullName, true); - } - - // - // Copy the library to the target directory: - // - Console.Write(" deploying ..."); - var library = GetLibraryPath(rid); + if (string.IsNullOrWhiteSpace(library.Path)) { Console.WriteLine($" failed to find the library path for {rid.ToUserFriendlyName()}"); return; } - - var pdfiumLibSourcePath = Path.Join(pdfiumTmpExtractPath.FullName, library.Path); - var pdfiumLibTargetPath = Path.Join(cwd, "resources", "libraries", library.Filename); - if (!File.Exists(pdfiumLibSourcePath)) + + var pdfiumLibTargetDirectory = Path.Join(cwd, "resources", "libraries"); + var pdfiumLibTmpTargetPath = Path.Join(pdfiumLibTargetDirectory, $"{library.Filename}.{Guid.NewGuid():N}.tmp"); + var pdfiumLibArchivePath = library.Path.Replace('\\', '/'); + + // + // Download the file: + // + Console.Write(" downloading ..."); + using var response = await CLIENT.GetAsync(pdfiumUrl, HttpCompletionOption.ResponseHeadersRead); + if (!response.IsSuccessStatusCode) { - Console.WriteLine($" failed to find the library file '{pdfiumLibSourcePath}'"); + Console.WriteLine($" failed to download Pdfium {version} for {rid.ToUserFriendlyName()} from {pdfiumUrl}"); return; } - - Directory.CreateDirectory(Path.Join(cwd, "resources", "libraries")); - if (File.Exists(pdfiumLibTargetPath)) - File.Delete(pdfiumLibTargetPath); - - File.Copy(pdfiumLibSourcePath, pdfiumLibTargetPath); - + // - // Cleanup: + // Extract the library from the downloaded file: // - Console.Write(" cleaning up ..."); - File.Delete(pdfiumTmpDownloadPath); - Directory.Delete(pdfiumTmpExtractPath.FullName, true); - + Console.Write(" extracting ..."); + Directory.CreateDirectory(pdfiumLibTargetDirectory); + + var foundLibrary = false; + try + { + await using var downloadStream = await response.Content.ReadAsStreamAsync(); + await using var uncompressedStream = new GZipStream(downloadStream, CompressionMode.Decompress); + await using var tarReader = new TarReader(uncompressedStream); + + while (await tarReader.GetNextEntryAsync() is { } entry) + { + if (!string.Equals(entry.Name.Replace('\\', '/'), pdfiumLibArchivePath, StringComparison.Ordinal)) + continue; + + if (entry.DataStream == null) + break; + + await using var fileStream = File.Create(pdfiumLibTmpTargetPath); + await entry.DataStream.CopyToAsync(fileStream); + foundLibrary = true; + break; + } + + if (!foundLibrary) + { + Console.WriteLine($" failed to find the library file '{pdfiumLibArchivePath}' in the Pdfium archive"); + return; + } + + Console.Write(" deploying ..."); + File.Move(pdfiumLibTmpTargetPath, pdfiumLibTargetPath, true); + } + finally + { + if (File.Exists(pdfiumLibTmpTargetPath)) + File.Delete(pdfiumLibTmpTargetPath); + } + Console.WriteLine(" done."); } diff --git a/app/Build/Commands/UpdateMetadataCommands.cs b/app/Build/Commands/UpdateMetadataCommands.cs index 303edcd5..dad05f93 100644 --- a/app/Build/Commands/UpdateMetadataCommands.cs +++ b/app/Build/Commands/UpdateMetadataCommands.cs @@ -15,7 +15,8 @@ public sealed partial class UpdateMetadataCommands [Command("release", Description = "Prepare & build the next release")] public async Task Release( [Option("action", ['a'], Description = "The release action: patch, minor, or major")] PrepareAction action = PrepareAction.NONE, - [Option("version", ['v'], Description = "Set a specific version directly, e.g., 26.1.2")] string? version = null) + [Option("version", ['v'], Description = "Set a specific version directly, e.g., 26.1.2")] string? version = null, + [Option("offline", Description = "Skip downloads and use locally available build dependencies")] bool offline = false) { if(!Environment.IsWorkingDirectoryValid()) return; @@ -42,7 +43,7 @@ public sealed partial class UpdateMetadataCommands // Build once to allow the Rust compiler to read the changed metadata // and to update all .NET artifacts: - await this.Build(); + await this.Build(offline); // Now, we update the web assets (which may were updated by the first build): new UpdateWebAssetsCommand().UpdateWebAssets(); @@ -53,7 +54,7 @@ public sealed partial class UpdateMetadataCommands // Build the final release, where Rust knows the updated metadata, the .NET // artifacts are already in place, and .NET knows the updated web assets, etc.: - await this.Build(); + await this.Build(offline); } [Command("update-versions", Description = "The command will update the package versions in the metadata file")] @@ -136,7 +137,8 @@ public sealed partial class UpdateMetadataCommands } [Command("build", Description = "Build MindWork AI Studio")] - public async Task Build() + public async Task Build( + [Option("offline", Description = "Skip downloads and use locally available build dependencies")] bool offline = false) { if(!Environment.IsWorkingDirectoryValid()) return; @@ -153,7 +155,7 @@ public sealed partial class UpdateMetadataCommands await this.UpdateVectorStoreVersion(); var pdfiumVersion = await this.ReadPdfiumVersion(); - await Pdfium.InstallAsync(rid, pdfiumVersion); + await Pdfium.InstallAsync(rid, pdfiumVersion, Environment.IsOfflineBuildRequested(offline)); Console.Write($"- Start .NET build for {rid.ToUserFriendlyName()} ..."); await this.ReadCommandOutput(pathApp, "dotnet", $"clean --configuration release --runtime {rid.AsMicrosoftRid()}"); @@ -750,4 +752,4 @@ public sealed partial class UpdateMetadataCommands [GeneratedRegex("""(?[0-9]+)\.(?[0-9]+)\.(?[0-9]+)""")] private static partial Regex AppVersionRegex(); -} \ No newline at end of file +} diff --git a/app/Build/Tools/Environment.cs b/app/Build/Tools/Environment.cs index f03ff354..39c383f1 100644 --- a/app/Build/Tools/Environment.cs +++ b/app/Build/Tools/Environment.cs @@ -7,6 +7,7 @@ namespace Build.Tools; public static class Environment { public const string DOTNET_VERSION = "net9.0"; + public const string BUILD_OFFLINE_ENVIRONMENT_VARIABLE = "AI_STUDIO_BUILD_OFFLINE"; public static readonly Encoding UTF8_NO_BOM = new UTF8Encoding(false); private static readonly Dictionary ALL_RIDS = Enum.GetValues().Select(rid => new KeyValuePair(rid, rid.AsMicrosoftRid())).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); @@ -47,6 +48,19 @@ public static class Environment return Path.GetFullPath(directory); } + public static bool IsOfflineBuildRequested(bool offlineOption) + { + if (offlineOption) + return true; + + var environmentValue = global::System.Environment.GetEnvironmentVariable(BUILD_OFFLINE_ENVIRONMENT_VARIABLE); + return environmentValue?.Trim().ToLowerInvariant() switch + { + "1" or "true" or "yes" or "on" => true, + _ => false, + }; + } + public static string? GetOS() { if(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) diff --git a/app/MindWork AI Studio/Assistants/AssistantBase.razor b/app/MindWork AI Studio/Assistants/AssistantBase.razor index 59c9f7a2..796de962 100644 --- a/app/MindWork AI Studio/Assistants/AssistantBase.razor +++ b/app/MindWork AI Studio/Assistants/AssistantBase.razor @@ -153,7 +153,7 @@ } - @if (this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence) + @if (this.SettingsManager.ConfigurationData.Confidence.ShowProviderConfidence) { } diff --git a/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs b/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs index d9cf2afe..79f650bb 100644 --- a/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs +++ b/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs @@ -174,7 +174,7 @@ public abstract partial class AssistantBase : AssistantLowerBase wher private string TB(string fallbackEN) => this.T(fallbackEN, typeof(AssistantBase).Namespace, nameof(AssistantBase)); - 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 VisibleSendToAssistants => Enum.GetValues() .Where(this.CanSendToAssistant) diff --git a/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor.cs b/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor.cs index e7b4bf38..987c9f5c 100644 --- a/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor.cs +++ b/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor.cs @@ -439,10 +439,10 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore minimumLevel) minimumLevel = this.selectedPolicy.MinimumProviderConfidence; diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 6f910ba5..680efd06 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -2794,6 +2794,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" @@ -2850,6 +2898,8 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T34481 -- Couldn't delete the embedding provider '{0}'. The issue: {1}. We can ignore this issue and delete the embedding provider anyway. Do you want to ignore it and delete this embedding provider? UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3703173892"] = "Couldn't delete the embedding provider '{0}'. The issue: {1}. We can ignore this issue and delete the embedding provider anyway. Do you want to ignore it and delete this embedding provider?" +-- 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" @@ -2893,21 +2943,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" @@ -2923,27 +2964,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" @@ -2965,36 +2991,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" @@ -3043,6 +3048,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." @@ -3520,6 +3528,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" @@ -4315,6 +4326,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." @@ -4849,6 +4863,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." @@ -4888,6 +4905,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" @@ -5311,6 +5331,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" @@ -5335,6 +5358,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" @@ -5866,12 +5892,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." @@ -5883,6 +5918,11 @@ UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1847791252"] = "Update" -- Data sync UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1903948824"] = "Data sync" +-- 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" @@ -5890,6 +5930,9 @@ 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." @@ -5899,6 +5942,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" @@ -5925,6 +5971,8 @@ UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T714077986"] = "Embeddings are ru -- Embeddings UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T951463987"] = "Embeddings" +-- 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." @@ -6133,6 +6181,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" @@ -6859,6 +6910,12 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::OPENAI::PROVIDEROPENAI::T757371511"] = "It -- Model as configured by whisper.cpp UI_TEXT_CONTENT["AISTUDIO::PROVIDER::SELFHOSTED::PROVIDERSELFHOSTED::T3313940770"] = "Model as configured by whisper.cpp" +-- The llama.cpp provider '{0}' does not offer a usable text model. Please check your provider settings. +UI_TEXT_CONTENT["AISTUDIO::PROVIDER::SELFHOSTED::PROVIDERSELFHOSTED::T3839908321"] = "The llama.cpp provider '{0}' does not offer a usable text model. Please check your provider settings." + +-- The llama.cpp provider '{0}' offers multiple models. Please open the provider settings and select the model to use. +UI_TEXT_CONTENT["AISTUDIO::PROVIDER::SELFHOSTED::PROVIDERSELFHOSTED::T4018006464"] = "The llama.cpp provider '{0}' offers multiple models. Please open the provider settings and select the model to use." + -- Cannot export this chat template because example message {0} is not a text message. UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T1861800849"] = "Cannot export this chat template because example message {0} is not a text message." @@ -7420,6 +7477,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T3928871850"] = "Th -- The configured certificate bundle does not contain usable root CA certificates. UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T599774443"] = "The configured certificate bundle does not contain usable root CA certificates." +-- policy files +UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T632340680"] = "policy files" + -- AI Studio couldn't install Pandoc because the archive was not found. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1059477764"] = "AI Studio couldn't install Pandoc because the archive was not found." @@ -7972,6 +8032,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." diff --git a/app/MindWork AI Studio/Chat/ChatThread.cs b/app/MindWork AI Studio/Chat/ChatThread.cs index e8277cb5..2c9bb720 100644 --- a/app/MindWork AI Studio/Chat/ChatThread.cs +++ b/app/MindWork AI Studio/Chat/ChatThread.cs @@ -94,6 +94,8 @@ public sealed record ChatThread /// The prepared system prompt. public string PrepareSystemPrompt(SettingsManager settingsManager) { + this.allowProfile = true; + // // Use the information from the chat template, if provided. Otherwise, use the default system prompt // @@ -111,8 +113,8 @@ public sealed record ChatThread systemPromptTextWithChatTemplate = this.SystemPrompt; else { - var chatTemplate = settingsManager.ConfigurationData.ChatTemplates.FirstOrDefault(x => x.Id == this.SelectedChatTemplate); - if(chatTemplate == null) + var chatTemplate = settingsManager.GetChatTemplateById(this.SelectedChatTemplate); + if(chatTemplate == ChatTemplate.NO_CHAT_TEMPLATE) systemPromptTextWithChatTemplate = this.SystemPrompt; else { @@ -168,8 +170,8 @@ public sealed record ChatThread systemPromptText = systemPromptWithAugmentedData; else { - var profile = settingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == this.SelectedProfile); - if(profile is null) + var profile = settingsManager.GetProfileById(this.SelectedProfile); + if(profile == Profile.NO_PROFILE) systemPromptText = systemPromptWithAugmentedData; else { diff --git a/app/MindWork AI Studio/Chat/ChatThreadExtensions.cs b/app/MindWork AI Studio/Chat/ChatThreadExtensions.cs index 6b1b6500..2eb5395b 100644 --- a/app/MindWork AI Studio/Chat/ChatThreadExtensions.cs +++ b/app/MindWork AI Studio/Chat/ChatThreadExtensions.cs @@ -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(); + 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, }; } diff --git a/app/MindWork AI Studio/Components/AttachDocuments.razor b/app/MindWork AI Studio/Components/AttachDocuments.razor index bc66f9c2..e96825c3 100644 --- a/app/MindWork AI Studio/Components/AttachDocuments.razor +++ b/app/MindWork AI Studio/Components/AttachDocuments.razor @@ -52,29 +52,42 @@ } else { - - - @T("Drag and drop files into the marked area or click here to attach documents: ") - - - @T("Add file") - - + @if (!this.Disabled) + { + + + @T("Drag and drop files into the marked area or click here to attach documents: ") + + + @T("Add file") + + + }
@foreach (var fileAttachment in this.DocumentPaths) { - + @if (this.Disabled) + { + + } + else + { + + } }
- - @T("Clear file list") - + @if (!this.Disabled) + { + + @T("Clear file list") + + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/AttachDocuments.razor.cs b/app/MindWork AI Studio/Components/AttachDocuments.razor.cs index 65a901ef..9aab164f 100644 --- a/app/MindWork AI Studio/Components/AttachDocuments.razor.cs +++ b/app/MindWork AI Studio/Components/AttachDocuments.razor.cs @@ -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; - + /// /// On which layer to register the drop area. Higher layers have priority over lower layers. /// [Parameter] public int Layer { get; set; } - + /// /// When true, pause catching dropped files. Default is false. /// @@ -38,19 +38,23 @@ public partial class AttachDocuments : MSGComponentBase [Parameter] public Func, Task> OnChange { get; set; } = _ => Task.CompletedTask; - + /// - /// 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. /// - [Parameter] + [Parameter] public bool CatchAllDocuments { get; set; } - + [Parameter] public bool UseSmallForm { get; set; } [Parameter] public FileType[]? AllowedFileTypes { get; set; } + + [Parameter] + public bool Disabled { get; set; } + /// /// 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 @@ -59,16 +63,16 @@ public partial class AttachDocuments : MSGComponentBase /// [Parameter] public bool ValidateMediaFileTypes { get; set; } = true; - + [Parameter] public AIStudio.Settings.Provider? Provider { get; set; } - + [Inject] private ILogger Logger { get; set; } = null!; - + [Inject] private RustService RustService { get; init; } = null!; - + [Inject] private IDialogService DialogService { get; init; } = null!; @@ -77,17 +81,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(); @@ -95,6 +99,9 @@ public partial class AttachDocuments : MSGComponentBase protected override async Task ProcessIncomingMessage(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: @@ -114,7 +121,7 @@ public partial class AttachDocuments : MSGComponentBase { if(this.numDropAreasAboveThis > 0) this.numDropAreasAboveThis--; - + if(this.numDropAreasAboveThis is 0) this.PauseCatchingDrops = false; } @@ -125,40 +132,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); @@ -200,11 +207,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, @@ -231,43 +241,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(); @@ -276,6 +292,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); diff --git a/app/MindWork AI Studio/Components/Changelog.Logs.cs b/app/MindWork AI Studio/Components/Changelog.Logs.cs index 3d9cd1a0..6afb26fd 100644 --- a/app/MindWork AI Studio/Components/Changelog.Logs.cs +++ b/app/MindWork AI Studio/Components/Changelog.Logs.cs @@ -13,6 +13,8 @@ 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"), new (238, "v26.5.3, build 238 (2026-05-13 09:50 UTC)", "v26.5.3.md"), diff --git a/app/MindWork AI Studio/Components/ChatComponent.razor b/app/MindWork AI Studio/Components/ChatComponent.razor index 30261d8d..b4750b09 100644 --- a/app/MindWork AI Studio/Components/ChatComponent.razor +++ b/app/MindWork AI Studio/Components/ChatComponent.razor @@ -132,7 +132,7 @@ } - @if (this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence) + @if (this.SettingsManager.ConfigurationData.Confidence.ShowProviderConfidence) { } diff --git a/app/MindWork AI Studio/Components/ChatComponent.razor.cs b/app/MindWork AI Studio/Components/ChatComponent.razor.cs index 1c6a4ef2..5d5b9b70 100644 --- a/app/MindWork AI Studio/Components/ChatComponent.razor.cs +++ b/app/MindWork AI Studio/Components/ChatComponent.razor.cs @@ -71,6 +71,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; @@ -107,7 +110,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable protected override async Task OnInitializedAsync() { // Apply the filters for the message bus: - this.ApplyFilters([], [ Event.HAS_CHAT_UNSAVED_CHANGES, Event.RESET_CHAT_STATE, Event.CHAT_STREAMING_DONE, Event.AI_JOB_CHANGED, Event.AI_JOB_FINISHED, Event.CHAT_GENERATION_CHANGED, Event.WORKSPACE_RENAMED ]); + this.ApplyFilters([], [ Event.HAS_CHAT_UNSAVED_CHANGES, Event.RESET_CHAT_STATE, Event.CHAT_STREAMING_DONE, Event.AI_JOB_CHANGED, Event.AI_JOB_FINISHED, Event.CHAT_GENERATION_CHANGED, Event.WORKSPACE_RENAMED, Event.CONFIGURATION_CHANGED ]); // Configure the spellchecking for the user input: this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES); @@ -293,12 +296,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(); @@ -441,9 +457,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() { @@ -476,7 +492,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable private async Task ProfileWasChanged(Profile profile) { - this.currentProfile = profile; + this.currentProfile = this.SettingsManager.GetProfileById(profile.Id); if(this.ChatThread is null) return; @@ -490,7 +506,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable private async Task ChatTemplateWasChanged(ChatTemplate chatTemplate) { - this.currentChatTemplate = chatTemplate; + this.currentChatTemplate = this.SettingsManager.GetChatTemplateById(chatTemplate.Id); if(!string.IsNullOrWhiteSpace(this.currentChatTemplate.PredefinedUserPrompt)) this.ComposerState.SetSystemInput(this.currentChatTemplate.PredefinedUserPrompt); @@ -503,6 +519,42 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable await this.StartNewChat(true); } + private void RefreshCurrentProfileAndChatTemplate() + { + this.currentProfile = this.SettingsManager.GetProfileById(this.currentProfile.Id); + 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 GetAgentSelectedDataSources() { if (this.ChatThread is null) @@ -610,7 +662,9 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable if(!this.ChatThread.IsLLMProviderAllowed(this.Provider)) return; - + + this.RefreshCurrentProfileAndChatTemplate(); + // Blur the focus away from the input field to be able to clear it: await this.inputField.BlurAsync(); @@ -805,6 +859,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable // this.hasUnsavedChanges = false; this.ComposerState.Clear(); + this.RefreshCurrentProfileAndChatTemplate(); // // Reset the LLM provider considering the user's settings: @@ -977,14 +1032,11 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable // Try to select the profile: if (!string.IsNullOrWhiteSpace(chatProfile)) - this.currentProfile = this.SettingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == chatProfile) ?? Profile.NO_PROFILE; + this.currentProfile = this.SettingsManager.GetProfileById(chatProfile); // Try to select the chat template: if (!string.IsNullOrWhiteSpace(chatChatTemplate)) - { - var selectedTemplate = this.SettingsManager.ConfigurationData.ChatTemplates.FirstOrDefault(x => x.Id == chatChatTemplate); - this.currentChatTemplate = selectedTemplate ?? ChatTemplate.NO_CHAT_TEMPLATE; - } + this.currentChatTemplate = this.SettingsManager.GetChatTemplateById(chatChatTemplate); } private async Task ToggleWorkspaceOverlay() @@ -1113,6 +1165,12 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable if (data is Guid workspaceId) await this.RefreshRenamedWorkspaceHeaderAsync(workspaceId); break; + + case Event.CONFIGURATION_CHANGED: + case Event.PLUGINS_RELOADED: + await this.RefreshChatSelectionsAfterConfigurationChange(); + this.StateHasChanged(); + break; case Event.AI_JOB_CHANGED: case Event.AI_JOB_FINISHED: @@ -1121,7 +1179,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(); } diff --git a/app/MindWork AI Studio/Components/ChatTemplateSelection.razor b/app/MindWork AI Studio/Components/ChatTemplateSelection.razor index edfb9b41..6fea9a4d 100644 --- a/app/MindWork AI Studio/Components/ChatTemplateSelection.razor +++ b/app/MindWork AI Studio/Components/ChatTemplateSelection.razor @@ -6,7 +6,7 @@ @if (this.CurrentChatTemplate != ChatTemplate.NO_CHAT_TEMPLATE) { - + @this.CurrentChatTemplate.GetSafeName() } @@ -22,7 +22,7 @@ @foreach (var chatTemplate in this.SettingsManager.ConfigurationData.ChatTemplates.GetAllChatTemplates()) { - + @chatTemplate.GetSafeName() } diff --git a/app/MindWork AI Studio/Components/ChatTemplateSelection.razor.cs b/app/MindWork AI Studio/Components/ChatTemplateSelection.razor.cs index 25b72e11..25bbdd1c 100644 --- a/app/MindWork AI Studio/Components/ChatTemplateSelection.razor.cs +++ b/app/MindWork AI Studio/Components/ChatTemplateSelection.razor.cs @@ -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 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(T("Open Chat Template Options"), dialogParameters, DialogOptions.FULLSCREEN); } - + private async Task CreateNewChatTemplateFromChat() { var dialogParameters = new DialogParameters @@ -53,4 +71,16 @@ public partial class ChatTemplateSelection : MSGComponentBase }; await this.DialogService.ShowAsync(T("Open Chat Template Options"), dialogParameters, DialogOptions.FULLSCREEN); } + + #region Overrides of MSGComponentBase + + protected override Task ProcessIncomingMessage(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 } \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/ConfigurationMinConfidenceSelection.razor.cs b/app/MindWork AI Studio/Components/ConfigurationMinConfidenceSelection.razor.cs index c980d457..2319269a 100644 --- a/app/MindWork AI Studio/Components/ConfigurationMinConfidenceSelection.razor.cs +++ b/app/MindWork AI Studio/Components/ConfigurationMinConfidenceSelection.razor.cs @@ -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; } diff --git a/app/MindWork AI Studio/Components/ProfileSelection.razor.cs b/app/MindWork AI Studio/Components/ProfileSelection.razor.cs index 70747707..6c92aecc 100644 --- a/app/MindWork AI Studio/Components/ProfileSelection.razor.cs +++ b/app/MindWork AI Studio/Components/ProfileSelection.razor.cs @@ -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(T("Open Profile Options"), dialogParameters, DialogOptions.FULLSCREEN); } + + #region Overrides of MSGComponentBase + + protected override Task ProcessIncomingMessage(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 } \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/ProviderSelection.razor.cs b/app/MindWork AI Studio/Components/ProviderSelection.razor.cs index 809ed089..74bd75c9 100644 --- a/app/MindWork AI Studio/Components/ProviderSelection.razor.cs +++ b/app/MindWork AI Studio/Components/ProviderSelection.razor.cs @@ -25,6 +25,16 @@ public partial class ProviderSelection : MSGComponentBase [Inject] private ILogger 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(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 } \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelConfidence.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelConfidence.razor new file mode 100644 index 00000000..915b9b6d --- /dev/null +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelConfidence.razor @@ -0,0 +1,60 @@ +@using AIStudio.Provider +@using AIStudio.Settings +@inherits SettingsPanelBase + + + + @T("Provider Confidence") + + + @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.") + + + + @if(this.SettingsManager.ConfigurationData.Confidence.EnforceGlobalMinimumConfidence) + { + + } + + + @if (this.SettingsManager.ConfigurationData.Confidence.ShowProviderConfidence) + { + + @if (this.SettingsManager.ConfigurationData.Confidence.ConfidenceScheme is ConfidenceSchemes.CUSTOM) + { + + + + + + + + @T("Provider") + @T("Description") + @T("Confidence Level") + + + + @context.ToName() + + + + + + + @foreach (var confidenceLevel in Enum.GetValues().OrderBy(n => n)) + { + if(confidenceLevel is ConfidenceLevel.NONE or ConfidenceLevel.UNKNOWN) + continue; + + + @confidenceLevel.GetName() + + } + + + + + } + } + diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelConfidence.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelConfidence.razor.cs new file mode 100644 index 00000000..29a54335 --- /dev/null +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelConfidence.razor.cs @@ -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(this, Event.CONFIGURATION_CHANGED); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor index 9d14a99a..dc713dda 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor @@ -1,4 +1,5 @@ @using AIStudio.Provider +@using AIStudio.Settings @using AIStudio.Settings.DataModel @inherits SettingsPanelProviderBase @@ -39,6 +40,12 @@ + @if (context.IsTrustedByConfiguration(this.SettingsManager)) + { + + + + } @if (context.IsEnterpriseConfiguration) { diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor index 8a862702..4f954b5f 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor @@ -31,6 +31,12 @@ @this.GetLLMProviderModelName(context) + @if (context.IsTrustedByConfiguration(this.SettingsManager)) + { + + + + } @if (context.IsEnterpriseConfiguration) { @@ -68,59 +74,4 @@ } - - - @T("LLM Provider Confidence") - - - @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.") - - - - @if(this.SettingsManager.ConfigurationData.LLMProviders.EnforceGlobalMinimumConfidence) - { - - } - - - @if (this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence) - { - - @if (this.SettingsManager.ConfigurationData.LLMProviders.ConfidenceScheme is ConfidenceSchemes.CUSTOM) - { - - - - - - - - @T("LLM Provider") - @T("Description") - @T("Confidence Level") - - - - @context.ToName() - - - - - - - @foreach (var confidenceLevel in Enum.GetValues().OrderBy(n => n)) - { - if(confidenceLevel is ConfidenceLevel.NONE or ConfidenceLevel.UNKNOWN) - continue; - - - @confidenceLevel.GetName() - - } - - - - - } - } diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor.cs index e00b5211..9a33d599 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor.cs +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor.cs @@ -1,7 +1,6 @@ using System.Diagnostics.CodeAnalysis; using AIStudio.Dialogs; -using AIStudio.Provider; using AIStudio.Settings; using AIStudio.Tools.Rust; using AIStudio.Tools.Services; @@ -182,25 +181,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(); - } } diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor index d99a2e14..fbbd009e 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor @@ -1,4 +1,5 @@ @using AIStudio.Provider +@using AIStudio.Settings @using AIStudio.Settings.DataModel @inherits SettingsPanelProviderBase @@ -35,6 +36,12 @@ + @if (context.IsTrustedByConfiguration(this.SettingsManager)) + { + + + + } @if (context.IsEnterpriseConfiguration) { diff --git a/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor b/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor index d13a44bb..8080114e 100644 --- a/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor +++ b/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor @@ -10,7 +10,7 @@ @T("The name of the chat template is mandatory. Each chat template must have a unique name.") - + @* ReSharper disable once CSharpWarnings::CS8974 *@ - + @T("System Prompt") @@ -47,16 +48,17 @@ Class="mb-3" UserAttributes="@SPELLCHECK_ATTRIBUTES" HelperText="@T("Tell the AI your system prompt.")" + ReadOnly="@this.IsReadOnly" /> - + @T("Are you unsure which system prompt to use? You might start with the default system prompt that AI Studio uses for all chats.") - + @T("Use the default system prompt") - - + + @T("Predefined User Input") @@ -77,6 +79,7 @@ Class="mb-3" UserAttributes="@SPELLCHECK_ATTRIBUTES" HelperText="@T("Tell the AI your predefined user input.")" + ReadOnly="@this.IsReadOnly" /> @@ -92,6 +95,7 @@ UseSmallForm="false" CatchAllDocuments="true" ValidateMediaFileTypes="false" + Disabled="@this.IsReadOnly" /> @@ -100,8 +104,8 @@ @T("Using some chat templates in tandem with profiles might cause issues. Therefore, you might prohibit the usage of profiles here.") - - + + @T("Example Conversation") @@ -129,18 +133,18 @@ case ContentText textContent: break; - + case ContentImage { SourceType: ContentImageSource.URL or ContentImageSource.LOCAL_PATH } imageContent: break; - + default: @T("Unsupported content type") break; } - @if (!this.isInlineEditOnGoing) + @if (!this.isInlineEditOnGoing && !this.IsReadOnly) { @@ -153,22 +157,29 @@ - - @foreach (var role in ChatRoles.ChatTemplateRoles()) - { - - @role.ToChatTemplateName() - - } - + @if (this.IsReadOnly) + { + @context.Role.ToChatTemplateName() + } + else + { + + @foreach (var role in ChatRoles.ChatTemplateRoles()) + { + + @role.ToChatTemplateName() + + } + + } @switch(context.Content) { case ContentText textContent: - + break; - + default: @T("Only text content is supported in the editing mode yet.") @@ -182,8 +193,8 @@ - - @if (!this.isInlineEditOnGoing) + + @if (!this.isInlineEditOnGoing && !this.IsReadOnly) { @T("Add a message") @@ -193,22 +204,31 @@ - - @T("Cancel") - - - @if (!this.isInlineEditOnGoing) + @if (this.IsReadOnly) { - - @if (this.IsEditing) - { - @T("Update") - } - else - { - @T("Add") - } + + @T("Close") } + else + { + + @T("Cancel") + + + @if (!this.isInlineEditOnGoing) + { + + @if (this.IsEditing) + { + @T("Update") + } + else + { + @T("Add") + } + + } + } \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor.cs b/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor.cs index cbc438cf..24d0b0e7 100644 --- a/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor.cs @@ -16,37 +16,40 @@ public partial class ChatTemplateDialog : MSGComponentBase /// [Parameter] public uint DataNum { get; set; } - + /// /// The chat template's ID. /// [Parameter] public string DataId { get; set; } = Guid.NewGuid().ToString(); - + /// /// The chat template name chosen by the user. /// [Parameter] public string DataName { get; set; } = string.Empty; - + /// /// What is the system prompt? /// [Parameter] public string DataSystemPrompt { get; set; } = string.Empty; - + /// /// What is the predefined user prompt? /// [Parameter] public string PredefinedUserPrompt { get; set; } = string.Empty; - + /// /// Should the dialog be in editing mode? /// [Parameter] public bool IsEditing { get; init; } - + + [Parameter] + public bool IsReadOnly { get; init; } + [Parameter] public IReadOnlyCollection 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 Logger { get; init; } = null!; - + private static readonly Dictionary SPELLCHECK_ATTRIBUTES = new(); - + /// /// The list of used chat template names. We need this to check for uniqueness. /// private List UsedNames { get; set; } = []; - + private bool dataIsValid; private List dataExampleConversation = []; private HashSet 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; } diff --git a/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor.cs b/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor.cs index 0137f068..42463e38 100644 --- a/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor.cs @@ -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() { diff --git a/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryInfoDialog.razor.cs b/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryInfoDialog.razor.cs index b56bf06a..08ec4408 100644 --- a/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryInfoDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryInfoDialog.razor.cs @@ -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; diff --git a/app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor.cs b/app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor.cs index 324b0d71..13b8df1e 100644 --- a/app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor.cs @@ -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() { diff --git a/app/MindWork AI Studio/Dialogs/DataSourceLocalFileInfoDialog.razor.cs b/app/MindWork AI Studio/Dialogs/DataSourceLocalFileInfoDialog.razor.cs index 68f31aff..5926a907 100644 --- a/app/MindWork AI Studio/Dialogs/DataSourceLocalFileInfoDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/DataSourceLocalFileInfoDialog.razor.cs @@ -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; diff --git a/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs index 6a500323..cb44c9b0 100644 --- a/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs @@ -216,7 +216,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; diff --git a/app/MindWork AI Studio/Dialogs/ProfileDialog.razor b/app/MindWork AI Studio/Dialogs/ProfileDialog.razor index b9e4e1e3..a711e084 100644 --- a/app/MindWork AI Studio/Dialogs/ProfileDialog.razor +++ b/app/MindWork AI Studio/Dialogs/ProfileDialog.razor @@ -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" /> - + - + @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 @@ - - @T("Cancel") - - - @if(this.IsEditing) - { - @T("Update") - } - else - { - @T("Add") - } - + @if (this.IsReadOnly) + { + + @T("Close") + + } + else + { + + @T("Cancel") + + + @if(this.IsEditing) + { + @T("Update") + } + else + { + @T("Add") + } + + } \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/ProfileDialog.razor.cs b/app/MindWork AI Studio/Dialogs/ProfileDialog.razor.cs index 54fbb2b8..ba2dfff8 100644 --- a/app/MindWork AI Studio/Dialogs/ProfileDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/ProfileDialog.razor.cs @@ -15,19 +15,19 @@ public partial class ProfileDialog : MSGComponentBase /// [Parameter] public uint DataNum { get; set; } - + /// /// The profile's ID. /// [Parameter] public string DataId { get; set; } = Guid.NewGuid().ToString(); - + /// /// The profile name chosen by the user. /// [Parameter] public string DataName { get; set; } = string.Empty; - + /// /// What should the LLM know about you? /// @@ -39,27 +39,30 @@ public partial class ProfileDialog : MSGComponentBase /// [Parameter] public string DataActions { get; set; } = string.Empty; - + /// /// Should the dialog be in editing mode? /// [Parameter] public bool IsEditing { get; init; } - + + [Parameter] + public bool IsReadOnly { get; init; } + [Inject] private ILogger Logger { get; init; } = null!; - + private static readonly Dictionary SPELLCHECK_ATTRIBUTES = new(); - + /// /// The list of used profile names. We need this to check for uniqueness. /// private List 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; } diff --git a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor index 0e61ce5b..d7d5e588 100644 --- a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor +++ b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor @@ -72,7 +72,7 @@ @* ReSharper restore Asp.Entity *@ } - @if (!this.DataLLMProvider.IsLLMModelSelectionHidden(this.DataHost)) + @if (!this.IsLLMModelSelectionHidden) { diff --git a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs index 993aabcf..da486fe7 100644 --- a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs @@ -114,6 +114,7 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId private Task dataTokenizerValidationTask = Task.CompletedTask; private bool dataStoreWasAttempted; private int dataTokenizerValidationRevision; + private bool usesLegacySystemModelFallback; private bool showExpertSettings; // We get the form reference from Blazor code to validate it manually: @@ -134,6 +135,7 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId GetHost = () => this.DataHost, IsModelProvidedManually = () => this.DataLLMProvider.IsLLMModelProvidedManually(), GetCustomTokenizerValidationIssue = () => this.dataCustomTokenizerValidationIssue, + IsModelSelectionHidden = () => this.IsLLMModelSelectionHidden, }; } @@ -143,9 +145,9 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId // Determine the model based on the provider and host configuration: Model model; - if (this.DataLLMProvider.IsLLMModelSelectionHidden(this.DataHost)) + if (this.IsLLMModelSelectionHidden) { - // Use system model placeholder for hosts that don't support model selection (e.g., llama.cpp): + // Use system model placeholder for legacy hosts that don't support model selection: model = Model.SYSTEM_MODEL; } else if (this.DataLLMProvider is LLMProviders.FIREWORKS or LLMProviders.HUGGINGFACE) @@ -242,7 +244,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; @@ -385,6 +387,7 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId this.dataManuallyModel = string.Empty; this.availableModels.Clear(); this.dataLoadingModelsIssue = string.Empty; + this.usesLegacySystemModelFallback = false; } private async Task ReloadModels() @@ -406,6 +409,7 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId this.availableModels.Clear(); this.availableModels.AddRange(orderedModels); + this.UpdateModelSelectionAfterLoading(); } catch (Exception e) { @@ -419,6 +423,34 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId LLMProviders.SELF_HOSTED => T("(Optional) API Key"), _ => T("API Key"), }; + + private bool IsLLMModelSelectionHidden => this.DataLLMProvider.IsLLMModelSelectionHidden(this.DataHost) || + this.DataLLMProvider is LLMProviders.SELF_HOSTED && + this.DataHost is Host.LLAMA_CPP && + this.usesLegacySystemModelFallback; + + private void UpdateModelSelectionAfterLoading() + { + if (this.DataLLMProvider is not LLMProviders.SELF_HOSTED || this.DataHost is not Host.LLAMA_CPP) + return; + + this.usesLegacySystemModelFallback = this.availableModels.Count is 1 && this.availableModels[0].IsSystemModel; + if (this.usesLegacySystemModelFallback) + { + this.DataModel = Model.SYSTEM_MODEL; + return; + } + + var availableModel = this.availableModels.FirstOrDefault(model => + string.Equals(model.Id, this.DataModel.Id, StringComparison.OrdinalIgnoreCase)); + if (availableModel != default) + { + this.DataModel = availableModel; + return; + } + + this.DataModel = this.availableModels.Count is 1 ? this.availableModels[0] : default; + } private void ToggleExpertSettings() => this.showExpertSettings = !this.showExpertSettings; diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogBase.cs b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogBase.cs index 3fc5f45e..0b235fd2 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogBase.cs +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogBase.cs @@ -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; } diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChat.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChat.razor index 1dd6b9d7..348f7a53 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChat.razor +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChat.razor @@ -16,10 +16,10 @@ - - - - + + + + @if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager)) diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChatTemplate.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChatTemplate.razor index 2f8600a8..19680575 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChatTemplate.razor +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChatTemplate.razor @@ -33,9 +33,14 @@ @if (context.IsEnterpriseConfiguration) { - - - + + + + + + + + } else { diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChatTemplate.razor.cs b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChatTemplate.razor.cs index 89473518..54a2f631 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChatTemplate.razor.cs +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChatTemplate.razor.cs @@ -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 - + /// 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 @@ -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(this, Event.CONFIGURATION_CHANGED); } - + private async Task EditChatTemplate(ChatTemplate chatTemplate) { if (chatTemplate == ChatTemplate.NO_CHAT_TEMPLATE || chatTemplate.IsEnterpriseConfiguration) return; - + var dialogParameters = new DialogParameters { { 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(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(this, Event.CONFIGURATION_CHANGED); } + private async Task ViewChatTemplate(ChatTemplate chatTemplate) + { + var dialogParameters = new DialogParameters + { + { 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(T("View Chat Template"), dialogParameters, DialogOptions.FULLSCREEN); + } + private async Task DeleteChatTemplate(ChatTemplate chatTemplate) { var dialogParameters = new DialogParameters { { 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(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(this, Event.CONFIGURATION_CHANGED); } diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogProfiles.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogProfiles.razor index 784bfffc..1af4253c 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogProfiles.razor +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogProfiles.razor @@ -32,9 +32,14 @@ @if (context.IsEnterpriseConfiguration) { - - - + + + + + + + + } else { diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogProfiles.razor.cs b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogProfiles.razor.cs index 4fb6c67a..d5387dc0 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogProfiles.razor.cs +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogProfiles.razor.cs @@ -10,21 +10,21 @@ public partial class SettingsDialogProfiles : SettingsDialogBase { { x => x.IsEditing, false }, }; - + var dialogReference = await this.DialogService.ShowAsync(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(this, Event.CONFIGURATION_CHANGED); } - + private async Task EditProfile(Profile profile) { var dialogParameters = new DialogParameters @@ -36,19 +36,35 @@ public partial class SettingsDialogProfiles : SettingsDialogBase { x => x.DataActions, profile.Actions }, { x => x.IsEditing, true }, }; - + var dialogReference = await this.DialogService.ShowAsync(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(this, Event.CONFIGURATION_CHANGED); } + private async Task ViewProfile(Profile profile) + { + var dialogParameters = new DialogParameters + { + { 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(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(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(this, Event.CONFIGURATION_CHANGED); } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor.cs b/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor.cs index faa3d3be..bfcc68c2 100644 --- a/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor.cs @@ -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; diff --git a/app/MindWork AI Studio/Layout/MainLayout.razor.cs b/app/MindWork AI Studio/Layout/MainLayout.razor.cs index adadce30..c8c686db 100644 --- a/app/MindWork AI Studio/Layout/MainLayout.razor.cs +++ b/app/MindWork AI Studio/Layout/MainLayout.razor.cs @@ -62,6 +62,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 DataSourceEmbeddingOverview embeddingOverview = new(false, DataSourceEmbeddingState.COMPLETED, 0, 0, 0); @@ -136,6 +137,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(this, Event.USER_SEARCH_FOR_UPDATE); + }; + }); + } + #region Implementation of ILang /// @@ -286,6 +320,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(); this.LoadEmbeddingItem(); diff --git a/app/MindWork AI Studio/MindWork AI Studio.csproj b/app/MindWork AI Studio/MindWork AI Studio.csproj index a2247811..c5031664 100644 --- a/app/MindWork AI Studio/MindWork AI Studio.csproj +++ b/app/MindWork AI Studio/MindWork AI Studio.csproj @@ -50,7 +50,7 @@ - + diff --git a/app/MindWork AI Studio/Pages/Home.razor b/app/MindWork AI Studio/Pages/Home.razor index abf7ffb7..d6c4158a 100644 --- a/app/MindWork AI Studio/Pages/Home.razor +++ b/app/MindWork AI Studio/Pages/Home.razor @@ -8,39 +8,52 @@ - + - - - @T("Welcome to MindWork AI Studio!") - - - @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.") - - - @T("Here's what makes MindWork AI Studio stand out:") - - - - @T("We hope you enjoy using MindWork AI Studio to bring your AI projects to life!") - - + @if (this.SettingsManager.ConfigurationData.App.ShowIntroduction) + { + + + @T("Welcome to MindWork AI Studio!") + + + @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.") + + + @T("Here's what makes MindWork AI Studio stand out:") + + + + @T("We hope you enjoy using MindWork AI Studio to bring your AI projects to life!") + + + } - + @foreach (var introduction in this.introductions) + { + + + @T("Version"): @introduction.VersionText + + + + } + + - + @if (this.SettingsManager.ConfigurationData.App.ShowQuickStartGuide) { - + } - \ No newline at end of file + diff --git a/app/MindWork AI Studio/Pages/Home.razor.cs b/app/MindWork AI Studio/Pages/Home.razor.cs index b44724d0..5fb95872 100644 --- a/app/MindWork AI Studio/Pages/Home.razor.cs +++ b/app/MindWork AI Studio/Pages/Home.razor.cs @@ -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 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); diff --git a/app/MindWork AI Studio/Pages/Settings.razor b/app/MindWork AI Studio/Pages/Settings.razor index af89b157..fa711ee1 100644 --- a/app/MindWork AI Studio/Pages/Settings.razor +++ b/app/MindWork AI Studio/Pages/Settings.razor @@ -8,6 +8,7 @@ + @if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager)) diff --git a/app/MindWork AI Studio/Plugins/configuration/plugin.lua b/app/MindWork AI Studio/Plugins/configuration/plugin.lua index 526d24a2..5dbe1f93 100644 --- a/app/MindWork AI Studio/Plugins/configuration/plugin.lua +++ b/app/MindWork AI Studio/Plugins/configuration/plugin.lua @@ -213,6 +213,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 @@ -241,6 +244,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. @@ -290,6 +319,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"] = {} @@ -342,6 +431,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. diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index e2fa9f4d..e7f081a2 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -2796,6 +2796,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" @@ -2850,6 +2898,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" @@ -2892,21 +2943,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" @@ -2922,27 +2964,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" @@ -2964,36 +2991,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" @@ -3042,6 +3048,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." @@ -3519,6 +3528,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" @@ -4299,6 +4311,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." @@ -4818,6 +4833,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." @@ -4857,6 +4875,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" @@ -5265,6 +5286,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" @@ -5289,6 +5313,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" @@ -5820,12 +5847,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." @@ -5835,12 +5871,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." @@ -5850,6 +5895,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" @@ -5862,6 +5910,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." @@ -6045,6 +6096,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" @@ -6769,6 +6823,12 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::OPENAI::PROVIDEROPENAI::T757371511"] = "Ans -- Model as configured by whisper.cpp UI_TEXT_CONTENT["AISTUDIO::PROVIDER::SELFHOSTED::PROVIDERSELFHOSTED::T3313940770"] = "Modell wie in whisper.cpp konfiguriert" +-- The llama.cpp provider '{0}' does not offer a usable text model. Please check your provider settings. +UI_TEXT_CONTENT["AISTUDIO::PROVIDER::SELFHOSTED::PROVIDERSELFHOSTED::T3839908321"] = "Der llama.cpp-Anbieter „{0}“ bietet kein verwendbares Textmodell an. Bitte überprüfen Sie Ihre Anbieter-Einstellungen." + +-- The llama.cpp provider '{0}' offers multiple models. Please open the provider settings and select the model to use. +UI_TEXT_CONTENT["AISTUDIO::PROVIDER::SELFHOSTED::PROVIDERSELFHOSTED::T4018006464"] = "Der llama.cpp-Anbieter „{0}“ bietet mehrere Modelle an. Bitte öffnen Sie die Anbietereinstellungen und wählen Sie das zu verwendende Modell aus." + -- Cannot export this chat template because example message {0} is not a text message. UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T1861800849"] = "Diese Chatvorlage kann nicht exportiert werden, da die Beispielnachricht {0} keine Textnachricht ist." @@ -7333,6 +7393,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T3928871850"] = "Di -- The configured certificate bundle does not contain usable root CA certificates. UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T599774443"] = "Das konfigurierte Zertifikats-Bundle enthält keine verwendbaren Root-CA-Zertifikate." +-- policy files +UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T632340680"] = "Richtliniendateien" + -- AI Studio couldn't install Pandoc because the archive was not found. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1059477764"] = "AI Studio konnte Pandoc nicht installieren, da das Archiv nicht gefunden wurde." @@ -7870,6 +7933,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." diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index de25aecd..4a314c54 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -2796,6 +2796,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" @@ -2850,6 +2898,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" @@ -2892,21 +2943,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" @@ -2922,27 +2964,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" @@ -2964,36 +2991,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" @@ -3042,6 +3048,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." @@ -3519,6 +3528,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" @@ -4299,6 +4311,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." @@ -4818,6 +4833,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." @@ -4857,6 +4875,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" @@ -5265,6 +5286,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" @@ -5289,6 +5313,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" @@ -5820,12 +5847,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." @@ -5835,12 +5871,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." @@ -5850,6 +5895,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" @@ -5862,6 +5910,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." @@ -6045,6 +6096,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" @@ -6769,6 +6823,12 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::OPENAI::PROVIDEROPENAI::T757371511"] = "It -- Model as configured by whisper.cpp UI_TEXT_CONTENT["AISTUDIO::PROVIDER::SELFHOSTED::PROVIDERSELFHOSTED::T3313940770"] = "Model as configured by whisper.cpp" +-- The llama.cpp provider '{0}' does not offer a usable text model. Please check your provider settings. +UI_TEXT_CONTENT["AISTUDIO::PROVIDER::SELFHOSTED::PROVIDERSELFHOSTED::T3839908321"] = "The llama.cpp provider '{0}' does not offer a usable text model. Please check your provider settings." + +-- The llama.cpp provider '{0}' offers multiple models. Please open the provider settings and select the model to use. +UI_TEXT_CONTENT["AISTUDIO::PROVIDER::SELFHOSTED::PROVIDERSELFHOSTED::T4018006464"] = "The llama.cpp provider '{0}' offers multiple models. Please open the provider settings and select the model to use." + -- Cannot export this chat template because example message {0} is not a text message. UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T1861800849"] = "Cannot export this chat template because example message {0} is not a text message." @@ -7333,6 +7393,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T3928871850"] = "Th -- The configured certificate bundle does not contain usable root CA certificates. UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T599774443"] = "The configured certificate bundle does not contain usable root CA certificates." +-- policy files +UI_TEXT_CONTENT["AISTUDIO::TOOLS::EXTERNALHTTPCLIENTTIMEOUT::T632340680"] = "policy files" + -- AI Studio couldn't install Pandoc because the archive was not found. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1059477764"] = "AI Studio couldn't install Pandoc because the archive was not found." @@ -7870,6 +7933,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." diff --git a/app/MindWork AI Studio/Provider/AlibabaCloud/ProviderAlibabaCloud.cs b/app/MindWork AI Studio/Provider/AlibabaCloud/ProviderAlibabaCloud.cs index 79aef2bc..2382f95f 100644 --- a/app/MindWork AI Studio/Provider/AlibabaCloud/ProviderAlibabaCloud.cs +++ b/app/MindWork AI Studio/Provider/AlibabaCloud/ProviderAlibabaCloud.cs @@ -13,7 +13,7 @@ public sealed class ProviderAlibabaCloud() : BaseProvider(LLMProviders.ALIBABA_C #region Implementation of IProvider /// - public override string Id => LLMProviders.ALIBABA_CLOUD.ToName(); + public override string Id => LLMProviders.ALIBABA_CLOUD.ToSecretId(); /// public override string InstanceName { get; set; } = "AlibabaCloud"; @@ -68,7 +68,7 @@ public sealed class ProviderAlibabaCloud() : BaseProvider(LLMProviders.ALIBABA_C /// public override async Task>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts) { - var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER); + var requestedSecret = await Program.RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER); return await this.PerformStandardTextEmbeddingRequest(requestedSecret, embeddingModel, token: token, texts: texts); } diff --git a/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs b/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs index 1f322788..c1277911 100644 --- a/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs +++ b/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs @@ -15,7 +15,7 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, n #region Implementation of IProvider /// - public override string Id => LLMProviders.ANTHROPIC.ToName(); + public override string Id => LLMProviders.ANTHROPIC.ToSecretId(); /// public override string InstanceName { get; set; } = "Anthropic"; @@ -27,7 +27,7 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, n public override async IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) { // Get the API key: - var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER); + var requestedSecret = await Program.RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER); if(!requestedSecret.Success) yield break; @@ -93,7 +93,7 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, n var request = new HttpRequestMessage(HttpMethod.Post, "messages"); // Set the authorization header: - request.Headers.Add("x-api-key", await requestedSecret.Secret.Decrypt(ENCRYPTION)); + request.Headers.Add("x-api-key", await requestedSecret.Secret.Decrypt(Program.ENCRYPTION)); // Set the Anthropic version: request.Headers.Add("anthropic-version", "2023-06-01"); diff --git a/app/MindWork AI Studio/Provider/BaseProvider.cs b/app/MindWork AI Studio/Provider/BaseProvider.cs index 4f901f31..4ebbf4f5 100644 --- a/app/MindWork AI Studio/Provider/BaseProvider.cs +++ b/app/MindWork AI Studio/Provider/BaseProvider.cs @@ -13,7 +13,6 @@ using AIStudio.Settings; using AIStudio.Tools.MIME; using AIStudio.Tools.PluginSystem; using AIStudio.Tools.Rust; -using AIStudio.Tools.Services; using Host = AIStudio.Provider.SelfHosted.Host; @@ -36,16 +35,6 @@ public abstract class BaseProvider : IProvider, ISecretId /// private readonly ILogger logger; - static BaseProvider() - { - RUST_SERVICE = Program.RUST_SERVICE; - ENCRYPTION = Program.ENCRYPTION; - } - - protected static readonly RustService RUST_SERVICE; - - protected static readonly Encryption ENCRYPTION; - protected static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new() { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, @@ -88,6 +77,9 @@ public abstract class BaseProvider : IProvider, ISecretId /// public abstract string Id { get; } + + /// + public string ConfiguredProviderId { get; init; } = string.Empty; /// public abstract string InstanceName { get; set; } @@ -164,9 +156,9 @@ public abstract class BaseProvider : IProvider, ISecretId protected async Task GetModelLoadingSecretKey(SecretStoreType storeType, string? apiKeyProvisional = null, bool isTryingSecret = false) => apiKeyProvisional switch { not null => apiKeyProvisional, - _ => await RUST_SERVICE.GetAPIKey(this, storeType, isTrying: isTryingSecret) switch + _ => await Program.RUST_SERVICE.GetAPIKey(this, storeType, isTrying: isTryingSecret) switch { - { Success: true } result => await result.Secret.Decrypt(ENCRYPTION), + { Success: true } result => await result.Secret.Decrypt(Program.ENCRYPTION), _ => null, } }; @@ -984,7 +976,7 @@ public abstract class BaseProvider : IProvider, ISecretId where TAnnotation : IAnnotationStreamLine { // Get the API key: - var requestedSecret = await RUST_SERVICE.GetAPIKey(this, storeType, isTrying: isTryingSecret); + var requestedSecret = await Program.RUST_SERVICE.GetAPIKey(this, storeType, isTrying: isTryingSecret); if(!requestedSecret.Success && !isTryingSecret) yield break; @@ -1008,7 +1000,7 @@ public abstract class BaseProvider : IProvider, ISecretId // Set the authorization header: if (requestedSecret.Success) - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION)); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(Program.ENCRYPTION)); // Set provider-specific headers: headersAction?.Invoke(request.Headers); @@ -1056,7 +1048,7 @@ public abstract class BaseProvider : IProvider, ISecretId { case LLMProviders.SELF_HOSTED: if(requestedSecret.Success) - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION)); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(Program.ENCRYPTION)); break; @@ -1067,7 +1059,7 @@ public abstract class BaseProvider : IProvider, ISecretId return TranscriptionResult.Failure(); } - request.Headers.Add("Authorization", await requestedSecret.Secret.Decrypt(ENCRYPTION)); + request.Headers.Add("Authorization", await requestedSecret.Secret.Decrypt(Program.ENCRYPTION)); break; default: @@ -1077,7 +1069,7 @@ public abstract class BaseProvider : IProvider, ISecretId return TranscriptionResult.Failure(); } - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION)); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(Program.ENCRYPTION)); break; } @@ -1138,7 +1130,7 @@ public abstract class BaseProvider : IProvider, ISecretId { case LLMProviders.SELF_HOSTED: if(requestedSecret.Success) - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION)); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(Program.ENCRYPTION)); break; @@ -1149,7 +1141,7 @@ public abstract class BaseProvider : IProvider, ISecretId return []; } - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION)); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(Program.ENCRYPTION)); break; } diff --git a/app/MindWork AI Studio/Provider/DeepSeek/ProviderDeepSeek.cs b/app/MindWork AI Studio/Provider/DeepSeek/ProviderDeepSeek.cs index 8de74942..03e10255 100644 --- a/app/MindWork AI Studio/Provider/DeepSeek/ProviderDeepSeek.cs +++ b/app/MindWork AI Studio/Provider/DeepSeek/ProviderDeepSeek.cs @@ -13,7 +13,7 @@ public sealed class ProviderDeepSeek() : BaseProvider(LLMProviders.DEEP_SEEK, ne #region Implementation of IProvider /// - public override string Id => LLMProviders.DEEP_SEEK.ToName(); + public override string Id => LLMProviders.DEEP_SEEK.ToSecretId(); /// public override string InstanceName { get; set; } = "DeepSeek"; diff --git a/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs b/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs index a8840873..e8aecb60 100644 --- a/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs +++ b/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs @@ -13,7 +13,7 @@ public class ProviderFireworks() : BaseProvider(LLMProviders.FIREWORKS, new Uri( #region Implementation of IProvider /// - public override string Id => LLMProviders.FIREWORKS.ToName(); + public override string Id => LLMProviders.FIREWORKS.ToSecretId(); /// public override string InstanceName { get; set; } = "Fireworks.ai"; @@ -63,7 +63,7 @@ public class ProviderFireworks() : BaseProvider(LLMProviders.FIREWORKS, new Uri( /// public override async Task TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default) { - var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.TRANSCRIPTION_PROVIDER); + var requestedSecret = await Program.RUST_SERVICE.GetAPIKey(this, SecretStoreType.TRANSCRIPTION_PROVIDER); return await this.PerformStandardTranscriptionRequest(requestedSecret, transcriptionModel, audioFilePath, token: token); } diff --git a/app/MindWork AI Studio/Provider/GWDG/ProviderGWDG.cs b/app/MindWork AI Studio/Provider/GWDG/ProviderGWDG.cs index f6181c72..ac44d28e 100644 --- a/app/MindWork AI Studio/Provider/GWDG/ProviderGWDG.cs +++ b/app/MindWork AI Studio/Provider/GWDG/ProviderGWDG.cs @@ -13,7 +13,7 @@ public sealed class ProviderGWDG() : BaseProvider(LLMProviders.GWDG, new Uri("ht #region Implementation of IProvider /// - public override string Id => LLMProviders.GWDG.ToName(); + public override string Id => LLMProviders.GWDG.ToSecretId(); /// public override string InstanceName { get; set; } = "GWDG SAIA"; @@ -62,7 +62,7 @@ public sealed class ProviderGWDG() : BaseProvider(LLMProviders.GWDG, new Uri("ht /// public override async Task TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default) { - var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.TRANSCRIPTION_PROVIDER); + var requestedSecret = await Program.RUST_SERVICE.GetAPIKey(this, SecretStoreType.TRANSCRIPTION_PROVIDER); return await this.PerformStandardTranscriptionRequest(requestedSecret, transcriptionModel, audioFilePath, token: token); } diff --git a/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs b/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs index 5e12811e..bb1212dd 100644 --- a/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs +++ b/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs @@ -15,7 +15,7 @@ public class ProviderGoogle() : BaseProvider(LLMProviders.GOOGLE, new Uri("https #region Implementation of IProvider /// - public override string Id => LLMProviders.GOOGLE.ToName(); + public override string Id => LLMProviders.GOOGLE.ToSecretId(); /// public override string InstanceName { get; set; } = "Google Gemini"; @@ -71,7 +71,7 @@ public class ProviderGoogle() : BaseProvider(LLMProviders.GOOGLE, new Uri("https /// public override async Task>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts) { - var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER); + var requestedSecret = await Program.RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER); try { var modelName = embeddingModel.Id; @@ -104,7 +104,7 @@ public class ProviderGoogle() : BaseProvider(LLMProviders.GOOGLE, new Uri("https var embeddingRequest = JsonSerializer.Serialize(payload, JSON_SERIALIZER_OPTIONS); var embedUrl = $"https://generativelanguage.googleapis.com/v1beta/models/{modelName}:embedContent"; using var request = new HttpRequestMessage(HttpMethod.Post, embedUrl); - request.Headers.Add("x-goog-api-key", await requestedSecret.Secret.Decrypt(ENCRYPTION)); + request.Headers.Add("x-goog-api-key", await requestedSecret.Secret.Decrypt(Program.ENCRYPTION)); // Set the content: request.Content = new StringContent(embeddingRequest, Encoding.UTF8, "application/json"); diff --git a/app/MindWork AI Studio/Provider/Groq/ProviderGroq.cs b/app/MindWork AI Studio/Provider/Groq/ProviderGroq.cs index ae7d13e9..caa4c4df 100644 --- a/app/MindWork AI Studio/Provider/Groq/ProviderGroq.cs +++ b/app/MindWork AI Studio/Provider/Groq/ProviderGroq.cs @@ -13,7 +13,7 @@ public class ProviderGroq() : BaseProvider(LLMProviders.GROQ, new Uri("https://a #region Implementation of IProvider /// - public override string Id => LLMProviders.GROQ.ToName(); + public override string Id => LLMProviders.GROQ.ToSecretId(); /// public override string InstanceName { get; set; } = "Groq"; diff --git a/app/MindWork AI Studio/Provider/Helmholtz/ProviderHelmholtz.cs b/app/MindWork AI Studio/Provider/Helmholtz/ProviderHelmholtz.cs index bc6647d2..27aa4b05 100644 --- a/app/MindWork AI Studio/Provider/Helmholtz/ProviderHelmholtz.cs +++ b/app/MindWork AI Studio/Provider/Helmholtz/ProviderHelmholtz.cs @@ -15,7 +15,7 @@ public sealed class ProviderHelmholtz() : BaseProvider(LLMProviders.HELMHOLTZ, n #region Implementation of IProvider /// - public override string Id => LLMProviders.HELMHOLTZ.ToName(); + public override string Id => LLMProviders.HELMHOLTZ.ToSecretId(); /// public override string InstanceName { get; set; } = "Helmholtz Blablador"; @@ -70,7 +70,7 @@ public sealed class ProviderHelmholtz() : BaseProvider(LLMProviders.HELMHOLTZ, n /// public override async Task>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts) { - var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER); + var requestedSecret = await Program.RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER); return await this.PerformStandardTextEmbeddingRequest(requestedSecret, embeddingModel, token: token, texts: texts); } diff --git a/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs b/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs index ddb16062..1c20c646 100644 --- a/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs +++ b/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs @@ -18,7 +18,7 @@ public sealed class ProviderHuggingFace : BaseProvider #region Implementation of IProvider /// - public override string Id => LLMProviders.HUGGINGFACE.ToName(); + public override string Id => LLMProviders.HUGGINGFACE.ToSecretId(); /// public override string InstanceName { get; set; } = "HuggingFace"; diff --git a/app/MindWork AI Studio/Provider/IProvider.cs b/app/MindWork AI Studio/Provider/IProvider.cs index dcf5e42c..02955e32 100644 --- a/app/MindWork AI Studio/Provider/IProvider.cs +++ b/app/MindWork AI Studio/Provider/IProvider.cs @@ -18,6 +18,11 @@ public interface IProvider /// public string Id { get; } + /// + /// The ID of the configured provider instance. + /// + public string ConfiguredProviderId { get; } + /// /// The provider's instance name. Useful for multiple instances of the same provider, /// e.g., to distinguish between different OpenAI API keys. diff --git a/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs b/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs index f04d9af4..8563c979 100644 --- a/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs +++ b/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs @@ -29,6 +29,10 @@ public static class LLMProvidersExtensions /// /// Returns the human-readable name of the provider. /// + /// + /// This value is UI text and may be localized. Do not use it for persisted IDs, secret namespaces, + /// or other stable identifiers. + /// /// The provider. /// The human-readable name of the provider. public static string ToName(this LLMProviders llmProvider) => llmProvider switch @@ -56,6 +60,41 @@ public static class LLMProvidersExtensions _ => TB("Unknown"), }; + + /// + /// Returns the stable secret namespace for the provider. + /// + /// + /// These values are used for OS keyring namespaces. They must never be localized or changed without + /// an explicit migration for existing API keys. + /// + /// The provider. + /// The stable secret namespace for the provider. + 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", + }; /// /// Get a provider's confidence. @@ -186,7 +225,7 @@ public static class LLMProvidersExtensions /// The provider instance. public static IProvider CreateProvider(this AIStudio.Settings.Provider providerSettings) { - return providerSettings.UsedLLMProvider.CreateProvider(providerSettings.InstanceName, providerSettings.Host, providerSettings.Hostname, providerSettings.Model, providerSettings.HFInferenceProvider, providerSettings.TokenizerPath, providerSettings.AdditionalJsonApiParameters, providerSettings.IsEnterpriseConfiguration); + return providerSettings.UsedLLMProvider.CreateProvider(providerSettings.InstanceName, providerSettings.Host, providerSettings.Hostname, providerSettings.Model, providerSettings.HFInferenceProvider, providerSettings.Id, providerSettings.AdditionalJsonApiParameters, providerSettings.TokenizerPath, providerSettings.IsEnterpriseConfiguration); } /// @@ -196,7 +235,7 @@ public static class LLMProvidersExtensions /// The provider instance. public static IProvider CreateProvider(this EmbeddingProvider embeddingProviderSettings) { - return embeddingProviderSettings.UsedLLMProvider.CreateProvider(embeddingProviderSettings.Name, embeddingProviderSettings.Host, embeddingProviderSettings.Hostname, embeddingProviderSettings.Model, HFInferenceProvider.NONE, embeddingProviderSettings.TokenizerPath, isEnterpriseConfiguration: embeddingProviderSettings.IsEnterpriseConfiguration); + return embeddingProviderSettings.UsedLLMProvider.CreateProvider(embeddingProviderSettings.Name, embeddingProviderSettings.Host, embeddingProviderSettings.Hostname, embeddingProviderSettings.Model, HFInferenceProvider.NONE, configuredProviderId: embeddingProviderSettings.Id, embeddingProviderSettings.TokenizerPath, isEnterpriseConfiguration: embeddingProviderSettings.IsEnterpriseConfiguration); } /// @@ -206,33 +245,33 @@ public static class LLMProvidersExtensions /// The provider instance. public static IProvider CreateProvider(this TranscriptionProvider transcriptionProviderSettings) { - return transcriptionProviderSettings.UsedLLMProvider.CreateProvider(transcriptionProviderSettings.Name, transcriptionProviderSettings.Host, transcriptionProviderSettings.Hostname, transcriptionProviderSettings.Model, HFInferenceProvider.NONE, string.Empty, isEnterpriseConfiguration: transcriptionProviderSettings.IsEnterpriseConfiguration); + return transcriptionProviderSettings.UsedLLMProvider.CreateProvider(transcriptionProviderSettings.Name, transcriptionProviderSettings.Host, transcriptionProviderSettings.Hostname, transcriptionProviderSettings.Model, HFInferenceProvider.NONE, configuredProviderId: transcriptionProviderSettings.Id, string.Empty, isEnterpriseConfiguration: transcriptionProviderSettings.IsEnterpriseConfiguration); } - private static IProvider CreateProvider(this LLMProviders provider, string instanceName, Host host, string hostname, Model model, HFInferenceProvider inferenceProvider, string tokenizerPath = "", 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 tokenizerPath = "", string expertProviderApiParameter = "", bool isEnterpriseConfiguration = false) { try { return provider switch { - LLMProviders.OPEN_AI => new ProviderOpenAI { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - LLMProviders.ANTHROPIC => new ProviderAnthropic { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - LLMProviders.MISTRAL => new ProviderMistral { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - LLMProviders.GOOGLE => new ProviderGoogle { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - LLMProviders.X => new ProviderX { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - LLMProviders.DEEP_SEEK => new ProviderDeepSeek { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - LLMProviders.ALIBABA_CLOUD => new ProviderAlibabaCloud { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - LLMProviders.PERPLEXITY => new ProviderPerplexity { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - LLMProviders.OPEN_ROUTER => new ProviderOpenRouter { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.OPEN_AI => new ProviderOpenAI { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.ANTHROPIC => new ProviderAnthropic { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.MISTRAL => new ProviderMistral { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.GOOGLE => new ProviderGoogle { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.X => new ProviderX { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.DEEP_SEEK => new ProviderDeepSeek { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.ALIBABA_CLOUD => new ProviderAlibabaCloud { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.PERPLEXITY => new ProviderPerplexity { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.OPEN_ROUTER => new ProviderOpenRouter { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - LLMProviders.GROQ => new ProviderGroq { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - LLMProviders.FIREWORKS => new ProviderFireworks { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - LLMProviders.HUGGINGFACE => new ProviderHuggingFace(inferenceProvider, model) { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.GROQ => new ProviderGroq { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.FIREWORKS => new ProviderFireworks { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.HUGGINGFACE => new ProviderHuggingFace(inferenceProvider, model) { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - LLMProviders.SELF_HOSTED => new ProviderSelfHosted(host, hostname) { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.SELF_HOSTED => new ProviderSelfHosted(host, hostname) { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - LLMProviders.HELMHOLTZ => new ProviderHelmholtz { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - LLMProviders.GWDG => new ProviderGWDG { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.HELMHOLTZ => new ProviderHelmholtz { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.GWDG => new ProviderGWDG { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, TokenizerPath = tokenizerPath, IsEnterpriseConfiguration = isEnterpriseConfiguration }, _ => new NoProvider(), }; @@ -329,14 +368,13 @@ public static class LLMProvidersExtensions /// /// Determines if the model selection should be completely hidden for LLM providers. - /// This is the case when the host does not support model selection (e.g., llama.cpp). + /// This is the case when the host does not support model selection. /// /// The provider. /// The host for self-hosted providers. /// True if model selection should be hidden; otherwise, false. public static bool IsLLMModelSelectionHidden(this LLMProviders provider, Host host) => provider switch { - LLMProviders.SELF_HOSTED => host is Host.LLAMA_CPP, _ => false, }; @@ -416,11 +454,11 @@ public static class LLMProvidersExtensions switch (host) { case Host.NONE: - case Host.LLAMA_CPP: case Host.WHISPER_CPP: default: return false; + case Host.LLAMA_CPP: case Host.OLLAMA: case Host.LM_STUDIO: case Host.VLLM: diff --git a/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs b/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs index c4169b72..9f70fe16 100644 --- a/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs +++ b/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs @@ -13,7 +13,7 @@ public sealed class ProviderMistral() : BaseProvider(LLMProviders.MISTRAL, new U #region Implementation of IProvider /// - public override string Id => LLMProviders.MISTRAL.ToName(); + public override string Id => LLMProviders.MISTRAL.ToSecretId(); /// public override string InstanceName { get; set; } = "Mistral"; @@ -69,14 +69,14 @@ public sealed class ProviderMistral() : BaseProvider(LLMProviders.MISTRAL, new U /// public override async Task TranscribeAudioAsync(Provider.Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default) { - var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.TRANSCRIPTION_PROVIDER); + var requestedSecret = await Program.RUST_SERVICE.GetAPIKey(this, SecretStoreType.TRANSCRIPTION_PROVIDER); return await this.PerformStandardTranscriptionRequest(requestedSecret, transcriptionModel, audioFilePath, token: token); } /// public override async Task>> EmbedTextAsync(Provider.Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts) { - var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER); + var requestedSecret = await Program.RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER); return await this.PerformStandardTextEmbeddingRequest(requestedSecret, embeddingModel, token: token, texts: texts); } diff --git a/app/MindWork AI Studio/Provider/Model.cs b/app/MindWork AI Studio/Provider/Model.cs index 0cd43395..f0b64539 100644 --- a/app/MindWork AI Studio/Provider/Model.cs +++ b/app/MindWork AI Studio/Provider/Model.cs @@ -23,7 +23,7 @@ public readonly record struct Model(string Id, string? DisplayName) /// /// Checks if this model is the system-configured placeholder. /// - public bool IsSystemModel => this == SYSTEM_MODEL; + public bool IsSystemModel => string.Equals(this.Id, SYSTEM_MODEL_ID, StringComparison.Ordinal); private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(Model).Namespace, nameof(Model)); diff --git a/app/MindWork AI Studio/Provider/NoProvider.cs b/app/MindWork AI Studio/Provider/NoProvider.cs index d2a8c9e1..85db3a05 100644 --- a/app/MindWork AI Studio/Provider/NoProvider.cs +++ b/app/MindWork AI Studio/Provider/NoProvider.cs @@ -13,6 +13,8 @@ public class NoProvider : IProvider public string Id => "none"; + public string ConfiguredProviderId => string.Empty; + public string InstanceName { get; set; } = "None"; /// diff --git a/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs b/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs index 56744f91..d0ce2833 100644 --- a/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs +++ b/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs @@ -22,7 +22,7 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur #region Implementation of IProvider /// - public override string Id => LLMProviders.OPEN_AI.ToName(); + public override string Id => LLMProviders.OPEN_AI.ToSecretId(); /// public override string InstanceName { get; set; } = "OpenAI"; @@ -56,7 +56,7 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur public override async IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) { // Get the API key: - var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER); + var requestedSecret = await Program.RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER); if(!requestedSecret.Success) yield break; @@ -221,7 +221,7 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur var request = new HttpRequestMessage(HttpMethod.Post, requestPath); // Set the authorization header: - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION)); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(Program.ENCRYPTION)); // Set the content: request.Content = new StringContent(openAIChatRequest, Encoding.UTF8, "application/json"); @@ -250,14 +250,14 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur /// public override async Task TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default) { - var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.TRANSCRIPTION_PROVIDER); + var requestedSecret = await Program.RUST_SERVICE.GetAPIKey(this, SecretStoreType.TRANSCRIPTION_PROVIDER); return await this.PerformStandardTranscriptionRequest(requestedSecret, transcriptionModel, audioFilePath, token: token); } /// public override async Task>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts) { - var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER); + var requestedSecret = await Program.RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER); return await this.PerformStandardTextEmbeddingRequest(requestedSecret, embeddingModel, token: token, texts: texts); } diff --git a/app/MindWork AI Studio/Provider/OpenRouter/ProviderOpenRouter.cs b/app/MindWork AI Studio/Provider/OpenRouter/ProviderOpenRouter.cs index 6e09ef02..1d5654d8 100644 --- a/app/MindWork AI Studio/Provider/OpenRouter/ProviderOpenRouter.cs +++ b/app/MindWork AI Studio/Provider/OpenRouter/ProviderOpenRouter.cs @@ -17,7 +17,7 @@ public sealed class ProviderOpenRouter() : BaseProvider(LLMProviders.OPEN_ROUTER #region Implementation of IProvider /// - public override string Id => LLMProviders.OPEN_ROUTER.ToName(); + public override string Id => LLMProviders.OPEN_ROUTER.ToSecretId(); /// public override string InstanceName { get; set; } = "OpenRouter"; @@ -79,7 +79,7 @@ public sealed class ProviderOpenRouter() : BaseProvider(LLMProviders.OPEN_ROUTER /// public override async Task>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts) { - var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER); + var requestedSecret = await Program.RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER); return await this.PerformStandardTextEmbeddingRequest(requestedSecret, embeddingModel, token: token, texts: texts); } diff --git a/app/MindWork AI Studio/Provider/Perplexity/ProviderPerplexity.cs b/app/MindWork AI Studio/Provider/Perplexity/ProviderPerplexity.cs index fce52bf9..c64241b5 100644 --- a/app/MindWork AI Studio/Provider/Perplexity/ProviderPerplexity.cs +++ b/app/MindWork AI Studio/Provider/Perplexity/ProviderPerplexity.cs @@ -22,7 +22,7 @@ public sealed class ProviderPerplexity() : BaseProvider(LLMProviders.PERPLEXITY, #region Implementation of IProvider /// - public override string Id => LLMProviders.PERPLEXITY.ToName(); + public override string Id => LLMProviders.PERPLEXITY.ToSecretId(); /// public override string InstanceName { get; set; } = "Perplexity"; diff --git a/app/MindWork AI Studio/Provider/SelfHosted/ModelsResponse.cs b/app/MindWork AI Studio/Provider/SelfHosted/ModelsResponse.cs index 8ea8fb57..545c9939 100644 --- a/app/MindWork AI Studio/Provider/SelfHosted/ModelsResponse.cs +++ b/app/MindWork AI Studio/Provider/SelfHosted/ModelsResponse.cs @@ -1,5 +1,7 @@ namespace AIStudio.Provider.SelfHosted; -public readonly record struct ModelsResponse(string Object, Model[] Data); +public readonly record struct ModelsResponse(string? Object, Model[]? Data); -public readonly record struct Model(string Id, string Object, string OwnedBy); \ No newline at end of file +public readonly record struct Model(string Id, string? Object, string? OwnedBy, ModelArchitecture? Architecture); + +public readonly record struct ModelArchitecture(string[]? InputModalities, string[]? OutputModalities); \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs b/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs index cf3b858a..b1580a77 100644 --- a/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs +++ b/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs @@ -1,5 +1,6 @@ using System.Net.Http.Headers; using System.Runtime.CompilerServices; +using System.Text.Json; using AIStudio.Chat; using AIStudio.Provider.OpenAI; @@ -17,20 +18,21 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide #region Implementation of IProvider /// - public override string Id => LLMProviders.SELF_HOSTED.ToName(); + public override string Id => LLMProviders.SELF_HOSTED.ToSecretId(); /// public override string InstanceName { get; set; } = "Self-hosted"; /// - public override bool HasModelLoadingCapability => host is Host.OLLAMA or Host.LM_STUDIO or Host.VLLM; + public override bool HasModelLoadingCapability => host is Host.OLLAMA or Host.LM_STUDIO or Host.VLLM or Host.LLAMA_CPP; /// public override async IAsyncEnumerable StreamChatCompletion(Provider.Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) { + var effectiveChatModel = await this.ResolveChatModelForRequest(chatModel, token); await foreach (var content in this.StreamOpenAICompatibleChatCompletion( "self-hosted provider", - chatModel, + effectiveChatModel, chatThread, settingsManager, async (systemPrompt, apiParameters) => @@ -40,13 +42,13 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide // - LM Studio, vLLM, and llama.cpp use the nested image URL format: { "type": "image_url", "image_url": { "url": "data:..." } } var messages = host switch { - Host.OLLAMA => await chatThread.Blocks.BuildMessagesUsingDirectImageUrlAsync(this.Provider, chatModel), - _ => await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel), + Host.OLLAMA => await chatThread.Blocks.BuildMessagesUsingDirectImageUrlAsync(this.Provider, effectiveChatModel), + _ => await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, effectiveChatModel), }; return new ChatCompletionAPIRequest { - Model = chatModel.Id, + Model = effectiveChatModel.Id, // Build the messages: // - First of all the system prompt @@ -75,14 +77,14 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide /// public override async Task TranscribeAudioAsync(Provider.Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default) { - var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.TRANSCRIPTION_PROVIDER, isTrying: true); + var requestedSecret = await Program.RUST_SERVICE.GetAPIKey(this, SecretStoreType.TRANSCRIPTION_PROVIDER, isTrying: true); return await this.PerformStandardTranscriptionRequest(requestedSecret, transcriptionModel, audioFilePath, host, token); } /// public override async Task>> EmbedTextAsync(Provider.Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts) { - var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER, isTrying: true); + var requestedSecret = await Program.RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER, isTrying: true); return await this.PerformStandardTextEmbeddingRequest(requestedSecret, embeddingModel, host, token: token, texts: texts); } @@ -93,9 +95,7 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide switch (host) { case Host.LLAMA_CPP: - // Right now, llama.cpp only supports one model. - // There is no API to list the model(s). - return ModelLoadResult.FromModels([ new Provider.Model("as configured by llama.cpp", null) ]); + return await this.LoadLlamaCppTextModels(["embed"], [], token, apiKeyProvisional); case Host.LM_STUDIO: case Host.OLLAMA: @@ -188,8 +188,10 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide } var lmStudioModelResponse = await lmStudioResponse.Content.ReadFromJsonAsync(token); - return SuccessfulModelLoadResult(lmStudioModelResponse.Data. - Where(model => !ignorePhrases.Any(ignorePhrase => model.Id.Contains(ignorePhrase, StringComparison.InvariantCulture)) && + var models = lmStudioModelResponse.Data ?? []; + return SuccessfulModelLoadResult(models. + Where(model => !string.IsNullOrWhiteSpace(model.Id) && + !ignorePhrases.Any(ignorePhrase => model.Id.Contains(ignorePhrase, StringComparison.InvariantCulture)) && filterPhrases.All( filter => model.Id.Contains(filter, StringComparison.InvariantCulture))) .Select(n => new Provider.Model(n.Id, null))); } @@ -200,4 +202,127 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide return FailedModelLoadResult(ModelLoadFailureReason.PROVIDER_UNAVAILABLE, e.Message); } } + + private async Task ResolveChatModelForRequest(Provider.Model chatModel, CancellationToken token) + { + if (host is not Host.LLAMA_CPP || !chatModel.IsSystemModel) + return chatModel; + + var modelLoadResult = await this.LoadLlamaCppTextModels(["embed"], [], token); + if (!modelLoadResult.Success) + return chatModel; + + var availableModels = modelLoadResult.Models + .Where(model => !model.IsSystemModel && !string.IsNullOrWhiteSpace(model.Id)) + .ToList(); + + if (modelLoadResult.Models.All(model => !model.IsSystemModel) && availableModels.Count is 0) + { + LOGGER.LogError("The llama.cpp provider '{ProviderInstanceName}' does not offer a usable text model. Please check your provider settings.", this.InstanceName); + throw new ProviderRequestException( + ProviderRequestFailureReason.NONE, + string.Format( + TB("The llama.cpp provider '{0}' does not offer a usable text model. Please check your provider settings."), + this.InstanceName)); + } + + if (availableModels.Count is 1) + return availableModels[0]; + + if (availableModels.Count > 1) + { + LOGGER.LogError( + "The llama.cpp provider '{ProviderInstanceName}' offers {ModelCount} models, but the configured model is the legacy system placeholder. The provider settings must be updated to select a specific model.", + this.InstanceName, + availableModels.Count); + throw new ProviderRequestException( + ProviderRequestFailureReason.NONE, + string.Format( + TB("The llama.cpp provider '{0}' offers multiple models. Please open the provider settings and select the model to use."), + this.InstanceName)); + } + + return chatModel; + } + + private async Task LoadLlamaCppTextModels(string[] ignorePhrases, string[] filterPhrases, CancellationToken token, string? apiKeyProvisional = null) + { + var secretKey = await this.GetModelLoadingSecretKey(SecretStoreType.LLM_PROVIDER, apiKeyProvisional, true); + + try + { + using var request = new HttpRequestMessage(HttpMethod.Get, "models"); + if (!string.IsNullOrWhiteSpace(secretKey)) + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey); + + using var response = await this.HttpClient.SendAsync(request, token); + var responseBody = await response.Content.ReadAsStringAsync(token); + if (!response.IsSuccessStatusCode) + { + if (response.StatusCode is System.Net.HttpStatusCode.NotFound) + return LlamaCppLegacyModelResult(); + + LOGGER.LogError("llama.cpp model loading request failed with status code {ResponseStatusCode} (message = '{ResponseReasonPhrase}', error body = '{ErrorBody}').", response.StatusCode, response.ReasonPhrase, responseBody); + return FailedModelLoadResult(this.GetModelLoadFailureReason(response, responseBody), $"Status={(int)response.StatusCode} {response.ReasonPhrase}; Body='{responseBody}'"); + } + + try + { + var modelResponse = JsonSerializer.Deserialize(responseBody, JSON_SERIALIZER_OPTIONS); + var responseModels = modelResponse.Data? + .Where(model => !string.IsNullOrWhiteSpace(model.Id)) + .ToList() ?? []; + + if (responseModels.Count is 0) + return LlamaCppLegacyModelResult(); + + var models = responseModels + .Where(model => IsMatchingLlamaCppTextModel(model, ignorePhrases, filterPhrases)) + .Select(model => new Provider.Model(model.Id, null)) + .ToList(); + + return SuccessfulModelLoadResult(models); + } + catch (JsonException e) + { + LOGGER.LogWarning(e, "The llama.cpp model loading response could not be parsed. Falling back to the legacy system-configured model."); + return LlamaCppLegacyModelResult(); + } + } + catch (Exception e) when (this.IsTimeoutException(e, token)) + { + await this.SendTimeoutError("loading the available models"); + LOGGER.LogError(e, "Timed out while loading models from llama.cpp provider '{ProviderInstanceName}'.", this.InstanceName); + return FailedModelLoadResult(ModelLoadFailureReason.PROVIDER_UNAVAILABLE, e.Message); + } + catch (Exception e) + { + LOGGER.LogError(e, "Failed to load models from llama.cpp provider '{ProviderInstanceName}'.", this.InstanceName); + return FailedModelLoadResult(ModelLoadFailureReason.UNKNOWN, e.Message); + } + } + + private static bool IsMatchingLlamaCppTextModel(Model model, string[] ignorePhrases, string[] filterPhrases) + { + if (string.IsNullOrWhiteSpace(model.Id)) + return false; + + if (ignorePhrases.Any(ignorePhrase => model.Id.Contains(ignorePhrase, StringComparison.InvariantCultureIgnoreCase))) + return false; + + if (!filterPhrases.All(filter => model.Id.Contains(filter, StringComparison.InvariantCultureIgnoreCase))) + return false; + + var outputModalities = model.Architecture?.OutputModalities; + if (outputModalities is { Length: > 0 } && + !outputModalities.Any(modality => string.Equals(modality, "text", StringComparison.OrdinalIgnoreCase))) + return false; + + return true; + } + + private static ModelLoadResult LlamaCppLegacyModelResult() + { + return ModelLoadResult.FromModels([ AIStudio.Provider.Model.SYSTEM_MODEL ]); + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/X/ProviderX.cs b/app/MindWork AI Studio/Provider/X/ProviderX.cs index f187aa0c..c02fa94d 100644 --- a/app/MindWork AI Studio/Provider/X/ProviderX.cs +++ b/app/MindWork AI Studio/Provider/X/ProviderX.cs @@ -13,7 +13,7 @@ public sealed class ProviderX() : BaseProvider(LLMProviders.X, new Uri("https:// #region Implementation of IProvider /// - public override string Id => LLMProviders.X.ToName(); + public override string Id => LLMProviders.X.ToSecretId(); /// public override string InstanceName { get; set; } = "xAI"; diff --git a/app/MindWork AI Studio/Settings/ConfigMeta.cs b/app/MindWork AI Studio/Settings/ConfigMeta.cs index 46a248b3..8c597906 100644 --- a/app/MindWork AI Studio/Settings/ConfigMeta.cs +++ b/app/MindWork AI Studio/Settings/ConfigMeta.cs @@ -151,7 +151,7 @@ public record ConfigMeta : ConfigMetaBase /// private void Reset() { - var configInstance = this.ConfigSelection.Compile().Invoke(SETTINGS_MANAGER.ConfigurationData); + var configInstance = this.ConfigSelection.Compile().Invoke(SettingsManagerAccess.ConfigurationData); var memberExpression = this.PropertyExpression.GetMemberExpression(); if (memberExpression.Member is System.Reflection.PropertyInfo propertyInfo) propertyInfo.SetValue(configInstance, this.Default); @@ -163,7 +163,7 @@ public record ConfigMeta : ConfigMetaBase /// The value to set for the configuration property. public void SetValue(TValue value) { - var configInstance = this.ConfigSelection.Compile().Invoke(SETTINGS_MANAGER.ConfigurationData); + var configInstance = this.ConfigSelection.Compile().Invoke(SettingsManagerAccess.ConfigurationData); var memberExpression = this.PropertyExpression.GetMemberExpression(); if (memberExpression.Member is System.Reflection.PropertyInfo propertyInfo) propertyInfo.SetValue(configInstance, value); @@ -174,7 +174,7 @@ public record ConfigMeta : ConfigMetaBase /// public TValue GetValue() { - var configInstance = this.ConfigSelection.Compile().Invoke(SETTINGS_MANAGER.ConfigurationData); + var configInstance = this.ConfigSelection.Compile().Invoke(SettingsManagerAccess.ConfigurationData); var memberExpression = this.PropertyExpression.GetMemberExpression(); if (memberExpression.Member is System.Reflection.PropertyInfo propertyInfo && propertyInfo.GetValue(configInstance) is TValue value) return value; diff --git a/app/MindWork AI Studio/Settings/ConfigMetaBase.cs b/app/MindWork AI Studio/Settings/ConfigMetaBase.cs index 4ef74e88..d077a701 100644 --- a/app/MindWork AI Studio/Settings/ConfigMetaBase.cs +++ b/app/MindWork AI Studio/Settings/ConfigMetaBase.cs @@ -2,5 +2,5 @@ namespace AIStudio.Settings; public abstract record ConfigMetaBase : IConfig { - protected static readonly SettingsManager SETTINGS_MANAGER = Program.SERVICE_PROVIDER.GetRequiredService(); + protected static SettingsManager SettingsManagerAccess => Program.SERVICE_PROVIDER.GetRequiredService(); } \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/ConfigurationSelectDataFactory.cs b/app/MindWork AI Studio/Settings/ConfigurationSelectDataFactory.cs index 84ae11bf..7b2704ac 100644 --- a/app/MindWork AI Studio/Settings/ConfigurationSelectDataFactory.cs +++ b/app/MindWork AI Studio/Settings/ConfigurationSelectDataFactory.cs @@ -271,8 +271,8 @@ public static class ConfigurationSelectDataFactory public static IEnumerable> 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()) { diff --git a/app/MindWork AI Studio/Settings/DataModel/Data.cs b/app/MindWork AI Studio/Settings/DataModel/Data.cs index 31581611..33a92038 100644 --- a/app/MindWork AI Studio/Settings/DataModel/Data.cs +++ b/app/MindWork AI Studio/Settings/DataModel/Data.cs @@ -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. /// - public Version Version { get; init; } = Version.V5; + public Version Version { get; init; } = Version.V6; /// /// List of configured providers. @@ -19,9 +19,14 @@ public sealed class Data public List Providers { get; init; } = []; /// - /// Settings concerning the LLM providers. + /// Settings concerning confidence levels. /// - public DataLLMProviders LLMProviders { get; init; } = new(); + public DataConfidence Confidence { get; init; } = new(x => x.Confidence); + + /// + /// Settings concerning data source security checks. + /// + public DataSourceSecuritySettings DataSourceSecurity { get; init; } = new(x => x.DataSourceSecurity); /// /// 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(); diff --git a/app/MindWork AI Studio/Settings/DataModel/DataApp.cs b/app/MindWork AI Studio/Settings/DataModel/DataApp.cs index c9352514..a0c2c58e 100644 --- a/app/MindWork AI Studio/Settings/DataModel/DataApp.cs +++ b/app/MindWork AI Studio/Settings/DataModel/DataApp.cs @@ -57,6 +57,11 @@ public sealed class DataApp(Expression>? configSelection = n /// public StartPage StartPage { get; set; } = ManagedConfiguration.Register(configSelection, n => n.StartPage, StartPage.HOME); + /// + /// Should the built-in introduction be visible on the home page? + /// + public bool ShowIntroduction { get; set; } = ManagedConfiguration.Register(configSelection, n => n.ShowIntroduction, true); + /// /// Should the quick start guide be visible on the home page? /// diff --git a/app/MindWork AI Studio/Settings/DataModel/DataChat.cs b/app/MindWork AI Studio/Settings/DataModel/DataChat.cs index 147bb7ac..f2a7ea37 100644 --- a/app/MindWork AI Studio/Settings/DataModel/DataChat.cs +++ b/app/MindWork AI Studio/Settings/DataModel/DataChat.cs @@ -1,7 +1,16 @@ +using System.Linq.Expressions; + namespace AIStudio.Settings.DataModel; -public sealed class DataChat +public sealed class DataChat(Expression>? configSelection = null) { + /// + /// The default constructor for the JSON deserializer. + /// + public DataChat() : this(null) + { + } + /// /// Shortcuts to send the input to the AI. /// @@ -25,22 +34,22 @@ public sealed class DataChat /// /// Preselect any chat options? /// - public bool PreselectOptions { get; set; } + public bool PreselectOptions { get; set; } = ManagedConfiguration.Register(configSelection, n => n.PreselectOptions, false); /// /// Should we preselect a provider for the chat? /// - public string PreselectedProvider { get; set; } = string.Empty; + public string PreselectedProvider { get; set; } = ManagedConfiguration.Register(configSelection, n => n.PreselectedProvider, string.Empty); /// /// Preselect a profile? /// - public string PreselectedProfile { get; set; } = string.Empty; + public string PreselectedProfile { get; set; } = ManagedConfiguration.Register(configSelection, n => n.PreselectedProfile, string.Empty); /// /// Preselect a chat template? /// - public string PreselectedChatTemplate { get; set; } = string.Empty; + public string PreselectedChatTemplate { get; set; } = ManagedConfiguration.Register(configSelection, n => n.PreselectedChatTemplate, string.Empty); /// /// Should we preselect data sources options for a created chat? diff --git a/app/MindWork AI Studio/Settings/DataModel/DataConfidence.cs b/app/MindWork AI Studio/Settings/DataModel/DataConfidence.cs new file mode 100644 index 00000000..8da7516c --- /dev/null +++ b/app/MindWork AI Studio/Settings/DataModel/DataConfidence.cs @@ -0,0 +1,40 @@ +using System.Linq.Expressions; + +using AIStudio.Provider; + +namespace AIStudio.Settings.DataModel; + +public sealed class DataConfidence(Expression>? configSelection = null) +{ + /// + /// The default constructor for the JSON deserializer. + /// + public DataConfidence() : this(null) + { + } + + /// + /// Should we enforce a global minimum confidence level? + /// + public bool EnforceGlobalMinimumConfidence { get; set; } = ManagedConfiguration.Register(configSelection, n => n.EnforceGlobalMinimumConfidence, false); + + /// + /// The global minimum confidence level to enforce. + /// + public ConfidenceLevel GlobalMinimumConfidence { get; set; } = ManagedConfiguration.Register(configSelection, n => n.GlobalMinimumConfidence, ConfidenceLevel.NONE); + + /// + /// Should we show the provider confidence level? + /// + public bool ShowProviderConfidence { get; set; } = ManagedConfiguration.Register(configSelection, n => n.ShowProviderConfidence, true); + + /// + /// Which confidence scheme to use. + /// + public ConfidenceSchemes ConfidenceScheme { get; set; } = ManagedConfiguration.Register(configSelection, n => n.ConfidenceScheme, ConfidenceSchemes.TRUST_ALL); + + /// + /// Provide custom confidence levels for each provider family. + /// + public Dictionary CustomConfidenceScheme { get; set; } = ManagedConfiguration.Register(configSelection, n => n.CustomConfidenceScheme, []); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataModel/DataIntroduction.cs b/app/MindWork AI Studio/Settings/DataModel/DataIntroduction.cs new file mode 100644 index 00000000..22a6d4ab --- /dev/null +++ b/app/MindWork AI Studio/Settings/DataModel/DataIntroduction.cs @@ -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(); + + /// + /// The stable ID of the introduction. + /// + public string Id { get; private init; } = string.Empty; + + /// + /// The ID of the enterprise configuration plugin that provides this introduction. + /// + public Guid EnterpriseConfigurationPluginId { get; private init; } = Guid.Empty; + + /// + /// The title shown to the user. + /// + public string Title { get; private init; } = string.Empty; + + /// + /// The configured version string shown to the user. + /// + public string VersionText { get; private init; } = string.Empty; + + /// + /// The sort index used on the home page. + /// + public int Index { get; private init; } = 1; + + /// + /// The Markdown content shown to the user. + /// + 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(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(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(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(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; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataModel/DataMandatoryInfo.cs b/app/MindWork AI Studio/Settings/DataModel/DataMandatoryInfo.cs index 638ba6d8..d588332f 100644 --- a/app/MindWork AI Studio/Settings/DataModel/DataMandatoryInfo.cs +++ b/app/MindWork AI Studio/Settings/DataModel/DataMandatoryInfo.cs @@ -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(); diff --git a/app/MindWork AI Studio/Settings/DataModel/DataLLMProviders.cs b/app/MindWork AI Studio/Settings/DataModel/PreviousModels/DataLLMProvidersV5.cs similarity index 83% rename from app/MindWork AI Studio/Settings/DataModel/DataLLMProviders.cs rename to app/MindWork AI Studio/Settings/DataModel/PreviousModels/DataLLMProvidersV5.cs index 30ad8bab..ad4d9e94 100644 --- a/app/MindWork AI Studio/Settings/DataModel/DataLLMProviders.cs +++ b/app/MindWork AI Studio/Settings/DataModel/PreviousModels/DataLLMProvidersV5.cs @@ -1,8 +1,8 @@ using AIStudio.Provider; -namespace AIStudio.Settings.DataModel; +namespace AIStudio.Settings.DataModel.PreviousModels; -public sealed class DataLLMProviders +public sealed class DataLLMProvidersV5 { /// /// Should we enforce a global minimum confidence level? @@ -25,7 +25,7 @@ public sealed class DataLLMProviders public ConfidenceSchemes ConfidenceScheme { get; set; } = ConfidenceSchemes.TRUST_ALL; /// - /// Provide custom confidence levels for each LLM provider. + /// Provide custom confidence levels for each provider family. /// public Dictionary CustomConfidenceScheme { get; set; } = new(); } \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataModel/PreviousModels/DataV4.cs b/app/MindWork AI Studio/Settings/DataModel/PreviousModels/DataV4.cs index 61555a3c..69406467 100644 --- a/app/MindWork AI Studio/Settings/DataModel/PreviousModels/DataV4.cs +++ b/app/MindWork AI Studio/Settings/DataModel/PreviousModels/DataV4.cs @@ -16,7 +16,7 @@ public sealed class DataV4 /// /// Settings concerning the LLM providers. /// - public DataLLMProviders LLMProviders { get; init; } = new(); + public DataLLMProvidersV5 LLMProviders { get; init; } = new(); /// /// List of configured profiles. diff --git a/app/MindWork AI Studio/Settings/DataModel/PreviousModels/DataV5.cs b/app/MindWork AI Studio/Settings/DataModel/PreviousModels/DataV5.cs new file mode 100644 index 00000000..3f568ce6 --- /dev/null +++ b/app/MindWork AI Studio/Settings/DataModel/PreviousModels/DataV5.cs @@ -0,0 +1,149 @@ +using AIStudio.Tools.PluginSystem.Assistants; + +namespace AIStudio.Settings.DataModel.PreviousModels; + +public sealed class DataV5 +{ + /// + /// The version of the settings file. Allows us to upgrade the settings + /// when a new version is available. + /// + public Version Version { get; init; } = Version.V5; + + /// + /// List of configured providers. + /// + public List Providers { get; init; } = []; + + /// + /// Settings concerning the LLM providers. + /// + public DataLLMProvidersV5 LLMProviders { get; init; } = new(); + + /// + /// A collection of embedding providers configured. + /// + public List EmbeddingProviders { get; init; } = []; + + /// + /// A collection of speech providers configured. + /// + public List TranscriptionProviders { get; init; } = []; + + /// + /// A collection of data sources configured. + /// + public List DataSources { get; set; } = []; + + /// + /// List of configured profiles. + /// + public List Profiles { get; init; } = []; + + /// + /// List of configured chat templates. + /// + public List ChatTemplates { get; init; } = []; + + /// + /// List of enabled plugins. + /// + public List EnabledPlugins { get; set; } = []; + + /// + /// Metadata for managed settings that use a plugin-provided editable default. + /// + public Dictionary ManagedEditableDefaults { get; set; } = []; + + /// + /// Cached audit results for assistant plugins. + /// + public List AssistantPluginAudits { get; set; } = []; + + /// + /// The next provider number to use. + /// + public uint NextProviderNum { get; set; } = 1; + + /// + /// The next embedding provider number to use. + /// + public uint NextEmbeddingNum { get; set; } = 1; + + /// + /// The next transcription provider number to use. + /// + public uint NextTranscriptionNum { get; set; } = 1; + + /// + /// The next data source number to use. + /// + public uint NextDataSourceNum { get; set; } = 1; + + /// + /// The next profile number to use. + /// + public uint NextProfileNum { get; set; } = 1; + + /// + /// The next chat template number to use. + /// + public uint NextChatTemplateNum { get; set; } = 1; + + /// + /// The next document analysis policy number to use. + /// + 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(); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataSourceSecuritySettings.cs b/app/MindWork AI Studio/Settings/DataSourceSecuritySettings.cs new file mode 100644 index 00000000..5c84240a --- /dev/null +++ b/app/MindWork AI Studio/Settings/DataSourceSecuritySettings.cs @@ -0,0 +1,20 @@ +using System.Linq.Expressions; + +using AIStudio.Settings.DataModel; + +namespace AIStudio.Settings; + +public sealed class DataSourceSecuritySettings(Expression>? configSelection = null) +{ + /// + /// The default constructor for the JSON deserializer. + /// + public DataSourceSecuritySettings() : this(null) + { + } + + /// + /// Provider instance IDs trusted by an organization for data-source security checks. + /// + public HashSet TrustedProviderIds { get; set; } = ManagedConfiguration.Register(configSelection, n => n.TrustedProviderIds, []); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataSourceSecurityTrustExtensions.cs b/app/MindWork AI Studio/Settings/DataSourceSecurityTrustExtensions.cs new file mode 100644 index 00000000..bff1f898 --- /dev/null +++ b/app/MindWork AI Studio/Settings/DataSourceSecurityTrustExtensions.cs @@ -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)); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/EmbeddingProvider.cs b/app/MindWork AI Studio/Settings/EmbeddingProvider.cs index eb227489..3c02c07b 100644 --- a/app/MindWork AI Studio/Settings/EmbeddingProvider.cs +++ b/app/MindWork AI Studio/Settings/EmbeddingProvider.cs @@ -45,7 +45,7 @@ public sealed record EmbeddingProvider( /// [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(); /// [JsonIgnore] @@ -134,7 +134,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)); diff --git a/app/MindWork AI Studio/Settings/ManagedConfiguration.Parsing.cs b/app/MindWork AI Studio/Settings/ManagedConfiguration.Parsing.cs index 4b453d27..e44fc8dc 100644 --- a/app/MindWork AI Studio/Settings/ManagedConfiguration.Parsing.cs +++ b/app/MindWork AI Studio/Settings/ManagedConfiguration.Parsing.cs @@ -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(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); } - + /// /// Attempts to process the configuration settings from a Lua table for string values. /// @@ -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(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(out var configuredLuaValueText)) { @@ -304,7 +304,7 @@ public static partial class ManagedConfiguration } // ReSharper restore MethodOverloadWithOptionalParameter - + /// /// Attempts to process the configuration settings from a Lua table for enum list types. /// @@ -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(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(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); } - + /// /// Attempts to process the configuration settings from a Lua table for string list types. /// @@ -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(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(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(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(out var configuredLuaValueText)) { @@ -517,7 +517,7 @@ public static partial class ManagedConfiguration } // ReSharper restore MethodOverloadWithOptionalParameter - + /// /// Attempts to process the configuration settings from a Lua table for enum set types. /// @@ -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(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(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(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(out var configuredLuaValueText)) { @@ -654,7 +654,7 @@ public static partial class ManagedConfiguration if (successful) { - var configInstance = configSelection.Compile().Invoke(SETTINGS_MANAGER.ConfigurationData); + var configInstance = configSelection.Compile().Invoke(SettingsManagerAccess.ConfigurationData); var currentValue = propertyExpression.Compile().Invoke(configInstance); var merged = new HashSet(currentValue); merged.UnionWith(configuredValue); @@ -671,7 +671,7 @@ public static partial class ManagedConfiguration return successful; } - + /// /// Attempts to process the configuration settings from a Lua table for string set types. /// @@ -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(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(out var configuredLuaValueText)) set.Add(configuredLuaValueText); } - + configuredValue = set; successful = true; } - + if(dryRun) return successful; - + return HandleParsedValue(configPluginId, dryRun, successful, configMeta, configuredValue); } - + /// /// Attempts to process the configuration settings from a Lua table for string dictionary types. /// @@ -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(out var key); var hadValue = pair.Value.TryRead(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); } + /// + /// Attempts to process the configuration settings from a Lua table for enum dictionary types. + /// + /// + /// 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. + /// + /// The ID of the related configuration plugin. + /// The Lua table containing the settings to process. + /// The expression to select the configuration class. + /// The expression to select the property within the configuration class. + /// When true, the method will not apply any changes but only check if the configuration can be read. + /// The type of the configuration class. + /// The enum type of the dictionary keys. + /// The enum type of the dictionary values. + /// True when the configuration was successfully processed, otherwise false. + public static bool TryProcessConfiguration( + Expression> configSelection, + Expression>> 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(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(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(out var keyText); + var hadValue = pair.Value.TryRead(out var valueText); + + // If both key and value were read successfully, parse and add them to the dictionary: + if (hadKey + && hadValue + && Enum.TryParse(keyText, true, out var key) + && Enum.TryParse(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); + } + /// /// Handles the parsed configuration value based on whether the parsing was successful and whether it's a dry run. /// @@ -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() + .OrderBy(key => key.ToString(), StringComparer.Ordinal) + .Select(key => $"{key}:{dictionary[key]}")), IFormattable formattable => formattable.ToString(null, CultureInfo.InvariantCulture), - + _ => value.ToString() ?? string.Empty, }; } \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/ManagedConfiguration.Register.cs b/app/MindWork AI Studio/Settings/ManagedConfiguration.Register.cs index fbc33767..fad65bd0 100644 --- a/app/MindWork AI Studio/Settings/ManagedConfiguration.Register.cs +++ b/app/MindWork AI Studio/Settings/ManagedConfiguration.Register.cs @@ -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(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>(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>(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>(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>(configSelection, propertyExpression) { @@ -268,7 +268,48 @@ public static partial class ManagedConfiguration { Default = defaultValues, }; - + + return defaultValues; + } + + /// + /// Registers a configuration setting with a default dictionary of enum key-value pairs. + /// + /// + /// When the method is invoked with a null configSelection, the configuration path + /// is ignored, and the specified default values are returned without registration. + /// + /// The expression that selects the configuration class from the root Data model. + /// The expression to select the property within the configuration class. + /// The default dictionary of values to use when the setting is not configured. + /// The type of the configuration class from which the property is selected. + /// The enum type of the dictionary keys. + /// The enum type of the dictionary values. + /// A dictionary containing the default values. + public static Dictionary Register( + Expression>? configSelection, + Expression>> propertyExpression, + Dictionary 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>(configSelection, propertyExpression) + { + Default = defaultValues, + }; + return defaultValues; } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/ManagedConfiguration.cs b/app/MindWork AI Studio/Settings/ManagedConfiguration.cs index 0e62f2c6..876c8408 100644 --- a/app/MindWork AI Studio/Settings/ManagedConfiguration.cs +++ b/app/MindWork AI Studio/Settings/ManagedConfiguration.cs @@ -9,7 +9,7 @@ namespace AIStudio.Settings; public static partial class ManagedConfiguration { private static readonly ConcurrentDictionary METADATA = new(); - private static readonly SettingsManager SETTINGS_MANAGER = Program.SERVICE_PROVIDER.GetRequiredService(); + private static SettingsManager SettingsManagerAccess => Program.SERVICE_PROVIDER.GetRequiredService(); /// /// Attempts to retrieve the configuration metadata for a given configuration selection and @@ -231,6 +231,44 @@ public static partial class ManagedConfiguration return false; } + /// + /// Attempts to retrieve the configuration metadata for an enum dictionary-based setting. + /// + /// + /// 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. + /// + /// The expression to select the configuration class. + /// The expression to select the property within the + /// configuration class. + /// The output parameter that will hold the configuration metadata + /// if found. + /// The type of the configuration class. + /// The enum type of the dictionary keys. + /// The enum type of the dictionary values. + /// True if the configuration metadata was found, otherwise false. + public static bool TryGet( + Expression> configSelection, + Expression>> propertyExpression, + out ConfigMeta> configMeta) + where TKey : struct, Enum + where TValue : struct, Enum + { + var configPath = Path(configSelection, propertyExpression); + if (METADATA.TryGetValue(configPath, out var value) && value is ConfigMeta> meta) + { + configMeta = meta; + return true; + } + + configMeta = new NoConfig>(configSelection, propertyExpression) + { + Default = new Dictionary(), + }; + return false; + } + /// /// 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( + Expression> configSelection, + Expression>> propertyExpression, + IEnumerable 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(Expression> configSelection, Expression> propertyExpression) { @@ -418,19 +492,19 @@ public static partial class ManagedConfiguration private static bool TryGetEditableDefaultState(string settingName, out ManagedEditableDefaultState editableDefaultState) { - return SETTINGS_MANAGER.ConfigurationData.ManagedEditableDefaults.TryGetValue(settingName, out editableDefaultState!); + return SettingsManagerAccess.ConfigurationData.ManagedEditableDefaults.TryGetValue(settingName, out editableDefaultState!); } private static void SetEditableDefaultState(string settingName, Guid pluginId, string lastAppliedValue) { - SETTINGS_MANAGER.ConfigurationData.ManagedEditableDefaults[settingName] = new() + SettingsManagerAccess.ConfigurationData.ManagedEditableDefaults[settingName] = new() { ConfigPluginId = pluginId, LastAppliedValue = lastAppliedValue, }; } - private static bool ClearEditableDefaultState(string settingName) => SETTINGS_MANAGER.ConfigurationData.ManagedEditableDefaults.Remove(settingName); + private static bool ClearEditableDefaultState(string settingName) => SettingsManagerAccess.ConfigurationData.ManagedEditableDefaults.Remove(settingName); private static bool CleanupEditableDefaultState( ConfigMeta configMeta, diff --git a/app/MindWork AI Studio/Settings/Provider.cs b/app/MindWork AI Studio/Settings/Provider.cs index a793c4a9..d6541d90 100644 --- a/app/MindWork AI Studio/Settings/Provider.cs +++ b/app/MindWork AI Studio/Settings/Provider.cs @@ -73,7 +73,7 @@ public sealed record Provider( /// [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(); /// [JsonIgnore] @@ -191,7 +191,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)); diff --git a/app/MindWork AI Studio/Settings/SettingsManager.cs b/app/MindWork AI Studio/Settings/SettingsManager.cs index 3ec8906c..336d4f95 100644 --- a/app/MindWork AI Studio/Settings/SettingsManager.cs +++ b/app/MindWork AI Studio/Settings/SettingsManager.cs @@ -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 /// public bool HasCompletedInitialSettingsLoad { get; private set; } + /// + /// Indicates why settings writes are blocked for the current session. + /// + public SettingsWriteBlockReason SettingsWriteBlockReason { get; private set; } = SettingsWriteBlockReason.NONE; + + /// + /// Indicates that settings writes are blocked for the current session. + /// + public bool SettingsWriteBlocked => this.SettingsWriteBlockReason is not SettingsWriteBlockReason.NONE; + /// /// The configuration data. /// @@ -87,6 +102,7 @@ public sealed class SettingsManager /// A (migrated) settings snapshot, or null if it could not be read. public async Task 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 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 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 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 TryDeserializeCurrentSettings(string settingsPath, string sourceDescription) + { + try + { + var settingsData = JsonSerializer.Deserialize(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); } /// @@ -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 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) @@ -348,17 +530,13 @@ public sealed class SettingsManager return Profile.NO_PROFILE; if (preselection.UseSpecificProfile) - { - var componentProfile = this.ConfigurationData.Profiles.FirstOrDefault(x => x.Id.Equals(preselection.SpecificProfileId, StringComparison.OrdinalIgnoreCase)); - return componentProfile ?? Profile.NO_PROFILE; - } + return this.GetProfileById(preselection.SpecificProfileId); var appPreselection = ProfilePreselection.FromStoredValue(this.ConfigurationData.App.PreselectedProfile); if (appPreselection.DoNotPreselectProfile || !appPreselection.UseSpecificProfile) return Profile.NO_PROFILE; - var appProfile = this.ConfigurationData.Profiles.FirstOrDefault(x => x.Id.Equals(appPreselection.SpecificProfileId, StringComparison.OrdinalIgnoreCase)); - return appProfile ?? Profile.NO_PROFILE; + return this.GetProfileById(appPreselection.SpecificProfileId); } public Profile GetAppPreselectedProfile() @@ -367,8 +545,7 @@ public sealed class SettingsManager if (appPreselection.DoNotPreselectProfile || !appPreselection.UseSpecificProfile) return Profile.NO_PROFILE; - var appProfile = this.ConfigurationData.Profiles.FirstOrDefault(x => x.Id.Equals(appPreselection.SpecificProfileId, StringComparison.OrdinalIgnoreCase)); - return appProfile ?? Profile.NO_PROFILE; + return this.GetProfileById(appPreselection.SpecificProfileId); } public ChatTemplate GetPreselectedChatTemplate(Tools.Components component) @@ -377,8 +554,29 @@ public sealed class SettingsManager if (preselection != ChatTemplate.NO_CHAT_TEMPLATE) return preselection; - preselection = this.ConfigurationData.ChatTemplates.FirstOrDefault(x => x.Id.Equals(this.ConfigurationData.App.PreselectedChatTemplate, StringComparison.OrdinalIgnoreCase)); - return preselection ?? ChatTemplate.NO_CHAT_TEMPLATE; + return this.GetChatTemplateById(this.ConfigurationData.App.PreselectedChatTemplate); + } + + public Profile GetProfileById(string? profileId) + { + if (string.IsNullOrWhiteSpace(profileId)) + return Profile.NO_PROFILE; + + if (string.Equals(profileId, Profile.NO_PROFILE.Id, StringComparison.OrdinalIgnoreCase)) + return Profile.NO_PROFILE; + + return this.ConfigurationData.Profiles.FirstOrDefault(x => x.Id.Equals(profileId, StringComparison.OrdinalIgnoreCase)) ?? Profile.NO_PROFILE; + } + + public ChatTemplate GetChatTemplateById(string? chatTemplateId) + { + if (string.IsNullOrWhiteSpace(chatTemplateId)) + return ChatTemplate.NO_CHAT_TEMPLATE; + + if (string.Equals(chatTemplateId, ChatTemplate.NO_CHAT_TEMPLATE.Id, StringComparison.OrdinalIgnoreCase)) + return ChatTemplate.NO_CHAT_TEMPLATE; + + return this.ConfigurationData.ChatTemplates.FirstOrDefault(x => x.Id.Equals(chatTemplateId, StringComparison.OrdinalIgnoreCase)) ?? ChatTemplate.NO_CHAT_TEMPLATE; } public ConfidenceLevel GetConfiguredConfidenceLevel(LLMProviders llmProvider) @@ -386,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 @@ -446,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; @@ -469,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}"; } -} +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/SettingsMigrations.cs b/app/MindWork AI Studio/Settings/SettingsMigrations.cs index e5041817..fe68c85a 100644 --- a/app/MindWork AI Studio/Settings/SettingsMigrations.cs +++ b/app/MindWork AI Studio/Settings/SettingsMigrations.cs @@ -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(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(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(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(configData, jsonOptions); + case Version.V5: + var configV5 = JsonSerializer.Deserialize(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(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 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 logger, DataV4 previousConfig) + + private static DataV5 MigrateV4ToV5(ILogger 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 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, + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/SettingsWriteBlockReason.cs b/app/MindWork AI Studio/Settings/SettingsWriteBlockReason.cs new file mode 100644 index 00000000..d21285b0 --- /dev/null +++ b/app/MindWork AI Studio/Settings/SettingsWriteBlockReason.cs @@ -0,0 +1,11 @@ +namespace AIStudio.Settings; + +public enum SettingsWriteBlockReason +{ + NONE, + VERSION_MISSING, + VERSION_UNKNOWN, + VERSION_NEWER_THAN_APP, + FILE_UNREADABLE, + CURRENT_VERSION_INVALID, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/TranscriptionProvider.cs b/app/MindWork AI Studio/Settings/TranscriptionProvider.cs index ca95d821..973cd138 100644 --- a/app/MindWork AI Studio/Settings/TranscriptionProvider.cs +++ b/app/MindWork AI Studio/Settings/TranscriptionProvider.cs @@ -44,7 +44,7 @@ public sealed record TranscriptionProvider( /// [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(); /// [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)); diff --git a/app/MindWork AI Studio/Settings/Version.cs b/app/MindWork AI Studio/Settings/Version.cs index dc6f99df..a09ff20c 100644 --- a/app/MindWork AI Studio/Settings/Version.cs +++ b/app/MindWork AI Studio/Settings/Version.cs @@ -13,4 +13,5 @@ public enum Version V3, V4, V5, + V6, } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/ComponentsExtensions.cs b/app/MindWork AI Studio/Tools/ComponentsExtensions.cs index bd48dbc5..b95ab1cb 100644 --- a/app/MindWork AI Studio/Tools/ComponentsExtensions.cs +++ b/app/MindWork AI Studio/Tools/ComponentsExtensions.cs @@ -169,7 +169,7 @@ public static class ComponentsExtensions public static ChatTemplate PreselectedChatTemplate(this Components component, SettingsManager settingsManager) => component switch { - Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.ConfigurationData.ChatTemplates.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Chat.PreselectedChatTemplate) ?? ChatTemplate.NO_CHAT_TEMPLATE : ChatTemplate.NO_CHAT_TEMPLATE, + Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.GetChatTemplateById(settingsManager.ConfigurationData.Chat.PreselectedChatTemplate) : ChatTemplate.NO_CHAT_TEMPLATE, _ => ChatTemplate.NO_CHAT_TEMPLATE, }; diff --git a/app/MindWork AI Studio/Tools/ExternalHttpClientTimeout.cs b/app/MindWork AI Studio/Tools/ExternalHttpClientTimeout.cs index 3d465737..1181cb40 100644 --- a/app/MindWork AI Studio/Tools/ExternalHttpClientTimeout.cs +++ b/app/MindWork AI Studio/Tools/ExternalHttpClientTimeout.cs @@ -19,6 +19,10 @@ public static class ExternalHttpClientTimeout private const string ENV_CUSTOM_ROOT_CERTIFICATES_ENABLED = "MINDWORK_AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATES_ENABLED"; private const string ENV_CUSTOM_ROOT_CERTIFICATE_BUNDLE_PATH = "MINDWORK_AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATE_BUNDLE_PATH"; private const string ENV_CUSTOM_ROOT_CERTIFICATE_ALLOWED_HOSTS = "MINDWORK_AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATE_ALLOWED_HOSTS"; + private const string ENV_POLICY_CUSTOM_ROOT_CERTIFICATES_CONFIGURED = "AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATES_POLICY_CONFIGURED"; + private const string ENV_POLICY_CUSTOM_ROOT_CERTIFICATES_ENABLED = "AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATES_ENABLED"; + private const string ENV_POLICY_CUSTOM_ROOT_CERTIFICATE_BUNDLE_PATH = "AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATE_BUNDLE_PATH"; + private const string ENV_POLICY_CUSTOM_ROOT_CERTIFICATE_ALLOWED_HOSTS = "AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATE_ALLOWED_HOSTS"; // id-kp-serverAuth: Extended Key Usage for TLS server authentication. // See RFC 5280, section 4.2.1.12: https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.12 @@ -26,7 +30,7 @@ public static class ExternalHttpClientTimeout private static string TB(string fallbackEN) => PluginSystem.I18N.I.T(fallbackEN, typeof(ExternalHttpClientTimeout).Namespace, nameof(ExternalHttpClientTimeout)); private static readonly Lazy LOGGER = new(() => Program.LOGGER_FACTORY.CreateLogger(nameof(ExternalHttpClientTimeout))); - private static readonly Lazy SETTINGS_MANAGER = new(() => Program.SERVICE_PROVIDER.GetRequiredService()); + private static SettingsManager SettingsManagerAccess => Program.SERVICE_PROVIDER.GetRequiredService(); private static readonly Lock CUSTOM_ROOT_CERTIFICATE_LOCK = new(); private static CustomRootCertificateCache? CUSTOM_ROOT_CERTIFICATE_CACHE; @@ -91,7 +95,7 @@ public static class ExternalHttpClientTimeout private static TimeSpan GetTimeout() { - var seconds = SETTINGS_MANAGER.Value.ConfigurationData.App.HttpClientTimeoutSeconds; + var seconds = SettingsManagerAccess.ConfigurationData.App.HttpClientTimeoutSeconds; if (seconds <= 0) seconds = DEFAULT_HTTP_CLIENT_TIMEOUT_SECONDS; @@ -123,29 +127,43 @@ public static class ExternalHttpClientTimeout private static CustomRootCertificateConfiguration ReadCustomRootCertificateConfiguration() { + if (TryParseBooleanEnvironmentValue(Environment.GetEnvironmentVariable(ENV_POLICY_CUSTOM_ROOT_CERTIFICATES_CONFIGURED), out var policyConfigured) && policyConfigured) + { + var policyEnabled = TryParseBooleanEnvironmentValue(Environment.GetEnvironmentVariable(ENV_POLICY_CUSTOM_ROOT_CERTIFICATES_ENABLED), out var parsedPolicyEnabled) && parsedPolicyEnabled; + var policyBundlePath = Environment.GetEnvironmentVariable(ENV_POLICY_CUSTOM_ROOT_CERTIFICATE_BUNDLE_PATH)?.Trim() ?? string.Empty; + var policyAllowedHosts = Environment.GetEnvironmentVariable(ENV_POLICY_CUSTOM_ROOT_CERTIFICATE_ALLOWED_HOSTS) ?? string.Empty; + return new(policyEnabled, policyBundlePath, ReadAllowedHostPatternsFromDelimitedValue(policyAllowedHosts), TB("policy files")); + } + var envEnabled = Environment.GetEnvironmentVariable(ENV_CUSTOM_ROOT_CERTIFICATES_ENABLED); var envBundlePath = Environment.GetEnvironmentVariable(ENV_CUSTOM_ROOT_CERTIFICATE_BUNDLE_PATH); var envAllowedHosts = Environment.GetEnvironmentVariable(ENV_CUSTOM_ROOT_CERTIFICATE_ALLOWED_HOSTS); + if (!string.IsNullOrWhiteSpace(envEnabled) || !string.IsNullOrWhiteSpace(envBundlePath) || !string.IsNullOrWhiteSpace(envAllowedHosts)) + { + var enabled = TryParseBooleanEnvironmentValue(envEnabled, out var parsedEnvEnabled) + ? parsedEnvEnabled + : SettingsManagerAccess.ConfigurationData.App.ExternalHttpCustomRootCertificatesEnabled; - var enabled = TryParseBooleanEnvironmentValue(envEnabled, out var parsedEnvEnabled) - ? parsedEnvEnabled - : SETTINGS_MANAGER.Value.ConfigurationData.App.ExternalHttpCustomRootCertificatesEnabled; + var bundlePath = !string.IsNullOrWhiteSpace(envBundlePath) + ? envBundlePath.Trim() + : SettingsManagerAccess.ConfigurationData.App.ExternalHttpCustomRootCertificateBundlePath.Trim(); - var bundlePath = !string.IsNullOrWhiteSpace(envBundlePath) - ? envBundlePath.Trim() - : SETTINGS_MANAGER.Value.ConfigurationData.App.ExternalHttpCustomRootCertificateBundlePath.Trim(); + var allowedHostPatterns = !string.IsNullOrWhiteSpace(envAllowedHosts) + ? ReadAllowedHostPatternsFromDelimitedValue(envAllowedHosts) + : ReadAllowedHostPatterns(SettingsManagerAccess.ConfigurationData.App.ExternalHttpCustomRootCertificateAllowedHosts); - var allowedHostPatterns = ReadAllowedHostPatterns(envAllowedHosts); - var source = ReadCustomRootCertificateConfigurationSource(envEnabled, envBundlePath, envAllowedHosts); + return new(enabled, bundlePath, allowedHostPatterns, TB("environment variables")); + } - return new(enabled, bundlePath, allowedHostPatterns, source); + return new( + SettingsManagerAccess.ConfigurationData.App.ExternalHttpCustomRootCertificatesEnabled, + SettingsManagerAccess.ConfigurationData.App.ExternalHttpCustomRootCertificateBundlePath.Trim(), + ReadAllowedHostPatterns(SettingsManagerAccess.ConfigurationData.App.ExternalHttpCustomRootCertificateAllowedHosts), + ReadCustomRootCertificateSettingsSource()); } - private static string ReadCustomRootCertificateConfigurationSource(string? envEnabled, string? envBundlePath, string? envAllowedHosts) + private static string ReadCustomRootCertificateSettingsSource() { - if (!string.IsNullOrWhiteSpace(envEnabled) || !string.IsNullOrWhiteSpace(envBundlePath) || !string.IsNullOrWhiteSpace(envAllowedHosts)) - return TB("environment variables"); - var enabledIsManaged = ManagedConfiguration.TryGet(x => x.App, x => x.ExternalHttpCustomRootCertificatesEnabled, out var enabledMeta) && enabledMeta.IsLocked; var bundlePathIsManaged = ManagedConfiguration.TryGet(x => x.App, x => x.ExternalHttpCustomRootCertificateBundlePath, out var bundlePathMeta) && bundlePathMeta.IsLocked; var allowedHostsIsManaged = ManagedConfiguration.TryGet(x => x.App, x => x.ExternalHttpCustomRootCertificateAllowedHosts, out var allowedHostsMeta) && allowedHostsMeta.IsLocked; @@ -154,12 +172,13 @@ public static class ExternalHttpClientTimeout : TB("app settings"); } - private static IReadOnlyList ReadAllowedHostPatterns(string? envAllowedHosts) + private static IReadOnlyList ReadAllowedHostPatternsFromDelimitedValue(string allowedHosts) { - IEnumerable rawPatterns = !string.IsNullOrWhiteSpace(envAllowedHosts) - ? envAllowedHosts.Split([';', ','], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) - : SETTINGS_MANAGER.Value.ConfigurationData.App.ExternalHttpCustomRootCertificateAllowedHosts; + return ReadAllowedHostPatterns(allowedHosts.Split([';', ','], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)); + } + private static IReadOnlyList ReadAllowedHostPatterns(IEnumerable rawPatterns) + { var patterns = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var rawPattern in rawPatterns) { diff --git a/app/MindWork AI Studio/Tools/PandocProcessBuilder.cs b/app/MindWork AI Studio/Tools/PandocProcessBuilder.cs index 6d0909f8..dd31e38b 100644 --- a/app/MindWork AI Studio/Tools/PandocProcessBuilder.cs +++ b/app/MindWork AI Studio/Tools/PandocProcessBuilder.cs @@ -17,6 +17,7 @@ public sealed class PandocProcessBuilder private static readonly RID CPU_ARCHITECTURE = RIDExtensions.GetCurrentRID(); private static readonly RID METADATA_ARCHITECTURE = META_DATA_ARCH.Architecture.ToRID(); private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(nameof(PandocProcessBuilder)); + private const string FLATPAK_PANDOC_PLUGIN_BIN_DIRECTORY = "/app/plugins/pandoc/bin"; // Tracks whether the first log has been written to avoid log spam on repeated calls: private static bool HAS_LOGGED_ONCE; @@ -220,7 +221,8 @@ public sealed class PandocProcessBuilder } } - foreach (var candidate in SystemPandocExecutableCandidates(PandocExecutableName)) + var runtimeInfo = await rustService.GetRuntimeInfo(); + foreach (var candidate in SystemPandocExecutableCandidates(PandocExecutableName, runtimeInfo.LinuxPackageType)) { if (!File.Exists(candidate)) continue; @@ -250,7 +252,7 @@ public sealed class PandocProcessBuilder /// public static string PandocExecutableName => CPU_ARCHITECTURE is RID.WIN_ARM64 or RID.WIN_X64 ? "pandoc.exe" : "pandoc"; - private static IEnumerable SystemPandocExecutableCandidates(string executableName) + private static IEnumerable SystemPandocExecutableCandidates(string executableName, string linuxPackageType) { var candidates = new List(); @@ -269,6 +271,9 @@ public sealed class PandocProcessBuilder break; case RID.LINUX_X64 or RID.LINUX_ARM64: + if (string.Equals(linuxPackageType, "flatpak", StringComparison.OrdinalIgnoreCase)) + AddCandidate(candidates, FLATPAK_PANDOC_PLUGIN_BIN_DIRECTORY, executableName); + AddCandidate(candidates, "/usr/local/bin", executableName); AddCandidate(candidates, "/usr/bin", executableName); AddCandidate(candidates, "/snap/bin", executableName); diff --git a/app/MindWork AI Studio/Tools/PluginSystem/ILivePluginContent.cs b/app/MindWork AI Studio/Tools/PluginSystem/ILivePluginContent.cs new file mode 100644 index 00000000..4ad5a261 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/ILivePluginContent.cs @@ -0,0 +1,18 @@ +namespace AIStudio.Tools.PluginSystem; + +/// +/// Represents complex content from a configuration plugin that is read live from +/// running plugins and is not persisted to the settings data model. +/// +public interface ILivePluginContent +{ + /// + /// The stable ID of the live plugin content. + /// + public string Id { get; } + + /// + /// The ID of the enterprise configuration plugin that provides this content. + /// + public Guid EnterpriseConfigurationPluginId { get; } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs index a5f744ce..f6411589 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs @@ -9,11 +9,12 @@ namespace AIStudio.Tools.PluginSystem; public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginType type) : PluginBase(isInternal, state, type) { private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(PluginConfiguration).Namespace, nameof(PluginConfiguration)); - private static readonly SettingsManager SETTINGS_MANAGER = Program.SERVICE_PROVIDER.GetRequiredService(); + private static SettingsManager SettingsManagerAccess => Program.SERVICE_PROVIDER.GetRequiredService(); private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger(nameof(PluginConfiguration)); private List configObjects = []; private List mandatoryInfos = []; + private List introductions = []; /// /// 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 /// /// The list of mandatory infos provided by this configuration plugin. + /// Mandatory infos are live plugin content and are not persisted to ConfigurationData. /// public IReadOnlyList MandatoryInfos => this.mandatoryInfos; + /// + /// The list of introductions provided by this configuration plugin. + /// Introductions are live plugin content and are not persisted to ConfigurationData. + /// + public IReadOnlyList Introductions => this.introductions; + /// /// True/false when explicitly configured in the plugin, otherwise null. /// @@ -43,7 +51,7 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT await StoreEnterpriseApiKeysAsync(); await StoreEnterpriseSecretsAsync(); - await SETTINGS_MANAGER.StoreSettings(); + await SettingsManagerAccess.StoreSettings(); await MessageBus.INSTANCE.SendMessage(null, Event.CONFIGURATION_CHANGED); } } @@ -132,6 +140,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(out var mainTable)) @@ -156,6 +165,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); @@ -184,6 +196,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); @@ -208,6 +230,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); @@ -215,6 +240,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); @@ -242,4 +273,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(out var introductionsTable)) + return; + + for (var i = 1; i <= introductionsTable.ArrayLength; i++) + { + var luaIntroductionValue = introductionsTable[i]; + if (!luaIntroductionValue.TryRead(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); + } + } } diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfigurationObject.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfigurationObject.cs index 90ce305f..0a2b3045 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfigurationObject.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfigurationObject.cs @@ -15,8 +15,9 @@ namespace AIStudio.Tools.PluginSystem; /// public sealed record PluginConfigurationObject { - private static readonly RustService RUST_SERVICE = Program.SERVICE_PROVIDER.GetRequiredService(); - private static readonly SettingsManager SETTINGS_MANAGER = Program.SERVICE_PROVIDER.GetRequiredService(); + private static RustService RustService => Program.SERVICE_PROVIDER.GetRequiredService(); + private static SettingsManager SettingsManagerAccess => Program.SERVICE_PROVIDER.GetRequiredService(); + private static ThreadSafeRandom Rng => Program.SERVICE_PROVIDER.GetRequiredService(); private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger(); /// @@ -92,7 +93,8 @@ public sealed record PluginConfigurationObject return false; } - var storedObjects = configObjectSelection.Compile()(SETTINGS_MANAGER.ConfigurationData); + var localSettingsManager = SettingsManagerAccess; + var storedObjects = configObjectSelection.Compile()(localSettingsManager.ConfigurationData); var numberObjects = luaTable.ArrayLength; ThreadSafeRandom? random = null; for (var i = 1; i <= numberObjects; i++) @@ -142,7 +144,7 @@ public sealed record PluginConfigurationObject // Case: The object does not exist, we have to add it else { - if (nextConfigObjectNumSelection.TryIncrement(SETTINGS_MANAGER.ConfigurationData, IncrementType.POST) is { Success: true, UpdatedValue: var nextNum }) + if (nextConfigObjectNumSelection.TryIncrement(localSettingsManager.ConfigurationData, IncrementType.POST) is { Success: true, UpdatedValue: var nextNum }) { // Case: Increment the next number was successful configObject = configObject with { Num = nextNum }; @@ -151,7 +153,7 @@ public sealed record PluginConfigurationObject else { // Case: The next number could not be incremented, we use a random number - random ??= new ThreadSafeRandom(); + random ??= Rng; configObject = configObject with { Num = (uint)random.Next(500_000, 1_000_000) }; storedObjects.Add((TClass)configObject); LOG.LogWarning("The next number for the configuration object '{ConfigObjectName}' (id={ConfigObjectId}) could not be incremented. Using a random number instead (config plugin id: {ConfigPluginId}).", configObject.Name, configObject.Id, configPluginId); @@ -222,7 +224,8 @@ public sealed record PluginConfigurationObject return false; } - var storedObjects = SETTINGS_MANAGER.ConfigurationData.DataSources; + var localSettingsManager = SettingsManagerAccess; + var storedObjects = localSettingsManager.ConfigurationData.DataSources; var numberObjects = luaTable.ArrayLength; ThreadSafeRandom? random = null; for (var i = 1; i <= numberObjects; i++) @@ -259,14 +262,14 @@ public sealed record PluginConfigurationObject } else { - if (IncrementDataSourceNum() is { Success: true, UpdatedValue: var nextNum }) + if (IncrementDataSourceNum(localSettingsManager.ConfigurationData) is { Success: true, UpdatedValue: var nextNum }) { configObject = configObject with { Num = nextNum }; storedObjects.Add(configObject); } else { - random ??= new ThreadSafeRandom(); + random ??= Rng; configObject = configObject with { Num = (uint)random.Next(500_000, 1_000_000) }; storedObjects.Add(configObject); LOG.LogWarning("The next number for the data source '{ConfigObjectName}' (id={ConfigObjectId}) could not be incremented. Using a random number instead (config plugin id: {ConfigPluginId}).", configObject.Name, configObject.Id, configPluginId); @@ -276,9 +279,9 @@ public sealed record PluginConfigurationObject return true; - static IncrementResult IncrementDataSourceNum() + static IncrementResult IncrementDataSourceNum(Data data) { - return ((Expression>)(x => x.NextDataSourceNum)).TryIncrement(SETTINGS_MANAGER.ConfigurationData, IncrementType.POST); + return ((Expression>)(x => x.NextDataSourceNum)).TryIncrement(data, IncrementType.POST); } } @@ -301,7 +304,8 @@ public sealed record PluginConfigurationObject SecretStoreType? secretStoreType = null, bool deleteSecret = false) where TClass : IConfigurationObject { - var configuredObjects = configObjectSelection.Compile()(SETTINGS_MANAGER.ConfigurationData); + var localSettingsManager = SettingsManagerAccess; + var configuredObjects = configObjectSelection.Compile()(localSettingsManager.ConfigurationData); var leftOverObjects = new List(); foreach (var configuredObject in configuredObjects) { @@ -357,7 +361,7 @@ public sealed record PluginConfigurationObject // Delete the API key from the OS keyring if the removed object has one: if(deleteSecret && item is ISecretId regularSecretId) { - var deleteResult = await RUST_SERVICE.DeleteSecret(regularSecretId, secretStoreType ?? SecretStoreType.DATA_SOURCE); + var deleteResult = await RustService.DeleteSecret(regularSecretId, secretStoreType ?? SecretStoreType.DATA_SOURCE); if (deleteResult.Success) LOG.LogInformation($"Successfully deleted secret for removed enterprise object '{item.Name}' from the OS keyring."); else @@ -365,7 +369,7 @@ public sealed record PluginConfigurationObject } else if(secretStoreType is not null && item is ISecretId secretId) { - var deleteResult = await RUST_SERVICE.DeleteAPIKey(secretId, secretStoreType.Value); + var deleteResult = await RustService.DeleteAPIKey(secretId, secretStoreType.Value); if (deleteResult.Success) LOG.LogInformation($"Successfully deleted API key for removed enterprise provider '{item.Name}' from the OS keyring."); else diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs index e076a842..3f74c556 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs @@ -191,7 +191,7 @@ public static partial class PluginFactory wasConfigurationChanged = true; // Check left-over mandatory info acceptances: - if (SETTINGS_MANAGER.ConfigurationData.MandatoryInformation.RemoveLeftOverAcceptances(GetMandatoryInfos())) + if (SettingsManagerAccess.ConfigurationData.MandatoryInformation.RemoveLeftOverAcceptances(GetMandatoryInfos())) wasConfigurationChanged = true; // Check for a preselected provider: @@ -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; @@ -285,7 +322,7 @@ public static partial class PluginFactory if (wasConfigurationChanged) { - await SETTINGS_MANAGER.StoreSettings(); + await SettingsManagerAccess.StoreSettings(); await MessageBus.INSTANCE.SendMessage(null, Event.CONFIGURATION_CHANGED); } } diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Starting.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Starting.cs index 04bf73e3..3df2e224 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Starting.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Starting.cs @@ -64,7 +64,7 @@ public static partial class PluginFactory try { - if (availablePlugin.IsInternal || SETTINGS_MANAGER.IsPluginEnabled(availablePlugin) || availablePlugin.Type == PluginType.CONFIGURATION || availablePlugin.Type == PluginType.ASSISTANT) + if (availablePlugin.IsInternal || SettingsManagerAccess.IsPluginEnabled(availablePlugin) || availablePlugin.Type == PluginType.CONFIGURATION || availablePlugin.Type == PluginType.ASSISTANT) if(await Start(availablePlugin, cancellationToken) is { IsValid: true } plugin) { if (plugin is PluginConfiguration configPlugin) diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs index a707ab06..9efa9e9b 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs @@ -6,7 +6,7 @@ namespace AIStudio.Tools.PluginSystem; public static partial class PluginFactory { private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger(nameof(PluginFactory)); - private static readonly SettingsManager SETTINGS_MANAGER = Program.SERVICE_PROVIDER.GetRequiredService(); + private static SettingsManager SettingsManagerAccess => Program.SERVICE_PROVIDER.GetRequiredService(); private static string DATA_DIR = string.Empty; private static string PLUGINS_ROOT = string.Empty; @@ -136,4 +136,13 @@ public static partial class PluginFactory .SelectMany(plugin => plugin.MandatoryInfos) .ToList(); } + + public static IReadOnlyList GetIntroductions() + { + return RUNNING_PLUGINS + .OfType() + .SelectMany(plugin => plugin.Introductions) + .OrderBy(introduction => introduction.Index) + .ToList(); + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/RAG/IRetrievalContextExtensions.cs b/app/MindWork AI Studio/Tools/RAG/IRetrievalContextExtensions.cs index 74ff4e58..24b1d24e 100644 --- a/app/MindWork AI Studio/Tools/RAG/IRetrievalContextExtensions.cs +++ b/app/MindWork AI Studio/Tools/RAG/IRetrievalContextExtensions.cs @@ -6,7 +6,7 @@ namespace AIStudio.Tools.RAG; public static class IRetrievalContextExtensions { - private static readonly ILogger LOGGER = Program.SERVICE_PROVIDER.GetService>()!; + private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(); public static async Task AsMarkdown(this IReadOnlyList retrievalContexts, StringBuilder? sb = null, CancellationToken token = default) { diff --git a/app/MindWork AI Studio/Tools/Services/DataSourceService.cs b/app/MindWork AI Studio/Tools/Services/DataSourceService.cs index 06804474..dbd8954a 100644 --- a/app/MindWork AI Studio/Tools/Services/DataSourceService.cs +++ b/app/MindWork AI Studio/Tools/Services/DataSourceService.cs @@ -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); } /// @@ -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 GetDataSources(bool usingSelfHostedProvider, IReadOnlyCollection? previousSelectedDataSources = null) + private async Task GetDataSources(bool usingTrustedProvider, IReadOnlyCollection? previousSelectedDataSources = null) { var allDataSources = this.settingsManager.ConfigurationData.DataSources; var filteredDataSources = new List(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 CheckOneDataSource(IDataSource source, bool usingSelfHostedProvider) + private async Task 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; // diff --git a/app/MindWork AI Studio/Tools/Services/RustService.APIKeys.cs b/app/MindWork AI Studio/Tools/Services/RustService.APIKeys.cs index e2a8b88e..7a9a58e0 100644 --- a/app/MindWork AI Studio/Tools/Services/RustService.APIKeys.cs +++ b/app/MindWork AI Studio/Tools/Services/RustService.APIKeys.cs @@ -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 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"; + } + /// /// Try to get the API key for the given secret ID. /// @@ -13,24 +30,55 @@ public sealed partial class RustService /// The requested secret. public async Task 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 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(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 /// The store secret response. public async Task 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 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(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 /// The delete secret response. public async Task 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 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(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; } diff --git a/app/MindWork AI Studio/Tools/Services/TemporaryChatService.cs b/app/MindWork AI Studio/Tools/Services/TemporaryChatService.cs index 90203b2b..3da98ff4 100644 --- a/app/MindWork AI Studio/Tools/Services/TemporaryChatService.cs +++ b/app/MindWork AI Studio/Tools/Services/TemporaryChatService.cs @@ -17,7 +17,6 @@ public sealed class TemporaryChatService(ILogger logger, S logger.LogInformation("The temporary chat maintenance service was initialized."); - await settingsManager.LoadSettings(); if(settingsManager.ConfigurationData.Workspace.StorageTemporaryMaintenancePolicy is WorkspaceStorageTemporaryMaintenancePolicy.NO_AUTOMATIC_MAINTENANCE) { logger.LogWarning("Automatic maintenance of temporary chat storage is disabled. Exiting maintenance service."); diff --git a/app/MindWork AI Studio/Tools/Validation/ProviderValidation.cs b/app/MindWork AI Studio/Tools/Validation/ProviderValidation.cs index 595eb23e..5b38f4e9 100644 --- a/app/MindWork AI Studio/Tools/Validation/ProviderValidation.cs +++ b/app/MindWork AI Studio/Tools/Validation/ProviderValidation.cs @@ -23,6 +23,7 @@ public sealed class ProviderValidation public Func IsModelProvidedManually { get; init; } = () => false; public Func GetCustomTokenizerValidationIssue { get; init; } = () => string.Empty; + public Func IsModelSelectionHidden { get; init; } = () => false; public string? ValidatingHostname(string hostname) { @@ -78,9 +79,13 @@ public sealed class ProviderValidation if (this.GetProvider() is LLMProviders.NONE) return null; - // For self-hosted llama.cpp or whisper.cpp, no model selection needed + // For self-hosted whisper.cpp, no model selection needed // (model is loaded at startup): - if (this.GetProvider() is LLMProviders.SELF_HOSTED && this.GetHost() is Host.LLAMA_CPP or Host.WHISPER_CPP) + if (this.GetProvider() is LLMProviders.SELF_HOSTED && this.GetHost() is Host.WHISPER_CPP) + return null; + + // For legacy hosts without model selection, no selection validation is needed: + if (this.IsModelSelectionHidden()) return null; // For manually entered models, this validation doesn't apply: diff --git a/app/MindWork AI Studio/packages.lock.json b/app/MindWork AI Studio/packages.lock.json index 65751edc..0a2d8a16 100644 --- a/app/MindWork AI Studio/packages.lock.json +++ b/app/MindWork AI Studio/packages.lock.json @@ -32,18 +32,18 @@ }, "Microsoft.Extensions.FileProviders.Embedded": { "type": "Direct", - "requested": "[9.0.16, )", - "resolved": "9.0.16", - "contentHash": "QRlSWz7zEplBxETrySKK3qpPm/7NPaRGnUpEXQNP3k6Ht2KdVy59JcoUPXlNGnNE3tJd3ycXfMeWqxBG6SyV0w==", + "requested": "[9.0.17, )", + "resolved": "9.0.17", + "contentHash": "ItYX3BajZhWwq1wmvUnYA1jahNi9jyy2BMGzyWPTgdSuay8FfMF0gAfNe8mVE6F+GJaQWymElj8hKimRmGxOzw==", "dependencies": { - "Microsoft.Extensions.FileProviders.Abstractions": "9.0.16" + "Microsoft.Extensions.FileProviders.Abstractions": "9.0.17" } }, "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[9.0.16, )", - "resolved": "9.0.16", - "contentHash": "ccPBYGLPJt8DeJTUzQ0JzOh/iuUAgnjayU63PokVywAhUOx+dzDKSPTL7AG94U/VpvNXflTT2AjsFAIF1+bXBw==" + "requested": "[9.0.17, )", + "resolved": "9.0.17", + "contentHash": "P5qY/hIYMlo0+QRM0W3Gd/SRf20TX+z5W5NwpdzkOk0FtgcbSTNwNcYBRNDgfThFcLpcDFslz65RcGqWOq00/w==" }, "MudBlazor": { "type": "Direct", @@ -159,10 +159,10 @@ }, "Microsoft.Extensions.FileProviders.Abstractions": { "type": "Transitive", - "resolved": "9.0.16", - "contentHash": "/YLSWDs+p0Y4+UGPoWI3uUNq7R5/f/8zw8XeViuhfSTGnPowoqbllBE9aR4TteFgNfIH4IHkhUwSlhMLB0aL8g==", + "resolved": "9.0.17", + "contentHash": "uTkT+/Km0tEPOw9kiLTXJwXlEVQZ5IBxRQm2EvIAwebfKqqaVY/ClkgcZ7FyzzwqFkFmhklWet4Ju4yWRy5jPg==", "dependencies": { - "Microsoft.Extensions.Primitives": "9.0.16" + "Microsoft.Extensions.Primitives": "9.0.17" } }, "Microsoft.Extensions.Localization": { @@ -200,8 +200,8 @@ }, "Microsoft.Extensions.Primitives": { "type": "Transitive", - "resolved": "9.0.16", - "contentHash": "w5RE1MR0lnAElsRJaFd2POIXl/H62aBKmfX8ibYmRmbk0JB9V/9jR0VD5NxiP1ETWpnDAnPguTSe7fF/FdsHEQ==" + "resolved": "9.0.17", + "contentHash": "WBjZ/zeb6PyCLT6lpGSzNtdMyRDloFSPqjY9kIGb5rdSng03rd0+ix/jDEYU6DUjE7JVLuhggXeMONVBxBHEXg==" }, "Microsoft.JSInterop": { "type": "Transitive", diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.6.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.6.1.md index d3fd4a57..8086ea89 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.6.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.6.1.md @@ -1,15 +1,20 @@ -# v26.6.1, build 241 (2026-06-xx xx:xx UTC) +# v26.6.1, build 241 (2026-06-11 13:49 UTC) - Added support for up to 100 thousand enterprise configuration slots, using fixed-width slot names such as `config_00000` while keeping the existing first ten slot names compatible. - Added an enterprise configuration option to hide the quick start guide on the welcome page. - Added support for managed custom root certificate bundles and host allowlists for external HTTPS requests, helping Flatpak deployments connect to organization-internal services with private root CAs while keeping built-in cloud provider endpoints on system trust. - Added support for reading enterprise policy files from a Flatpak provisioning extension. - Added startup path and Linux package type details to the information page to make support easier. - Added the option to search for chats in all workspaces. +- Improved self-hosted llama.cpp providers by loading available models from the server and supporting servers that offer multiple models. Thanks to the GONICUS team for reporting this issue. - Improved workspaces by highlighting the currently open chat in the workspace view. - Improved workspaces by adding a shortcut to start a new chat directly from each workspace row. - Improved workspaces by allowing new workspaces to be created while moving a chat. - Improved voice recording shortcut labels so they match the user's keyboard layout after being configured. - Improved the enterprise configuration details on the information page by showing where each configuration comes from and which configuration slot was used. +- Fixed an issue where newly added profiles and chat templates were not usable until the app was restarted. +- Fixed an issue where renamed chat templates and profiles continued to show their old names in the chat toolbar until the app was restarted. - Fixed workspace creation and renaming to prevent new workspaces from using an existing name. - Fixed an issue on Microsoft Windows where reading attached documents could briefly open a terminal window while processing files. +- Fixed an issue where AI Studio could be started multiple times on Microsoft Windows by launching it from different virtual desktops. +- Fixed an issue where Flatpak installations could not find Pandoc from the bundled plugin extension. - Upgraded dependencies. \ No newline at end of file diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.6.2.md b/app/MindWork AI Studio/wwwroot/changelog/v26.6.2.md new file mode 100644 index 00000000..8e26f706 --- /dev/null +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.6.2.md @@ -0,0 +1,11 @@ +# 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. \ No newline at end of file diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.6.3.md b/app/MindWork AI Studio/wwwroot/changelog/v26.6.3.md new file mode 100644 index 00000000..435b1554 --- /dev/null +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.6.3.md @@ -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. \ No newline at end of file diff --git a/app/SourceCodeRules/SourceCodeRules/AnalyzerReleases.Shipped.md b/app/SourceCodeRules/SourceCodeRules/AnalyzerReleases.Shipped.md index e6f97e74..2d96342e 100644 --- a/app/SourceCodeRules/SourceCodeRules/AnalyzerReleases.Shipped.md +++ b/app/SourceCodeRules/SourceCodeRules/AnalyzerReleases.Shipped.md @@ -11,4 +11,5 @@ MWAIS0005 | Usage | Error | ThisUsageAnalyzer MWAIS0006 | Style | Error | SwitchExpressionMethodAnalyzer MWAIS0007 | Usage | Error | EmptyStringAnalyzer - MWAIS0008 | Naming | Error | LocalConstantsAnalyzer \ No newline at end of file + MWAIS0008 | Naming | Error | LocalConstantsAnalyzer + MWAIS0009 | Usage | Error | StaticServiceProviderCacheAnalyzer \ No newline at end of file diff --git a/app/SourceCodeRules/SourceCodeRules/Identifier.cs b/app/SourceCodeRules/SourceCodeRules/Identifier.cs index aa782cf9..ae9e3b57 100644 --- a/app/SourceCodeRules/SourceCodeRules/Identifier.cs +++ b/app/SourceCodeRules/SourceCodeRules/Identifier.cs @@ -10,4 +10,5 @@ public static class Identifier public const string SWITCH_EXPRESSION_METHOD_ANALYZER = $"{Tools.ID_PREFIX}0006"; public const string EMPTY_STRING_ANALYZER = $"{Tools.ID_PREFIX}0007"; public const string LOCAL_CONSTANTS_ANALYZER = $"{Tools.ID_PREFIX}0008"; + public const string STATIC_SERVICE_PROVIDER_CACHE_ANALYZER = $"{Tools.ID_PREFIX}0009"; } \ No newline at end of file diff --git a/app/SourceCodeRules/SourceCodeRules/UsageAnalyzers/StaticServiceProviderCacheAnalyzer.cs b/app/SourceCodeRules/SourceCodeRules/UsageAnalyzers/StaticServiceProviderCacheAnalyzer.cs new file mode 100644 index 00000000..4cf823db --- /dev/null +++ b/app/SourceCodeRules/SourceCodeRules/UsageAnalyzers/StaticServiceProviderCacheAnalyzer.cs @@ -0,0 +1,159 @@ +using System.Collections.Immutable; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace SourceCodeRules.UsageAnalyzers; + +#pragma warning disable RS1038 +[DiagnosticAnalyzer(LanguageNames.CSharp)] +#pragma warning restore RS1038 +public sealed class StaticServiceProviderCacheAnalyzer : DiagnosticAnalyzer +{ + private const string DIAGNOSTIC_ID = Identifier.STATIC_SERVICE_PROVIDER_CACHE_ANALYZER; + + private static readonly string TITLE = "Services from Program.SERVICE_PROVIDER must not be cached in static state"; + + private static readonly string MESSAGE_FORMAT = "Do not cache services from Program.SERVICE_PROVIDER in static state. Use constructor injection, method-local resolution, or a non-caching get-only property."; + + private static readonly string DESCRIPTION = MESSAGE_FORMAT; + + private const string CATEGORY = "Usage"; + + private static readonly DiagnosticDescriptor RULE = new(DIAGNOSTIC_ID, TITLE, MESSAGE_FORMAT, CATEGORY, DiagnosticSeverity.Error, isEnabledByDefault: true, description: DESCRIPTION); + + public override ImmutableArray SupportedDiagnostics => [RULE]; + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(this.AnalyzeFieldDeclaration, SyntaxKind.FieldDeclaration); + context.RegisterSyntaxNodeAction(this.AnalyzeVariableDeclarator, SyntaxKind.VariableDeclarator); + context.RegisterSyntaxNodeAction(this.AnalyzePropertyDeclaration, SyntaxKind.PropertyDeclaration); + context.RegisterSyntaxNodeAction(this.AnalyzeAssignmentExpression, SyntaxKind.SimpleAssignmentExpression); + } + + private void AnalyzeFieldDeclaration(SyntaxNodeAnalysisContext context) + { + var fieldDeclaration = (FieldDeclarationSyntax)context.Node; + foreach (var variable in fieldDeclaration.Declaration.Variables) + this.AnalyzeStaticFieldInitializer(context, variable); + } + + private void AnalyzeVariableDeclarator(SyntaxNodeAnalysisContext context) + { + var variable = (VariableDeclaratorSyntax)context.Node; + if (variable.Parent?.Parent is FieldDeclarationSyntax) + return; + + this.AnalyzeStaticFieldInitializer(context, variable); + } + + private void AnalyzePropertyDeclaration(SyntaxNodeAnalysisContext context) + { + var propertyDeclaration = (PropertyDeclarationSyntax)context.Node; + if (propertyDeclaration.Initializer is null) + return; + + if (context.SemanticModel.GetDeclaredSymbol(propertyDeclaration) is not { IsStatic: true }) + return; + + if (!this.IsProgramServiceProviderGetCall(propertyDeclaration.Initializer.Value)) + return; + + var diagnostic = Diagnostic.Create(RULE, propertyDeclaration.Initializer.Value.GetLocation()); + context.ReportDiagnostic(diagnostic); + } + + private void AnalyzeAssignmentExpression(SyntaxNodeAnalysisContext context) + { + var assignment = (AssignmentExpressionSyntax)context.Node; + if (!this.IsProgramServiceProviderGetCall(assignment.Right)) + return; + + var targetSymbol = context.SemanticModel.GetSymbolInfo(assignment.Left).Symbol; + if (targetSymbol is not IFieldSymbol { IsStatic: true } && targetSymbol is not IPropertySymbol { IsStatic: true }) + return; + + var diagnostic = Diagnostic.Create(RULE, assignment.Right.GetLocation()); + context.ReportDiagnostic(diagnostic); + } + + private void AnalyzeStaticFieldInitializer(SyntaxNodeAnalysisContext context, VariableDeclaratorSyntax variable) + { + if (variable.Initializer is null) + return; + + if (context.SemanticModel.GetDeclaredSymbol(variable) is not IFieldSymbol { IsStatic: true }) + return; + + if (!this.IsProgramServiceProviderGetCall(variable.Initializer.Value)) + return; + + var diagnostic = Diagnostic.Create(RULE, variable.Initializer.Value.GetLocation()); + context.ReportDiagnostic(diagnostic); + } + + private bool IsProgramServiceProviderGetCall(ExpressionSyntax expression) + { + if (this.UnwrapSimpleExpression(expression) is not InvocationExpressionSyntax invocation) + return false; + + if (this.UnwrapSimpleExpression(invocation.Expression) is not MemberAccessExpressionSyntax memberAccess) + return false; + + if (!this.IsServiceProviderGetMethod(memberAccess.Name)) + return false; + + return this.IsProgramServiceProviderAccess(memberAccess.Expression); + } + + private bool IsServiceProviderGetMethod(SimpleNameSyntax name) => name switch + { + GenericNameSyntax genericName when genericName.TypeArgumentList.Arguments.Count == 1 => + genericName.Identifier.Text is "GetService" or "GetRequiredService", + _ => false, + }; + + private bool IsProgramServiceProviderAccess(ExpressionSyntax expression) + { + if (this.UnwrapSimpleExpression(expression) is not MemberAccessExpressionSyntax memberAccess) + return false; + + if (memberAccess.Name.Identifier.Text != "SERVICE_PROVIDER") + return false; + + return this.UnwrapSimpleExpression(memberAccess.Expression) is IdentifierNameSyntax { Identifier.Text: "Program" }; + } + + private ExpressionSyntax UnwrapSimpleExpression(ExpressionSyntax expression) + { + while (true) + { + switch (expression) + { + case ParenthesizedExpressionSyntax parenthesized: + expression = parenthesized.Expression; + continue; + + case PostfixUnaryExpressionSyntax { RawKind: (int)SyntaxKind.SuppressNullableWarningExpression } postfixUnary: + expression = postfixUnary.Operand; + continue; + + case CastExpressionSyntax castExpression: + expression = castExpression.Expression; + continue; + + case BinaryExpressionSyntax { RawKind: (int)SyntaxKind.AsExpression } asExpression: + expression = asExpression.Left; + continue; + + default: + return expression; + } + } + } +} \ No newline at end of file diff --git a/documentation/Enterprise IT.md b/documentation/Enterprise IT.md index 3d7a9c1b..d5f626d9 100644 --- a/documentation/Enterprise IT.md +++ b/documentation/Enterprise IT.md @@ -129,6 +129,18 @@ Optional encryption secret file: config_encryption_secret: "BASE64..." ``` +Optional custom root certificate policy file: + +- `external_http_custom_root_certificates.yaml` + +```yaml +enabled: true +bundle_path: "/app/etc/MindWorkAI/company-root-cas.pem" +allowed_hosts: "*.intra.example.org;eri.example.org" +``` + +When this file exists and contains a valid `enabled` value, it takes precedence over the custom root certificate environment variables described below. This is useful for Flatpak deployments because a Flatpak provisioning extension can provide the policy file and the PEM bundle together. Set `enabled: false` to explicitly disable additional root certificates and ignore lower-priority environment variables. + ### Environment variable example If you need the fallback environment-variable format, configure the values like this: @@ -172,7 +184,18 @@ If your organization uses private root CAs, place a PEM bundle with the required -----END CERTIFICATE----- ``` -For the first enterprise configuration download, configure these environment variables before AI Studio starts: +For Flatpak deployments, the recommended approach is to provide an enterprise policy file through the Flatpak provisioning extension: + +```yaml +# /app/etc/MindWorkAI/external_http_custom_root_certificates.yaml +enabled: true +bundle_path: "/app/etc/MindWorkAI/company-root-cas.pem" +allowed_hosts: "*.intra.example.org;eri.example.org" +``` + +Place the PEM bundle at the configured path inside the sandbox, for example, through the same provisioning extension. This allows AI Studio to use the additional root certificates during the first enterprise configuration download. + +As a fallback, you can configure these environment variables before AI Studio starts: ```bash MINDWORK_AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATES_ENABLED=true diff --git a/documentation/compatibility-shims/2026-06-qdrant-edge-migration.md b/documentation/compatibility-shims/2026-06-qdrant-edge-migration.md new file mode 100644 index 00000000..6aadeba5 --- /dev/null +++ b/documentation/compatibility-shims/2026-06-qdrant-edge-migration.md @@ -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`. \ No newline at end of file diff --git a/documentation/compatibility-shims/2026-06-self-hosted-secret-id.md b/documentation/compatibility-shims/2026-06-self-hosted-secret-id.md new file mode 100644 index 00000000..8e38a2c1 --- /dev/null +++ b/documentation/compatibility-shims/2026-06-self-hosted-secret-id.md @@ -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`. \ No newline at end of file diff --git a/documentation/compatibility-shims/README.md b/documentation/compatibility-shims/README.md new file mode 100644 index 00000000..9730512f --- /dev/null +++ b/documentation/compatibility-shims/README.md @@ -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. +``` diff --git a/metadata.txt b/metadata.txt index 7883dc5d..6259022c 100644 --- a/metadata.txt +++ b/metadata.txt @@ -1,12 +1,12 @@ -26.5.5 -2026-05-25 18:52:12 UTC -240 -9.0.117 (commit 6e241a69c1) -9.0.16 (commit a1e6809fb8) +26.6.2 +2026-06-21 14:07:27 UTC +242 +9.0.118 (commit c8cbca4ed1) +9.0.17 (commit f2c8152eed) 1.96.0 (commit ac68faa20) 8.15.0 2.11.2 -d05ff26e628, release +64e91ff4ffd, release osx-arm64 148.0.7763.0 -0.6.1 \ No newline at end of file +0.7.2 \ No newline at end of file diff --git a/runtime/Cargo.lock b/runtime/Cargo.lock index ffc6b325..082189f7 100644 --- a/runtime/Cargo.lock +++ b/runtime/Cargo.lock @@ -214,7 +214,7 @@ dependencies = [ "objc2-foundation 0.3.2", "parking_lot", "percent-encoding", - "windows-sys 0.59.0", + "windows-sys 0.60.2", "x11rb", ] @@ -1769,7 +1769,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2066,7 +2066,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3828,7 +3828,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -4000,7 +4000,7 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "mindwork-ai-studio" -version = "26.5.5" +version = "26.6.2" dependencies = [ "aes 0.9.1", "apple-native-keyring-store", @@ -4041,6 +4041,7 @@ dependencies = [ "tauri-plugin-global-shortcut", "tauri-plugin-opener", "tauri-plugin-shell", + "tauri-plugin-single-instance", "tauri-plugin-updater", "tauri-plugin-window-state", "tempfile", @@ -4350,7 +4351,7 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ - "proc-macro-crate 1.3.1", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", "syn 2.0.117", @@ -5393,7 +5394,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -5813,7 +5814,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -5872,7 +5873,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -7005,6 +7006,21 @@ dependencies = [ "tokio", ] +[[package]] +name = "tauri-plugin-single-instance" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8f29386f5e9fdc699182388a33ee80a56de436d91b67459e86afef426282af" +dependencies = [ + "serde", + "serde_json", + "tauri", + "thiserror 2.0.18", + "tracing", + "windows-sys 0.60.2", + "zbus", +] + [[package]] name = "tauri-plugin-updater" version = "2.10.1" @@ -7163,7 +7179,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix 1.1.4", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 457d1f04..e522dd87 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mindwork-ai-studio" -version = "26.5.5" +version = "26.6.2" edition = "2024" description = "MindWork AI Studio" authors = ["Thorsten Sommer"] @@ -14,6 +14,7 @@ tauri-plugin-window-state = { version = "2.4.1" } tauri-plugin-shell = "2.3.5" tauri-plugin-dialog = "2.7.1" tauri-plugin-opener = "2.5.4" +tauri-plugin-single-instance = "2" serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.150" keyring-core = "1.0.0" diff --git a/runtime/src/app_window.rs b/runtime/src/app_window.rs index 7f2fe904..cc913248 100644 --- a/runtime/src/app_window.rs +++ b/runtime/src/app_window.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; use std::convert::Infallible; +use std::path::{Path, PathBuf}; use std::sync::Mutex; use std::time::Duration; use async_stream::stream; @@ -10,6 +11,7 @@ use axum::Json; use bytes::Bytes; use log::{debug, error, info, trace, warn}; use once_cell::sync::Lazy; +use pdfium_render::prelude::Pdfium; use serde::{Deserialize, Serialize}; use strum_macros::Display; use tauri::{DragDropEvent,RunEvent, Manager, WindowEvent, generate_context}; @@ -86,6 +88,26 @@ pub fn start_tauri() { }); let app = tauri::Builder::default() + .plugin(tauri_plugin_single_instance::init(|app, args, cwd| { + info!(Source = "Tauri"; "Prevented second app instance from starting. cwd='{cwd}', args={args:?}"); + + let Some(window) = app.get_webview_window("main") else { + warn!(Source = "Tauri"; "Second app instance was blocked, but the main window was not available for activation."); + return; + }; + + if let Err(error) = window.show() { + warn!(Source = "Tauri"; "Failed to show main window after second app start: {error}"); + } + + if let Err(error) = window.unminimize() { + warn!(Source = "Tauri"; "Failed to unminimize main window after second app start: {error}"); + } + + if let Err(error) = window.set_focus() { + warn!(Source = "Tauri"; "Failed to focus main window after second app start: {error}"); + } + })) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_opener::init()) @@ -955,19 +977,9 @@ fn set_pdfium_path(path_resolver: &PathResolver) { } }; - let candidate_paths = [ - resource_dir.join("resources").join("libraries"), - resource_dir.join("libraries"), - ]; - - let pdfium_source_path = candidate_paths - .iter() - .find(|path| path.exists()) - .map(|path| path.to_string_lossy().to_string()); - - match pdfium_source_path { + match select_pdfium_library_directory(&resource_dir) { Some(path) => { - *PDFIUM_LIB_PATH.lock().unwrap() = Some(path); + *PDFIUM_LIB_PATH.lock().unwrap() = Some(path.to_string_lossy().to_string()); } None => { error!(Source = "Bootloader Tauri"; "Failed to set the PDFium library path."); @@ -975,9 +987,76 @@ fn set_pdfium_path(path_resolver: &PathResolver) { } } +fn select_pdfium_library_directory(resource_dir: &Path) -> Option { + let candidate_paths = [ + resource_dir.join("resources").join("libraries"), + resource_dir.join("libraries"), + ]; + + for path in candidate_paths { + let pdfium_library_path = Pdfium::pdfium_platform_library_name_at_path(&path); + if pdfium_library_path.exists() { + return Some(path); + } + + if path.exists() { + warn!( + Source = "Bootloader Tauri"; + "PDFium library directory exists, but the library file was not found at '{path}'.", + path = pdfium_library_path.to_string_lossy(), + ); + } + } + + None +} + #[cfg(test)] mod tests { use super::*; + use std::fs; + + #[test] + fn pdfium_library_directory_prefers_resources_libraries() { + let temp_dir = tempfile::tempdir().unwrap(); + let resources_libraries = temp_dir.path().join("resources").join("libraries"); + let libraries = temp_dir.path().join("libraries"); + create_pdfium_library_in(&resources_libraries); + create_pdfium_library_in(&libraries); + + assert_eq!( + select_pdfium_library_directory(temp_dir.path()), + Some(resources_libraries) + ); + } + + #[test] + fn pdfium_library_directory_falls_back_when_first_directory_has_no_library() { + let temp_dir = tempfile::tempdir().unwrap(); + let resources_libraries = temp_dir.path().join("resources").join("libraries"); + let libraries = temp_dir.path().join("libraries"); + fs::create_dir_all(&resources_libraries).unwrap(); + create_pdfium_library_in(&libraries); + + assert_eq!( + select_pdfium_library_directory(temp_dir.path()), + Some(libraries) + ); + } + + #[test] + fn pdfium_library_directory_requires_library_file() { + let temp_dir = tempfile::tempdir().unwrap(); + fs::create_dir_all(temp_dir.path().join("resources").join("libraries")).unwrap(); + fs::create_dir_all(temp_dir.path().join("libraries")).unwrap(); + + assert_eq!(select_pdfium_library_directory(temp_dir.path()), None); + } + + fn create_pdfium_library_in(path: &Path) { + fs::create_dir_all(path).unwrap(); + fs::File::create(Pdfium::pdfium_platform_library_name_at_path(path)).unwrap(); + } #[test] fn tauri_localhost_is_tauri_asset_url() { diff --git a/runtime/src/dotnet.rs b/runtime/src/dotnet.rs index c5158e13..f269c3ee 100644 --- a/runtime/src/dotnet.rs +++ b/runtime/src/dotnet.rs @@ -13,7 +13,13 @@ use crate::runtime_api_token::API_TOKEN; use crate::app_window::change_location_to; use crate::runtime_certificate::CERTIFICATE_FINGERPRINT; use crate::encryption::ENCRYPTION; -use crate::environment::{is_dev, DATA_DIRECTORY}; +use crate::environment::{ + is_dev, resolve_external_http_custom_root_certificate_policy, DATA_DIRECTORY, + DOTNET_ENV_CUSTOM_ROOT_CERTIFICATE_ALLOWED_HOSTS, + DOTNET_ENV_CUSTOM_ROOT_CERTIFICATE_BUNDLE_PATH, + DOTNET_ENV_CUSTOM_ROOT_CERTIFICATE_POLICY_CONFIGURED, + DOTNET_ENV_CUSTOM_ROOT_CERTIFICATES_ENABLED, +}; use crate::network::get_available_port; use crate::runtime_api::API_SERVER_PORT; use crate::stale_process_cleanup::{kill_stale_process, log_potential_stale_process}; @@ -93,6 +99,20 @@ pub async fn dotnet_port(_token: APIToken) -> String { format!("{dotnet_server_port}") } +fn external_http_custom_root_certificate_policy_environment() -> Vec<(String, String)> { + let policy = resolve_external_http_custom_root_certificate_policy(); + if !policy.is_configured { + return Vec::new(); + } + + vec![ + (String::from(DOTNET_ENV_CUSTOM_ROOT_CERTIFICATE_POLICY_CONFIGURED), String::from("true")), + (String::from(DOTNET_ENV_CUSTOM_ROOT_CERTIFICATES_ENABLED), policy.enabled.to_string()), + (String::from(DOTNET_ENV_CUSTOM_ROOT_CERTIFICATE_BUNDLE_PATH), policy.bundle_path), + (String::from(DOTNET_ENV_CUSTOM_ROOT_CERTIFICATE_ALLOWED_HOSTS), policy.allowed_hosts), + ] +} + /// Creates the startup environment file for the .NET server in the development /// environment. The file is created in the root directory of the repository. /// Creating that env file on a production environment would be a security @@ -113,18 +133,18 @@ pub fn create_startup_env_file() { warn!(Source = "Bootloader .NET"; "Development environment detected; create the startup env file at '../startup.env'."); let env_file_path = std::path::PathBuf::from("..").join("startup.env"); let mut env_file = std::fs::File::create(env_file_path).unwrap(); - let env_file_content = format!( - "AI_STUDIO_SECRET_PASSWORD={secret_password}\n\ - AI_STUDIO_SECRET_KEY_SALT={secret_key_salt}\n\ - AI_STUDIO_CERTIFICATE_FINGERPRINT={cert_fingerprint}\n\ - AI_STUDIO_API_PORT={api_port}\n\ - AI_STUDIO_API_TOKEN={api_token}", + let mut env_file_lines = vec![ + format!("AI_STUDIO_SECRET_PASSWORD={secret_password}"), + format!("AI_STUDIO_SECRET_KEY_SALT={secret_key_salt}"), + format!("AI_STUDIO_CERTIFICATE_FINGERPRINT={}", CERTIFICATE_FINGERPRINT.get().unwrap()), + format!("AI_STUDIO_API_PORT={api_port}"), + format!("AI_STUDIO_API_TOKEN={}", API_TOKEN.to_hex_text()), + ]; + for (key, value) in external_http_custom_root_certificate_policy_environment() { + env_file_lines.push(format!("{key}={value}")); + } - cert_fingerprint = CERTIFICATE_FINGERPRINT.get().unwrap(), - api_token = API_TOKEN.to_hex_text() - ); - - std::io::Write::write_all(&mut env_file, env_file_content.as_bytes()).unwrap(); + std::io::Write::write_all(&mut env_file, env_file_lines.join("\n").as_bytes()).unwrap(); info!(Source = "Bootloader .NET"; "The startup env file was created successfully."); } @@ -136,13 +156,14 @@ pub fn start_dotnet_server(app_handle: tauri::AppHandle) { let secret_key_salt = BASE64_STANDARD.encode(ENCRYPTION.secret_key_salt); let api_port = *API_SERVER_PORT; - let dotnet_server_environment: HashMap = HashMap::from_iter([ + let mut dotnet_server_environment: HashMap = HashMap::from_iter([ (String::from("AI_STUDIO_SECRET_PASSWORD"), secret_password), (String::from("AI_STUDIO_SECRET_KEY_SALT"), secret_key_salt), (String::from("AI_STUDIO_CERTIFICATE_FINGERPRINT"), CERTIFICATE_FINGERPRINT.get().unwrap().to_string()), (String::from("AI_STUDIO_API_PORT"), format!("{api_port}")), (String::from("AI_STUDIO_API_TOKEN"), API_TOKEN.to_hex_text().to_string()), ]); + dotnet_server_environment.extend(external_http_custom_root_certificate_policy_environment()); info!("Try to start the .NET server..."); let server_spawn_clone = DOTNET_SERVER.clone(); diff --git a/runtime/src/environment.rs b/runtime/src/environment.rs index 400b2fa8..8da33ced 100644 --- a/runtime/src/environment.rs +++ b/runtime/src/environment.rs @@ -21,6 +21,12 @@ const ENTERPRISE_CONFIG_SERVER_URL_KEY_PREFIX: &str = "config_server_url"; const ENTERPRISE_REGISTRY_KEY_PATH: &str = r"Software\github\MindWork AI Studio\Enterprise IT"; const ENTERPRISE_POLICY_SECRET_FILE_NAME: &str = "config_encryption_secret.yaml"; +const EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATE_POLICY_FILE_NAME: &str = "external_http_custom_root_certificates.yaml"; + +pub const DOTNET_ENV_CUSTOM_ROOT_CERTIFICATE_POLICY_CONFIGURED: &str = "AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATES_POLICY_CONFIGURED"; +pub const DOTNET_ENV_CUSTOM_ROOT_CERTIFICATES_ENABLED: &str = "AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATES_ENABLED"; +pub const DOTNET_ENV_CUSTOM_ROOT_CERTIFICATE_BUNDLE_PATH: &str = "AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATE_BUNDLE_PATH"; +pub const DOTNET_ENV_CUSTOM_ROOT_CERTIFICATE_ALLOWED_HOSTS: &str = "AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATE_ALLOWED_HOSTS"; #[cfg(any(target_os = "linux", test))] const FLATPAK_ENTERPRISE_POLICY_DIRECTORY: &str = "/app/etc/MindWorkAI"; @@ -99,13 +105,18 @@ fn detect_linux_package_type() -> &'static str { } #[cfg(target_os = "linux")] -fn is_flatpak() -> bool { +pub(crate) fn is_flatpak() -> bool { env_var_has_value("FLATPAK_ID") || Path::new("/.flatpak-info").is_file() || env::var("container") .is_ok_and(|value| value.trim().eq_ignore_ascii_case("flatpak")) } +#[cfg(not(target_os = "linux"))] +pub(crate) fn is_flatpak() -> bool { + false +} + #[cfg(target_os = "linux")] fn is_appimage() -> bool { env_var_has_value("APPIMAGE") || env_var_has_value("APPDIR") @@ -257,6 +268,15 @@ pub struct EnterpriseConfig { pub slot: String, } +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct ExternalHttpCustomRootCertificatePolicy { + pub is_configured: bool, + pub enabled: bool, + pub bundle_path: String, + pub allowed_hosts: String, + pub source_detail: String, +} + #[derive(Clone, Debug, PartialEq, Eq)] struct EnterpriseSourceValue { value: String, @@ -337,6 +357,10 @@ pub async fn read_enterprise_configs(_token: APIToken) -> Json ExternalHttpCustomRootCertificatePolicy { + load_external_http_custom_root_certificate_policy_from_directories(&enterprise_policy_directories()) +} + fn resolve_effective_enterprise_config_source() -> EnterpriseSourceData { select_effective_enterprise_config_source(gather_enterprise_sources()) } @@ -646,6 +670,54 @@ fn load_policy_values_from_directories(directories: &[PathBuf]) -> EnterpriseSou values } +fn load_external_http_custom_root_certificate_policy_from_directories(directories: &[PathBuf]) -> ExternalHttpCustomRootCertificatePolicy { + for directory in directories { + let path = directory.join(EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATE_POLICY_FILE_NAME); + let Some(values) = read_policy_yaml_mapping(&path) else { + continue; + }; + + if let Some(policy) = parse_external_http_custom_root_certificate_policy(&path, &values) { + info!("Using external HTTP custom root certificate policy from '{}'.", policy.source_detail); + return policy; + } + } + + ExternalHttpCustomRootCertificatePolicy::default() +} + +fn parse_external_http_custom_root_certificate_policy(path: &Path, values: &HashMap) -> Option { + let Some(raw_enabled) = values.get("enabled") else { + warn!("Ignoring external HTTP custom root certificate policy '{}': missing 'enabled'.", path.display()); + return None; + }; + + let Some(enabled) = parse_policy_boolean_value(raw_enabled) else { + warn!("Ignoring external HTTP custom root certificate policy '{}': invalid 'enabled' value.", path.display()); + return None; + }; + + let source_detail = path + .canonicalize() + .unwrap_or_else(|_| path.to_path_buf()) + .to_string_lossy() + .into_owned(); + + Some(ExternalHttpCustomRootCertificatePolicy { + is_configured: true, + enabled, + bundle_path: values + .get("bundle_path") + .and_then(|value| normalize_enterprise_value(value)) + .unwrap_or_default(), + allowed_hosts: values + .get("allowed_hosts") + .and_then(|value| normalize_enterprise_value(value)) + .unwrap_or_default(), + source_detail, + }) +} + fn enterprise_policy_file_slot_suffix(file_name: &str) -> Option<&str> { let suffix = file_name .strip_prefix("config")? @@ -737,6 +809,25 @@ fn parse_policy_yaml_value(raw_value: &str) -> Option { Some(String::from(trimmed)) } +fn parse_policy_boolean_value(raw_value: &str) -> Option { + let normalized = raw_value.trim(); + if normalized.eq_ignore_ascii_case("true") + || normalized == "1" + || normalized.eq_ignore_ascii_case("yes") + || normalized.eq_ignore_ascii_case("on") { + return Some(true); + } + + if normalized.eq_ignore_ascii_case("false") + || normalized == "0" + || normalized.eq_ignore_ascii_case("no") + || normalized.eq_ignore_ascii_case("off") { + return Some(false); + } + + None +} + fn insert_first_non_empty_value(values: &mut EnterpriseSourceValues, key: &str, raw_value: &str, source_detail: &str) { if let Some(value) = normalize_enterprise_value(raw_value) { values @@ -963,10 +1054,12 @@ fn normalize_enterprise_config_id(value: &str) -> Option { mod tests { use super::{ enterprise_environment_key_name, enterprise_policy_file_slot_suffix, + load_external_http_custom_root_certificate_policy_from_directories, linux_policy_directories_from_xdg, load_policy_values_from_directories, normalize_locale_tag, parse_enterprise_source_values, select_effective_enterprise_config_source, select_effective_enterprise_secret_source, EnterpriseConfig, EnterpriseSourceData, EnterpriseSourceValue, EnterpriseSourceValues, + ExternalHttpCustomRootCertificatePolicy, }; use std::collections::HashMap; use std::fs; @@ -1490,6 +1583,120 @@ mod tests { assert_eq!(source.encryption_secret, "POLICY-SECRET"); } + #[test] + fn load_external_http_custom_root_certificate_policy_uses_first_valid_directory() { + let directory_a = tempdir().unwrap(); + let directory_b = tempdir().unwrap(); + + fs::write( + directory_a.path().join("external_http_custom_root_certificates.yaml"), + "enabled: true\nbundle_path: \"/app/etc/MindWorkAI/company-a.pem\"\nallowed_hosts: \"*.a.example.org;eri.a.example.org\"", + ) + .unwrap(); + fs::write( + directory_b.path().join("external_http_custom_root_certificates.yaml"), + "enabled: true\nbundle_path: \"/app/etc/MindWorkAI/company-b.pem\"\nallowed_hosts: \"*.b.example.org\"", + ) + .unwrap(); + + let policy = load_external_http_custom_root_certificate_policy_from_directories(&[ + directory_a.path().to_path_buf(), + directory_b.path().to_path_buf(), + ]); + + assert_eq!( + policy, + ExternalHttpCustomRootCertificatePolicy { + is_configured: true, + enabled: true, + bundle_path: String::from("/app/etc/MindWorkAI/company-a.pem"), + allowed_hosts: String::from("*.a.example.org;eri.a.example.org"), + source_detail: policy_path(directory_a.path().join("external_http_custom_root_certificates.yaml")), + } + ); + } + + #[test] + fn load_external_http_custom_root_certificate_policy_allows_disabled_policy_to_win() { + let directory_a = tempdir().unwrap(); + let directory_b = tempdir().unwrap(); + + fs::write( + directory_a.path().join("external_http_custom_root_certificates.yaml"), + "enabled: false", + ) + .unwrap(); + fs::write( + directory_b.path().join("external_http_custom_root_certificates.yaml"), + "enabled: true\nbundle_path: \"/app/etc/MindWorkAI/company-b.pem\"\nallowed_hosts: \"*.b.example.org\"", + ) + .unwrap(); + + let policy = load_external_http_custom_root_certificate_policy_from_directories(&[ + directory_a.path().to_path_buf(), + directory_b.path().to_path_buf(), + ]); + + assert_eq!( + policy, + ExternalHttpCustomRootCertificatePolicy { + is_configured: true, + enabled: false, + bundle_path: String::new(), + allowed_hosts: String::new(), + source_detail: policy_path(directory_a.path().join("external_http_custom_root_certificates.yaml")), + } + ); + } + + #[test] + fn load_external_http_custom_root_certificate_policy_skips_invalid_files() { + let directory_a = tempdir().unwrap(); + let directory_b = tempdir().unwrap(); + + fs::write( + directory_a.path().join("external_http_custom_root_certificates.yaml"), + "enabled: maybe\nbundle_path: \"/app/etc/MindWorkAI/ignored.pem\"", + ) + .unwrap(); + fs::write( + directory_b.path().join("external_http_custom_root_certificates.yaml"), + "enabled: yes\nbundle_path: \"/app/etc/MindWorkAI/company-b.pem\"\nallowed_hosts: \"*.b.example.org,eri.b.example.org\"", + ) + .unwrap(); + + let policy = load_external_http_custom_root_certificate_policy_from_directories(&[ + directory_a.path().to_path_buf(), + directory_b.path().to_path_buf(), + ]); + + assert_eq!( + policy, + ExternalHttpCustomRootCertificatePolicy { + is_configured: true, + enabled: true, + bundle_path: String::from("/app/etc/MindWorkAI/company-b.pem"), + allowed_hosts: String::from("*.b.example.org,eri.b.example.org"), + source_detail: policy_path(directory_b.path().join("external_http_custom_root_certificates.yaml")), + } + ); + } + + #[test] + fn load_external_http_custom_root_certificate_policy_requires_enabled_key() { + let directory = tempdir().unwrap(); + + fs::write( + directory.path().join("external_http_custom_root_certificates.yaml"), + "bundle_path: \"/app/etc/MindWorkAI/company.pem\"\nallowed_hosts: \"*.example.org\"", + ) + .unwrap(); + + let policy = load_external_http_custom_root_certificate_policy_from_directories(&[directory.path().to_path_buf()]); + + assert_eq!(policy, ExternalHttpCustomRootCertificatePolicy::default()); + } + #[test] fn load_policy_values_from_directories_ignores_invalid_and_incomplete_files() { let directory = tempdir().unwrap(); diff --git a/runtime/src/pandoc.rs b/runtime/src/pandoc.rs index b49c0c28..b5fffc3d 100644 --- a/runtime/src/pandoc.rs +++ b/runtime/src/pandoc.rs @@ -5,12 +5,13 @@ use std::path::{Path, PathBuf}; use std::sync::OnceLock; use log::{info, warn}; use tokio::process::Command; -use crate::environment::DATA_DIRECTORY; +use crate::environment::{DATA_DIRECTORY, is_flatpak}; use crate::metadata::META_DATA; /// Tracks whether the RID mismatch warning has been logged. static HAS_LOGGED_RID_MISMATCH: OnceLock<()> = OnceLock::new(); static HAS_LOGGED_PANDOC_PATH: OnceLock<()> = OnceLock::new(); +const FLATPAK_PANDOC_PLUGIN_BIN_DIRECTORY: &str = "/app/plugins/pandoc/bin"; /// Microsoft documents CREATE_NO_WINDOW as a process creation flag with value 0x08000000. /// It starts console applications without opening a console window: @@ -186,8 +187,12 @@ impl PandocProcessBuilder { } fn system_pandoc_executable_candidates(executable_name: &str) -> Vec { + Self::system_pandoc_executable_candidates_for(env::consts::OS, executable_name, is_flatpak()) + } + + fn system_pandoc_executable_candidates_for(os: &str, executable_name: &str, include_flatpak_extension: bool) -> Vec { let mut candidates: Vec = Vec::new(); - match env::consts::OS { + match os { "windows" => { Self::push_env_candidate(&mut candidates, "LOCALAPPDATA", &["Pandoc", executable_name]); Self::push_env_candidate(&mut candidates, "ProgramFiles", &["Pandoc", executable_name]); @@ -199,6 +204,9 @@ impl PandocProcessBuilder { candidates.push(PathBuf::from("/usr/bin").join(executable_name)); }, "linux" => { + if include_flatpak_extension { + candidates.push(PathBuf::from(FLATPAK_PANDOC_PLUGIN_BIN_DIRECTORY).join(executable_name)); + } candidates.push(PathBuf::from("/usr/local/bin").join(executable_name)); candidates.push(PathBuf::from("/usr/bin").join(executable_name)); candidates.push(PathBuf::from("/snap/bin").join(executable_name)); @@ -281,4 +289,46 @@ impl PandocProcessBuilder { _ => "pandoc".to_string(), } } +} + +#[cfg(test)] +mod tests { + use super::{FLATPAK_PANDOC_PLUGIN_BIN_DIRECTORY, PandocProcessBuilder}; + use std::fs; + use std::path::PathBuf; + use tempfile::tempdir; + + #[test] + fn linux_candidates_include_flatpak_pandoc_extension_first_when_flatpak() { + let candidates = PandocProcessBuilder::system_pandoc_executable_candidates_for("linux", "pandoc", true); + let flatpak_candidate = PathBuf::from(FLATPAK_PANDOC_PLUGIN_BIN_DIRECTORY).join("pandoc"); + let usr_local_candidate = PathBuf::from("/usr/local/bin").join("pandoc"); + + let flatpak_index = candidates.iter().position(|candidate| candidate == &flatpak_candidate).unwrap(); + let usr_local_index = candidates.iter().position(|candidate| candidate == &usr_local_candidate).unwrap(); + + assert!(flatpak_index < usr_local_index); + } + + #[test] + fn linux_candidates_skip_flatpak_pandoc_extension_when_not_flatpak() { + let candidates = PandocProcessBuilder::system_pandoc_executable_candidates_for("linux", "pandoc", false); + let flatpak_candidate = PathBuf::from(FLATPAK_PANDOC_PLUGIN_BIN_DIRECTORY).join("pandoc"); + + assert!(!candidates.contains(&flatpak_candidate)); + } + + #[test] + fn local_pandoc_search_finds_data_directory_installation() { + let directory = tempdir().unwrap(); + let pandoc_directory = directory.path().join("pandoc").join("bin"); + fs::create_dir_all(&pandoc_directory).unwrap(); + let pandoc_path = pandoc_directory.join("pandoc"); + fs::File::create(&pandoc_path).unwrap(); + + assert_eq!( + PandocProcessBuilder::find_executable_in_dir(directory.path(), "pandoc").unwrap(), + pandoc_path + ); + } } \ No newline at end of file diff --git a/runtime/src/qdrant_edge_database.rs b/runtime/src/qdrant_edge_database.rs index 89f33bc4..0c495cb4 100644 --- a/runtime/src/qdrant_edge_database.rs +++ b/runtime/src/qdrant_edge_database.rs @@ -384,6 +384,8 @@ fn set_qdrant_edge_unavailable(reason: String) { status.unavailable_reason = Some(reason); } +// Temporary compatibility shim until 2026-12-02: +// documentation/compatibility-shims/2026-06-qdrant-edge-migration.md fn remove_obsolete_qdrant_sidecar_files(app_handle: &tauri::AppHandle) { let mut paths = Vec::new(); diff --git a/runtime/tauri.conf.json b/runtime/tauri.conf.json index e29bb1a4..0896777d 100644 --- a/runtime/tauri.conf.json +++ b/runtime/tauri.conf.json @@ -1,7 +1,7 @@ { "productName": "MindWork AI Studio", "mainBinaryName": "MindWork AI Studio", - "version": "26.5.5", + "version": "26.6.2", "identifier": "com.github.mindwork-ai.ai-studio", "build": {