From d4dd368e64d62c7d0cd228fe0e7cb58e4580efe5 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sat, 26 Apr 2025 18:18:44 +0200 Subject: [PATCH] Finished I18N assistant --- .../Assistants/I18N/AssistantI18N.razor | 34 ++++ .../Assistants/I18N/AssistantI18N.razor.cs | 156 +++++++++++++++++- 2 files changed, 185 insertions(+), 5 deletions(-) diff --git a/app/MindWork AI Studio/Assistants/I18N/AssistantI18N.razor b/app/MindWork AI Studio/Assistants/I18N/AssistantI18N.razor index 38e9255f..ffb25555 100644 --- a/app/MindWork AI Studio/Assistants/I18N/AssistantI18N.razor +++ b/app/MindWork AI Studio/Assistants/I18N/AssistantI18N.razor @@ -87,4 +87,38 @@ else if (!this.isLoading && string.IsNullOrWhiteSpace(this.loadingIssue)) { } + + @if (this.localizedContent.Count > 0) + { +
+ + Localized Content (@this.localizedContent.Count entries of @this.addedContent.Count) + + + + + + + + + + + Key + Text + + + +
+                    @context.Key
+                
+
+ + @context.Value + +
+ + + +
+ } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/I18N/AssistantI18N.razor.cs b/app/MindWork AI Studio/Assistants/I18N/AssistantI18N.razor.cs index 592ff8b4..e644d1b2 100644 --- a/app/MindWork AI Studio/Assistants/I18N/AssistantI18N.razor.cs +++ b/app/MindWork AI Studio/Assistants/I18N/AssistantI18N.razor.cs @@ -1,8 +1,13 @@ +using System.Diagnostics; +using System.Text; + using AIStudio.Dialogs.Settings; using AIStudio.Tools.PluginSystem; using Microsoft.Extensions.FileProviders; +using SharedTools; + namespace AIStudio.Assistants.I18N; public partial class AssistantI18N : AssistantBaseCore @@ -17,22 +22,55 @@ public partial class AssistantI18N : AssistantBaseCore """; protected override string SystemPrompt => - """ - TODO + $""" + # Assignment + You are an expert in professional translations from English (US) to {this.SystemPromptLanguage()}. + You translate the texts without adding any new information. When necessary, you correct + spelling and grammar. + + # Context + The texts to be translated come from the open source app "MindWork AI Studio". The goal + is to localize the app so that it can be offered in other languages. You will always + receive one text at a time. A text may be, for example, for a button, a label, or an + explanation within the app. The app "AI Studio" is a desktop app for macOS, Linux, + and Windows. Users can use Large Language Models (LLMs) in practical ways in their + daily lives with it. The app offers the regular chat mode for which LLMs have become + known. However, AI Studio also offers so-called assistants, where users no longer + have to prompt. + + # Target Audience + The app is intended for everyone, not just IT specialists or scientists. When translating, + make sure the texts are easy for everyone to understand. """; protected override bool AllowProfiles => false; protected override bool ShowResult => false; - protected override IReadOnlyList FooterButtons => []; + protected override bool ShowCopyResult => false; + + protected override bool ShowSendTo => false; + + protected override IReadOnlyList FooterButtons => + [ + new ButtonData + { + Text = "Copy Lua code to clipboard", + Icon = Icons.Material.Filled.Extension, + Color = Color.Default, + AsyncAction = async () => await this.RustService.CopyText2Clipboard(this.Snackbar, this.finalLuaCode.ToString()), + DisabledActionParam = () => this.finalLuaCode.Length == 0, + }, + ]; protected override string SubmitText => "Localize AI Studio & generate the Lua code"; - protected override Func SubmitAction => this.LocalizeTextConntent; + protected override Func SubmitAction => this.LocalizeTextContent; protected override bool SubmitDisabled => !this.localizationPossible; + protected override bool ShowDedicatedProgress => true; + protected override void ResetForm() { if (!this.MightPreselectValues()) @@ -68,6 +106,8 @@ public partial class AssistantI18N : AssistantBaseCore private ILanguagePlugin? selectedLanguagePlugin; private Dictionary addedContent = []; private Dictionary removedContent = []; + private Dictionary localizedContent = []; + private StringBuilder finalLuaCode = new(); #region Overrides of AssistantBase @@ -80,6 +120,12 @@ public partial class AssistantI18N : AssistantBaseCore #endregion + private string SystemPromptLanguage() => this.selectedTargetLanguage switch + { + CommonLanguages.OTHER => this.customTargetLanguage, + _ => $"{this.selectedTargetLanguage.Name()}", + }; + private async Task OnLanguagePluginChanged(Guid pluginId) { this.selectedLanguagePluginId = pluginId; @@ -89,9 +135,17 @@ public partial class AssistantI18N : AssistantBaseCore private async Task OnChangedLanguage() { if (PluginFactory.RunningPlugins.FirstOrDefault(n => n is PluginLanguage && n.Id == this.selectedLanguagePluginId) is not PluginLanguage comparisonPlugin) + { this.loadingIssue = $"Was not able to load the language plugin for comparison ({this.selectedLanguagePluginId}). Please select a valid, loaded & running language plugin."; + this.localizationPossible = false; + this.selectedLanguagePlugin = null; + } else if (comparisonPlugin.IETFTag != this.selectedTargetLanguage.ToIETFTag()) + { this.loadingIssue = $"The selected language plugin for comparison uses the IETF tag '{comparisonPlugin.IETFTag}' which does not match the selected target language '{this.selectedTargetLanguage.ToIETFTag()}'. Please select a valid, loaded & running language plugin which matches the target language."; + this.localizationPossible = false; + this.selectedLanguagePlugin = null; + } else { this.selectedLanguagePlugin = comparisonPlugin; @@ -195,7 +249,99 @@ public partial class AssistantI18N : AssistantBaseCore return null; } - private async Task LocalizeTextConntent() + private async Task LocalizeTextContent() { + await this.form!.Validate(); + if (!this.inputIsValid) + return; + + if(this.selectedLanguagePlugin is null) + return; + + if (this.selectedLanguagePlugin.IETFTag != this.selectedTargetLanguage.ToIETFTag()) + return; + + this.localizedContent.Clear(); + if (this.selectedTargetLanguage is not CommonLanguages.EN_US) + { + // Phase 1: Translate added content + await this.Phase1TranslateAddedContent(); + } + else + { + // Case: no translation needed + this.localizedContent = this.addedContent.ToDictionary(); + } + + if(this.cancellationTokenSource!.IsCancellationRequested) + return; + + // + // Now, we have localized the added content. Next, we must merge + // the localized content with the existing content. However, we + // must skip the removed content. We use the localizedContent + // dictionary for the final result: + // + foreach (var keyValuePair in this.selectedLanguagePlugin.Content) + { + if (this.cancellationTokenSource!.IsCancellationRequested) + break; + + if (this.localizedContent.ContainsKey(keyValuePair.Key)) + continue; + + if (this.removedContent.ContainsKey(keyValuePair.Key)) + continue; + + this.localizedContent.Add(keyValuePair.Key, keyValuePair.Value); + } + + if(this.cancellationTokenSource!.IsCancellationRequested) + return; + + // Phase 2: Create the Lua code + this.Phase2CreateLuaCode(); + } + + private async Task Phase1TranslateAddedContent() + { + var stopwatch = new Stopwatch(); + var minimumTime = TimeSpan.FromMilliseconds(500); + foreach (var keyValuePair in this.addedContent) + { + if(this.cancellationTokenSource!.IsCancellationRequested) + break; + + // + // We measure the time for each translation. + // We do not want to make more than 120 requests + // per minute, i.e., 2 requests per second. + // + stopwatch.Reset(); + stopwatch.Start(); + + // + // Translate one text at a time: + // + this.CreateChatThread(); + var time = this.AddUserRequest(keyValuePair.Value); + this.localizedContent.Add(keyValuePair.Key, await this.AddAIResponseAsync(time)); + + if (this.cancellationTokenSource!.IsCancellationRequested) + break; + + // + // Ensure that we do not exceed the rate limit of 2 requests per second: + // + stopwatch.Stop(); + if (stopwatch.Elapsed < minimumTime) + await Task.Delay(minimumTime - stopwatch.Elapsed); + } + } + + private void Phase2CreateLuaCode() + { + this.finalLuaCode.Clear(); + LuaTable.Create(ref this.finalLuaCode, "UI_TEXT_CONTENT", this.localizedContent, this.cancellationTokenSource!.Token); } } \ No newline at end of file