refactor: Session 1 - critical safety bug fixes

- Remove hardcoded Replicate API key -> appsettings.Development.json
- Replace static Dictionary with ConcurrentDictionary for thread safety
- Remove static myHome field; ProcessAudio2 and OpenEmailForm2 now
  use session-keyed _instances lookup for correct multi-user routing
- Register _instances cleanup in DisposeAsync
- CreateSiteWizard: Singleton -> Scoped, static _instance replaced
  with DotNetObjectReference for per-circuit JS interop
- Remove duplicate DbContext registration from Program.cs
- HandleBrandNameChanged: remove spurious async void
- UpdateTextContentForVoice: add try/catch to contain exceptions
- UI event handlers (UpdateContent/Finished/Status): early-return
  guards + try/catch with _logger instead of silent failures

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Adam 2026-03-07 00:44:56 +01:00
parent e0325bb4bb
commit f1aedcaa1e
8 changed files with 155 additions and 181 deletions

View File

@ -197,8 +197,8 @@
<script> <script>
function openContactForm(emailAddress) { function openContactForm(emailAddress) {
console.log(emailAddress); console.log(emailAddress);
if (emailAddress) { if (emailAddress && sessionId) {
DotNet.invokeMethodAsync('BLAIzor', 'OpenEmailForm2', emailAddress) DotNet.invokeMethodAsync('BLAIzor', 'OpenEmailForm2', emailAddress, sessionId)
} }
} }
</script> </script>
@ -337,11 +337,6 @@
StateHasChanged(); StateHasChanged();
} }
public Index()
{
myHome = this; // Set the static reference to the current instance
}
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
await _logger.InfoAsync("Index component initialized.", $"{SiteId}"); await _logger.InfoAsync("Index component initialized.", $"{SiteId}");
@ -359,42 +354,25 @@
private async void UpdateContent(string receivedSessionId, string content, MenuItem? menuItem) 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.Clear();
HtmlContent.Append(content); HtmlContent.Append(content);
//InvokeAsync(StateHasChanged); // Ensures UI updates dynamically await InvokeAsync(StateHasChanged);
await InvokeAsync(() =>
{
StateHasChanged();
});
//_scopedContentService.CurrentDOM = await jsRuntime.InvokeAsync<string>("getDivContent", "currentContent");
} }
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<string>("getDivContent", "currentContent");
// }
// }
private async void UpdateFinished(string receivedSessionId) 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<object>("getDivContent", "currentContent"); var result = await jsRuntime.InvokeAsync<object>("getDivContent", "currentContent");
//await ConvertTextToSpeech();
_scopedContentService.CurrentDOM = JsonSerializer.Serialize(result); _scopedContentService.CurrentDOM = JsonSerializer.Serialize(result);
Console.Write(_scopedContentService.CurrentDOM);
} }
catch (Exception ex) { await _logger.ErrorAsync("UpdateFinished failed", ex.Message); }
} }
private async Task ContentChangedInForm() private async Task ContentChangedInForm()
@ -406,17 +384,16 @@
private async void UpdateStatus(string receivedSessionId, string content) 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; 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) public async Task Enter(KeyboardEventArgs e)
{ {
if (e.Code == "Enter" || e.Code == "NumpadEnter") if (e.Code == "Enter" || e.Code == "NumpadEnter")
@ -451,4 +428,4 @@
} }
} }

View File

