diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 091faafb..74351c33 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -14,6 +14,8 @@ jobs: read_metadata: name: Read metadata runs-on: ubuntu-latest + permissions: + contents: read outputs: formatted_version: ${{ steps.format_metadata.outputs.formatted_version }} formatted_build_time: ${{ steps.format_metadata.outputs.formatted_build_time }} @@ -80,6 +82,8 @@ jobs: build_main: name: Build app (${{ matrix.dotnet_runtime }}) needs: read_metadata + permissions: + contents: read strategy: fail-fast: true @@ -703,6 +707,7 @@ jobs: runs-on: ubuntu-latest needs: [build_main, read_metadata] if: startsWith(github.ref, 'refs/tags/v') + permissions: {} steps: - name: Create artifact directory run: mkdir -p $GITHUB_WORKSPACE/artifacts diff --git a/AGENTS.md b/AGENTS.md index 02078f06..6bf4eb5f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,6 +29,14 @@ dotnet run build ``` This builds the .NET app as a Tauri "sidecar" binary, which is required even for development. +### Running .NET builds from an agent +- Do not run `.NET` builds such as `dotnet run build`, `dotnet build`, or similar build commands from an agent. Codex agents can hit a known sandbox issue during `.NET` builds, typically surfacing as `CSSM_ModuleLoad()` or other sandbox-related failures. +- Instead, ask the user to run the `.NET` build locally in their IDE and report the result back. +- Recommend the canonical repo build flow for the user: open an IDE terminal in the repository and run `cd app/Build && dotnet run build`. +- If the context fits better, it is also acceptable to ask the user to start the build using their IDE's built-in build action, as long as it is clear the build must be run locally by the user. +- After asking for the build, wait for the user's feedback before diagnosing issues, making follow-up changes, or suggesting the next step. +- Treat the user's build output, error messages, or success confirmation as the source of truth for further troubleshooting. +- For reference: https://github.com/openai/codex/issues/4915 ### Running Tests Currently, no automated test suite exists in the repository. @@ -144,7 +152,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 +160,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 +176,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 @@ -177,9 +185,33 @@ Multi-level confidence scheme allows users to control which providers see which ## Important Development Notes - **File changes require Write/Edit tools** - Never use bash commands like `cat <` +- **End of file formatting** - Do not append an extra empty line at the end of files. - **Spaces in paths** - Always quote paths with spaces in bash commands +- **Agent-run .NET builds** - Do not run `.NET` builds from an agent. Ask the user to run the build locally in their IDE, preferably via `cd app/Build && dotnet run build` in an IDE terminal, then wait for their feedback before continuing. - **Debug environment** - Reads `startup.env` file with IPC credentials - **Production environment** - Runtime launches .NET sidecar with environment variables - **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 +- **Naming conventions** - Constants, enum members, and `static readonly` fields use `UPPER_SNAKE_CASE` such as `MY_CONSTANT`. +- **Empty lines** - Avoid adding extra empty lines at the end of files. + +## 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 +- Released +- 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/README.md b/README.md index 624cbfc8..a594ff41 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Since November 2024: Work on RAG (integration of your data and files) has begun. - [x] ~~Runtime: Extract data from txt / md / pdf / docx / xlsx files (PR [#374](https://github.com/MindWorkAI/AI-Studio/pull/374))~~ - [ ] (*Optional*) Runtime: Implement internal embedding provider through [fastembed-rs](https://github.com/Anush008/fastembed-rs) - [x] ~~App: Implement dialog for checking & handling [pandoc](https://pandoc.org/) installation ([PR #393](https://github.com/MindWorkAI/AI-Studio/pull/393), [PR #487](https://github.com/MindWorkAI/AI-Studio/pull/487))~~ -- [ ] App: Implement external embedding providers +- [x] ~~App: Implement external embedding providers ([PR #654](https://github.com/MindWorkAI/AI-Studio/pull/654))~~ - [ ] App: Implement the process to vectorize one local file using embeddings - [x] ~~Runtime: Integration of the vector database [Qdrant](https://github.com/qdrant/qdrant) ([PR #580](https://github.com/MindWorkAI/AI-Studio/pull/580))~~ - [ ] App: Implement the continuous process of vectorizing data @@ -67,7 +67,7 @@ Since March 2025: We have started developing the plugin system. There will be la - [x] ~~Provide MindWork AI Studio in German ([PR #430](https://github.com/MindWorkAI/AI-Studio/pull/430), [PR #446](https://github.com/MindWorkAI/AI-Studio/pull/446), [PR #451](https://github.com/MindWorkAI/AI-Studio/pull/451), [PR #455](https://github.com/MindWorkAI/AI-Studio/pull/455), [PR #458](https://github.com/MindWorkAI/AI-Studio/pull/458), [PR #462](https://github.com/MindWorkAI/AI-Studio/pull/462), [PR #469](https://github.com/MindWorkAI/AI-Studio/pull/469), [PR #486](https://github.com/MindWorkAI/AI-Studio/pull/486))~~ - [x] ~~Add configuration plugins, which allow pre-defining some LLM providers in organizations ([PR #491](https://github.com/MindWorkAI/AI-Studio/pull/491), [PR #493](https://github.com/MindWorkAI/AI-Studio/pull/493), [PR #494](https://github.com/MindWorkAI/AI-Studio/pull/494), [PR #497](https://github.com/MindWorkAI/AI-Studio/pull/497))~~ - [ ] Add an app store for plugins, showcasing community-contributed plugins from public GitHub and GitLab repositories. This will enable AI Studio users to discover, install, and update plugins directly within the platform. -- [ ] Add assistant plugins +- [ ] Add assistant plugins ([PR #659](https://github.com/MindWorkAI/AI-Studio/pull/659)) @@ -79,6 +79,7 @@ Since March 2025: We have started developing the plugin system. There will be la +- 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. - v26.1.1: Added the option to attach files, including images, to chat templates; added support for source code file attachments in chats and document analysis; added a preview feature for recording your own voice for transcription; fixed various bugs in provider dialogs and profile selection. - v0.10.0: Added support for newer models like Mistral 3 & GPT 5.2, OpenRouter as LLM and embedding provider, the possibility to use file attachments in chats, and support for images as input. - v0.9.51: Added support for [Perplexity](https://www.perplexity.ai/); citations added so that LLMs can provide source references (e.g., some OpenAI models, Perplexity); added support for OpenAI's Responses API so that all text LLMs from OpenAI now work in MindWork AI Studio, including Deep Research models; web searches are now possible (some OpenAI models, Perplexity). @@ -90,7 +91,6 @@ Since March 2025: We have started developing the plugin system. There will be la - v0.9.39: Added the plugin system as a preview feature. - v0.9.31: Added Helmholtz & GWDG as LLM providers. This is a huge improvement for many researchers out there who can use these providers for free. We added DeepSeek as a provider as well. - v0.9.29: Added agents to support the RAG process (selecting the best data sources & validating retrieved data as part of the augmentation process) -- v0.9.26+: Added RAG for external data sources using our [ERI interface](https://mindworkai.org/#eri---external-retrieval-interface) as a preview feature. diff --git a/app/MindWork AI Studio/App.razor b/app/MindWork AI Studio/App.razor index b314b033..7df24793 100644 --- a/app/MindWork AI Studio/App.razor +++ b/app/MindWork AI Studio/App.razor @@ -27,6 +27,7 @@ + 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/DocumentAnalysis/DocumentAnalysisAssistant.razor b/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor index 51dd8f7d..4e7a38ee 100644 --- a/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor +++ b/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor @@ -1,8 +1,8 @@ @attribute [Route(Routes.ASSISTANT_DOCUMENT_ANALYSIS)] @inherits AssistantBaseCore +@using AIStudio.Settings @using AIStudio.Settings.DataModel -
@@ -108,7 +108,7 @@ else - + diff --git a/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor.cs b/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor.cs index f58e6619..419d4c9e 100644 --- a/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor.cs +++ b/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor.cs @@ -176,7 +176,7 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore loadedDocumentPaths = []; private readonly List> availableLLMProviders = new(); @@ -450,14 +450,21 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore x.Id == this.selectedPolicy.PreselectedProfile); + var policyProfile = this.SettingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == policyProfilePreselection.SpecificProfileId); if (policyProfile is not null) return policyProfile; } - return this.SettingsManager.GetPreselectedProfile(this.Component); + return this.SettingsManager.GetAppPreselectedProfile(); } private async Task PolicyMinimumConfidenceWasChangedAsync(ConfidenceLevel level) @@ -479,11 +486,11 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore
- \ No newline at end of file + diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor index cbc33d79..c5ae753f 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor @@ -17,6 +17,7 @@ + diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor.cs index 70b6d24a..a5fbc06b 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor.cs +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor.cs @@ -11,6 +11,15 @@ public partial class SettingsPanelApp : SettingsPanelBase var secret = EnterpriseEncryption.GenerateSecret(); await this.RustService.CopyText2Clipboard(this.Snackbar, secret); } + + private string GetStartPageHelpText() + { + var helpText = T("Choose which page AI Studio should open first when you start the app. Changes take effect the next time you launch AI Studio."); + if (!ManagedConfiguration.TryGet(x => x.App, x => x.StartPage, out var meta) || meta.ManagedMode is not ManagedConfigurationMode.EDITABLE_DEFAULT) + return helpText; + + return $"{helpText} {T("Your organization provided a default start page, but you can still change it.")}"; + } private IEnumerable> GetFilteredTranscriptionProviders() { diff --git a/app/MindWork AI Studio/Components/TreeItemType.cs b/app/MindWork AI Studio/Components/TreeItemType.cs index f43823b4..35028056 100644 --- a/app/MindWork AI Studio/Components/TreeItemType.cs +++ b/app/MindWork AI Studio/Components/TreeItemType.cs @@ -4,6 +4,7 @@ public enum TreeItemType { NONE, + LOADING, CHAT, WORKSPACE, } \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/VoiceRecorder.razor b/app/MindWork AI Studio/Components/VoiceRecorder.razor index e247f439..a99afd14 100644 --- a/app/MindWork AI Studio/Components/VoiceRecorder.razor +++ b/app/MindWork AI Studio/Components/VoiceRecorder.razor @@ -1,9 +1,7 @@ -@using AIStudio.Settings.DataModel - @namespace AIStudio.Components @inherits MSGComponentBase -@if (PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.SettingsManager) && !string.IsNullOrWhiteSpace(this.SettingsManager.ConfigurationData.App.UseTranscriptionProvider)) +@if (this.ShouldRenderVoiceRecording) { @if (this.isTranscribing || this.isPreparing) @@ -16,6 +14,7 @@ ToggledChanged="@this.OnRecordingToggled" Icon="@Icons.Material.Filled.Mic" ToggledIcon="@Icons.Material.Filled.Stop" + Disabled="@(!this.IsVoiceRecordingAvailable)" Color="Color.Primary" ToggledColor="Color.Error"/> } diff --git a/app/MindWork AI Studio/Components/VoiceRecorder.razor.cs b/app/MindWork AI Studio/Components/VoiceRecorder.razor.cs index 73a95e8d..686656dd 100644 --- a/app/MindWork AI Studio/Components/VoiceRecorder.razor.cs +++ b/app/MindWork AI Studio/Components/VoiceRecorder.razor.cs @@ -1,4 +1,5 @@ using AIStudio.Provider; +using AIStudio.Settings.DataModel; using AIStudio.Tools.MIME; using AIStudio.Tools.Rust; using AIStudio.Tools.Services; @@ -21,24 +22,25 @@ public partial class VoiceRecorder : MSGComponentBase [Inject] private ISnackbar Snackbar { get; init; } = null!; + [Inject] + private VoiceRecordingAvailabilityService VoiceRecordingAvailabilityService { get; init; } = null!; + #region Overrides of MSGComponentBase protected override async Task OnInitializedAsync() { // Register for global shortcut events: - this.ApplyFilters([], [Event.TAURI_EVENT_RECEIVED]); + this.ApplyFilters([], [Event.TAURI_EVENT_RECEIVED, Event.VOICE_RECORDING_AVAILABILITY_CHANGED]); await base.OnInitializedAsync(); + } - try - { - // Initialize sound effects. This "warms up" the AudioContext and preloads all sounds for reliable playback: - await this.JsRuntime.InvokeVoidAsync("initSoundEffects"); - } - catch (Exception ex) - { - this.Logger.LogError(ex, "Failed to initialize sound effects."); - } + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender && this.ShouldRenderVoiceRecording) + await this.EnsureSoundEffectsAvailableAsync("during the first interactive render"); + + await base.OnAfterRenderAsync(firstRender); } protected override async Task ProcessIncomingMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default @@ -54,6 +56,10 @@ public partial class VoiceRecorder : MSGComponentBase } break; + + case Event.VOICE_RECORDING_AVAILABILITY_CHANGED: + this.StateHasChanged(); + break; } } @@ -62,6 +68,12 @@ public partial class VoiceRecorder : MSGComponentBase /// private async Task ToggleRecordingFromShortcut() { + if (!this.IsVoiceRecordingAvailable) + { + this.Logger.LogDebug("Ignoring shortcut: voice recording is unavailable in the current session."); + return; + } + // Don't allow toggle if transcription is in progress or preparing: if (this.isTranscribing || this.isPreparing) { @@ -85,27 +97,38 @@ public partial class VoiceRecorder : MSGComponentBase private string? finalRecordingPath; private DotNetObjectReference? dotNetReference; - private string Tooltip => this.isTranscribing - ? T("Transcription in progress...") - : this.isRecording - ? T("Stop recording and start transcription") - : T("Start recording your voice for a transcription"); + private bool ShouldRenderVoiceRecording => PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.SettingsManager) + && !string.IsNullOrWhiteSpace(this.SettingsManager.ConfigurationData.App.UseTranscriptionProvider); + + private bool IsVoiceRecordingAvailable => this.ShouldRenderVoiceRecording + && this.VoiceRecordingAvailabilityService.IsAvailable; + + private string Tooltip => !this.VoiceRecordingAvailabilityService.IsAvailable + ? T("Voice recording is unavailable because the client could not initialize audio playback.") + : this.isTranscribing + ? T("Transcription in progress...") + : this.isRecording + ? T("Stop recording and start transcription") + : T("Start recording your voice for a transcription"); private async Task OnRecordingToggled(bool toggled) { if (toggled) { + if (!this.IsVoiceRecordingAvailable) + { + this.Logger.LogDebug("Ignoring recording start: voice recording is unavailable in the current session."); + return; + } + this.isPreparing = true; this.StateHasChanged(); - - try + + if (!await this.EnsureSoundEffectsAvailableAsync("before starting audio recording")) { - // Warm up sound effects: - await this.JsRuntime.InvokeVoidAsync("initSoundEffects"); - } - catch (Exception ex) - { - this.Logger.LogError(ex, "Failed to initialize sound effects."); + this.isPreparing = false; + this.StateHasChanged(); + return; } var mimeTypes = GetPreferredMimeTypes( @@ -416,11 +439,66 @@ public partial class VoiceRecorder : MSGComponentBase } } - private sealed class AudioRecordingResult + private async Task EnsureSoundEffectsAvailableAsync(string context) { - public string MimeType { get; init; } = string.Empty; + if (!this.ShouldRenderVoiceRecording) + return false; - public bool ChangedMimeType { get; init; } + if (!this.VoiceRecordingAvailabilityService.IsAvailable) + return false; + + try + { + var result = await this.JsRuntime.InvokeAsync("initSoundEffects"); + if (result.Success) + return true; + + var failureDetails = BuildSoundEffectsFailureDetails(result); + this.Logger.LogError("Failed to initialize sound effects {Context}. {FailureDetails}", context, failureDetails); + await this.DisableVoiceRecordingAsync(failureDetails); + } + catch (JSDisconnectedException ex) + { + this.Logger.LogError(ex, "Failed to initialize sound effects {Context}. The JS runtime disconnected.", context); + await this.DisableVoiceRecordingAsync("The JS runtime disconnected while initializing audio playback."); + } + catch (OperationCanceledException ex) + { + this.Logger.LogError(ex, "Failed to initialize sound effects {Context}. The interop call was canceled.", context); + await this.DisableVoiceRecordingAsync("The interop call for audio playback initialization was canceled."); + } + catch (Exception ex) + { + this.Logger.LogError(ex, "Failed to initialize sound effects {Context}.", context); + await this.DisableVoiceRecordingAsync(ex.Message); + } + + return false; + } + + private async Task DisableVoiceRecordingAsync(string reason) + { + if (!this.VoiceRecordingAvailabilityService.TryDisable(reason)) + return; + + this.Logger.LogWarning("Voice recording was disabled for the current session. Reason: {Reason}", reason); + await this.MessageBus.SendWarning(new(Icons.Material.Filled.MicOff, this.T("Voice recording has been disabled for this session because audio playback could not be initialized on the client."))); + await this.SendMessage(Event.VOICE_RECORDING_AVAILABILITY_CHANGED, reason); + this.StateHasChanged(); + } + + private static string BuildSoundEffectsFailureDetails(SoundEffectsInitializationResult result) + { + var details = new List(); + if (result.FailedPaths.Length > 0) + details.Add($"Failed sound files: {string.Join(", ", result.FailedPaths)}."); + + if (!string.IsNullOrWhiteSpace(result.ErrorMessage)) + details.Add($"Client error: {result.ErrorMessage}"); + + return details.Count > 0 + ? string.Join(" ", details) + : "The client did not provide additional details."; } #region Overrides of MSGComponentBase @@ -440,4 +518,4 @@ public partial class VoiceRecorder : MSGComponentBase } #endregion -} +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/Workspaces.razor b/app/MindWork AI Studio/Components/Workspaces.razor index 80a81f60..56e5e59e 100644 --- a/app/MindWork AI Studio/Components/Workspaces.razor +++ b/app/MindWork AI Studio/Components/Workspaces.razor @@ -1,93 +1,114 @@ @inherits MSGComponentBase - - - @switch (item.Value) +@if (this.isInitialLoading) +{ + + + @for (var i = 0; i < 10; i++) { - case TreeDivider: -
  • - -
  • - break; - - case TreeItemData treeItem: - @if (treeItem.Type is TreeItemType.CHAT) - { - - -
    - - @if (string.IsNullOrWhiteSpace(treeItem.Text)) - { - @T("Empty chat") - } - else - { - @treeItem.ShortenedText - } - -
    - - - - - - - - - - - - -
    -
    -
    -
    - } - else if (treeItem.Type is TreeItemType.WORKSPACE) - { - - -
    - - @treeItem.Text - -
    - - - - - - - -
    -
    -
    -
    - } - else - { - - -
    - - @treeItem.Text - -
    -
    -
    - } - break; - - case TreeButton treeButton: -
  • -
    -
    - - @treeButton.Text - -
    -
  • - break; + } -
    -
    + +} +else +{ + + + @switch (item.Value) + { + case TreeDivider: +
  • + +
  • + break; + + case TreeItemData treeItem: + @if (treeItem.Type is TreeItemType.LOADING) + { + + + + + + } + else if (treeItem.Type is TreeItemType.CHAT) + { + + +
    + + @if (string.IsNullOrWhiteSpace(treeItem.Text)) + { + @T("Empty chat") + } + else + { + @treeItem.ShortenedText + } + +
    + + + + + + + + + + + + +
    +
    +
    +
    + } + else if (treeItem.Type is TreeItemType.WORKSPACE) + { + + +
    + + @treeItem.Text + +
    + + + + + + + +
    +
    +
    +
    + } + else + { + + +
    + + @treeItem.Text + +
    +
    +
    + } + break; + + case TreeButton treeButton: +
  • +
    +
    + + @treeButton.Text + +
    +
  • + break; + } +
    +
    +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/Workspaces.razor.cs b/app/MindWork AI Studio/Components/Workspaces.razor.cs index f3564e65..106d5719 100644 --- a/app/MindWork AI Studio/Components/Workspaces.razor.cs +++ b/app/MindWork AI Studio/Components/Workspaces.razor.cs @@ -1,4 +1,4 @@ -using System.Text; +using System.Text; using System.Text.Json; using AIStudio.Chat; @@ -29,31 +29,64 @@ public partial class Workspaces : MSGComponentBase public bool ExpandRootNodes { get; set; } = true; private const Placement WORKSPACE_ITEM_TOOLTIP_PLACEMENT = Placement.Bottom; + private readonly SemaphoreSlim treeLoadingSemaphore = new(1, 1); + private readonly List> treeItems = []; + private readonly HashSet loadingWorkspaceChatLists = []; - private readonly List> treeItems = new(); + private CancellationTokenSource? prefetchCancellationTokenSource; + private bool isInitialLoading = true; + private bool isDisposed; #region Overrides of ComponentBase protected override async Task OnInitializedAsync() { await base.OnInitializedAsync(); - - // - // Notice: In order to get the server-based loading to work, we need to respect the following rules: - // - We must have initial tree items - // - Those initial tree items cannot have children - // - When assigning the tree items to the MudTreeViewItem component, we must set the Value property to the value of the item - // - // We won't await the loading of the tree items here, - // to avoid blocking the UI thread: - _ = this.LoadTreeItems(); + _ = this.LoadTreeItemsAsync(startPrefetch: true); } #endregion - private async Task LoadTreeItems() + private async Task LoadTreeItemsAsync(bool startPrefetch = true, bool forceReload = false) + { + await this.treeLoadingSemaphore.WaitAsync(); + try + { + if (this.isDisposed) + return; + + if (forceReload) + await WorkspaceBehaviour.ForceReloadWorkspaceTreeAsync(); + + var snapshot = await WorkspaceBehaviour.GetOrLoadWorkspaceTreeShellAsync(); + this.BuildTreeItems(snapshot); + this.isInitialLoading = false; + } + finally + { + this.treeLoadingSemaphore.Release(); + } + + await this.SafeStateHasChanged(); + + if (startPrefetch) + await this.StartPrefetchAsync(); + } + + private void BuildTreeItems(WorkspaceTreeCacheSnapshot snapshot) { this.treeItems.Clear(); + + var workspaceChildren = new List>(); + foreach (var workspace in snapshot.Workspaces) + workspaceChildren.Add(this.CreateWorkspaceTreeItem(workspace)); + + workspaceChildren.Add(new TreeItemData + { + Expandable = false, + Value = new TreeButton(WorkspaceBranch.WORKSPACES, 1, T("Add workspace"), Icons.Material.Filled.LibraryAdd, this.AddWorkspaceAsync), + }); + this.treeItems.Add(new TreeItemData { Expanded = this.ExpandRootNodes, @@ -66,7 +99,7 @@ public partial class Workspaces : MSGComponentBase Icon = Icons.Material.Filled.Folder, Expandable = true, Path = "root", - Children = await this.LoadWorkspaces(), + Children = workspaceChildren, }, }); @@ -76,7 +109,10 @@ public partial class Workspaces : MSGComponentBase Value = new TreeDivider(), }); - await this.InvokeAsync(this.StateHasChanged); + var temporaryChatsChildren = new List>(); + foreach (var temporaryChat in snapshot.TemporaryChats.OrderByDescending(x => x.LastEditTime)) + temporaryChatsChildren.Add(CreateChatTreeItem(temporaryChat, WorkspaceBranch.TEMPORARY_CHATS, depth: 1, icon: Icons.Material.Filled.Timer)); + this.treeItems.Add(new TreeItemData { Expanded = this.ExpandRootNodes, @@ -89,234 +125,219 @@ public partial class Workspaces : MSGComponentBase Icon = Icons.Material.Filled.Timer, Expandable = true, Path = "temp", - Children = await this.LoadTemporaryChats(), + Children = temporaryChatsChildren, }, }); - + } + + private TreeItemData CreateWorkspaceTreeItem(WorkspaceTreeWorkspace workspace) + { + var children = new List>(); + if (workspace.ChatsLoaded) + { + foreach (var workspaceChat in workspace.Chats.OrderByDescending(x => x.LastEditTime)) + children.Add(CreateChatTreeItem(workspaceChat, WorkspaceBranch.WORKSPACES, depth: 2, icon: Icons.Material.Filled.Chat)); + } + else if (this.loadingWorkspaceChatLists.Contains(workspace.WorkspaceId)) + children.AddRange(this.CreateLoadingRows(workspace.WorkspacePath)); + + children.Add(new TreeItemData + { + Expandable = false, + Value = new TreeButton(WorkspaceBranch.WORKSPACES, 2, T("Add chat"), Icons.Material.Filled.AddComment, () => this.AddChatAsync(workspace.WorkspacePath)), + }); + + return new TreeItemData + { + Expandable = true, + Value = new TreeItemData + { + Type = TreeItemType.WORKSPACE, + Depth = 1, + Branch = WorkspaceBranch.WORKSPACES, + Text = workspace.Name, + Icon = Icons.Material.Filled.Description, + Expandable = true, + Path = workspace.WorkspacePath, + Children = children, + }, + }; + } + + private IReadOnlyCollection> CreateLoadingRows(string workspacePath) + { + return + [ + this.CreateLoadingTreeItem(workspacePath, "loading_1"), + this.CreateLoadingTreeItem(workspacePath, "loading_2"), + this.CreateLoadingTreeItem(workspacePath, "loading_3"), + ]; + } + + private TreeItemData CreateLoadingTreeItem(string workspacePath, string suffix) + { + return new TreeItemData + { + Expandable = false, + Value = new TreeItemData + { + Type = TreeItemType.LOADING, + Depth = 2, + Branch = WorkspaceBranch.WORKSPACES, + Text = T("Loading chats..."), + Icon = Icons.Material.Filled.HourglassTop, + Expandable = false, + Path = Path.Join(workspacePath, suffix), + }, + }; + } + + private static TreeItemData CreateChatTreeItem(WorkspaceTreeChat chat, WorkspaceBranch branch, int depth, string icon) + { + return new TreeItemData + { + Expandable = false, + Value = new TreeItemData + { + Type = TreeItemType.CHAT, + Depth = depth, + Branch = branch, + Text = chat.Name, + Icon = icon, + Expandable = false, + Path = chat.ChatPath, + LastEditTime = chat.LastEditTime, + }, + }; + } + + private async Task SafeStateHasChanged() + { + if (this.isDisposed) + return; + await this.InvokeAsync(this.StateHasChanged); } - private async Task>> LoadTemporaryChats() + private async Task StartPrefetchAsync() { - var tempChildren = new List(); - - // Get the temp root directory: - var temporaryDirectories = Path.Join(SettingsManager.DataDirectory, "tempChats"); - - // Ensure the directory exists: - Directory.CreateDirectory(temporaryDirectories); - - // Enumerate the chat directories: - foreach (var tempChatDirPath in Directory.EnumerateDirectories(temporaryDirectories)) + if (this.prefetchCancellationTokenSource is not null) { - // Read or create the `name` file (self-heal): - var chatNamePath = Path.Join(tempChatDirPath, "name"); - string chatName; - try + await this.prefetchCancellationTokenSource.CancelAsync(); + this.prefetchCancellationTokenSource.Dispose(); + } + + this.prefetchCancellationTokenSource = new CancellationTokenSource(); + await this.PrefetchWorkspaceChatsAsync(this.prefetchCancellationTokenSource.Token); + } + + private async Task PrefetchWorkspaceChatsAsync(CancellationToken cancellationToken) + { + try + { + await WorkspaceBehaviour.TryPrefetchRemainingChatsAsync(async _ => { - if (!File.Exists(chatNamePath)) - { - chatName = T("Unnamed chat"); - await File.WriteAllTextAsync(chatNamePath, chatName, Encoding.UTF8); - } - else - { - chatName = await File.ReadAllTextAsync(chatNamePath, Encoding.UTF8); - if (string.IsNullOrWhiteSpace(chatName)) - { - chatName = T("Unnamed chat"); - await File.WriteAllTextAsync(chatNamePath, chatName, Encoding.UTF8); - } - } - } - catch - { - chatName = T("Unnamed chat"); - } - - // Read the last change time of the chat: - var chatThreadPath = Path.Join(tempChatDirPath, "thread.json"); - var lastEditTime = File.GetLastWriteTimeUtc(chatThreadPath); - - tempChildren.Add(new TreeItemData - { - Type = TreeItemType.CHAT, - Depth = 1, - Branch = WorkspaceBranch.TEMPORARY_CHATS, - Text = chatName, - Icon = Icons.Material.Filled.Timer, - Expandable = false, - Path = tempChatDirPath, - LastEditTime = lastEditTime, - }); + if (this.isDisposed || cancellationToken.IsCancellationRequested) + return; + + await this.LoadTreeItemsAsync(startPrefetch: false); + }, cancellationToken); + } + catch (OperationCanceledException) + { + // Expected when the component is hidden or disposed. + } + catch (Exception ex) + { + this.Logger.LogWarning(ex, "Failed while prefetching workspace chats."); + } + } + + private async Task OnWorkspaceClicked(TreeItemData treeItem) + { + if (treeItem.Type is not TreeItemType.WORKSPACE) + return; + + if (!Guid.TryParse(Path.GetFileName(treeItem.Path), out var workspaceId)) + return; + + await this.EnsureWorkspaceChatsLoadedAsync(workspaceId); + } + + private async Task EnsureWorkspaceChatsLoadedAsync(Guid workspaceId) + { + var snapshot = await WorkspaceBehaviour.GetOrLoadWorkspaceTreeShellAsync(); + var hasWorkspace = false; + var chatsLoaded = false; + + foreach (var workspace in snapshot.Workspaces) + { + if (workspace.WorkspaceId != workspaceId) + continue; + + hasWorkspace = true; + chatsLoaded = workspace.ChatsLoaded; + break; + } + + if (!hasWorkspace || chatsLoaded || !this.loadingWorkspaceChatLists.Add(workspaceId)) + return; + + await this.LoadTreeItemsAsync(startPrefetch: false); + + try + { + await WorkspaceBehaviour.GetWorkspaceChatsAsync(workspaceId); + } + finally + { + this.loadingWorkspaceChatLists.Remove(workspaceId); + } + + await this.LoadTreeItemsAsync(startPrefetch: false); + } + + public async Task ForceRefreshFromDiskAsync() + { + if (this.prefetchCancellationTokenSource is not null) + { + await this.prefetchCancellationTokenSource.CancelAsync(); + this.prefetchCancellationTokenSource.Dispose(); + this.prefetchCancellationTokenSource = null; } - var result = new List>(tempChildren.OrderByDescending(n => n.LastEditTime).Select(n => new TreeItemData - { - Expandable = false, - Value = n, - })); - return result; - } - - private async Task>> LoadWorkspaces() - { - var workspaces = new List>(); + this.loadingWorkspaceChatLists.Clear(); + this.isInitialLoading = true; - // - // Search for workspace folders in the data directory: - // - - // Get the workspace root directory: - var workspaceDirectories = Path.Join(SettingsManager.DataDirectory, "workspaces"); - - // Ensure the directory exists: - Directory.CreateDirectory(workspaceDirectories); - - // Enumerate the workspace directories: - foreach (var workspaceDirPath in Directory.EnumerateDirectories(workspaceDirectories)) - { - // Read or create the `name` file (self-heal): - var workspaceNamePath = Path.Join(workspaceDirPath, "name"); - string workspaceName; - try - { - if (!File.Exists(workspaceNamePath)) - { - workspaceName = T("Unnamed workspace"); - await File.WriteAllTextAsync(workspaceNamePath, workspaceName, Encoding.UTF8); - } - else - { - workspaceName = await File.ReadAllTextAsync(workspaceNamePath, Encoding.UTF8); - if (string.IsNullOrWhiteSpace(workspaceName)) - { - workspaceName = T("Unnamed workspace"); - await File.WriteAllTextAsync(workspaceNamePath, workspaceName, Encoding.UTF8); - } - } - } - catch - { - workspaceName = T("Unnamed workspace"); - } - - workspaces.Add(new TreeItemData - { - Expandable = true, - Value = new TreeItemData - { - Type = TreeItemType.WORKSPACE, - Depth = 1, - Branch = WorkspaceBranch.WORKSPACES, - Text = workspaceName, - Icon = Icons.Material.Filled.Description, - Expandable = true, - Path = workspaceDirPath, - Children = await this.LoadWorkspaceChats(workspaceDirPath), - }, - }); - } - - workspaces.Add(new TreeItemData - { - Expandable = false, - Value = new TreeButton(WorkspaceBranch.WORKSPACES, 1, T("Add workspace"),Icons.Material.Filled.LibraryAdd, this.AddWorkspace), - }); - return workspaces; + await this.SafeStateHasChanged(); + await this.LoadTreeItemsAsync(startPrefetch: true, forceReload: true); } - private async Task>> LoadWorkspaceChats(string workspacePath) + public async Task StoreChatAsync(ChatThread chat, bool reloadTreeItems = false) { - var workspaceChats = new List(); + await WorkspaceBehaviour.StoreChatAsync(chat); - // Enumerate the workspace directory: - foreach (var chatPath in Directory.EnumerateDirectories(workspacePath)) - { - // Read or create the `name` file (self-heal): - var chatNamePath = Path.Join(chatPath, "name"); - string chatName; - try - { - if (!File.Exists(chatNamePath)) - { - chatName = T("Unnamed chat"); - await File.WriteAllTextAsync(chatNamePath, chatName, Encoding.UTF8); - } - else - { - chatName = await File.ReadAllTextAsync(chatNamePath, Encoding.UTF8); - if (string.IsNullOrWhiteSpace(chatName)) - { - chatName = T("Unnamed chat"); - await File.WriteAllTextAsync(chatNamePath, chatName, Encoding.UTF8); - } - } - } - catch - { - chatName = T("Unnamed chat"); - } - - // Read the last change time of the chat: - var chatThreadPath = Path.Join(chatPath, "thread.json"); - var lastEditTime = File.GetLastWriteTimeUtc(chatThreadPath); - - workspaceChats.Add(new TreeItemData - { - Type = TreeItemType.CHAT, - Depth = 2, - Branch = WorkspaceBranch.WORKSPACES, - Text = chatName, - Icon = Icons.Material.Filled.Chat, - Expandable = false, - Path = chatPath, - LastEditTime = lastEditTime, - }); - } + if (reloadTreeItems) + this.loadingWorkspaceChatLists.Clear(); - var result = new List>(workspaceChats.OrderByDescending(n => n.LastEditTime).Select(n => new TreeItemData - { - Expandable = false, - Value = n, - })); - - result.Add(new() - { - Expandable = false, - Value = new TreeButton(WorkspaceBranch.WORKSPACES, 2, T("Add chat"),Icons.Material.Filled.AddComment, () => this.AddChat(workspacePath)), - }); - - return result; + await this.LoadTreeItemsAsync(startPrefetch: false); } - public async Task StoreChat(ChatThread chat, bool reloadTreeItems = true) + private async Task LoadChatAsync(string? chatPath, bool switchToChat) { - await WorkspaceBehaviour.StoreChat(chat); - - // Reload the tree items: - if(reloadTreeItems) - await this.LoadTreeItems(); - - this.StateHasChanged(); - } - - private async Task LoadChat(string? chatPath, bool switchToChat) - { - if(string.IsNullOrWhiteSpace(chatPath)) + if (string.IsNullOrWhiteSpace(chatPath)) return null; - if(!Directory.Exists(chatPath)) + if (!Directory.Exists(chatPath)) return null; - // Check if the chat has unsaved changes: if (switchToChat && await MessageBus.INSTANCE.SendMessageUseFirstResult(this, Event.HAS_CHAT_UNSAVED_CHANGES)) { var dialogParameters = new DialogParameters { { x => x.Message, T("Are you sure you want to load another chat? All unsaved changes will be lost.") }, }; - + var dialogReference = await this.DialogService.ShowAsync(T("Load Chat"), dialogParameters, DialogOptions.FULLSCREEN); var dialogResult = await dialogReference.Result; if (dialogResult is null || dialogResult.Canceled) @@ -344,15 +365,15 @@ public partial class Workspaces : MSGComponentBase return null; } - public async Task DeleteChat(string? chatPath, bool askForConfirmation = true, bool unloadChat = true) + public async Task DeleteChatAsync(string? chatPath, bool askForConfirmation = true, bool unloadChat = true) { - var chat = await this.LoadChat(chatPath, false); + var chat = await this.LoadChatAsync(chatPath, false); if (chat is null) return; if (askForConfirmation) { - var workspaceName = await WorkspaceBehaviour.LoadWorkspaceName(chat.WorkspaceId); + var workspaceName = await WorkspaceBehaviour.LoadWorkspaceNameAsync(chat.WorkspaceId); var dialogParameters = new DialogParameters { { @@ -370,16 +391,10 @@ public partial class Workspaces : MSGComponentBase return; } - string chatDirectory; - if (chat.WorkspaceId == Guid.Empty) - chatDirectory = Path.Join(SettingsManager.DataDirectory, "tempChats", chat.ChatId.ToString()); - else - chatDirectory = Path.Join(SettingsManager.DataDirectory, "workspaces", chat.WorkspaceId.ToString(), chat.ChatId.ToString()); - - Directory.Delete(chatDirectory, true); - await this.LoadTreeItems(); + await WorkspaceBehaviour.DeleteChatAsync(this.DialogService, chat.WorkspaceId, chat.ChatId, askForConfirmation: false); + await this.LoadTreeItemsAsync(startPrefetch: false); - if(unloadChat && this.CurrentChatThread?.ChatId == chat.ChatId) + if (unloadChat && this.CurrentChatThread?.ChatId == chat.ChatId) { this.CurrentChatThread = null; await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread); @@ -387,9 +402,9 @@ public partial class Workspaces : MSGComponentBase } } - private async Task RenameChat(string? chatPath) + private async Task RenameChatAsync(string? chatPath) { - var chat = await this.LoadChat(chatPath, false); + var chat = await this.LoadChatAsync(chatPath, false); if (chat is null) return; @@ -410,24 +425,24 @@ public partial class Workspaces : MSGComponentBase return; chat.Name = (dialogResult.Data as string)!; - if(this.CurrentChatThread?.ChatId == chat.ChatId) + if (this.CurrentChatThread?.ChatId == chat.ChatId) { this.CurrentChatThread.Name = chat.Name; await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread); await MessageBus.INSTANCE.SendMessage(this, Event.WORKSPACE_LOADED_CHAT_CHANGED); } - await this.StoreChat(chat); - await this.LoadTreeItems(); + await WorkspaceBehaviour.StoreChatAsync(chat); + await this.LoadTreeItemsAsync(startPrefetch: false); } - - private async Task RenameWorkspace(string? workspacePath) + + private async Task RenameWorkspaceAsync(string? workspacePath) { - if(workspacePath is null) + if (workspacePath is null) return; var workspaceId = Guid.Parse(Path.GetFileName(workspacePath)); - var workspaceName = await WorkspaceBehaviour.LoadWorkspaceName(workspaceId); + var workspaceName = await WorkspaceBehaviour.LoadWorkspaceNameAsync(workspaceId); var dialogParameters = new DialogParameters { { x => x.Message, string.Format(T("Please enter a new or edit the name for your workspace '{0}':"), workspaceName) }, @@ -447,10 +462,11 @@ public partial class Workspaces : MSGComponentBase var alteredWorkspaceName = (dialogResult.Data as string)!; var workspaceNamePath = Path.Join(workspacePath, "name"); await File.WriteAllTextAsync(workspaceNamePath, alteredWorkspaceName, Encoding.UTF8); - await this.LoadTreeItems(); + await WorkspaceBehaviour.UpdateWorkspaceNameInCacheAsync(workspaceId, alteredWorkspaceName); + await this.LoadTreeItemsAsync(startPrefetch: false); } - private async Task AddWorkspace() + private async Task AddWorkspaceAsync() { var dialogParameters = new DialogParameters { @@ -472,23 +488,23 @@ public partial class Workspaces : MSGComponentBase var workspacePath = Path.Join(SettingsManager.DataDirectory, "workspaces", workspaceId.ToString()); Directory.CreateDirectory(workspacePath); + var workspaceName = (dialogResult.Data as string)!; var workspaceNamePath = Path.Join(workspacePath, "name"); - await File.WriteAllTextAsync(workspaceNamePath, (dialogResult.Data as string)!, Encoding.UTF8); + await File.WriteAllTextAsync(workspaceNamePath, workspaceName, Encoding.UTF8); + await WorkspaceBehaviour.AddWorkspaceToCacheAsync(workspaceId, workspacePath, workspaceName); - await this.LoadTreeItems(); + await this.LoadTreeItemsAsync(startPrefetch: false); } - private async Task DeleteWorkspace(string? workspacePath) + private async Task DeleteWorkspaceAsync(string? workspacePath) { - if(workspacePath is null) + if (workspacePath is null) return; var workspaceId = Guid.Parse(Path.GetFileName(workspacePath)); - var workspaceName = await WorkspaceBehaviour.LoadWorkspaceName(workspaceId); + var workspaceName = await WorkspaceBehaviour.LoadWorkspaceNameAsync(workspaceId); - // Determine how many chats are in the workspace: var chatCount = Directory.EnumerateDirectories(workspacePath).Count(); - var dialogParameters = new DialogParameters { { x => x.Message, string.Format(T("Are you sure you want to delete the workspace '{0}'? This will also delete {1} chat(s) in this workspace."), workspaceName, chatCount) }, @@ -500,12 +516,13 @@ public partial class Workspaces : MSGComponentBase return; Directory.Delete(workspacePath, true); - await this.LoadTreeItems(); + await WorkspaceBehaviour.RemoveWorkspaceFromCacheAsync(workspaceId); + await this.LoadTreeItemsAsync(startPrefetch: false); } - private async Task MoveChat(string? chatPath) + private async Task MoveChatAsync(string? chatPath) { - var chat = await this.LoadChat(chatPath, false); + var chat = await this.LoadChatAsync(chatPath, false); if (chat is null) return; @@ -525,22 +542,9 @@ public partial class Workspaces : MSGComponentBase if (workspaceId == Guid.Empty) return; - // Delete the chat from the current workspace or the temporary storage: - if (chat.WorkspaceId == Guid.Empty) - { - // Case: The chat is stored in the temporary storage: - await this.DeleteChat(Path.Join(SettingsManager.DataDirectory, "tempChats", chat.ChatId.ToString()), askForConfirmation: false, unloadChat: false); - } - else - { - // Case: The chat is stored in a workspace. - await this.DeleteChat(Path.Join(SettingsManager.DataDirectory, "workspaces", chat.WorkspaceId.ToString(), chat.ChatId.ToString()), askForConfirmation: false, unloadChat: false); - } + await WorkspaceBehaviour.DeleteChatAsync(this.DialogService, chat.WorkspaceId, chat.ChatId, askForConfirmation: false); - // Update the chat's workspace: chat.WorkspaceId = workspaceId; - - // Handle the case where the chat is the active chat: if (this.CurrentChatThread?.ChatId == chat.ChatId) { this.CurrentChatThread = chat; @@ -548,12 +552,12 @@ public partial class Workspaces : MSGComponentBase await MessageBus.INSTANCE.SendMessage(this, Event.WORKSPACE_LOADED_CHAT_CHANGED); } - await this.StoreChat(chat); + await WorkspaceBehaviour.StoreChatAsync(chat); + await this.LoadTreeItemsAsync(startPrefetch: false); } - private async Task AddChat(string workspacePath) + private async Task AddChatAsync(string workspacePath) { - // Check if the chat has unsaved changes: if (await MessageBus.INSTANCE.SendMessageUseFirstResult(this, Event.HAS_CHAT_UNSAVED_CHANGES)) { var dialogParameters = new DialogParameters @@ -579,9 +583,9 @@ public partial class Workspaces : MSGComponentBase var chatPath = Path.Join(workspacePath, chat.ChatId.ToString()); - await this.StoreChat(chat); - await this.LoadChat(chatPath, switchToChat: true); - await this.LoadTreeItems(); + await WorkspaceBehaviour.StoreChatAsync(chat); + await this.LoadChatAsync(chatPath, switchToChat: true); + await this.LoadTreeItemsAsync(startPrefetch: false); } #region Overrides of MSGComponentBase @@ -591,11 +595,20 @@ public partial class Workspaces : MSGComponentBase switch (triggeredEvent) { case Event.PLUGINS_RELOADED: - await this.LoadTreeItems(); - await this.InvokeAsync(this.StateHasChanged); + await this.ForceRefreshFromDiskAsync(); break; } } + protected override void DisposeResources() + { + this.isDisposed = true; + this.prefetchCancellationTokenSource?.Cancel(); + this.prefetchCancellationTokenSource?.Dispose(); + this.prefetchCancellationTokenSource = null; + + base.DisposeResources(); + } + #endregion } \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor b/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor index 0a72ef07..d13a44bb 100644 --- a/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor +++ b/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor @@ -55,6 +55,7 @@ @T("Use the default system prompt") + @T("Predefined User Input") 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/Dialogs/Settings/SettingsDialogAgenda.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogAgenda.razor index f217da79..dcaf18ff 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogAgenda.razor +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogAgenda.razor @@ -34,7 +34,7 @@ } - + @@ -42,4 +42,4 @@ @T("Close") - \ No newline at end of file + diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogAssistantBias.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogAssistantBias.razor index ec6776f0..40f3331f 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogAssistantBias.razor +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogAssistantBias.razor @@ -27,7 +27,7 @@ { } - + @@ -38,4 +38,4 @@ @T("Close") - \ No newline at end of file + diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChat.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChat.razor index 111b6a93..d9ed5a90 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChat.razor +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChat.razor @@ -18,7 +18,7 @@ - + @@ -33,4 +33,4 @@ @T("Close") - \ No newline at end of file + diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogCoding.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogCoding.razor index dde19c0c..6cfed1ac 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogCoding.razor +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogCoding.razor @@ -20,10 +20,10 @@ } - + Close - \ No newline at end of file + diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogERIServer.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogERIServer.razor index cb4e20ac..9f0e2272 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogERIServer.razor +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogERIServer.razor @@ -13,7 +13,7 @@ - + @T("Most ERI server options can be customized and saved directly in the ERI server assistant. For this, the ERI server assistant has an auto-save function.") @@ -25,4 +25,4 @@ @T("Close") - \ No newline at end of file + diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogLegalCheck.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogLegalCheck.razor index 42cb70d4..71947b14 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogLegalCheck.razor +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogLegalCheck.razor @@ -15,7 +15,7 @@ - + @@ -23,4 +23,4 @@ @T("Close") - \ No newline at end of file + diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogMyTasks.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogMyTasks.razor index 626f421d..1fed1f08 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogMyTasks.razor +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogMyTasks.razor @@ -16,7 +16,7 @@ { } - + @@ -26,4 +26,4 @@ @T("Close") - \ No newline at end of file + diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogSlideBuilder.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogSlideBuilder.razor new file mode 100644 index 00000000..1c8dad1f --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogSlideBuilder.razor @@ -0,0 +1,34 @@ +@using AIStudio.Settings +@inherits SettingsDialogBase + + + + + + @T("Assistant: Slide Assistant Options") + + + + + + + + @if (this.SettingsManager.ConfigurationData.SlideBuilder.PreselectedTargetLanguage is CommonLanguages.OTHER) + { + + } + + + + + + + + + + + + @T("Close") + + + diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogSlideBuilder.razor.cs b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogSlideBuilder.razor.cs new file mode 100644 index 00000000..e6f093f8 --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogSlideBuilder.razor.cs @@ -0,0 +1,3 @@ +namespace AIStudio.Dialogs.Settings; + +public partial class SettingsDialogSlideBuilder : SettingsDialogBase; \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogWritingEMails.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogWritingEMails.razor index 6f31f266..ff96ced6 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogWritingEMails.razor +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogWritingEMails.razor @@ -21,7 +21,7 @@ - + @@ -29,4 +29,4 @@ @T("Close") - \ No newline at end of file + diff --git a/app/MindWork AI Studio/Dialogs/WorkspaceSelectionDialog.razor.cs b/app/MindWork AI Studio/Dialogs/WorkspaceSelectionDialog.razor.cs index 9a096e27..ca4b625e 100644 --- a/app/MindWork AI Studio/Dialogs/WorkspaceSelectionDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/WorkspaceSelectionDialog.razor.cs @@ -1,7 +1,4 @@ -using System.Text; - using AIStudio.Components; -using AIStudio.Settings; using Microsoft.AspNetCore.Components; @@ -29,25 +26,10 @@ public partial class WorkspaceSelectionDialog : MSGComponentBase protected override async Task OnInitializedAsync() { this.selectedWorkspace = this.SelectedWorkspace; - - // Get the workspace root directory: - var workspaceDirectories = Path.Join(SettingsManager.DataDirectory, "workspaces"); - if(!Directory.Exists(workspaceDirectories)) - { - await base.OnInitializedAsync(); - return; - } - // Enumerate the workspace directories: - foreach (var workspaceDirPath in Directory.EnumerateDirectories(workspaceDirectories)) - { - // Read the `name` file: - var workspaceNamePath = Path.Join(workspaceDirPath, "name"); - var workspaceName = await File.ReadAllTextAsync(workspaceNamePath, Encoding.UTF8); - - // Add the workspace to the list: - this.workspaces.Add(workspaceName, Guid.Parse(Path.GetFileName(workspaceDirPath))); - } + var snapshot = await WorkspaceBehaviour.GetOrLoadWorkspaceTreeShellAsync(); + foreach (var workspace in snapshot.Workspaces) + this.workspaces[workspace.Name] = workspace.WorkspaceId; this.StateHasChanged(); await base.OnInitializedAsync(); diff --git a/app/MindWork AI Studio/Layout/MainLayout.razor.cs b/app/MindWork AI Studio/Layout/MainLayout.razor.cs index 07dfebd2..0fc41f7c 100644 --- a/app/MindWork AI Studio/Layout/MainLayout.razor.cs +++ b/app/MindWork AI Studio/Layout/MainLayout.razor.cs @@ -97,7 +97,6 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan // Set the snackbar for the update service: UpdateService.SetBlazorDependencies(this.Snackbar); - GlobalShortcutService.Initialize(); TemporaryChatService.Initialize(); // Should the navigation bar be open by default? @@ -251,6 +250,7 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan // Set up hot reloading for plugins: PluginFactory.SetUpHotReloading(); + await this.MessageBus.SendMessage(this, Event.STARTUP_COMPLETED); } }); break; diff --git a/app/MindWork AI Studio/MindWork AI Studio.csproj b/app/MindWork AI Studio/MindWork AI Studio.csproj index 8c9725e2..01a9295b 100644 --- a/app/MindWork AI Studio/MindWork AI Studio.csproj +++ b/app/MindWork AI Studio/MindWork AI Studio.csproj @@ -49,7 +49,7 @@ - + diff --git a/app/MindWork AI Studio/Pages/Assistants.razor b/app/MindWork AI Studio/Pages/Assistants.razor index 19f88cee..e44f4694 100644 --- a/app/MindWork AI Studio/Pages/Assistants.razor +++ b/app/MindWork AI Studio/Pages/Assistants.razor @@ -52,12 +52,13 @@ @if (this.SettingsManager.IsAnyCategoryAssistantVisible("Business", (Components.EMAIL_ASSISTANT, PreviewFeatures.NONE), - (Components.DOCUMENT_ANALYSIS_ASSISTANT, PreviewFeatures.PRE_DOCUMENT_ANALYSIS_2025), + (Components.DOCUMENT_ANALYSIS_ASSISTANT, PreviewFeatures.NONE), (Components.MY_TASKS_ASSISTANT, PreviewFeatures.NONE), (Components.AGENDA_ASSISTANT, PreviewFeatures.NONE), (Components.JOB_POSTING_ASSISTANT, PreviewFeatures.NONE), (Components.LEGAL_CHECK_ASSISTANT, PreviewFeatures.NONE), - (Components.ICON_FINDER_ASSISTANT, PreviewFeatures.NONE) + (Components.ICON_FINDER_ASSISTANT, PreviewFeatures.NONE), + (Components.SLIDE_BUILDER_ASSISTANT, PreviewFeatures.NONE) )) { @@ -65,12 +66,13 @@ - + + } diff --git a/app/MindWork AI Studio/Pages/Chat.razor b/app/MindWork AI Studio/Pages/Chat.razor index 1b2df035..b1b48dc3 100644 --- a/app/MindWork AI Studio/Pages/Chat.razor +++ b/app/MindWork AI Studio/Pages/Chat.razor @@ -48,6 +48,9 @@ + + + @@ -71,6 +74,9 @@ + + + @@ -137,6 +143,9 @@ + + + diff --git a/app/MindWork AI Studio/Pages/Chat.razor.cs b/app/MindWork AI Studio/Pages/Chat.razor.cs index 420828c6..9adef5bb 100644 --- a/app/MindWork AI Studio/Pages/Chat.razor.cs +++ b/app/MindWork AI Studio/Pages/Chat.razor.cs @@ -98,6 +98,14 @@ public partial class Chat : MSGComponentBase await this.DialogService.ShowAsync(T("Open Workspaces Configuration"), dialogParameters, DialogOptions.FULLSCREEN); } + private async Task RefreshWorkspaces() + { + if (this.workspaces is null) + return; + + await this.workspaces.ForceRefreshFromDiskAsync(); + } + #region Overrides of MSGComponentBase protected override void DisposeResources() diff --git a/app/MindWork AI Studio/Pages/Home.razor.cs b/app/MindWork AI Studio/Pages/Home.razor.cs index 610d22b0..40431683 100644 --- a/app/MindWork AI Studio/Pages/Home.razor.cs +++ b/app/MindWork AI Studio/Pages/Home.razor.cs @@ -1,4 +1,5 @@ using AIStudio.Components; +using AIStudio.Settings.DataModel; using Microsoft.AspNetCore.Components; @@ -10,6 +11,9 @@ public partial class Home : MSGComponentBase { [Inject] private HttpClient HttpClient { get; init; } = null!; + + [Inject] + private NavigationManager NavigationManager { get; init; } = null!; private string LastChangeContent { get; set; } = string.Empty; @@ -27,6 +31,19 @@ public partial class Home : MSGComponentBase _ = this.ReadLastChangeAsync(); } + protected override Task OnAfterRenderAsync(bool firstRender) + { + if (this.SettingsManager.StartupStartPageRedirectHandled || !this.SettingsManager.HasCompletedInitialSettingsLoad) + return base.OnAfterRenderAsync(firstRender); + + this.SettingsManager.StartupStartPageRedirectHandled = true; + var startPageRoute = this.SettingsManager.ConfigurationData.App.StartPage.ToRoute(); + if (!string.IsNullOrWhiteSpace(startPageRoute)) + this.NavigationManager.NavigateTo(startPageRoute, replace: true); + + return base.OnAfterRenderAsync(firstRender); + } + private void InitializeAdvantagesItems() { this.itemsAdvantages = [ @@ -95,7 +112,7 @@ public partial class Home : MSGComponentBase ## Step 4: Load OpenAI Models 1. Ensure you have an internet connection and your API key is valid. 2. Click "Reload" to retrieve a list of available OpenAI models. - 3. Select "gpt-4o" to use the latest model. + 3. Select "gpt-5.4" to use a current model. 4. Provide a name for this combination of provider, API key, and model. This is called the "instance name". For example, you can name it based on the usage context, such as "Personal OpenAI" or "Work OpenAI". ## Step 5: Save the Provider 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/configuration/plugin.lua b/app/MindWork AI Studio/Plugins/configuration/plugin.lua index 5918b691..03a9b0f4 100644 --- a/app/MindWork AI Studio/Plugins/configuration/plugin.lua +++ b/app/MindWork AI Studio/Plugins/configuration/plugin.lua @@ -146,6 +146,16 @@ CONFIG["SETTINGS"] = {} -- Allowed values are: MANUAL, AUTOMATIC -- CONFIG["SETTINGS"]["DataApp.UpdateInstallation"] = "MANUAL" +-- Configure the page that should be opened when AI Studio starts. +-- Allowed values are: HOME, CHAT, ASSISTANTS, INFORMATION, PLUGINS, SUPPORTERS, SETTINGS +-- CONFIG["SETTINGS"]["DataApp.StartPage"] = "CHAT" +-- +-- Allow users to change the configured start page locally. +-- Allowed values are: true, false +-- When set to true, the configured start page becomes the organization default, +-- but users can still choose another start page in the app settings. +-- CONFIG["SETTINGS"]["DataApp.StartPage.AllowUserOverride"] = true + -- Configure the user permission to add providers: -- Allowed values are: true, false -- CONFIG["SETTINGS"]["DataApp.AllowUserToAddProvider"] = false @@ -163,8 +173,8 @@ CONFIG["SETTINGS"] = {} -- Configure the enabled preview features: -- Allowed values are can be found in https://github.com/MindWorkAI/AI-Studio/app/MindWork%20AI%20Studio/Settings/DataModel/PreviewFeatures.cs --- Examples are PRE_WRITER_MODE_2024, PRE_RAG_2024, PRE_DOCUMENT_ANALYSIS_2025. --- CONFIG["SETTINGS"]["DataApp.EnabledPreviewFeatures"] = { "PRE_RAG_2024", "PRE_DOCUMENT_ANALYSIS_2025" } +-- Examples are PRE_WRITER_MODE_2024, PRE_RAG_2024, PRE_SPEECH_TO_TEXT_2026. +-- CONFIG["SETTINGS"]["DataApp.EnabledPreviewFeatures"] = { "PRE_RAG_2024", "PRE_SPEECH_TO_TEXT_2026" } -- Configure the preselected provider. -- It must be one of the provider IDs defined in CONFIG["LLM_PROVIDERS"]. @@ -189,7 +199,7 @@ CONFIG["SETTINGS"] = {} -- TEXT_SUMMARIZER_ASSISTANT, EMAIL_ASSISTANT, LEGAL_CHECK_ASSISTANT, -- SYNONYMS_ASSISTANT, MY_TASKS_ASSISTANT, JOB_POSTING_ASSISTANT, -- BIAS_DAY_ASSISTANT, ERI_ASSISTANT, DOCUMENT_ANALYSIS_ASSISTANT, --- I18N_ASSISTANT +-- SLIDE_BUILDER_ASSISTANT, I18N_ASSISTANT -- CONFIG["SETTINGS"]["DataApp.HiddenAssistants"] = { "ERI_ASSISTANT", "I18N_ASSISTANT" } -- Configure a global shortcut for starting and stopping dictation. 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 3afbf26f..a9104433 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 @@ -432,6 +432,12 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTA -- Load output rules from document UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTANT::T2168201568"] = "Regeln für die Ausgabe aus einem Dokument laden" +-- Choose whether the policy should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTANT::T2214900121"] = "Wählen Sie, ob das Regelwerk das Standardprofil der App, kein Profil oder ein bestimmtes Profil verwenden soll." + +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTANT::T2322771068"] = "Profil vorauswählen" + -- The analysis rules specify what the AI should pay particular attention to while reviewing the documents you provide, and which aspects it should highlight or save. For example, if you want to extract the potential of green hydrogen for agriculture from a variety of general publications, you can explicitly define this in the analysis rules. UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTANT::T238145218"] = "Die Analyseregeln legen fest, worauf die KI bei der Prüfung der von Ihnen bereitgestellten Dokumente besonders achten und welche Aspekte sie hervorheben oder speichern soll. Wenn Sie beispielsweise das Potenzial von grünem Wasserstoff für die Landwirtschaft aus einer Vielzahl allgemeiner Publikationen extrahieren möchten, können Sie dies in den Analyseregeln explizit definieren." @@ -1282,7 +1288,7 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T3646084045"] UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T3848935911"] = "Benutzerdefinierte Zielsprache" -- Please select one of your profiles. -UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T465395981"] = "Bitte wählen Sie eines ihrer Profile aus." +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T465395981"] = "Bitte wählen Sie eines Ihrer Profile aus." -- Text or email UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T534887559"] = "Text oder E-Mail" @@ -1297,7 +1303,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" @@ -1362,6 +1368,189 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::REWRITEIMPROVE::WRITINGSTYLESEXTENSIONS:: -- Marketing (advertisements, sales texts) UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::REWRITEIMPROVE::WRITINGSTYLESEXTENSIONS::T945714286"] = "Marketing (Werbung, Verkaufstexte)" +-- Children +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEAGEGROUPEXTENSIONS::T188567026"] = "Kinder" + +-- Unspecified age group +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEAGEGROUPEXTENSIONS::T300604284"] = "Nicht angegebene Altersgruppe" + +-- Adults +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEAGEGROUPEXTENSIONS::T3335941460"] = "Erwachsene" + +-- Teenagers +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEAGEGROUPEXTENSIONS::T3696960735"] = "Jugendliche" + +-- No expertise +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEEXPERTISEEXTENSIONS::T1612807521"] = "Keine Vorkenntnisse" + +-- Intermediate expertise +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEEXPERTISEEXTENSIONS::T2131860427"] = "Mittlere Kenntnisse" + +-- Unspecified expertise +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEEXPERTISEEXTENSIONS::T2879202483"] = "Nicht spezifizierte Fachkenntnisse" + +-- Basic expertise +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEEXPERTISEEXTENSIONS::T2909686714"] = "Grundlegende Kenntnisse" + +-- Experts +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEEXPERTISEEXTENSIONS::T3130182982"] = "Experten" + +-- Individual contributors +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEORGANIZATIONALLEVELEXTENSIONS::T1893672448"] = "Einzelne Mitwirkende" + +-- Team leads +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEORGANIZATIONALLEVELEXTENSIONS::T2112906979"] = "Teamleitung" + +-- Board members +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEORGANIZATIONALLEVELEXTENSIONS::T2483400884"] = "Vorstand" + +-- Unspecified organizational level +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEORGANIZATIONALLEVELEXTENSIONS::T2633679224"] = "Nicht angegebene Organisationsebene" + +-- Trainees +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEORGANIZATIONALLEVELEXTENSIONS::T2678344512"] = "Einstieg" + +-- Managers +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEORGANIZATIONALLEVELEXTENSIONS::T3187355853"] = "Management" + +-- Executives +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEORGANIZATIONALLEVELEXTENSIONS::T3317335174"] = "Führung" + +-- Journalists +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T1589799277"] = "Journalisten" + +-- Healthcare professionals +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T1652521346"] = "Medizinisches Fachpersonal" + +-- Unspecified audience profile +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T1845793571"] = "Nicht angegebenes Zielgruppenprofil" + +-- Lawyers +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T1959098902"] = "Anwälte" + +-- Investors +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T2516036290"] = "Investoren" + +-- Students +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T2905889225"] = "Studierende" + +-- Scientists +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T332785734"] = "Wissenschaftler" + +-- Business professionals +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T3670621687"] = "Business Professionals" + +-- Engineers +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T3904961809"] = "Ingenieure" + +-- Public officials +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T439009390"] = "Amtsträger" + +-- Software developers +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T831424531"] = "Softwareentwickler" + +-- Important Aspects +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T1379883528"] = "Wichtige Aspekte" + +-- Extent of the planned presentation +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T1408740929"] = "Umfang der geplanten Präsentation" + +-- You might want to specify important aspects that the LLM should consider when creating the slides. For example, the use of emojis or specific topics that should be highlighted. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T1672597841"] = "Vielleicht möchten Sie wichtige Aspekte angeben, die das LLM bei der Erstellung der Folien berücksichtigen soll. Zum Beispiel die Verwendung von Emojis oder bestimmte Themen, die hervorgehoben werden sollen." + +-- Details about the desired presentation +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T1793579367"] = "Details zur gewünschten Präsentation" + +-- Text content +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T1820253043"] = "Textinhalt" + +-- Slide Assistant +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T1883918574"] = "Folienassistent" + +-- Please provide a text or at least one valid document or image. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T2013746884"] = "Bitte geben Sie einen Text oder mindestens ein gültiges Dokument oder Bild an." + +-- Content to derive slide from +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T2086228265"] = "Inhalt, aus dem die Folie erstellt werden soll" + +-- Target language +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T237828418"] = "Zielsprache" + +-- (Optional) Important Aspects +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T24391765"] = "(Optional) Wichtige Aspekte" + +-- Audience age group +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T2496533563"] = "Altersgruppe" + +-- You can enter text, use one or more documents or images, or use both. At least one of these options is required. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T2542045947"] = "Sie können Text eingeben, ein oder mehrere Dokumente oder Bilder verwenden oder beides nutzen. Mindestens eine dieser Optionen ist erforderlich." + +-- Language +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T2591284123"] = "Sprache" + +-- Audience organizational level +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T2599228833"] = "Organisatorische Ebene der Zielgruppe" + +-- Number of slides +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T2823798965"] = "Anzahl der Folien" + +-- This assistant helps you create clear, structured slides from long texts or documents. Enter a presentation title and provide the content either as text or with one or more documents. Important aspects allow you to add instructions to the LLM regarding output or formatting. Set the number of slides either directly or based on your desired presentation duration. You can also specify the number of bullet points. If the default value of 0 is not changed, the LLM will independently determine how many slides or bullet points to generate. The output can be flexibly generated in various languages and tailored to a specific audience. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T2910177051"] = "Dieser Assistent hilft Ihnen, aus langen Texten oder Dokumenten klare, strukturierte Folien zu erstellen. Geben Sie einen Titel für die Präsentation ein und stellen Sie den Inhalt entweder als Text oder über ein oder mehrere Dokumente bereit. Unter „Wichtige Aspekte“ können Sie dem LLM Anweisungen zur Ausgabe oder Formatierung geben. Legen Sie die Anzahl der Folien entweder direkt oder anhand der gewünschten Präsentationsdauer fest. Sie können auch die Anzahl der Aufzählungspunkte angeben. Wenn der Standardwert 0 nicht geändert wird, bestimmt das LLM selbstständig, wie viele Folien oder Aufzählungspunkte erstellt werden. Die Ausgabe kann flexibel in verschiedenen Sprachen erzeugt und auf eine bestimmte Zielgruppe zugeschnitten werden." + +-- The result of your previous slide builder session. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3000286990"] = "Das Ergebnis Ihrer vorherigen Folienassistent-Sitzung." + +-- Please enter a title for the presentation. This will help the LLM to select more relevant content. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3013824309"] = "Bitte geben Sie einen Titel für die Präsentation ein. Dies hilft dem LLM, relevantere Inhalte auszuwählen." + +-- Please provide a title +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3049299559"] = "Bitte geben Sie einen Titel ein" + +-- Create Slides +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3079776593"] = "Folien erstellen" + +-- Please specify the audience for the planned presentation. This will help the LLM to create a presentation that fits your needs. You can specify the audience profile, the age group, organizational level, and the expertise. You don't have to specify all of these parameters, for example you might only want to specify the audience profile and leave the other parameters at their default values. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3465256803"] = "Bitte geben Sie die Zielgruppe für die geplante Präsentation an. So kann das LLM eine Präsentation erstellen, die zu Ihren Anforderungen passt. Sie können das Profil der Zielgruppe, die Altersgruppe, die Organisationsebene und die Fachkenntnisse angeben. Sie müssen nicht alle diese Parameter festlegen; Sie können zum Beispiel nur das Profil der Zielgruppe angeben und die anderen Parameter bei ihren Standardwerten belassen." + +-- (Optional) Specify aspects that the LLM should consider when creating the slides. For example, the use of emojis or specific topics that should be highlighted. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3476149293"] = "(Optional) Gib Aspekte an, die das LLM beim Erstellen der Folien berücksichtigen soll. Zum Beispiel die Verwendung von Emojis oder bestimmte Themen, die hervorgehoben werden sollen." + +-- Audience +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3506118019"] = "Zielgruppe" + +-- Time specification (minutes) +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3539067305"] = "Zeitangabe (Minuten)" + +-- Audience profile +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3649769130"] = "Profil der Zielgruppe" + +-- Attach documents +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3666048746"] = "Dokumente anhängen" + +-- Number of bullet points +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3796347042"] = "Anzahl der Aufzählungspunkte" + +-- Custom target language +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3848935911"] = "Benutzerdefinierte Zielsprache" + +-- Presentation title +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3893271035"] = "Präsentationstitel" + +-- {0} - Slide Builder Session +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3922788056"] = "{0} – Folienassistent-Sitzung" + +-- Please specify the extent of the planned presentation. This can be the number of slides, the number of bullet points per slide, or the time specification for the presentation. This will help the LLM to create a presentation that fits your needs. Leave the default values if you don't have specific requirements regarding the extent of the presentation. You might only want to specify one of these parameters, for example the time specification, and leave the others at their default values. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T4131419342"] = "Bitte geben Sie den Umfang der geplanten Präsentation an. Das kann die Anzahl der Folien, die Anzahl der Aufzählungspunkte pro Folie oder die Zeitvorgabe für die Präsentation sein. Das hilft dem LLM, eine Präsentation zu erstellen, die Ihren Anforderungen entspricht. Lassen Sie die Standardwerte unverändert, wenn Sie keine besonderen Anforderungen an den Umfang der Präsentation haben. Möglicherweise möchten Sie nur einen dieser Parameter angeben, zum Beispiel die Zeitvorgabe, und die anderen auf den Standardwerten belassen." + +-- Audience expertise +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T4279519256"] = "Fachkenntnisse der Zielgruppe" + +-- Title +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T617902505"] = "Titel" + +-- Please provide a custom language. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T656744944"] = "Bitte geben Sie eine benutzerdefinierte Sprache an." + -- Your word or phrase UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SYNONYM::ASSISTANTSYNONYMS::T1847246020"] = "Ihr Wort oder Phrase" @@ -1599,6 +1788,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1133040906"] = "Chat vers -- Are you sure you want to move this chat? All unsaved changes will be lost. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1142475422"] = "Sind Sie sicher, dass Sie diesen Chat verschieben möchten? Alle ungespeicherten Änderungen gehen verloren." +-- Bold +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1165397398"] = "Fett" + -- Stop generation UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1317408357"] = "Generierung stoppen" @@ -1611,9 +1803,18 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1849313532"] = "Geben Sie -- Your Prompt (use selected instance '{0}', provider '{1}') UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1967611328"] = "Ihr Prompt (verwendete Instanz: '{0}', Anbieter: '{1}')" +-- Code +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2036185364"] = "Code" + +-- Italic +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2377171085"] = "Kursiv" + -- Profile usage is disabled according to your chat template settings. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2670286472"] = "Die Profilnutzung ist gemäß den Einstellungen ihrer Chat-Vorlage deaktiviert." +-- Bulleted List +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2957125464"] = "Aufzählungszeichen" + -- Delete this chat & start a new one. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2991985411"] = "Diesen Chat löschen & einen neuen beginnen." @@ -1632,6 +1833,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T3928697643"] = "Neuen Cha -- New disappearing chat UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T4113970938"] = "Neuen selbstlöschenden Chat starten" +-- Heading +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T4231005109"] = "Überschrift" + -- Please select the workspace where you want to move the chat to. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T474393241"] = "Bitte wählen Sie den Arbeitsbereich aus, in den Sie den Chat verschieben möchten." @@ -1776,6 +1980,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" @@ -1885,7 +2092,7 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PREVIEWRELEASECANDIDATE::T3451939995"] = UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PREVIEWRELEASECANDIDATE::T696585888"] = "Release-Kandidaten sind der letzte Schritt, bevor eine Funktion als stabil gilt." -- Select one of your profiles -UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PROFILEFORMSELECTION::T2003449133"] = "Wählen Sie eines ihrer Profile aus" +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PROFILEFORMSELECTION::T2003449133"] = "Wählen Sie eines Ihrer Profile aus" -- Open Profile Options UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PROFILEFORMSELECTION::T3654011106"] = "Profil-Optionen öffnen" @@ -2070,6 +2277,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1364944735"] -- Select preview features UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1439783084"] = "Vorschaufunktionen auswählen" +-- Your organization provided a default start page, but you can still change it. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1454730224"] = "Ihre Organisation hat eine Standard-Startseite festgelegt, die Sie jedoch ändern können." + -- Select the desired behavior for the navigation bar. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1555038969"] = "Wählen Sie das gewünschte Verhalten für die Navigationsleiste aus." @@ -2118,6 +2328,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2591284123"] -- Administration settings are visible UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2591866808"] = "Die Optionen für die Administration sind sichtbar." +-- Choose which page AI Studio should open first when you start the app. Changes take effect the next time you launch AI Studio. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2655930524"] = "Wählen Sie aus, welche Seite AI Studio beim Start der App zuerst öffnen soll. Änderungen werden beim nächsten Start von AI Studio wirksam." + -- Save energy? UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3100928009"] = "Energie sparen?" @@ -2166,6 +2379,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T71162186"] = -- Energy saving is disabled UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T716338721"] = "Energiesparmodus ist deaktiviert" +-- Start page +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T78084670"] = "Startseite" + -- Preview feature visibility UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T817101267"] = "Sichtbarkeit der Vorschaufunktion" @@ -2506,7 +2722,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." @@ -2526,6 +2742,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T428040679"] = "Erstellung von In -- Useful assistants UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T586430036"] = "Nützliche Assistenten" +-- Voice recording has been disabled for this session because audio playback could not be initialized on the client. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T1123032432"] = "Die Sprachaufnahme wurde für diese Sitzung deaktiviert, da die Audiowiedergabe auf dem Client nicht initialisiert werden konnte." + -- Failed to create the transcription provider. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T1689988905"] = "Der Anbieter für die Transkription konnte nicht erstellt werden." @@ -2535,6 +2754,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T2144994226"] = "Audioaufn -- Stop recording and start transcription UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T224155287"] = "Aufnahme beenden und Transkription starten" +-- Voice recording is unavailable because the client could not initialize audio playback. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T2260302339"] = "Die Sprachaufnahme ist nicht verfügbar, da der Client die Audiowiedergabe nicht initialisieren konnte." + -- Start recording your voice for a transcription UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T2372624045"] = "Beginnen Sie mit der Aufnahme Ihrer Stimme für eine Transkription" @@ -2565,8 +2787,8 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1016188706"] = "Möchten Sie -- Move chat UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1133040906"] = "Chat verschieben" --- Unnamed workspace -UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1307384014"] = "Unbenannter Arbeitsbereich" +-- Loading chats... +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1364857726"] = "Chats werden geladen..." -- Delete UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1469573738"] = "Löschen" @@ -2622,9 +2844,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T323280982"] = "Bitte geben S -- Please enter a workspace name. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3288132732"] = "Bitte geben Sie einen Namen für diesen Arbeitsbereich ein." --- Unnamed chat -UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3310482275"] = "Unbenannter Chat" - -- Rename UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3355849203"] = "Umbenennen" @@ -2751,6 +2970,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T380891852"] = "Beispiel -- Actions UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3865031940"] = "Aktionen" +-- Load system prompt from file +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3883091650"] = "System-Prompt aus Datei laden" + -- Messages per page UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3893704289"] = "Nachrichten pro Seite" @@ -3543,6 +3765,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" @@ -3564,12 +3789,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." @@ -3750,6 +3981,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T1471770981" -- Preselect whether participants needs to arrive and depart UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T1648427207"] = "Legen Sie im Voraus fest, ob Teilnehmer anreisen und abreisen müssen" +-- Choose whether the assistant should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T1766361623"] = "Wählen Sie, ob der Assistent das Standardprofil der App, kein Profil oder ein bestimmtes Profil verwenden soll." + -- Preselect a start time? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T1901151023"] = "Startzeit vorauswählen?" @@ -3762,8 +3996,8 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T1998244307" -- Preselect whether the meeting is virtual UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T2084951012"] = "Wählen Sie aus, ob das Meeting virtuell ist" --- Would you like to preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T2221665527"] = "Möchten Sie eines ihrer Profile vorauswählen?" +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T2322771068"] = "Profil vorauswählen" -- When enabled, you can preselect most agenda options. This is might be useful when you need to create similar agendas often. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T2373110543"] = "Wenn diese Option aktiviert ist, können Sie die meisten Agendapunkte vorauswählen. Das kann hilfreich sein, wenn Sie häufig ähnliche Agenden erstellst." @@ -3816,9 +4050,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T3709527588" -- Preselect a topic? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T3835166371"] = "Ein Thema vorauswählen?" --- Preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T4004501229"] = "Eines ihrer Profile vorauswählen?" - -- Preselect the agenda language UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T4055846391"] = "Wählen Sie die Sprache der Agenda vorab aus" @@ -3855,15 +4086,18 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T1608 -- Yes, you can only retrieve one bias per day UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T1765683725"] = "Ja, Sie können nur einmal pro Tag eine Voreingenommenheit abrufen." +-- Choose whether the assistant should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T1766361623"] = "Wähle aus, ob der Assistent das Standardprofil der App, kein Profil oder ein bestimmtes Profil verwenden soll." + -- Reset UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T180921696"] = "Zurücksetzen" --- Would you like to preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T2221665527"] = "Möchten Sie eines ihrer Profile vorauswählen?" - -- No restriction. You can retrieve as many biases as you want per day. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T2305356277"] = "Keine Einschränkung. Sie können beliebig viele Vorurteile pro Tag abrufen." +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T2322771068"] = "Profil vorauswählen" + -- Which language should be preselected? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T2345162613"] = "Welche Sprache soll vorausgewählt werden?" @@ -3888,9 +4122,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T3848 -- Options are preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T3875604319"] = "Optionen sind vorausgewählt" --- Preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T4004501229"] = "Möchten Sie eines ihrer Profile vorauswählen?" - -- Are you sure you want to reset your bias-of-the-day statistics? The system will no longer remember which biases you already know. As a result, biases you are already familiar with may be addressed again. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T405627382"] = "Sind Sie sicher, dass Sie ihre „Vorurteil des Tages“-Statistiken zurücksetzen möchten? Das System merkt sich dann nicht mehr, welche Verzerrungen Sie bereits kennen. Dadurch kann es sein, dass Ihnen bereits bekannte Verzerrungen erneut angezeigt werden." @@ -3921,8 +4152,11 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T1773585398"] -- Provider selection when creating new chats UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T189306836"] = "Anbieterauswahl beim Erstellen neuer Chats" --- Would you like to set one of your profiles as the default for chats? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T1933521846"] = "Möchten Sie eines ihrer Profile als Standardprofil für Chats festlegen?" +-- Choose whether chats should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T1915793195"] = "Wählen Sie aus, ob Chats das Standardprofil der App, kein Profil oder ein bestimmtes Profil verwenden sollen." + +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T2322771068"] = "Profil vorauswählen" -- Apply default data source option when sending assistant results to chat UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T2510376349"] = "Standarddatenquelle verwenden, wenn Assistentenergebnisse in den Chat gesendet werden" @@ -3960,9 +4194,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T3730599555"] -- Latest message is shown, after loading a chat UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T3755993611"] = "Die neueste Nachricht wird nach dem Laden eines Chats angezeigt." --- Preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T4004501229"] = "Eines ihrer Profile vorauswählen?" - -- Do you want to apply the default data source options when sending assistant results to chat? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T4033153439"] = "Möchten Sie die Standardoptionen für Datenquellen verwenden, wenn die Ergebnisse des Assistenten an den Chat gesendet werden?" @@ -4023,11 +4254,14 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T1073540083" -- Compiler messages are preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T1110902070"] = "Compiler-Nachrichten sind vorausgewählt" +-- Choose whether the assistant should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T1766361623"] = "Wählen Sie aus, ob der Assistent das Standardprofil der App, kein Profil oder ein bestimmtes Profil verwenden soll." + -- Preselect a programming language UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T2181567002"] = "Programmiersprache vorauswählen" --- Would you like to preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T2221665527"] = "Möchten Sie eines ihrer Profile vorauswählen?" +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T2322771068"] = "Profil vorauswählen" -- When enabled, you can preselect the coding options. This is might be useful when you prefer a specific programming language or LLM model. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T2619641701"] = "Wenn aktiviert, können Sie die Code-Optionen im Voraus auswählen. Das kann nützlich sein, wenn Sie eine bestimmte Programmiersprache oder ein bestimmtes LLM-Modell bevorzugen." @@ -4044,9 +4278,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T3015105896" -- Coding options are preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T3567850751"] = "Codierungsoptionen sind vorausgewählt" --- Preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T4004501229"] = "Eines ihrer Profile vorauswählen?" - -- Preselect another programming language UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T4230412334"] = "Eine andere Programmiersprache vorauswählen" @@ -4146,14 +4377,17 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T12806662 -- Preselect ERI server options? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T1664055662"] = "ERI-Serveroptionen vorauswählen?" +-- Choose whether the assistant should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T1766361623"] = "Wählen Sie, ob der Assistent das Standardprofil der App, kein Profil oder ein bestimmtes Profil verwenden soll." + -- No ERI server options are preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T1793785587"] = "Keine ERI-Serveroptionen sind vorausgewählt" -- Most ERI server options can be customized and saved directly in the ERI server assistant. For this, the ERI server assistant has an auto-save function. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T2093534613"] = "Die meisten ERI-Serveroptionen können direkt im ERI-Server-Assistenten angepasst und gespeichert werden. Dazu verfügt der ERI-Server-Assistent über eine automatische Speicherfunktion." --- Would you like to preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T2221665527"] = "Möchten Sie eines ihrer Profile vorauswählen?" +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T2322771068"] = "Profil vorauswählen" -- Close UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T3448155331"] = "Schließen" @@ -4161,9 +4395,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T34481553 -- Assistant: ERI Server Options UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T3629372826"] = "Assistent: ERI-Server-Optionen" --- Preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T4004501229"] = "Möchten Sie eines ihrer Profile vorauswählen?" - -- ERI server options are preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T488190224"] = "ERI-Serveroptionen sind vorausgewählt" @@ -4311,6 +4542,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T1633101 -- Web content reader is not preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T1701127912"] = "Der Web-Content-Reader zum Lesen von Webinhalten ist nicht vorausgewählt" +-- Choose whether the assistant should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T1766361623"] = "Wählen Sie aus, ob der Assistent das Standardprofil der App, kein Profil oder ein bestimmtes Profil verwenden soll." + -- Content cleaner agent is not preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T1969816694"] = "Agent zur Inhaltsbereinigung ist nicht vorausgewählt" @@ -4320,8 +4554,8 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T2090693 -- When enabled, you can preselect some legal check options. This is might be useful when you prefer a specific LLM model. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T2164667361"] = "Wenn aktiviert, können Sie einige rechtliche Prüfoptionen vorauswählen. Dies kann nützlich sein, wenn Sie ein bestimmtes LLM-Modell bevorzugen." --- Would you like to preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T2221665527"] = "Möchten Sie eines ihrer Profile vorauswählen?" +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T2322771068"] = "Profil vorauswählen" -- Legal check options are preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T252916114"] = "Rechtsprüfungsoptionen sind vorausgewählt" @@ -4341,17 +4575,17 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T3641773 -- Preselect the content cleaner agent? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T3649428096"] = "Assistent zur Inhaltsbereinigungs vorauswählen?" --- Preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T4004501229"] = "Eines ihrer Profile vorauswählen?" - -- Assistant: Legal Check Options UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T4033382756"] = "Assistent: Optionen für rechtliche Prüfung" -- Preselect the web content reader? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T629158142"] = "Den Web-Content-Reader zum Lesen von Webinhalten vorauswählen?" --- Would you like to preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T2221665527"] = "Möchten Sie eines ihrer Profile vorauswählen?" +-- Choose whether the assistant should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T1766361623"] = "Wählen Sie, ob der Assistent das Standardprofil der App, kein Profil oder ein bestimmtes Profil verwenden soll." + +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T2322771068"] = "Profil vorauswählen" -- Which language should be preselected? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T2345162613"] = "Welche Sprache soll vorausgewählt werden?" @@ -4374,9 +4608,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T3710380967 -- Options are preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T3875604319"] = "Optionen sind vorausgewählt" --- Preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T4004501229"] = "Möchten Sie eines ihrer Profile vorauswählen?" - -- Preselect options? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T42672465"] = "Optionen vorauswählen?" @@ -4438,7 +4669,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?" @@ -4447,7 +4678,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" @@ -4456,13 +4687,76 @@ 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" + +-- Preselect the audience expertise +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T1017131030"] = "Expertise der Zielgruppe vorauswählen" + +-- When enabled, you can preselect slide builder options. This is might be useful when you prefer a specific language or LLM model. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T1393378753"] = "Wenn diese Option aktiviert ist, können Sie Optionen für den Folienassistent vorab auswählen. Dies kann nützlich sein, wenn Sie eine bestimmte Sprache oder ein bestimmtes LLM-Modell bevorzugen." + +-- Preselect aspects for the LLM to focus on when generating slides, such as bullet points or specific topics to emphasize. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T1528169602"] = "Wählen Sie Aspekte vorab aus, auf die sich das LLM bei der Erstellung von Folien konzentrieren soll, z. B. Aufzählungspunkte oder bestimmte Themen, die hervorgehoben werden sollen." + +-- Choose whether the assistant should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T1766361623"] = "Wählen Sie aus, ob der Assistent das Standardprofil der App, kein Profil oder ein bestimmtes Profil verwenden soll." + +-- Preselect the audience organizational level +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T2014662371"] = "Organisationsebene der Zielgruppe vorauswählen" + +-- Which audience organizational level should be preselected? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T216511105"] = "Welche organisatorische Ebene der Zielgruppe soll vorausgewählt werden?" + +-- Preselect Slide Assistant options? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T227645894"] = "Optionen des Folienassistenten vorauswählen?" + +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T2322771068"] = "Profil vorauswählen" + +-- Which language should be preselected? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T2345162613"] = "Welche Sprache sollte vorausgewählt sein?" + +-- Preselect another language +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T2382415529"] = "Andere Sprache vorauswählen" + +-- Preselect the language +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T2571465005"] = "Sprache vorauswählen" + +-- Preselect the audience age group +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T2645589441"] = "Altersgruppe der Zielgruppe vorauswählen" + +-- Assistant: Slide Assistant Options +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T3215549988"] = "Assistent: Optionen für die Erstellung von Folien" + +-- Which audience expertise should be preselected? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T3228597992"] = "Welche Expertise der Zielgruppe sollte vorausgewählt werden?" + +-- Close +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T3448155331"] = "Schließen" + +-- Preselect important aspects +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T3705987833"] = "Wichtige Aspekte vorauswählen" + +-- No Slide Assistant options are preselected +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T4214398691"] = "Keine Optionen für den Folienassistenten sind vorausgewählt." + +-- Preselect the audience profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T861397972"] = "Zielgruppenprofil vorauswählen" + +-- Slide Assistant options are preselected +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T93124146"] = "Optionen des Folienassistenten sind vorausgewählt" + +-- Which audience age group should be preselected? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T956845877"] = "Welche Altersgruppe der Zielgruppe sollte vorausgewählt sein?" + +-- Which audience profile should be preselected? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T973572510"] = "Welches Zielgruppenprofil soll vorausgewählt sein?" -- 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." @@ -4683,6 +4977,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T1417 -- Preselect another target language UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T1462295644"] = "Eine andere Zielsprache auswählen" +-- Choose whether the assistant should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T1766361623"] = "Wählen Sie, ob der Assistent das Standardprofil der App, kein Profil oder ein bestimmtes Profil verwenden soll." + -- Assistant: Writing E-Mails Options UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T2021226503"] = "Assistent: Optionen zum Schreiben von E-Mails" @@ -4692,8 +4989,8 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T2116 -- Preselect your name for the closing salutation? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T221974240"] = "Ihren Namen für die Grußformel vorauswählen?" --- Would you like to preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T2221665527"] = "Möchten Sie eines ihrer Profile vorauswählen?" +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T2322771068"] = "Profil vorauswählen" -- Preselect a writing style UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T28456020"] = "Wähle einen Schreibstil vor" @@ -4713,9 +5010,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T3547 -- Preselect e-mail options? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T3832719342"] = "E-Mail-Optionen vorauswählen?" --- Preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T4004501229"] = "Eines ihrer Profile vorauswählen?" - -- Save UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T1294818664"] = "Speichern" @@ -4902,6 +5196,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1617786407"] = "Programmieren" -- Analyze a text or an email for tasks you need to complete. UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1728590051"] = "Analysieren Sie einen Text oder eine E-Mail nach Aufgaben, die Sie erledigen müssen." +-- Slide Assistant +UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1883918574"] = "Folienassistent" + -- Text Summarizer UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1907192403"] = "Texte zusammenfassen" @@ -4944,6 +5241,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T3011450657"] = "Meine Aufgaben" -- E-Mail UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T3026443472"] = "E-Mail" +-- Develop slide content based on a given topic and content. +UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T311912219"] = "Folieninhalte basierend auf einem vorgegebenen Thema und Inhalt erstellen." + -- Translate AI Studio text content into other languages UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T3181803840"] = "AI Studio Textinhalte in andere Sprachen übersetzen." @@ -4992,6 +5292,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T878695986"] = "Lerne jeden Tag ei -- Localization UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T897888480"] = "Lokalisierung" +-- Reload your workspaces +UI_TEXT_CONTENT["AISTUDIO::PAGES::CHAT::T194629703"] = "Arbeitsbereiche neu laden" + -- Hide your workspaces UI_TEXT_CONTENT["AISTUDIO::PAGES::CHAT::T2351468526"] = "Arbeitsbereiche ausblenden" @@ -5301,6 +5604,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." @@ -5322,6 +5628,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." @@ -5595,12 +5904,21 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1188453609 -- Show also prototype features: these are works in progress; expect bugs and missing features UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1245257804"] = "Auch Prototyp-Funktionen anzeigen: Diese befinden sich noch in der Entwicklung; Fehler und fehlende Funktionen sind zu erwarten" +-- Settings +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1258653480"] = "Einstellungen" + -- No key is sending the input; you have to click the send button UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1311973034"] = "Keine Taste sendet die Eingabe; Sie müssen auf die Schaltfläche klicken, um zu senden" -- Navigation never expands, no tooltips; there are only icons UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1402851833"] = "Navigationsleiste wird nie erweitert, keine Tooltips; es werden nur Icons angezeigt" +-- Welcome +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1485461907"] = "Willkommen" + +-- Assistants +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1614176092"] = "Assistenten" + -- Store chats automatically UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1664293672"] = "Chats automatisch speichern" @@ -5625,6 +5943,9 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T2195945406 -- Install updates manually UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T220653235"] = "Updates manuell installieren" +-- Plugins +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T2222816203"] = "Plugins" + -- Also show features ready for release; these should be stable UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T2301448762"] = "Auch Funktionen anzeigen, die bereit für die Veröffentlichung sind; diese sollten stabil sein." @@ -5646,6 +5967,9 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T2566503670 -- No minimum confidence level chosen UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T2828607242"] = "Kein Mindestvertrauensniveau ausgewählt" +-- Supporters +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T2929332068"] = "Unterstützer" + -- Do not specify the language UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T2960082609"] = "Sprache nicht festlegen" @@ -5667,6 +5991,9 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3491430707 -- Install updates automatically UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3569059463"] = "Updates automatisch installieren" +-- Use app default profile +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3587225583"] = "Standardprofil der App verwenden" + -- Disable workspaces UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3612390107"] = "Arbeitsbereiche deaktivieren" @@ -5676,9 +6003,15 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3711207137 -- Also show features in alpha: these are in development; expect bugs and missing features UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T4146964761"] = "Zeige auch Funktionen im Alpha-Stadium an: Diese befinden sich in der Entwicklung; es werden Fehler und fehlende Funktionen auftreten." +-- Information +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T4256323669"] = "Information" + -- All preview features are hidden UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T4289410063"] = "Alle Vorschaufunktionen sind ausgeblendet" +-- Chat +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T578410699"] = "Chat" + -- When possible, use the LLM provider which was used for each chat in the first place UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T75376144"] = "Wenn möglich, verwende den LLM-Anbieter, der ursprünglich für jeden Chat verwendet wurde." @@ -5847,6 +6180,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T1546040625"] = "Meine A -- Grammar & Spelling Assistant UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T166453786"] = "Grammatik- & Rechtschreib-Assistent" +-- Slide Assistant +UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T1883918574"] = "Folienassistent" + -- Legal Check Assistant UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T1886447798"] = "Rechtlichen Prüfungs-Assistent" @@ -5907,6 +6243,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" @@ -6557,3 +6902,6 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T1307384014"] = "Unbenannt -- Delete Chat UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T2244038752"] = "Chat löschen" + +-- Unnamed chat +UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T3310482275"] = "Unbenannter Chat" 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 aa703185..efb6451d 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 @@ -432,6 +432,12 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTA -- Load output rules from document UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTANT::T2168201568"] = "Load output rules from document" +-- Choose whether the policy should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTANT::T2214900121"] = "Choose whether the policy should use the app default profile, no profile, or a specific profile." + +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTANT::T2322771068"] = "Preselect a profile" + -- The analysis rules specify what the AI should pay particular attention to while reviewing the documents you provide, and which aspects it should highlight or save. For example, if you want to extract the potential of green hydrogen for agriculture from a variety of general publications, you can explicitly define this in the analysis rules. UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTANT::T238145218"] = "The analysis rules specify what the AI should pay particular attention to while reviewing the documents you provide, and which aspects it should highlight or save. For example, if you want to extract the potential of green hydrogen for agriculture from a variety of general publications, you can explicitly define this in the analysis rules." @@ -1362,6 +1368,189 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::REWRITEIMPROVE::WRITINGSTYLESEXTENSIONS:: -- Marketing (advertisements, sales texts) UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::REWRITEIMPROVE::WRITINGSTYLESEXTENSIONS::T945714286"] = "Marketing (advertisements, sales texts)" +-- Children +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEAGEGROUPEXTENSIONS::T188567026"] = "Children" + +-- Unspecified age group +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEAGEGROUPEXTENSIONS::T300604284"] = "Unspecified age group" + +-- Adults +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEAGEGROUPEXTENSIONS::T3335941460"] = "Adults" + +-- Teenagers +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEAGEGROUPEXTENSIONS::T3696960735"] = "Teenagers" + +-- No expertise +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEEXPERTISEEXTENSIONS::T1612807521"] = "No expertise" + +-- Intermediate expertise +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEEXPERTISEEXTENSIONS::T2131860427"] = "Intermediate expertise" + +-- Unspecified expertise +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEEXPERTISEEXTENSIONS::T2879202483"] = "Unspecified expertise" + +-- Basic expertise +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEEXPERTISEEXTENSIONS::T2909686714"] = "Basic expertise" + +-- Experts +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEEXPERTISEEXTENSIONS::T3130182982"] = "Experts" + +-- Individual contributors +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEORGANIZATIONALLEVELEXTENSIONS::T1893672448"] = "Individual contributors" + +-- Team leads +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEORGANIZATIONALLEVELEXTENSIONS::T2112906979"] = "Team leads" + +-- Board members +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEORGANIZATIONALLEVELEXTENSIONS::T2483400884"] = "Board members" + +-- Unspecified organizational level +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEORGANIZATIONALLEVELEXTENSIONS::T2633679224"] = "Unspecified organizational level" + +-- Trainees +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEORGANIZATIONALLEVELEXTENSIONS::T2678344512"] = "Trainees" + +-- Managers +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEORGANIZATIONALLEVELEXTENSIONS::T3187355853"] = "Managers" + +-- Executives +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEORGANIZATIONALLEVELEXTENSIONS::T3317335174"] = "Executives" + +-- Journalists +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T1589799277"] = "Journalists" + +-- Healthcare professionals +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T1652521346"] = "Healthcare professionals" + +-- Unspecified audience profile +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T1845793571"] = "Unspecified audience profile" + +-- Lawyers +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T1959098902"] = "Lawyers" + +-- Investors +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T2516036290"] = "Investors" + +-- Students +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T2905889225"] = "Students" + +-- Scientists +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T332785734"] = "Scientists" + +-- Business professionals +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T3670621687"] = "Business professionals" + +-- Engineers +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T3904961809"] = "Engineers" + +-- Public officials +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T439009390"] = "Public officials" + +-- Software developers +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::AUDIENCEPROFILEEXTENSIONS::T831424531"] = "Software developers" + +-- Important Aspects +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T1379883528"] = "Important Aspects" + +-- Extent of the planned presentation +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T1408740929"] = "Extent of the planned presentation" + +-- You might want to specify important aspects that the LLM should consider when creating the slides. For example, the use of emojis or specific topics that should be highlighted. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T1672597841"] = "You might want to specify important aspects that the LLM should consider when creating the slides. For example, the use of emojis or specific topics that should be highlighted." + +-- Details about the desired presentation +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T1793579367"] = "Details about the desired presentation" + +-- Text content +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T1820253043"] = "Text content" + +-- Slide Assistant +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T1883918574"] = "Slide Assistant" + +-- Please provide a text or at least one valid document or image. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T2013746884"] = "Please provide a text or at least one valid document or image." + +-- Content to derive slide from +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T2086228265"] = "Content to derive slide from" + +-- Target language +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T237828418"] = "Target language" + +-- (Optional) Important Aspects +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T24391765"] = "(Optional) Important Aspects" + +-- Audience age group +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T2496533563"] = "Audience age group" + +-- You can enter text, use one or more documents or images, or use both. At least one of these options is required. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T2542045947"] = "You can enter text, use one or more documents or images, or use both. At least one of these options is required." + +-- Language +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T2591284123"] = "Language" + +-- Audience organizational level +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T2599228833"] = "Audience organizational level" + +-- Number of slides +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T2823798965"] = "Number of slides" + +-- This assistant helps you create clear, structured slides from long texts or documents. Enter a presentation title and provide the content either as text or with one or more documents. Important aspects allow you to add instructions to the LLM regarding output or formatting. Set the number of slides either directly or based on your desired presentation duration. You can also specify the number of bullet points. If the default value of 0 is not changed, the LLM will independently determine how many slides or bullet points to generate. The output can be flexibly generated in various languages and tailored to a specific audience. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T2910177051"] = "This assistant helps you create clear, structured slides from long texts or documents. Enter a presentation title and provide the content either as text or with one or more documents. Important aspects allow you to add instructions to the LLM regarding output or formatting. Set the number of slides either directly or based on your desired presentation duration. You can also specify the number of bullet points. If the default value of 0 is not changed, the LLM will independently determine how many slides or bullet points to generate. The output can be flexibly generated in various languages and tailored to a specific audience." + +-- The result of your previous slide builder session. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3000286990"] = "The result of your previous slide builder session." + +-- Please enter a title for the presentation. This will help the LLM to select more relevant content. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3013824309"] = "Please enter a title for the presentation. This will help the LLM to select more relevant content." + +-- Please provide a title +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3049299559"] = "Please provide a title" + +-- Create Slides +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3079776593"] = "Create Slides" + +-- Please specify the audience for the planned presentation. This will help the LLM to create a presentation that fits your needs. You can specify the audience profile, the age group, organizational level, and the expertise. You don't have to specify all of these parameters, for example you might only want to specify the audience profile and leave the other parameters at their default values. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3465256803"] = "Please specify the audience for the planned presentation. This will help the LLM to create a presentation that fits your needs. You can specify the audience profile, the age group, organizational level, and the expertise. You don't have to specify all of these parameters, for example you might only want to specify the audience profile and leave the other parameters at their default values." + +-- (Optional) Specify aspects that the LLM should consider when creating the slides. For example, the use of emojis or specific topics that should be highlighted. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3476149293"] = "(Optional) Specify aspects that the LLM should consider when creating the slides. For example, the use of emojis or specific topics that should be highlighted." + +-- Audience +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3506118019"] = "Audience" + +-- Time specification (minutes) +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3539067305"] = "Time specification (minutes)" + +-- Audience profile +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3649769130"] = "Audience profile" + +-- Attach documents +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3666048746"] = "Attach documents" + +-- Number of bullet points +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3796347042"] = "Number of bullet points" + +-- Custom target language +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3848935911"] = "Custom target language" + +-- Presentation title +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3893271035"] = "Presentation title" + +-- {0} - Slide Builder Session +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T3922788056"] = "{0} - Slide Builder Session" + +-- Please specify the extent of the planned presentation. This can be the number of slides, the number of bullet points per slide, or the time specification for the presentation. This will help the LLM to create a presentation that fits your needs. Leave the default values if you don't have specific requirements regarding the extent of the presentation. You might only want to specify one of these parameters, for example the time specification, and leave the others at their default values. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T4131419342"] = "Please specify the extent of the planned presentation. This can be the number of slides, the number of bullet points per slide, or the time specification for the presentation. This will help the LLM to create a presentation that fits your needs. Leave the default values if you don't have specific requirements regarding the extent of the presentation. You might only want to specify one of these parameters, for example the time specification, and leave the others at their default values." + +-- Audience expertise +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T4279519256"] = "Audience expertise" + +-- Title +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T617902505"] = "Title" + +-- Please provide a custom language. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T656744944"] = "Please provide a custom language." + -- Your word or phrase UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SYNONYM::ASSISTANTSYNONYMS::T1847246020"] = "Your word or phrase" @@ -1599,6 +1788,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1133040906"] = "Move chat -- Are you sure you want to move this chat? All unsaved changes will be lost. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1142475422"] = "Are you sure you want to move this chat? All unsaved changes will be lost." +-- Bold +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1165397398"] = "Bold" + -- Stop generation UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1317408357"] = "Stop generation" @@ -1611,9 +1803,18 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1849313532"] = "Type your -- Your Prompt (use selected instance '{0}', provider '{1}') UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1967611328"] = "Your Prompt (use selected instance '{0}', provider '{1}')" +-- Code +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2036185364"] = "Code" + +-- Italic +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2377171085"] = "Italic" + -- Profile usage is disabled according to your chat template settings. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2670286472"] = "Profile usage is disabled according to your chat template settings." +-- Bulleted List +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2957125464"] = "Bulleted List" + -- Delete this chat & start a new one. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2991985411"] = "Delete this chat & start a new one." @@ -1632,6 +1833,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T3928697643"] = "Start new -- New disappearing chat UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T4113970938"] = "New disappearing chat" +-- Heading +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T4231005109"] = "Heading" + -- Please select the workspace where you want to move the chat to. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T474393241"] = "Please select the workspace where you want to move the chat to." @@ -1776,6 +1980,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" @@ -2070,6 +2277,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1364944735"] -- Select preview features UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1439783084"] = "Select preview features" +-- Your organization provided a default start page, but you can still change it. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1454730224"] = "Your organization provided a default start page, but you can still change it." + -- Select the desired behavior for the navigation bar. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1555038969"] = "Select the desired behavior for the navigation bar." @@ -2118,6 +2328,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2591284123"] -- Administration settings are visible UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2591866808"] = "Administration settings are visible" +-- Choose which page AI Studio should open first when you start the app. Changes take effect the next time you launch AI Studio. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2655930524"] = "Choose which page AI Studio should open first when you start the app. Changes take effect the next time you launch AI Studio." + -- Save energy? UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3100928009"] = "Save energy?" @@ -2166,6 +2379,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T71162186"] = -- Energy saving is disabled UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T716338721"] = "Energy saving is disabled" +-- Start page +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T78084670"] = "Start page" + -- Preview feature visibility UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T817101267"] = "Preview feature visibility" @@ -2526,6 +2742,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T428040679"] = "Content creation" -- Useful assistants UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VISION::T586430036"] = "Useful assistants" +-- Voice recording has been disabled for this session because audio playback could not be initialized on the client. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T1123032432"] = "Voice recording has been disabled for this session because audio playback could not be initialized on the client." + -- Failed to create the transcription provider. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T1689988905"] = "Failed to create the transcription provider." @@ -2535,6 +2754,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T2144994226"] = "Failed to -- Stop recording and start transcription UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T224155287"] = "Stop recording and start transcription" +-- Voice recording is unavailable because the client could not initialize audio playback. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T2260302339"] = "Voice recording is unavailable because the client could not initialize audio playback." + -- Start recording your voice for a transcription UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T2372624045"] = "Start recording your voice for a transcription" @@ -2565,8 +2787,8 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1016188706"] = "Are you sure -- Move chat UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1133040906"] = "Move chat" --- Unnamed workspace -UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1307384014"] = "Unnamed workspace" +-- Loading chats... +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1364857726"] = "Loading chats..." -- Delete UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1469573738"] = "Delete" @@ -2622,9 +2844,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T323280982"] = "Please enter -- Please enter a workspace name. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3288132732"] = "Please enter a workspace name." --- Unnamed chat -UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3310482275"] = "Unnamed chat" - -- Rename UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3355849203"] = "Rename" @@ -2751,6 +2970,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T380891852"] = "Example -- Actions UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3865031940"] = "Actions" +-- Load system prompt from file +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3883091650"] = "Load system prompt from file" + -- Messages per page UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3893704289"] = "Messages per page" @@ -3543,6 +3765,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" @@ -3564,12 +3789,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." @@ -3750,6 +3981,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T1471770981" -- Preselect whether participants needs to arrive and depart UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T1648427207"] = "Preselect whether participants needs to arrive and depart" +-- Choose whether the assistant should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T1766361623"] = "Choose whether the assistant should use the app default profile, no profile, or a specific profile." + -- Preselect a start time? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T1901151023"] = "Preselect a start time?" @@ -3762,8 +3996,8 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T1998244307" -- Preselect whether the meeting is virtual UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T2084951012"] = "Preselect whether the meeting is virtual" --- Would you like to preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T2221665527"] = "Would you like to preselect one of your profiles?" +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T2322771068"] = "Preselect a profile" -- When enabled, you can preselect most agenda options. This is might be useful when you need to create similar agendas often. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T2373110543"] = "When enabled, you can preselect most agenda options. This is might be useful when you need to create similar agendas often." @@ -3816,9 +4050,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T3709527588" -- Preselect a topic? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T3835166371"] = "Preselect a topic?" --- Preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T4004501229"] = "Preselect one of your profiles?" - -- Preselect the agenda language UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T4055846391"] = "Preselect the agenda language" @@ -3855,15 +4086,18 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T1608 -- Yes, you can only retrieve one bias per day UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T1765683725"] = "Yes, you can only retrieve one bias per day" +-- Choose whether the assistant should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T1766361623"] = "Choose whether the assistant should use the app default profile, no profile, or a specific profile." + -- Reset UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T180921696"] = "Reset" --- Would you like to preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T2221665527"] = "Would you like to preselect one of your profiles?" - -- No restriction. You can retrieve as many biases as you want per day. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T2305356277"] = "No restriction. You can retrieve as many biases as you want per day." +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T2322771068"] = "Preselect a profile" + -- Which language should be preselected? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T2345162613"] = "Which language should be preselected?" @@ -3888,9 +4122,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T3848 -- Options are preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T3875604319"] = "Options are preselected" --- Preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T4004501229"] = "Preselect one of your profiles?" - -- Are you sure you want to reset your bias-of-the-day statistics? The system will no longer remember which biases you already know. As a result, biases you are already familiar with may be addressed again. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T405627382"] = "Are you sure you want to reset your bias-of-the-day statistics? The system will no longer remember which biases you already know. As a result, biases you are already familiar with may be addressed again." @@ -3921,8 +4152,11 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T1773585398"] -- Provider selection when creating new chats UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T189306836"] = "Provider selection when creating new chats" --- Would you like to set one of your profiles as the default for chats? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T1933521846"] = "Would you like to set one of your profiles as the default for chats?" +-- Choose whether chats should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T1915793195"] = "Choose whether chats should use the app default profile, no profile, or a specific profile." + +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T2322771068"] = "Preselect a profile" -- Apply default data source option when sending assistant results to chat UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T2510376349"] = "Apply default data source option when sending assistant results to chat" @@ -3960,9 +4194,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T3730599555"] -- Latest message is shown, after loading a chat UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T3755993611"] = "Latest message is shown, after loading a chat" --- Preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T4004501229"] = "Preselect one of your profiles?" - -- Do you want to apply the default data source options when sending assistant results to chat? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T4033153439"] = "Do you want to apply the default data source options when sending assistant results to chat?" @@ -4023,11 +4254,14 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T1073540083" -- Compiler messages are preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T1110902070"] = "Compiler messages are preselected" +-- Choose whether the assistant should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T1766361623"] = "Choose whether the assistant should use the app default profile, no profile, or a specific profile." + -- Preselect a programming language UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T2181567002"] = "Preselect a programming language" --- Would you like to preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T2221665527"] = "Would you like to preselect one of your profiles?" +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T2322771068"] = "Preselect a profile" -- When enabled, you can preselect the coding options. This is might be useful when you prefer a specific programming language or LLM model. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T2619641701"] = "When enabled, you can preselect the coding options. This is might be useful when you prefer a specific programming language or LLM model." @@ -4044,9 +4278,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T3015105896" -- Coding options are preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T3567850751"] = "Coding options are preselected" --- Preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T4004501229"] = "Preselect one of your profiles?" - -- Preselect another programming language UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T4230412334"] = "Preselect another programming language" @@ -4146,14 +4377,17 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T12806662 -- Preselect ERI server options? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T1664055662"] = "Preselect ERI server options?" +-- Choose whether the assistant should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T1766361623"] = "Choose whether the assistant should use the app default profile, no profile, or a specific profile." + -- No ERI server options are preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T1793785587"] = "No ERI server options are preselected" -- Most ERI server options can be customized and saved directly in the ERI server assistant. For this, the ERI server assistant has an auto-save function. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T2093534613"] = "Most ERI server options can be customized and saved directly in the ERI server assistant. For this, the ERI server assistant has an auto-save function." --- Would you like to preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T2221665527"] = "Would you like to preselect one of your profiles?" +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T2322771068"] = "Preselect a profile" -- Close UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T3448155331"] = "Close" @@ -4161,9 +4395,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T34481553 -- Assistant: ERI Server Options UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T3629372826"] = "Assistant: ERI Server Options" --- Preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T4004501229"] = "Preselect one of your profiles?" - -- ERI server options are preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T488190224"] = "ERI server options are preselected" @@ -4311,6 +4542,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T1633101 -- Web content reader is not preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T1701127912"] = "Web content reader is not preselected" +-- Choose whether the assistant should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T1766361623"] = "Choose whether the assistant should use the app default profile, no profile, or a specific profile." + -- Content cleaner agent is not preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T1969816694"] = "Content cleaner agent is not preselected" @@ -4320,8 +4554,8 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T2090693 -- When enabled, you can preselect some legal check options. This is might be useful when you prefer a specific LLM model. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T2164667361"] = "When enabled, you can preselect some legal check options. This is might be useful when you prefer a specific LLM model." --- Would you like to preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T2221665527"] = "Would you like to preselect one of your profiles?" +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T2322771068"] = "Preselect a profile" -- Legal check options are preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T252916114"] = "Legal check options are preselected" @@ -4341,17 +4575,17 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T3641773 -- Preselect the content cleaner agent? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T3649428096"] = "Preselect the content cleaner agent?" --- Preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T4004501229"] = "Preselect one of your profiles?" - -- Assistant: Legal Check Options UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T4033382756"] = "Assistant: Legal Check Options" -- Preselect the web content reader? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T629158142"] = "Preselect the web content reader?" --- Would you like to preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T2221665527"] = "Would you like to preselect one of your profiles?" +-- Choose whether the assistant should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T1766361623"] = "Choose whether the assistant should use the app default profile, no profile, or a specific profile." + +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T2322771068"] = "Preselect a profile" -- Which language should be preselected? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T2345162613"] = "Which language should be preselected?" @@ -4374,9 +4608,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T3710380967 -- Options are preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T3875604319"] = "Options are preselected" --- Preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T4004501229"] = "Preselect one of your profiles?" - -- Preselect options? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T42672465"] = "Preselect options?" @@ -4464,6 +4695,69 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGREWRITE::T3745021518 -- No rewrite & improve text options are preselected UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGREWRITE::T553954963"] = "No rewrite & improve text options are preselected" +-- Preselect the audience expertise +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T1017131030"] = "Preselect the audience expertise" + +-- When enabled, you can preselect slide builder options. This is might be useful when you prefer a specific language or LLM model. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T1393378753"] = "When enabled, you can preselect slide builder options. This is might be useful when you prefer a specific language or LLM model." + +-- Preselect aspects for the LLM to focus on when generating slides, such as bullet points or specific topics to emphasize. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T1528169602"] = "Preselect aspects for the LLM to focus on when generating slides, such as bullet points or specific topics to emphasize." + +-- Choose whether the assistant should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T1766361623"] = "Choose whether the assistant should use the app default profile, no profile, or a specific profile." + +-- Preselect the audience organizational level +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T2014662371"] = "Preselect the audience organizational level" + +-- Which audience organizational level should be preselected? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T216511105"] = "Which audience organizational level should be preselected?" + +-- Preselect Slide Assistant options? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T227645894"] = "Preselect Slide Assistant options?" + +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T2322771068"] = "Preselect a profile" + +-- Which language should be preselected? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T2345162613"] = "Which language should be preselected?" + +-- Preselect another language +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T2382415529"] = "Preselect another language" + +-- Preselect the language +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T2571465005"] = "Preselect the language" + +-- Preselect the audience age group +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T2645589441"] = "Preselect the audience age group" + +-- Assistant: Slide Assistant Options +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T3215549988"] = "Assistant: Slide Assistant Options" + +-- Which audience expertise should be preselected? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T3228597992"] = "Which audience expertise should be preselected?" + +-- Close +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T3448155331"] = "Close" + +-- Preselect important aspects +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T3705987833"] = "Preselect important aspects" + +-- No Slide Assistant options are preselected +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T4214398691"] = "No Slide Assistant options are preselected" + +-- Preselect the audience profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T861397972"] = "Preselect the audience profile" + +-- Slide Assistant options are preselected +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T93124146"] = "Slide Assistant options are preselected" + +-- Which audience age group should be preselected? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T956845877"] = "Which audience age group should be preselected?" + +-- Which audience profile should be preselected? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T973572510"] = "Which audience profile should be preselected?" + -- 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"] = "When enabled, you can preselect synonym options. This is might be useful when you prefer a specific language or LLM model." @@ -4683,6 +4977,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T1417 -- Preselect another target language UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T1462295644"] = "Preselect another target language" +-- Choose whether the assistant should use the app default profile, no profile, or a specific profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T1766361623"] = "Choose whether the assistant should use the app default profile, no profile, or a specific profile." + -- Assistant: Writing E-Mails Options UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T2021226503"] = "Assistant: Writing E-Mails Options" @@ -4692,8 +4989,8 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T2116 -- Preselect your name for the closing salutation? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T221974240"] = "Preselect your name for the closing salutation?" --- Would you like to preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T2221665527"] = "Would you like to preselect one of your profiles?" +-- Preselect a profile +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T2322771068"] = "Preselect a profile" -- Preselect a writing style UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T28456020"] = "Preselect a writing style" @@ -4713,9 +5010,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T3547 -- Preselect e-mail options? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T3832719342"] = "Preselect e-mail options?" --- Preselect one of your profiles? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T4004501229"] = "Preselect one of your profiles?" - -- Save UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T1294818664"] = "Save" @@ -4902,6 +5196,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1617786407"] = "Coding" -- Analyze a text or an email for tasks you need to complete. UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1728590051"] = "Analyze a text or an email for tasks you need to complete." +-- Slide Assistant +UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1883918574"] = "Slide Assistant" + -- Text Summarizer UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1907192403"] = "Text Summarizer" @@ -4944,6 +5241,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T3011450657"] = "My Tasks" -- E-Mail UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T3026443472"] = "E-Mail" +-- Develop slide content based on a given topic and content. +UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T311912219"] = "Develop slide content based on a given topic and content." + -- Translate AI Studio text content into other languages UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T3181803840"] = "Translate AI Studio text content into other languages" @@ -4992,6 +5292,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T878695986"] = "Learn about one co -- Localization UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T897888480"] = "Localization" +-- Reload your workspaces +UI_TEXT_CONTENT["AISTUDIO::PAGES::CHAT::T194629703"] = "Reload your workspaces" + -- Hide your workspaces UI_TEXT_CONTENT["AISTUDIO::PAGES::CHAT::T2351468526"] = "Hide your workspaces" @@ -5301,6 +5604,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." @@ -5322,6 +5628,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." @@ -5595,12 +5904,21 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1188453609 -- Show also prototype features: these are works in progress; expect bugs and missing features UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1245257804"] = "Show also prototype features: these are works in progress; expect bugs and missing features" +-- Settings +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1258653480"] = "Settings" + -- No key is sending the input; you have to click the send button UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1311973034"] = "No key is sending the input; you have to click the send button" -- Navigation never expands, no tooltips; there are only icons UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1402851833"] = "Navigation never expands, no tooltips; there are only icons" +-- Welcome +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1485461907"] = "Welcome" + +-- Assistants +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1614176092"] = "Assistants" + -- Store chats automatically UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1664293672"] = "Store chats automatically" @@ -5625,6 +5943,9 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T2195945406 -- Install updates manually UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T220653235"] = "Install updates manually" +-- Plugins +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T2222816203"] = "Plugins" + -- Also show features ready for release; these should be stable UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T2301448762"] = "Also show features ready for release; these should be stable" @@ -5646,6 +5967,9 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T2566503670 -- No minimum confidence level chosen UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T2828607242"] = "No minimum confidence level chosen" +-- Supporters +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T2929332068"] = "Supporters" + -- Do not specify the language UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T2960082609"] = "Do not specify the language" @@ -5667,6 +5991,9 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3491430707 -- Install updates automatically UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3569059463"] = "Install updates automatically" +-- Use app default profile +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3587225583"] = "Use app default profile" + -- Disable workspaces UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3612390107"] = "Disable workspaces" @@ -5676,9 +6003,15 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3711207137 -- Also show features in alpha: these are in development; expect bugs and missing features UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T4146964761"] = "Also show features in alpha: these are in development; expect bugs and missing features" +-- Information +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T4256323669"] = "Information" + -- All preview features are hidden UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T4289410063"] = "All preview features are hidden" +-- Chat +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T578410699"] = "Chat" + -- When possible, use the LLM provider which was used for each chat in the first place UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T75376144"] = "When possible, use the LLM provider which was used for each chat in the first place" @@ -5847,6 +6180,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T1546040625"] = "My Task -- Grammar & Spelling Assistant UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T166453786"] = "Grammar & Spelling Assistant" +-- Slide Assistant +UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T1883918574"] = "Slide Assistant" + -- Legal Check Assistant UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T1886447798"] = "Legal Check Assistant" @@ -5907,6 +6243,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" @@ -6557,3 +6902,6 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T1307384014"] = "Unnamed w -- Delete Chat UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T2244038752"] = "Delete Chat" + +-- Unnamed chat +UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T3310482275"] = "Unnamed chat" diff --git a/app/MindWork AI Studio/Program.cs b/app/MindWork AI Studio/Program.cs index 85e97b07..f19344d6 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 => @@ -160,6 +169,7 @@ internal sealed class Program builder.Services.AddMudMarkdownClipboardService(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddTransient(); @@ -169,7 +179,7 @@ internal sealed class Program builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); - builder.Services.AddSingleton(databaseClient); + builder.Services.AddSingleton(databaseClient); builder.Services.AddHostedService(); builder.Services.AddHostedService(); 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/Routes.razor.cs b/app/MindWork AI Studio/Routes.razor.cs index 932a7190..7a43b89d 100644 --- a/app/MindWork AI Studio/Routes.razor.cs +++ b/app/MindWork AI Studio/Routes.razor.cs @@ -22,6 +22,7 @@ public sealed partial class Routes public const string ASSISTANT_EMAIL = "/assistant/email"; public const string ASSISTANT_LEGAL_CHECK = "/assistant/legal-check"; public const string ASSISTANT_SYNONYMS = "/assistant/synonyms"; + public const string ASSISTANT_SLIDE_BUILDER = "/assistant/slide-builder"; public const string ASSISTANT_MY_TASKS = "/assistant/my-tasks"; public const string ASSISTANT_JOB_POSTING = "/assistant/job-posting"; public const string ASSISTANT_BIAS = "/assistant/bias-of-the-day"; @@ -30,4 +31,4 @@ public sealed partial class Routes public const string ASSISTANT_DOCUMENT_ANALYSIS = "/assistant/document-analysis"; public const string ASSISTANT_DYNAMIC = "/assistant/dynamic"; // ReSharper restore InconsistentNaming -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Settings/ConfigMeta.cs b/app/MindWork AI Studio/Settings/ConfigMeta.cs index 6b81c3e8..46a248b3 100644 --- a/app/MindWork AI Studio/Settings/ConfigMeta.cs +++ b/app/MindWork AI Studio/Settings/ConfigMeta.cs @@ -36,6 +36,16 @@ public record ConfigMeta : ConfigMetaBase /// The ID of the plugin that locked this configuration. /// public Guid LockedByConfigPluginId { get; private set; } + + /// + /// How this setting is managed by a configuration plugin, if at all. + /// + public ManagedConfigurationMode? ManagedMode { get; private set; } + + /// + /// The ID of the plugin that currently provides an editable default value. + /// + public Guid EditableDefaultByConfigPluginId { get; private set; } /// /// The default value for the configuration property. This is used when resetting the property to its default state. @@ -65,6 +75,8 @@ public record ConfigMeta : ConfigMetaBase { this.IsLocked = true; this.LockedByConfigPluginId = pluginId; + this.ManagedMode = ManagedConfigurationMode.LOCKED; + this.EditableDefaultByConfigPluginId = Guid.Empty; } /// @@ -75,6 +87,9 @@ public record ConfigMeta : ConfigMetaBase { this.IsLocked = false; this.LockedByConfigPluginId = Guid.Empty; + if (this.ManagedMode is ManagedConfigurationMode.LOCKED) + this.ManagedMode = null; + this.Reset(); } @@ -85,6 +100,30 @@ public record ConfigMeta : ConfigMetaBase { this.IsLocked = false; this.LockedByConfigPluginId = Guid.Empty; + if (this.ManagedMode is ManagedConfigurationMode.LOCKED) + this.ManagedMode = null; + } + + /// + /// Marks the setting as having an editable default provided by a configuration plugin. + /// + public void SetEditableDefaultConfiguration(Guid pluginId) + { + this.IsLocked = false; + this.LockedByConfigPluginId = Guid.Empty; + this.ManagedMode = ManagedConfigurationMode.EDITABLE_DEFAULT; + this.EditableDefaultByConfigPluginId = pluginId; + } + + /// + /// Clears the editable-default state without changing the current value. + /// + public void ClearEditableDefaultConfiguration() + { + if (this.ManagedMode is ManagedConfigurationMode.EDITABLE_DEFAULT) + this.ManagedMode = null; + + this.EditableDefaultByConfigPluginId = Guid.Empty; } /// @@ -129,4 +168,17 @@ public record ConfigMeta : ConfigMetaBase if (memberExpression.Member is System.Reflection.PropertyInfo propertyInfo) propertyInfo.SetValue(configInstance, value); } + + /// + /// Gets the current value of the configuration property. + /// + public TValue GetValue() + { + var configInstance = this.ConfigSelection.Compile().Invoke(SETTINGS_MANAGER.ConfigurationData); + var memberExpression = this.PropertyExpression.GetMemberExpression(); + if (memberExpression.Member is System.Reflection.PropertyInfo propertyInfo && propertyInfo.GetValue(configInstance) is TValue value) + return value; + + return default!; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/ConfigurableAssistant.cs b/app/MindWork AI Studio/Settings/ConfigurableAssistant.cs index f1076d9e..d2a8a76e 100644 --- a/app/MindWork AI Studio/Settings/ConfigurableAssistant.cs +++ b/app/MindWork AI Studio/Settings/ConfigurableAssistant.cs @@ -23,6 +23,7 @@ public enum ConfigurableAssistant BIAS_DAY_ASSISTANT, ERI_ASSISTANT, DOCUMENT_ANALYSIS_ASSISTANT, + SLIDE_BUILDER_ASSISTANT, // ReSharper disable InconsistentNaming I18N_ASSISTANT, diff --git a/app/MindWork AI Studio/Settings/ConfigurationSelectDataFactory.cs b/app/MindWork AI Studio/Settings/ConfigurationSelectDataFactory.cs index 7aa2441e..c6465e5b 100644 --- a/app/MindWork AI Studio/Settings/ConfigurationSelectDataFactory.cs +++ b/app/MindWork AI Studio/Settings/ConfigurationSelectDataFactory.cs @@ -2,6 +2,7 @@ using AIStudio.Assistants.Agenda; using AIStudio.Assistants.Coding; using AIStudio.Assistants.IconFinder; using AIStudio.Assistants.RewriteImprove; +using AIStudio.Assistants.SlideBuilder; using AIStudio.Assistants.TextSummarizer; using AIStudio.Assistants.EMail; using AIStudio.Provider; @@ -132,6 +133,17 @@ public static class ConfigurationSelectDataFactory yield return new(TB("Always expand navigation"), NavBehavior.ALWAYS_EXPAND); } + public static IEnumerable> GetStartPageData() + { + yield return new(TB("Welcome"), StartPage.HOME); + yield return new(TB("Chat"), StartPage.CHAT); + yield return new(TB("Assistants"), StartPage.ASSISTANTS); + yield return new(TB("Information"), StartPage.INFORMATION); + yield return new(TB("Plugins"), StartPage.PLUGINS); + yield return new(TB("Supporters"), StartPage.SUPPORTERS); + yield return new(TB("Settings"), StartPage.SETTINGS); + } + public static IEnumerable> GetIconSourcesData() { foreach (var source in Enum.GetValues()) @@ -197,6 +209,30 @@ public static class ConfigurationSelectDataFactory foreach (var voice in Enum.GetValues()) yield return new(voice.Name(), voice); } + + public static IEnumerable> GetSlideBuilderAudienceProfileData() + { + foreach (var profile in Enum.GetValues()) + yield return new(profile.Name(), profile); + } + + public static IEnumerable> GetSlideBuilderAudienceAgeGroupData() + { + foreach (var ageGroup in Enum.GetValues()) + yield return new(ageGroup.Name(), ageGroup); + } + + public static IEnumerable> GetSlideBuilderAudienceOrganizationalLevelData() + { + foreach (var level in Enum.GetValues()) + yield return new(level.Name(), level); + } + + public static IEnumerable> GetSlideBuilderAudienceExpertiseData() + { + foreach (var expertise in Enum.GetValues()) + yield return new(expertise.Name(), expertise); + } public static IEnumerable> GetProfilesData(IEnumerable profiles) { @@ -204,6 +240,15 @@ public static class ConfigurationSelectDataFactory yield return new(profile.GetSafeName(), profile.Id); } + public static IEnumerable> GetComponentProfilesData(IEnumerable profiles) + { + yield return new(TB("Use app default profile"), ProfilePreselection.AppDefault); + yield return new(Profile.NO_PROFILE.GetSafeName(), ProfilePreselection.NoProfile); + + foreach (var profile in profiles) + yield return new(profile.GetSafeName(), ProfilePreselection.Specific(profile.Id)); + } + public static IEnumerable> GetTranscriptionProvidersData(IEnumerable transcriptionProviders) { foreach (var provider in transcriptionProviders) @@ -254,4 +299,4 @@ public static class ConfigurationSelectDataFactory foreach (var theme in Enum.GetValues()) yield return new(theme.GetName(), theme); } -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Settings/DataModel/Data.cs b/app/MindWork AI Studio/Settings/DataModel/Data.cs index f174d6c2..d6339739 100644 --- a/app/MindWork AI Studio/Settings/DataModel/Data.cs +++ b/app/MindWork AI Studio/Settings/DataModel/Data.cs @@ -51,6 +51,11 @@ public sealed class Data /// public List EnabledPlugins { get; set; } = []; + /// + /// Metadata for managed settings that use a plugin-provided editable default. + /// + public Dictionary ManagedEditableDefaults { get; set; } = []; + /// /// The next provider number to use. /// @@ -118,6 +123,8 @@ public sealed class Data public DataEMail EMail { get; init; } = new(); + public DataSlideBuilder SlideBuilder { get; init; } = new(); + public DataLegalCheck LegalCheck { get; init; } = new(); public DataSynonyms Synonyms { get; init; } = new(); diff --git a/app/MindWork AI Studio/Settings/DataModel/DataApp.cs b/app/MindWork AI Studio/Settings/DataModel/DataApp.cs index a1def46f..3a62164b 100644 --- a/app/MindWork AI Studio/Settings/DataModel/DataApp.cs +++ b/app/MindWork AI Studio/Settings/DataModel/DataApp.cs @@ -52,6 +52,11 @@ public sealed class DataApp(Expression>? configSelection = n /// public NavBehavior NavigationBehavior { get; set; } = NavBehavior.NEVER_EXPAND_USE_TOOLTIPS; + /// + /// Which page should be opened first when the app starts? + /// + public StartPage StartPage { get; set; } = ManagedConfiguration.Register(configSelection, n => n.StartPage, StartPage.HOME); + /// /// The visibility setting for previews features. /// diff --git a/app/MindWork AI Studio/Settings/DataModel/DataSlideBuilder.cs b/app/MindWork AI Studio/Settings/DataModel/DataSlideBuilder.cs new file mode 100644 index 00000000..1702cde4 --- /dev/null +++ b/app/MindWork AI Studio/Settings/DataModel/DataSlideBuilder.cs @@ -0,0 +1,62 @@ +using AIStudio.Assistants.SlideBuilder; +using AIStudio.Provider; + +namespace AIStudio.Settings.DataModel; + +public class DataSlideBuilder +{ + /// + /// Preselect any Slide Builder options? + /// + public bool PreselectOptions { get; set; } = true; + + /// + /// Preselect a profile? + /// + public string PreselectedProfile { get; set; } = string.Empty; + + /// + /// Preselect a Slide Builder provider? + /// + public string PreselectedProvider { get; set; } = string.Empty; + + /// + /// Preselect the target language? + /// + public CommonLanguages PreselectedTargetLanguage { get; set; } + + /// + /// Preselect any other language? + /// + public string PreselectedOtherLanguage { get; set; } = string.Empty; + + /// + /// Preselect the audience profile? + /// + public AudienceProfile PreselectedAudienceProfile { get; set; } + + /// + /// Preselect the audience age group? + /// + public AudienceAgeGroup PreselectedAudienceAgeGroup { get; set; } + + /// + /// Preselect the audience organizational level? + /// + public AudienceOrganizationalLevel PreselectedAudienceOrganizationalLevel { get; set; } + + /// + /// Preselect the audience expertise? + /// + public AudienceExpertise PreselectedAudienceExpertise { get; set; } + + /// + /// Preselect any important aspects that the Slide Builder should take into account? + /// + public string PreselectedImportantAspects { get; set; } = string.Empty; + + /// + /// The minimum confidence level required for a provider to be considered. + /// + public ConfidenceLevel MinimumProviderConfidence { get; set; } = ConfidenceLevel.NONE; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataModel/PreviewFeaturesExtensions.cs b/app/MindWork AI Studio/Settings/DataModel/PreviewFeaturesExtensions.cs index 00399f0b..fa10ecad 100644 --- a/app/MindWork AI Studio/Settings/DataModel/PreviewFeaturesExtensions.cs +++ b/app/MindWork AI Studio/Settings/DataModel/PreviewFeaturesExtensions.cs @@ -32,6 +32,7 @@ public static class PreviewFeaturesExtensions { PreviewFeatures.PRE_READ_PDF_2025 => true, PreviewFeatures.PRE_PLUGINS_2025 => true, + PreviewFeatures.PRE_DOCUMENT_ANALYSIS_2025 => true, _ => false }; diff --git a/app/MindWork AI Studio/Settings/DataModel/StartPage.cs b/app/MindWork AI Studio/Settings/DataModel/StartPage.cs new file mode 100644 index 00000000..07b95b1a --- /dev/null +++ b/app/MindWork AI Studio/Settings/DataModel/StartPage.cs @@ -0,0 +1,12 @@ +namespace AIStudio.Settings.DataModel; + +public enum StartPage +{ + HOME, + CHAT, + ASSISTANTS, + INFORMATION, + PLUGINS, + SUPPORTERS, + SETTINGS, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataModel/StartPageExtensions.cs b/app/MindWork AI Studio/Settings/DataModel/StartPageExtensions.cs new file mode 100644 index 00000000..d37ca52b --- /dev/null +++ b/app/MindWork AI Studio/Settings/DataModel/StartPageExtensions.cs @@ -0,0 +1,17 @@ +namespace AIStudio.Settings.DataModel; + +public static class StartPageExtensions +{ + public static string ToRoute(this StartPage startPage) => startPage switch + { + StartPage.HOME => string.Empty, + StartPage.CHAT => Routes.CHAT, + StartPage.ASSISTANTS => Routes.ASSISTANTS, + StartPage.INFORMATION => Routes.ABOUT, + StartPage.PLUGINS => Routes.PLUGINS, + StartPage.SUPPORTERS => Routes.SUPPORTERS, + StartPage.SETTINGS => Routes.SETTINGS, + + _ => string.Empty, + }; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/ManagedConfiguration.Parsing.cs b/app/MindWork AI Studio/Settings/ManagedConfiguration.Parsing.cs index e4cf5f2e..4b453d27 100644 --- a/app/MindWork AI Studio/Settings/ManagedConfiguration.Parsing.cs +++ b/app/MindWork AI Studio/Settings/ManagedConfiguration.Parsing.cs @@ -63,8 +63,10 @@ public static partial class ManagedConfiguration if(dryRun) return successful; - - return HandleParsedValue(configPluginId, dryRun, successful, configMeta, configuredValue); + + var settingName = SettingName(propertyExpression); + var managedMode = ReadManagedConfigurationMode(propertyExpression, settings); + return HandleParsedScalarValue(configPluginId, dryRun, successful, configMeta, configuredValue, managedMode, settingName); } /// @@ -128,8 +130,10 @@ public static partial class ManagedConfiguration if(dryRun) return successful; - - return HandleParsedValue(configPluginId, dryRun, successful, configMeta, configuredValue); + + var settingName = SettingName(propertyExpression); + var managedMode = ReadManagedConfigurationMode(propertyExpression, settings); + return HandleParsedScalarValue(configPluginId, dryRun, successful, configMeta, configuredValue, managedMode, settingName); } /// @@ -216,7 +220,9 @@ public static partial class ManagedConfiguration } } - return HandleParsedValue(configPluginId, dryRun, successful, configMeta, configuredValue); + var settingName = SettingName(propertyExpression); + var managedMode = ReadManagedConfigurationMode(propertyExpression, settings); + return HandleParsedScalarValue(configPluginId, dryRun, successful, configMeta, configuredValue, managedMode, settingName); } /// @@ -857,4 +863,91 @@ public static partial class ManagedConfiguration return successful; } + + private static bool HandleParsedScalarValue( + Guid configPluginId, + bool dryRun, + bool successful, + ConfigMeta configMeta, + TValue configuredValue, + ManagedConfigurationMode managedMode, + string settingName) + { + if (dryRun) + return successful; + + switch (successful) + { + case true when managedMode is ManagedConfigurationMode.LOCKED: + ClearEditableDefaultState(settingName); + configMeta.ClearEditableDefaultConfiguration(); + configMeta.SetValue(configuredValue); + configMeta.LockConfiguration(configPluginId); + break; + + case true when managedMode is ManagedConfigurationMode.EDITABLE_DEFAULT: + var currentValueSerialized = SerializeManagedScalarValue(configMeta.GetValue()); + var configuredValueSerialized = SerializeManagedScalarValue(configuredValue); + + string lastAppliedValue; + if (!TryGetEditableDefaultState(settingName, out var editableDefaultState)) + { + configMeta.SetValue(configuredValue); + lastAppliedValue = configuredValueSerialized; + } + else + { + lastAppliedValue = editableDefaultState.LastAppliedValue; + if (string.Equals(currentValueSerialized, lastAppliedValue, StringComparison.Ordinal)) + { + configMeta.SetValue(configuredValue); + lastAppliedValue = configuredValueSerialized; + } + } + + SetEditableDefaultState(settingName, configPluginId, lastAppliedValue); + configMeta.UnlockConfiguration(); + configMeta.SetEditableDefaultConfiguration(configPluginId); + break; + + case false when configMeta.IsLocked && configMeta.LockedByConfigPluginId == configPluginId: + configMeta.ResetLockedConfiguration(); + break; + + case false when configMeta.ManagedMode is ManagedConfigurationMode.EDITABLE_DEFAULT + && TryGetEditableDefaultState(settingName, out var editableDefaultStateToRemove) + && editableDefaultStateToRemove.ConfigPluginId == configPluginId: + configMeta.ClearEditableDefaultConfiguration(); + ClearEditableDefaultState(settingName); + break; + } + + return successful; + } + + private static ManagedConfigurationMode ReadManagedConfigurationMode( + Expression> propertyExpression, + LuaTable settings) + { + var allowUserOverrideSettingName = $"{SettingsManager.ToSettingName(propertyExpression)}.AllowUserOverride"; + if (!settings.TryGetValue(allowUserOverrideSettingName, out var allowUserOverrideValue)) + return ManagedConfigurationMode.LOCKED; + + if (allowUserOverrideValue.TryRead(out var allowUserOverride)) + return allowUserOverride ? ManagedConfigurationMode.EDITABLE_DEFAULT : ManagedConfigurationMode.LOCKED; + + if (allowUserOverrideValue.TryRead(out var allowUserOverrideText) && bool.TryParse(allowUserOverrideText, out allowUserOverride)) + return allowUserOverride ? ManagedConfigurationMode.EDITABLE_DEFAULT : ManagedConfigurationMode.LOCKED; + + return ManagedConfigurationMode.LOCKED; + } + + private static string SerializeManagedScalarValue(TValue value) => value switch + { + null => string.Empty, + string text => text, + 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.cs b/app/MindWork AI Studio/Settings/ManagedConfiguration.cs index 363cccc1..0e62f2c6 100644 --- a/app/MindWork AI Studio/Settings/ManagedConfiguration.cs +++ b/app/MindWork AI Studio/Settings/ManagedConfiguration.cs @@ -246,68 +246,68 @@ public static partial class ManagedConfiguration public static bool IsConfigurationLeftOver( Expression> configSelection, Expression> propertyExpression, - IEnumerable availablePlugins) + IReadOnlyList availablePlugins) where TValue : Enum { if (!TryGet(configSelection, propertyExpression, out var configMeta)) return false; - if (configMeta.LockedByConfigPluginId == Guid.Empty || !configMeta.IsLocked) - return false; - - var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.LockedByConfigPluginId); - if (plugin is null) + if (configMeta.LockedByConfigPluginId != Guid.Empty && configMeta.IsLocked) { - configMeta.ResetLockedConfiguration(); - return true; + var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.LockedByConfigPluginId); + if (plugin is null) + { + configMeta.ResetLockedConfiguration(); + return true; + } } - return false; + return CleanupEditableDefaultState(configMeta, SettingName(propertyExpression), availablePlugins); } public static bool IsConfigurationLeftOver( Expression> configSelection, Expression> propertyExpression, - IEnumerable availablePlugins) + IReadOnlyList availablePlugins) { if (!TryGet(configSelection, propertyExpression, out var configMeta)) return false; - if (configMeta.LockedByConfigPluginId == Guid.Empty || !configMeta.IsLocked) - return false; - - var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.LockedByConfigPluginId); - if (plugin is null) + if (configMeta.LockedByConfigPluginId != Guid.Empty && configMeta.IsLocked) { - configMeta.ResetLockedConfiguration(); - return true; + var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.LockedByConfigPluginId); + if (plugin is null) + { + configMeta.ResetLockedConfiguration(); + return true; + } } - return false; + return CleanupEditableDefaultState(configMeta, SettingName(propertyExpression), availablePlugins); } // ReSharper disable MethodOverloadWithOptionalParameter public static bool IsConfigurationLeftOver( Expression> configSelection, Expression> propertyExpression, - IEnumerable availablePlugins, + IReadOnlyList availablePlugins, ISpanParsable? _ = null) where TValue : struct, ISpanParsable { if (!TryGet(configSelection, propertyExpression, out var configMeta)) return false; - if (configMeta.LockedByConfigPluginId == Guid.Empty || !configMeta.IsLocked) - return false; - - var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.LockedByConfigPluginId); - if (plugin is null) + if (configMeta.LockedByConfigPluginId != Guid.Empty && configMeta.IsLocked) { - configMeta.ResetLockedConfiguration(); - return true; + var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.LockedByConfigPluginId); + if (plugin is null) + { + configMeta.ResetLockedConfiguration(); + return true; + } } - return false; + return CleanupEditableDefaultState(configMeta, SettingName(propertyExpression), availablePlugins); } // ReSharper restore MethodOverloadWithOptionalParameter @@ -413,4 +413,44 @@ public static partial class ManagedConfiguration var configPath = $"{configName}.{className}.{propertyName}"; return configPath; } + + private static string SettingName(Expression> propertyExpression) => SettingsManager.ToSettingName(propertyExpression); + + private static bool TryGetEditableDefaultState(string settingName, out ManagedEditableDefaultState editableDefaultState) + { + return SETTINGS_MANAGER.ConfigurationData.ManagedEditableDefaults.TryGetValue(settingName, out editableDefaultState!); + } + + private static void SetEditableDefaultState(string settingName, Guid pluginId, string lastAppliedValue) + { + SETTINGS_MANAGER.ConfigurationData.ManagedEditableDefaults[settingName] = new() + { + ConfigPluginId = pluginId, + LastAppliedValue = lastAppliedValue, + }; + } + + private static bool ClearEditableDefaultState(string settingName) => SETTINGS_MANAGER.ConfigurationData.ManagedEditableDefaults.Remove(settingName); + + private static bool CleanupEditableDefaultState( + ConfigMeta configMeta, + string settingName, + IReadOnlyList availablePlugins) + { + if (!TryGetEditableDefaultState(settingName, out var editableDefaultState)) + { + if (configMeta.ManagedMode is not ManagedConfigurationMode.EDITABLE_DEFAULT) + return false; + + configMeta.ClearEditableDefaultConfiguration(); + return true; + } + + var plugin = availablePlugins.FirstOrDefault(x => x.Id == editableDefaultState.ConfigPluginId); + if (plugin is not null) + return false; + + configMeta.ClearEditableDefaultConfiguration(); + return ClearEditableDefaultState(settingName); + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/ManagedConfigurationMode.cs b/app/MindWork AI Studio/Settings/ManagedConfigurationMode.cs new file mode 100644 index 00000000..324a5748 --- /dev/null +++ b/app/MindWork AI Studio/Settings/ManagedConfigurationMode.cs @@ -0,0 +1,14 @@ +namespace AIStudio.Settings; + +public enum ManagedConfigurationMode +{ + /// + /// The configuration is locked by a configuration plugin. The user cannot change the value of this setting, and it will be overridden by the plugin on each update. + /// + LOCKED, + + /// + /// The configuration has an editable default provided by a configuration plugin. The user can change the value of this setting. + /// + EDITABLE_DEFAULT, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/ManagedEditableDefaultState.cs b/app/MindWork AI Studio/Settings/ManagedEditableDefaultState.cs new file mode 100644 index 00000000..8904c497 --- /dev/null +++ b/app/MindWork AI Studio/Settings/ManagedEditableDefaultState.cs @@ -0,0 +1,8 @@ +namespace AIStudio.Settings; + +public sealed class ManagedEditableDefaultState +{ + public Guid ConfigPluginId { get; init; } = Guid.Empty; + + public string LastAppliedValue { get; init; } = string.Empty; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/ProfilePreselection.cs b/app/MindWork AI Studio/Settings/ProfilePreselection.cs new file mode 100644 index 00000000..ba4b55e1 --- /dev/null +++ b/app/MindWork AI Studio/Settings/ProfilePreselection.cs @@ -0,0 +1,55 @@ +namespace AIStudio.Settings; + +public readonly record struct ProfilePreselection +{ + public ProfilePreselectionMode Mode { get; } + + public string SpecificProfileId { get; } + + public bool UseAppDefault => this.Mode == ProfilePreselectionMode.USE_APP_DEFAULT; + + public bool DoNotPreselectProfile => this.Mode == ProfilePreselectionMode.USE_NO_PROFILE; + + public bool UseSpecificProfile => this.Mode == ProfilePreselectionMode.USE_SPECIFIC_PROFILE; + + public static ProfilePreselection AppDefault => new(ProfilePreselectionMode.USE_APP_DEFAULT, string.Empty); + + public static ProfilePreselection NoProfile => new(ProfilePreselectionMode.USE_NO_PROFILE, Profile.NO_PROFILE.Id); + + private ProfilePreselection(ProfilePreselectionMode mode, string specificProfileId) + { + this.Mode = mode; + this.SpecificProfileId = specificProfileId; + } + + public static ProfilePreselection Specific(string profileId) + { + if (string.IsNullOrWhiteSpace(profileId)) + throw new ArgumentException("A specific profile preselection requires a profile ID.", nameof(profileId)); + + if (profileId.Equals(Profile.NO_PROFILE.Id, StringComparison.OrdinalIgnoreCase)) + throw new ArgumentException("Use NoProfile for the NO_PROFILE selection.", nameof(profileId)); + + return new(ProfilePreselectionMode.USE_SPECIFIC_PROFILE, profileId); + } + + public static ProfilePreselection FromStoredValue(string? storedValue) + { + if (string.IsNullOrWhiteSpace(storedValue)) + return AppDefault; + + if (storedValue.Equals(Profile.NO_PROFILE.Id, StringComparison.OrdinalIgnoreCase)) + return NoProfile; + + return new(ProfilePreselectionMode.USE_SPECIFIC_PROFILE, storedValue); + } + + public static implicit operator string(ProfilePreselection preselection) => preselection.Mode switch + { + ProfilePreselectionMode.USE_APP_DEFAULT => string.Empty, + ProfilePreselectionMode.USE_NO_PROFILE => Profile.NO_PROFILE.Id, + ProfilePreselectionMode.USE_SPECIFIC_PROFILE => preselection.SpecificProfileId, + + _ => string.Empty, + }; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/ProfilePreselectionMode.cs b/app/MindWork AI Studio/Settings/ProfilePreselectionMode.cs new file mode 100644 index 00000000..8addad93 --- /dev/null +++ b/app/MindWork AI Studio/Settings/ProfilePreselectionMode.cs @@ -0,0 +1,8 @@ +namespace AIStudio.Settings; + +public enum ProfilePreselectionMode +{ + USE_APP_DEFAULT, + USE_NO_PROFILE, + USE_SPECIFIC_PROFILE, +} \ 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/Settings/ProviderExtensions.Alibaba.cs b/app/MindWork AI Studio/Settings/ProviderExtensions.Alibaba.cs index c57aeeed..2a38c9fb 100644 --- a/app/MindWork AI Studio/Settings/ProviderExtensions.Alibaba.cs +++ b/app/MindWork AI Studio/Settings/ProviderExtensions.Alibaba.cs @@ -24,6 +24,17 @@ public static partial class ProviderExtensions Capability.CHAT_COMPLETION_API, ]; + // Check for Qwen 3.5: + if(modelName.StartsWith("qwen3.5")) + return + [ + Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, + Capability.TEXT_OUTPUT, + + Capability.OPTIONAL_REASONING, Capability.FUNCTION_CALLING, + Capability.CHAT_COMPLETION_API, + ]; + // Check for Qwen 3: if(modelName.StartsWith("qwen3")) return diff --git a/app/MindWork AI Studio/Settings/ProviderExtensions.OpenSource.cs b/app/MindWork AI Studio/Settings/ProviderExtensions.OpenSource.cs index fefa87dc..dc30e53b 100644 --- a/app/MindWork AI Studio/Settings/ProviderExtensions.OpenSource.cs +++ b/app/MindWork AI Studio/Settings/ProviderExtensions.OpenSource.cs @@ -102,6 +102,17 @@ public static partial class ProviderExtensions Capability.CHAT_COMPLETION_API, ]; + // Check for Qwen 3.5: + if(modelName.IndexOf("qwen3.5") is not -1) + return + [ + Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, + Capability.TEXT_OUTPUT, + + Capability.ALWAYS_REASONING, Capability.FUNCTION_CALLING, + Capability.CHAT_COMPLETION_API, + ]; + if(modelName.IndexOf("-vl-") is not -1) return [ Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, diff --git a/app/MindWork AI Studio/Settings/SettingsManager.cs b/app/MindWork AI Studio/Settings/SettingsManager.cs index d4bfc7e3..50c8c03e 100644 --- a/app/MindWork AI Studio/Settings/SettingsManager.cs +++ b/app/MindWork AI Studio/Settings/SettingsManager.cs @@ -52,6 +52,16 @@ public sealed class SettingsManager /// public bool IsDarkMode { get; set; } + /// + /// Ensures that the startup start-page redirect is evaluated at most once per app session. + /// + public bool StartupStartPageRedirectHandled { get; set; } + + /// + /// Indicates that the initial settings load attempt has completed. + /// + public bool HasCompletedInitialSettingsLoad { get; private set; } + /// /// The configuration data. /// @@ -63,18 +73,31 @@ public sealed class SettingsManager /// Loads the settings from the file system. /// public async Task LoadSettings() + { + var settingsSnapshot = await this.TryReadSettingsSnapshot(); + if (settingsSnapshot is not null) + this.ConfigurationData = settingsSnapshot; + + this.HasCompletedInitialSettingsLoad = true; + } + + /// + /// Reads the settings from disk without mutating the current in-memory state. + /// + /// A (migrated) settings snapshot, or null if it could not be read. + public async Task TryReadSettingsSnapshot() { if(!this.IsSetUp) { this.logger.LogWarning("Cannot load settings, because the configuration is not set up yet."); - return; + return null; } var settingsPath = Path.Combine(ConfigDirectory!, SETTINGS_FILENAME); if(!File.Exists(settingsPath)) { this.logger.LogWarning("Cannot load settings, because the settings file does not exist."); - return; + return null; } // We read the `"Version": "V3"` line to determine the version of the settings file: @@ -87,30 +110,28 @@ public sealed class SettingsManager // Extract the version from the line: var settingsVersionText = line.Split('"')[3]; - + // Parse the version: Enum.TryParse(settingsVersionText, out Version settingsVersion); if(settingsVersion is Version.UNKNOWN) { this.logger.LogError("Unknown version of the settings file found."); - this.ConfigurationData = new(); - return; + return new(); } - - this.ConfigurationData = SettingsMigrations.Migrate(this.logger, settingsVersion, await File.ReadAllTextAsync(settingsPath), JSON_OPTIONS); - + + 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. // - this.ConfigurationData.App.EnabledPreviewFeatures = this.ConfigurationData.App.PreviewVisibility.FilterPreviewFeatures(this.ConfigurationData.App.EnabledPreviewFeatures); - - return; + settingsData.App.EnabledPreviewFeatures = settingsData.App.PreviewVisibility.FilterPreviewFeatures(settingsData.App.EnabledPreviewFeatures); + return settingsData; } - + this.logger.LogError("Failed to read the version of the settings file."); - this.ConfigurationData = new(); + return new(); } /// @@ -285,12 +306,32 @@ public sealed class SettingsManager public Profile GetPreselectedProfile(Tools.Components component) { - var preselection = component.PreselectedProfile(this); - if (preselection != Profile.NO_PROFILE) - return preselection; - - preselection = this.ConfigurationData.Profiles.FirstOrDefault(x => x.Id.Equals(this.ConfigurationData.App.PreselectedProfile, StringComparison.OrdinalIgnoreCase)); - return preselection ?? Profile.NO_PROFILE; + var preselection = component.GetProfilePreselection(this); + if (preselection.DoNotPreselectProfile) + 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; + } + + 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; + } + + public Profile GetAppPreselectedProfile() + { + 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; } public ChatTemplate GetPreselectedChatTemplate(Tools.Components component) diff --git a/app/MindWork AI Studio/Tools/AssistantVisibilityExtensions.cs b/app/MindWork AI Studio/Tools/AssistantVisibilityExtensions.cs index cb4e7fdc..29db307d 100644 --- a/app/MindWork AI Studio/Tools/AssistantVisibilityExtensions.cs +++ b/app/MindWork AI Studio/Tools/AssistantVisibilityExtensions.cs @@ -59,6 +59,7 @@ public static class AssistantVisibilityExtensions Components.BIAS_DAY_ASSISTANT => ConfigurableAssistant.BIAS_DAY_ASSISTANT, Components.ERI_ASSISTANT => ConfigurableAssistant.ERI_ASSISTANT, Components.DOCUMENT_ANALYSIS_ASSISTANT => ConfigurableAssistant.DOCUMENT_ANALYSIS_ASSISTANT, + Components.SLIDE_BUILDER_ASSISTANT => ConfigurableAssistant.SLIDE_BUILDER_ASSISTANT, Components.I18N_ASSISTANT => ConfigurableAssistant.I18N_ASSISTANT, _ => ConfigurableAssistant.UNKNOWN, diff --git a/app/MindWork AI Studio/Tools/AudioRecordingResult.cs b/app/MindWork AI Studio/Tools/AudioRecordingResult.cs new file mode 100644 index 00000000..cdde82ac --- /dev/null +++ b/app/MindWork AI Studio/Tools/AudioRecordingResult.cs @@ -0,0 +1,8 @@ +namespace AIStudio.Tools; + +public sealed class AudioRecordingResult +{ + public string MimeType { get; init; } = string.Empty; + + public bool ChangedMimeType { get; init; } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/CommonLanguageExtensions.cs b/app/MindWork AI Studio/Tools/CommonLanguageExtensions.cs index 02c24f75..734e1861 100644 --- a/app/MindWork AI Studio/Tools/CommonLanguageExtensions.cs +++ b/app/MindWork AI Studio/Tools/CommonLanguageExtensions.cs @@ -58,6 +58,14 @@ public static class CommonLanguageExtensions _ => $"Translate the given text in {language.Name()} ({language}).", }; + + public static string PromptGeneralPurpose(this CommonLanguages language, string customLanguage) => language switch + { + CommonLanguages.AS_IS => "Use the language the user input is written in for the output.", + CommonLanguages.OTHER => $"use the language {customLanguage} for your output.", + + _ => $"Use the language {language.Name()} ({language}) for your output.", + }; public static string NameSelecting(this CommonLanguages language) { diff --git a/app/MindWork AI Studio/Tools/Components.cs b/app/MindWork AI Studio/Tools/Components.cs index 45ccba91..02718736 100644 --- a/app/MindWork AI Studio/Tools/Components.cs +++ b/app/MindWork AI Studio/Tools/Components.cs @@ -19,6 +19,7 @@ public enum Components BIAS_DAY_ASSISTANT, ERI_ASSISTANT, DOCUMENT_ANALYSIS_ASSISTANT, + SLIDE_BUILDER_ASSISTANT, // ReSharper disable InconsistentNaming I18N_ASSISTANT, diff --git a/app/MindWork AI Studio/Tools/ComponentsExtensions.cs b/app/MindWork AI Studio/Tools/ComponentsExtensions.cs index 4e346d82..70f06380 100644 --- a/app/MindWork AI Studio/Tools/ComponentsExtensions.cs +++ b/app/MindWork AI Studio/Tools/ComponentsExtensions.cs @@ -45,6 +45,7 @@ public static class ComponentsExtensions Components.ERI_ASSISTANT => TB("ERI Server"), Components.I18N_ASSISTANT => TB("Localization Assistant"), Components.DOCUMENT_ANALYSIS_ASSISTANT => TB("Document Analysis Assistant"), + Components.SLIDE_BUILDER_ASSISTANT => TB("Slide Assistant"), Components.CHAT => TB("New Chat"), @@ -66,6 +67,7 @@ public static class ComponentsExtensions Components.MY_TASKS_ASSISTANT => new(Event.SEND_TO_MY_TASKS_ASSISTANT, Routes.ASSISTANT_MY_TASKS), Components.JOB_POSTING_ASSISTANT => new(Event.SEND_TO_JOB_POSTING_ASSISTANT, Routes.ASSISTANT_JOB_POSTING), Components.DOCUMENT_ANALYSIS_ASSISTANT => new(Event.SEND_TO_DOCUMENT_ANALYSIS_ASSISTANT, Routes.ASSISTANT_DOCUMENT_ANALYSIS), + Components.SLIDE_BUILDER_ASSISTANT => new(Event.SEND_TO_SLIDE_BUILDER_ASSISTANT, Routes.ASSISTANT_SLIDE_BUILDER), Components.CHAT => new(Event.SEND_TO_CHAT, Routes.CHAT), @@ -88,6 +90,7 @@ public static class ComponentsExtensions Components.JOB_POSTING_ASSISTANT => settingsManager.ConfigurationData.JobPostings.PreselectOptions ? settingsManager.ConfigurationData.JobPostings.MinimumProviderConfidence : default, Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.ConfigurationData.BiasOfTheDay.MinimumProviderConfidence : default, Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.ConfigurationData.ERI.MinimumProviderConfidence : default, + Components.SLIDE_BUILDER_ASSISTANT => settingsManager.ConfigurationData.SlideBuilder.PreselectOptions ? settingsManager.ConfigurationData.SlideBuilder.MinimumProviderConfidence : default, // The minimum confidence for the Document Analysis Assistant is set per policy. // We do this inside the Document Analysis Assistant component: @@ -116,6 +119,7 @@ public static class ComponentsExtensions Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.BiasOfTheDay.PreselectedProvider) : null, Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.ERI.PreselectedProvider) : null, Components.I18N_ASSISTANT => settingsManager.ConfigurationData.I18N.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.I18N.PreselectedProvider) : null, + Components.SLIDE_BUILDER_ASSISTANT => settingsManager.ConfigurationData.SlideBuilder.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.SlideBuilder.PreselectedProvider) : null, // The Document Analysis Assistant does not have a preselected provider at the component level. // The provider is selected per policy instead. We do this inside the Document Analysis Assistant component. @@ -133,24 +137,29 @@ public static class ComponentsExtensions return preselectedProvider ?? Settings.Provider.NONE; } - public static Profile PreselectedProfile(this Components component, SettingsManager settingsManager) => component switch + public static ProfilePreselection GetProfilePreselection(this Components component, SettingsManager settingsManager) { - Components.AGENDA_ASSISTANT => settingsManager.ConfigurationData.Agenda.PreselectOptions ? settingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Agenda.PreselectedProfile) ?? Profile.NO_PROFILE : Profile.NO_PROFILE, - Components.CODING_ASSISTANT => settingsManager.ConfigurationData.Coding.PreselectOptions ? settingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Coding.PreselectedProfile) ?? Profile.NO_PROFILE : Profile.NO_PROFILE, - Components.EMAIL_ASSISTANT => settingsManager.ConfigurationData.EMail.PreselectOptions ? settingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.EMail.PreselectedProfile) ?? Profile.NO_PROFILE : Profile.NO_PROFILE, - Components.LEGAL_CHECK_ASSISTANT => settingsManager.ConfigurationData.LegalCheck.PreselectOptions ? settingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.LegalCheck.PreselectedProfile) ?? Profile.NO_PROFILE : Profile.NO_PROFILE, - Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.MyTasks.PreselectedProfile) ?? Profile.NO_PROFILE : Profile.NO_PROFILE, - Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.BiasOfTheDay.PreselectedProfile) ?? Profile.NO_PROFILE : Profile.NO_PROFILE, - Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.ERI.PreselectedProfile) ?? Profile.NO_PROFILE : Profile.NO_PROFILE, + var storedValue = component switch + { + Components.AGENDA_ASSISTANT => settingsManager.ConfigurationData.Agenda.PreselectOptions ? settingsManager.ConfigurationData.Agenda.PreselectedProfile : string.Empty, + Components.CODING_ASSISTANT => settingsManager.ConfigurationData.Coding.PreselectOptions ? settingsManager.ConfigurationData.Coding.PreselectedProfile : string.Empty, + Components.EMAIL_ASSISTANT => settingsManager.ConfigurationData.EMail.PreselectOptions ? settingsManager.ConfigurationData.EMail.PreselectedProfile : string.Empty, + Components.LEGAL_CHECK_ASSISTANT => settingsManager.ConfigurationData.LegalCheck.PreselectOptions ? settingsManager.ConfigurationData.LegalCheck.PreselectedProfile : string.Empty, + Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.ConfigurationData.MyTasks.PreselectedProfile : string.Empty, + Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.ConfigurationData.BiasOfTheDay.PreselectedProfile : string.Empty, + Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.ConfigurationData.ERI.PreselectedProfile : string.Empty, + Components.SLIDE_BUILDER_ASSISTANT => settingsManager.ConfigurationData.SlideBuilder.PreselectOptions ? settingsManager.ConfigurationData.SlideBuilder.PreselectedProfile : string.Empty, + Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.ConfigurationData.Chat.PreselectedProfile : string.Empty, - // The Document Analysis Assistant does not have a preselected profile at the component level. - // The profile is selected per policy instead. We do this inside the Document Analysis Assistant component: - Components.DOCUMENT_ANALYSIS_ASSISTANT => Profile.NO_PROFILE, - - Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Chat.PreselectedProfile) ?? Profile.NO_PROFILE : Profile.NO_PROFILE, - - _ => Profile.NO_PROFILE, - }; + // The Document Analysis Assistant does not have a preselected profile at the component level. + // The profile is selected per policy instead. We do this inside the Document Analysis Assistant component: + Components.DOCUMENT_ANALYSIS_ASSISTANT => Profile.NO_PROFILE.Id, + + _ => string.Empty, + }; + + return ProfilePreselection.FromStoredValue(storedValue); + } public static ChatTemplate PreselectedChatTemplate(this Components component, SettingsManager settingsManager) => component switch { 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/Event.cs b/app/MindWork AI Studio/Tools/Event.cs index b3d3628f..6e899a79 100644 --- a/app/MindWork AI Studio/Tools/Event.cs +++ b/app/MindWork AI Studio/Tools/Event.cs @@ -9,6 +9,7 @@ public enum Event CONFIGURATION_CHANGED, COLOR_THEME_CHANGED, STARTUP_PLUGIN_SYSTEM, + STARTUP_COMPLETED, STARTUP_ENTERPRISE_ENVIRONMENT, PLUGINS_RELOADED, SHOW_ERROR, @@ -16,6 +17,7 @@ public enum Event SHOW_SUCCESS, TAURI_EVENT_RECEIVED, RUST_SERVICE_UNAVAILABLE, + VOICE_RECORDING_AVAILABILITY_CHANGED, // Update events: USER_SEARCH_FOR_UPDATE, @@ -54,4 +56,5 @@ public enum Event SEND_TO_MY_TASKS_ASSISTANT, SEND_TO_JOB_POSTING_ASSISTANT, SEND_TO_DOCUMENT_ANALYSIS_ASSISTANT, + SEND_TO_SLIDE_BUILDER_ASSISTANT } diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs index b4007b9d..99031624 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs @@ -111,6 +111,9 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT // Config: how should updates be installed? ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.UpdateInstallation, this.Id, settingsTable, dryRun); + + // Config: what should be the start page? + ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.StartPage, this.Id, settingsTable, dryRun); // Config: allow the user to add providers? ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.AllowUserToAddProvider, this.Id, settingsTable, dryRun); diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs index 4c10c43c..d885a019 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs @@ -204,6 +204,10 @@ public static partial class PluginFactory // Check for the update installation method: if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.UpdateInstallation, AVAILABLE_PLUGINS)) wasConfigurationChanged = true; + + // Check for the start page: + if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.StartPage, AVAILABLE_PLUGINS)) + wasConfigurationChanged = true; // Check for users allowed to added providers: if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.AllowUserToAddProvider, AVAILABLE_PLUGINS)) 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/Tools/Services/GlobalShortcutService.cs b/app/MindWork AI Studio/Tools/Services/GlobalShortcutService.cs index 4515b78b..7d701670 100644 --- a/app/MindWork AI Studio/Tools/Services/GlobalShortcutService.cs +++ b/app/MindWork AI Studio/Tools/Services/GlobalShortcutService.cs @@ -8,41 +8,50 @@ namespace AIStudio.Tools.Services; public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiver { - private static bool IS_INITIALIZED; + private static bool IS_STARTUP_COMPLETED; + private enum ShortcutSyncSource + { + CONFIGURATION_CHANGED, + STARTUP_COMPLETED, + PLUGINS_RELOADED, + VOICE_RECORDING_AVAILABILITY_CHANGED, + } + + private readonly SemaphoreSlim registrationSemaphore = new(1, 1); private readonly ILogger logger; private readonly SettingsManager settingsManager; private readonly MessageBus messageBus; private readonly RustService rustService; + private readonly VoiceRecordingAvailabilityService voiceRecordingAvailabilityService; public GlobalShortcutService( ILogger logger, SettingsManager settingsManager, MessageBus messageBus, - RustService rustService) + RustService rustService, + VoiceRecordingAvailabilityService voiceRecordingAvailabilityService) { this.logger = logger; this.settingsManager = settingsManager; this.messageBus = messageBus; this.rustService = rustService; + this.voiceRecordingAvailabilityService = voiceRecordingAvailabilityService; this.messageBus.RegisterComponent(this); - this.ApplyFilters([], [Event.CONFIGURATION_CHANGED]); + this.ApplyFilters([], [Event.CONFIGURATION_CHANGED, Event.PLUGINS_RELOADED, Event.STARTUP_COMPLETED, Event.VOICE_RECORDING_AVAILABILITY_CHANGED]); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - // Wait until the app is fully initialized: - while (!stoppingToken.IsCancellationRequested && !IS_INITIALIZED) - await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken); - - // Register shortcuts on startup: - await this.RegisterAllShortcuts(); + this.logger.LogInformation("The global shortcut service was initialized."); + await Task.Delay(Timeout.InfiniteTimeSpan, stoppingToken); } public override async Task StopAsync(CancellationToken cancellationToken) { this.messageBus.Unregister(this); + this.registrationSemaphore.Dispose(); await base.StopAsync(cancellationToken); } @@ -53,7 +62,29 @@ public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiv switch (triggeredEvent) { case Event.CONFIGURATION_CHANGED: - await this.RegisterAllShortcuts(); + if (!IS_STARTUP_COMPLETED) + return; + + await this.RegisterAllShortcuts(ShortcutSyncSource.CONFIGURATION_CHANGED); + break; + + case Event.STARTUP_COMPLETED: + IS_STARTUP_COMPLETED = true; + await this.RegisterAllShortcuts(ShortcutSyncSource.STARTUP_COMPLETED); + break; + + case Event.PLUGINS_RELOADED: + if (!IS_STARTUP_COMPLETED) + return; + + await this.RegisterAllShortcuts(ShortcutSyncSource.PLUGINS_RELOADED); + break; + + case Event.VOICE_RECORDING_AVAILABILITY_CHANGED: + if (!IS_STARTUP_COMPLETED) + return; + + await this.RegisterAllShortcuts(ShortcutSyncSource.VOICE_RECORDING_AVAILABILITY_CHANGED); break; } } @@ -62,33 +93,64 @@ public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiv #endregion - private async Task RegisterAllShortcuts() + private async Task RegisterAllShortcuts(ShortcutSyncSource source) { - this.logger.LogInformation("Registering global shortcuts."); - foreach (var shortcutId in Enum.GetValues()) + await this.registrationSemaphore.WaitAsync(); + try { - if(shortcutId is Shortcut.NONE) - continue; - - var shortcut = this.GetShortcutValue(shortcutId); - var isEnabled = this.IsShortcutAllowed(shortcutId); - - if (isEnabled && !string.IsNullOrWhiteSpace(shortcut)) + this.logger.LogInformation("Registering global shortcuts (source='{Source}').", source); + foreach (var shortcutId in Enum.GetValues()) { - var success = await this.rustService.UpdateGlobalShortcut(shortcutId, shortcut); - if (success) - this.logger.LogInformation("Global shortcut '{ShortcutId}' ({Shortcut}) registered.", shortcutId, shortcut); + if(shortcutId is Shortcut.NONE) + continue; + + var shortcutState = await this.GetShortcutState(shortcutId, source); + var shortcut = shortcutState.Shortcut; + var isEnabled = shortcutState.IsEnabled; + this.logger.LogInformation( + "Sync shortcut '{ShortcutId}' (source='{Source}', enabled={IsEnabled}, configured='{Shortcut}').", + shortcutId, + source, + isEnabled, + shortcut); + + if (shortcutState.UsesPersistedFallback) + { + this.logger.LogWarning( + "Using persisted shortcut fallback for '{ShortcutId}' during startup completion (source='{Source}', configured='{Shortcut}').", + shortcutId, + source, + shortcut); + } + + if (isEnabled && !string.IsNullOrWhiteSpace(shortcut)) + { + var success = await this.rustService.UpdateGlobalShortcut(shortcutId, shortcut); + if (success) + this.logger.LogInformation("Global shortcut '{ShortcutId}' ({Shortcut}) registered.", shortcutId, shortcut); + else + this.logger.LogWarning("Failed to register global shortcut '{ShortcutId}' ({Shortcut}).", shortcutId, shortcut); + } else - this.logger.LogWarning("Failed to register global shortcut '{ShortcutId}' ({Shortcut}).", shortcutId, shortcut); - } - else - { - // Disable the shortcut when empty or feature is disabled: - await this.rustService.UpdateGlobalShortcut(shortcutId, string.Empty); - } - } + { + this.logger.LogInformation( + "Disabling global shortcut '{ShortcutId}' (source='{Source}', enabled={IsEnabled}, configured='{Shortcut}').", + shortcutId, + source, + isEnabled, + shortcut); - this.logger.LogInformation("Global shortcuts registration completed."); + // Disable the shortcut when empty or feature is disabled: + await this.rustService.UpdateGlobalShortcut(shortcutId, string.Empty); + } + } + + this.logger.LogInformation("Global shortcuts registration completed (source='{Source}').", source); + } + finally + { + this.registrationSemaphore.Release(); + } } private string GetShortcutValue(Shortcut name) => name switch @@ -101,11 +163,37 @@ public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiv private bool IsShortcutAllowed(Shortcut name) => name switch { // Voice recording is a preview feature: - Shortcut.VOICE_RECORDING_TOGGLE => PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.settingsManager), - + Shortcut.VOICE_RECORDING_TOGGLE => PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.settingsManager) + && this.voiceRecordingAvailabilityService.IsAvailable, + // Other shortcuts are always allowed: _ => true, }; - public static void Initialize() => IS_INITIALIZED = true; + private async Task GetShortcutState(Shortcut shortcutId, ShortcutSyncSource source) + { + var shortcut = this.GetShortcutValue(shortcutId); + var isEnabled = this.IsShortcutAllowed(shortcutId); + if (isEnabled && !string.IsNullOrWhiteSpace(shortcut)) + return new(shortcut, true, false); + + if (source is not ShortcutSyncSource.STARTUP_COMPLETED || shortcutId is not Shortcut.VOICE_RECORDING_TOGGLE) + return new(shortcut, isEnabled, false); + + var settingsSnapshot = await this.settingsManager.TryReadSettingsSnapshot(); + if (settingsSnapshot is null) + return new(shortcut, isEnabled, false); + + var fallbackShortcut = settingsSnapshot.App.ShortcutVoiceRecording; + var fallbackEnabled = + settingsSnapshot.App.EnabledPreviewFeatures.Contains(PreviewFeatures.PRE_SPEECH_TO_TEXT_2026) && + !string.IsNullOrWhiteSpace(settingsSnapshot.App.UseTranscriptionProvider); + + if (!fallbackEnabled || string.IsNullOrWhiteSpace(fallbackShortcut)) + return new(shortcut, isEnabled, false); + + return new(fallbackShortcut, true, true); + } + + private readonly record struct ShortcutState(string Shortcut, bool IsEnabled, bool UsesPersistedFallback); } diff --git a/app/MindWork AI Studio/Tools/Services/TemporaryChatService.cs b/app/MindWork AI Studio/Tools/Services/TemporaryChatService.cs index 61a6e4c8..90203b2b 100644 --- a/app/MindWork AI Studio/Tools/Services/TemporaryChatService.cs +++ b/app/MindWork AI Studio/Tools/Services/TemporaryChatService.cs @@ -67,6 +67,7 @@ public sealed class TemporaryChatService(ILogger logger, S { logger.LogInformation($"Deleting temporary chat storage directory '{tempChatDirPath}' due to maintenance policy."); Directory.Delete(tempChatDirPath, true); + WorkspaceBehaviour.InvalidateWorkspaceTreeCache(); } } diff --git a/app/MindWork AI Studio/Tools/Services/VoiceRecordingAvailabilityService.cs b/app/MindWork AI Studio/Tools/Services/VoiceRecordingAvailabilityService.cs new file mode 100644 index 00000000..010176c7 --- /dev/null +++ b/app/MindWork AI Studio/Tools/Services/VoiceRecordingAvailabilityService.cs @@ -0,0 +1,23 @@ +namespace AIStudio.Tools.Services; + +public sealed class VoiceRecordingAvailabilityService +{ + private readonly Lock stateLock = new(); + + public bool IsAvailable { get; private set; } = true; + + public string? DisableReason { get; private set; } + + public bool TryDisable(string reason) + { + lock (this.stateLock) + { + if (!this.IsAvailable) + return false; + + this.IsAvailable = false; + this.DisableReason = reason; + return true; + } + } +} diff --git a/app/MindWork AI Studio/Tools/SoundEffectsInitializationResult.cs b/app/MindWork AI Studio/Tools/SoundEffectsInitializationResult.cs new file mode 100644 index 00000000..70ec2c10 --- /dev/null +++ b/app/MindWork AI Studio/Tools/SoundEffectsInitializationResult.cs @@ -0,0 +1,10 @@ +namespace AIStudio.Tools; + +public sealed class SoundEffectsInitializationResult +{ + public bool Success { get; init; } + + public string[] FailedPaths { get; init; } = []; + + public string? ErrorMessage { get; init; } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/WorkspaceBehaviour.cs b/app/MindWork AI Studio/Tools/WorkspaceBehaviour.cs index 253b4431..c03fccc8 100644 --- a/app/MindWork AI Studio/Tools/WorkspaceBehaviour.cs +++ b/app/MindWork AI Studio/Tools/WorkspaceBehaviour.cs @@ -12,21 +12,73 @@ namespace AIStudio.Tools; public static class WorkspaceBehaviour { + private sealed class WorkspaceChatCacheEntry + { + public Guid WorkspaceId { get; init; } + + public Guid ChatId { get; init; } + + public string ChatPath { get; init; } = string.Empty; + + public string ChatName { get; set; } = string.Empty; + + public DateTimeOffset LastEditTime { get; set; } + + public bool IsTemporary { get; init; } + } + + private sealed class WorkspaceCacheEntry + { + public Guid WorkspaceId { get; init; } + + public string WorkspacePath { get; init; } = string.Empty; + + public string WorkspaceName { get; set; } = string.Empty; + + public bool ChatsLoaded { get; set; } + + public List Chats { get; set; } = []; + } + + private sealed class WorkspaceTreeCacheState + { + public Dictionary Workspaces { get; } = []; + + public List WorkspaceOrder { get; } = []; + + public List TemporaryChats { get; set; } = []; + + public bool IsShellLoaded { get; set; } + } + private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger(nameof(WorkspaceBehaviour)); + private static readonly ConcurrentDictionary CHAT_STORAGE_SEMAPHORES = new(); + private static readonly SemaphoreSlim WORKSPACE_TREE_CACHE_SEMAPHORE = new(1, 1); + private static readonly WorkspaceTreeCacheState WORKSPACE_TREE_CACHE = new(); + + private static readonly TimeSpan SEMAPHORE_TIMEOUT = TimeSpan.FromSeconds(6); + private static volatile bool WORKSPACE_TREE_CACHE_INVALIDATED = true; private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(WorkspaceBehaviour).Namespace, nameof(WorkspaceBehaviour)); - /// - /// Semaphores for synchronizing chat storage operations per chat. - /// This prevents race conditions when multiple threads try to write - /// the same chat file simultaneously. - /// - private static readonly ConcurrentDictionary CHAT_STORAGE_SEMAPHORES = new(); + public static readonly JsonSerializerOptions JSON_OPTIONS = new() + { + WriteIndented = true, + AllowTrailingCommas = true, + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true, + Converters = + { + new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseUpper), + } + }; - /// - /// Timeout for acquiring the chat storage semaphore. - /// - private static readonly TimeSpan SEMAPHORE_TIMEOUT = TimeSpan.FromSeconds(6); + private static readonly TimeSpan PREFETCH_DELAY_DURATION = TimeSpan.FromMilliseconds(45); + + private static string WorkspaceRootDirectory => Path.Join(SettingsManager.DataDirectory, "workspaces"); + + private static string TemporaryChatsRootDirectory => Path.Join(SettingsManager.DataDirectory, "tempChats"); private static SemaphoreSlim GetChatSemaphore(Guid workspaceId, Guid chatId) { @@ -34,13 +86,6 @@ public static class WorkspaceBehaviour return CHAT_STORAGE_SEMAPHORES.GetOrAdd(key, _ => new SemaphoreSlim(1, 1)); } - /// - /// Tries to acquire the chat storage semaphore within the configured timeout. - /// - /// The workspace ID. - /// The chat ID. - /// The name of the calling method for logging purposes. - /// A tuple containing whether the semaphore was acquired and the semaphore instance. private static async Task<(bool Acquired, SemaphoreSlim Semaphore)> TryAcquireChatSemaphoreAsync(Guid workspaceId, Guid chatId, string callerName) { var semaphore = GetChatSemaphore(workspaceId, chatId); @@ -56,18 +101,357 @@ public static class WorkspaceBehaviour return (acquired, semaphore); } - public static readonly JsonSerializerOptions JSON_OPTIONS = new() + private static WorkspaceTreeChat ToPublicChat(WorkspaceChatCacheEntry chat) => new(chat.WorkspaceId, chat.ChatId, chat.ChatPath, chat.ChatName, chat.LastEditTime, chat.IsTemporary); + + private static WorkspaceTreeWorkspace ToPublicWorkspace(WorkspaceCacheEntry workspace) => new(workspace.WorkspaceId, + workspace.WorkspacePath, + workspace.WorkspaceName, + workspace.ChatsLoaded, + workspace.Chats.Select(ToPublicChat).ToList()); + + private static async Task ReadNameOrDefaultAsync(string nameFilePath, string fallbackName) { - WriteIndented = true, - AllowTrailingCommas = true, - PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, - DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, - PropertyNameCaseInsensitive = true, - Converters = + try { - new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseUpper), + if (!File.Exists(nameFilePath)) + return fallbackName; + + var name = await File.ReadAllTextAsync(nameFilePath, Encoding.UTF8); + return string.IsNullOrWhiteSpace(name) ? fallbackName : name; } - }; + catch + { + return fallbackName; + } + } + + private static async Task> ReadWorkspaceChatsCoreAsync(Guid workspaceId, string workspacePath) + { + var chats = new List(); + if (!Directory.Exists(workspacePath)) + return chats; + + foreach (var chatPath in Directory.EnumerateDirectories(workspacePath)) + { + if (!Guid.TryParse(Path.GetFileName(chatPath), out var chatId)) + continue; + + var chatName = await ReadNameOrDefaultAsync(Path.Join(chatPath, "name"), TB("Unnamed chat")); + var chatThreadPath = Path.Join(chatPath, "thread.json"); + var lastEditTime = File.Exists(chatThreadPath) ? File.GetLastWriteTimeUtc(chatThreadPath) : DateTimeOffset.MinValue; + chats.Add(new WorkspaceChatCacheEntry + { + WorkspaceId = workspaceId, + ChatId = chatId, + ChatPath = chatPath, + ChatName = chatName, + LastEditTime = lastEditTime, + IsTemporary = false, + }); + } + + return chats.OrderByDescending(x => x.LastEditTime).ToList(); + } + + private static async Task> ReadTemporaryChatsCoreAsync() + { + var chats = new List(); + Directory.CreateDirectory(TemporaryChatsRootDirectory); + + foreach (var tempChatPath in Directory.EnumerateDirectories(TemporaryChatsRootDirectory)) + { + if (!Guid.TryParse(Path.GetFileName(tempChatPath), out var chatId)) + continue; + + var chatName = await ReadNameOrDefaultAsync(Path.Join(tempChatPath, "name"), TB("Unnamed chat")); + var chatThreadPath = Path.Join(tempChatPath, "thread.json"); + var lastEditTime = File.Exists(chatThreadPath) ? File.GetLastWriteTimeUtc(chatThreadPath) : DateTimeOffset.MinValue; + chats.Add(new WorkspaceChatCacheEntry + { + WorkspaceId = Guid.Empty, + ChatId = chatId, + ChatPath = tempChatPath, + ChatName = chatName, + LastEditTime = lastEditTime, + IsTemporary = true, + }); + } + + return chats.OrderByDescending(x => x.LastEditTime).ToList(); + } + + private static async Task EnsureTreeShellLoadedCoreAsync() + { + if (!WORKSPACE_TREE_CACHE_INVALIDATED && WORKSPACE_TREE_CACHE.IsShellLoaded) + return; + + WORKSPACE_TREE_CACHE.Workspaces.Clear(); + WORKSPACE_TREE_CACHE.WorkspaceOrder.Clear(); + + Directory.CreateDirectory(WorkspaceRootDirectory); + foreach (var workspacePath in Directory.EnumerateDirectories(WorkspaceRootDirectory)) + { + if (!Guid.TryParse(Path.GetFileName(workspacePath), out var workspaceId)) + continue; + + var workspaceName = await ReadNameOrDefaultAsync(Path.Join(workspacePath, "name"), TB("Unnamed workspace")); + WORKSPACE_TREE_CACHE.Workspaces[workspaceId] = new WorkspaceCacheEntry + { + WorkspaceId = workspaceId, + WorkspacePath = workspacePath, + WorkspaceName = workspaceName, + ChatsLoaded = false, + Chats = [], + }; + + WORKSPACE_TREE_CACHE.WorkspaceOrder.Add(workspaceId); + } + + WORKSPACE_TREE_CACHE.TemporaryChats = await ReadTemporaryChatsCoreAsync(); + WORKSPACE_TREE_CACHE.IsShellLoaded = true; + WORKSPACE_TREE_CACHE_INVALIDATED = false; + } + + private static void UpsertChatInCache(List chats, WorkspaceChatCacheEntry chat) + { + var existingIndex = chats.FindIndex(existing => existing.ChatId == chat.ChatId); + if (existingIndex >= 0) + chats[existingIndex] = chat; + else + chats.Add(chat); + + chats.Sort((a, b) => b.LastEditTime.CompareTo(a.LastEditTime)); + } + + private static void DeleteChatFromCache(List chats, Guid chatId) + { + var existingIndex = chats.FindIndex(existing => existing.ChatId == chatId); + if (existingIndex >= 0) + chats.RemoveAt(existingIndex); + } + + private static async Task UpdateCacheAfterChatStored(Guid workspaceId, Guid chatId, string chatDirectory, string chatName, DateTimeOffset lastEditTime) + { + await WORKSPACE_TREE_CACHE_SEMAPHORE.WaitAsync(); + try + { + if (!WORKSPACE_TREE_CACHE.IsShellLoaded || WORKSPACE_TREE_CACHE_INVALIDATED) + return; + + var chatCacheEntry = new WorkspaceChatCacheEntry + { + WorkspaceId = workspaceId, + ChatId = chatId, + ChatPath = chatDirectory, + ChatName = string.IsNullOrWhiteSpace(chatName) ? TB("Unnamed chat") : chatName, + LastEditTime = lastEditTime, + IsTemporary = workspaceId == Guid.Empty, + }; + + if (workspaceId == Guid.Empty) + { + UpsertChatInCache(WORKSPACE_TREE_CACHE.TemporaryChats, chatCacheEntry); + return; + } + + if (WORKSPACE_TREE_CACHE.Workspaces.TryGetValue(workspaceId, out var workspace) && workspace.ChatsLoaded) + UpsertChatInCache(workspace.Chats, chatCacheEntry); + } + finally + { + WORKSPACE_TREE_CACHE_SEMAPHORE.Release(); + } + } + + private static async Task UpdateCacheAfterChatDeleted(Guid workspaceId, Guid chatId) + { + await WORKSPACE_TREE_CACHE_SEMAPHORE.WaitAsync(); + try + { + if (!WORKSPACE_TREE_CACHE.IsShellLoaded || WORKSPACE_TREE_CACHE_INVALIDATED) + return; + + if (workspaceId == Guid.Empty) + { + DeleteChatFromCache(WORKSPACE_TREE_CACHE.TemporaryChats, chatId); + return; + } + + if (WORKSPACE_TREE_CACHE.Workspaces.TryGetValue(workspaceId, out var workspace) && workspace.ChatsLoaded) + DeleteChatFromCache(workspace.Chats, chatId); + } + finally + { + WORKSPACE_TREE_CACHE_SEMAPHORE.Release(); + } + } + + public static void InvalidateWorkspaceTreeCache() + { + WORKSPACE_TREE_CACHE_INVALIDATED = true; + } + + public static async Task ForceReloadWorkspaceTreeAsync() + { + await WORKSPACE_TREE_CACHE_SEMAPHORE.WaitAsync(); + try + { + WORKSPACE_TREE_CACHE_INVALIDATED = false; + WORKSPACE_TREE_CACHE.IsShellLoaded = false; + await EnsureTreeShellLoadedCoreAsync(); + } + finally + { + WORKSPACE_TREE_CACHE_SEMAPHORE.Release(); + } + } + + public static async Task GetOrLoadWorkspaceTreeShellAsync() + { + await WORKSPACE_TREE_CACHE_SEMAPHORE.WaitAsync(); + try + { + await EnsureTreeShellLoadedCoreAsync(); + var workspaces = WORKSPACE_TREE_CACHE.WorkspaceOrder + .Where(workspaceId => WORKSPACE_TREE_CACHE.Workspaces.ContainsKey(workspaceId)) + .Select(workspaceId => ToPublicWorkspace(WORKSPACE_TREE_CACHE.Workspaces[workspaceId])) + .ToList(); + var temporaryChats = WORKSPACE_TREE_CACHE.TemporaryChats.Select(ToPublicChat).ToList(); + return new WorkspaceTreeCacheSnapshot(workspaces, temporaryChats); + } + finally + { + WORKSPACE_TREE_CACHE_SEMAPHORE.Release(); + } + } + + public static async Task> GetWorkspaceChatsAsync(Guid workspaceId, bool forceRefresh = false) + { + await WORKSPACE_TREE_CACHE_SEMAPHORE.WaitAsync(); + try + { + await EnsureTreeShellLoadedCoreAsync(); + if (!WORKSPACE_TREE_CACHE.Workspaces.TryGetValue(workspaceId, out var workspace)) + return []; + + if (forceRefresh || !workspace.ChatsLoaded) + { + workspace.Chats = await ReadWorkspaceChatsCoreAsync(workspaceId, workspace.WorkspacePath); + workspace.ChatsLoaded = true; + } + + return workspace.Chats.Select(ToPublicChat).ToList(); + } + finally + { + WORKSPACE_TREE_CACHE_SEMAPHORE.Release(); + } + } + + public static async Task TryPrefetchRemainingChatsAsync(Func? onWorkspaceUpdated = null, CancellationToken token = default) + { + while (true) + { + token.ThrowIfCancellationRequested(); + Guid? workspaceToPrefetch = null; + + await WORKSPACE_TREE_CACHE_SEMAPHORE.WaitAsync(token); + try + { + await EnsureTreeShellLoadedCoreAsync(); + foreach (var workspaceId in WORKSPACE_TREE_CACHE.WorkspaceOrder) + { + if (WORKSPACE_TREE_CACHE.Workspaces.TryGetValue(workspaceId, out var workspace) && !workspace.ChatsLoaded) + { + workspaceToPrefetch = workspaceId; + break; + } + } + } + finally + { + WORKSPACE_TREE_CACHE_SEMAPHORE.Release(); + } + + if (workspaceToPrefetch is null) + return; + + await GetWorkspaceChatsAsync(workspaceToPrefetch.Value); + if (onWorkspaceUpdated is not null) + { + try + { + await onWorkspaceUpdated(workspaceToPrefetch.Value); + } + catch (Exception ex) + { + LOG.LogWarning(ex, "Failed to process callback after prefetching workspace '{WorkspaceId}'.", workspaceToPrefetch.Value); + } + } + + await Task.Delay(PREFETCH_DELAY_DURATION, token); + } + } + + public static async Task AddWorkspaceToCacheAsync(Guid workspaceId, string workspacePath, string workspaceName) + { + await WORKSPACE_TREE_CACHE_SEMAPHORE.WaitAsync(); + try + { + await EnsureTreeShellLoadedCoreAsync(); + if (WORKSPACE_TREE_CACHE.Workspaces.TryGetValue(workspaceId, out var workspace)) + { + workspace.WorkspaceName = workspaceName; + return; + } + + WORKSPACE_TREE_CACHE.Workspaces[workspaceId] = new WorkspaceCacheEntry + { + WorkspaceId = workspaceId, + WorkspacePath = workspacePath, + WorkspaceName = workspaceName, + Chats = [], + ChatsLoaded = false, + }; + WORKSPACE_TREE_CACHE.WorkspaceOrder.Add(workspaceId); + } + finally + { + WORKSPACE_TREE_CACHE_SEMAPHORE.Release(); + } + } + + public static async Task UpdateWorkspaceNameInCacheAsync(Guid workspaceId, string workspaceName) + { + await WORKSPACE_TREE_CACHE_SEMAPHORE.WaitAsync(); + try + { + await EnsureTreeShellLoadedCoreAsync(); + if (WORKSPACE_TREE_CACHE.Workspaces.TryGetValue(workspaceId, out var workspace)) + workspace.WorkspaceName = workspaceName; + } + finally + { + WORKSPACE_TREE_CACHE_SEMAPHORE.Release(); + } + } + + public static async Task RemoveWorkspaceFromCacheAsync(Guid workspaceId) + { + await WORKSPACE_TREE_CACHE_SEMAPHORE.WaitAsync(); + try + { + if (!WORKSPACE_TREE_CACHE.IsShellLoaded || WORKSPACE_TREE_CACHE_INVALIDATED) + return; + + WORKSPACE_TREE_CACHE.Workspaces.Remove(workspaceId); + WORKSPACE_TREE_CACHE.WorkspaceOrder.Remove(workspaceId); + } + finally + { + WORKSPACE_TREE_CACHE_SEMAPHORE.Release(); + } + } public static bool IsChatExisting(LoadChat loadChat) { @@ -78,31 +462,28 @@ public static class WorkspaceBehaviour return Directory.Exists(chatPath); } - public static async Task StoreChat(ChatThread chat) + public static async Task StoreChatAsync(ChatThread chat) { - // Try to acquire the semaphore for this specific chat to prevent concurrent writes to the same file: - var (acquired, semaphore) = await TryAcquireChatSemaphoreAsync(chat.WorkspaceId, chat.ChatId, nameof(StoreChat)); + var (acquired, semaphore) = await TryAcquireChatSemaphoreAsync(chat.WorkspaceId, chat.ChatId, nameof(StoreChatAsync)); if (!acquired) return; try { - string chatDirectory; - if (chat.WorkspaceId == Guid.Empty) - chatDirectory = Path.Join(SettingsManager.DataDirectory, "tempChats", chat.ChatId.ToString()); - else - chatDirectory = Path.Join(SettingsManager.DataDirectory, "workspaces", chat.WorkspaceId.ToString(), chat.ChatId.ToString()); + var chatDirectory = chat.WorkspaceId == Guid.Empty + ? Path.Join(SettingsManager.DataDirectory, "tempChats", chat.ChatId.ToString()) + : Path.Join(SettingsManager.DataDirectory, "workspaces", chat.WorkspaceId.ToString(), chat.ChatId.ToString()); - // Ensure the directory exists: Directory.CreateDirectory(chatDirectory); - // Save the chat name: var chatNamePath = Path.Join(chatDirectory, "name"); await File.WriteAllTextAsync(chatNamePath, chat.Name); - // Save the thread as thread.json: var chatPath = Path.Join(chatDirectory, "thread.json"); await File.WriteAllTextAsync(chatPath, JsonSerializer.Serialize(chat, JSON_OPTIONS), Encoding.UTF8); + + var lastEditTime = File.GetLastWriteTimeUtc(chatPath); + await UpdateCacheAfterChatStored(chat.WorkspaceId, chat.ChatId, chatDirectory, chat.Name, lastEditTime); } finally { @@ -110,10 +491,9 @@ public static class WorkspaceBehaviour } } - public static async Task LoadChat(LoadChat loadChat) + public static async Task LoadChatAsync(LoadChat loadChat) { - // Try to acquire the semaphore for this specific chat to prevent concurrent read/writes to the same file: - var (acquired, semaphore) = await TryAcquireChatSemaphoreAsync(loadChat.WorkspaceId, loadChat.ChatId, nameof(LoadChat)); + var (acquired, semaphore) = await TryAcquireChatSemaphoreAsync(loadChat.WorkspaceId, loadChat.ChatId, nameof(LoadChatAsync)); if (!acquired) return null; @@ -123,12 +503,11 @@ public static class WorkspaceBehaviour ? Path.Join(SettingsManager.DataDirectory, "tempChats", loadChat.ChatId.ToString()) : Path.Join(SettingsManager.DataDirectory, "workspaces", loadChat.WorkspaceId.ToString(), loadChat.ChatId.ToString()); - if(!Directory.Exists(chatPath)) + if (!Directory.Exists(chatPath)) return null; var chatData = await File.ReadAllTextAsync(Path.Join(chatPath, "thread.json"), Encoding.UTF8); - var chat = JsonSerializer.Deserialize(chatData, JSON_OPTIONS); - return chat; + return JsonSerializer.Deserialize(chatData, JSON_OPTIONS); } catch (Exception) { @@ -140,51 +519,68 @@ public static class WorkspaceBehaviour } } - public static async Task LoadWorkspaceName(Guid workspaceId) + public static async Task LoadWorkspaceNameAsync(Guid workspaceId) { - if(workspaceId == Guid.Empty) + if (workspaceId == Guid.Empty) return string.Empty; - - var workspacePath = Path.Join(SettingsManager.DataDirectory, "workspaces", workspaceId.ToString()); - var workspaceNamePath = Path.Join(workspacePath, "name"); - + + await WORKSPACE_TREE_CACHE_SEMAPHORE.WaitAsync(); try { - // If the name file does not exist or is empty, self-heal with a default name. - if (!File.Exists(workspaceNamePath)) + await EnsureTreeShellLoadedCoreAsync(); + if (WORKSPACE_TREE_CACHE.Workspaces.TryGetValue(workspaceId, out var cachedWorkspace) && !string.IsNullOrWhiteSpace(cachedWorkspace.WorkspaceName)) + return cachedWorkspace.WorkspaceName; + + // Not in cache — read from disk and update cache in the same semaphore scope + // to avoid a second semaphore acquisition via UpdateWorkspaceNameInCacheAsync: + var workspacePath = Path.Join(WorkspaceRootDirectory, workspaceId.ToString()); + var workspaceNamePath = Path.Join(workspacePath, "name"); + string workspaceName; + + try { - var defaultName = TB("Unnamed workspace"); - Directory.CreateDirectory(workspacePath); - await File.WriteAllTextAsync(workspaceNamePath, defaultName, Encoding.UTF8); - return defaultName; + if (!File.Exists(workspaceNamePath)) + { + workspaceName = TB("Unnamed workspace"); + Directory.CreateDirectory(workspacePath); + await File.WriteAllTextAsync(workspaceNamePath, workspaceName, Encoding.UTF8); + } + else + { + workspaceName = await File.ReadAllTextAsync(workspaceNamePath, Encoding.UTF8); + if (string.IsNullOrWhiteSpace(workspaceName)) + { + workspaceName = TB("Unnamed workspace"); + await File.WriteAllTextAsync(workspaceNamePath, workspaceName, Encoding.UTF8); + } + } } - - var name = await File.ReadAllTextAsync(workspaceNamePath, Encoding.UTF8); - if (string.IsNullOrWhiteSpace(name)) + catch { - var defaultName = TB("Unnamed workspace"); - await File.WriteAllTextAsync(workspaceNamePath, defaultName, Encoding.UTF8); - return defaultName; + workspaceName = TB("Unnamed workspace"); } - - return name; + + // Update the cache directly (we already hold the semaphore): + if (WORKSPACE_TREE_CACHE.Workspaces.TryGetValue(workspaceId, out var workspace)) + workspace.WorkspaceName = workspaceName; + + return workspaceName; } - catch + finally { - // On any error, return a localized default without throwing. - return TB("Unnamed workspace"); + WORKSPACE_TREE_CACHE_SEMAPHORE.Release(); } } - public static async Task DeleteChat(IDialogService dialogService, Guid workspaceId, Guid chatId, bool askForConfirmation = true) + public static async Task DeleteChatAsync(IDialogService dialogService, Guid workspaceId, Guid chatId, bool askForConfirmation = true) { - var chat = await LoadChat(new(workspaceId, chatId)); + var chat = await LoadChatAsync(new(workspaceId, chatId)); if (chat is null) return; if (askForConfirmation) { - var workspaceName = await LoadWorkspaceName(chat.WorkspaceId); + var workspaceName = await LoadWorkspaceNameAsync(chat.WorkspaceId); var dialogParameters = new DialogParameters { { @@ -202,20 +598,20 @@ public static class WorkspaceBehaviour return; } - string chatDirectory; - if (chat.WorkspaceId == Guid.Empty) - chatDirectory = Path.Join(SettingsManager.DataDirectory, "tempChats", chat.ChatId.ToString()); - else - chatDirectory = Path.Join(SettingsManager.DataDirectory, "workspaces", chat.WorkspaceId.ToString(), chat.ChatId.ToString()); + var chatDirectory = chat.WorkspaceId == Guid.Empty + ? Path.Join(SettingsManager.DataDirectory, "tempChats", chat.ChatId.ToString()) + : Path.Join(SettingsManager.DataDirectory, "workspaces", chat.WorkspaceId.ToString(), chat.ChatId.ToString()); - // Try to acquire the semaphore to prevent deleting while another thread is writing: - var (acquired, semaphore) = await TryAcquireChatSemaphoreAsync(workspaceId, chatId, nameof(DeleteChat)); + var (acquired, semaphore) = await TryAcquireChatSemaphoreAsync(workspaceId, chatId, nameof(DeleteChatAsync)); if (!acquired) return; try { - Directory.Delete(chatDirectory, true); + if (Directory.Exists(chatDirectory)) + Directory.Delete(chatDirectory, true); + + await UpdateCacheAfterChatDeleted(workspaceId, chatId); } finally { @@ -225,16 +621,14 @@ public static class WorkspaceBehaviour private static async Task EnsureWorkspace(Guid workspaceId, string workspaceName) { - var workspacePath = Path.Join(SettingsManager.DataDirectory, "workspaces", workspaceId.ToString()); + var workspacePath = Path.Join(WorkspaceRootDirectory, workspaceId.ToString()); var workspaceNamePath = Path.Join(workspacePath, "name"); - if(!Path.Exists(workspacePath)) + if (!Path.Exists(workspacePath)) Directory.CreateDirectory(workspacePath); try { - // When the name file is missing or empty, write it (self-heal). - // Otherwise, keep the existing name: if (!File.Exists(workspaceNamePath)) { await File.WriteAllTextAsync(workspaceNamePath, workspaceName, Encoding.UTF8); @@ -250,9 +644,11 @@ public static class WorkspaceBehaviour { // Ignore IO issues to avoid interrupting background initialization. } + + await AddWorkspaceToCacheAsync(workspaceId, workspacePath, workspaceName); } - + public static async Task EnsureBiasWorkspace() => await EnsureWorkspace(KnownWorkspaces.BIAS_WORKSPACE_ID, "Bias of the Day"); - + public static async Task EnsureERIServerWorkspace() => await EnsureWorkspace(KnownWorkspaces.ERI_SERVER_WORKSPACE_ID, "ERI Servers"); -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Tools/WorkspaceTreeCacheSnapshot.cs b/app/MindWork AI Studio/Tools/WorkspaceTreeCacheSnapshot.cs new file mode 100644 index 00000000..8e36f5d5 --- /dev/null +++ b/app/MindWork AI Studio/Tools/WorkspaceTreeCacheSnapshot.cs @@ -0,0 +1,3 @@ +namespace AIStudio.Tools; + +public readonly record struct WorkspaceTreeCacheSnapshot(IReadOnlyList Workspaces, IReadOnlyList TemporaryChats); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/WorkspaceTreeChat.cs b/app/MindWork AI Studio/Tools/WorkspaceTreeChat.cs new file mode 100644 index 00000000..3976667d --- /dev/null +++ b/app/MindWork AI Studio/Tools/WorkspaceTreeChat.cs @@ -0,0 +1,4 @@ +// ReSharper disable NotAccessedPositionalProperty.Global +namespace AIStudio.Tools; + +public readonly record struct WorkspaceTreeChat(Guid WorkspaceId, Guid ChatId, string ChatPath, string Name, DateTimeOffset LastEditTime, bool IsTemporary); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/WorkspaceTreeWorkspace.cs b/app/MindWork AI Studio/Tools/WorkspaceTreeWorkspace.cs new file mode 100644 index 00000000..d8eed9bb --- /dev/null +++ b/app/MindWork AI Studio/Tools/WorkspaceTreeWorkspace.cs @@ -0,0 +1,3 @@ +namespace AIStudio.Tools; + +public readonly record struct WorkspaceTreeWorkspace(Guid WorkspaceId, string WorkspacePath, string Name, bool ChatsLoaded, IReadOnlyList Chats); \ No newline at end of file diff --git a/app/MindWork AI Studio/packages.lock.json b/app/MindWork AI Studio/packages.lock.json index 308f8758..c4a3fa82 100644 --- a/app/MindWork AI Studio/packages.lock.json +++ b/app/MindWork AI Studio/packages.lock.json @@ -28,18 +28,18 @@ }, "Microsoft.Extensions.FileProviders.Embedded": { "type": "Direct", - "requested": "[9.0.12, )", - "resolved": "9.0.12", - "contentHash": "mJ89qzHqx6BWhD6ATEkWXQ3QGKkSy1zyALJOLjGB0N0O4znKPafR9DjEkKunpWpUQuvnudsrUdQCfseHIQl+Vw==", + "requested": "[9.0.14, )", + "resolved": "9.0.14", + "contentHash": "Mw7HO29yv8DIo2e//a/OdK1lFu47v7k9BaLQmdTp75i+i867FlgfS54fKuJl8KCC5YBCh8ov2+q9DHC5tLIoMg==", "dependencies": { - "Microsoft.Extensions.FileProviders.Abstractions": "9.0.12" + "Microsoft.Extensions.FileProviders.Abstractions": "9.0.14" } }, "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[9.0.12, )", - "resolved": "9.0.12", - "contentHash": "StA3kyImQHqDo8A8ZHaSxgASbEuT5UIqgeCvK5SzUPj//xE1QSys421J9pEs4cYuIVwq7CJvWSKxtyH7aPr1LA==" + "requested": "[9.0.14, )", + "resolved": "9.0.14", + "contentHash": "+MeWjj5sGq6Oj/l0E9RPMgXDyCIPxczzCbGuvuVTZFEGiy2S/atsfoAoKUnkEin/GeGpN+HenCzRmiQKSc99eQ==" }, "MudBlazor": { "type": "Direct", @@ -182,10 +182,10 @@ }, "Microsoft.Extensions.FileProviders.Abstractions": { "type": "Transitive", - "resolved": "9.0.12", - "contentHash": "DIRWbcei4olf0EvIqAXJZiXnsaCCq6RP+sADmbz7FDMHAWIG2eEh50BeT/z9VEgmYfly3bXp2UCuS5hf3KK1Zw==", + "resolved": "9.0.14", + "contentHash": "zQHjufn8oR4VdjtrCQZNTfNKolDeT/VOhF/YFsZqaQMHZzTIMzWD56UpoEMQYbYwjxiTRzRGuNfFlINP0AcC6w==", "dependencies": { - "Microsoft.Extensions.Primitives": "9.0.12" + "Microsoft.Extensions.Primitives": "9.0.14" } }, "Microsoft.Extensions.Localization": { @@ -223,8 +223,8 @@ }, "Microsoft.Extensions.Primitives": { "type": "Transitive", - "resolved": "9.0.12", - "contentHash": "nmGbgxTfuvuEdcQ9NH5DEwAKDKB+c39dAcKQ4+sb8WpGA3pMIgAJfowC+aRH/6gFmdRq2ssRp031Uvv7rTrOMg==" + "resolved": "9.0.14", + "contentHash": "1bP1fEv6MdXvX4TsxrT94AE2aOIPI9p0xgVsxUliB91wDXHUwbBHV1hXKbfu0ZHEdBuYEusyTVoUwUXp71fh8w==" }, "Microsoft.JSInterop": { "type": "Transitive", diff --git a/app/MindWork AI Studio/wwwroot/app.css b/app/MindWork AI Studio/wwwroot/app.css index cd80c5a9..909d350d 100644 --- a/app/MindWork AI Studio/wwwroot/app.css +++ b/app/MindWork AI Studio/wwwroot/app.css @@ -150,4 +150,19 @@ .sources-card-header { top: 0em !important; left: 2.2em !important; -} \ No newline at end of file +} + +.chat-mathjax-block { + text-align: left; +} + +.chat-mathjax-block mjx-container[display="true"] { + text-align: left !important; + margin-left: 0 !important; + margin-right: 0 !important; +} + +.chat-mathjax-block mjx-container[display="true"] mjx-math { + margin-left: 0 !important; + margin-right: 0 !important; +} diff --git a/app/MindWork AI Studio/wwwroot/app.js b/app/MindWork AI Studio/wwwroot/app.js index aa6b8e2b..a2f8f967 100644 --- a/app/MindWork AI Studio/wwwroot/app.js +++ b/app/MindWork AI Studio/wwwroot/app.js @@ -25,4 +25,109 @@ window.clearDiv = function (divName) { window.scrollToBottom = function(element) { element.scrollIntoView({ behavior: 'smooth', block: 'end', inline: 'nearest' }); -} \ No newline at end of file +} + +window.formatChatInputMarkdown = function (inputId, formatType) { + let input = document.getElementById(inputId) + if (input && input.tagName !== 'TEXTAREA' && input.tagName !== 'INPUT') + input = input.querySelector('textarea, input') + + if (!input) + return '' + + input.focus() + + const value = input.value ?? '' + const start = input.selectionStart ?? value.length + const end = input.selectionEnd ?? value.length + const hasSelection = end > start + const selectedText = value.substring(start, end) + + let insertedText = '' + let selectionStart = start + let selectionEnd = start + + switch (formatType) { + case 'bold': { + const text = hasSelection ? selectedText : '' + insertedText = `**${text}**` + selectionStart = start + 2 + selectionEnd = selectionStart + text.length + break + } + + case 'italic': { + const text = hasSelection ? selectedText : '' + insertedText = `*${text}*` + selectionStart = start + 1 + selectionEnd = selectionStart + text.length + break + } + + case 'heading': { + if (hasSelection) { + insertedText = selectedText + .split('\n') + .map(line => line.startsWith('# ') ? line : `# ${line}`) + .join('\n') + + selectionStart = start + selectionEnd = start + insertedText.length + } else { + const text = '' + insertedText = `# ${text}` + selectionStart = start + 2 + selectionEnd = selectionStart + text.length + } + + break + } + + case 'bullet_list': { + if (hasSelection) { + insertedText = selectedText + .split('\n') + .map(line => line.startsWith('- ') ? line : `- ${line}`) + .join('\n') + + selectionStart = start + selectionEnd = start + insertedText.length + } else { + insertedText = '- ' + selectionStart = start + 2 + selectionEnd = start + insertedText.length + } + + break + } + + case 'code': + default: { + if (hasSelection) { + if (selectedText.includes('\n')) { + insertedText = `\`\`\`\n${selectedText}\n\`\`\`` + selectionStart = start + 4 + selectionEnd = selectionStart + selectedText.length + } else { + insertedText = `\`${selectedText}\`` + selectionStart = start + 1 + selectionEnd = selectionStart + selectedText.length + } + } else { + const text = '' + insertedText = `\`${text}\`` + selectionStart = start + 1 + selectionEnd = selectionStart + text.length + } + + break + } + } + + const nextValue = value.slice(0, start) + insertedText + value.slice(end) + input.value = nextValue + input.setSelectionRange(selectionStart, selectionEnd) + input.dispatchEvent(new Event('input', { bubbles: true })) + + return nextValue +} diff --git a/app/MindWork AI Studio/wwwroot/audio.js b/app/MindWork AI Studio/wwwroot/audio.js index 689bc50f..4e9f40b5 100644 --- a/app/MindWork AI Studio/wwwroot/audio.js +++ b/app/MindWork AI Studio/wwwroot/audio.js @@ -21,59 +21,68 @@ const SOUND_EFFECT_PATHS = [ '/sounds/transcription_done.ogg' ]; +function createSoundEffectsInitResult(success, failedPaths = [], errorMessage = null) { + return { + success: success, + failedPaths: failedPaths, + errorMessage: errorMessage + }; +} + // Initialize the audio context with low-latency settings. // Should be called from a user interaction (click, keypress) // to satisfy browser autoplay policies: window.initSoundEffects = async function() { - if (soundEffectContext && soundEffectContext.state !== 'closed') { - // Already initialized, just ensure it's running: - if (soundEffectContext.state === 'suspended') { - await soundEffectContext.resume(); - } - - return; - } - try { - // Create the context with the interactive latency hint for the lowest latency: - soundEffectContext = new (window.AudioContext || window.webkitAudioContext)({ - latencyHint: 'interactive' - }); + if (soundEffectContext && soundEffectContext.state !== 'closed') { + // Already initialized, just ensure it's running: + if (soundEffectContext.state === 'suspended') { + await soundEffectContext.resume(); + } + } else { + // Create the context with the interactive latency hint for the lowest latency: + soundEffectContext = new (window.AudioContext || window.webkitAudioContext)({ + latencyHint: 'interactive' + }); - // Resume immediately (needed for Safari/macOS): - if (soundEffectContext.state === 'suspended') { - await soundEffectContext.resume(); + // Resume immediately (needed for Safari/macOS): + if (soundEffectContext.state === 'suspended') { + await soundEffectContext.resume(); + } + + // Reset the queue timing: + nextAvailablePlayTime = 0; + + // + // Play a very short silent buffer to "warm up" the audio pipeline. + // This helps prevent the first real sound from being cut off: + // + const silentBuffer = soundEffectContext.createBuffer(1, 1, soundEffectContext.sampleRate); + const silentSource = soundEffectContext.createBufferSource(); + silentSource.buffer = silentBuffer; + silentSource.connect(soundEffectContext.destination); + silentSource.start(0); + + console.log('Sound effects - AudioContext initialized with latency:', soundEffectContext.baseLatency); } - // Reset the queue timing: - nextAvailablePlayTime = 0; - - // - // Play a very short silent buffer to "warm up" the audio pipeline. - // This helps prevent the first real sound from being cut off: - // - const silentBuffer = soundEffectContext.createBuffer(1, 1, soundEffectContext.sampleRate); - const silentSource = soundEffectContext.createBufferSource(); - silentSource.buffer = silentBuffer; - silentSource.connect(soundEffectContext.destination); - silentSource.start(0); - - console.log('Sound effects - AudioContext initialized with latency:', soundEffectContext.baseLatency); - // Preload all sound effects in parallel: if (!soundEffectsPreloaded) { - await window.preloadSoundEffects(); + return await window.preloadSoundEffects(); } + + return createSoundEffectsInitResult(true); } catch (error) { console.warn('Failed to initialize sound effects:', error); + return createSoundEffectsInitResult(false, [], error?.message || String(error)); } }; // Preload all sound effect files into the cache: window.preloadSoundEffects = async function() { if (soundEffectsPreloaded) { - return; + return createSoundEffectsInitResult(true); } // Ensure that the context exists: @@ -84,10 +93,15 @@ window.preloadSoundEffects = async function() { } console.log('Sound effects - preloading', SOUND_EFFECT_PATHS.length, 'sound files...'); + const failedPaths = []; const preloadPromises = SOUND_EFFECT_PATHS.map(async (soundPath) => { try { const response = await fetch(soundPath); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + const arrayBuffer = await response.arrayBuffer(); const audioBuffer = await soundEffectContext.decodeAudioData(arrayBuffer); soundEffectCache.set(soundPath, audioBuffer); @@ -95,12 +109,20 @@ window.preloadSoundEffects = async function() { console.log('Sound effects - preloaded:', soundPath, 'duration:', audioBuffer.duration.toFixed(2), 's'); } catch (error) { console.warn('Sound effects - failed to preload:', soundPath, error); + failedPaths.push(soundPath); } }); await Promise.all(preloadPromises); - soundEffectsPreloaded = true; - console.log('Sound effects - all files preloaded'); + soundEffectsPreloaded = failedPaths.length === 0; + + if (soundEffectsPreloaded) { + console.log('Sound effects - all files preloaded'); + return createSoundEffectsInitResult(true); + } + + console.warn('Sound effects - preload finished with failures:', failedPaths); + return createSoundEffectsInitResult(false, failedPaths, 'One or more sound effects could not be loaded.'); }; window.playSound = async function(soundPath) { 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 f5bd763b..db925ca1 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md @@ -1,7 +1,26 @@ # v26.3.1, build 235 (2026-03-xx xx:xx UTC) +- Added support for the new Qwen 3.5 model family. +- Added a slide assistant, which helps you turn longer texts or documents into clear, structured presentation slides. Many thanks to Sabrina `Sabrina-devops` for her wonderful work on this assistant. +- 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 instead of typing Markdown directly. +- Added the ability to load a system prompt from a file when creating or editing chat templates. +- Added a start-page setting, so AI Studio can now open directly on your preferred page when the app starts. Configuration plugins can also provide and optionally lock this default for organizations. +- Added math rendering in chats for LaTeX display formulas, including block formats such as `$$ ... $$` and `\[ ... \]`. +- Released the document analysis assistant after an intense testing phase. +- Improved the profile selection for assistants and the chat. You can now explicitly choose between the app default profile, no profile, or a specific profile. - 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. +- Improved the reliability of the global voice recording shortcut so it stays available more consistently. - 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. \ 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 with voice recording where AI Studio could log errors and keep the feature available even though required parts failed to initialize. Voice recording is now disabled automatically for the current session in that case. +- 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. +- Fixed security issues in the native app runtime by strengthening how AI Studio creates and protects the secret values used for its internal secure connection. +- Updated several security-sensitive Rust dependencies in the native runtime to address known vulnerabilities. +- Updated .NET to v9.0.14 \ No newline at end of file diff --git a/app/MindWork AI Studio/wwwroot/chat-math.js b/app/MindWork AI Studio/wwwroot/chat-math.js new file mode 100644 index 00000000..1c1bf0f6 --- /dev/null +++ b/app/MindWork AI Studio/wwwroot/chat-math.js @@ -0,0 +1,287 @@ +const MATH_JAX_SCRIPT_ID = 'mudblazor-markdown-mathjax' +const MATH_JAX_SCRIPT_SRC = '_content/MudBlazor.Markdown/MudBlazor.Markdown.MathJax.min.js' +const INTERSECTION_ROOT_MARGIN = '240px 0px 240px 0px' +const MAX_TYPES_PER_BATCH = 4 +const containerStates = new Map() +const pendingMathElements = new Set() + +let mathJaxReadyPromise = null +let batchScheduled = false +let typesetInProgress = false + +function applyMathJaxConfiguration() { + window.MathJax = window.MathJax ?? {} + window.MathJax.options = window.MathJax.options ?? {} + window.MathJax.options.enableMenu = false +} + +function isMathJaxReady() { + return typeof window.MathJax?.typesetPromise === 'function' || typeof window.MathJax?.typeset === 'function' +} + +function waitForMathJaxReady(attempt = 0) { + if (isMathJaxReady()) + return Promise.resolve() + + if (attempt >= 80) + return Promise.reject(new Error('MathJax did not finish loading in time.')) + + return new Promise((resolve, reject) => { + window.setTimeout(() => { + waitForMathJaxReady(attempt + 1).then(resolve).catch(reject) + }, 50) + }) +} + +function ensureMathJaxLoaded() { + if (isMathJaxReady()) + return Promise.resolve() + + if (mathJaxReadyPromise) + return mathJaxReadyPromise + + mathJaxReadyPromise = new Promise((resolve, reject) => { + applyMathJaxConfiguration() + let script = document.getElementById(MATH_JAX_SCRIPT_ID) + + const onLoad = () => { + waitForMathJaxReady().then(resolve).catch(reject) + } + + const onError = () => reject(new Error('Failed to load the MathJax script.')) + + if (!script) { + script = document.createElement('script') + script.id = MATH_JAX_SCRIPT_ID + script.type = 'text/javascript' + script.src = MATH_JAX_SCRIPT_SRC + script.addEventListener('load', onLoad, { once: true }) + script.addEventListener('error', onError, { once: true }) + document.head.appendChild(script) + return + } + + script.addEventListener('load', onLoad, { once: true }) + script.addEventListener('error', onError, { once: true }) + void waitForMathJaxReady().then(resolve).catch(() => {}) + }).catch(error => { + mathJaxReadyPromise = null + throw error + }) + + return mathJaxReadyPromise +} + +function createContainerState() { + return { + signature: '', + observer: null, + observedElements: new Set() + } +} + +function disconnectContainerState(state) { + if (state.observer) { + state.observer.disconnect() + state.observer = null + } + + for (const element of state.observedElements) + pendingMathElements.delete(element) + + state.observedElements.clear() +} + +function isNearViewport(element) { + const rect = element.getBoundingClientRect() + return rect.bottom >= -240 && rect.top <= window.innerHeight + 240 +} + +function queueElementForTypeset(element, signature) { + if (!element || !element.isConnected) + return + + if (element.dataset.chatMathProcessedSignature === signature) + return + + element.dataset.chatMathTargetSignature = signature + element.dataset.chatMathPending = 'true' + pendingMathElements.add(element) + schedulePendingTypeset(false) +} + +function schedulePendingTypeset(useIdleCallback) { + if (batchScheduled) + return + + batchScheduled = true + const flush = () => { + batchScheduled = false + void flushPendingTypeset() + } + + if (useIdleCallback && typeof window.requestIdleCallback === 'function') { + window.requestIdleCallback(flush, { timeout: 120 }) + return + } + + window.requestAnimationFrame(flush) +} + +async function flushPendingTypeset() { + if (typesetInProgress || pendingMathElements.size === 0) + return + + typesetInProgress = true + const elementsToTypeset = [] + + try { + await ensureMathJaxLoaded() + + for (const element of pendingMathElements) { + if (elementsToTypeset.length >= MAX_TYPES_PER_BATCH) + break + + if (!element.isConnected) { + pendingMathElements.delete(element) + continue + } + + const targetSignature = element.dataset.chatMathTargetSignature ?? '' + if (element.dataset.chatMathProcessedSignature === targetSignature) { + pendingMathElements.delete(element) + element.dataset.chatMathPending = 'false' + continue + } + + elementsToTypeset.push(element) + } + + if (elementsToTypeset.length === 0) + return + + for (const element of elementsToTypeset) + pendingMathElements.delete(element) + + if (typeof window.MathJax?.typesetClear === 'function') { + try { + window.MathJax.typesetClear(elementsToTypeset) + } catch (error) { + console.warn('chatMath: failed to clear previous MathJax state.', error) + } + } + + if (typeof window.MathJax?.typesetPromise === 'function') + await window.MathJax.typesetPromise(elementsToTypeset) + else if (typeof window.MathJax?.typeset === 'function') + window.MathJax.typeset(elementsToTypeset) + + for (const element of elementsToTypeset) { + element.dataset.chatMathProcessedSignature = element.dataset.chatMathTargetSignature ?? '' + element.dataset.chatMathPending = 'false' + } + } catch (error) { + console.warn('chatMath: failed to typeset math content.', error) + + for (const element of elementsToTypeset) + if (element.isConnected) + pendingMathElements.add(element) + } finally { + typesetInProgress = false + + if (pendingMathElements.size > 0) + schedulePendingTypeset(true) + } +} + +function createIntersectionObserver(state, signature) { + return new IntersectionObserver(entries => { + let queuedVisibleElement = false + + for (const entry of entries) { + if (!entry.isIntersecting) + continue + + const element = entry.target + state.observer?.unobserve(element) + state.observedElements.delete(element) + queueElementForTypeset(element, signature) + queuedVisibleElement = true + } + + if (queuedVisibleElement) + schedulePendingTypeset(true) + }, { + root: null, + rootMargin: INTERSECTION_ROOT_MARGIN, + threshold: 0.01 + }) +} + +function getMathElements(container) { + return Array.from(container.querySelectorAll('.chat-mathjax-block')) +} + +window.chatMath = { + syncContainer: async function(container, signature) { + if (!container) + return + + let state = containerStates.get(container) + if (!state) { + state = createContainerState() + containerStates.set(container, state) + } + + if (state.signature === signature) + return + + disconnectContainerState(state) + state.signature = signature + + const mathElements = getMathElements(container) + if (mathElements.length === 0) + return + + await ensureMathJaxLoaded() + + state.observer = createIntersectionObserver(state, signature) + + for (const element of mathElements) { + if (isNearViewport(element)) { + queueElementForTypeset(element, signature) + continue + } + + element.dataset.chatMathTargetSignature = signature + state.observer.observe(element) + state.observedElements.add(element) + } + + schedulePendingTypeset(false) + }, + + disposeContainer: function(container) { + if (!container) + return + + const state = containerStates.get(container) + if (!state) + return + + disconnectContainerState(state) + containerStates.delete(container) + + const mathElements = getMathElements(container) + for (const element of mathElements) + pendingMathElements.delete(element) + + if (typeof window.MathJax?.typesetClear === 'function' && mathElements.length > 0) { + try { + window.MathJax.typesetClear(mathElements) + } catch (error) { + console.warn('chatMath: failed to clear container MathJax state during dispose.', error) + } + } + } +} \ No newline at end of file diff --git a/metadata.txt b/metadata.txt index db22a53e..c7d68f81 100644 --- a/metadata.txt +++ b/metadata.txt @@ -1,11 +1,11 @@ 26.2.2 2026-02-22 14:14:47 UTC 234 -9.0.114 (commit 4c5aac3d56) -9.0.13 (commit 9ecbfd4f3f) +9.0.115 (commit 45056ad45c) +9.0.14 (commit 19c07820cb) 1.93.1 (commit 01f6ddf75) 8.15.0 -1.8.1 +1.8.3 3eb367d4c9e, release win-x64 144.0.7543.0 diff --git a/runtime/Cargo.lock b/runtime/Cargo.lock index 407a5627..57018f5b 100644 --- a/runtime/Cargo.lock +++ b/runtime/Cargo.lock @@ -22,7 +22,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.12", ] [[package]] @@ -123,7 +123,7 @@ checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", "synstructure", ] @@ -135,7 +135,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -157,7 +157,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -168,7 +168,7 @@ checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -233,9 +233,9 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "aws-lc-rs" -version = "1.13.3" +version = "1.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c953fe1ba023e6b7730c0d4b031d06f267f23a46167dcbd40316644b10a17ba" +checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" dependencies = [ "aws-lc-sys", "zeroize", @@ -243,11 +243,10 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.30.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbfd150b5dbdb988bcc8fb1fe787eb6b7ee6180ca24da683b61ea5405f3d43ff" +checksum = "1fa7e52a4c5c547c741610a2c6f123f3881e409b714cd27e6798ef020c514f0a" dependencies = [ - "bindgen", "cc", "cmake", "dunce", @@ -287,29 +286,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bindgen" -version = "0.69.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" -dependencies = [ - "bitflags 2.6.0", - "cexpr", - "clang-sys", - "itertools 0.12.1", - "lazy_static", - "lazycell", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash 1.1.0", - "shlex", - "syn 2.0.93", - "which", -] - [[package]] name = "bit_field" version = "0.10.2" @@ -354,9 +330,9 @@ dependencies = [ [[package]] name = "brotli" -version = "6.0.0" +version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" +checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -461,9 +437,9 @@ dependencies = [ [[package]] name = "calamine" -version = "0.33.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96ae094b353c7810cd5efd2e69413ebb9354816138a387c09f7b90d4e826a49f" +checksum = "20ae05a4e39297eecf9a994210d27501318c37a9318201f8e11050add82bb6f0" dependencies = [ "atoi_simd", "byteorder", @@ -471,7 +447,7 @@ dependencies = [ "encoding_rs", "fast-float2", "log", - "quick-xml 0.38.4", + "quick-xml 0.39.2", "serde", "zip 7.4.0", ] @@ -513,15 +489,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - [[package]] name = "cfb" version = "0.7.3" @@ -564,6 +531,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.0", +] + [[package]] name = "chrono" version = "0.4.40" @@ -589,17 +567,6 @@ dependencies = [ "inout", ] -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - [[package]] name = "clipboard-win" version = "5.4.0" @@ -775,6 +742,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc" version = "3.3.0" @@ -873,7 +849,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -883,7 +859,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" dependencies = [ "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -907,7 +883,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -918,7 +894,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -995,7 +971,7 @@ checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -1008,7 +984,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -1041,7 +1017,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -1100,7 +1076,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -1120,9 +1096,9 @@ dependencies = [ [[package]] name = "dunce" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "either" @@ -1167,12 +1143,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1204,9 +1180,9 @@ checksum = "f8eb564c5c7423d25c886fb561d1e4ee69f72354d16918afa32c08811f6b6a55" [[package]] name = "fastrand" -version = "2.1.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fdeflate" @@ -1304,6 +1280,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "foreign-types" version = "0.3.2" @@ -1346,9 +1328,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -1361,9 +1343,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -1371,15 +1353,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -1388,38 +1370,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -1429,7 +1411,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -1599,6 +1580,20 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "rand_core 0.10.0", + "wasip2", + "wasip3", +] + [[package]] name = "gif" version = "0.13.1" @@ -1828,6 +1823,9 @@ name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "foldhash", +] [[package]] name = "heck" @@ -1877,15 +1875,6 @@ dependencies = [ "digest", ] -[[package]] -name = "home" -version = "0.5.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "html5ever" version = "0.26.0" @@ -2115,9 +2104,9 @@ dependencies = [ [[package]] name = "ico" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3804960be0bb5e4edb1e1ad67afd321a9ecfd875c3e65c099468fd2717d7cae" +checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98" dependencies = [ "byteorder", "png", @@ -2238,9 +2227,15 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -2400,15 +2395,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.14.0" @@ -2517,17 +2503,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "json-patch" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec9ad60d674508f3ca8f380a928cfe7b096bc729c4e2dbfe3852bc45da3ab30b" -dependencies = [ - "serde", - "serde_json", - "thiserror 1.0.63", -] - [[package]] name = "json-patch" version = "2.0.0" @@ -2585,10 +2560,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] -name = "lazycell" -version = "1.3.0" +name = "leb128fmt" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "lebe" @@ -2598,9 +2573,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" -version = "0.2.174" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libdbus-sys" @@ -2646,6 +2621,12 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "litemap" version = "0.7.4" @@ -2799,7 +2780,6 @@ dependencies = [ "calamine", "cbc", "cfg-if", - "cipher", "file-format", "flexi_logger", "futures", @@ -2811,10 +2791,10 @@ dependencies = [ "pbkdf2", "pdfium-render", "pptx-to-md", - "rand 0.9.1", - "rand_chacha 0.9.0", + "rand 0.10.0", + "rand_chacha 0.10.0", "rcgen", - "reqwest 0.13.1", + "reqwest 0.13.2", "rocket", "serde", "serde_json", @@ -2822,6 +2802,7 @@ dependencies = [ "strum_macros", "sys-locale", "sysinfo", + "tar", "tauri", "tauri-build", "tauri-plugin-window-state", @@ -3221,9 +3202,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "open" @@ -3237,9 +3218,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.75" +version = "0.10.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" dependencies = [ "bitflags 2.6.0", "cfg-if", @@ -3258,7 +3239,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -3284,9 +3265,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.111" +version = "0.9.112" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" dependencies = [ "cc", "libc", @@ -3382,7 +3363,7 @@ dependencies = [ "console_error_panic_hook", "console_log", "image 0.25.2", - "itertools 0.14.0", + "itertools", "js-sys", "libloading", "log", @@ -3415,7 +3396,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -3538,7 +3519,7 @@ dependencies = [ "phf_shared 0.11.2", "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -3574,12 +3555,6 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - [[package]] name = "piston-float" version = "1.0.1" @@ -3652,12 +3627,12 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "prettyplease" -version = "0.2.25" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -3717,7 +3692,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", "version_check", "yansi", ] @@ -3742,9 +3717,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.38.4" +version = "0.39.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" dependencies = [ "encoding_rs", "memchr", @@ -3761,7 +3736,7 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash 2.1.1", + "rustc-hash", "rustls 0.23.28", "socket2 0.6.2", "thiserror 2.0.12", @@ -3772,9 +3747,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.13" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ "aws-lc-rs", "bytes", @@ -3782,7 +3757,7 @@ dependencies = [ "lru-slab", "rand 0.9.1", "ring", - "rustc-hash 2.1.1", + "rustc-hash", "rustls 0.23.28", "rustls-pki-types", "slab", @@ -3815,6 +3790,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.7.3" @@ -3850,6 +3831,17 @@ dependencies = [ "rand_core 0.9.0", ] +[[package]] +name = "rand" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.0", +] + [[package]] name = "rand_chacha" version = "0.2.2" @@ -3880,6 +3872,16 @@ dependencies = [ "rand_core 0.9.0", ] +[[package]] +name = "rand_chacha" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e6af7f3e25ded52c41df4e0b1af2d047e45896c2f3281792ed68a1c243daedb" +dependencies = [ + "ppv-lite86", + "rand_core 0.10.0", +] + [[package]] name = "rand_core" version = "0.5.1" @@ -3908,6 +3910,12 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "rand_core" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" + [[package]] name = "rand_hc" version = "0.2.0" @@ -4012,7 +4020,7 @@ checksum = "bcc303e793d3734489387d205e9b186fac9c6cfacedd98cbb2e8a5943595f3e6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -4088,9 +4096,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04e9018c9d814e5f30cc16a0f03271aeab3571e609612d9fe78c1aa8d11c2f62" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" dependencies = [ "base64 0.22.1", "bytes", @@ -4215,7 +4223,7 @@ dependencies = [ "proc-macro2", "quote", "rocket_http", - "syn 2.0.93", + "syn 2.0.117", "unicode-xid", "version_check", ] @@ -4256,12 +4264,6 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - [[package]] name = "rustc-hash" version = "2.1.1" @@ -4295,10 +4297,23 @@ dependencies = [ "bitflags 2.6.0", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.4.14", "windows-sys 0.52.0", ] +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + [[package]] name = "rustls" version = "0.21.12" @@ -4320,7 +4335,7 @@ dependencies = [ "aws-lc-rs", "once_cell", "rustls-pki-types", - "rustls-webpki 0.103.3", + "rustls-webpki 0.103.10", "subtle", "zeroize", ] @@ -4348,11 +4363,12 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.11.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ "web-time", + "zeroize", ] [[package]] @@ -4369,7 +4385,7 @@ dependencies = [ "rustls 0.23.28", "rustls-native-certs", "rustls-platform-verifier-android", - "rustls-webpki 0.103.3", + "rustls-webpki 0.103.10", "security-framework 3.5.1", "security-framework-sys", "webpki-root-certs", @@ -4394,9 +4410,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.3" +version = "0.103.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" dependencies = [ "aws-lc-rs", "ring", @@ -4548,7 +4564,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -4573,7 +4589,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -4624,7 +4640,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -4646,7 +4662,7 @@ checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -4666,7 +4682,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.12", "digest", ] @@ -4677,7 +4693,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.12", "digest", ] @@ -4863,14 +4879,14 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum_macros" -version = "0.27.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -4892,9 +4908,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.93" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c786062daee0d6db1132800e623df74274a0a87322d8e183338e01b3d98d058" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -4924,7 +4940,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -4938,9 +4954,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.38.0" +version = "0.38.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe840c5b1afe259a5657392a4dbb74473a14c8db999c3ec2f4ae812e028a94da" +checksum = "92ab6a2f8bfe508deb3c6406578252e491d299cbbf3bc0529ecc3313aee4a52f" dependencies = [ "libc", "memchr", @@ -5078,9 +5094,9 @@ dependencies = [ [[package]] name = "tar" -version = "0.4.41" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb797dad5fb5b76fcf519e702f4a589483b5ef06567f160c392832c1f5e44909" +checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" dependencies = [ "filetime", "libc", @@ -5095,9 +5111,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "1.8.1" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf327e247698d3f39af8aa99401c9708384290d1f5c544bf5d251d44c2fea22" +checksum = "3ae1f57c291a6ab8e1d2e6b8ad0a35ff769c9925deb8a89de85425ff08762d0c" dependencies = [ "anyhow", "base64 0.22.1", @@ -5157,15 +5173,15 @@ dependencies = [ [[package]] name = "tauri-build" -version = "1.5.3" +version = "1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c6ec7a5c3296330c7818478948b422967ce4649094696c985f61d50076d29c" +checksum = "2db08694eec06f53625cfc6fff3a363e084e5e9a238166d2989996413c346453" dependencies = [ "anyhow", "cargo_toml", "dirs-next", "heck 0.5.0", - "json-patch 1.4.0", + "json-patch", "semver", "serde", "serde_json", @@ -5176,14 +5192,14 @@ dependencies = [ [[package]] name = "tauri-codegen" -version = "1.4.5" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93a9e3f5cebf779a63bf24903e714ec91196c307d8249a0008b882424328bcda" +checksum = "53438d78c4a037ffe5eafa19e447eea599bedfb10844cb08ec53c2471ac3ac3f" dependencies = [ "base64 0.21.7", "brotli", "ico", - "json-patch 2.0.0", + "json-patch", "plist", "png", "proc-macro2", @@ -5202,9 +5218,9 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "1.4.6" +version = "1.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1d0e989f54fe06c5ef0875c5e19cf96453d099a0a774d5192ab47e80471cdab" +checksum = "233988ac08c1ed3fe794cd65528d48d8f7ed4ab3895ca64cdaa6ad4d00c45c0b" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -5230,9 +5246,9 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "0.14.5" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f33fda7d213e239077fad52e96c6b734cecedb30c2382118b64f94cb5103ff3a" +checksum = "8066855882f00172935e3fa7d945126580c34dcbabab43f5d4f0c2398a67d47b" dependencies = [ "gtk", "http 0.2.12", @@ -5251,9 +5267,9 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "0.14.10" +version = "0.14.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18c447dcd9b0f09c7dc4b752cc33e72788805bfd761fbda5692d30c48289efec" +checksum = "ce361fec1e186705371f1c64ae9dd2a3a6768bc530d0a2d5e75a634bb416ad4d" dependencies = [ "cocoa", "gtk", @@ -5271,9 +5287,9 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "1.6.1" +version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83a0c939e88d82903a0a7dfb28388b12a3c03504d6bd6086550edaa3b6d8beaa" +checksum = "c357952645e679de02cd35007190fcbce869b93ffc61b029f33fe02648453774" dependencies = [ "brotli", "ctor", @@ -5282,7 +5298,7 @@ dependencies = [ "heck 0.5.0", "html5ever", "infer", - "json-patch 2.0.0", + "json-patch", "kuchikiki", "log", "memchr", @@ -5311,14 +5327,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.10.1" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ - "cfg-if", "fastrand", - "rustix", - "windows-sys 0.52.0", + "getrandom 0.3.1", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", ] [[package]] @@ -5364,7 +5381,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -5375,7 +5392,7 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -5457,9 +5474,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.49.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", @@ -5479,7 +5496,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -5668,7 +5685,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -5919,6 +5936,24 @@ dependencies = [ "wit-bindgen-rt", ] +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -5941,7 +5976,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", "wasm-bindgen-shared", ] @@ -5975,7 +6010,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5989,6 +6024,28 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.7.0", + "wasm-encoder", + "wasmparser", +] + [[package]] name = "wasm-streams" version = "0.4.0" @@ -6002,6 +6059,18 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.6.0", + "hashbrown 0.15.2", + "indexmap 2.7.0", + "semver", +] + [[package]] name = "web-sys" version = "0.3.69" @@ -6122,18 +6191,6 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" -[[package]] -name = "which" -version = "4.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" -dependencies = [ - "either", - "home", - "once_cell", - "rustix", -] - [[package]] name = "winapi" version = "0.3.9" @@ -6283,7 +6340,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -6294,7 +6351,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -6817,6 +6874,26 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + [[package]] name = "wit-bindgen-rt" version = "0.33.0" @@ -6826,6 +6903,74 @@ dependencies = [ "bitflags 2.6.0", ] +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap 2.7.0", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.6.0", + "indexmap 2.7.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.7.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + [[package]] name = "write16" version = "1.0.0" @@ -6904,7 +7049,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" dependencies = [ "gethostname", - "rustix", + "rustix 0.38.34", "x11rb-protocol", ] @@ -6939,8 +7084,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" dependencies = [ "libc", - "linux-raw-sys", - "rustix", + "linux-raw-sys 0.4.14", + "rustix 0.38.34", ] [[package]] @@ -6990,7 +7135,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", "synstructure", ] @@ -7011,7 +7156,7 @@ checksum = "06718a168365cad3d5ff0bb133aad346959a2074bd4a85c121255a11304a8626" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -7031,7 +7176,7 @@ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", "synstructure", ] @@ -7052,7 +7197,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] @@ -7074,7 +7219,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.93", + "syn 2.0.117", ] [[package]] diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index b3c1b32e..0fb62f1a 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -6,27 +6,26 @@ description = "MindWork AI Studio" authors = ["Thorsten Sommer"] [build-dependencies] -tauri-build = { version = "1.5", features = [] } +tauri-build = { version = "1.5.6", features = [] } [dependencies] -tauri = { version = "1.8", features = [ "http-all", "updater", "shell-sidecar", "shell-open", "dialog", "global-shortcut"] } +tauri = { version = "1.8.3", features = [ "http-all", "updater", "shell-sidecar", "shell-open", "dialog", "global-shortcut"] } tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" keyring = { version = "3.6.2", features = ["apple-native", "windows-native", "sync-secret-service"] } arboard = "3.6.1" -tokio = { version = "1.49.0", features = ["rt", "rt-multi-thread", "macros", "process"] } +tokio = { version = "1.50.0", features = ["rt", "rt-multi-thread", "macros", "process"] } tokio-stream = "0.1.18" -futures = "0.3.31" +futures = "0.3.32" async-stream = "0.3.6" flexi_logger = "0.31.8" log = { version = "0.4.29", features = ["kv"] } -once_cell = "1.21.3" +once_cell = "1.21.4" rocket = { version = "0.5.1", features = ["json", "tls"] } -rand = "0.9.1" -rand_chacha = "0.9" +rand = "0.10.0" +rand_chacha = "0.10.0" base64 = "0.22.1" -cipher = { version = "0.4.4", features = ["std"] } aes = "0.8.4" cbc = "0.1.2" pbkdf2 = "0.12.2" @@ -34,25 +33,26 @@ hmac = "0.12.1" sha2 = "0.10.8" rcgen = { version = "0.14.7", features = ["pem"] } file-format = "0.28.0" -calamine = "0.33.0" +calamine = "0.34.0" pdfium-render = "0.8.37" sys-locale = "0.3.2" cfg-if = "1.0.4" pptx-to-md = "0.4.0" -tempfile = "3.8" -strum_macros = "0.27" -sysinfo = "0.38.0" +tempfile = "3.27.0" +strum_macros = "0.28.0" +sysinfo = "0.38.4" # Fixes security vulnerability downstream, where the upstream is not fixed yet: time = "0.3.47" # -> Rocket bytes = "1.11.1" # -> almost every dependency +tar = "0.4.45" # -> Tauri v1 [target.'cfg(target_os = "linux")'.dependencies] # See issue https://github.com/tauri-apps/tauri/issues/4470 -reqwest = { version = "0.13.1", features = ["native-tls-vendored"] } +reqwest = { version = "0.13.2", features = ["native-tls-vendored"] } # Fixes security vulnerability downstream, where the upstream is not fixed yet: -openssl = "0.10.75" +openssl = "0.10.76" # -> reqwest, Tauri v1 [target.'cfg(target_os = "windows")'.dependencies] windows-registry = "0.6.1" diff --git a/runtime/src/api_token.rs b/runtime/src/api_token.rs index e945095e..7fc8238f 100644 --- a/runtime/src/api_token.rs +++ b/runtime/src/api_token.rs @@ -1,4 +1,6 @@ -use rand::{RngCore, SeedableRng}; +use log::error; +use rand::rngs::SysRng; +use rand::{Rng, SeedableRng}; use rand_chacha::ChaChaRng; /// The API token data structure used to authenticate requests. @@ -36,7 +38,16 @@ impl APIToken { pub fn generate_api_token() -> APIToken { let mut token = [0u8; 32]; - let mut rng = ChaChaRng::from_os_rng(); + + // The API token authenticates privileged runtime requests. If the OS-backed + // RNG cannot provide a secure seed, we abort instead of using a weaker RNG + // because a predictable token would silently break the app's security model. + let mut sys_rng = SysRng; + let mut rng = ChaChaRng::try_from_rng(&mut sys_rng) + .unwrap_or_else(|e| { + error!(Source = "API Token"; "Failed to seed ChaChaRng from SysRng: {e}"); + panic!("Failed to seed ChaChaRng from SysRng: {e}"); + }); rng.fill_bytes(&mut token); APIToken::from_bytes(token.to_vec()) } \ No newline at end of file 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/encryption.rs b/runtime/src/encryption.rs index 632915d7..41506855 100644 --- a/runtime/src/encryption.rs +++ b/runtime/src/encryption.rs @@ -4,10 +4,11 @@ use base64::Engine; use base64::prelude::BASE64_STANDARD; use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, BlockEncryptMut, KeyIvInit}; use hmac::Hmac; -use log::info; +use log::{error, info}; use once_cell::sync::Lazy; use pbkdf2::pbkdf2; -use rand::{RngCore, SeedableRng}; +use rand::rngs::SysRng; +use rand::{Rng, SeedableRng}; use rocket::{data, Data, Request}; use rocket::data::ToByteUnit; use rocket::http::Status; @@ -31,15 +32,25 @@ pub static ENCRYPTION: Lazy = Lazy::new(|| { // We use a cryptographically secure pseudo-random number generator // to generate the secret password & salt. ChaCha20Rng is the algorithm - // of our choice: - let mut rng = rand_chacha::ChaChaRng::from_os_rng(); + // of our choice. If the OS-backed RNG is unavailable, we fail fast instead + // of falling back to a weaker RNG because these values protect the IPC + // channel and must remain cryptographically secure. + let mut sys_rng = SysRng; + let mut rng = rand_chacha::ChaChaRng::try_from_rng(&mut sys_rng) + .unwrap_or_else(|e| { + error!(Source = "Encryption"; "Failed to seed ChaChaRng from SysRng: {e}"); + panic!("Failed to seed ChaChaRng from SysRng: {e}"); + }); // Fill the secret key & salt with random bytes: rng.fill_bytes(&mut secret_key); rng.fill_bytes(&mut secret_key_salt); info!("Secret password for the IPC channel was generated successfully."); - Encryption::new(&secret_key, &secret_key_salt).unwrap() + Encryption::new(&secret_key, &secret_key_salt).unwrap_or_else(|e| { + error!(Source = "Encryption"; "Failed to initialize encryption for the IPC channel: {e}"); + panic!("Failed to initialize encryption for the IPC channel: {e}"); + }) }); /// The encryption struct used for the IPC channel. @@ -98,9 +109,14 @@ impl Encryption { /// Encrypts the given data. pub fn encrypt(&self, data: &str) -> Result { let cipher = Aes256CbcEnc::new(&self.key.into(), &self.iv.into()); - let encrypted = cipher.encrypt_padded_vec_mut::(data.as_bytes()); + let data = data.as_bytes(); + let mut buffer = vec![0u8; data.len() + 16]; + buffer[..data.len()].copy_from_slice(data); + let encrypted = cipher + .encrypt_padded_mut::(&mut buffer, data.len()) + .map_err(|e| format!("Error encrypting data: {e}"))?; let mut result = BASE64_STANDARD.encode(self.secret_key_salt); - result.push_str(&BASE64_STANDARD.encode(&encrypted)); + result.push_str(&BASE64_STANDARD.encode(encrypted)); Ok(EncryptedText::new(result)) } @@ -118,9 +134,12 @@ impl Encryption { } let cipher = Aes256CbcDec::new(&self.key.into(), &self.iv.into()); - let decrypted = cipher.decrypt_padded_vec_mut::(encrypted).map_err(|e| format!("Error decrypting data: {e}"))?; + let mut buffer = encrypted.to_vec(); + let decrypted = cipher + .decrypt_padded_mut::(&mut buffer) + .map_err(|e| format!("Error decrypting data: {e}"))?; - String::from_utf8(decrypted).map_err(|e| format!("Error converting decrypted data to string: {}", e)) + String::from_utf8(decrypted.to_vec()).map_err(|e| format!("Error converting decrypted data to string: {}", e)) } } 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"