@page "/" @implements IDisposable @using System.Threading @using System.Net.Http.Json @inject HttpClient Http @inject LocalizationService Loc @Loc["app.title"]

@Loc["app.title"]

@Loc["section.visualization"]
@Loc["status.currentState"]: @Machine.CurrentState @Loc["status.steps"]: @Steps @Loc["status.status"]: @(Machine.IsHalted ? Loc["status.halted"] : Loc["status.ready"])
@Loc["section.config"]

@Loc["label.tapeValue"] @Loc["label.headIndex"]
@Loc["hint.tape"]

@Loc["hint.format"] @Loc["hint.csvFormat"]

@if (!string.IsNullOrEmpty(RuleParseError)) {
@RuleParseError
}
@code { private Machine Machine { get; set; } = new(); private bool IsRunning { get; set; } private int Speed { get; set; } = 500; private int Steps { get; set; } private CancellationTokenSource? _cts; // UI Bindings private string TapeInput { get; set; } = ""; private int HeadInput { get; set; } = 0; // CSV Rules private string CsvRulesInput { get; set; } = ""; private string RuleParseError { get; set; } = ""; // Add inert attribute to block all interaction while running private Dictionary InertWhenRunning => IsRunning ? new Dictionary { ["inert"] = "" } : new(); // Example list loaded from server private List Examples { get; set; } = new(); private int SelectedExampleIndex { get; set; } = 0; protected override async Task OnInitializedAsync() { Loc.OnLanguageChanged += OnLanguageChanged; // Load example list from server try { var examples = await Http.GetFromJsonAsync>("api/examples"); if (examples != null && examples.Count > 0) { Examples = examples; // Apply the first example as default ApplyExample(Examples[0]); } } catch { // Start with empty state if example loading fails } } /// /// Called when an example is selected from the ComboBox. /// private void OnExampleChanged(ChangeEventArgs e) { if (IsRunning) return; if (int.TryParse(e.Value?.ToString(), out int index)) { SelectedExampleIndex = index; if (index >= 0 && index < Examples.Count) { ApplyExample(Examples[index]); } else { // Clear all values when "None" is selected ClearConfiguration(); } } } /// /// Resets all control values in the Configuration section. /// private void ClearConfiguration() { _cts?.Cancel(); IsRunning = false; TapeInput = ""; HeadInput = 0; CsvRulesInput = ""; RuleParseError = ""; Machine = new Machine(); Steps = 0; StateHasChanged(); } /// /// Applies an example configuration to the UI. /// private void ApplyExample(ExampleConfig example) { _cts?.Cancel(); IsRunning = false; TapeInput = example.TapeInput; HeadInput = example.HeadPosition; CsvRulesInput = BuildCsvFromActions(example.Actions); LoadRules(); SetTape(); Steps = 0; StateHasChanged(); } /// /// Returns the value matching the current language from a localized text list. /// private string GetLocalizedText(List texts) { return texts.FirstOrDefault(t => t.Language == Loc.CurrentLanguage)?.Value ?? texts.FirstOrDefault()?.Value ?? ""; } /// /// Converts the Actions array to a CSV string with comments in the current language. /// private string BuildCsvFromActions(List actions) { var lines = new List(); foreach (var action in actions) { var comment = GetLocalizedText(action.Comment); lines.Add(string.IsNullOrEmpty(comment) ? action.Value : $"{action.Value}, {comment}"); } return string.Join("\n", lines); } private void LoadRules() { RuleParseError = ""; var newTransitions = new List<(string, char, string, char, MoveDirection, string)>(); var seenKeys = new Dictionary<(string, char), int>(); var lines = CsvRulesInput.Split('\n', StringSplitOptions.RemoveEmptyEntries); int lineNum = 0; foreach (var line in lines) { lineNum++; if (string.IsNullOrWhiteSpace(line)) continue; var parts = line.Split(',', 6); if (parts.Length < 5 || parts.Length > 6) { RuleParseError = Loc["error.columnCount", lineNum, parts.Length]; return; } var state = parts[0].Trim(); var readStr = parts[1].Trim(); var newState = parts[2].Trim(); var writeStr = parts[3].Trim(); var moveStr = parts[4].Trim(); if (readStr.Length != 1) { RuleParseError = Loc["error.readSymbol", lineNum, readStr]; return; } if (writeStr.Length != 1) { RuleParseError = Loc["error.writeSymbol", lineNum, writeStr]; return; } MoveDirection move; if (moveStr.Equals("L", StringComparison.OrdinalIgnoreCase) || moveStr.Equals("Left", StringComparison.OrdinalIgnoreCase)) move = MoveDirection.Left; else if (moveStr.Equals("R", StringComparison.OrdinalIgnoreCase) || moveStr.Equals("Right", StringComparison.OrdinalIgnoreCase)) move = MoveDirection.Right; else if (moveStr.Equals("S", StringComparison.OrdinalIgnoreCase) || moveStr.Equals("Stay", StringComparison.OrdinalIgnoreCase)) move = MoveDirection.Stay; else { RuleParseError = Loc["error.moveDirection", lineNum, moveStr]; return; } // 6th column is an optional comment string comment = parts.Length > 5 ? parts[5].Trim() : ""; var key = (state, readStr[0]); if (seenKeys.TryGetValue(key, out int prevLine)) { RuleParseError = Loc["error.duplicateKey", lineNum, state, readStr, prevLine]; return; } seenKeys[key] = lineNum; newTransitions.Add((state, readStr[0], newState, writeStr[0], move, comment)); } if (newTransitions.Count == 0) { RuleParseError = Loc["error.noRules"]; return; } // Assume initial state is the first state mentioned in the rules string initialState = newTransitions.First().Item1; Machine.LoadProgram(initialState, newTransitions); StateHasChanged(); } private void SetTape() { _cts?.Cancel(); IsRunning = false; // Also apply CSV rules LoadRules(); Machine.Reset(); // Apply initial tape values if (!string.IsNullOrEmpty(TapeInput)) { for (int i = 0; i < TapeInput.Length; i++) { Machine.Tape[i] = TapeInput[i]; } } Machine.ForceHeadPosition(HeadInput); Steps = 0; StateHasChanged(); } private void Step() { if (Machine.IsHalted) return; Machine.Step(); Steps++; StateHasChanged(); } private async Task ToggleRun(bool shouldRun) { IsRunning = shouldRun; if (IsRunning) { _cts = new CancellationTokenSource(); try { await RunLoop(_cts.Token); } catch (OperationCanceledException) { // Stopped } finally { IsRunning = false; _cts = null; StateHasChanged(); } } else { _cts?.Cancel(); } } private async Task RunLoop(CancellationToken token) { while (!token.IsCancellationRequested && !Machine.IsHalted) { Step(); await Task.Delay(Speed, token); } } private void Reset() { // Re-apply the current inputs to simulate a "Reset to Initial" SetTape(); } private void OnLanguageChanged() { // Rebuild comments in the new language if an example is selected if (SelectedExampleIndex >= 0 && SelectedExampleIndex < Examples.Count) { CsvRulesInput = BuildCsvFromActions(Examples[SelectedExampleIndex].Actions); } InvokeAsync(StateHasChanged); } public void Dispose() { Loc.OnLanguageChanged -= OnLanguageChanged; } }