diff --git a/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor b/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor index 9f19942d..e1973b8a 100644 --- a/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor +++ b/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor @@ -41,7 +41,7 @@ } else { - + @foreach (var server in this.SettingsManager.ConfigurationData.ERI.ERIServers) { @@ -52,10 +52,10 @@ else } - + @T("Add ERI server preset") - + @T("Delete this server preset") @@ -82,18 +82,18 @@ else } - +
@T("Common ERI server settings") - - + + - + @foreach (var language in Enum.GetValues()) { @@ -103,12 +103,12 @@ else @if (this.selectedProgrammingLanguage is ProgrammingLanguages.OTHER) { - + } - + @foreach (var version in Enum.GetValues()) { @@ -116,7 +116,7 @@ else } - + @T("Download specification") @@ -126,7 +126,7 @@ else - + @foreach (var dataSource in Enum.GetValues()) { @@ -136,21 +136,21 @@ else @if (this.selectedDataSource is DataSources.CUSTOM) { - + } @if(this.selectedDataSource > DataSources.FILE_SYSTEM) { - + } @if (this.NeedHostnamePort()) {
- - + + @if (this.dataSourcePort < 1024) { @@ -168,7 +168,7 @@ else } - + @if (this.selectedAuthenticationMethods.Contains(Auth.KERBEROS)) { - + @foreach (var os in Enum.GetValues()) { @@ -204,7 +204,7 @@ else @T("Data protection settings") - + @foreach (var option in Enum.GetValues()) { @@ -227,7 +227,7 @@ else @if (!this.IsNoneERIServerSelected) { - + @@ -243,10 +243,10 @@ else @context.EmbeddingType - + @T("Edit") - + @T("Delete") @@ -262,7 +262,7 @@ else } } - + @T("Add Embedding Method") @@ -276,7 +276,7 @@ else @if (!this.IsNoneERIServerSelected) { - + @@ -289,10 +289,10 @@ else @context.Name - + @T("Edit") - + @T("Delete") @@ -308,7 +308,7 @@ else } } - + @T("Add Retrieval Process") @@ -316,7 +316,7 @@ else @T("You can integrate additional libraries. Perhaps you want to evaluate the prompts in advance using a machine learning method or analyze them with a text mining approach? Or maybe you want to preprocess images in the prompts? For such advanced scenarios, you can specify which libraries you want to use here. It's best to describe which library you want to integrate for which purpose. This way, the LLM that writes the ERI server for you can try to use these libraries effectively. This should result in less rework being necessary. If you don't know the necessary libraries, you can instead attempt to describe the intended use. The LLM can then attempt to choose suitable libraries. However, hallucinations can occur, and fictional libraries might be selected.") - + @T("Provider selection for generation") @@ -330,7 +330,7 @@ else @T("Important:") @T("The LLM may need to generate many files. This reaches the request limit of most providers. Typically, only a certain number of requests can be made per minute, and only a maximum number of tokens can be generated per minute. AI Studio automatically considers this.") @T("However, generating all the files takes a certain amount of time.") @T("Local or self-hosted models may work without these limitations and can generate responses faster. AI Studio dynamically adapts its behavior and always tries to achieve the fastest possible data processing.") - + @T("Write code to file system") @@ -344,5 +344,5 @@ else @T("When you rebuild / re-generate the ERI server code, AI Studio proceeds as follows: All files generated last time will be deleted. All other files you have created remain. Then, the AI generates the new files.") @T("But beware:") @T("It may happen that the AI generates a file this time that you manually created last time. In this case, your manually created file will then be overwritten. Therefore, you should always create a Git repository and commit or revert all changes before using this assistant. With a diff visualization, you can immediately see where the AI has made changes. It is best to use an IDE suitable for your selected language for this purpose.") - - + + diff --git a/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor.cs b/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor.cs index 96f3f953..229a6368 100644 --- a/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor.cs +++ b/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor.cs @@ -534,15 +534,26 @@ public partial class AssistantERI : AssistantBaseCore } private bool AreServerPresetsBlocked => !this.SettingsManager.ConfigurationData.ERI.PreselectOptions; + + /// + /// Gets whether ERI server preset controls should be disabled. + /// + private bool AreServerPresetControlsDisabled => this.AreServerPresetsBlocked || this.IsProcessing; private void SelectedERIServerChanged(DataERIServer? server) { + if (this.IsProcessing) + return; + this.selectedERIServer = server; this.ResetForm(); } private async Task AddERIServer() { + if (this.IsProcessing) + return; + this.SettingsManager.ConfigurationData.ERI.ERIServers.Add(new () { ServerName = string.Format(T("ERI Server {0}"), DateTimeOffset.UtcNow), @@ -553,6 +564,9 @@ public partial class AssistantERI : AssistantBaseCore private async Task RemoveERIServer() { + if (this.IsProcessing) + return; + if(this.selectedERIServer is null) return; @@ -576,6 +590,31 @@ public partial class AssistantERI : AssistantBaseCore private bool IsNoneERIServerSelected => this.selectedERIServer is null; + /// + /// Gets whether ERI configuration input controls should be disabled. + /// + private bool IsERIInputDisabled => this.IsNoneERIServerSelected || this.IsProcessing; + + /// + /// Gets whether the selected ERI specification cannot be downloaded. + /// + private bool IsSpecificationDownloadDisabled => !this.selectedERIVersion.WasSpecificationSelected() || this.IsERIInputDisabled; + + /// + /// Gets whether the generated-code target directory selection should be disabled. + /// + private bool IsBaseDirectorySelectionDisabled => this.IsERIInputDisabled || !this.writeToFilesystem; + + /// + /// Gets a stable row snapshot for the embedding-method table. + /// + private EmbeddingInfo[] EmbeddingRows => this.embeddings.ToArray(); + + /// + /// Gets a stable row snapshot for the retrieval-process table. + /// + private RetrievalInfo[] RetrievalProcessRows => this.retrievalProcesses.ToArray(); + /// /// Gets called when the server name was changed by typing. /// @@ -863,6 +902,9 @@ public partial class AssistantERI : AssistantBaseCore private async Task AddEmbedding() { + if (this.IsProcessing) + return; + var dialogParameters = new DialogParameters { { x => x.IsEditing, false }, @@ -881,6 +923,9 @@ public partial class AssistantERI : AssistantBaseCore private async Task EditEmbedding(EmbeddingInfo embeddingInfo) { + if (this.IsProcessing) + return; + var dialogParameters = new DialogParameters { { x => x.DataEmbeddingName, embeddingInfo.EmbeddingName }, @@ -906,6 +951,9 @@ public partial class AssistantERI : AssistantBaseCore private async Task DeleteEmbedding(EmbeddingInfo embeddingInfo) { + if (this.IsProcessing) + return; + var message = this.retrievalProcesses.Any(n => n.Embeddings?.Contains(embeddingInfo) is true) ? string.Format(T("The embedding '{0}' is used in one or more retrieval processes. Are you sure you want to delete it?"), embeddingInfo.EmbeddingName) : string.Format(T("Are you sure you want to delete the embedding '{0}'?"), embeddingInfo.EmbeddingName); @@ -928,6 +976,9 @@ public partial class AssistantERI : AssistantBaseCore private async Task AddRetrievalProcess() { + if (this.IsProcessing) + return; + var dialogParameters = new DialogParameters { { x => x.IsEditing, false }, @@ -947,6 +998,9 @@ public partial class AssistantERI : AssistantBaseCore private async Task EditRetrievalProcess(RetrievalInfo retrievalInfo) { + if (this.IsProcessing) + return; + var dialogParameters = new DialogParameters { { x => x.DataName, retrievalInfo.Name }, @@ -973,6 +1027,9 @@ public partial class AssistantERI : AssistantBaseCore private async Task DeleteRetrievalProcess(RetrievalInfo retrievalInfo) { + if (this.IsProcessing) + return; + var dialogParameters = new DialogParameters { { x => x.Message, string.Format(T("Are you sure you want to delete the retrieval process '{0}'?"), retrievalInfo.Name) }, @@ -1032,6 +1089,10 @@ public partial class AssistantERI : AssistantBaseCore this.AddInputIssue(T("Please describe at least one retrieval process.")); return; } + + var writeToFilesystemSnapshot = this.writeToFilesystem; + var baseDirectorySnapshot = this.baseDirectory; + var previouslyGeneratedFilesSnapshot = this.previouslyGeneratedFiles.ToArray(); this.eriSpecification = await this.selectedERIVersion.ReadSpecification(this.HttpClient); if (string.IsNullOrWhiteSpace(this.eriSpecification)) @@ -1073,9 +1134,9 @@ public partial class AssistantERI : AssistantBaseCore var fileListAnswer = await this.AddAIResponseAsync(time, true); // Is this an update of the ERI server? If so, we need to delete the previously generated files: - if (this.writeToFilesystem && this.previouslyGeneratedFiles.Count > 0 && !string.IsNullOrWhiteSpace(fileListAnswer)) + if (writeToFilesystemSnapshot && previouslyGeneratedFilesSnapshot.Length > 0 && !string.IsNullOrWhiteSpace(fileListAnswer)) { - foreach (var file in this.previouslyGeneratedFiles) + foreach (var file in previouslyGeneratedFilesSnapshot) { try { @@ -1097,7 +1158,8 @@ public partial class AssistantERI : AssistantBaseCore } var generatedFiles = new List(); - foreach (var file in this.ExtractFiles(fileListAnswer)) + var filesToGenerate = this.ExtractFiles(fileListAnswer).ToArray(); + foreach (var file in filesToGenerate) { this.Logger.LogInformation($"The LLM want to create the file: '{file}'"); @@ -1117,15 +1179,15 @@ public partial class AssistantERI : AssistantBaseCore ``` """, true); var generatedCodeMarkdown = await this.AddAIResponseAsync(time); - if (this.writeToFilesystem) + if (writeToFilesystemSnapshot) { - var desiredFilePath = Path.Join(this.baseDirectory, file); + var desiredFilePath = Path.Join(baseDirectorySnapshot, file); // Security check: ensure that the desired file path is inside the base directory. // We cannot trust the beginning of the file path because it would be possible // to escape by using `..` in the file path. - if (!desiredFilePath.StartsWith(this.baseDirectory, StringComparison.InvariantCultureIgnoreCase) || desiredFilePath.Contains("..")) - this.Logger.LogWarning($"The file path '{desiredFilePath}' is may not inside the base directory '{this.baseDirectory}'."); + if (!desiredFilePath.StartsWith(baseDirectorySnapshot, StringComparison.InvariantCultureIgnoreCase) || desiredFilePath.Contains("..")) + this.Logger.LogWarning($"The file path '{desiredFilePath}' is may not inside the base directory '{baseDirectorySnapshot}'."); else { @@ -1160,7 +1222,7 @@ public partial class AssistantERI : AssistantBaseCore } } - if(this.writeToFilesystem) + if(writeToFilesystemSnapshot) { this.previouslyGeneratedFiles = generatedFiles; this.selectedERIServer!.PreviouslyGeneratedFiles = generatedFiles;