PlotDirector/PlotLine/Views/Admin/StoryIntelligenceIllustrationLibrary.cshtml

242 lines
13 KiB
Plaintext

@model IllustrationLibraryAdminViewModel
@{
ViewData["Title"] = "Illustration Library";
}
<div class="page-heading compact">
<div>
<p class="eyebrow">Admin / Story Intelligence</p>
<h1>Illustration Library</h1>
<p class="lead-text">Representative visual identifiers for the Story Intelligence experience. These are reusable library illustrations, not canonical manuscript artwork.</p>
</div>
</div>
<nav class="mb-3" aria-label="Breadcrumb">
<a asp-controller="Projects" asp-action="Index">Projects</a>
<span class="muted"> / </span>
<a asp-action="Index">Admin</a>
<span class="muted"> / Illustration Library</span>
</nav>
@if (TempData["AdminMessage"] is string message)
{
<div class="alert alert-success">@message</div>
}
@if (TempData["AdminError"] is string error)
{
<div class="alert alert-danger">@error</div>
}
<section class="edit-panel">
<div class="d-flex flex-wrap gap-3 align-items-center justify-content-between">
<div>
<p class="eyebrow">Generation workflow</p>
<h2 class="mb-1">Planned → queued → generating → generated</h2>
<p class="muted mb-0">@Model.ProviderStatus</p>
<p class="muted mb-0">Storage: @Model.StoragePathHint</p>
</div>
<div class="d-flex flex-wrap gap-2">
<form asp-action="GenerateIllustrationStarterLibrary" method="post">
@Html.AntiForgeryToken()
<input type="hidden" name="Confirmed" value="true" />
<button type="submit" class="btn btn-primary">Generate Starter Library</button>
</form>
<form asp-action="RequeueFailedIllustrationLibraryItems" method="post">
@Html.AntiForgeryToken()
<input type="hidden" name="Category" value="@Model.Filter.Category" />
<input type="hidden" name="Status" value="@Model.Filter.Status" />
<input type="hidden" name="Search" value="@Model.Filter.Search" />
<button type="submit" class="btn btn-outline-primary">Requeue All Failed</button>
</form>
</div>
</div>
</section>
<section class="edit-panel">
<div class="row g-3">
<div class="col-6 col-md-3 col-xl">
<div class="border rounded p-3 h-100"><p class="eyebrow">Total</p><h3>@Model.Summary.TotalCount.ToString("N0")</h3></div>
</div>
<div class="col-6 col-md-3 col-xl">
<div class="border rounded p-3 h-100"><p class="eyebrow">Planned</p><h3>@Model.Summary.PlannedCount.ToString("N0")</h3></div>
</div>
<div class="col-6 col-md-3 col-xl">
<div class="border rounded p-3 h-100"><p class="eyebrow">Queued</p><h3>@Model.Summary.QueuedCount.ToString("N0")</h3></div>
</div>
<div class="col-6 col-md-3 col-xl">
<div class="border rounded p-3 h-100"><p class="eyebrow">Generated</p><h3>@Model.Summary.GeneratedCount.ToString("N0")</h3></div>
</div>
<div class="col-6 col-md-3 col-xl">
<div class="border rounded p-3 h-100"><p class="eyebrow">Approved</p><h3>@Model.Summary.ApprovedCount.ToString("N0")</h3></div>
</div>
<div class="col-6 col-md-3 col-xl">
<div class="border rounded p-3 h-100"><p class="eyebrow">Failed</p><h3>@Model.Summary.FailedCount.ToString("N0")</h3></div>
</div>
<div class="col-6 col-md-3 col-xl">
<div class="border rounded p-3 h-100"><p class="eyebrow">Superseded</p><h3>@Model.Summary.SupersededCount.ToString("N0")</h3></div>
</div>
<div class="col-6 col-md-3 col-xl">
<div class="border rounded p-3 h-100"><p class="eyebrow">Completed</p><h3>@Model.CompletedTotal.ToString("N0")</h3></div>
</div>
</div>
<p class="muted mt-3 mb-0">Starter library: @Model.StarterCharacterCount characters, @Model.StarterLocationCount locations, @Model.StarterAssetCount assets. Next generation click will queue @Model.StarterGenerationPlan.TotalToGenerate total.</p>
</section>
<section class="edit-panel">
<form method="get" class="row g-2 align-items-end">
<div class="col-md-3">
<label class="form-label" for="category">Category</label>
<select class="form-select" id="category" name="Category">
<option value="">All</option>
@foreach (var category in Model.Categories)
{
<option value="@category" selected="@(string.Equals(Model.Filter.Category, category, StringComparison.OrdinalIgnoreCase))">@category</option>
}
</select>
</div>
<div class="col-md-3">
<label class="form-label" for="status">Status</label>
<select class="form-select" id="status" name="Status">
<option value="">All</option>
@foreach (var status in Model.Statuses)
{
<option value="@status" selected="@(string.Equals(Model.Filter.Status, status, StringComparison.OrdinalIgnoreCase))">@status</option>
}
</select>
</div>
<div class="col-md-4">
<label class="form-label" for="search">Search</label>
<input class="form-control" id="search" name="Search" value="@Model.Filter.Search" />
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-outline-primary w-100">Filter</button>
</div>
</form>
</section>
<section class="project-card-grid">
@foreach (var item in Model.Items)
{
var preview = !string.IsNullOrWhiteSpace(item.ThumbnailPath) ? item.ThumbnailPath : item.FilePath;
<article class="project-card">
<div class="project-card__body">
<div class="d-flex justify-content-between gap-2 align-items-start">
<div>
<p class="eyebrow">@item.Category / @item.Status</p>
<h2>@item.DisplayTitle</h2>
</div>
<span class="badge bg-secondary">@item.AttemptCount.ToString("N0") tries</span>
</div>
@if (!string.IsNullOrWhiteSpace(preview))
{
<img src="@preview" alt="@item.DisplayTitle" class="img-fluid rounded border mb-3" loading="lazy" />
}
<p class="project-card__description">@item.ShortDescription</p>
<dl class="small mb-3">
<dt>Code</dt>
<dd><code>@item.StableCode</code></dd>
<dt>Provider</dt>
<dd>@(item.Provider ?? "Not generated") @(item.Model is null ? string.Empty : $" / {item.Model}")</dd>
<dt>Updated</dt>
<dd>@item.UpdatedUtc.ToString("yyyy-MM-dd HH:mm") UTC</dd>
@if (item.ApprovedUtc.HasValue)
{
<dt>Approved</dt>
<dd>@item.ApprovedUtc.Value.ToString("yyyy-MM-dd HH:mm") UTC</dd>
}
@if (item.RejectedUtc.HasValue)
{
<dt>Rejected</dt>
<dd>@item.RejectedUtc.Value.ToString("yyyy-MM-dd HH:mm") UTC</dd>
}
@if (!string.IsNullOrWhiteSpace(item.LastError))
{
<dt>Failure</dt>
<dd>@(item.ErrorMessage ?? item.LastError)</dd>
}
</dl>
<details class="mb-3">
<summary>Prompt and metadata</summary>
<label class="form-label mt-2">Prompt</label>
<textarea class="form-control" rows="6" readonly>@(item.Prompt ?? item.PromptText)</textarea>
<label class="form-label mt-2">Specification</label>
<textarea class="form-control" rows="6" readonly>@item.SpecificationJson</textarea>
<label class="form-label mt-2">Metadata</label>
<textarea class="form-control" rows="5" readonly>@item.MetadataJson</textarea>
</details>
<form asp-action="UpdateIllustrationLibraryMetadata" asp-route-id="@item.IllustrationLibraryItemID" method="post" class="mb-2">
@Html.AntiForgeryToken()
<input type="hidden" name="DisplayTitle" value="@item.DisplayTitle" />
<input type="hidden" name="ShortDescription" value="@item.ShortDescription" />
<input type="hidden" name="MetadataJson" value="@item.MetadataJson" />
<input type="hidden" name="Category" value="@Model.Filter.Category" />
<input type="hidden" name="Status" value="@Model.Filter.Status" />
<input type="hidden" name="Search" value="@Model.Filter.Search" />
<label class="form-label">Admin notes</label>
<textarea class="form-control mb-2" name="AdminNotes" rows="2">@item.AdminNotes</textarea>
<button type="submit" class="btn btn-outline-primary btn-sm">Save notes</button>
</form>
</div>
<div class="project-card__actions">
@if (item.Status is IllustrationLibraryStatuses.Planned or IllustrationLibraryStatuses.Failed or IllustrationLibraryStatuses.Rejected)
{
<form asp-action="QueueIllustrationLibraryItem" asp-route-id="@item.IllustrationLibraryItemID" method="post">
@Html.AntiForgeryToken()
<input type="hidden" name="Confirmed" value="true" />
<input type="hidden" name="Category" value="@Model.Filter.Category" />
<input type="hidden" name="Status" value="@Model.Filter.Status" />
<input type="hidden" name="Search" value="@Model.Filter.Search" />
<button type="submit" class="btn btn-primary btn-sm">Queue</button>
</form>
}
@if (item.Status == IllustrationLibraryStatuses.Generated)
{
<form asp-action="ApproveIllustrationLibraryItem" asp-route-id="@item.IllustrationLibraryItemID" method="post">
@Html.AntiForgeryToken()
<input type="hidden" name="Category" value="@Model.Filter.Category" />
<input type="hidden" name="Status" value="@Model.Filter.Status" />
<input type="hidden" name="Search" value="@Model.Filter.Search" />
<button type="submit" class="btn btn-success btn-sm">Approve</button>
</form>
<form asp-action="RejectIllustrationLibraryItem" asp-route-id="@item.IllustrationLibraryItemID" method="post">
@Html.AntiForgeryToken()
<input type="hidden" name="Reason" value="Rejected from Studio review" />
<input type="hidden" name="Category" value="@Model.Filter.Category" />
<input type="hidden" name="Status" value="@Model.Filter.Status" />
<input type="hidden" name="Search" value="@Model.Filter.Search" />
<button type="submit" class="btn btn-outline-danger btn-sm">Reject</button>
</form>
}
@if (item.Status is IllustrationLibraryStatuses.Generated or IllustrationLibraryStatuses.Approved or IllustrationLibraryStatuses.Rejected or IllustrationLibraryStatuses.Failed or IllustrationLibraryStatuses.Superseded)
{
<form asp-action="RegenerateIllustrationLibraryItem" asp-route-id="@item.IllustrationLibraryItemID" method="post">
@Html.AntiForgeryToken()
<input type="hidden" name="Confirmed" value="true" />
<input type="hidden" name="Category" value="@Model.Filter.Category" />
<input type="hidden" name="Status" value="@Model.Filter.Status" />
<input type="hidden" name="Search" value="@Model.Filter.Search" />
<button type="submit" class="btn btn-outline-primary btn-sm">Regenerate</button>
</form>
}
@if (item.Status != IllustrationLibraryStatuses.Disabled)
{
<form asp-action="DisableIllustrationLibraryItem" asp-route-id="@item.IllustrationLibraryItemID" method="post" onsubmit="return confirm('Disable this library illustration?');">
@Html.AntiForgeryToken()
<input type="hidden" name="Category" value="@Model.Filter.Category" />
<input type="hidden" name="Status" value="@Model.Filter.Status" />
<input type="hidden" name="Search" value="@Model.Filter.Search" />
<button type="submit" class="btn btn-outline-secondary btn-sm">Disable</button>
</form>
}
</div>
</article>
}
</section>
@if (Model.Items.Count == 0)
{
<section class="edit-panel">
<p class="mb-0">No library items match this filter.</p>
</section>
}