turing-machine/Implementation/Client/Pages/Home.razor

393 lines
12 KiB
Plaintext

@page "/"
@implements IDisposable
@using System.Threading
@using System.Net.Http.Json
@inject HttpClient Http
@inject LocalizationService Loc
<PageTitle>@Loc["app.title"]</PageTitle>
<div class="container">
<h1 class="mb-4">@Loc["app.title"]</h1>
<!-- READ-ONLY SECTION (Top) -->
<div class="card mb-4">
<div class="card-header bg-light">
<strong>@Loc["section.visualization"]</strong>
</div>
<div class="card-body">
<div class="status-panel mb-3">
<span class="badge bg-info text-dark me-2">@Loc["status.currentState"]: @Machine.CurrentState</span>
<span class="badge bg-secondary me-2">@Loc["status.steps"]: @Steps</span>
<span class="badge @(Machine.IsHalted ? "bg-danger" : "bg-success")">@Loc["status.status"]: @(Machine.IsHalted ? Loc["status.halted"] : Loc["status.ready"])</span>
</div>
<TapeVisualizer Tape="@Machine.Tape" HeadPosition="@Machine.HeadPosition" />
<Controls
OnStep="Step"
OnRunToggle="ToggleRun"
OnReset="Reset"
IsRunning="@IsRunning"
@bind-Speed="Speed" />
<TransitionTableVisualizer
Transitions="@Machine.Transitions"
CurrentState="@Machine.CurrentState"
CurrentReadSymbol="@Machine.Tape[Machine.HeadPosition]" />
<div class="mt-2">
<button class="btn btn-primary" @onclick="SetTape" disabled="@IsRunning">@Loc["button.loadConfig"]</button>
</div>
</div>
</div>
<!-- EDITABLE SECTION (Bottom) -->
<div class="card">
<div class="card-header bg-warning text-dark">
<strong>@Loc["section.config"]</strong>
</div>
<div class="card-body">
<!-- Examples -->
<div class="mb-3">
<label class="form-label fw-bold">@Loc["label.examples"]</label>
<div @attributes="InertWhenRunning">
<select class="form-select @(IsRunning ? "input-disabled" : "")" value="@SelectedExampleIndex" @onchange="OnExampleChanged">
<option value="-1">@Loc["option.none"]</option>
@for (int i = 0; i < Examples.Count; i++)
{
<option value="@i">@(i + 1). @GetLocalizedText(Examples[i].Name)</option>
}
</select>
</div>
</div>
<hr />
<!-- Tape Configuration -->
<div class="mb-4">
<label class="form-label fw-bold">@Loc["label.tapeState"]</label>
<div class="input-group">
<span class="input-group-text">@Loc["label.tapeValue"]</span>
<input type="text" class="form-control" @bind="TapeInput" placeholder="e.g. 1011" />
<span class="input-group-text">@Loc["label.headIndex"]</span>
<input type="number" class="form-control" @bind="HeadInput" style="max-width: 100px;" />
<button class="btn btn-primary" @onclick="SetTape" disabled="@IsRunning">@Loc["button.applyTape"]</button>
</div>
<small class="text-muted">@Loc["hint.tape"]</small>
</div>
<hr />
<!-- Action Table Configuration -->
<div class="mb-3">
<label class="form-label fw-bold">@Loc["label.actionTable"]</label>
<p class="text-muted small mb-1">@Loc["hint.format"] <code>@Loc["hint.csvFormat"]</code></p>
<textarea class="form-control mb-2 font-monospace" rows="6" @bind="CsvRulesInput" placeholder="q0, 1, q0, 0, L"></textarea>
@if (!string.IsNullOrEmpty(RuleParseError))
{
<div class="alert alert-danger py-2">@RuleParseError</div>
}
</div>
</div>
</div>
</div>
@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<string, object> InertWhenRunning =>
IsRunning ? new Dictionary<string, object> { ["inert"] = "" } : new();
// Example list loaded from server
private List<ExampleConfig> 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<List<ExampleConfig>>("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
}
}
/// <summary>
/// Called when an example is selected from the ComboBox.
/// </summary>
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();
}
}
}
/// <summary>
/// Resets all control values in the Configuration section.
/// </summary>
private void ClearConfiguration()
{
_cts?.Cancel();
IsRunning = false;
TapeInput = "";
HeadInput = 0;
CsvRulesInput = "";
RuleParseError = "";
Machine = new Machine();
Steps = 0;
StateHasChanged();
}
/// <summary>
/// Applies an example configuration to the UI.
/// </summary>
private void ApplyExample(ExampleConfig example)
{
_cts?.Cancel();
IsRunning = false;
TapeInput = example.TapeInput;
HeadInput = example.HeadPosition;
CsvRulesInput = BuildCsvFromActions(example.Actions);
LoadRules();
SetTape();
Steps = 0;
StateHasChanged();
}
/// <summary>
/// Returns the value matching the current language from a localized text list.
/// </summary>
private string GetLocalizedText(List<LocalizedText> texts)
{
return texts.FirstOrDefault(t => t.Language == Loc.CurrentLanguage)?.Value
?? texts.FirstOrDefault()?.Value
?? "";
}
/// <summary>
/// Converts the Actions array to a CSV string with comments in the current language.
/// </summary>
private string BuildCsvFromActions(List<ActionEntry> actions)
{
var lines = new List<string>();
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;
}
}