diff --git a/AGENTS.md b/AGENTS.md index 02078f06..49524e17 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -144,7 +144,7 @@ Multi-level confidence scheme allows users to control which providers see which **Rust:** - Tauri 1.8 - Desktop application framework -- Rocket 0.5 - HTTPS API server +- Rocket - HTTPS API server - tokio - Async runtime - keyring - OS keyring integration - pdfium-render - PDF text extraction @@ -152,7 +152,7 @@ Multi-level confidence scheme allows users to control which providers see which **.NET:** - Blazor Server - UI framework -- MudBlazor 8.12 - Component library +- MudBlazor - Component library - LuaCSharp - Lua scripting engine - HtmlAgilityPack - HTML parsing - ReverseMarkdown - HTML to Markdown conversion @@ -168,7 +168,7 @@ Multi-level confidence scheme allows users to control which providers see which 1. Create changelog file: `app/MindWork AI Studio/wwwroot/changelog/vX.Y.Z.md` 2. Commit changelog -3. Run from `app/Build`: `dotnet run release --action ` +3. Run from `app/Build`: `dotnet run release --action ` 4. Create PR with version bump and changes 5. After PR merge, maintainer creates git tag: `vX.Y.Z` 6. GitHub Actions builds release binaries for all platforms @@ -183,3 +183,22 @@ Multi-level confidence scheme allows users to control which providers see which - **MudBlazor** - Component library requires DI setup in Program.cs - **Encryption** - Initialized before Rust service is marked ready - **Message Bus** - Singleton event bus for cross-component communication inside the .NET app + +## Changelogs +Changelogs are located in `app/MindWork AI Studio/wwwroot/changelog/` with filenames `vX.Y.Z.md`. These changelogs are meant to be for normal end-users +and should be written in a non-technical way, focusing on user-facing changes and improvements. Additionally, changes made regarding the plugin system +should be included in the changelog, especially if they affect how users can configure the app or if they introduce new capabilities for plugins. Plugin +developers should also be informed about these changes, as they might need to update their plugins accordingly. When adding entries to the changelog, +please ensure they are clear and concise, avoiding technical jargon where possible. Each entry starts with a dash and a space (`- `) and one of the +following words: + +- Added +- Improved +- Changed +- Fixed +- Updated +- Removed +- Downgraded +- Upgraded + +The entire changelog is sorted by these categories in the order shown above. The language used for the changelog is US English. \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/AssistantBase.razor b/app/MindWork AI Studio/Assistants/AssistantBase.razor index 5fee5f0a..3268612d 100644 --- a/app/MindWork AI Studio/Assistants/AssistantBase.razor +++ b/app/MindWork AI Studio/Assistants/AssistantBase.razor @@ -80,10 +80,10 @@ @if (!this.FooterButtons.Any(x => x.Type is ButtonTypes.SEND_TO)) { - @if (this.ShowSendTo) + @if (this.ShowSendTo && this.VisibleSendToAssistants.Count > 0) { - @foreach (var assistant in Enum.GetValues().Where(n => n.AllowSendTo()).OrderBy(n => n.Name().Length)) + @foreach (var assistant in this.VisibleSendToAssistants) { @assistant.Name() @@ -112,14 +112,17 @@ break; case SendToButton sendToButton: - - @foreach (var assistant in Enum.GetValues().Where(n => n.AllowSendTo()).OrderBy(n => n.Name().Length)) - { - - @assistant.Name() - - } - + @if (this.VisibleSendToAssistants.Count > 0) + { + + @foreach (var assistant in this.VisibleSendToAssistants) + { + + @assistant.Name() + + } + + } break; } } @@ -147,6 +150,9 @@ { } + + + diff --git a/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs b/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs index a91f2b57..632722ab 100644 --- a/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs +++ b/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs @@ -105,6 +105,13 @@ public abstract partial class AssistantBase : AssistantLowerBase wher protected override async Task OnInitializedAsync() { await base.OnInitializedAsync(); + + if (!this.SettingsManager.IsAssistantVisible(this.Component, assistantName: this.Title)) + { + this.Logger.LogInformation("Assistant '{AssistantTitle}' is hidden. Redirecting to the assistants overview.", this.Title); + this.NavigationManager.NavigateTo(Routes.ASSISTANTS); + return; + } this.formChangeTimer.AutoReset = false; this.formChangeTimer.Elapsed += async (_, _) => @@ -142,6 +149,11 @@ 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 IReadOnlyList VisibleSendToAssistants => Enum.GetValues() + .Where(this.CanSendToAssistant) + .OrderBy(component => component.Name().Length) + .ToArray(); protected string? ValidatingProvider(AIStudio.Settings.Provider provider) { @@ -339,7 +351,7 @@ public abstract partial class AssistantBase : AssistantLowerBase wher protected Task SendToAssistant(Tools.Components destination, SendToButton sendToButton) { - if (!destination.AllowSendTo()) + if (!this.CanSendToAssistant(destination)) return Task.CompletedTask; var contentToSend = sendToButton == default ? string.Empty : sendToButton.UseResultingContentBlockData switch @@ -369,6 +381,14 @@ public abstract partial class AssistantBase : AssistantLowerBase wher this.NavigationManager.NavigateTo(sendToData.Route); return Task.CompletedTask; } + + private bool CanSendToAssistant(Tools.Components component) + { + if (!component.AllowSendTo()) + return false; + + return this.SettingsManager.IsAssistantVisible(component, withLogging: false); + } private async Task InnerResetForm() { diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 559bb05e..6473f2e1 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -1783,6 +1783,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T700666808"] = "Mana -- Available Data Sources UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T86053874"] = "Available Data Sources" +-- LLMs can make mistakes. Check important information. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::HALLUZINATIONREMINDER::T3528806904"] = "LLMs can make mistakes. Check important information." + -- Issues UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ISSUES::T3229841001"] = "Issues" @@ -3547,6 +3550,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1870831108"] = "Failed to l -- Please enter a model name. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1936099896"] = "Please enter a model name." +-- Additional API parameters must form a JSON object. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2051143391"] = "Additional API parameters must form a JSON object." + -- Model UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2189814010"] = "Model" @@ -3568,12 +3574,18 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2842060373"] = "Instance Na -- Show Expert Settings UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T3361153305"] = "Show Expert Settings" +-- Invalid JSON: Add the parameters in proper JSON formatting, e.g., \"temperature\": 0.5. Remove trailing commas. The usual surrounding curly brackets {} must not be used, though. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T3502745319"] = "Invalid JSON: Add the parameters in proper JSON formatting, e.g., \\\"temperature\\\": 0.5. Remove trailing commas. The usual surrounding curly brackets {} must not be used, though." + -- Show available models UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T3763891899"] = "Show available models" -- This host uses the model configured at the provider level. No model selection is available. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T3783329915"] = "This host uses the model configured at the provider level. No model selection is available." +-- Duplicate key '{0}' found. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T3804472591"] = "Duplicate key '{0}' found." + -- Currently, we cannot query the models for the selected provider and/or host. Therefore, please enter the model name manually. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T4116737656"] = "Currently, we cannot query the models for the selected provider and/or host. Therefore, please enter the model name manually." @@ -5305,6 +5317,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3494984593"] = "Tauri is used to -- Motivation UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3563271893"] = "Motivation" +-- not available +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3574465749"] = "not available" + -- This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat." @@ -5326,6 +5341,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3986423270"] = "Check Pandoc Ins -- Versions UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4010195468"] = "Versions" +-- Database +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4036243672"] = "Database" + -- This library is used to create asynchronous streams in Rust. It allows us to work with streams of data that can be produced asynchronously, making it easier to handle events or data that arrive over time. We use this, e.g., to stream arbitrary data from the file system to the embedding system. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4079152443"] = "This library is used to create asynchronous streams in Rust. It allows us to work with streams of data that can be produced asynchronously, making it easier to handle events or data that arrive over time. We use this, e.g., to stream arbitrary data from the file system to the embedding system." @@ -5911,6 +5929,15 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T3893997203"] = " -- Trust all LLM providers UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T4107860491"] = "Trust all LLM providers" +-- Reason +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T1093747001"] = "Reason" + +-- Unavailable +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T3662391977"] = "Unavailable" + +-- Status +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T6222351"] = "Status" + -- Storage size UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T1230141403"] = "Storage size" diff --git a/app/MindWork AI Studio/Chat/FileAttachment.cs b/app/MindWork AI Studio/Chat/FileAttachment.cs index 1208303f..f364ed8f 100644 --- a/app/MindWork AI Studio/Chat/FileAttachment.cs +++ b/app/MindWork AI Studio/Chat/FileAttachment.cs @@ -84,6 +84,9 @@ public record FileAttachment(FileAttachmentType Type, string FileName, string Fi { var extension = Path.GetExtension(filePath).TrimStart('.').ToLowerInvariant(); + if (FileTypeFilter.Executables.FilterExtensions.Contains(extension)) + return FileAttachmentType.FORBIDDEN; + // Check if it's an image file: if (FileTypeFilter.AllImages.FilterExtensions.Contains(extension)) return FileAttachmentType.IMAGE; @@ -96,7 +99,8 @@ public record FileAttachment(FileAttachmentType Type, string FileName, string Fi if (FileTypeFilter.PDF.FilterExtensions.Contains(extension) || FileTypeFilter.Text.FilterExtensions.Contains(extension) || FileTypeFilter.AllOffice.FilterExtensions.Contains(extension) || - FileTypeFilter.AllSourceCode.FilterExtensions.Contains(extension)) + FileTypeFilter.AllSourceCode.FilterExtensions.Contains(extension) || + FileTypeFilter.IsAllowedSourceLikeFileName(filePath)) return FileAttachmentType.DOCUMENT; // All other file types are forbidden: diff --git a/app/MindWork AI Studio/Components/ChatComponent.razor b/app/MindWork AI Studio/Components/ChatComponent.razor index 828d97cb..897f04b4 100644 --- a/app/MindWork AI Studio/Components/ChatComponent.razor +++ b/app/MindWork AI Studio/Components/ChatComponent.razor @@ -148,7 +148,9 @@ } - + + + diff --git a/app/MindWork AI Studio/Components/HalluzinationReminder.razor b/app/MindWork AI Studio/Components/HalluzinationReminder.razor new file mode 100644 index 00000000..d6c1046b --- /dev/null +++ b/app/MindWork AI Studio/Components/HalluzinationReminder.razor @@ -0,0 +1,5 @@ + + + @this.Text + + diff --git a/app/MindWork AI Studio/Components/HalluzinationReminder.razor.cs b/app/MindWork AI Studio/Components/HalluzinationReminder.razor.cs new file mode 100644 index 00000000..9970b0fa --- /dev/null +++ b/app/MindWork AI Studio/Components/HalluzinationReminder.razor.cs @@ -0,0 +1,15 @@ +using AIStudio.Tools.PluginSystem; +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Components; + +public partial class HalluzinationReminder: ComponentBase +{ + private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(HalluzinationReminder).Namespace, nameof(HalluzinationReminder)); + + [Parameter] + public string Text { get; set; } = TB("LLMs can make mistakes. Check important information."); + + [Parameter] + public string ContainerClass { get; set; } = "mt-2 mb-1"; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor index 96e94a2f..4c09da2f 100644 --- a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor +++ b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor @@ -160,7 +160,7 @@ @T("Please be aware: This section is for experts only. You are responsible for verifying the correctness of the additional parameters you provide to the API call. By default, AI Studio uses the OpenAI-compatible chat completions API, when that it is supported by the underlying service and model.") - + @@ -181,4 +181,4 @@ } - \ No newline at end of file + diff --git a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs index 216f49ee..9e84bea8 100644 --- a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs @@ -1,3 +1,6 @@ +using System.Text; +using System.Text.Json; + using AIStudio.Components; using AIStudio.Provider; using AIStudio.Provider.HuggingFace; @@ -334,7 +337,168 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId private void OnInputChangeExpertSettings() { - this.AdditionalJsonApiParameters = this.AdditionalJsonApiParameters.Trim().TrimEnd(',', ' '); + this.AdditionalJsonApiParameters = NormalizeAdditionalJsonApiParameters(this.AdditionalJsonApiParameters) + .Trim() + .TrimEnd(',', ' '); + } + + private string? ValidateAdditionalJsonApiParameters(string additionalParams) + { + if (string.IsNullOrWhiteSpace(additionalParams)) + return null; + + var normalized = NormalizeAdditionalJsonApiParameters(additionalParams); + if (!string.Equals(normalized, additionalParams, StringComparison.Ordinal)) + this.AdditionalJsonApiParameters = normalized; + + var json = $"{{{normalized}}}"; + try + { + if (!this.TryValidateJsonObjectWithDuplicateCheck(json, out var errorMessage)) + return errorMessage; + + return null; + } + catch (JsonException) + { + return T("Invalid JSON: Add the parameters in proper JSON formatting, e.g., \"temperature\": 0.5. Remove trailing commas. The usual surrounding curly brackets {} must not be used, though."); + } + } + + private static string NormalizeAdditionalJsonApiParameters(string input) + { + var sb = new StringBuilder(input.Length); + var inString = false; + var escape = false; + for (var i = 0; i < input.Length; i++) + { + var c = input[i]; + if (inString) + { + sb.Append(c); + if (escape) + { + escape = false; + continue; + } + + if (c == '\\') + { + escape = true; + continue; + } + + if (c == '"') + inString = false; + + continue; + } + + if (c == '"') + { + inString = true; + sb.Append(c); + continue; + } + + if (TryReadToken(input, i, "True", out var tokenLength)) + { + sb.Append("true"); + i += tokenLength - 1; + continue; + } + + if (TryReadToken(input, i, "False", out tokenLength)) + { + sb.Append("false"); + i += tokenLength - 1; + continue; + } + + if (TryReadToken(input, i, "Null", out tokenLength)) + { + sb.Append("null"); + i += tokenLength - 1; + continue; + } + + sb.Append(c); + } + + return sb.ToString(); + } + + private static bool TryReadToken(string input, int startIndex, string token, out int tokenLength) + { + tokenLength = 0; + if (startIndex + token.Length > input.Length) + return false; + + if (!input.AsSpan(startIndex, token.Length).SequenceEqual(token)) + return false; + + var beforeIndex = startIndex - 1; + if (beforeIndex >= 0 && IsIdentifierChar(input[beforeIndex])) + return false; + + var afterIndex = startIndex + token.Length; + if (afterIndex < input.Length && IsIdentifierChar(input[afterIndex])) + return false; + + tokenLength = token.Length; + return true; + } + + private static bool IsIdentifierChar(char c) => char.IsLetterOrDigit(c) || c == '_'; + + private bool TryValidateJsonObjectWithDuplicateCheck(string json, out string? errorMessage) + { + errorMessage = null; + var bytes = Encoding.UTF8.GetBytes(json); + var reader = new Utf8JsonReader(bytes, new JsonReaderOptions + { + AllowTrailingCommas = false, + CommentHandling = JsonCommentHandling.Disallow + }); + + var objectStack = new Stack>(); + while (reader.Read()) + { + switch (reader.TokenType) + { + case JsonTokenType.StartObject: + objectStack.Push(new HashSet(StringComparer.Ordinal)); + break; + + case JsonTokenType.EndObject: + if (objectStack.Count > 0) + objectStack.Pop(); + break; + + case JsonTokenType.PropertyName: + if (objectStack.Count == 0) + { + errorMessage = T("Additional API parameters must form a JSON object."); + return false; + } + + var name = reader.GetString() ?? string.Empty; + if (!objectStack.Peek().Add(name)) + { + errorMessage = string.Format(T("Duplicate key '{0}' found."), name); + return false; + } + break; + } + } + + if (objectStack.Count != 0) + { + errorMessage = T("Invalid JSON: Add the parameters in proper JSON formatting, e.g., \"temperature\": 0.5. Remove trailing commas. The usual surrounding curly brackets {} must not be used, though."); + return false; + } + + return true; } private string GetExpertStyles => this.showExpertSettings ? "border-2 border-dashed rounded pa-2" : string.Empty; @@ -345,4 +509,4 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId "top_p": 0.9, "frequency_penalty": 0.0 """; -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Pages/Information.razor.cs b/app/MindWork AI Studio/Pages/Information.razor.cs index 2027285f..1f3d946e 100644 --- a/app/MindWork AI Studio/Pages/Information.razor.cs +++ b/app/MindWork AI Studio/Pages/Information.razor.cs @@ -58,7 +58,9 @@ public partial class Information : MSGComponentBase private string VersionPdfium => $"{T("Used PDFium version")}: v{META_DATA_LIBRARIES.PdfiumVersion}"; - private string VersionDatabase => $"{T("Database version")}: {this.DatabaseClient.Name} v{META_DATA_DATABASES.DatabaseVersion}"; + private string VersionDatabase => this.DatabaseClient.IsAvailable + ? $"{T("Database version")}: {this.DatabaseClient.Name} v{META_DATA_DATABASES.DatabaseVersion}" + : $"{T("Database")}: {this.DatabaseClient.Name} - {T("not available")}"; private string versionPandoc = TB("Determine Pandoc version, please wait..."); private PandocInstallation pandocInstallation; diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/icon.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/icon.lua index c10dd294..1d3acd2a 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/icon.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/icon.lua @@ -1,9 +1,9 @@ SVG = [[ - + - + 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 0d6fab35..f943abbf 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 @@ -1291,7 +1291,7 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::REWRITEIMPROVE::ASSISTANTREWRITEIMPROVE:: UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::REWRITEIMPROVE::ASSISTANTREWRITEIMPROVE::T1714063121"] = "Satzstruktur" -- Rewrite & Improve Text -UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::REWRITEIMPROVE::ASSISTANTREWRITEIMPROVE::T1994150308"] = "Text umschreiben & verbessern" +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::REWRITEIMPROVE::ASSISTANTREWRITEIMPROVE::T1994150308"] = "Text umformulieren & verbessern" -- Improve your text UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::REWRITEIMPROVE::ASSISTANTREWRITEIMPROVE::T2163831433"] = "Verbessern Sie ihren Text" @@ -1785,6 +1785,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T700666808"] = "Date -- Available Data Sources UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T86053874"] = "Verfügbare Datenquellen" +-- LLMs can make mistakes. Check important information. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::HALLUZINATIONREMINDER::T3528806904"] = "LLMs können Fehler machen. Überprüfen Sie wichtige Informationen." + -- Issues UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ISSUES::T3229841001"] = "Probleme" @@ -2515,7 +2518,7 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T2868740431"] = "Spezifische Anfo UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T2899555955"] = "Wir werden weitere Assistenten für alltägliche Aufgaben entwickeln." -- We're working on offering AI Studio features in your browser via a plugin, allowing, e.g., for spell-checking or text rewriting directly in the browser. -UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T308543246"] = "Wir arbeiten daran, die Funktionen von AI Studio über ein Plugin auch in ihrem Browser anzubieten. So können Sie zum Beispiel direkt im Browser Rechtschreibprüfungen durchführen oder Texte umschreiben lassen." +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T308543246"] = "Wir arbeiten daran, die Funktionen von AI Studio über ein Plugin auch in ihrem Browser anzubieten. So können Sie zum Beispiel direkt im Browser Rechtschreibprüfungen durchführen oder Texte umformulieren lassen." -- There will be an interface for AI Studio to create content in other apps. You could, for example, create blog posts directly on the target platform or add entries to an internal knowledge management tool. This requires development work by the tool developers. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T3290746961"] = "Es wird eine Schnittstelle für AI Studio geben, um Inhalte in anderen Apps zu erstellen. So könnten Sie zum Beispiel Blogbeiträge direkt auf der Zielplattform verfassen oder Einträge zu einem internen Wissensmanagement-Tool hinzufügen. Dafür ist Entwicklungsarbeit durch die jeweiligen Tool-Entwickler erforderlich." @@ -3549,6 +3552,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1870831108"] = "Der API-Sch -- Please enter a model name. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1936099896"] = "Bitte geben Sie einen Modellnamen ein." +-- Additional API parameters must form a JSON object. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2051143391"] = "Zusätzliche API-Parameter müssen ein JSON-Objekt bilden." + -- Model UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2189814010"] = "Modell" @@ -3570,12 +3576,18 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2842060373"] = "Instanzname -- Show Expert Settings UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T3361153305"] = "Experten-Einstellungen anzeigen" +-- Invalid JSON: Add the parameters in proper JSON formatting, e.g., \"temperature\": 0.5. Remove trailing commas. The usual surrounding curly brackets {} must not be used, though. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T3502745319"] = "Ungültiges JSON: Fügen Sie die Parameter in korrektem JSON-Format hinzu, z. B. \"temperature\": 0.5. Entfernen Sie abschließende Kommas. Die üblichen umgebenden geschweiften Klammern {} dürfen jedoch nicht verwendet werden." + -- Show available models UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T3763891899"] = "Verfügbare Modelle anzeigen" -- This host uses the model configured at the provider level. No model selection is available. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T3783329915"] = "Dieser Host verwendet das auf Anbieterebene konfigurierte Modell. Es ist keine Modellauswahl verfügbar." +-- Duplicate key '{0}' found. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T3804472591"] = "Doppelter Schlüssel '{0}' gefunden." + -- Currently, we cannot query the models for the selected provider and/or host. Therefore, please enter the model name manually. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T4116737656"] = "Derzeit können wir die Modelle für den ausgewählten Anbieter und/oder Host nicht abfragen. Bitte geben Sie daher den Modellnamen manuell ein." @@ -4444,7 +4456,7 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGREWRITE::T1462295644 UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGREWRITE::T1621537655"] = "Satzstruktur vorauswählen" -- Assistant: Rewrite & Improve Text Options -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGREWRITE::T1995708818"] = "Assistent: Text umschreiben & verbessern" +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGREWRITE::T1995708818"] = "Assistent: Text umformulieren & verbessern" -- Which voice should be preselected for the sentence structure? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGREWRITE::T2661599097"] = "Welche Stimme soll für die Satzstruktur vorausgewählt werden?" @@ -4453,7 +4465,7 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGREWRITE::T2661599097 UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGREWRITE::T28456020"] = "Wählen Sie einen Schreibstil aus" -- Rewrite & improve text options are preselected -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGREWRITE::T3303192024"] = "Optionen für „Text umschreiben & verbessern“ sind bereits vorausgewählt." +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGREWRITE::T3303192024"] = "Optionen für „Text umformulieren & verbessern“ sind bereits vorausgewählt." -- Close UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGREWRITE::T3448155331"] = "Schließen" @@ -4462,13 +4474,13 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGREWRITE::T3448155331 UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGREWRITE::T3547337928"] = "Welche Zielsprache soll vorausgewählt werden?" -- When enabled, you can preselect the rewrite & improve text options. This is might be useful when you prefer a specific language or LLM model. -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGREWRITE::T3657121735"] = "Wenn diese Option aktiviert ist, können Sie Optionen für „Text umschreiben & verbessern“ im Voraus auswählen. Das kann hilfreich sein, wenn Sie eine bestimmte Sprache oder ein bestimmtes LLM-Modell bevorzugen." +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGREWRITE::T3657121735"] = "Wenn diese Option aktiviert ist, können Sie Optionen für „Text umformulieren & verbessern“ im Voraus auswählen. Das kann hilfreich sein, wenn Sie eine bestimmte Sprache oder ein bestimmtes LLM-Modell bevorzugen." -- Preselect rewrite & improve text options? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGREWRITE::T3745021518"] = "Vorauswahl von Optionen für „Text umschreiben & verbessern“?" +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGREWRITE::T3745021518"] = "Vorauswahl von Optionen für „Text umformulieren & verbessern“?" -- No rewrite & improve text options are preselected -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGREWRITE::T553954963"] = "Keine Optionen für „Text umschreiben & verbessern“ sind vorausgewählt" +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGREWRITE::T553954963"] = "Keine Optionen für „Text umformulieren & verbessern“ sind vorausgewählt" -- When enabled, you can preselect synonym options. This is might be useful when you prefer a specific language or LLM model. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSYNONYMS::T183953912"] = "Wenn diese Option aktiviert ist, können Sie Synonymoptionen im Voraus auswählen. Dies kann nützlich sein, wenn Sie eine bestimmte Sprache oder ein bestimmtes LLM-Modell bevorzugen." @@ -5307,6 +5319,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3494984593"] = "Tauri wird verwe -- Motivation UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3563271893"] = "Motivation" +-- not available +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3574465749"] = "nicht verfügbar" + -- This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "Diese Bibliothek wird verwendet, um Excel- und OpenDocument-Tabellendateien zu lesen. Dies ist zum Beispiel notwendig, wenn Tabellen als Datenquelle für einen Chat verwendet werden sollen." @@ -5328,6 +5343,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3986423270"] = "Pandoc-Installat -- Versions UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4010195468"] = "Versionen" +-- Database +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4036243672"] = "Datenbank" + -- This library is used to create asynchronous streams in Rust. It allows us to work with streams of data that can be produced asynchronously, making it easier to handle events or data that arrive over time. We use this, e.g., to stream arbitrary data from the file system to the embedding system. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4079152443"] = "Diese Bibliothek wird verwendet, um asynchrone Datenströme in Rust zu erstellen. Sie ermöglicht es uns, mit Datenströmen zu arbeiten, die asynchron bereitgestellt werden, wodurch sich Ereignisse oder Daten, die nach und nach eintreffen, leichter verarbeiten lassen. Wir nutzen dies zum Beispiel, um beliebige Daten aus dem Dateisystem an das Einbettungssystem zu übertragen." @@ -5913,6 +5931,15 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T3893997203"] = " -- Trust all LLM providers UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T4107860491"] = "Allen LLM-Anbietern vertrauen" +-- Reason +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T1093747001"] = "Grund" + +-- Unavailable +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T3662391977"] = "Nicht verfügbar" + +-- Status +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T6222351"] = "Status" + -- Storage size UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T1230141403"] = "Speichergröße" diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/icon.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/icon.lua index 75320473..211231ed 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/icon.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/icon.lua @@ -1,12 +1,12 @@ SVG = [[ - + - + - ]] \ No newline at end of file + ]] 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 79a9aceb..029b3b17 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 @@ -1785,6 +1785,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T700666808"] = "Mana -- Available Data Sources UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T86053874"] = "Available Data Sources" +-- LLMs can make mistakes. Check important information. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::HALLUZINATIONREMINDER::T3528806904"] = "LLMs can make mistakes. Check important information." + -- Issues UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ISSUES::T3229841001"] = "Issues" @@ -3549,6 +3552,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1870831108"] = "Failed to l -- Please enter a model name. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T1936099896"] = "Please enter a model name." +-- Additional API parameters must form a JSON object. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2051143391"] = "Additional API parameters must form a JSON object." + -- Model UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2189814010"] = "Model" @@ -3570,12 +3576,18 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T2842060373"] = "Instance Na -- Show Expert Settings UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T3361153305"] = "Show Expert Settings" +-- Invalid JSON: Add the parameters in proper JSON formatting, e.g., \"temperature\": 0.5. Remove trailing commas. The usual surrounding curly brackets {} must not be used, though. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T3502745319"] = "Invalid JSON: Add the parameters in proper JSON formatting, e.g., \\\"temperature\\\": 0.5. Remove trailing commas. The usual surrounding curly brackets {} must not be used, though." + -- Show available models UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T3763891899"] = "Show available models" -- This host uses the model configured at the provider level. No model selection is available. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T3783329915"] = "This host uses the model configured at the provider level. No model selection is available." +-- Duplicate key '{0}' found. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T3804472591"] = "Duplicate key '{0}' found." + -- Currently, we cannot query the models for the selected provider and/or host. Therefore, please enter the model name manually. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROVIDERDIALOG::T4116737656"] = "Currently, we cannot query the models for the selected provider and/or host. Therefore, please enter the model name manually." @@ -5307,6 +5319,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3494984593"] = "Tauri is used to -- Motivation UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3563271893"] = "Motivation" +-- not available +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3574465749"] = "not available" + -- This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat." @@ -5328,6 +5343,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3986423270"] = "Check Pandoc Ins -- Versions UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4010195468"] = "Versions" +-- Database +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4036243672"] = "Database" + -- This library is used to create asynchronous streams in Rust. It allows us to work with streams of data that can be produced asynchronously, making it easier to handle events or data that arrive over time. We use this, e.g., to stream arbitrary data from the file system to the embedding system. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4079152443"] = "This library is used to create asynchronous streams in Rust. It allows us to work with streams of data that can be produced asynchronously, making it easier to handle events or data that arrive over time. We use this, e.g., to stream arbitrary data from the file system to the embedding system." @@ -5913,6 +5931,15 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T3893997203"] = " -- Trust all LLM providers UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T4107860491"] = "Trust all LLM providers" +-- Reason +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T1093747001"] = "Reason" + +-- Unavailable +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T3662391977"] = "Unavailable" + +-- Status +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T6222351"] = "Status" + -- Storage size UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T1230141403"] = "Storage size" diff --git a/app/MindWork AI Studio/Program.cs b/app/MindWork AI Studio/Program.cs index 85e97b07..70cc6831 100644 --- a/app/MindWork AI Studio/Program.cs +++ b/app/MindWork AI Studio/Program.cs @@ -86,37 +86,46 @@ internal sealed class Program } var qdrantInfo = await rust.GetQdrantInfo(); - if (qdrantInfo.Path == string.Empty) + DatabaseClient databaseClient; + if (!qdrantInfo.IsAvailable) { - Console.WriteLine("Error: Failed to get the Qdrant path from Rust."); - return; + Console.WriteLine($"Warning: Qdrant is not available. Starting without vector database. Reason: '{qdrantInfo.UnavailableReason ?? "unknown"}'."); + databaseClient = new NoDatabaseClient("Qdrant", qdrantInfo.UnavailableReason); } - - if (qdrantInfo.PortHttp == 0) + else { - Console.WriteLine("Error: Failed to get the Qdrant HTTP port from Rust."); - return; - } + if (qdrantInfo.Path == string.Empty) + { + Console.WriteLine("Error: Failed to get the Qdrant path from Rust."); + return; + } + + if (qdrantInfo.PortHttp == 0) + { + Console.WriteLine("Error: Failed to get the Qdrant HTTP port from Rust."); + return; + } - if (qdrantInfo.PortGrpc == 0) - { - Console.WriteLine("Error: Failed to get the Qdrant gRPC port from Rust."); - return; - } + if (qdrantInfo.PortGrpc == 0) + { + Console.WriteLine("Error: Failed to get the Qdrant gRPC port from Rust."); + return; + } - if (qdrantInfo.Fingerprint == string.Empty) - { - Console.WriteLine("Error: Failed to get the Qdrant fingerprint from Rust."); - return; - } - - if (qdrantInfo.ApiToken == string.Empty) - { - Console.WriteLine("Error: Failed to get the Qdrant API token from Rust."); - return; - } + if (qdrantInfo.Fingerprint == string.Empty) + { + Console.WriteLine("Error: Failed to get the Qdrant fingerprint from Rust."); + return; + } + + if (qdrantInfo.ApiToken == string.Empty) + { + Console.WriteLine("Error: Failed to get the Qdrant API token from Rust."); + return; + } - var databaseClient = new QdrantClientImplementation("Qdrant", qdrantInfo.Path, qdrantInfo.PortHttp, qdrantInfo.PortGrpc, qdrantInfo.Fingerprint, qdrantInfo.ApiToken); + databaseClient = new QdrantClientImplementation("Qdrant", qdrantInfo.Path, qdrantInfo.PortHttp, qdrantInfo.PortGrpc, qdrantInfo.Fingerprint, qdrantInfo.ApiToken); + } var builder = WebApplication.CreateBuilder(); builder.WebHost.ConfigureKestrel(kestrelServerOptions => diff --git a/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs b/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs index 5eb8fe2b..49a0e6ea 100644 --- a/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs +++ b/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs @@ -29,6 +29,9 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, " // Parse the API parameters: var apiParameters = this.ParseAdditionalApiParameters("system"); + var maxTokens = 4_096; + if (TryPopIntParameter(apiParameters, "max_tokens", out var parsedMaxTokens)) + maxTokens = parsedMaxTokens; // Build the list of messages: var messages = await chatThread.Blocks.BuildMessagesAsync( @@ -73,7 +76,7 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, " Messages = [..messages], System = chatThread.PrepareSystemPrompt(settingsManager), - MaxTokens = apiParameters.TryGetValue("max_tokens", out var value) && value is int intValue ? intValue : 4_096, + MaxTokens = maxTokens, // Right now, we only support streaming completions: Stream = true, @@ -188,4 +191,4 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, " var modelResponse = await response.Content.ReadFromJsonAsync(JSON_SERIALIZER_OPTIONS, token); return modelResponse.Data; } -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Provider/BaseProvider.cs b/app/MindWork AI Studio/Provider/BaseProvider.cs index 4acefc62..9b729824 100644 --- a/app/MindWork AI Studio/Provider/BaseProvider.cs +++ b/app/MindWork AI Studio/Provider/BaseProvider.cs @@ -731,7 +731,7 @@ public abstract class BaseProvider : IProvider, ISecretId /// Optional list of keys to remove from the final dictionary /// (case-insensitive). The parameters stream, model, and messages are removed by default. protected IDictionary ParseAdditionalApiParameters( - params List keysToRemove) + params string[] keysToRemove) { if(string.IsNullOrWhiteSpace(this.AdditionalJsonApiParameters)) return new Dictionary(); @@ -744,14 +744,23 @@ public abstract class BaseProvider : IProvider, ISecretId var dict = ConvertToDictionary(jsonDoc); // Some keys are always removed because we set them: - keysToRemove.Add("stream"); - keysToRemove.Add("model"); - keysToRemove.Add("messages"); + var removeSet = new HashSet(StringComparer.OrdinalIgnoreCase); + if (keysToRemove.Length > 0) + removeSet.UnionWith(keysToRemove); + + removeSet.Add("stream"); + removeSet.Add("model"); + removeSet.Add("messages"); // Remove the specified keys (case-insensitive): - var removeSet = new HashSet(keysToRemove, StringComparer.OrdinalIgnoreCase); - foreach (var key in removeSet) - dict.Remove(key); + if (removeSet.Count > 0) + { + foreach (var key in dict.Keys.ToList()) + { + if (removeSet.Contains(key)) + dict.Remove(key); + } + } return dict; } @@ -761,6 +770,85 @@ public abstract class BaseProvider : IProvider, ISecretId return new Dictionary(); } } + + protected static bool TryPopIntParameter(IDictionary parameters, string key, out int value) + { + value = default; + if (!TryPopParameter(parameters, key, out var raw) || raw is null) + return false; + + switch (raw) + { + case int i: + value = i; + return true; + + case long l when l is >= int.MinValue and <= int.MaxValue: + value = (int)l; + return true; + + case double d when d is >= int.MinValue and <= int.MaxValue: + value = (int)d; + return true; + + case decimal m when m is >= int.MinValue and <= int.MaxValue: + value = (int)m; + return true; + } + + return false; + } + + protected static bool TryPopBoolParameter(IDictionary parameters, string key, out bool value) + { + value = default; + if (!TryPopParameter(parameters, key, out var raw) || raw is null) + return false; + + switch (raw) + { + case bool b: + value = b; + return true; + + case string s when bool.TryParse(s, out var parsed): + value = parsed; + return true; + + case int i: + value = i != 0; + return true; + + case long l: + value = l != 0; + return true; + + case double d: + value = Math.Abs(d) > double.Epsilon; + return true; + + case decimal m: + value = m != 0; + return true; + } + + return false; + } + + private static bool TryPopParameter(IDictionary parameters, string key, out object? value) + { + value = null; + if (parameters.Count == 0) + return false; + + var foundKey = parameters.Keys.FirstOrDefault(k => string.Equals(k, key, StringComparison.OrdinalIgnoreCase)); + if (foundKey is null) + return false; + + value = parameters[foundKey]; + parameters.Remove(foundKey); + return true; + } private static IDictionary ConvertToDictionary(JsonElement element) { @@ -785,4 +873,4 @@ public abstract class BaseProvider : IProvider, ISecretId _ => string.Empty, }; -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Provider/Mistral/ChatRequest.cs b/app/MindWork AI Studio/Provider/Mistral/ChatRequest.cs index 01a45a89..1d42081f 100644 --- a/app/MindWork AI Studio/Provider/Mistral/ChatRequest.cs +++ b/app/MindWork AI Studio/Provider/Mistral/ChatRequest.cs @@ -14,11 +14,12 @@ public readonly record struct ChatRequest( string Model, IList Messages, bool Stream, - int RandomSeed, + [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + int? RandomSeed, bool SafePrompt = false ) { // Attention: The "required" modifier is not supported for [JsonExtensionData]. [JsonExtensionData] public IDictionary AdditionalApiParameters { get; init; } = new Dictionary(); -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs b/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs index f4cb07f4..485729fb 100644 --- a/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs +++ b/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs @@ -36,6 +36,8 @@ public sealed class ProviderMistral() : BaseProvider(LLMProviders.MISTRAL, "http // Parse the API parameters: var apiParameters = this.ParseAdditionalApiParameters(); + var safePrompt = TryPopBoolParameter(apiParameters, "safe_prompt", out var parsedSafePrompt) && parsedSafePrompt; + var randomSeed = TryPopIntParameter(apiParameters, "random_seed", out var parsedRandomSeed) ? parsedRandomSeed : (int?)null; // Build the list of messages: var messages = await chatThread.Blocks.BuildMessagesUsingDirectImageUrlAsync(this.Provider, chatModel); @@ -52,7 +54,8 @@ public sealed class ProviderMistral() : BaseProvider(LLMProviders.MISTRAL, "http // Right now, we only support streaming completions: Stream = true, - SafePrompt = apiParameters.TryGetValue("safe_prompt", out var value) && value is true, + RandomSeed = randomSeed, + SafePrompt = safePrompt, AdditionalApiParameters = apiParameters }, JSON_SERIALIZER_OPTIONS); @@ -165,4 +168,4 @@ public sealed class ProviderMistral() : BaseProvider(LLMProviders.MISTRAL, "http var modelResponse = await response.Content.ReadFromJsonAsync(token); return modelResponse; } -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Settings/Provider.cs b/app/MindWork AI Studio/Settings/Provider.cs index a2a0a0d3..0ccf272c 100644 --- a/app/MindWork AI Studio/Settings/Provider.cs +++ b/app/MindWork AI Studio/Settings/Provider.cs @@ -253,7 +253,7 @@ public sealed record Provider( ["AdditionalJsonApiParameters"] = "{{LuaTools.EscapeLuaString(this.AdditionalJsonApiParameters)}}", ["Model"] = { ["Id"] = "{{LuaTools.EscapeLuaString(this.Model.Id)}}", - ["DisplayName"] = "{{LuaTools.EscapeLuaString(this.Model.DisplayName ?? string.Empty)}}", + ["DisplayName"] = "{{LuaTools.EscapeLuaString(this.Model.DisplayName ?? this.Model.Id)}}", }, } """; diff --git a/app/MindWork AI Studio/Tools/Databases/DatabaseClient.cs b/app/MindWork AI Studio/Tools/Databases/DatabaseClient.cs index b50aafe1..b80cba94 100644 --- a/app/MindWork AI Studio/Tools/Databases/DatabaseClient.cs +++ b/app/MindWork AI Studio/Tools/Databases/DatabaseClient.cs @@ -3,6 +3,8 @@ public abstract class DatabaseClient(string name, string path) { public string Name => name; + + public virtual bool IsAvailable => true; private string Path => path; diff --git a/app/MindWork AI Studio/Tools/Databases/NoDatabaseClient.cs b/app/MindWork AI Studio/Tools/Databases/NoDatabaseClient.cs new file mode 100644 index 00000000..7b3b0cd4 --- /dev/null +++ b/app/MindWork AI Studio/Tools/Databases/NoDatabaseClient.cs @@ -0,0 +1,24 @@ +using AIStudio.Tools.PluginSystem; + +namespace AIStudio.Tools.Databases; + +public sealed class NoDatabaseClient(string name, string? unavailableReason) : DatabaseClient(name, string.Empty) +{ + private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(NoDatabaseClient).Namespace, nameof(NoDatabaseClient)); + + public override bool IsAvailable => false; + + public override async IAsyncEnumerable<(string Label, string Value)> GetDisplayInfo() + { + yield return (TB("Status"), TB("Unavailable")); + + if (!string.IsNullOrWhiteSpace(unavailableReason)) + yield return (TB("Reason"), unavailableReason); + + await Task.CompletedTask; + } + + public override void Dispose() + { + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Databases/Qdrant/QdrantClientImplementation.cs b/app/MindWork AI Studio/Tools/Databases/Qdrant/QdrantClientImplementation.cs index 25f37253..60a13419 100644 --- a/app/MindWork AI Studio/Tools/Databases/Qdrant/QdrantClientImplementation.cs +++ b/app/MindWork AI Studio/Tools/Databases/Qdrant/QdrantClientImplementation.cs @@ -43,8 +43,8 @@ public class QdrantClientImplementation : DatabaseClient private async Task GetVersion() { - var operation = await this.GrpcClient.HealthAsync(); - return "v"+operation.Version; + var operation = await this.GrpcClient.HealthAsync(); + return $"v{operation.Version}"; } private async Task GetCollectionsAmount() diff --git a/app/MindWork AI Studio/Tools/Rust/FileTypeFilter.cs b/app/MindWork AI Studio/Tools/Rust/FileTypeFilter.cs index 03232070..d93f44e0 100644 --- a/app/MindWork AI Studio/Tools/Rust/FileTypeFilter.cs +++ b/app/MindWork AI Studio/Tools/Rust/FileTypeFilter.cs @@ -12,6 +12,51 @@ namespace AIStudio.Tools.Rust; public readonly record struct FileTypeFilter(string FilterName, string[] FilterExtensions) { private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(FileTypeFilter).Namespace, nameof(FileTypeFilter)); + + private static string[] AllowedSourceLikeFileNames => + [ + "Dockerfile", + "Containerfile", + "Jenkinsfile", + "Makefile", + "GNUmakefile", + "Procfile", + "Vagrantfile", + "Tiltfile", + "Justfile", + "Brewfile", + "Caddyfile", + "Gemfile", + "Podfile", + "Fastfile", + "Appfile", + "Rakefile", + "Dangerfile", + "BUILD", + "WORKSPACE", + "BUCK", + ]; + + private static string[] AllowedSourceLikeFileNamePrefixes => + [ + "Dockerfile", + "Containerfile", + "Jenkinsfile", + "Procfile", + "Caddyfile", + ]; + + public static bool IsAllowedSourceLikeFileName(string filePath) + { + var fileName = Path.GetFileName(filePath); + if (string.IsNullOrWhiteSpace(fileName)) + return false; + + if (AllowedSourceLikeFileNames.Any(name => string.Equals(name, fileName, StringComparison.OrdinalIgnoreCase))) + return true; + + return AllowedSourceLikeFileNamePrefixes.Any(prefix => fileName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)); + } public static FileTypeFilter PDF => new(TB("PDF Files"), ["pdf"]); diff --git a/app/MindWork AI Studio/Tools/Rust/QdrantInfo.cs b/app/MindWork AI Studio/Tools/Rust/QdrantInfo.cs index c847235f..5315eca7 100644 --- a/app/MindWork AI Studio/Tools/Rust/QdrantInfo.cs +++ b/app/MindWork AI Studio/Tools/Rust/QdrantInfo.cs @@ -5,6 +5,10 @@ /// public readonly record struct QdrantInfo { + public bool IsAvailable { get; init; } + + public string? UnavailableReason { get; init; } + public string Path { get; init; } public int PortHttp { get; init; } diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md index 7c987227..f7aa87d1 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md @@ -1,4 +1,7 @@ # v26.3.1, build 235 (2026-03-xx xx:xx UTC) +- Added support for the new Qwen 3.5 model family. +- Added a reminder in chats and assistants that LLMs can make mistakes, helping you double-check important information more easily. +- Added the ability to format your user prompt in the chat using icons shortcuts. - Improved the performance by caching the OS language detection and requesting the user language only once per app start. - Improved the chat performance by reducing unnecessary UI updates, making chats smoother and more responsive, especially in longer conversations. - Improved the workspace loading experience: when opening the chat for the first time, your workspaces now appear faster and load step by step in the background, with placeholder rows so the app feels responsive right away. @@ -6,7 +9,8 @@ - Improved the user-language logging by limiting language detection logs to a single entry per app start. - Improved the logbook readability by removing non-readable special characters from log entries. - Improved the logbook reliability by significantly reducing duplicate log entries. -- Fixed an issue where the app could turn white or appear invisible in certain chats after HTML-like content was shown. Thanks Inga for reporting this issue and providing some context on how to reproduce it. -- Upgraded to Rust v1.94.0 -- Added the model capabilities of the new Qwen3.5 model family. -- Added the ability to format your user prompt in the chat using Markdown formatting. \ No newline at end of file +- Improved file attachments in chats: configuration and project files such as `Dockerfile`, `Caddyfile`, `Makefile`, or `Jenkinsfile` are now included more reliably when you send them to the AI. +- Improved the validation of additional API parameters in the advanced provider settings to help catch formatting mistakes earlier. +- Improved the app startup resilience by allowing AI Studio to continue without Qdrant if it fails to initialize. +- Fixed an issue where assistants hidden via configuration plugins still appear in "Send to ..." menus. Thanks, Gunnar, for reporting this issue. +- Fixed an issue where the app could turn white or appear invisible in certain chats after HTML-like content was shown. Thanks, Inga, for reporting this issue and providing some context on how to reproduce it. \ No newline at end of file diff --git a/metadata.txt b/metadata.txt index 6660d464..77e953d5 100644 --- a/metadata.txt +++ b/metadata.txt @@ -3,7 +3,7 @@ 234 9.0.114 (commit 4c5aac3d56) 9.0.13 (commit 9ecbfd4f3f) -1.94.0 (commit 4a4ef493e) +1.93.1 (commit 01f6ddf75) 8.15.0 1.8.1 3eb367d4c9e, release diff --git a/runtime/src/app_window.rs b/runtime/src/app_window.rs index c7e7bd74..0066cfae 100644 --- a/runtime/src/app_window.rs +++ b/runtime/src/app_window.rs @@ -107,16 +107,16 @@ pub fn start_tauri() { DATA_DIRECTORY.set(data_path.to_str().unwrap().to_string()).map_err(|_| error!("Was not able to set the data directory.")).unwrap(); CONFIG_DIRECTORY.set(app.path_resolver().app_config_dir().unwrap().to_str().unwrap().to_string()).map_err(|_| error!("Was not able to set the config directory.")).unwrap(); - cleanup_qdrant(); - cleanup_dotnet_server(); - if is_dev() { #[cfg(debug_assertions)] create_startup_env_file(); } else { + cleanup_dotnet_server(); start_dotnet_server(); } - start_qdrant_server(); + + cleanup_qdrant(); + start_qdrant_server(app.path_resolver()); info!(Source = "Bootloader Tauri"; "Reconfigure the file logger to use the app data directory {data_path:?}"); switch_to_file_logging(data_path).map_err(|e| error!("Failed to switch logging to file: {e}")).unwrap(); @@ -1067,4 +1067,4 @@ fn set_pdfium_path(path_resolver: PathResolver) { let pdfium_source_path = pdfium_source_path.unwrap(); let pdfium_source_path = pdfium_source_path.to_str().unwrap().to_string(); *PDFIUM_LIB_PATH.lock().unwrap() = Some(pdfium_source_path.clone()); -} \ No newline at end of file +} diff --git a/runtime/src/qdrant.rs b/runtime/src/qdrant.rs index 41429431..dff8814f 100644 --- a/runtime/src/qdrant.rs +++ b/runtime/src/qdrant.rs @@ -12,9 +12,10 @@ use rocket::serde::json::Json; use rocket::serde::Serialize; use tauri::api::process::{Command, CommandChild, CommandEvent}; use crate::api_token::{APIToken}; -use crate::environment::DATA_DIRECTORY; +use crate::environment::{is_dev, DATA_DIRECTORY}; use crate::certificate_factory::generate_certificate; use std::path::PathBuf; +use tauri::PathResolver; use tempfile::{TempDir, Builder}; use crate::stale_process_cleanup::{kill_stale_process, log_potential_stale_process}; use crate::sidecar_types::SidecarType; @@ -38,10 +39,24 @@ static API_TOKEN: Lazy = Lazy::new(|| { }); static TMPDIR: Lazy>> = Lazy::new(|| Mutex::new(None)); +static QDRANT_STATUS: Lazy> = Lazy::new(|| Mutex::new(QdrantStatus::default())); const PID_FILE_NAME: &str = "qdrant.pid"; const SIDECAR_TYPE:SidecarType = SidecarType::Qdrant; +#[derive(Default)] +struct QdrantStatus { + is_available: bool, + unavailable_reason: Option, +} + +fn qdrant_base_path() -> PathBuf { + let qdrant_directory = if is_dev() { "qdrant_test" } else { "qdrant" }; + Path::new(DATA_DIRECTORY.get().unwrap()) + .join("databases") + .join(qdrant_directory) +} + #[derive(Serialize)] pub struct ProvideQdrantInfo { path: String, @@ -49,34 +64,62 @@ pub struct ProvideQdrantInfo { port_grpc: u16, fingerprint: String, api_token: String, + is_available: bool, + unavailable_reason: Option, } #[get("/system/qdrant/info")] pub fn qdrant_port(_token: APIToken) -> Json { + let status = QDRANT_STATUS.lock().unwrap(); + let is_available = status.is_available; + let unavailable_reason = status.unavailable_reason.clone(); + Json(ProvideQdrantInfo { - path: Path::new(DATA_DIRECTORY.get().unwrap()).join("databases").join("qdrant").to_str().unwrap().to_string(), - port_http: *QDRANT_SERVER_PORT_HTTP, - port_grpc: *QDRANT_SERVER_PORT_GRPC, - fingerprint: CERTIFICATE_FINGERPRINT.get().expect("Certificate fingerprint not available").to_string(), - api_token: API_TOKEN.to_hex_text().to_string(), + path: if is_available { + qdrant_base_path().to_string_lossy().to_string() + } else { + String::new() + }, + port_http: if is_available { *QDRANT_SERVER_PORT_HTTP } else { 0 }, + port_grpc: if is_available { *QDRANT_SERVER_PORT_GRPC } else { 0 }, + fingerprint: if is_available { + CERTIFICATE_FINGERPRINT.get().cloned().unwrap_or_default() + } else { + String::new() + }, + api_token: if is_available { + API_TOKEN.to_hex_text().to_string() + } else { + String::new() + }, + is_available, + unavailable_reason, }) } /// Starts the Qdrant server in a separate process. -pub fn start_qdrant_server(){ - - let base_path = DATA_DIRECTORY.get().unwrap(); - let path = Path::new(base_path).join("databases").join("qdrant"); +pub fn start_qdrant_server(path_resolver: PathResolver){ + let path = qdrant_base_path(); if !path.exists() { if let Err(e) = fs::create_dir_all(&path){ - error!(Source="Qdrant"; "The required directory to host the Qdrant database could not be created: {}", e.to_string()); + error!(Source="Qdrant"; "The required directory to host the Qdrant database could not be created: {}", e); + set_qdrant_unavailable(format!("The Qdrant data directory could not be created: {e}")); + return; }; } - let (cert_path, key_path) =create_temp_tls_files(&path).unwrap(); - - let storage_path = path.join("storage").to_str().unwrap().to_string(); - let snapshot_path = path.join("snapshots").to_str().unwrap().to_string(); - let init_path = path.join(".qdrant-initalized").to_str().unwrap().to_string(); + + let (cert_path, key_path) = match create_temp_tls_files(&path) { + Ok(paths) => paths, + Err(e) => { + error!(Source="Qdrant"; "TLS files for Qdrant could not be created: {e}"); + set_qdrant_unavailable(format!("TLS files for Qdrant could not be created: {e}")); + return; + } + }; + + let storage_path = path.join("storage").to_string_lossy().to_string(); + let snapshot_path = path.join("snapshots").to_string_lossy().to_string(); + let init_path = path.join(".qdrant-initialized").to_string_lossy().to_string(); let qdrant_server_environment = HashMap::from_iter([ (String::from("QDRANT__SERVICE__HTTP_PORT"), QDRANT_SERVER_PORT_HTTP.to_string()), @@ -84,22 +127,52 @@ pub fn start_qdrant_server(){ (String::from("QDRANT_INIT_FILE_PATH"), init_path), (String::from("QDRANT__STORAGE__STORAGE_PATH"), storage_path), (String::from("QDRANT__STORAGE__SNAPSHOTS_PATH"), snapshot_path), - (String::from("QDRANT__TLS__CERT"), cert_path.to_str().unwrap().to_string()), - (String::from("QDRANT__TLS__KEY"), key_path.to_str().unwrap().to_string()), + (String::from("QDRANT__TLS__CERT"), cert_path.to_string_lossy().to_string()), + (String::from("QDRANT__TLS__KEY"), key_path.to_string_lossy().to_string()), (String::from("QDRANT__SERVICE__ENABLE_TLS"), "true".to_string()), (String::from("QDRANT__SERVICE__API_KEY"), API_TOKEN.to_hex_text().to_string()), ]); let server_spawn_clone = QDRANT_SERVER.clone(); + let qdrant_relative_source_path = "resources/databases/qdrant/config.yaml"; + let qdrant_source_path = match path_resolver.resolve_resource(qdrant_relative_source_path) { + Some(path) => path, + None => { + let reason = format!("The Qdrant config resource '{qdrant_relative_source_path}' could not be resolved."); + error!(Source = "Qdrant"; "{reason} Starting the app without Qdrant."); + set_qdrant_unavailable(reason); + return; + } + }; + + let qdrant_source_path_display = qdrant_source_path.to_string_lossy().to_string(); tauri::async_runtime::spawn(async move { - let (mut rx, child) = Command::new_sidecar("qdrant") - .expect("Failed to create sidecar for Qdrant") - .args(["--config-path", "resources/databases/qdrant/config.yaml"]) + let sidecar = match Command::new_sidecar("qdrant") { + Ok(sidecar) => sidecar, + Err(e) => { + let reason = format!("Failed to create sidecar for Qdrant: {e}"); + error!(Source = "Qdrant"; "{reason}"); + set_qdrant_unavailable(reason); + return; + } + }; + + let (mut rx, child) = match sidecar + .args(["--config-path", qdrant_source_path_display.as_str()]) .envs(qdrant_server_environment) .spawn() - .expect("Failed to spawn Qdrant server process."); + { + Ok(process) => process, + Err(e) => { + let reason = format!("Failed to spawn Qdrant server process with config path '{}': {e}", qdrant_source_path_display); + error!(Source = "Qdrant"; "{reason}"); + set_qdrant_unavailable(reason); + return; + } + }; let server_pid = child.pid(); + set_qdrant_available(); info!(Source = "Bootloader Qdrant"; "Qdrant server process started with PID={server_pid}."); log_potential_stale_process(path.join(PID_FILE_NAME), server_pid, SIDECAR_TYPE); @@ -137,7 +210,10 @@ pub fn stop_qdrant_server() { if let Some(server_process) = QDRANT_SERVER.lock().unwrap().take() { let server_kill_result = server_process.kill(); match server_kill_result { - Ok(_) => warn!(Source = "Qdrant"; "Qdrant server process was stopped."), + Ok(_) => { + set_qdrant_unavailable("Qdrant server was stopped.".to_string()); + warn!(Source = "Qdrant"; "Qdrant server process was stopped.") + }, Err(e) => error!(Source = "Qdrant"; "Failed to stop Qdrant server process: {e}."), } } else { @@ -148,7 +224,7 @@ pub fn stop_qdrant_server() { cleanup_qdrant(); } -/// Create temporary directory with TLS relevant files +/// Create a temporary directory with TLS relevant files pub fn create_temp_tls_files(path: &PathBuf) -> Result<(PathBuf, PathBuf), Box> { let cert = generate_certificate(); @@ -157,10 +233,10 @@ pub fn create_temp_tls_files(path: &PathBuf) -> Result<(PathBuf, PathBuf), Box Result<(), Box> { - let dir_path = Path::new(DATA_DIRECTORY.get().unwrap()).join("databases").join("qdrant"); +fn set_qdrant_available() { + let mut status = QDRANT_STATUS.lock().unwrap(); + status.is_available = true; + status.unavailable_reason = None; +} - if !dir_path.exists() { +fn set_qdrant_unavailable(reason: String) { + let mut status = QDRANT_STATUS.lock().unwrap(); + status.is_available = false; + status.unavailable_reason = Some(reason); +} + +pub fn delete_old_certificates(path: PathBuf) -> Result<(), Box> { + if !path.exists() { return Ok(()); } - for entry in fs::read_dir(dir_path)? { + for entry in fs::read_dir(path)? { let entry = entry?; let path = entry.path(); diff --git a/runtime/tauri.conf.json b/runtime/tauri.conf.json index 27e0aae7..4381d359 100644 --- a/runtime/tauri.conf.json +++ b/runtime/tauri.conf.json @@ -68,7 +68,7 @@ "target/databases/qdrant/qdrant" ], "resources": [ - "resources/*" + "resources/**" ], "macOS": { "exceptionDomain": "localhost"