@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.JSInterop; using Microsoft.JSInterop;
using Radzen; using Radzen;
using System.Collections.Concurrent;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
@ -24,10 +25,9 @@ namespace BLAIzor.Components.Pages
[Inject] protected NotificationService NotificationService { get; set; } [Inject] protected NotificationService NotificationService { get; set; }
[Inject] protected CacheService CacheService { get; set; } [Inject] protected CacheService CacheService { get; set; }
public static readonly Dictionary<string, MainPageBase> _instances = new(); public static readonly ConcurrentDictionary<string, MainPageBase> _instances = new();
public string SessionId; public string SessionId;
public static MainPageBase myHome;
public int SiteId; public int SiteId;
public SiteInfo SiteInfo; public SiteInfo SiteInfo;
@ -65,29 +65,17 @@ namespace BLAIzor.Components.Pages
{ {
// Logic here // Logic here
} }
public async void HandleBrandNameChanged() public void HandleBrandNameChanged()
{ {
SelectedBrandName = _scopedContentService.SelectedBrandName; SelectedBrandName = _scopedContentService.SelectedBrandName;
//await InvokeAsync(() =>
// {
// StateHasChanged();
// });
try try
{ {
StateHasChanged(); StateHasChanged();
//await Task.Run(() =>
//{
// StateHasChanged();
//}).ConfigureAwait(false);
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.WriteLine(ex); _ = _logger.ErrorAsync("HandleBrandNameChanged failed", ex.Message);
} }
} }
public async Task<string> GetMenuList(int siteId) public async Task<string> 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; welcomeStage = false;
if (!string.IsNullOrEmpty(UserInput)) if (!string.IsNullOrEmpty(UserInput))
@ -260,13 +248,15 @@ namespace BLAIzor.Components.Pages
protected async void UpdateTextContentForVoice(string receivedSessionId, string content) protected async void UpdateTextContentForVoice(string receivedSessionId, string content)
{ {
Console.WriteLine("UPDATETEXTCONTENT called"); if (receivedSessionId != SessionId) return;
if (receivedSessionId == SessionId) // Only accept messages meant for this tab TextContent = content;
try
{ {
TextContent = content;
await ConvertTextToSpeech(content); await ConvertTextToSpeech(content);
//_scopedContentService.CurrentDOM = await jsRuntime.InvokeAsync<string>("getDivContent", "currentContent"); }
catch (Exception ex)
{
await _logger.ErrorAsync("UpdateTextContentForVoice failed", ex.Message);
} }
} }
@ -282,64 +272,47 @@ namespace BLAIzor.Components.Pages
} }
[JSInvokable("ProcessAudio2")] [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"); var languageCode = instance._scopedContentService.SelectedLanguage switch
if (myHome != null)
{ {
if (myHome.STTEnabled == false) return; "English" => "en-US",
Console.WriteLine("STT ENABLED -------------------------------------------------------------------------------"); "German" => "de-DE",
var languageCode = "hu-HU"; _ => "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<string>("CredentialsPath");
Console.Write(credentialsPath);
var builder = new SpeechClientBuilder
{
CredentialsPath = credentialsPath
};
var speech = builder.Build();
byte[] audioBytes = Convert.FromBase64String(base64Audio); var credentialsPath = instance.configuration.GetSection("GoogleAPI").GetValue<string>("CredentialsPath");
myHome.HtmlContent.Clear(); var speech = new SpeechClientBuilder { CredentialsPath = credentialsPath }.Build();
var response = await speech.RecognizeAsync(new RecognitionConfig
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, await instance.HandleVoiceCommand(alternative.Transcript, sessionId);
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);
}
} }
} }
} }
[JSInvokable("OpenEmailForm2")] [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() public async Task SendMessage()
@ -377,11 +350,8 @@ namespace BLAIzor.Components.Pages
public async ValueTask DisposeAsync() public async ValueTask DisposeAsync()
{ {
//await CssTemplateService.DeleteSessionCssFile(SessionId); _instances.TryRemove(SessionId, out _);
_scopedContentService.OnBrandNameChanged -= HandleBrandNameChanged; _scopedContentService.OnBrandNameChanged -= HandleBrandNameChanged;
//AIService.OnContentReceived -= UpdateContent;
//AIService.OnContentReceiveFinished -= UpdateFinished;
//AIService.OnStatusChangeReceived -= UpdateStatus;
ChatGptService.OnTextContentAvailable -= UpdateTextContentForVoice; ChatGptService.OnTextContentAvailable -= UpdateTextContentForVoice;
} }

View File

@ -373,10 +373,8 @@
<script> <script>
function openContactForm(emailAddress) { function openContactForm(emailAddress) {
console.log(emailAddress); console.log(emailAddress);
if (emailAddress && sessionId) {
if (emailAddress) { DotNet.invokeMethodAsync('BLAIzor', 'OpenEmailForm2', emailAddress, sessionId)
DotNet.invokeMethodAsync('BLAIzor', 'OpenEmailForm2', emailAddress)
} }
} }
</script> </script>
@ -481,12 +479,6 @@
StateHasChanged(); StateHasChanged();
} }
public Preview()
{
myHome = this; // Set the static reference to the current instance
}
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
SessionId = _scopedContentService.SessionId; SessionId = _scopedContentService.SessionId;
@ -557,33 +549,67 @@
} }
} }
// else
// {
// UpdateContent(SessionId, HtmlContent.ToString(), currentMenuItem);
// }
UserInput = string.Empty; UserInput = string.Empty;
_initVoicePending = true; _initVoicePending = true;
} }
private async void UpdateContent(string receivedSessionId, string content, MenuItem menuItem) 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.Clear();
HtmlContent.Append(content); HtmlContent.Append(content);
//TODO SAVE TO DB await InvokeAsync(StateHasChanged);
if (menuItem != null) }
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; currentMenuItem = menuItem;
// IsContentSaved = string.IsNullOrEmpty(currentMenuItem.StoredHtml) ? false : true; if (!string.IsNullOrEmpty(menuItem.StoredHtml))
// _logger.InfoAsync($"Preview - UpdateContent: {IsContentSaved}"); {
displayOptions = true;
StateHasChanged(); 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 UserInput = string.Empty;
await InvokeAsync(() =>
{
StateHasChanged();
});
var result = await jsRuntime.InvokeAsync<object>("getDivContent", "currentContent");
_scopedContentService.CurrentDOM = JsonSerializer.Serialize(result);
//_scopedContentService.CurrentDOM = await jsRuntime.InvokeAsync<string>("getDivContent", "currentContent");
} }
} }
@ -600,13 +626,13 @@
private async void UpdateFinished(string receivedSessionId) 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<object>("getDivContent", "currentContent"); var result = await jsRuntime.InvokeAsync<object>("getDivContent", "currentContent");
_scopedContentService.CurrentDOM = JsonSerializer.Serialize(result); _scopedContentService.CurrentDOM = JsonSerializer.Serialize(result);
Console.Write(_scopedContentService.CurrentDOM);
} }
catch (Exception ex) { await _logger.ErrorAsync("UpdateFinished failed", ex.Message); }
} }
private async Task ContentChangedInForm() private async Task ContentChangedInForm()
@ -618,17 +644,16 @@
private async void UpdateStatus(string receivedSessionId, string content) 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; 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) public async Task Enter(KeyboardEventArgs e)
{ {
if (e.Code == "Enter" || e.Code == "NumpadEnter") if (e.Code == "Enter" || e.Code == "NumpadEnter")
@ -1106,4 +1131,4 @@
} }
} }

