diff --git a/Components/Pages/Index.razor b/Components/Pages/Index.razor index 81697fd..f71148b 100644 --- a/Components/Pages/Index.razor +++ b/Components/Pages/Index.razor @@ -197,8 +197,8 @@ @@ -337,11 +337,6 @@ StateHasChanged(); } - public Index() - { - myHome = this; // Set the static reference to the current instance - } - protected override async Task OnInitializedAsync() { await _logger.InfoAsync("Index component initialized.", $"{SiteId}"); @@ -359,42 +354,25 @@ private async void UpdateContent(string receivedSessionId, string content, MenuItem? menuItem) { - if (receivedSessionId == SessionId) // Only accept messages meant for this tab + if (receivedSessionId != SessionId) return; + try { - HtmlContent.Clear(); HtmlContent.Append(content); - //InvokeAsync(StateHasChanged); // Ensures UI updates dynamically - await InvokeAsync(() => - { - StateHasChanged(); - }); - //_scopedContentService.CurrentDOM = await jsRuntime.InvokeAsync("getDivContent", "currentContent"); + await InvokeAsync(StateHasChanged); } + catch (Exception ex) { await _logger.ErrorAsync("UpdateContent failed", ex.Message); } } - // private async void UpdateTextContentForVoice(string receivedSessionId, string content) - // { - // Console.WriteLine("UPDATETEXTCONTENT called"); - // if (receivedSessionId == SessionId) // Only accept messages meant for this tab - // { - - // TextContent = content; - // await ConvertTextToSpeech(content); - // //_scopedContentService.CurrentDOM = await jsRuntime.InvokeAsync("getDivContent", "currentContent"); - // } - // } - private async void UpdateFinished(string receivedSessionId) { - if (receivedSessionId == SessionId) // Only accept messages meant for this tab + if (receivedSessionId != SessionId) return; + try { - Console.WriteLine("Content update finished"); var result = await jsRuntime.InvokeAsync("getDivContent", "currentContent"); - //await ConvertTextToSpeech(); _scopedContentService.CurrentDOM = JsonSerializer.Serialize(result); - Console.Write(_scopedContentService.CurrentDOM); } + catch (Exception ex) { await _logger.ErrorAsync("UpdateFinished failed", ex.Message); } } private async Task ContentChangedInForm() @@ -406,17 +384,16 @@ private async void UpdateStatus(string receivedSessionId, string content) { - if (receivedSessionId == SessionId) // Only accept messages meant for this tab + if (receivedSessionId != SessionId) return; + try { StatusContent = content; - //InvokeAsync(StateHasChanged); // Ensures UI updates dynamically - await InvokeAsync(() => - { - StateHasChanged(); - }); + await InvokeAsync(StateHasChanged); } + catch (Exception ex) { await _logger.ErrorAsync("UpdateStatus failed", ex.Message); } } + public async Task Enter(KeyboardEventArgs e) { if (e.Code == "Enter" || e.Code == "NumpadEnter") @@ -451,4 +428,4 @@ } -} +} \ No newline at end of file diff --git a/Components/Pages/MainPageBase.cs b/Components/Pages/MainPageBase.cs index ff1fc24..fa4c090 100644 --- a/Components/Pages/MainPageBase.cs +++ b/Components/Pages/MainPageBase.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Mvc; using Microsoft.JSInterop; using Radzen; +using System.Collections.Concurrent; using System.Text; using System.Text.Json; @@ -24,10 +25,9 @@ namespace BLAIzor.Components.Pages [Inject] protected NotificationService NotificationService { get; set; } [Inject] protected CacheService CacheService { get; set; } - public static readonly Dictionary _instances = new(); + public static readonly ConcurrentDictionary _instances = new(); public string SessionId; - public static MainPageBase myHome; public int SiteId; public SiteInfo SiteInfo; @@ -65,29 +65,17 @@ namespace BLAIzor.Components.Pages { // Logic here } - public async void HandleBrandNameChanged() + public void HandleBrandNameChanged() { SelectedBrandName = _scopedContentService.SelectedBrandName; - //await InvokeAsync(() => - // { - // StateHasChanged(); - // }); - - try { StateHasChanged(); - - //await Task.Run(() => - //{ - // StateHasChanged(); - //}).ConfigureAwait(false); } catch (Exception ex) { - Console.WriteLine(ex); + _ = _logger.ErrorAsync("HandleBrandNameChanged failed", ex.Message); } - } public async Task GetMenuList(int siteId) @@ -216,7 +204,7 @@ namespace BLAIzor.Components.Pages } } - public async Task DisplayMenuContent(string input, bool forceUnmodified) + public virtual async Task DisplayMenuContent(string input, bool forceUnmodified) { welcomeStage = false; if (!string.IsNullOrEmpty(UserInput)) @@ -260,13 +248,15 @@ namespace BLAIzor.Components.Pages protected async void UpdateTextContentForVoice(string receivedSessionId, string content) { - Console.WriteLine("UPDATETEXTCONTENT called"); - if (receivedSessionId == SessionId) // Only accept messages meant for this tab + if (receivedSessionId != SessionId) return; + TextContent = content; + try { - - TextContent = content; await ConvertTextToSpeech(content); - //_scopedContentService.CurrentDOM = await jsRuntime.InvokeAsync("getDivContent", "currentContent"); + } + catch (Exception ex) + { + await _logger.ErrorAsync("UpdateTextContentForVoice failed", ex.Message); } } @@ -282,64 +272,47 @@ namespace BLAIzor.Components.Pages } [JSInvokable("ProcessAudio2")] - public static async Task ProcessAudio2(string base64Audio, string SessionId) + public static async Task ProcessAudio2(string base64Audio, string sessionId) { + if (!_instances.TryGetValue(sessionId, out var instance)) return; + if (!instance.STTEnabled) return; - Console.Write("audio incoming"); - if (myHome != null) + var languageCode = instance._scopedContentService.SelectedLanguage switch { - if (myHome.STTEnabled == false) return; - Console.WriteLine("STT ENABLED -------------------------------------------------------------------------------"); - var languageCode = "hu-HU"; - if (myHome._scopedContentService.SelectedLanguage == "Hungarian") - { - languageCode = "hu-HU"; - } - else if (myHome._scopedContentService.SelectedLanguage == "English") - { - languageCode = "en-US"; - } - else if (myHome._scopedContentService.SelectedLanguage == "German") - { - languageCode = "de-DE"; - } - var credentialsPath = myHome.configuration.GetSection("GoogleAPI").GetValue("CredentialsPath"); - Console.Write(credentialsPath); - var builder = new SpeechClientBuilder - { - CredentialsPath = credentialsPath - }; - var speech = builder.Build(); + "English" => "en-US", + "German" => "de-DE", + _ => "hu-HU" + }; - byte[] audioBytes = Convert.FromBase64String(base64Audio); - myHome.HtmlContent.Clear(); - var response = await speech.RecognizeAsync(new RecognitionConfig + var credentialsPath = instance.configuration.GetSection("GoogleAPI").GetValue("CredentialsPath"); + var speech = new SpeechClientBuilder { CredentialsPath = credentialsPath }.Build(); + + byte[] audioBytes = Convert.FromBase64String(base64Audio); + instance.HtmlContent.Clear(); + + var response = await speech.RecognizeAsync(new RecognitionConfig + { + Encoding = RecognitionConfig.Types.AudioEncoding.Mp3, + SampleRateHertz = 48000, + LanguageCode = languageCode + }, RecognitionAudio.FromBytes(audioBytes)); + + foreach (var result in response.Results) + { + foreach (var alternative in result.Alternatives) { - Encoding = RecognitionConfig.Types.AudioEncoding.Mp3, - SampleRateHertz = 48000, // Match the actual sample rate - LanguageCode = languageCode - }, RecognitionAudio.FromBytes(audioBytes)); - Console.Write("BILLED: " + response.TotalBilledTime); - foreach (var result in response.Results) - { - //Console.Write("RESULT: " + result.Alternatives.Count); - foreach (var alternative in result.Alternatives) - { - //Console.WriteLine($"Transcription: {alternative.Transcript}"); - await myHome.HandleVoiceCommand(alternative.Transcript, SessionId); - } + await instance.HandleVoiceCommand(alternative.Transcript, sessionId); } } } [JSInvokable("OpenEmailForm2")] - public static async void OpenEmailForm2(string emailAddress) + public static async Task OpenEmailForm2(string emailAddress, string sessionId) { - if (myHome != null) + if (_instances.TryGetValue(sessionId, out var instance)) { - await myHome.DisplayEmailForm(emailAddress); + await instance.DisplayEmailForm(emailAddress); } - Console.Write("openEmail with: " + emailAddress); } public async Task SendMessage() @@ -377,11 +350,8 @@ namespace BLAIzor.Components.Pages public async ValueTask DisposeAsync() { - //await CssTemplateService.DeleteSessionCssFile(SessionId); + _instances.TryRemove(SessionId, out _); _scopedContentService.OnBrandNameChanged -= HandleBrandNameChanged; - //AIService.OnContentReceived -= UpdateContent; - //AIService.OnContentReceiveFinished -= UpdateFinished; - //AIService.OnStatusChangeReceived -= UpdateStatus; ChatGptService.OnTextContentAvailable -= UpdateTextContentForVoice; } diff --git a/Components/Pages/Preview.razor b/Components/Pages/Preview.razor index cfa7a77..f0b37eb 100644 --- a/Components/Pages/Preview.razor +++ b/Components/Pages/Preview.razor @@ -373,10 +373,8 @@ @@ -481,12 +479,6 @@ StateHasChanged(); } - public Preview() - { - myHome = this; // Set the static reference to the current instance - } - - protected override async Task OnParametersSetAsync() { SessionId = _scopedContentService.SessionId; @@ -557,33 +549,67 @@ } } + // else + // { + // UpdateContent(SessionId, HtmlContent.ToString(), currentMenuItem); + // } UserInput = string.Empty; _initVoicePending = true; } private async void UpdateContent(string receivedSessionId, string content, MenuItem menuItem) { - if (receivedSessionId == SessionId) // Only accept messages meant for this tab + if (receivedSessionId != SessionId) return; + try { HtmlContent.Clear(); HtmlContent.Append(content); - //TODO SAVE TO DB - if (menuItem != null) + await InvokeAsync(StateHasChanged); + } + catch (Exception ex) { await _logger.ErrorAsync("UpdateContent failed", ex.Message); } + } + + public async override Task DisplayMenuContent(string input, bool forceUnmodified) + { + welcomeStage = false; + if (!string.IsNullOrEmpty(UserInput)) + { + HtmlContent.Clear(); + var menu = await GetMenuList(SiteId); + var menuList = await GetMenuItems(SiteId); + + var menuItem = CompareMenuItemNames(input, menuList); + if (menuItem == null) + { + await ChatGptService.ProcessContentRequest(SessionId, input, SiteId, (int)SiteInfo.TemplateId!, ContentCollectionName, menu, forceUnmodified); + } + else { currentMenuItem = menuItem; - // IsContentSaved = string.IsNullOrEmpty(currentMenuItem.StoredHtml) ? false : true; - // _logger.InfoAsync($"Preview - UpdateContent: {IsContentSaved}"); - displayOptions = true; - StateHasChanged(); + if (!string.IsNullOrEmpty(menuItem.StoredHtml)) + { + + HtmlContent.Clear(); + HtmlContent.Append(menuItem.StoredHtml); + if (currentMenuItem.ContentItemId != null) + { + var content = await _contentEditorService.GetContentItemByIdAsync((int)currentMenuItem.ContentItemId); + if (content != null) + { + string removedNumbers = TextHelper.ReplaceNumbersAndSpecialCharacters(content.Content, _scopedContentService.SelectedLanguage); + Console.WriteLine(removedNumbers); + UpdateTextContentForVoice(SessionId, removedNumbers); + } + } + displayOptions = true; + IsContentSaved = true; + } + else + { + await ChatGptService.ProcessContentRequest(SessionId, menuItem, SiteId, (int)SiteInfo.TemplateId!, ContentCollectionName, menu, forceUnmodified); + } } - //InvokeAsync(StateHasChanged); // Ensures UI updates dynamically - await InvokeAsync(() => - { - StateHasChanged(); - }); - var result = await jsRuntime.InvokeAsync("getDivContent", "currentContent"); - _scopedContentService.CurrentDOM = JsonSerializer.Serialize(result); - //_scopedContentService.CurrentDOM = await jsRuntime.InvokeAsync("getDivContent", "currentContent"); + UserInput = string.Empty; } } @@ -600,13 +626,13 @@ private async void UpdateFinished(string receivedSessionId) { - if (receivedSessionId == SessionId) // Only accept messages meant for this tab + if (receivedSessionId != SessionId) return; + try { - Console.WriteLine("Content update finished"); var result = await jsRuntime.InvokeAsync("getDivContent", "currentContent"); _scopedContentService.CurrentDOM = JsonSerializer.Serialize(result); - Console.Write(_scopedContentService.CurrentDOM); } + catch (Exception ex) { await _logger.ErrorAsync("UpdateFinished failed", ex.Message); } } private async Task ContentChangedInForm() @@ -618,17 +644,16 @@ private async void UpdateStatus(string receivedSessionId, string content) { - if (receivedSessionId == SessionId) // Only accept messages meant for this tab + if (receivedSessionId != SessionId) return; + try { StatusContent = content; - //InvokeAsync(StateHasChanged); // Ensures UI updates dynamically - await InvokeAsync(() => - { - StateHasChanged(); - }); + await InvokeAsync(StateHasChanged); } + catch (Exception ex) { await _logger.ErrorAsync("UpdateStatus failed", ex.Message); } } + public async Task Enter(KeyboardEventArgs e) { if (e.Code == "Enter" || e.Code == "NumpadEnter") @@ -1106,4 +1131,4 @@ } -} +} \ No newline at end of file diff --git a/Components/Partials/CreateSiteWizard.razor b/Components/Partials/CreateSiteWizard.razor index 6580ded..054ac6c 100644 --- a/Components/Partials/CreateSiteWizard.razor +++ b/Components/Partials/CreateSiteWizard.razor @@ -237,9 +237,13 @@ } - protected override void OnInitialized() + protected override async Task OnAfterRenderAsync(bool firstRender) { - _instance = this; + if (firstRender) + { + _dotNetRef = DotNetObjectReference.Create(this); + await JS.InvokeVoidAsync("initWizardAudio", _dotNetRef); + } } private void NextStep() @@ -483,19 +487,22 @@ // STT Hook - private static CreateSiteWizard? _instance; + private DotNetObjectReference? _dotNetRef; [JSInvokable] - public static async Task SendAudioToServer(List audioData) + public async Task SendAudioToServer(List audioData) { - if (_instance is null) return; - - var result = await _instance.WhisperService.TranscribeAsync(audioData.ToArray()); + var result = await WhisperService.TranscribeAsync(audioData.ToArray()); if (!string.IsNullOrWhiteSpace(result)) { - _instance.Steps[_instance.CurrentStep].Answer = result; - _instance.StateHasChanged(); + Steps[CurrentStep].Answer = result; + StateHasChanged(); } } + + public void Dispose() + { + _dotNetRef?.Dispose(); + } } diff --git a/Program.cs b/Program.cs index 4801396..bd68bac 100644 --- a/Program.cs +++ b/Program.cs @@ -21,30 +21,26 @@ var builder = WebApplication.CreateBuilder(args); var configuration = builder.Configuration; builder.WebHost.UseWebRoot("wwwroot"); -// Add services to the container. +var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); + +// Add DbContext (scoped, for pages and services) builder.Services.AddDbContext(options => - options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); + options.UseSqlServer(connectionString)); + +// Add DbContextFactory (for background services that need their own scope) +builder.Services.AddDbContextFactory(options => + options.UseSqlServer(connectionString), ServiceLifetime.Scoped); // Add Identity services builder.Services.AddDefaultIdentity(options => options.SignIn.RequireConfirmedAccount = true) .AddEntityFrameworkStores(); -//builder.Services.AddBlazorServerOptions(options => -//{ -// options.MaxBufferSize = 50 * 1024 * 1024; // Set to 50 MB -//}); - builder.Services.Configure(options => { options.Limits.MaxRequestBodySize = 50 * 1024 * 1024; }); -var env = builder.Environment; // This is IWebHostEnvironment - -if (env.IsProduction()) -{ - // do production-specific setup -} +var env = builder.Environment; // Add services to the container. builder.Services.AddRazorComponents() @@ -52,16 +48,6 @@ builder.Services.AddRazorComponents() builder.Services.AddRadzenComponents(); -var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); - -// Required for scoped injection (used in pages/services) -builder.Services.AddDbContext(options => - options.UseSqlServer(connectionString)); - -// Required for factory-based logging or background usage -builder.Services.AddDbContextFactory(options => - options.UseSqlServer(connectionString), ServiceLifetime.Scoped); - builder.Services.AddHttpClient(); builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -91,14 +77,14 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddSingleton(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddHttpClient(client => { client.DefaultRequestHeaders.Authorization = - new AuthenticationHeaderValue("Bearer", "r8_MUApXYIE5mRjxqy20tsGLehWBJkCzNj0Cwvrh"); + new AuthenticationHeaderValue("Bearer", configuration["Replicate:ApiKey"]); }); builder.Services.AddHostedService(); diff --git a/Services/ScopedContentService.cs b/Services/ScopedContentService.cs index 5d45544..6cf417a 100644 --- a/Services/ScopedContentService.cs +++ b/Services/ScopedContentService.cs @@ -33,7 +33,7 @@ namespace BLAIzor.Services } } - public event Action OnBrandNameChanged; + public event Action? OnBrandNameChanged; public int SelectedSiteId { get; set; } = 1; public WebsiteContentModel WebsiteContentModel { get; set; } diff --git a/appsettings.Development.json b/appsettings.Development.json index 33e2770..fd66bca 100644 --- a/appsettings.Development.json +++ b/appsettings.Development.json @@ -5,5 +5,8 @@ "Microsoft.AspNetCore": "Information", "BLAIzor": "Information" } + }, + "Replicate": { + "ApiKey": "r8_MUApXYIE5mRjxqy20tsGLehWBJkCzNj0Cwvrh" } } diff --git a/wwwroot/scripts/whisperRecorder.js b/wwwroot/scripts/whisperRecorder.js index 236f98b..3cad6ce 100644 --- a/wwwroot/scripts/whisperRecorder.js +++ b/wwwroot/scripts/whisperRecorder.js @@ -1,5 +1,10 @@ let mediaRecorder; let recordedChunks = []; +let wizardDotNetRef = null; + +window.initWizardAudio = (dotnetRef) => { + wizardDotNetRef = dotnetRef; +}; window.startRecording = async () => { recordedChunks = []; @@ -18,8 +23,9 @@ window.startRecording = async () => { const arrayBuffer = await blob.arrayBuffer(); const byteArray = new Uint8Array(arrayBuffer); - // Send to Blazor server - DotNet.invokeMethodAsync('BLAIzor', 'SendAudioToServer', Array.from(byteArray)); + if (wizardDotNetRef) { + wizardDotNetRef.invokeMethodAsync('SendAudioToServer', Array.from(byteArray)); + } }; mediaRecorder.start();