Add inline quick parse to curation dialog
This commit is contained in:
@@ -290,9 +290,13 @@ Because the import path depends on OCR, PDF XML extraction, and heuristics, the
|
|||||||
Current curation flow:
|
Current curation flow:
|
||||||
|
|
||||||
1. Browse a table on the `/tables` page.
|
1. Browse a table on the `/tables` page.
|
||||||
2. Hover a populated cell to identify editable entries.
|
2. Hover a populated cell to identify editable or curation-ready entries.
|
||||||
3. Open the popup editor for that cell.
|
3. Open the curation popup for a cell that still needs review.
|
||||||
4. Edit the entire `critical_result` graph:
|
4. Either:
|
||||||
|
- mark the preview as curated,
|
||||||
|
- click the compact result card to make a quick parse adjustment inline, or
|
||||||
|
- jump into the full popup editor.
|
||||||
|
5. In the full editor, edit the entire `critical_result` graph:
|
||||||
- base raw cell text
|
- base raw cell text
|
||||||
- curated prose / description
|
- curated prose / description
|
||||||
- raw affix text
|
- raw affix text
|
||||||
@@ -301,7 +305,7 @@ Current curation flow:
|
|||||||
- parsed JSON
|
- parsed JSON
|
||||||
- nested `critical_branch` rows
|
- nested `critical_branch` rows
|
||||||
- nested `critical_effect` rows for both the base result and branches
|
- nested `critical_effect` rows for both the base result and branches
|
||||||
5. Save the result back through the API.
|
6. Save the result back through the API.
|
||||||
|
|
||||||
The corresponding API endpoints are:
|
The corresponding API endpoints are:
|
||||||
|
|
||||||
|
|||||||
@@ -280,11 +280,17 @@
|
|||||||
Model="curationModel"
|
Model="curationModel"
|
||||||
IsLoading="isCurationLoading"
|
IsLoading="isCurationLoading"
|
||||||
IsSaving="isCurationSaving"
|
IsSaving="isCurationSaving"
|
||||||
|
IsReparsing="isCurationReparsing"
|
||||||
|
IsQuickParseMode="isCurationQuickParseMode"
|
||||||
ErrorMessage="@curationError"
|
ErrorMessage="@curationError"
|
||||||
|
QuickParseErrorMessage="@curationQuickParseError"
|
||||||
LegendEntries="@(tableDetail?.Legend ?? Array.Empty<CriticalTableLegendEntry>())"
|
LegendEntries="@(tableDetail?.Legend ?? Array.Empty<CriticalTableLegendEntry>())"
|
||||||
OnClose="CloseCellCurationAsync"
|
OnClose="CloseCellCurationAsync"
|
||||||
OnMarkCurated="MarkCellCuratedAsync"
|
OnMarkCurated="MarkCellCuratedAsync"
|
||||||
OnEdit="OpenEditorFromCurationAsync" />
|
OnEdit="OpenEditorFromCurationAsync"
|
||||||
|
OnEnterQuickParse="EnterCurationQuickParseMode"
|
||||||
|
OnCancelQuickParse="CancelCurationQuickParseMode"
|
||||||
|
OnReparse="ReparseCurationCellAsync" />
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (isEditorOpen)
|
@if (isEditorOpen)
|
||||||
@@ -328,7 +334,10 @@
|
|||||||
private bool isCurationOpen;
|
private bool isCurationOpen;
|
||||||
private bool isCurationLoading;
|
private bool isCurationLoading;
|
||||||
private bool isCurationSaving;
|
private bool isCurationSaving;
|
||||||
|
private bool isCurationReparsing;
|
||||||
|
private bool isCurationQuickParseMode;
|
||||||
private string? curationError;
|
private string? curationError;
|
||||||
|
private string? curationQuickParseError;
|
||||||
private int? curatingResultId;
|
private int? curatingResultId;
|
||||||
private CriticalCellEditorModel? curationModel;
|
private CriticalCellEditorModel? curationModel;
|
||||||
private bool isTableMenuOpen;
|
private bool isTableMenuOpen;
|
||||||
@@ -487,6 +496,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
isCurationSaving = false;
|
isCurationSaving = false;
|
||||||
|
isCurationReparsing = false;
|
||||||
|
isCurationQuickParseMode = false;
|
||||||
isCurationOpen = true;
|
isCurationOpen = true;
|
||||||
await LoadCurationCellAsync(resultId, "The selected cell could not be loaded for curation.");
|
await LoadCurationCellAsync(resultId, "The selected cell could not be loaded for curation.");
|
||||||
}
|
}
|
||||||
@@ -496,7 +507,10 @@
|
|||||||
isCurationOpen = false;
|
isCurationOpen = false;
|
||||||
isCurationLoading = false;
|
isCurationLoading = false;
|
||||||
isCurationSaving = false;
|
isCurationSaving = false;
|
||||||
|
isCurationReparsing = false;
|
||||||
|
isCurationQuickParseMode = false;
|
||||||
curationError = null;
|
curationError = null;
|
||||||
|
curationQuickParseError = null;
|
||||||
curatingResultId = null;
|
curatingResultId = null;
|
||||||
curationModel = null;
|
curationModel = null;
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
@@ -555,12 +569,39 @@
|
|||||||
await OpenCellEditorAsync(resultId);
|
await OpenCellEditorAsync(resultId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Task EnterCurationQuickParseMode()
|
||||||
|
{
|
||||||
|
if (curationModel is null)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
curationQuickParseError = null;
|
||||||
|
isCurationQuickParseMode = true;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task CancelCurationQuickParseMode()
|
||||||
|
{
|
||||||
|
if (isCurationReparsing)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
curationQuickParseError = null;
|
||||||
|
isCurationQuickParseMode = false;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
private async Task LoadCurationCellAsync(int resultId, string loadFailureMessage)
|
private async Task LoadCurationCellAsync(int resultId, string loadFailureMessage)
|
||||||
{
|
{
|
||||||
curationError = null;
|
curationError = null;
|
||||||
|
curationQuickParseError = null;
|
||||||
curationModel = null;
|
curationModel = null;
|
||||||
curatingResultId = resultId;
|
curatingResultId = resultId;
|
||||||
isCurationLoading = true;
|
isCurationLoading = true;
|
||||||
|
isCurationQuickParseMode = false;
|
||||||
|
isCurationReparsing = false;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -585,6 +626,39 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task ReparseCurationCellAsync()
|
||||||
|
{
|
||||||
|
if (curationModel is null || string.IsNullOrWhiteSpace(selectedTableSlug) || curatingResultId is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isCurationReparsing = true;
|
||||||
|
curationQuickParseError = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await ReparseCriticalCellAsync(curationModel, curatingResultId.Value);
|
||||||
|
if (response is null)
|
||||||
|
{
|
||||||
|
curationQuickParseError = "The selected cell could not be re-parsed.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
curationModel = CriticalCellEditorModel.FromResponse(response);
|
||||||
|
isCurationQuickParseMode = false;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
curationQuickParseError = exception.Message;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
isCurationReparsing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private int? FindNextUncuratedResultId(int currentResultId)
|
private int? FindNextUncuratedResultId(int currentResultId)
|
||||||
{
|
{
|
||||||
if (tableDetail?.Cells is null || tableDetail.Cells.Count == 0)
|
if (tableDetail?.Cells is null || tableDetail.Cells.Count == 0)
|
||||||
@@ -674,7 +748,7 @@
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var comparisonBaseline = editorModel.Clone();
|
var comparisonBaseline = editorModel.Clone();
|
||||||
var response = await LookupService.ReparseCriticalCellAsync(selectedTableSlug, editingResultId.Value, editorModel.ToRequest());
|
var response = await ReparseCriticalCellAsync(editorModel, editingResultId.Value);
|
||||||
if (response is null)
|
if (response is null)
|
||||||
{
|
{
|
||||||
editorReparseError = "The selected cell could not be re-parsed.";
|
editorReparseError = "The selected cell could not be re-parsed.";
|
||||||
@@ -695,6 +769,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Task<CriticalCellEditorResponse?> ReparseCriticalCellAsync(CriticalCellEditorModel model, int resultId) =>
|
||||||
|
LookupService.ReparseCriticalCellAsync(selectedTableSlug, resultId, model.ToRequest());
|
||||||
|
|
||||||
private async Task SaveCellEditorAsync()
|
private async Task SaveCellEditorAsync()
|
||||||
{
|
{
|
||||||
if (editorModel is null || string.IsNullOrWhiteSpace(selectedTableSlug) || editingResultId is null)
|
if (editorModel is null || string.IsNullOrWhiteSpace(selectedTableSlug) || editingResultId is null)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
@using System.Collections.Generic
|
@using System.Collections.Generic
|
||||||
@using System.Linq
|
@using System.Linq
|
||||||
|
@using Microsoft.AspNetCore.Components.Web
|
||||||
@using Microsoft.JSInterop
|
@using Microsoft.JSInterop
|
||||||
@using RolemasterDb.App.Features
|
@using RolemasterDb.App.Features
|
||||||
@implements IAsyncDisposable
|
@implements IAsyncDisposable
|
||||||
@@ -37,63 +38,108 @@
|
|||||||
<p class="muted">Loading curation preview...</p>
|
<p class="muted">Loading curation preview...</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
else if (!string.IsNullOrWhiteSpace(ErrorMessage))
|
else if (Model is null)
|
||||||
{
|
{
|
||||||
<div class="critical-editor-body">
|
<div class="critical-editor-body">
|
||||||
<p class="error-text critical-editor-error">@ErrorMessage</p>
|
@if (!string.IsNullOrWhiteSpace(ErrorMessage))
|
||||||
|
{
|
||||||
|
<p class="error-text critical-editor-error">@ErrorMessage</p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<p class="muted">No curation preview is available.</p>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
else if (Model is not null)
|
else
|
||||||
{
|
{
|
||||||
<div class="critical-editor-body critical-curation-body">
|
<div class="critical-editor-body critical-curation-body @(IsQuickParseMode ? "is-quick-parse" : null)">
|
||||||
<div class="critical-curation-grid">
|
@if (!string.IsNullOrWhiteSpace(ErrorMessage))
|
||||||
<div class="critical-editor-card critical-curation-preview-card">
|
{
|
||||||
<CompactCriticalCell
|
<p class="error-text critical-editor-error">@ErrorMessage</p>
|
||||||
Description="@Model.DescriptionText"
|
|
||||||
Effects="@BuildPreviewEffects(Model)"
|
|
||||||
Branches="@BuildPreviewBranches(Model)" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="critical-editor-card critical-curation-source-card">
|
|
||||||
@if (!string.IsNullOrWhiteSpace(Model.SourceImageUrl))
|
|
||||||
{
|
|
||||||
<img
|
|
||||||
class="critical-curation-source-image"
|
|
||||||
src="@Model.SourceImageUrl"
|
|
||||||
alt="@BuildSourceImageAltText(Model)" />
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<div class="critical-curation-source-empty">
|
|
||||||
<p class="muted">No source image is available for this cell yet.</p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@{
|
|
||||||
var usedLegendEntries = GetUsedLegendEntries(Model, LegendEntries);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (usedLegendEntries.Count > 0)
|
@if (IsQuickParseMode)
|
||||||
{
|
{
|
||||||
<div class="critical-curation-legend">
|
<CriticalCellQuickParseEditor
|
||||||
@foreach (var entry in usedLegendEntries)
|
Model="Model"
|
||||||
{
|
IsBusy="IsReparsing"
|
||||||
<div class="critical-curation-legend-item" title="@entry.Tooltip">
|
IsDisabled="IsSaving"
|
||||||
<span class="legend-symbol">@entry.Symbol</span>
|
ShowActionButton="false"
|
||||||
<strong>@entry.Label</strong>
|
ShowDescriptionEditor="false"
|
||||||
</div>
|
ShowValidationSummary="false"
|
||||||
}
|
ErrorMessage="@QuickParseErrorMessage"
|
||||||
|
SectionClass="critical-editor-section critical-curation-quick-parse-section" />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="critical-curation-grid">
|
||||||
|
<div class="critical-editor-card critical-curation-preview-card">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="critical-curation-preview-button"
|
||||||
|
@onclick="OnEnterQuickParse"
|
||||||
|
disabled="@(IsSaving || IsReparsing)">
|
||||||
|
<CompactCriticalCell
|
||||||
|
Description="@Model.DescriptionText"
|
||||||
|
Effects="@CriticalCellPresentation.BuildPreviewEffects(Model)"
|
||||||
|
Branches="@CriticalCellPresentation.BuildPreviewBranches(Model)" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="critical-editor-card critical-curation-source-card">
|
||||||
|
@if (!string.IsNullOrWhiteSpace(Model.SourceImageUrl))
|
||||||
|
{
|
||||||
|
<img
|
||||||
|
class="critical-curation-source-image"
|
||||||
|
src="@Model.SourceImageUrl"
|
||||||
|
alt="@CriticalCellPresentation.BuildSourceImageAltText(Model)" />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="critical-curation-source-empty">
|
||||||
|
<p class="muted">No source image is available for this cell yet.</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if (GetUsedLegendEntries(Model, LegendEntries) is { Count: > 0 } usedLegendEntries)
|
||||||
|
{
|
||||||
|
<div class="critical-curation-legend">
|
||||||
|
@foreach (var entry in usedLegendEntries)
|
||||||
|
{
|
||||||
|
<div class="critical-curation-legend-item" title="@entry.Tooltip">
|
||||||
|
<span class="legend-symbol">@entry.Symbol</span>
|
||||||
|
<strong>@entry.Label</strong>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="critical-editor-footer">
|
<footer class="critical-editor-footer">
|
||||||
<button type="button" class="btn btn-link" @onclick="OnEdit" disabled="@(IsSaving)">Edit</button>
|
@if (IsQuickParseMode)
|
||||||
<button type="button" class="btn-ritual" @onclick="OnMarkCurated" disabled="@(IsSaving)">
|
{
|
||||||
@(IsSaving ? "Saving..." : "Mark as Curated")
|
<button type="button" class="btn btn-link" @onclick="OnCancelQuickParse" disabled="@(IsSaving || IsReparsing)">Cancel</button>
|
||||||
</button>
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-ritual"
|
||||||
|
@onclick="HandleReparseClickAsync"
|
||||||
|
@onclick:stopPropagation="true"
|
||||||
|
@onclick:preventDefault="true"
|
||||||
|
disabled="@(IsSaving || IsReparsing)">
|
||||||
|
@(IsReparsing ? "Parsing..." : "Parse")
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<button type="button" class="btn btn-link" @onclick="OnEdit" disabled="@(IsSaving)">Edit</button>
|
||||||
|
<button type="button" class="btn-ritual" @onclick="OnMarkCurated" disabled="@(IsSaving)">
|
||||||
|
@(IsSaving ? "Saving..." : "Mark as Curated")
|
||||||
|
</button>
|
||||||
|
}
|
||||||
</footer>
|
</footer>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -109,9 +155,18 @@
|
|||||||
[Parameter]
|
[Parameter]
|
||||||
public bool IsSaving { get; set; }
|
public bool IsSaving { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public bool IsReparsing { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public bool IsQuickParseMode { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public string? ErrorMessage { get; set; }
|
public string? ErrorMessage { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string? QuickParseErrorMessage { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public IReadOnlyList<CriticalTableLegendEntry>? LegendEntries { get; set; }
|
public IReadOnlyList<CriticalTableLegendEntry>? LegendEntries { get; set; }
|
||||||
|
|
||||||
@@ -124,6 +179,15 @@
|
|||||||
[Parameter, EditorRequired]
|
[Parameter, EditorRequired]
|
||||||
public EventCallback OnEdit { get; set; }
|
public EventCallback OnEdit { get; set; }
|
||||||
|
|
||||||
|
[Parameter, EditorRequired]
|
||||||
|
public EventCallback OnEnterQuickParse { get; set; }
|
||||||
|
|
||||||
|
[Parameter, EditorRequired]
|
||||||
|
public EventCallback OnCancelQuickParse { get; set; }
|
||||||
|
|
||||||
|
[Parameter, EditorRequired]
|
||||||
|
public EventCallback OnReparse { get; set; }
|
||||||
|
|
||||||
private IJSObjectReference? jsModule;
|
private IJSObjectReference? jsModule;
|
||||||
private bool isBackdropPointerDown;
|
private bool isBackdropPointerDown;
|
||||||
|
|
||||||
@@ -194,71 +258,11 @@
|
|||||||
isBackdropPointerDown = false;
|
isBackdropPointerDown = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string BuildSourceImageAltText(CriticalCellEditorModel model)
|
|
||||||
{
|
|
||||||
var segments = new List<string>
|
|
||||||
{
|
|
||||||
model.TableName,
|
|
||||||
$"roll band {model.RollBand}",
|
|
||||||
$"column {model.ColumnLabel}"
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(model.GroupLabel))
|
|
||||||
{
|
|
||||||
segments.Add($"variant {model.GroupLabel}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return string.Join(", ", segments);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string BuildColumnDisplayText(CriticalCellEditorModel model) =>
|
private static string BuildColumnDisplayText(CriticalCellEditorModel model) =>
|
||||||
string.IsNullOrWhiteSpace(model.GroupLabel)
|
string.IsNullOrWhiteSpace(model.GroupLabel)
|
||||||
? model.ColumnLabel
|
? model.ColumnLabel
|
||||||
: $"{model.GroupLabel} / {model.ColumnLabel}";
|
: $"{model.GroupLabel} / {model.ColumnLabel}";
|
||||||
|
|
||||||
private static IReadOnlyList<CriticalEffectLookupResponse> BuildPreviewEffects(CriticalCellEditorModel model) =>
|
|
||||||
model.Effects
|
|
||||||
.Select(effect => new CriticalEffectLookupResponse(
|
|
||||||
effect.EffectCode,
|
|
||||||
effect.Target,
|
|
||||||
effect.ValueInteger,
|
|
||||||
effect.ValueExpression,
|
|
||||||
effect.DurationRounds,
|
|
||||||
effect.PerRound,
|
|
||||||
effect.Modifier,
|
|
||||||
effect.BodyPart,
|
|
||||||
effect.IsPermanent,
|
|
||||||
effect.SourceType,
|
|
||||||
effect.SourceText))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
private static IReadOnlyList<CriticalBranchLookupResponse> BuildPreviewBranches(CriticalCellEditorModel model) =>
|
|
||||||
model.Branches
|
|
||||||
.OrderBy(branch => branch.SortOrder)
|
|
||||||
.Select(branch => new CriticalBranchLookupResponse(
|
|
||||||
branch.BranchKind,
|
|
||||||
branch.ConditionKey,
|
|
||||||
branch.ConditionText,
|
|
||||||
branch.DescriptionText,
|
|
||||||
branch.RawAffixText,
|
|
||||||
branch.Effects
|
|
||||||
.Select(effect => new CriticalEffectLookupResponse(
|
|
||||||
effect.EffectCode,
|
|
||||||
effect.Target,
|
|
||||||
effect.ValueInteger,
|
|
||||||
effect.ValueExpression,
|
|
||||||
effect.DurationRounds,
|
|
||||||
effect.PerRound,
|
|
||||||
effect.Modifier,
|
|
||||||
effect.BodyPart,
|
|
||||||
effect.IsPermanent,
|
|
||||||
effect.SourceType,
|
|
||||||
effect.SourceText))
|
|
||||||
.ToList(),
|
|
||||||
branch.RawText,
|
|
||||||
branch.SortOrder))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
private static IReadOnlyList<CriticalTableLegendEntry> GetUsedLegendEntries(
|
private static IReadOnlyList<CriticalTableLegendEntry> GetUsedLegendEntries(
|
||||||
CriticalCellEditorModel model,
|
CriticalCellEditorModel model,
|
||||||
IReadOnlyList<CriticalTableLegendEntry>? legendEntries)
|
IReadOnlyList<CriticalTableLegendEntry>? legendEntries)
|
||||||
@@ -278,4 +282,9 @@
|
|||||||
.Where(entry => usedEffectCodes.Contains(entry.EffectCode))
|
.Where(entry => usedEffectCodes.Contains(entry.EffectCode))
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task HandleReparseClickAsync(MouseEventArgs _)
|
||||||
|
{
|
||||||
|
await OnReparse.InvokeAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,89 +57,19 @@
|
|||||||
{
|
{
|
||||||
<EditForm @key="Model" Model="Model" OnSubmit="HandleSubmitAsync" class="critical-editor-form">
|
<EditForm @key="Model" Model="Model" OnSubmit="HandleSubmitAsync" class="critical-editor-form">
|
||||||
<div class="critical-editor-body">
|
<div class="critical-editor-body">
|
||||||
@if (!string.IsNullOrWhiteSpace(ReparseErrorMessage))
|
|
||||||
{
|
|
||||||
<p class="error-text critical-editor-error">@ReparseErrorMessage</p>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (!string.IsNullOrWhiteSpace(SaveErrorMessage))
|
@if (!string.IsNullOrWhiteSpace(SaveErrorMessage))
|
||||||
{
|
{
|
||||||
<p class="error-text critical-editor-error">@SaveErrorMessage</p>
|
<p class="error-text critical-editor-error">@SaveErrorMessage</p>
|
||||||
}
|
}
|
||||||
|
|
||||||
<section class="critical-editor-section">
|
<CriticalCellQuickParseEditor
|
||||||
<div class="critical-editor-section-header">
|
Model="Model"
|
||||||
<div>
|
IsBusy="IsReparsing"
|
||||||
<h4>Quick Parse Input</h4>
|
IsDisabled="IsSaving"
|
||||||
<p class="muted">First line is the result prose. Later lines are base affixes or <code>condition: ...</code> lines with comma-separated shorthand.</p>
|
ShowDescriptionEditor="true"
|
||||||
</div>
|
ErrorMessage="@ReparseErrorMessage"
|
||||||
<button
|
ComparisonHintText="@(HasComparisonDifferences(Model, ComparisonBaseline) ? "Fresh generation differs from the current edited card. Review Generated Compare before saving if you want to keep the overrides." : null)"
|
||||||
type="button"
|
OnReparse="OnReparse" />
|
||||||
class="btn-ritual"
|
|
||||||
@onclick="HandleReparseClickAsync"
|
|
||||||
@onclick:stopPropagation="true"
|
|
||||||
@onclick:preventDefault="true"
|
|
||||||
disabled="@(IsSaving || IsReparsing)">
|
|
||||||
@(IsReparsing ? "Generating..." : "Generate From Quick Input")
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="critical-editor-quick-parse-grid">
|
|
||||||
<div class="field-shell">
|
|
||||||
<label>Quick Parse Input</label>
|
|
||||||
<textarea
|
|
||||||
class="input-shell critical-editor-textarea tall"
|
|
||||||
value="@Model.QuickParseInput"
|
|
||||||
@oninput="HandleQuickParseInputChanged"></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="critical-editor-quick-parse-source">
|
|
||||||
<div class="critical-editor-quick-parse-source-header">
|
|
||||||
<label>Source Cell</label>
|
|
||||||
@if (Model.SourcePageNumber is not null)
|
|
||||||
{
|
|
||||||
<span class="chip">Page @Model.SourcePageNumber</span>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="critical-editor-quick-parse-source-frame">
|
|
||||||
@if (!string.IsNullOrWhiteSpace(Model.SourceImageUrl))
|
|
||||||
{
|
|
||||||
<img
|
|
||||||
class="critical-editor-source-image is-inline"
|
|
||||||
src="@Model.SourceImageUrl"
|
|
||||||
alt="@BuildSourceImageAltText(Model)" />
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<p class="muted critical-editor-inline-copy">No source image is available for this cell yet.</p>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p class="muted critical-editor-advanced-hint">Example: <code>Foe brings his guard up, frightened by your display.</code> then <code>+5, 1mp</code> or <code>w/o shield: glancing blow, +15, 3s, 3np</code>.</p>
|
|
||||||
<div class="critical-editor-quick-legend">
|
|
||||||
@foreach (var entry in QuickParseLegendEntries)
|
|
||||||
{
|
|
||||||
<div class="critical-editor-quick-legend-item">
|
|
||||||
<code>@entry.Token</code>
|
|
||||||
<span class="critical-editor-quick-legend-equals">=</span>
|
|
||||||
<AffixBadgeList Effects="@entry.Effects" />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
@if (Model.ValidationMessages.Count > 0)
|
|
||||||
{
|
|
||||||
<p class="muted critical-editor-advanced-hint">@GetParserNoteSummary(Model.ValidationMessages.Count)</p>
|
|
||||||
}
|
|
||||||
@if (HasComparisonDifferences(Model, ComparisonBaseline))
|
|
||||||
{
|
|
||||||
<p class="muted critical-editor-advanced-hint">Fresh generation differs from the current edited card. Review Generated Compare before saving if you want to keep the overrides.</p>
|
|
||||||
}
|
|
||||||
<div class="field-shell">
|
|
||||||
<label>Result Text</label>
|
|
||||||
<InputTextArea class="input-shell critical-editor-textarea compact" @bind-Value="Model.DescriptionText" @bind-Value:after="MarkDescriptionOverridden" />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="critical-editor-section">
|
<section class="critical-editor-section">
|
||||||
<div class="critical-editor-section-header">
|
<div class="critical-editor-section-header">
|
||||||
@@ -267,8 +197,8 @@
|
|||||||
Title="Current Edited Card"
|
Title="Current Edited Card"
|
||||||
Caption="@GetCurrentComparisonCaption(ComparisonBaseline)"
|
Caption="@GetCurrentComparisonCaption(ComparisonBaseline)"
|
||||||
Description="@GetComparisonSourceModel(Model, ComparisonBaseline).DescriptionText"
|
Description="@GetComparisonSourceModel(Model, ComparisonBaseline).DescriptionText"
|
||||||
Effects="@BuildPreviewEffects(GetComparisonSourceModel(Model, ComparisonBaseline))"
|
Effects="@CriticalCellPresentation.BuildPreviewEffects(GetComparisonSourceModel(Model, ComparisonBaseline))"
|
||||||
Branches="@BuildPreviewBranches(GetComparisonSourceModel(Model, ComparisonBaseline))" />
|
Branches="@CriticalCellPresentation.BuildPreviewBranches(GetComparisonSourceModel(Model, ComparisonBaseline))" />
|
||||||
<CriticalResultPreviewCard
|
<CriticalResultPreviewCard
|
||||||
Title="Generated From Quick Input"
|
Title="Generated From Quick Input"
|
||||||
Caption="This is the fresh generated output before override preservation."
|
Caption="This is the fresh generated output before override preservation."
|
||||||
@@ -282,8 +212,8 @@
|
|||||||
Title="Current After Re-Parse"
|
Title="Current After Re-Parse"
|
||||||
Caption="This is the merged editor state after keeping the existing overrides."
|
Caption="This is the merged editor state after keeping the existing overrides."
|
||||||
Description="@Model.DescriptionText"
|
Description="@Model.DescriptionText"
|
||||||
Effects="@BuildPreviewEffects(Model)"
|
Effects="@CriticalCellPresentation.BuildPreviewEffects(Model)"
|
||||||
Branches="@BuildPreviewBranches(Model)" />
|
Branches="@CriticalCellPresentation.BuildPreviewBranches(Model)" />
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -357,17 +287,6 @@
|
|||||||
[Parameter, EditorRequired]
|
[Parameter, EditorRequired]
|
||||||
public EventCallback OnSave { get; set; }
|
public EventCallback OnSave { get; set; }
|
||||||
|
|
||||||
private static readonly IReadOnlyList<(string Token, IReadOnlyList<CriticalEffectLookupResponse> Effects)> QuickParseLegendEntries =
|
|
||||||
[
|
|
||||||
("+15", [CreateQuickLegendEffect(CriticalEffectCodes.DirectHits, valueInteger: 15)]),
|
|
||||||
("3s", [CreateQuickLegendEffect(CriticalEffectCodes.StunnedRounds, durationRounds: 3)]),
|
|
||||||
("1mp", [CreateQuickLegendEffect(CriticalEffectCodes.MustParryRounds, durationRounds: 1)]),
|
|
||||||
("3np", [CreateQuickLegendEffect(CriticalEffectCodes.NoParryRounds, durationRounds: 3)]),
|
|
||||||
("1hpr", [CreateQuickLegendEffect(CriticalEffectCodes.BleedPerRound, perRound: 1)]),
|
|
||||||
("-20", [CreateQuickLegendEffect(CriticalEffectCodes.FoePenalty, modifier: -20)]),
|
|
||||||
("+20b", [CreateQuickLegendEffect(CriticalEffectCodes.AttackerBonusNextRound, modifier: 20)]),
|
|
||||||
("+2d10-3pp", [CreateQuickLegendEffect(CriticalEffectCodes.PowerPointModifier, valueExpression: "2d10-3")])
|
|
||||||
];
|
|
||||||
private IJSObjectReference? jsModule;
|
private IJSObjectReference? jsModule;
|
||||||
private bool isBackdropPointerDown;
|
private bool isBackdropPointerDown;
|
||||||
|
|
||||||
@@ -443,21 +362,6 @@
|
|||||||
await OnSave.InvokeAsync();
|
await OnSave.InvokeAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task HandleReparseClickAsync(MouseEventArgs _)
|
|
||||||
{
|
|
||||||
await OnReparse.InvokeAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleQuickParseInputChanged(ChangeEventArgs args)
|
|
||||||
{
|
|
||||||
if (Model is null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Model.QuickParseInput = args.Value?.ToString() ?? string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddBaseEffect()
|
private void AddBaseEffect()
|
||||||
{
|
{
|
||||||
if (Model is null)
|
if (Model is null)
|
||||||
@@ -583,14 +487,6 @@
|
|||||||
effect.IsOverridden = true;
|
effect.IsOverridden = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void MarkDescriptionOverridden()
|
|
||||||
{
|
|
||||||
if (Model is not null)
|
|
||||||
{
|
|
||||||
Model.IsDescriptionOverridden = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void MarkBranchOverridden(CriticalBranchEditorModel branch)
|
private static void MarkBranchOverridden(CriticalBranchEditorModel branch)
|
||||||
{
|
{
|
||||||
branch.IsOverridden = true;
|
branch.IsOverridden = true;
|
||||||
@@ -620,31 +516,6 @@
|
|||||||
return segments.Count == 0 ? "Generated compare" : string.Join(" · ", segments);
|
return segments.Count == 0 ? "Generated compare" : string.Join(" · ", segments);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetParserNoteSummary(int noteCount) =>
|
|
||||||
noteCount == 1
|
|
||||||
? "1 parser note is available under Advanced Review."
|
|
||||||
: $"{noteCount} parser notes are available under Advanced Review.";
|
|
||||||
|
|
||||||
private static CriticalEffectLookupResponse CreateQuickLegendEffect(
|
|
||||||
string effectCode,
|
|
||||||
int? valueInteger = null,
|
|
||||||
string? valueExpression = null,
|
|
||||||
int? durationRounds = null,
|
|
||||||
int? perRound = null,
|
|
||||||
int? modifier = null) =>
|
|
||||||
new(
|
|
||||||
effectCode,
|
|
||||||
"foe",
|
|
||||||
valueInteger,
|
|
||||||
valueExpression,
|
|
||||||
durationRounds,
|
|
||||||
perRound,
|
|
||||||
modifier,
|
|
||||||
null,
|
|
||||||
false,
|
|
||||||
"legend",
|
|
||||||
null);
|
|
||||||
|
|
||||||
private static IReadOnlyList<string> GetComparisonSummaryItems(CriticalCellEditorModel model, CriticalCellEditorModel? comparisonBaseline)
|
private static IReadOnlyList<string> GetComparisonSummaryItems(CriticalCellEditorModel model, CriticalCellEditorModel? comparisonBaseline)
|
||||||
{
|
{
|
||||||
if (model.GeneratedState is null)
|
if (model.GeneratedState is null)
|
||||||
@@ -653,8 +524,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
var comparisonSourceModel = GetComparisonSourceModel(model, comparisonBaseline);
|
var comparisonSourceModel = GetComparisonSourceModel(model, comparisonBaseline);
|
||||||
var comparisonSourceEffects = BuildPreviewEffects(comparisonSourceModel);
|
var comparisonSourceEffects = CriticalCellPresentation.BuildPreviewEffects(comparisonSourceModel);
|
||||||
var comparisonSourceBranches = BuildPreviewBranches(comparisonSourceModel);
|
var comparisonSourceBranches = CriticalCellPresentation.BuildPreviewBranches(comparisonSourceModel);
|
||||||
var items = new List<string>();
|
var items = new List<string>();
|
||||||
|
|
||||||
if (CriticalCellComparisonEvaluator.DescriptionDiffers(comparisonSourceModel.DescriptionText, model.GeneratedState.DescriptionText))
|
if (CriticalCellComparisonEvaluator.DescriptionDiffers(comparisonSourceModel.DescriptionText, model.GeneratedState.DescriptionText))
|
||||||
@@ -700,8 +571,8 @@
|
|||||||
var comparisonSourceModel = GetComparisonSourceModel(model, comparisonBaseline);
|
var comparisonSourceModel = GetComparisonSourceModel(model, comparisonBaseline);
|
||||||
return CriticalCellComparisonEvaluator.GetDifferenceCount(
|
return CriticalCellComparisonEvaluator.GetDifferenceCount(
|
||||||
comparisonSourceModel.DescriptionText,
|
comparisonSourceModel.DescriptionText,
|
||||||
BuildPreviewEffects(comparisonSourceModel),
|
CriticalCellPresentation.BuildPreviewEffects(comparisonSourceModel),
|
||||||
BuildPreviewBranches(comparisonSourceModel),
|
CriticalCellPresentation.BuildPreviewBranches(comparisonSourceModel),
|
||||||
model.GeneratedState);
|
model.GeneratedState);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -718,34 +589,6 @@
|
|||||||
? "This is the result that will be saved."
|
? "This is the result that will be saved."
|
||||||
: "This is the edited card before the last re-parse.";
|
: "This is the edited card before the last re-parse.";
|
||||||
|
|
||||||
private static string BuildSourceImageAltText(CriticalCellEditorModel model)
|
|
||||||
{
|
|
||||||
var segments = new List<string>
|
|
||||||
{
|
|
||||||
model.TableName,
|
|
||||||
$"roll band {model.RollBand}",
|
|
||||||
$"column {model.ColumnLabel}"
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(model.GroupLabel))
|
|
||||||
{
|
|
||||||
segments.Add($"variant {model.GroupLabel}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return string.Join(", ", segments);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IReadOnlyList<CriticalEffectLookupResponse> BuildPreviewEffects(CriticalCellEditorModel model) =>
|
|
||||||
model.Effects
|
|
||||||
.Select(CreatePreviewEffect)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
private static IReadOnlyList<CriticalBranchLookupResponse> BuildPreviewBranches(CriticalCellEditorModel model) =>
|
|
||||||
model.Branches
|
|
||||||
.OrderBy(branch => branch.SortOrder)
|
|
||||||
.Select(CreatePreviewBranch)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
private static IReadOnlyList<CriticalEffectLookupResponse> BuildSingleBadgeEffect(CriticalEffectEditorModel effect) =>
|
private static IReadOnlyList<CriticalEffectLookupResponse> BuildSingleBadgeEffect(CriticalEffectEditorModel effect) =>
|
||||||
[CreateBadgeEffect(effect)];
|
[CreateBadgeEffect(effect)];
|
||||||
|
|
||||||
@@ -763,21 +606,6 @@
|
|||||||
effect.SourceType,
|
effect.SourceType,
|
||||||
effect.SourceText);
|
effect.SourceText);
|
||||||
|
|
||||||
private static CriticalEffectLookupResponse CreatePreviewEffect(CriticalEffectEditorModel effect) =>
|
|
||||||
CreateBadgeEffect(effect);
|
|
||||||
|
|
||||||
private static CriticalBranchLookupResponse CreatePreviewBranch(CriticalBranchEditorModel branch) =>
|
|
||||||
new(
|
|
||||||
branch.BranchKind,
|
|
||||||
branch.ConditionKey,
|
|
||||||
branch.ConditionText,
|
|
||||||
branch.DescriptionText,
|
|
||||||
branch.RawAffixText,
|
|
||||||
branch.Effects
|
|
||||||
.Select(CreatePreviewEffect)
|
|
||||||
.ToList(),
|
|
||||||
branch.RawText,
|
|
||||||
branch.SortOrder);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@functions {
|
@functions {
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using RolemasterDb.App.Features;
|
||||||
|
|
||||||
|
namespace RolemasterDb.App.Components.Shared;
|
||||||
|
|
||||||
|
public static class CriticalCellPresentation
|
||||||
|
{
|
||||||
|
public static string BuildSourceImageAltText(CriticalCellEditorModel model)
|
||||||
|
{
|
||||||
|
var segments = new List<string>
|
||||||
|
{
|
||||||
|
model.TableName,
|
||||||
|
$"roll band {model.RollBand}",
|
||||||
|
$"column {model.ColumnLabel}"
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(model.GroupLabel))
|
||||||
|
{
|
||||||
|
segments.Add($"variant {model.GroupLabel}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Join(", ", segments);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IReadOnlyList<CriticalEffectLookupResponse> BuildPreviewEffects(CriticalCellEditorModel model) =>
|
||||||
|
model.Effects
|
||||||
|
.Select(CreatePreviewEffect)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
public static IReadOnlyList<CriticalBranchLookupResponse> BuildPreviewBranches(CriticalCellEditorModel model) =>
|
||||||
|
model.Branches
|
||||||
|
.OrderBy(branch => branch.SortOrder)
|
||||||
|
.Select(CreatePreviewBranch)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
private static CriticalEffectLookupResponse CreatePreviewEffect(CriticalEffectEditorModel effect) =>
|
||||||
|
new(
|
||||||
|
effect.EffectCode,
|
||||||
|
effect.Target,
|
||||||
|
effect.ValueInteger,
|
||||||
|
effect.ValueExpression,
|
||||||
|
effect.DurationRounds,
|
||||||
|
effect.PerRound,
|
||||||
|
effect.Modifier,
|
||||||
|
effect.BodyPart,
|
||||||
|
effect.IsPermanent,
|
||||||
|
effect.SourceType,
|
||||||
|
effect.SourceText);
|
||||||
|
|
||||||
|
private static CriticalBranchLookupResponse CreatePreviewBranch(CriticalBranchEditorModel branch) =>
|
||||||
|
new(
|
||||||
|
branch.BranchKind,
|
||||||
|
branch.ConditionKey,
|
||||||
|
branch.ConditionText,
|
||||||
|
branch.DescriptionText,
|
||||||
|
branch.RawAffixText,
|
||||||
|
branch.Effects
|
||||||
|
.Select(CreatePreviewEffect)
|
||||||
|
.ToList(),
|
||||||
|
branch.RawText,
|
||||||
|
branch.SortOrder);
|
||||||
|
}
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
@using Microsoft.AspNetCore.Components.Web
|
||||||
|
@using RolemasterDb.App.Domain
|
||||||
|
@using RolemasterDb.App.Features
|
||||||
|
|
||||||
|
<section class="@SectionClass">
|
||||||
|
@if (!string.IsNullOrWhiteSpace(ErrorMessage))
|
||||||
|
{
|
||||||
|
<p class="error-text critical-editor-error">@ErrorMessage</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="critical-editor-section-header">
|
||||||
|
<div>
|
||||||
|
<h4>Quick Parse Input</h4>
|
||||||
|
<p class="muted">First line is the result prose. Later lines are base affixes or <code>condition: ...</code> lines with comma-separated shorthand.</p>
|
||||||
|
</div>
|
||||||
|
@if (ShowActionButton)
|
||||||
|
{
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-ritual"
|
||||||
|
@onclick="HandleReparseClickAsync"
|
||||||
|
@onclick:stopPropagation="true"
|
||||||
|
@onclick:preventDefault="true"
|
||||||
|
disabled="@(IsBusy || IsDisabled)">
|
||||||
|
@(IsBusy ? BusyActionButtonText : ActionButtonText)
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="@GetQuickParseGridCssClass()">
|
||||||
|
<div class="field-shell">
|
||||||
|
<label>Quick Parse Input</label>
|
||||||
|
<textarea
|
||||||
|
class="@GetTextAreaCssClass()"
|
||||||
|
value="@Model.QuickParseInput"
|
||||||
|
@oninput="HandleQuickParseInputChanged"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (ShowSourcePreview)
|
||||||
|
{
|
||||||
|
<div class="critical-editor-quick-parse-source">
|
||||||
|
<div class="critical-editor-quick-parse-source-header">
|
||||||
|
<label>Source Cell</label>
|
||||||
|
@if (Model.SourcePageNumber is not null)
|
||||||
|
{
|
||||||
|
<span class="chip">Page @Model.SourcePageNumber</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="critical-editor-quick-parse-source-frame">
|
||||||
|
@if (!string.IsNullOrWhiteSpace(Model.SourceImageUrl))
|
||||||
|
{
|
||||||
|
<img
|
||||||
|
class="critical-editor-source-image is-inline"
|
||||||
|
src="@Model.SourceImageUrl"
|
||||||
|
alt="@CriticalCellPresentation.BuildSourceImageAltText(Model)" />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<p class="muted critical-editor-inline-copy">No source image is available for this cell yet.</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="muted critical-editor-advanced-hint">Example: <code>Foe brings his guard up, frightened by your display.</code> then <code>+5, 1mp</code> or <code>w/o shield: glancing blow, +15, 3s, 3np</code>.</p>
|
||||||
|
|
||||||
|
@if (ShowLegend)
|
||||||
|
{
|
||||||
|
<div class="critical-editor-quick-legend">
|
||||||
|
@foreach (var entry in QuickParseLegendEntries)
|
||||||
|
{
|
||||||
|
<div class="critical-editor-quick-legend-item">
|
||||||
|
<code>@entry.Token</code>
|
||||||
|
<span class="critical-editor-quick-legend-equals">=</span>
|
||||||
|
<AffixBadgeList Effects="@entry.Effects" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (ShowValidationSummary && Model.ValidationMessages.Count > 0)
|
||||||
|
{
|
||||||
|
<p class="muted critical-editor-advanced-hint">@GetParserNoteSummary(Model.ValidationMessages.Count)</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (!string.IsNullOrWhiteSpace(ComparisonHintText))
|
||||||
|
{
|
||||||
|
<p class="muted critical-editor-advanced-hint">@ComparisonHintText</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (ShowDescriptionEditor)
|
||||||
|
{
|
||||||
|
<div class="field-shell">
|
||||||
|
<label>Result Text</label>
|
||||||
|
<InputTextArea class="input-shell critical-editor-textarea compact" @bind-Value="Model.DescriptionText" @bind-Value:after="MarkDescriptionOverridden" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter, EditorRequired]
|
||||||
|
public CriticalCellEditorModel Model { get; set; } = default!;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public bool IsBusy { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public bool IsDisabled { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public bool ShowActionButton { get; set; } = true;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public bool ShowSourcePreview { get; set; } = true;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public bool ShowLegend { get; set; } = true;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public bool ShowValidationSummary { get; set; } = true;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public bool ShowDescriptionEditor { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string SectionClass { get; set; } = "critical-editor-section";
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string TextAreaSizeCssClass { get; set; } = "tall";
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string? GridCssClass { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string ActionButtonText { get; set; } = "Generate From Quick Input";
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string BusyActionButtonText { get; set; } = "Generating...";
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string? ComparisonHintText { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string? ErrorMessage { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback OnReparse { get; set; }
|
||||||
|
|
||||||
|
private static readonly IReadOnlyList<(string Token, IReadOnlyList<CriticalEffectLookupResponse> Effects)> QuickParseLegendEntries =
|
||||||
|
[
|
||||||
|
("+15", [CreateQuickLegendEffect(CriticalEffectCodes.DirectHits, valueInteger: 15)]),
|
||||||
|
("3s", [CreateQuickLegendEffect(CriticalEffectCodes.StunnedRounds, durationRounds: 3)]),
|
||||||
|
("1mp", [CreateQuickLegendEffect(CriticalEffectCodes.MustParryRounds, durationRounds: 1)]),
|
||||||
|
("3np", [CreateQuickLegendEffect(CriticalEffectCodes.NoParryRounds, durationRounds: 3)]),
|
||||||
|
("1hpr", [CreateQuickLegendEffect(CriticalEffectCodes.BleedPerRound, perRound: 1)]),
|
||||||
|
("-20", [CreateQuickLegendEffect(CriticalEffectCodes.FoePenalty, modifier: -20)]),
|
||||||
|
("+20b", [CreateQuickLegendEffect(CriticalEffectCodes.AttackerBonusNextRound, modifier: 20)]),
|
||||||
|
("+2d10-3pp", [CreateQuickLegendEffect(CriticalEffectCodes.PowerPointModifier, valueExpression: "2d10-3")])
|
||||||
|
];
|
||||||
|
|
||||||
|
private async Task HandleReparseClickAsync(MouseEventArgs _)
|
||||||
|
{
|
||||||
|
await OnReparse.InvokeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleQuickParseInputChanged(ChangeEventArgs args)
|
||||||
|
{
|
||||||
|
Model.QuickParseInput = args.Value?.ToString() ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MarkDescriptionOverridden()
|
||||||
|
{
|
||||||
|
Model.IsDescriptionOverridden = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetQuickParseGridCssClass() =>
|
||||||
|
string.IsNullOrWhiteSpace(GridCssClass)
|
||||||
|
? "critical-editor-quick-parse-grid"
|
||||||
|
: $"critical-editor-quick-parse-grid {GridCssClass}";
|
||||||
|
|
||||||
|
private string GetTextAreaCssClass() => $"input-shell critical-editor-textarea {TextAreaSizeCssClass}";
|
||||||
|
|
||||||
|
private static string GetParserNoteSummary(int noteCount) =>
|
||||||
|
noteCount == 1
|
||||||
|
? "1 parser note is available under Advanced Review."
|
||||||
|
: $"{noteCount} parser notes are available under Advanced Review.";
|
||||||
|
|
||||||
|
private static CriticalEffectLookupResponse CreateQuickLegendEffect(
|
||||||
|
string effectCode,
|
||||||
|
int? valueInteger = null,
|
||||||
|
string? valueExpression = null,
|
||||||
|
int? durationRounds = null,
|
||||||
|
int? perRound = null,
|
||||||
|
int? modifier = null) =>
|
||||||
|
new(
|
||||||
|
effectCode,
|
||||||
|
"foe",
|
||||||
|
valueInteger,
|
||||||
|
valueExpression,
|
||||||
|
durationRounds,
|
||||||
|
perRound,
|
||||||
|
modifier,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
"legend",
|
||||||
|
null);
|
||||||
|
}
|
||||||
@@ -1376,10 +1376,14 @@ textarea {
|
|||||||
.critical-curation-body {
|
.critical-curation-body {
|
||||||
flex: 0 1 auto;
|
flex: 0 1 auto;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
overflow: hidden;
|
overflow: auto;
|
||||||
padding: 0.85rem 1rem 1rem;
|
padding: 0.85rem 1rem 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.critical-curation-body.is-quick-parse {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.critical-curation-grid {
|
.critical-curation-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.85rem;
|
gap: 0.85rem;
|
||||||
@@ -1415,6 +1419,39 @@ textarea {
|
|||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.critical-curation-preview-button {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
display: block;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
text-align: left;
|
||||||
|
border-radius: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 140ms ease, box-shadow 140ms ease, background-color 140ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.critical-curation-preview-button:hover:not(:disabled),
|
||||||
|
.critical-curation-preview-button:focus-visible {
|
||||||
|
background: rgba(247, 239, 225, 0.65);
|
||||||
|
box-shadow: 0 0 0 1px rgba(127, 96, 55, 0.16);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.critical-curation-preview-button:disabled {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.critical-curation-quick-parse-section {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.critical-curation-quick-parse-section .critical-editor-quick-parse-grid {
|
||||||
|
grid-template-columns: minmax(0, 1.45fr) minmax(260px, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
.critical-curation-source-card {
|
.critical-curation-source-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user