View File

@ -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() private void NextStep()
@ -483,19 +487,22 @@
// STT Hook // STT Hook
private static CreateSiteWizard? _instance; private DotNetObjectReference<CreateSiteWizard>? _dotNetRef;
[JSInvokable] [JSInvokable]
public static async Task SendAudioToServer(List<byte> audioData) public async Task SendAudioToServer(List<byte> audioData)
{ {
if (_instance is null) return; var result = await WhisperService.TranscribeAsync(audioData.ToArray());
var result = await _instance.WhisperService.TranscribeAsync(audioData.ToArray());
if (!string.IsNullOrWhiteSpace(result)) if (!string.IsNullOrWhiteSpace(result))
{ {
_instance.Steps[_instance.CurrentStep].Answer = result; Steps[CurrentStep].Answer = result;
_instance.StateHasChanged(); StateHasChanged();
} }
} }
public void Dispose()
{
_dotNetRef?.Dispose();
}
} }

View File

@ -21,30 +21,26 @@ var builder = WebApplication.CreateBuilder(args);
var configuration = builder.Configuration; var configuration = builder.Configuration;
builder.WebHost.UseWebRoot("wwwroot"); 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<ApplicationDbContext>(options => builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); options.UseSqlServer(connectionString));
// Add DbContextFactory (for background services that need their own scope)
builder.Services.AddDbContextFactory<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString), ServiceLifetime.Scoped);
// Add Identity services // Add Identity services
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true) builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>(); .AddEntityFrameworkStores<ApplicationDbContext>();
//builder.Services.AddBlazorServerOptions(options =>
//{
// options.MaxBufferSize = 50 * 1024 * 1024; // Set to 50 MB
//});
builder.Services.Configure<KestrelServerOptions>(options => builder.Services.Configure<KestrelServerOptions>(options =>
{ {
options.Limits.MaxRequestBodySize = 50 * 1024 * 1024; options.Limits.MaxRequestBodySize = 50 * 1024 * 1024;
}); });
var env = builder.Environment; // This is IWebHostEnvironment var env = builder.Environment;
if (env.IsProduction())
{
// do production-specific setup
}
// Add services to the container. // Add services to the container.
builder.Services.AddRazorComponents() builder.Services.AddRazorComponents()
@ -52,16 +48,6 @@ builder.Services.AddRazorComponents()
builder.Services.AddRadzenComponents(); builder.Services.AddRadzenComponents();
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
// Required for scoped injection (used in pages/services)
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
// Required for factory-based logging or background usage
builder.Services.AddDbContextFactory<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString), ServiceLifetime.Scoped);
builder.Services.AddHttpClient(); builder.Services.AddHttpClient();
builder.Services.AddScoped<ISimpleLogger, SimpleLogger>(); builder.Services.AddScoped<ISimpleLogger, SimpleLogger>();
builder.Services.AddScoped<AIService>(); builder.Services.AddScoped<AIService>();
@ -91,14 +77,14 @@ builder.Services.AddScoped<CssInjectorService>();
builder.Services.AddScoped<LocalVectorSearchService>(); builder.Services.AddScoped<LocalVectorSearchService>();
builder.Services.AddScoped<WebsiteContentLoaderService>(); builder.Services.AddScoped<WebsiteContentLoaderService>();
builder.Services.AddScoped<CacheService>(); builder.Services.AddScoped<CacheService>();
builder.Services.AddSingleton<CreateSiteWizard>(); builder.Services.AddScoped<CreateSiteWizard>();
builder.Services.AddScoped<WhisperTranscriptionService>(); builder.Services.AddScoped<WhisperTranscriptionService>();
builder.Services.AddScoped<IBrightDataService, BrightDataService>(); builder.Services.AddScoped<IBrightDataService, BrightDataService>();
builder.Services.AddHttpClient<ReplicateService>(client => builder.Services.AddHttpClient<ReplicateService>(client =>
{ {
client.DefaultRequestHeaders.Authorization = client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", "r8_MUApXYIE5mRjxqy20tsGLehWBJkCzNj0Cwvrh"); new AuthenticationHeaderValue("Bearer", configuration["Replicate:ApiKey"]);
}); });
builder.Services.AddHostedService<TempFileCleanupService>(); builder.Services.AddHostedService<TempFileCleanupService>();

