PlotDirector/PlotLine/Views/Scenes/_SceneInspector.cshtml
Nick Beckley cea82a922f Implemented Phase 1 Scene Occupancy.
What Changed
Added scene-owned occupancy data:Scenes.FloorPlanID
Scenes.InitialFloorPlanFloorID
new SceneFloorPlanOccupancy table

Added Occupancy section to the Scene Inspector with AJAX save/refresh.
Added Floor Plan editor Structure / Occupancy mode.
Occupancy mode shows linked scenes, previous/next scene controls, and character/asset tokens inside assigned locations.
Character tokens use image thumbnail, then initials fallback. Asset tokens do the same, smaller.
No Features, no new floor-plan architecture, no transition changes.
Files Changed
[CoreModels.cs](C:/Source/PlotLine/PlotLine/Models/CoreModels.cs)
[CoreViewModels.cs](C:/Source/PlotLine/PlotLine/ViewModels/CoreViewModels.cs)
[Repositories.cs](C:/Source/PlotLine/PlotLine/Data/Repositories.cs)
[CoreServices.cs](C:/Source/PlotLine/PlotLine/Services/CoreServices.cs)
[ScenesController.cs](C:/Source/PlotLine/PlotLine/Controllers/ScenesController.cs)
[Program.cs](C:/Source/PlotLine/PlotLine/Program.cs)
[Edit.cshtml](C:/Source/PlotLine/PlotLine/Views/FloorPlans/Edit.cshtml)
[_SceneInspector.cshtml](C:/Source/PlotLine/PlotLine/Views/Scenes/_SceneInspector.cshtml)
[_SceneOccupancy.cshtml](C:/Source/PlotLine/PlotLine/Views/Scenes/_SceneOccupancy.cshtml)
[site.js](C:/Source/PlotLine/PlotLine/wwwroot/js/site.js)
[site.css](C:/Source/PlotLine/PlotLine/wwwroot/css/site.css)
minified CSS/JS updated.
Database
Added new migration only: [080_SceneFloorPlanOccupancy.sql](C:/Source/PlotLine/PlotLine/Sql/080_SceneFloorPlanOccupancy.sql)
Applied locally.
Added constraints/FKs and filtered unique indexes for one character/asset occupancy row per scene.
Stored Procedures Added/Updated
Scene_Save
Scene_Get
Scene_FloorPlanLink_Save
Scene_ListByFloorPlan
FloorPlanFloor_ListByPlan
FloorPlanBlock_ListByPlan
SceneFloorPlanOccupancy_ListByScene
SceneFloorPlanOccupancy_ListByFloorPlan
SceneFloorPlanOccupancy_Save
Test Data
Permanent Calendar House seed data added:
Book: The Calendar House
Chapters 1-3
14 scenes
Characters: Ellie, Grant, Stephen, Housekeeper, Gardener
Assets: Brass Key, Journal, Candle, Map, Letter
45 occupancy assignments
Verification
dotnet build PlotLine.slnx passes with 0 warnings/errors.
Migration applied locally.
Local app boots and returns HTTP 200.
DB checks confirm 14 linked scenes and 45 occupancy rows.
Manual browser click-through of the authenticated UI still needs a human pass for the AJAX controls and visual token placement.
2026-06-22 10:45:30 +01:00

1722 lines
116 KiB
Plaintext

