253 lines
14 KiB
Plaintext
253 lines
14 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;
|
|
var generationSource = item.StableCode.StartsWith("char-demand-", StringComparison.OrdinalIgnoreCase)
|
|
? "Demand generation"
|
|
: item.ParentItemID.HasValue
|
|
? "Manual regeneration"
|
|
: IllustrationStarterBatchDefinition.StarterCodes.Contains(item.StableCode)
|
|
? "Starter batch"
|
|
: "Manual library item";
|
|
<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>Template</dt>
|
|
<dd>@(item.PromptTemplateVersion ?? "Not generated")</dd>
|
|
<dt>Source</dt>
|
|
<dd>@generationSource</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>
|
|
}
|