View File

@ -33,7 +33,7 @@ namespace BLAIzor.Services
} }
} }
public event Action OnBrandNameChanged; public event Action? OnBrandNameChanged;
public int SelectedSiteId { get; set; } = 1; public int SelectedSiteId { get; set; } = 1;
public WebsiteContentModel WebsiteContentModel { get; set; } public WebsiteContentModel WebsiteContentModel { get; set; }

View File

@ -5,5 +5,8 @@
"Microsoft.AspNetCore": "Information", "Microsoft.AspNetCore": "Information",
"BLAIzor": "Information" "BLAIzor": "Information"
} }
},
"Replicate": {
"ApiKey": "r8_MUApXYIE5mRjxqy20tsGLehWBJkCzNj0Cwvrh"
} }
} }

View File

@ -1,5 +1,10 @@
let mediaRecorder; let mediaRecorder;
let recordedChunks = []; let recordedChunks = [];
let wizardDotNetRef = null;
window.initWizardAudio = (dotnetRef) => {
wizardDotNetRef = dotnetRef;
};
window.startRecording = async () => { window.startRecording = async () => {
recordedChunks = []; recordedChunks = [];
@ -18,8 +23,9 @@ window.startRecording = async () => {
const arrayBuffer = await blob.arrayBuffer(); const arrayBuffer = await blob.arrayBuffer();
const byteArray = new Uint8Array(arrayBuffer); const byteArray = new Uint8Array(arrayBuffer);
// Send to Blazor server if (wizardDotNetRef) {
DotNet.invokeMethodAsync('BLAIzor', 'SendAudioToServer', Array.from(byteArray)); wizardDotNetRef.invokeMethodAsync('SendAudioToServer', Array.from(byteArray));
}
}; };
mediaRecorder.start(); mediaRecorder.start();