@model SceneEditViewModel
@{
var revisionStatus = Model.RevisionStatuses.FirstOrDefault(x => x.Value == Model.RevisionStatusID.ToString())?.Text ?? "Not set";
var primaryLocation = Model.LocationOptions.FirstOrDefault(x => x.Value == Model.PrimaryLocationID?.ToString())?.Text ?? "No primary location";
var povCharacter = Model.SceneCharacters.FirstOrDefault(x => x.RoleInSceneTypeName?.Contains("POV", StringComparison.OrdinalIgnoreCase) == true)?.CharacterName
?? Model.SceneCharacters.FirstOrDefault()?.CharacterName
?? "No POV";
var timeLabel = !string.IsNullOrWhiteSpace(Model.RelativeTimeText)
? Model.RelativeTimeText
: Model.StartDateTime?.ToString("dd MMM yyyy HH:mm")
?? Model.StartDateTime?.ToString("dd MMM yyyy")
?? "Time not set";
var selectedPurposeLabels = Model.ScenePurposeTypes
.Where(x => Model.SelectedPurposeTypeIds.Contains(x.ScenePurposeTypeID))
.Select(x => x.PurposeName)
.ToList();
var dependencyCount = Model.DependenciesThisSceneNeeds.Count + Model.DependenciesNeedingThisScene.Count;
var totalWarningCount = Model.Warnings.Count + Model.AgeContinuityWarnings.Count;
var exactDateTimeModeId = Model.TimeModes.FirstOrDefault(x => string.Equals(x.Text, "Exact DateTime", StringComparison.OrdinalIgnoreCase))?.Value ?? string.Empty;
var exactDateModeId = Model.TimeModes.FirstOrDefault(x => string.Equals(x.Text, "Exact Date", StringComparison.OrdinalIgnoreCase))?.Value ?? string.Empty;
}
@functions {
private static string StoryStateBadgeClass(string state) => state switch
{
"Explicit" => "bg-success",
"Inferred" => "bg-secondary",
_ => "bg-warning text-dark"
};
}
<div class="scene-inspector-root" data-scene-id="@Model.SceneID">
<section class="inspector-section scene-overview-card" data-section-title="Overview" data-section-id="inspector-overview" data-inspector-search-text="@($"{Model.SceneTitle} {povCharacter} {primaryLocation} {timeLabel} {revisionStatus} {string.Join(' ', selectedPurposeLabels)}")">
<div>
<p class="eyebrow">Working context</p>
<h3>@(string.IsNullOrWhiteSpace(Model.SceneTitle) ? "Untitled scene" : Model.SceneTitle) <help-icon key="sceneInspector.overview" /></h3>
<p class="muted">
@if (Model.Chapter is not null)
{
<span>Chapter @Model.Chapter.ChapterNumber</span>
}
<span>Scene @Model.SceneNumber</span>
<span>@povCharacter</span>
<span>@primaryLocation</span>
</p>
</div>
<div class="overview-chip-grid">
<span class="overview-chip">@revisionStatus</span>
<span class="overview-chip">@timeLabel</span>
<span class="overview-chip @(totalWarningCount > 0 ? "attention" : "")">@totalWarningCount warning@(totalWarningCount == 1 ? "" : "s")</span>
<span class="overview-chip">@Model.SceneCharacters.Count character@(Model.SceneCharacters.Count == 1 ? "" : "s")</span>
<span class="overview-chip">@Model.AssetEvents.Count asset event@(Model.AssetEvents.Count == 1 ? "" : "s")</span>
<span class="overview-chip">@Model.ThreadEvents.Count thread event@(Model.ThreadEvents.Count == 1 ? "" : "s")</span>
<span class="overview-chip">@dependencyCount dependenc@(dependencyCount == 1 ? "y" : "ies")</span>
</div>
@if (selectedPurposeLabels.Any())
{
<div class="purpose-chip-row">
@foreach (var purpose in selectedPurposeLabels)
{
<span class="purpose-chip">@purpose</span>
}
</div>
}
@if (!string.IsNullOrWhiteSpace(Model.SceneOutcomeNotes))
{
<p class="overview-outcome">@Model.SceneOutcomeNotes</p>
}
</section>
<div class="inspector-quick-nav" data-inspector-nav>
<input class="form-control form-control-sm inspector-search" type="search" placeholder="Find in scene..." data-inspector-search />
<div class="inspector-nav-links">
<a href="#inspector-overview">Overview</a>
<a href="#inspector-timing">Time</a>
<a href="#inspector-purpose">Purpose</a>
<a href="#inspector-notes">Notes</a>
<a href="#inspector-characters">Characters</a>
<a href="#inspector-character-attributes">Attributes</a>
<a href="#inspector-character-knowledge">Knowledge</a>
<a href="#inspector-relationships">Relationships</a>
<a href="#inspector-thread-events">Threads</a>
<a href="#inspector-asset-events">Assets</a>
<a href="#inspector-asset-locations">Locations</a>
<a href="#inspector-occupancy">Occupancy</a>
<a href="#inspector-asset-custody">Custody</a>
<a href="#inspector-story-state">Story State</a>
<a href="#inspector-writer">Writer</a>
<a href="#inspector-checklist">Checklist</a>
<a href="#inspector-attachments">Attachments</a>
<a href="#inspector-metrics">Metrics</a>
<a href="#inspector-warnings">Warnings</a>
<a href="#inspector-dependencies">Dependencies</a>
<a href="#inspector-move">Move</a>
</div>
</div>
<form asp-controller="Scenes" asp-action="Save" method="post" class="scene-inspector-form" id="scene-inspector-save-form">
<input asp-for="SceneID" type="hidden" />
<input asp-for="ChapterID" type="hidden" />
<input asp-for="ReturnProjectID" type="hidden" />
<input asp-for="ReturnBookID" type="hidden" />
<input asp-for="ReturnToTimeline" type="hidden" />
<input asp-for="FloorPlanID" type="hidden" />
<input asp-for="InitialFloorPlanFloorID" type="hidden" />
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="inspector-section" data-section-title="Timing & Structure" data-section-id="inspector-timing">
<h3>Timing & Structure <help-icon key="sceneInspector.time" /></h3>
<div class="row g-2">
<div class="col-4">
<label asp-for="SceneNumber" class="form-label"></label>
<input asp-for="SceneNumber" class="form-control form-control-sm" />
</div>
<div class="col-8">
<label asp-for="RevisionStatusID" class="form-label">Revision status <help-icon key="sceneInspector.fields.revisionStatus" mode="inline" /></label>
<select asp-for="RevisionStatusID" asp-items="Model.RevisionStatuses" class="form-select form-select-sm"></select>
</div>
<div class="col-12">
<label asp-for="SceneTitle" class="form-label"></label>
<input asp-for="SceneTitle" class="form-control" />
<span asp-validation-for="SceneTitle" class="text-danger"></span>
</div>
<div class="col-12">
<label asp-for="Summary" class="form-label"></label>
<textarea asp-for="Summary" class="form-control" rows="4"></textarea>
</div>
<div class="col-12">
<label asp-for="PrimaryLocationID" class="form-label">Primary location <help-icon key="sceneInspector.fields.primaryLocation" mode="inline" /></label>
<select asp-for="PrimaryLocationID" asp-items="Model.LocationOptions" class="form-select form-select-sm">
<option value="">No primary location</option>
</select>
</div>
</div>
@if (Model.LocationConsistencyMessages.Any())
{
<div class="location-helper mt-2">
<h3>Location checks</h3>
<ul class="location-warning-list">
@foreach (var message in Model.LocationConsistencyMessages)
{
<li>@message</li>
}
</ul>
</div>
}
<h4 class="inspector-subheading">Time</h4>
<div class="row g-2">
<div class="col-sm-6">
<label asp-for="TimeModeID" class="form-label">Time mode <help-icon key="sceneInspector.fields.timeMode" mode="inline" /></label>
<select asp-for="TimeModeID" asp-items="Model.TimeModes" class="form-select form-select-sm"></select>
</div>
<div class="col-sm-6">
<label asp-for="TimeConfidenceID" class="form-label">Time confidence <help-icon key="sceneInspector.fields.timeConfidence" mode="inline" /></label>
<select asp-for="TimeConfidenceID" asp-items="Model.TimeConfidences" class="form-select form-select-sm"></select>
</div>
<div class="col-12">
<div class="row g-2" data-scene-time-editor data-exact-datetime-mode-id="@exactDateTimeModeId" data-exact-date-mode-id="@exactDateModeId">
<input asp-for="StartDateTime" type="hidden" value="@Model.StartDateTime?.ToString("yyyy-MM-ddTHH:mm")" />
<input asp-for="EndDateTime" type="hidden" value="@Model.EndDateTime?.ToString("yyyy-MM-ddTHH:mm")" />
<div class="col-sm-6">
<label class="form-label" for="StartDateInput">Start date</label>
<input id="StartDateInput" name="StartDateInput" type="date" value="@Model.StartDateTime?.ToString("yyyy-MM-dd")" class="form-control form-control-sm" data-date-part-for="StartDateTime" />
</div>
<div class="col-sm-6" data-time-part-field>
<label class="form-label" for="StartTimeInput">Start time</label>
<input id="StartTimeInput" name="StartTimeInput" type="time" value="@Model.StartDateTime?.ToString("HH:mm")" class="form-control form-control-sm" data-time-part-for="StartDateTime" />
</div>
<div class="col-sm-6">
<label class="form-label" for="EndDateInput">End date</label>
<input id="EndDateInput" name="EndDateInput" type="date" value="@Model.EndDateTime?.ToString("yyyy-MM-dd")" class="form-control form-control-sm" data-date-part-for="EndDateTime" />
</div>
<div class="col-sm-6" data-time-part-field>
<label class="form-label" for="EndTimeInput">End time</label>
<input id="EndTimeInput" name="EndTimeInput" type="time" value="@Model.EndDateTime?.ToString("HH:mm")" class="form-control form-control-sm" data-time-part-for="EndDateTime" />
</div>
</div>
</div>
<div class="col-sm-4">
<label asp-for="DurationAmount" class="form-label"></label>
<input asp-for="DurationAmount" class="form-control form-control-sm" />
</div>
<div class="col-sm-8">
<label asp-for="DurationUnitID" class="form-label">Duration unit <help-icon key="sceneInspector.fields.durationUnit" mode="inline" /></label>
<select asp-for="DurationUnitID" asp-items="Model.DurationUnits" class="form-select form-select-sm">
<option value="">Not set</option>
</select>
</div>
<div class="col-12">
<label asp-for="RelativeTimeText" class="form-label"></label>
<input asp-for="RelativeTimeText" class="form-control form-control-sm" placeholder="e.g. Two days later" />
</div>
</div>
</div>
<div class="inspector-section" data-section-title="Scene Purpose & Outcome" data-section-id="inspector-purpose">
<h3>Purposes <help-icon key="sceneInspector.purpose" /></h3>
<div class="purpose-list inspector-purpose-list">
@foreach (var purpose in Model.ScenePurposeTypes)
{
<label class="purpose-check">
@if (Model.SelectedPurposeTypeIds.Contains(purpose.ScenePurposeTypeID))
{
<input type="checkbox" name="SelectedPurposeTypeIds" value="@purpose.ScenePurposeTypeID" checked />
}
else
{
<input type="checkbox" name="SelectedPurposeTypeIds" value="@purpose.ScenePurposeTypeID" />
}
<span>@purpose.PurposeName</span>
</label>
}
</div>
<div class="purpose-notes-block mt-2">
<h3>Notes</h3>
<label asp-for="ScenePurposeNotes" class="form-label"></label>
<textarea asp-for="ScenePurposeNotes" class="form-control mb-2" rows="4"></textarea>
<label asp-for="SceneOutcomeNotes" class="form-label"></label>
<textarea asp-for="SceneOutcomeNotes" class="form-control" rows="4"></textarea>
</div>
</div>
<div class="inspector-section" data-section-title="Scene Metrics" data-section-id="inspector-metrics">
<h3>Scene Metrics <help-icon key="timeline.metrics" /></h3>
@if (!Model.Metrics.Any())
{
<div class="empty-panel compact">
<p>This project has no scene metrics yet.</p>
@if (Model.ReturnProjectID.HasValue)
{
<a class="btn btn-outline-primary btn-sm" asp-controller="ProjectMetrics" asp-action="Index" asp-route-projectId="@Model.ReturnProjectID">Add default metrics</a>
}
</div>
}
else
{
<div class="metric-slider-stack">
@for (var index = 0; index < Model.Metrics.Count; index++)
{
<div class="metric-slider">
<input asp-for="Metrics[index].MetricTypeID" type="hidden" />
<input asp-for="Metrics[index].MetricName" type="hidden" />
<input asp-for="Metrics[index].Description" type="hidden" />
<input asp-for="Metrics[index].MinValue" type="hidden" />
<input asp-for="Metrics[index].MaxValue" type="hidden" />
<div class="metric-slider-label">
<label asp-for="Metrics[index].Value">@Model.Metrics[index].MetricName</label>
<output>@Model.Metrics[index].Value</output>
</div>
<input asp-for="Metrics[index].Value" type="range" min="@Model.Metrics[index].MinValue" max="@Model.Metrics[index].MaxValue" class="form-range" oninput="this.previousElementSibling.querySelector('output').value = this.value" />
<input asp-for="Metrics[index].Notes" class="form-control form-control-sm" placeholder="Metric note" />
</div>
}
</div>
}
</div>
<div class="inspector-actions">
<button class="btn btn-primary" type="submit" form="scene-inspector-save-form">Save scene</button>
</div>
</form>
@if (Model.SceneID > 0 && Model.ReturnToTimeline)
{
<form asp-controller="Scenes" asp-action="Archive" method="post" class="inspector-archive-form archive-button-form mt-2" data-confirm-message="Archive this scene?&#10;&#10;This will hide it from active lists and timelines, but the data will be kept and can be restored later.">
<input type="hidden" name="id" value="@Model.SceneID" />
<input type="hidden" name="chapterId" value="@Model.ChapterID" />
<button class="btn btn-outline-danger btn-sm" type="submit">Archive Scene</button>
</form>
}
@if (TempData["WriterWorkspaceError"] is string writerWorkspaceError)
{
<div class="alert alert-warning">@writerWorkspaceError</div>
}
<!-- Scene foundation continues: notes sit immediately after the main scene save form so timing, purpose and metrics still submit together. -->
<section class="inspector-section writer-notes-section" data-section-title="Scene Notes" data-section-id="inspector-notes">
<h3>Scene Notes <help-icon key="sceneInspector.notes" /></h3>
@if (!Model.Notes.Any())
{
<p class="muted">No working notes yet. Capture drafting thoughts, research reminders, or revision questions as they come up.</p>
}
else
{
<div class="writer-note-list">
@foreach (var note in Model.Notes)
{
<details class="writer-mini-card" @(note.IsPinned ? "open" : "")>
<summary>
<span class="status-pill">@note.TypeName</span>
<strong>@(string.IsNullOrWhiteSpace(note.NoteTitle) ? note.NoteText : note.NoteTitle)</strong>
@if (note.IsPinned)
{
<span class="badge text-bg-light">Pinned</span>
}
@if (note.IsResolved)
{
<span class="badge text-bg-success">Resolved</span>
}
</summary>
<form asp-controller="Scenes" asp-action="SaveNote" method="post" class="writer-edit-form">
<input type="hidden" name="SceneNoteID" value="@note.SceneNoteID" />
<input type="hidden" name="SceneID" value="@Model.SceneID" />
<input type="hidden" name="ReturnProjectID" value="@Model.ReturnProjectID" />
<input type="hidden" name="ReturnBookID" value="@Model.ReturnBookID" />
<label class="form-label mb-0">Note type <help-icon key="sceneInspector.fields.noteType" mode="inline" /></label>
<select class="form-select form-select-sm" name="SceneNoteTypeID">
@foreach (var option in Model.SceneNoteTypeOptions)
{
<option value="@option.Value" selected="@(option.Value == note.SceneNoteTypeID.ToString())">@option.Text</option>
}
</select>
<input class="form-control form-control-sm" name="NoteTitle" value="@note.NoteTitle" placeholder="Title" />
<textarea class="form-control form-control-sm" name="NoteText" rows="3">@note.NoteText</textarea>
<div class="writer-checkbox-grid">
<label><input type="checkbox" name="IsPinned" value="true" checked="@note.IsPinned" /> Pinned</label>
<label><input type="checkbox" name="IsResolved" value="true" checked="@note.IsResolved" /> Resolved</label>
</div>
<div class="button-row compact-buttons">
<button class="btn btn-primary btn-sm" type="submit">Save note</button>
</div>
</form>
<form asp-controller="Scenes" asp-action="DeleteNote" method="post" class="mt-1">
<input type="hidden" name="id" value="@note.SceneNoteID" />
<input type="hidden" name="sceneId" value="@Model.SceneID" />
<input type="hidden" name="projectId" value="@Model.ReturnProjectID" />
<input type="hidden" name="bookId" value="@Model.ReturnBookID" />
<button class="btn btn-outline-danger btn-sm" type="submit">Remove note</button>
</form>
</details>
}
</div>
}
<form asp-controller="Scenes" asp-action="SaveNote" method="post" class="writer-add-form">
<input type="hidden" name="SceneID" value="@Model.SceneID" />
<input type="hidden" name="ReturnProjectID" value="@Model.ReturnProjectID" />
<input type="hidden" name="ReturnBookID" value="@Model.ReturnBookID" />
<label class="form-label mb-0">Note type <help-icon key="sceneInspector.fields.noteType" mode="inline" /></label>
<select class="form-select form-select-sm" name="SceneNoteTypeID" asp-items="Model.SceneNoteTypeOptions"></select>
<input class="form-control form-control-sm" name="NoteTitle" placeholder="Optional title" />
<textarea class="form-control form-control-sm" name="NoteText" rows="3" placeholder="Add a drafting, revision, research, or TODO note"></textarea>
<div class="writer-checkbox-grid">
<label><input type="checkbox" name="IsPinned" value="true" /> Pin note</label>
</div>
<button class="btn btn-outline-primary btn-sm" type="submit">Add note</button>
</form>
</section>
<!-- Characters -->
<section class="inspector-section character-section" data-section-title="Characters" data-section-id="inspector-characters">
<h3>Characters <help-icon key="sceneInspector.characters" /></h3>
@if (!Model.SceneCharacters.Any())
{
<p class="muted">No characters attached to this scene yet. Add the people who appear, are mentioned, or meaningfully affect the scene.</p>
}
else
{
<div class="asset-event-list">
@foreach (var character in Model.SceneCharacters)
{
var editId = $"scene-character-edit-{character.SceneCharacterID}";
var ageDisplay = CharacterAgeDisplayService.GetAgeForScene(character, Model.TimeModeName, Model.StartDateTime);
<article class="character-appearance-card">
<div class="character-appearance-summary">
<div>
<strong>@character.CharacterName</strong>
@if (!string.IsNullOrWhiteSpace(ageDisplay.DisplayText))
{
<span class="muted">@ageDisplay.Label: @ageDisplay.DisplayText</span>
}
<span class="muted">@((character.RoleInSceneTypeName ?? "Role not set")) • @((character.PresenceTypeName ?? "Presence not set"))</span>
<span class="muted" title="@character.LocationPath">Location: @(character.LocationName ?? "Not set")</span>
<span class="muted">Entry: @(character.EntryLocationName ?? "Not set")</span>
<span class="muted">Exit: @(character.ExitLocationName ?? "Not set")</span>
@if (!string.IsNullOrWhiteSpace(character.OutfitDescription) || !string.IsNullOrWhiteSpace(character.PhysicalCondition) || !string.IsNullOrWhiteSpace(character.EmotionalState))
{
<span class="muted">@character.OutfitDescription @character.PhysicalCondition @character.EmotionalState</span>
}
</div>
<div class="character-appearance-actions">
<button class="btn btn-outline-primary btn-sm" type="button" data-bs-toggle="collapse" data-bs-target="#@editId" aria-expanded="false" aria-controls="@editId">Edit</button>
<form asp-controller="Scenes" asp-action="DeleteSceneCharacter" method="post">
<input type="hidden" name="id" value="@character.SceneCharacterID" />
<input type="hidden" name="sceneId" value="@Model.SceneID" />
<input type="hidden" name="projectId" value="@Model.ReturnProjectID" />
<input type="hidden" name="bookId" value="@Model.ReturnBookID" />
<button class="btn btn-outline-secondary btn-sm" type="submit">Remove</button>
</form>
</div>
</div>
<div class="collapse character-appearance-edit" id="@editId">
<form asp-controller="Scenes" asp-action="UpdateSceneCharacter" method="post" class="asset-create-form">
<input type="hidden" name="SceneCharacterID" value="@character.SceneCharacterID" />
<input type="hidden" name="SceneID" value="@Model.SceneID" />
<input type="hidden" name="CharacterID" value="@character.CharacterID" />
<input type="hidden" name="ReturnProjectID" value="@Model.ReturnProjectID" />
<input type="hidden" name="ReturnBookID" value="@Model.ReturnBookID" />
<div class="row g-2">
<div class="col-6">
<label class="form-label" for="RoleInSceneTypeID-@character.SceneCharacterID">Role <help-icon key="sceneInspector.fields.characterRole" mode="inline" /></label>
<select class="form-select form-select-sm" id="RoleInSceneTypeID-@character.SceneCharacterID" name="RoleInSceneTypeID">
<option value="">Not set</option>
@foreach (var option in Model.CharacterRoleOptions)
{
<option value="@option.Value" selected="@(option.Value == character.RoleInSceneTypeID?.ToString())">@option.Text</option>
}
</select>
</div>
<div class="col-6">
<label class="form-label" for="PresenceTypeID-@character.SceneCharacterID">Presence <help-icon key="sceneInspector.fields.characterPresence" mode="inline" /></label>
<select class="form-select form-select-sm" id="PresenceTypeID-@character.SceneCharacterID" name="PresenceTypeID">
<option value="">Not set</option>
@foreach (var option in Model.PresenceTypeOptions)
{
<option value="@option.Value" selected="@(option.Value == character.PresenceTypeID?.ToString())">@option.Text</option>
}
</select>
</div>
<div class="col-12">
<label class="form-label" for="CharacterLocationID-@character.SceneCharacterID">Current location <help-icon key="sceneInspector.fields.characterLocation" mode="inline" /></label>
<select class="form-select form-select-sm" id="CharacterLocationID-@character.SceneCharacterID" name="LocationID">
<option value="">Not set</option>
@foreach (var option in Model.LocationOptions)
{
<option value="@option.Value" selected="@(option.Value == character.LocationID?.ToString())">@option.Text</option>
}
</select>
</div>
<div class="col-6">
<label class="form-label" for="EntryLocationID-@character.SceneCharacterID">Enters from <help-icon key="sceneInspector.fields.characterLocation" mode="inline" /></label>
<select class="form-select form-select-sm" id="EntryLocationID-@character.SceneCharacterID" name="EntryLocationID">
<option value="">Not set</option>
@foreach (var option in Model.LocationOptions)
{
<option value="@option.Value" selected="@(option.Value == character.EntryLocationID?.ToString())">@option.Text</option>
}
</select>
</div>
<div class="col-6">
<label class="form-label" for="ExitLocationID-@character.SceneCharacterID">Exits to <help-icon key="sceneInspector.fields.characterLocation" mode="inline" /></label>
<select class="form-select form-select-sm" id="ExitLocationID-@character.SceneCharacterID" name="ExitLocationID">
<option value="">Not set</option>
@foreach (var option in Model.LocationOptions)
{
<option value="@option.Value" selected="@(option.Value == character.ExitLocationID?.ToString())">@option.Text</option>
}
</select>
</div>
<div class="col-12">
<label class="form-label" for="AppearanceNotes-@character.SceneCharacterID">Appearance notes</label>
<textarea class="form-control form-control-sm" id="AppearanceNotes-@character.SceneCharacterID" name="AppearanceNotes" rows="2">@character.AppearanceNotes</textarea>
</div>
<div class="col-12">
<label class="form-label" for="OutfitDescription-@character.SceneCharacterID">Outfit description</label>
<input class="form-control form-control-sm" id="OutfitDescription-@character.SceneCharacterID" name="OutfitDescription" value="@character.OutfitDescription" />
</div>
<div class="col-6">
<label class="form-label" for="PhysicalCondition-@character.SceneCharacterID">Physical condition</label>
<input class="form-control form-control-sm" id="PhysicalCondition-@character.SceneCharacterID" name="PhysicalCondition" value="@character.PhysicalCondition" />
</div>
<div class="col-6">
<label class="form-label" for="EmotionalState-@character.SceneCharacterID">Emotional state</label>
<input class="form-control form-control-sm" id="EmotionalState-@character.SceneCharacterID" name="EmotionalState" value="@character.EmotionalState" />
</div>
<div class="col-12">
<label class="form-label" for="KnowledgeNotes-@character.SceneCharacterID">Knowledge notes</label>
<textarea class="form-control form-control-sm" id="KnowledgeNotes-@character.SceneCharacterID" name="KnowledgeNotes" rows="2">@character.KnowledgeNotes</textarea>
</div>
</div>
<button class="btn btn-outline-primary btn-sm mt-2" type="submit">Save character</button>
</form>
</div>
</article>
}
</div>
}
<form asp-controller="Scenes" asp-action="AddSceneCharacter" method="post" class="asset-create-form">
<input type="hidden" name="SceneID" value="@Model.SceneID" />
<input type="hidden" name="ReturnProjectID" value="@Model.ReturnProjectID" />
<input type="hidden" name="ReturnBookID" value="@Model.ReturnBookID" />
<h4 class="inspector-subheading">Add character to scene</h4>
@if (TempData["SceneCharacterAddError"] is string addCharacterError)
{
<p class="text-danger form-helper-message">@addCharacterError</p>
}
<div class="row g-2">
<div class="col-12">
<label class="form-label" for="CharacterID">Character <help-icon key="sceneInspector.fields.characterSelector" mode="inline" /></label>
<select class="form-select form-select-sm" id="CharacterID" name="CharacterID" asp-items="Model.CharacterOptions">
<option value="">Create or choose a character</option>
</select>
</div>
<div class="col-12">
<label class="form-label" for="NewCharacterName">New character name</label>
<input class="form-control form-control-sm" id="NewCharacterName" name="NewCharacterName" placeholder="Optional" />
</div>
<div class="col-6">
<label class="form-label" for="RoleInSceneTypeID">Role <help-icon key="sceneInspector.fields.characterRole" mode="inline" /></label>
<select class="form-select form-select-sm" id="RoleInSceneTypeID" name="RoleInSceneTypeID" asp-items="Model.CharacterRoleOptions">
<option value="">Not set</option>
</select>
</div>
<div class="col-6">
<label class="form-label" for="PresenceTypeID">Presence <help-icon key="sceneInspector.fields.characterPresence" mode="inline" /></label>
<select class="form-select form-select-sm" id="PresenceTypeID" name="PresenceTypeID" asp-items="Model.PresenceTypeOptions">
<option value="">Not set</option>
</select>
</div>
<div class="col-12">
<label class="form-label" for="CharacterLocationID">Current location <help-icon key="sceneInspector.fields.characterLocation" mode="inline" /></label>
<select class="form-select form-select-sm" id="CharacterLocationID" name="LocationID" asp-items="Model.LocationOptions">
<option value="">Not set</option>
</select>
</div>
<div class="col-6">
<label class="form-label" for="EntryLocationID">Enters from <help-icon key="sceneInspector.fields.characterLocation" mode="inline" /></label>
<select class="form-select form-select-sm" id="EntryLocationID" name="EntryLocationID" asp-items="Model.LocationOptions">
<option value="">Not set</option>
</select>
</div>
<div class="col-6">
<label class="form-label" for="ExitLocationID">Exits to <help-icon key="sceneInspector.fields.characterLocation" mode="inline" /></label>
<select class="form-select form-select-sm" id="ExitLocationID" name="ExitLocationID" asp-items="Model.LocationOptions">
<option value="">Not set</option>
</select>
</div>
<div class="col-12"><input class="form-control form-control-sm" name="AppearanceNotes" placeholder="Appearance notes" /></div>
<div class="col-12"><input class="form-control form-control-sm" name="OutfitDescription" placeholder="Outfit description" /></div>
<div class="col-6"><input class="form-control form-control-sm" name="PhysicalCondition" placeholder="Physical condition" /></div>
<div class="col-6"><input class="form-control form-control-sm" name="EmotionalState" placeholder="Emotional state" /></div>
<div class="col-12"><textarea class="form-control form-control-sm" name="KnowledgeNotes" rows="2" placeholder="Knowledge notes"></textarea></div>
</div>
<button class="btn btn-outline-primary btn-sm mt-2" type="submit">Add character to scene</button>
</form>
</section>
<section class="inspector-section character-section" data-section-title="Character Attributes" data-section-id="inspector-character-attributes">
<h3>Character Attributes <help-icon key="sceneInspector.characterAttributes" /></h3>
@if (Model.CharacterAttributeEvents.Any())
{
<div class="asset-event-list">
@foreach (var item in Model.CharacterAttributeEvents)
{
<div class="asset-event-item character-item">
<div>
<strong>@item.CharacterName: @item.AttributeName</strong>
<span class="muted">@item.AttributeValue</span>
<span class="muted">@item.Description</span>
</div>
</div>
}
</div>
}
else
{
<p class="muted">Character attribute history for this scene will appear here when available.</p>
}
<form asp-controller="Scenes" asp-action="AddCharacterAttributeEvent" method="post" class="asset-create-form">
<input type="hidden" name="SceneID" value="@Model.SceneID" />
<input type="hidden" name="ReturnProjectID" value="@Model.ReturnProjectID" />
<input type="hidden" name="ReturnBookID" value="@Model.ReturnBookID" />
<input type="hidden" name="ReturnUrl" value="@(Context.Request.Path + Context.Request.QueryString)" />
@if (TempData["CharacterAttributeAddError"] is string addCharacterAttributeError)
{
<p class="text-danger form-helper-message">@addCharacterAttributeError</p>
}
<div class="row g-2">
<div class="col-12">
<label class="form-label mb-1">Character <help-icon key="sceneInspector.fields.characterSelector" mode="inline" /></label>
<select class="form-select form-select-sm" name="CharacterID" asp-items="Model.CharacterOptions"></select>
</div>
<div class="col-12">
<label class="form-label mb-1">Attribute type <help-icon key="sceneInspector.fields.attributeType" mode="inline" /></label>
<select class="form-select form-select-sm" name="CharacterAttributeTypeID" asp-items="Model.CharacterAttributeTypeOptions"></select>
</div>
<div class="col-12">
<label class="form-label mb-1">Attribute value <help-icon key="sceneInspector.fields.attributeValue" mode="inline" /></label>
<input class="form-control form-control-sm" name="AttributeValue" placeholder="Value, e.g. Bruised ribs" />
</div>
<div class="col-12">
<textarea class="form-control form-control-sm" name="Description" rows="2" placeholder="Description"></textarea>
</div>
</div>
<button class="btn btn-outline-primary btn-sm mt-2" type="submit">Add attribute</button>
</form>
</section>
<section class="inspector-section character-section" data-section-title="Character Knowledge" data-section-id="inspector-character-knowledge">
<h3>Character Knowledge <help-icon key="sceneInspector.knowledge" /></h3>
@if (Model.CharacterKnowledge.Any())
{
<div class="asset-event-list">
@foreach (var item in Model.CharacterKnowledge)
{
<div class="asset-event-item character-item">
<div>
<strong>@item.CharacterName @item.KnowledgeStateName</strong>
<span class="muted">@(item.AssetName ?? item.ThreadTitle)</span>
<span class="muted">@item.Description</span>
</div>
<form asp-controller="Scenes" asp-action="DeleteCharacterKnowledge" method="post">
<input type="hidden" name="id" value="@item.CharacterKnowledgeID" />
<input type="hidden" name="sceneId" value="@Model.SceneID" />
<input type="hidden" name="projectId" value="@Model.ReturnProjectID" />
<input type="hidden" name="bookId" value="@Model.ReturnBookID" />
<button class="btn btn-outline-secondary btn-sm" type="submit">Remove</button>
</form>
</div>
}
</div>
}
<form asp-controller="Scenes" asp-action="AddCharacterKnowledge" method="post" class="asset-create-form">
<input type="hidden" name="SceneID" value="@Model.SceneID" />
<input type="hidden" name="ReturnProjectID" value="@Model.ReturnProjectID" />
<input type="hidden" name="ReturnBookID" value="@Model.ReturnBookID" />
<div class="row g-2">
<div class="col-12"><label class="form-label mb-1">Character <help-icon key="sceneInspector.fields.characterSelector" mode="inline" /></label><select class="form-select form-select-sm" name="CharacterID" asp-items="Model.CharacterOptions"></select></div>
<div class="col-12">
<label class="form-label mb-1">Related asset <help-icon key="sceneInspector.fields.knowledgeSubject" mode="inline" /></label>
<select class="form-select form-select-sm" name="StoryAssetID" asp-items="Model.AssetOptions">
<option value="">No asset</option>
</select>
</div>
<div class="col-12">
<label class="form-label mb-1">Related plot thread <help-icon key="sceneInspector.fields.knowledgeSubject" mode="inline" /></label>
<select class="form-select form-select-sm" name="PlotThreadID" asp-items="Model.PlotThreadOptions">
<option value="">No plot thread</option>
</select>
</div>
<div class="col-12"><label class="form-label mb-1">Knowledge state <help-icon key="sceneInspector.fields.knowledgeState" mode="inline" /></label><select class="form-select form-select-sm" name="KnowledgeStateID" asp-items="Model.KnowledgeStateOptions"></select></div>
<div class="col-12"><textarea class="form-control form-control-sm" name="Description" rows="2" placeholder="Description"></textarea></div>
</div>
<button class="btn btn-outline-primary btn-sm mt-2" type="submit">Add knowledge</button>
</form>
</section>
<section class="inspector-section character-section" data-section-title="Relationship Events" data-section-id="inspector-relationships">
<h3>Relationship Events <help-icon key="sceneInspector.relationships" /></h3>
@if (Model.RelationshipEvents.Any())
{
<div class="asset-event-list">
@foreach (var item in Model.RelationshipEvents)
{
<div class="asset-event-item character-item">
<div>
<strong>@item.CharacterAName -> @item.CharacterBName</strong>
<span class="muted">@item.RelationshipStateName / intensity @item.Intensity</span>
<span class="muted">@item.Description</span>
</div>
<form asp-controller="Scenes" asp-action="DeleteRelationshipEvent" method="post">
<input type="hidden" name="id" value="@item.RelationshipEventID" />
<input type="hidden" name="sceneId" value="@Model.SceneID" />
<input type="hidden" name="projectId" value="@Model.ReturnProjectID" />
<input type="hidden" name="bookId" value="@Model.ReturnBookID" />
<button class="btn btn-outline-secondary btn-sm" type="submit">Remove</button>
</form>
</div>
}
</div>
}
<form asp-controller="Scenes" asp-action="AddRelationshipEvent" method="post" class="asset-create-form">
<input type="hidden" name="SceneID" value="@Model.SceneID" />
<input type="hidden" name="ReturnProjectID" value="@Model.ReturnProjectID" />
<input type="hidden" name="ReturnBookID" value="@Model.ReturnBookID" />
<div class="row g-2">
@if (Model.RelationshipOptions.Count == 1)
{
<div class="col-12">
<input type="hidden" name="CharacterRelationshipID" value="@Model.RelationshipOptions[0].Value" />
<span class="form-control form-control-sm">@Model.RelationshipOptions[0].Text</span>
</div>
}
else
{
<div class="col-12">
<label class="form-label mb-1">Relationship <help-icon key="sceneInspector.fields.relationshipSelector" mode="inline" /></label>
<select class="form-select form-select-sm" name="CharacterRelationshipID" asp-items="Model.RelationshipOptions">
<option value="">Create or choose relationship</option>
</select>
</div>
}
<div class="col-6"><label class="form-label mb-1">Character A <help-icon key="sceneInspector.fields.relationshipCharacters" mode="inline" /></label><select class="form-select form-select-sm" name="CharacterAID" asp-items="Model.CharacterOptions"><option value="">Character A</option></select></div>
<div class="col-6"><label class="form-label mb-1">Character B <help-icon key="sceneInspector.fields.relationshipCharacters" mode="inline" /></label><select class="form-select form-select-sm" name="CharacterBID" asp-items="Model.CharacterOptions"><option value="">Character B</option></select></div>
<div class="col-12"><label class="form-label mb-1">Relationship type <help-icon key="sceneInspector.fields.relationshipType" mode="inline" /></label><select class="form-select form-select-sm" name="RelationshipTypeID" asp-items="Model.RelationshipTypeOptions"><option value="">Relationship type</option></select></div>
<div class="col-8"><label class="form-label mb-1">Relationship state <help-icon key="sceneInspector.fields.relationshipState" mode="inline" /></label><select class="form-select form-select-sm" name="RelationshipStateID" asp-items="Model.RelationshipStateOptions"></select></div>
<div class="col-4">
<label class="form-label">Intensity <output>5</output> <help-icon key="sceneInspector.fields.relationshipIntensity" mode="inline" /></label>
<input class="form-range" name="Intensity" type="range" min="1" max="10" step="1" value="5" oninput="this.previousElementSibling.querySelector('output').value = this.value" />
</div>
<div class="col-12">
<label class="form-check">
<input class="form-check-input" name="IsKnownToOtherCharacter" type="checkbox" value="true" />
<span class="form-check-label">Other character knows <help-icon key="sceneInspector.fields.relationshipKnown" mode="inline" /></span>
</label>
</div>
<div class="col-12"><textarea class="form-control form-control-sm" name="Description" rows="2" placeholder="Description"></textarea></div>
</div>
<button class="btn btn-outline-primary btn-sm mt-2" type="submit">Add relationship event</button>
</form>
</section>
<!-- Plot and story development -->
<section class="inspector-section thread-events-section" data-section-title="Plot Thread Events" data-section-id="inspector-thread-events">
<h3>Plot Thread Events <help-icon key="sceneInspector.threads" /></h3>
@if (!Model.ThreadEvents.Any())
{
<p class="muted">No plot thread events attached to this scene yet. Add clues, reveals, promises, payoffs, or reversals when the scene touches a thread.</p>
}
else
{
<div class="thread-event-list">
@foreach (var threadEvent in Model.ThreadEvents)
{
<div class="thread-event-item" style="--plot-colour:@threadEvent.Colour">
<div>
<span class="thread-event-marker">@threadEvent.MarkerText</span>
<strong>@threadEvent.EventTitle</strong>
<span class="muted">@threadEvent.PlotEventTypeName / @threadEvent.PlotLineName / @threadEvent.ThreadTitle</span>
</div>
<form asp-controller="Scenes" asp-action="DeleteThreadEvent" method="post">
<input type="hidden" name="id" value="@threadEvent.ThreadEventID" />
<input type="hidden" name="sceneId" value="@Model.SceneID" />
<input type="hidden" name="projectId" value="@Model.ReturnProjectID" />
<input type="hidden" name="bookId" value="@Model.ReturnBookID" />
<button class="btn btn-outline-secondary btn-sm" type="submit">Remove</button>
</form>
<details class="mt-2">
<summary>Edit</summary>
<form asp-controller="Scenes" asp-action="UpdateThreadEvent" method="post" class="thread-event-create mt-2" data-thread-event-form>
<input type="hidden" name="ThreadEventID" value="@threadEvent.ThreadEventID" />
<input type="hidden" name="SceneID" value="@Model.SceneID" />
<input type="hidden" name="PlotThreadID" value="@threadEvent.PlotThreadID" />
<input type="hidden" name="ReturnProjectID" value="@Model.ReturnProjectID" />
<input type="hidden" name="ReturnBookID" value="@Model.ReturnBookID" />
<input type="hidden" data-thread-event-source-plot-line value="@threadEvent.PlotLineID" />
@if (TempData[$"ThreadEventEditError:{threadEvent.ThreadEventID}"] is string editThreadEventError)
{
<p class="text-danger form-helper-message">@editThreadEventError</p>
}
<div class="row g-2">
<div class="col-12">
<label class="form-label" for="PlotEventTypeID-@threadEvent.ThreadEventID">Event type <help-icon key="sceneInspector.fields.threadEventType" mode="inline" /></label>
<select class="form-select form-select-sm" id="PlotEventTypeID-@threadEvent.ThreadEventID" name="PlotEventTypeID" data-plot-event-type>
<option value="">Choose event type</option>
@foreach (var eventType in Model.PlotEventTypeOptions)
{
if (eventType.Value == threadEvent.PlotEventTypeID.ToString())
{
<option value="@eventType.Value" selected>@eventType.Text</option>
}
else
{
<option value="@eventType.Value">@eventType.Text</option>
}
}
</select>
<div class="form-text">Branch starts another plot line while this one continues. Split ends this plot line and starts two or more others. Merge ends this plot line by feeding into another.</div>
</div>
<div class="col-12" data-plot-event-target="branch">
<label class="form-label" for="TargetPlotLineID-@threadEvent.ThreadEventID">Target plot line <help-icon key="sceneInspector.fields.threadTarget" mode="inline" /></label>
<select class="form-select form-select-sm" id="TargetPlotLineID-@threadEvent.ThreadEventID" name="TargetPlotLineID" data-thread-event-target-single>
<option value="">Choose target plot line</option>
@foreach (var plotLine in Model.PlotLineEventOptions)
{
if (threadEvent.TargetPlotLineID == plotLine.PlotLineID)
{
<option value="@plotLine.PlotLineID" data-target-option selected>@plotLine.PlotLineName</option>
}
else
{
<option value="@plotLine.PlotLineID" data-target-option>@plotLine.PlotLineName</option>
}
}
</select>
</div>
<div class="col-12" data-plot-event-target="merge">
<label class="form-label" for="MergeTargetPlotLineID-@threadEvent.ThreadEventID">Merge into plot line <help-icon key="sceneInspector.fields.threadTarget" mode="inline" /></label>
<select class="form-select form-select-sm" id="MergeTargetPlotLineID-@threadEvent.ThreadEventID" data-thread-event-target-merge>
<option value="">Choose merge target</option>
@foreach (var plotLine in Model.PlotLineEventOptions)
{
if (threadEvent.TargetPlotLineID == plotLine.PlotLineID)
{
<option value="@plotLine.PlotLineID" data-target-option selected>@plotLine.PlotLineName</option>
}
else
{
<option value="@plotLine.PlotLineID" data-target-option>@plotLine.PlotLineName</option>
}
}
</select>
</div>
<div class="col-12" data-plot-event-target="split">
<label class="form-label" for="SelectedTargetPlotLineIDs-@threadEvent.ThreadEventID">Target plot lines <help-icon key="sceneInspector.fields.threadTarget" mode="inline" /></label>
<select class="form-select form-select-sm" id="SelectedTargetPlotLineIDs-@threadEvent.ThreadEventID" name="SelectedTargetPlotLineIDs" multiple size="4" data-thread-event-target-split>
@foreach (var plotLine in Model.PlotLineEventOptions)
{
if (threadEvent.TargetPlotLineIDs.Contains(plotLine.PlotLineID))
{
<option value="@plotLine.PlotLineID" data-target-option selected>@plotLine.PlotLineName</option>
}
else
{
<option value="@plotLine.PlotLineID" data-target-option>@plotLine.PlotLineName</option>
}
}
</select>
</div>
<div class="col-12">
<label class="form-label" for="EventTypeID-@threadEvent.ThreadEventID">Marker type <help-icon key="sceneInspector.fields.threadMarkerType" mode="inline" /></label>
<select class="form-select form-select-sm" id="EventTypeID-@threadEvent.ThreadEventID" name="EventTypeID">
@foreach (var markerType in Model.ThreadEventTypeOptions)
{
if (markerType.Value == threadEvent.EventTypeID.ToString())
{
<option value="@markerType.Value" selected>@markerType.Text</option>
}
else
{
<option value="@markerType.Value">@markerType.Text</option>
}
}
</select>
</div>
<div class="col-12">
<label class="form-label" for="EventTitle-@threadEvent.ThreadEventID">Event title</label>
<input class="form-control form-control-sm" id="EventTitle-@threadEvent.ThreadEventID" name="EventTitle" value="@threadEvent.EventTitle" />
</div>
<div class="col-12">
<label class="form-label" for="EventDescription-@threadEvent.ThreadEventID">Description</label>
<textarea class="form-control form-control-sm" id="EventDescription-@threadEvent.ThreadEventID" name="EventDescription" rows="3">@threadEvent.EventDescription</textarea>
</div>
</div>
<button class="btn btn-outline-primary btn-sm mt-2" type="submit">Save event</button>
</form>
</details>
</div>
}
</div>
}
<form asp-controller="Scenes" asp-action="AddThreadEvent" method="post" class="thread-event-create" data-thread-event-form>
<input type="hidden" name="SceneID" value="@Model.SceneID" />
<input type="hidden" name="ReturnProjectID" value="@Model.ReturnProjectID" />
<input type="hidden" name="ReturnBookID" value="@Model.ReturnBookID" />
<p class="form-text mb-2">Choose an existing thread, or choose a plot line to create one from the event title.</p>
@if (TempData["ThreadEventAddError"] is string addThreadEventError)
{
<p class="text-danger form-helper-message">@addThreadEventError</p>
}
<div class="row g-2">
<div class="col-12">
<label class="form-label" for="PlotLineID">Plot line <help-icon key="sceneInspector.fields.threadSelector" mode="inline" /></label>
<select class="form-select form-select-sm" id="PlotLineID" name="PlotLineID" data-thread-event-source-plot-line>
<option value="">Choose plot line</option>
@foreach (var plotLine in Model.PlotLineEventOptions)
{
<option value="@plotLine.PlotLineID">@plotLine.PlotLineName</option>
}
</select>
</div>
<div class="col-12">
<label class="form-label" for="PlotThreadID">Existing plot thread <help-icon key="sceneInspector.fields.threadSelector" mode="inline" /></label>
<select class="form-select form-select-sm" id="PlotThreadID" name="PlotThreadID" data-thread-event-source-thread>
<option value="">Create or choose a thread</option>
@foreach (var plotThread in Model.PlotThreadEventOptions)
{
<option value="@plotThread.PlotThreadID" data-plot-line-id="@plotThread.PlotLineID">@plotThread.PlotLineName: @plotThread.ThreadTitle</option>
}
</select>
</div>
<div class="col-8" data-thread-event-new-thread-field>
<label class="form-label" for="NewThreadTitle">New thread title</label>
<input class="form-control form-control-sm" id="NewThreadTitle" name="NewThreadTitle" placeholder="Optional" />
</div>
<div class="col-4" data-thread-event-new-thread-field>
<label class="form-label" for="NewThreadTypeID">Thread type <help-icon key="sceneInspector.fields.threadType" mode="inline" /></label>
<select class="form-select form-select-sm" id="NewThreadTypeID" name="NewThreadTypeID" asp-items="Model.ThreadTypeOptions">
<option value="">Default</option>
</select>
</div>
<div class="col-12">
<label class="form-label" for="EventTypeID">Event type <help-icon key="sceneInspector.fields.threadEventType" mode="inline" /></label>
<select class="form-select form-select-sm" id="PlotEventTypeID" name="PlotEventTypeID" data-plot-event-type>
<option value="">Choose event type</option>
@foreach (var eventType in Model.PlotEventTypeOptions)
{
if (eventType.Value == Model.NewThreadEvent.PlotEventTypeID.ToString())
{
<option value="@eventType.Value" selected>@eventType.Text</option>
}
else
{
<option value="@eventType.Value">@eventType.Text</option>
}
}
</select>
<div class="form-text">Branch starts another plot line while this one continues. Split ends this plot line and starts two or more others. Merge ends this plot line by feeding into another.</div>
</div>
<div class="col-12" data-plot-event-target="branch">
<label class="form-label" for="TargetPlotLineID">Target plot line <help-icon key="sceneInspector.fields.threadTarget" mode="inline" /></label>
<select class="form-select form-select-sm" id="TargetPlotLineID" name="TargetPlotLineID" data-thread-event-target-single>
<option value="">Choose target plot line</option>
@foreach (var plotLine in Model.PlotLineEventOptions)
{
<option value="@plotLine.PlotLineID" data-target-option>@plotLine.PlotLineName</option>
}
</select>
</div>
<div class="col-12" data-plot-event-target="merge">
<label class="form-label" for="MergeTargetPlotLineID">Merge into plot line <help-icon key="sceneInspector.fields.threadTarget" mode="inline" /></label>
<select class="form-select form-select-sm" id="MergeTargetPlotLineID" data-thread-event-target-merge>
<option value="">Choose merge target</option>
@foreach (var plotLine in Model.PlotLineEventOptions)
{
<option value="@plotLine.PlotLineID" data-target-option>@plotLine.PlotLineName</option>
}
</select>
</div>
<div class="col-12" data-plot-event-target="split">
<label class="form-label" for="SelectedTargetPlotLineIDs">Target plot lines <help-icon key="sceneInspector.fields.threadTarget" mode="inline" /></label>
<select class="form-select form-select-sm" id="SelectedTargetPlotLineIDs" name="SelectedTargetPlotLineIDs" multiple size="4" data-thread-event-target-split>
@foreach (var plotLine in Model.PlotLineEventOptions)
{
<option value="@plotLine.PlotLineID" data-target-option>@plotLine.PlotLineName</option>
}
</select>
</div>
<div class="col-12">
<label class="form-label" for="EventTypeID">Marker type <help-icon key="sceneInspector.fields.threadMarkerType" mode="inline" /></label>
<select class="form-select form-select-sm" id="EventTypeID" name="EventTypeID" asp-items="Model.ThreadEventTypeOptions"></select>
</div>
<div class="col-12">
<label class="form-label" for="EventTitle">Event title</label>
<input class="form-control form-control-sm" id="EventTitle" name="EventTitle" placeholder="Defaults to event type if blank" />
</div>
<div class="col-12">
<label class="form-label" for="EventDescription">Description</label>
<textarea class="form-control form-control-sm" id="EventDescription" name="EventDescription" rows="3"></textarea>
</div>
</div>
<button class="btn btn-outline-primary btn-sm mt-2" type="submit">Add thread event</button>
</form>
<script>
(() => {
const section = document.currentScript?.closest("[data-section-id='inspector-thread-events']");
section?.querySelectorAll("[data-thread-event-form]").forEach(form => {
const plotEventType = form.querySelector("[data-plot-event-type]");
const sourcePlotLine = form.querySelector("[data-thread-event-source-plot-line]");
const sourceThread = form.querySelector("[data-thread-event-source-thread]");
const branchSection = form.querySelector("[data-plot-event-target='branch']");
const mergeSection = form.querySelector("[data-plot-event-target='merge']");
const splitSection = form.querySelector("[data-plot-event-target='split']");
const newThreadFields = form.querySelectorAll("[data-thread-event-new-thread-field]");
const branchTarget = form.querySelector("[data-thread-event-target-single]");
const mergeTarget = form.querySelector("[data-thread-event-target-merge]");
const splitTargets = form.querySelector("[data-thread-event-target-split]");
const eventTypeName = () => plotEventType?.selectedOptions?.[0]?.textContent?.trim().toLowerCase() || "";
const sourcePlotLineId = () => sourceThread?.selectedOptions?.[0]?.dataset.plotLineId || sourcePlotLine?.value || "";
const syncTargetOptions = () => {
const sourceId = sourcePlotLineId();
form.querySelectorAll("[data-target-option]").forEach(option => {
const isSource = sourceId && option.value === sourceId;
option.disabled = Boolean(isSource);
option.hidden = Boolean(isSource);
if (isSource) option.selected = false;
});
};
const sync = () => {
const type = eventTypeName();
const isStructural = type === "branch" || type === "merge" || type === "split";
branchSection.hidden = type !== "branch";
mergeSection.hidden = type !== "merge";
splitSection.hidden = type !== "split";
newThreadFields.forEach(field => {
field.hidden = isStructural;
field.querySelectorAll("input, select, textarea").forEach(input => input.disabled = isStructural);
});
if (branchTarget) branchTarget.disabled = type !== "branch";
if (mergeTarget) mergeTarget.disabled = type !== "merge";
if (splitTargets) splitTargets.disabled = type !== "split";
if (branchTarget) branchTarget.name = type === "branch" ? "TargetPlotLineID" : "";
if (mergeTarget) mergeTarget.name = type === "merge" ? "TargetPlotLineID" : "";
syncTargetOptions();
};
form.addEventListener("submit", () => {
form.querySelectorAll("[data-split-target-mirror]").forEach(input => input.remove());
if (eventTypeName() !== "split" || !splitTargets) return;
[...splitTargets.selectedOptions].forEach(option => {
if (!option.value || option.disabled) return;
const mirror = document.createElement("input");
mirror.type = "hidden";
mirror.name = "SelectedTargetPlotLineIDs";
mirror.value = option.value;
mirror.dataset.splitTargetMirror = "true";
form.append(mirror);
});
});
plotEventType?.addEventListener("change", sync);
sourcePlotLine?.addEventListener("change", sync);
sourceThread?.addEventListener("change", sync);
sync();
});
})();
</script>
</section>
<section class="inspector-section asset-events-section" data-section-title="Story Assets" data-section-id="inspector-asset-events">
<h3>Story Assets <help-icon key="sceneInspector.assets" /></h3>
@if (!Model.AssetEvents.Any())
{
<p class="muted">No asset events attached to this scene yet. Add one when an object, clue, secret, or important item changes here.</p>
}
else
{
<div class="asset-event-list">
@foreach (var assetEvent in Model.AssetEvents)
{
<div class="asset-event-item">
<div>
<span class="asset-event-marker">@assetEvent.MarkerText</span>
<strong>@assetEvent.EventTitle</strong>
<span class="muted">@assetEvent.AssetName / @assetEvent.AssetEventTypeName</span>
@if (!string.IsNullOrWhiteSpace(assetEvent.ToStateName))
{
<span class="muted">State: @(assetEvent.FromStateName ?? "Any") -> @assetEvent.ToStateName</span>
}
</div>
<form asp-controller="Scenes" asp-action="DeleteAssetEvent" method="post">
<input type="hidden" name="id" value="@assetEvent.AssetEventID" />
<input type="hidden" name="sceneId" value="@Model.SceneID" />
<input type="hidden" name="projectId" value="@Model.ReturnProjectID" />
<input type="hidden" name="bookId" value="@Model.ReturnBookID" />
<button class="btn btn-outline-secondary btn-sm" type="submit">Remove</button>
</form>
</div>
}
</div>
}
<form asp-controller="Scenes" asp-action="AddAssetEvent" method="post" class="asset-create-form">
<input type="hidden" name="SceneID" value="@Model.SceneID" />
<input type="hidden" name="ReturnProjectID" value="@Model.ReturnProjectID" />
<input type="hidden" name="ReturnBookID" value="@Model.ReturnBookID" />
<p class="form-text mb-2">Choose an existing asset, or enter a new asset name to create it from this scene.</p>
<div class="row g-2">
<div class="col-12">
<label class="form-label" for="StoryAssetID">Asset <help-icon key="sceneInspector.fields.assetSelector" mode="inline" /></label>
<select class="form-select form-select-sm" id="StoryAssetID" name="StoryAssetID" asp-items="Model.AssetOptions">
<option value="">Create or choose an asset</option>
</select>
</div>
<div class="col-8">
<label class="form-label" for="NewAssetName">New asset name</label>
<input class="form-control form-control-sm" id="NewAssetName" name="NewAssetName" placeholder="Optional" />
</div>
<div class="col-4">
<label class="form-label" for="NewAssetKindID">Kind <help-icon key="sceneInspector.fields.assetKind" mode="inline" /></label>
<select class="form-select form-select-sm" id="NewAssetKindID" name="NewAssetKindID" asp-items="Model.AssetKindOptions"></select>
</div>
<div class="col-12">
<label class="form-label" for="AssetEventTypeID">Event type <help-icon key="sceneInspector.fields.assetEventType" mode="inline" /></label>
<select class="form-select form-select-sm" id="AssetEventTypeID" name="AssetEventTypeID" asp-items="Model.AssetEventTypeOptions"></select>
</div>
<div class="col-6">
<label class="form-label" for="FromStateID">From state <help-icon key="sceneInspector.fields.assetState" mode="inline" /></label>
<select class="form-select form-select-sm" id="FromStateID" name="FromStateID" asp-items="Model.AssetStateOptions">
<option value="">Any</option>
</select>
</div>
<div class="col-6">
<label class="form-label" for="ToStateID">To state <help-icon key="sceneInspector.fields.assetState" mode="inline" /></label>
<select class="form-select form-select-sm" id="ToStateID" name="ToStateID" asp-items="Model.AssetStateOptions">
<option value="">No change</option>
</select>
</div>
<div class="col-12">
<label class="form-label" for="AssetEventTitle">Event title</label>
<input class="form-control form-control-sm" id="AssetEventTitle" name="EventTitle" placeholder="Defaults to event type if blank" />
</div>
<div class="col-12">
<label class="form-label" for="AssetEventDescription">Description</label>
<textarea class="form-control form-control-sm" id="AssetEventDescription" name="EventDescription" rows="3"></textarea>
</div>
</div>
<button class="btn btn-outline-primary btn-sm mt-2" type="submit">Add asset event</button>
</form>
</section>
<section class="inspector-section asset-events-section" data-section-title="Asset Locations" data-section-id="inspector-asset-locations">
<h3>Asset Locations <help-icon key="sceneInspector.locations" /></h3>
@if (!Model.SceneAssetLocations.Any())
{
<p class="muted">No assets have been placed in a location for this scene yet.</p>
}
else
{
<div class="asset-event-list">
@foreach (var assetLocation in Model.SceneAssetLocations)
{
<div class="asset-event-item">
<div>
<strong>@assetLocation.AssetName</strong>
<span class="muted" title="@assetLocation.LocationPath">@assetLocation.LocationName</span>
<span class="muted">@assetLocation.Description</span>
</div>
<form asp-controller="Scenes" asp-action="DeleteSceneAssetLocation" method="post">
<input type="hidden" name="id" value="@assetLocation.SceneAssetLocationID" />
<input type="hidden" name="sceneId" value="@Model.SceneID" />
<input type="hidden" name="projectId" value="@Model.ReturnProjectID" />
<input type="hidden" name="bookId" value="@Model.ReturnBookID" />
<button class="btn btn-outline-secondary btn-sm" type="submit">Remove</button>
</form>
</div>
}
</div>
}
<form asp-controller="Scenes" asp-action="AddSceneAssetLocation" method="post" class="asset-create-form">
<input type="hidden" name="SceneID" value="@Model.SceneID" />
<input type="hidden" name="ReturnProjectID" value="@Model.ReturnProjectID" />
<input type="hidden" name="ReturnBookID" value="@Model.ReturnBookID" />
<div class="row g-2">
<div class="col-12">
<label class="form-label" for="SceneAssetStoryAssetID">Asset <help-icon key="sceneInspector.fields.assetSelector" mode="inline" /></label>
<select class="form-select form-select-sm" id="SceneAssetStoryAssetID" name="StoryAssetID" asp-items="Model.AssetOptions"></select>
</div>
<div class="col-12">
<label class="form-label" for="SceneAssetLocationID">Location <help-icon key="sceneInspector.fields.assetLocation" mode="inline" /></label>
<select class="form-select form-select-sm" id="SceneAssetLocationID" name="LocationID" asp-items="Model.LocationOptions"></select>
</div>
<div class="col-12">
<input class="form-control form-control-sm" name="Description" placeholder="e.g. desk drawer, Beth's handbag" />
</div>
<div class="col-12">
<label class="form-check">
<input class="form-check-input" name="UpdateCurrentLocation" type="checkbox" value="true" checked />
<span class="form-check-label">Update asset's current location <help-icon key="sceneInspector.fields.updateAssetLocation" mode="inline" /></span>
</label>
</div>
</div>
<button class="btn btn-outline-primary btn-sm mt-2" type="submit">Place asset</button>
</form>
</section>
<section class="inspector-section asset-events-section" data-section-title="Occupancy" data-section-id="inspector-occupancy">
<h3>Occupancy <help-icon key="sceneInspector.occupancy" /></h3>
<partial name="_SceneOccupancy" model="Model.Occupancy" />
</section>
<section class="inspector-section asset-events-section" data-section-title="Asset Custody" data-section-id="inspector-asset-custody">
<h3>Asset Custody <help-icon key="sceneInspector.custody" /></h3>
@if (!Model.AssetCustodyEvents.Any())
{
<p class="muted">No custody entries attached to this scene yet.</p>
}
else
{
<div class="asset-event-list">
@foreach (var custody in Model.AssetCustodyEvents)
{
<div class="asset-event-item custody">
<div>
<strong>@custody.CustodyEventTypeName</strong>
<span class="muted">@custody.AssetName</span>
<span class="muted">@custody.CustodianSummary</span>
</div>
<form asp-controller="Scenes" asp-action="DeleteAssetCustodyEvent" method="post">
<input type="hidden" name="id" value="@custody.AssetCustodyEventID" />
<input type="hidden" name="sceneId" value="@Model.SceneID" />
<input type="hidden" name="projectId" value="@Model.ReturnProjectID" />
<input type="hidden" name="bookId" value="@Model.ReturnBookID" />
<button class="btn btn-outline-secondary btn-sm" type="submit">Remove</button>
</form>
</div>
}
</div>
}
<form asp-controller="Scenes" asp-action="AddAssetCustodyEvent" method="post" class="asset-create-form">
<input type="hidden" name="SceneID" value="@Model.SceneID" />
<input type="hidden" name="ReturnProjectID" value="@Model.ReturnProjectID" />
<input type="hidden" name="ReturnBookID" value="@Model.ReturnBookID" />
<div class="row g-2">
<div class="col-12">
<label class="form-label" for="CustodyStoryAssetID">Asset <help-icon key="sceneInspector.fields.assetSelector" mode="inline" /></label>
<select class="form-select form-select-sm" id="CustodyStoryAssetID" name="StoryAssetID" asp-items="Model.AssetOptions"></select>
</div>
<div class="col-6">
<label class="form-label" for="AssetCustodyEventTypeID">Custody event <help-icon key="sceneInspector.fields.custodyEventType" mode="inline" /></label>
<select class="form-select form-select-sm" id="AssetCustodyEventTypeID" name="AssetCustodyEventTypeID" asp-items="Model.AssetCustodyEventTypeOptions"></select>
</div>
<div class="col-6">
<label class="form-label" for="CustodyRoleID">Role <help-icon key="sceneInspector.fields.custodyRole" mode="inline" /></label>
<select class="form-select form-select-sm" id="CustodyRoleID" name="CustodyRoleID" asp-items="Model.CustodyRoleOptions"></select>
</div>
<div class="col-12">
<label class="form-label" for="CustodianCharacterIds">Existing custodians <help-icon key="sceneInspector.fields.custodians" mode="inline" /></label>
<select class="form-select form-select-sm" id="CustodianCharacterIds" name="CustodianCharacterIds" asp-items="Model.CharacterOptions" multiple></select>
</div>
<div class="col-12">
<label class="form-label" for="CustodianNames">Custodian names</label>
<input class="form-control form-control-sm" id="CustodianNames" name="CustodianNames" placeholder="Beth, Maggie" />
</div>
<div class="col-12">
<label class="form-label" for="CustodyDescription">Description</label>
<textarea class="form-control form-control-sm" id="CustodyDescription" name="Description" rows="3"></textarea>
</div>
</div>
<button class="btn btn-outline-primary btn-sm mt-2" type="submit">Add custody</button>
</form>
</section>
<section class="inspector-section" data-section-title="Story State" data-section-id="inspector-story-state" data-inspector-search-text="@($"Story State {string.Join(' ', Model.StoryState.Characters.Select(x => $"{x.CharacterName} {x.CurrentLocation} {x.State} {x.LastConfirmed}"))} {string.Join(' ', Model.StoryState.Assets.Select(x => $"{x.AssetName} {x.CurrentHolderOrLocation} {x.State} {x.LastConfirmed}"))}")">
<h3>Story State <help-icon key="sceneInspector.storyState" /></h3>
<h4 class="inspector-subheading">Characters</h4>
@if (!Model.StoryState.Characters.Any())
{
<p class="muted">No project characters found.</p>
}
else
{
<div class="table-responsive">
<table class="table table-sm align-middle mb-3">
<thead>
<tr>
<th>Character</th>
<th>Current Location</th>
<th>State</th>
<th>Last Confirmed</th>
</tr>
</thead>
<tbody>
@foreach (var characterState in Model.StoryState.Characters)
{
<tr>
<td>@characterState.CharacterName</td>
<td>@characterState.CurrentLocation</td>
<td><span class="badge @StoryStateBadgeClass(characterState.State)">@characterState.State</span></td>
<td>@characterState.LastConfirmed</td>
</tr>
}
</tbody>
</table>
</div>
}
<h4 class="inspector-subheading">Assets</h4>
@if (!Model.StoryState.Assets.Any())
{
<p class="muted">No project assets found.</p>
}
else
{
<div class="table-responsive">
<table class="table table-sm align-middle mb-0">
<thead>
<tr>
<th>Asset</th>
<th>Current Holder / Location</th>
<th>State</th>
<th>Last Confirmed</th>
</tr>
</thead>
<tbody>
@foreach (var assetState in Model.StoryState.Assets)
{
<tr>
<td>@assetState.AssetName</td>
<td>@assetState.CurrentHolderOrLocation</td>
<td><span class="badge @StoryStateBadgeClass(assetState.State)">@assetState.State</span></td>
<td>@assetState.LastConfirmed</td>
</tr>
}
</tbody>
</table>
</div>
}
</section>
<!-- Planning and workflow -->
<section class="inspector-section writer-workspace-section" data-section-title="Writer Workspace" data-section-id="inspector-writer">
<div class="inspector-section-heading">
<h3>Writer Workspace <help-icon key="sceneInspector.writer" /></h3>
<a class="btn btn-outline-secondary btn-sm" asp-controller="Writer" asp-action="Chapter" asp-route-id="@Model.ChapterID">Chapter workflow</a>
</div>
<form asp-controller="Scenes" asp-action="SaveWorkflow" method="post" class="writer-workspace-form">
<input type="hidden" name="SceneID" value="@Model.SceneID" />
<input type="hidden" name="ReturnProjectID" value="@Model.ReturnProjectID" />
<input type="hidden" name="ReturnBookID" value="@Model.ReturnBookID" />
<div class="row g-2">
<div class="col-12">
<label class="form-label" for="Priority">Priority <help-icon key="sceneInspector.fields.writerPriority" mode="inline" /></label>
<div class="writer-priority-control">
<input class="form-range" id="Priority" name="Priority" type="range" min="1" max="10" value="@(Model.Workflow.Priority ?? 5)" data-priority-slider />
<span class="status-pill" data-priority-label></span>
</div>
</div>
<div class="col-sm-6">
<label class="form-label" for="EstimatedWordCount">Estimated words</label>
<input class="form-control form-control-sm" id="EstimatedWordCount" name="EstimatedWordCount" type="number" min="0" value="@Model.Workflow.EstimatedWordCount" />
</div>
<div class="col-sm-6">
<label class="form-label" for="ActualWordCount">Actual words</label>
<input class="form-control form-control-sm" id="ActualWordCount" name="ActualWordCount" type="number" min="0" value="@Model.Workflow.ActualWordCount" />
</div>
<div class="col-sm-6">
<label class="form-label" for="DraftedDate">Drafted date</label>
<input class="form-control form-control-sm" id="DraftedDate" name="DraftedDate" type="date" value="@Model.Workflow.DraftedDate?.ToString("yyyy-MM-dd")" />
</div>
<div class="col-sm-6">
<label class="form-label" for="LastWorkedOn">Last worked on</label>
<input class="form-control form-control-sm" id="LastWorkedOn" name="LastWorkedOn" type="date" value="@Model.Workflow.LastWorkedOn?.ToString("yyyy-MM-dd")" />
</div>
<div class="col-12 writer-checkbox-grid">
<label><input type="checkbox" name="IsBlocked" value="true" checked="@Model.Workflow.IsBlocked" /> Blocked <help-icon key="sceneInspector.fields.writerBlocked" mode="inline" /></label>
</div>
<div class="col-12">
<label class="form-label" for="BlockedReason">Blocked reason</label>
<input class="form-control form-control-sm" id="BlockedReason" name="BlockedReason" value="@Model.Workflow.BlockedReason" />
</div>
</div>
<div class="button-row compact-buttons mt-2">
<button class="btn btn-primary btn-sm" type="submit">Save workflow</button>
</div>
</form>
<script>
(() => {
const slider = document.querySelector('[data-priority-slider]');
const label = document.querySelector('[data-priority-label]');
if (!slider || !label) return;
const labelFor = value => {
const priority = Number(value);
if (priority <= 2) return `${priority} - Very Low`;
if (priority <= 4) return `${priority} - Low`;
if (priority <= 6) return `${priority} - Medium`;
if (priority <= 8) return `${priority} - High`;
return `${priority} - Critical`;
};
const update = () => label.textContent = labelFor(slider.value);
slider.addEventListener('input', update);
update();
})();
</script>
</section>
<section class="inspector-section writer-checklist-section" data-section-title="Scene Checklist" data-section-id="inspector-checklist">
<h3>Scene Checklist <help-icon key="sceneInspector.checklist" /></h3>
<div class="writer-checklist">
@foreach (var item in Model.ChecklistItems)
{
<form asp-controller="Scenes" asp-action="SaveChecklistItem" method="post" class="writer-checklist-row">
<input type="hidden" name="SceneChecklistItemID" value="@item.SceneChecklistItemID" />
<input type="hidden" name="SceneID" value="@Model.SceneID" />
<input type="hidden" name="ReturnProjectID" value="@Model.ReturnProjectID" />
<input type="hidden" name="ReturnBookID" value="@Model.ReturnBookID" />
<label class="visually-hidden">Checklist completion <help-icon key="sceneInspector.fields.checklistCompletion" mode="inline" /></label>
<input type="checkbox" name="IsCompleted" value="true" checked="@item.IsCompleted" title="Checklist completion" />
<input class="form-control form-control-sm" name="ItemText" value="@item.ItemText" />
<button class="btn btn-outline-primary btn-sm" type="submit">Save</button>
</form>
<form asp-controller="Scenes" asp-action="DeleteChecklistItem" method="post" class="writer-remove-inline">
<input type="hidden" name="id" value="@item.SceneChecklistItemID" />
<input type="hidden" name="sceneId" value="@Model.SceneID" />
<input type="hidden" name="projectId" value="@Model.ReturnProjectID" />
<input type="hidden" name="bookId" value="@Model.ReturnBookID" />
<button class="btn btn-outline-danger btn-sm" type="submit">Remove</button>
</form>
}
</div>
<form asp-controller="Scenes" asp-action="SaveChecklistItem" method="post" class="writer-add-form writer-add-inline">
<input type="hidden" name="SceneID" value="@Model.SceneID" />
<input type="hidden" name="ReturnProjectID" value="@Model.ReturnProjectID" />
<input type="hidden" name="ReturnBookID" value="@Model.ReturnBookID" />
<input class="form-control form-control-sm" name="ItemText" placeholder="Add checklist item" />
<button class="btn btn-outline-primary btn-sm" type="submit">Add item</button>
</form>
</section>
<section class="inspector-section writer-attachments-section" data-section-title="Attachments / Research" data-section-id="inspector-attachments">
<h3>Attachments / Research <help-icon key="sceneInspector.attachments" /></h3>
@if (Model.StorageUsage is not null)
{
<p class="muted">Storage used: @Model.StorageUsage.DisplayLabel</p>
}
@if (!Model.Attachments.Any())
{
<p class="muted">No scene references yet. Add images, links, floorplans, research notes, or files that help you write this scene.</p>
}
else
{
<div class="attachment-card-list">
@foreach (var attachment in Model.Attachments)
{
var hasFile = !string.IsNullOrWhiteSpace(attachment.FilePath);
var fileHref = hasFile && attachment.FilePath!.StartsWith("http", StringComparison.OrdinalIgnoreCase)
? attachment.FilePath
: hasFile && attachment.FilePath!.StartsWith("/")
? attachment.FilePath
: hasFile && !System.IO.Path.IsPathRooted(attachment.FilePath!)
? "/" + attachment.FilePath!.TrimStart('/', '\\')
: null;
var fileExtension = hasFile ? System.IO.Path.GetExtension(attachment.FilePath!) : string.Empty;
var isImageAttachment = !string.IsNullOrWhiteSpace(fileHref)
&& new[] { ".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp" }.Contains(fileExtension, StringComparer.OrdinalIgnoreCase);
<details class="writer-mini-card attachment-card">
<summary class="attachment-summary">
<span class="attachment-kind">@(hasFile ? "File" : "Link")</span>
<span class="status-pill">@attachment.TypeName</span>
<strong>@attachment.Title</strong>
</summary>
<div class="attachment-card-body">
<div class="attachment-preview-row">
@if (isImageAttachment)
{
<a class="attachment-thumb" href="@fileHref" target="_blank" rel="noopener" aria-label="Open @attachment.Title image">
<img src="@fileHref" alt="@attachment.Title preview" loading="lazy" />
</a>
}
else
{
<div class="attachment-thumb attachment-thumb-fallback" aria-hidden="true">
<span>@(hasFile ? "File" : "Link")</span>
</div>
}
<div class="attachment-preview-copy">
@if (!string.IsNullOrWhiteSpace(attachment.Notes))
{
<p>@attachment.Notes</p>
}
@if (hasFile)
{
<p class="muted">@System.IO.Path.GetFileName(attachment.FilePath)</p>
}
<div class="button-row compact-buttons">
@if (!string.IsNullOrWhiteSpace(fileHref))
{
<a class="btn btn-outline-secondary btn-sm" href="@fileHref" target="_blank" rel="noopener">Open</a>
<a class="btn btn-outline-secondary btn-sm" href="@fileHref" download>Download</a>
}
@if (!string.IsNullOrWhiteSpace(attachment.ExternalUrl))
{
<a class="btn btn-outline-secondary btn-sm" href="@attachment.ExternalUrl" target="_blank" rel="noopener">Open link</a>
}
</div>
</div>
</div>
</div>
<form asp-controller="Scenes" asp-action="SaveAttachment" method="post" enctype="multipart/form-data" class="writer-edit-form">
<input type="hidden" name="SceneAttachmentID" value="@attachment.SceneAttachmentID" />
<input type="hidden" name="SceneID" value="@Model.SceneID" />
<input type="hidden" name="ReturnProjectID" value="@Model.ReturnProjectID" />
<input type="hidden" name="ReturnBookID" value="@Model.ReturnBookID" />
<input type="hidden" name="FilePath" value="@attachment.FilePath" />
<label class="form-label mb-0">Attachment type <help-icon key="sceneInspector.fields.attachmentType" mode="inline" /></label>
<select class="form-select form-select-sm" name="SceneAttachmentTypeID">
@foreach (var option in Model.SceneAttachmentTypeOptions)
{
<option value="@option.Value" selected="@(option.Value == attachment.SceneAttachmentTypeID.ToString())">@option.Text</option>
}
</select>
<input class="form-control form-control-sm" name="Title" value="@attachment.Title" />
<label class="form-label mb-0">Replace file</label>
<input class="form-control form-control-sm" name="UploadedFile" type="file" />
<input class="form-control form-control-sm" name="ExternalUrl" value="@attachment.ExternalUrl" placeholder="Or external URL" />
<textarea class="form-control form-control-sm" name="Notes" rows="2">@attachment.Notes</textarea>
<button class="btn btn-primary btn-sm" type="submit">Save attachment</button>
</form>
<form asp-controller="Scenes" asp-action="DeleteAttachment" method="post" class="mt-1">
<input type="hidden" name="id" value="@attachment.SceneAttachmentID" />
<input type="hidden" name="sceneId" value="@Model.SceneID" />
<input type="hidden" name="projectId" value="@Model.ReturnProjectID" />
<input type="hidden" name="bookId" value="@Model.ReturnBookID" />
<input type="hidden" name="filePath" value="@attachment.FilePath" />
<button class="btn btn-outline-danger btn-sm" type="submit">Remove attachment</button>
</form>
</details>
}
</div>
}
<details class="writer-mini-card attachment-add-card">
<summary><strong>+ Add Attachment</strong></summary>
<form asp-controller="Scenes" asp-action="SaveAttachment" method="post" enctype="multipart/form-data" class="writer-add-form">
<input type="hidden" name="SceneID" value="@Model.SceneID" />
<input type="hidden" name="ReturnProjectID" value="@Model.ReturnProjectID" />
<input type="hidden" name="ReturnBookID" value="@Model.ReturnBookID" />
<label class="form-label mb-0">Attachment type <help-icon key="sceneInspector.fields.attachmentType" mode="inline" /></label>
<select class="form-select form-select-sm" name="SceneAttachmentTypeID" asp-items="Model.SceneAttachmentTypeOptions"></select>
<label class="form-label mb-0">Title</label>
<input class="form-control form-control-sm" name="Title" placeholder="Reference title" />
<label class="form-label mb-0">Choose file</label>
<input class="form-control form-control-sm" name="UploadedFile" type="file" />
<label class="form-label mb-0">Or external URL</label>
<input class="form-control form-control-sm" name="ExternalUrl" placeholder="https://..." />
<textarea class="form-control form-control-sm" name="Notes" rows="2" placeholder="Why this reference matters"></textarea>
<button class="btn btn-outline-primary btn-sm" type="submit">Add attachment</button>
</form>
</details>
</section>
<!-- Validation -->
<section class="inspector-section warning-inspector-section" data-section-title="Continuity Warnings" data-section-id="inspector-warnings">
<div class="inspector-section-heading">
<h3>Continuity Warnings <help-icon key="sceneInspector.warnings" /></h3>
@if (Model.SceneID > 0 && Model.ReturnProjectID.HasValue)
{
<form asp-controller="Warnings" asp-action="ValidateScene" method="post">
<input type="hidden" name="projectId" value="@Model.ReturnProjectID" />
<input type="hidden" name="bookId" value="@Model.ReturnBookID" />
<input type="hidden" name="sceneId" value="@Model.SceneID" />
<input type="hidden" name="returnToTimeline" value="@Model.ReturnToTimeline.ToString().ToLowerInvariant()" />
<button class="btn btn-outline-primary btn-sm" type="submit">Validate scene</button>
<help-icon key="sceneInspector.fields.warningActions" mode="inline" />
</form>
}
</div>
@if (!Model.Warnings.Any() && !Model.AgeContinuityWarnings.Any())
{
<p class="muted">No active warnings for this scene. Run validation again after major story changes.</p>
}
else
{
<div class="warning-mini-list">
@foreach (var warning in Model.Warnings)
{
<div class="warning-mini-card">
<span class="severity-pill @warning.SeverityName.ToLowerInvariant()">@warning.SeverityName</span>
<strong>@warning.WarningTypeName</strong>
<p>@warning.Message</p>
<div class="button-row compact-buttons">
<form asp-controller="Warnings" asp-action="Dismiss" method="post">
<input type="hidden" name="id" value="@warning.ContinuityWarningID" />
<input type="hidden" name="projectId" value="@Model.ReturnProjectID" />
<input type="hidden" name="bookId" value="@Model.ReturnBookID" />
<input type="hidden" name="sceneId" value="@Model.SceneID" />
<input type="hidden" name="returnToTimeline" value="@Model.ReturnToTimeline.ToString().ToLowerInvariant()" />
<button class="btn btn-outline-secondary btn-sm" type="submit">Dismiss</button>
<help-icon key="sceneInspector.fields.warningActions" mode="inline" />
</form>
<form asp-controller="Warnings" asp-action="MarkIntentional" method="post">
<input type="hidden" name="id" value="@warning.ContinuityWarningID" />
<input type="hidden" name="projectId" value="@Model.ReturnProjectID" />
<input type="hidden" name="bookId" value="@Model.ReturnBookID" />
<input type="hidden" name="sceneId" value="@Model.SceneID" />
<input type="hidden" name="returnToTimeline" value="@Model.ReturnToTimeline.ToString().ToLowerInvariant()" />
<button class="btn btn-outline-secondary btn-sm" type="submit">Intentional</button>
<help-icon key="sceneInspector.fields.warningActions" mode="inline" />
</form>
</div>
</div>
}
@foreach (var warning in Model.AgeContinuityWarnings)
{
<div class="warning-mini-card">
<span class="severity-pill @warning.SeverityName.ToLowerInvariant()">@warning.SeverityName</span>
<strong>@warning.WarningTypeName</strong>
<p>@warning.Message</p>
@if (!string.IsNullOrWhiteSpace(warning.Details))
{
<p class="muted">@warning.Details</p>
}
</div>
}
</div>
}
</section>
<section class="inspector-section scene-dependency-section" data-section-title="Scene Dependencies" data-section-id="inspector-dependencies">
<h3>Scene Dependencies <help-icon key="sceneInspector.dependencies" /></h3>
<div class="dependency-columns">
<div>
<p class="eyebrow">This scene depends on</p>
@if (!Model.DependenciesThisSceneNeeds.Any())
{
<p class="muted">No dependencies yet. Add one when this scene needs another moment to happen first.</p>
}
else
{
@foreach (var dependency in Model.DependenciesThisSceneNeeds)
{
<div class="dependency-card">
<strong>Ch @dependency.SourceChapterNumber, Scene @dependency.SourceSceneNumber</strong>
<span>@dependency.SourceSceneTitle</span>
<span class="muted">@dependency.SceneDependencyTypeName @(dependency.IsHardDependency ? "/ hard" : "/ soft")</span>
<form asp-controller="Scenes" asp-action="DeleteSceneDependency" method="post">
<input type="hidden" name="id" value="@dependency.SceneDependencyID" />
<input type="hidden" name="sceneId" value="@Model.SceneID" />
<input type="hidden" name="projectId" value="@Model.ReturnProjectID" />
<input type="hidden" name="bookId" value="@Model.ReturnBookID" />
<button class="btn btn-outline-secondary btn-sm" type="submit">Remove</button>
</form>
</div>
}
}
</div>
<div>
<p class="eyebrow">Scenes depending on this</p>
@if (!Model.DependenciesNeedingThisScene.Any())
{
<p class="muted">No dependent scenes yet. Later scenes that rely on this one will appear here.</p>
}
else
{
@foreach (var dependency in Model.DependenciesNeedingThisScene)
{
<div class="dependency-card">
<strong>Ch @dependency.TargetChapterNumber, Scene @dependency.TargetSceneNumber</strong>
<span>@dependency.TargetSceneTitle</span>
<span class="muted">@dependency.SceneDependencyTypeName @(dependency.IsHardDependency ? "/ hard" : "/ soft")</span>
<form asp-controller="Scenes" asp-action="DeleteSceneDependency" method="post">
<input type="hidden" name="id" value="@dependency.SceneDependencyID" />
<input type="hidden" name="sceneId" value="@Model.SceneID" />
<input type="hidden" name="projectId" value="@Model.ReturnProjectID" />
<input type="hidden" name="bookId" value="@Model.ReturnBookID" />
<button class="btn btn-outline-secondary btn-sm" type="submit">Remove</button>
</form>
</div>
}
}
</div>
</div>
<form asp-controller="Scenes" asp-action="AddSceneDependency" method="post" class="asset-create-form mt-2">
<input type="hidden" name="ProjectID" value="@Model.ReturnProjectID" />
<input type="hidden" name="CurrentSceneID" value="@Model.SceneID" />
<input type="hidden" name="ReturnProjectID" value="@Model.ReturnProjectID" />
<input type="hidden" name="ReturnBookID" value="@Model.ReturnBookID" />
<div class="row g-2">
<div class="col-12">
<label class="form-label" for="DependencyDirection">Plain English <help-icon key="sceneInspector.fields.dependencyControls" mode="inline" /></label>
<select class="form-select form-select-sm" id="DependencyDirection" name="Direction">
<option value="CurrentDependsOnRelated">This scene depends on another scene</option>
<option value="RelatedDependsOnCurrent">Another scene depends on this scene</option>
</select>
</div>
<div class="col-12">
<label class="form-label" for="RelatedSceneID">Related scene <help-icon key="sceneInspector.fields.dependencyControls" mode="inline" /></label>
<select class="form-select form-select-sm" id="RelatedSceneID" name="RelatedSceneID" asp-items="Model.SceneOptions"></select>
</div>
<div class="col-8">
<label class="form-label" for="SceneDependencyTypeID">Dependency type <help-icon key="sceneInspector.fields.dependencyControls" mode="inline" /></label>
<select class="form-select form-select-sm" id="SceneDependencyTypeID" name="SceneDependencyTypeID" asp-items="Model.SceneDependencyTypeOptions"></select>
</div>
<div class="col-4 d-flex align-items-end">
<label class="form-check mb-2">
<input class="form-check-input" name="IsHardDependency" type="checkbox" value="true" />
<span class="form-check-label">Hard <help-icon key="sceneInspector.fields.dependencyControls" mode="inline" /></span>
</label>
</div>
<div class="col-12">
<textarea class="form-control form-control-sm" name="Description" rows="2" placeholder="Why this order matters"></textarea>
</div>
</div>
<button class="btn btn-outline-primary btn-sm mt-2" type="submit">Add dependency</button>
</form>
</section>
<!-- Management -->
<section class="inspector-section scene-move-section" data-section-title="Move / Reorder" data-section-id="inspector-move">
<h3>Move / Reorder <help-icon key="sceneInspector.move" /></h3>
<form asp-controller="Scenes" asp-action="PreviewMove" method="post" class="asset-create-form">
<input type="hidden" name="SceneID" value="@Model.SceneID" />
<input type="hidden" name="ReturnProjectID" value="@Model.ReturnProjectID" />
<input type="hidden" name="ReturnBookID" value="@Model.ReturnBookID" />
<div class="row g-2">
<div class="col-12">
<label class="form-label" for="TargetChapterID">Move to chapter <help-icon key="sceneInspector.fields.moveControls" mode="inline" /></label>
<select class="form-select form-select-sm" id="TargetChapterID" name="TargetChapterID" asp-items="Model.ChapterOptions"></select>
</div>
<div class="col-6">
<label class="form-label" for="MovePosition">Position <help-icon key="sceneInspector.fields.moveControls" mode="inline" /></label>
<select class="form-select form-select-sm" id="MovePosition" name="Position">
<option value="End">End of chapter</option>
<option value="Start">Start of chapter</option>
</select>
</div>
<div class="col-6 d-flex align-items-end">
<label class="form-check mb-2">
<input class="form-check-input" name="RenumberScenes" type="checkbox" value="true" />
<span class="form-check-label">Renumber scenes <help-icon key="sceneInspector.fields.moveControls" mode="inline" /></span>
</label>
</div>
</div>
<button class="btn btn-outline-primary btn-sm mt-2" type="submit">Preview move</button>
</form>
<form asp-controller="Scenes" asp-action="RenumberChapter" method="post" class="mt-2">
<input type="hidden" name="chapterId" value="@Model.ChapterID" />
<input type="hidden" name="projectId" value="@Model.ReturnProjectID" />
<input type="hidden" name="bookId" value="@Model.ReturnBookID" />
<input type="hidden" name="sceneId" value="@Model.SceneID" />
<button class="btn btn-outline-secondary btn-sm" type="submit">Renumber this chapter</button>
</form>
</section>
</div>