257 lines
10 KiB
C#
257 lines
10 KiB
C#
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
|
using PlotLine.Models;
|
|
using PlotLine.Services;
|
|
using PlotLine.ViewModels;
|
|
|
|
namespace PlotLine.Controllers;
|
|
|
|
[Authorize]
|
|
[Route("Writing")]
|
|
public sealed class WritingController(
|
|
IStoryContextService storyContext,
|
|
IRelationshipMapService relationshipMap,
|
|
IFloorPlanService floorPlans,
|
|
IWriterWorkspaceService writerWorkspace,
|
|
ILogger<WritingController> logger) : Controller
|
|
{
|
|
[HttpGet("Context")]
|
|
public async Task<IActionResult> Context(int? projectId, int? bookId, int? chapterId, int? sceneId, Guid? documentGuid)
|
|
{
|
|
logger.LogInformation(
|
|
"Opening Writing Context. ProjectID={ProjectID} BookID={BookID} ChapterID={ChapterID} SceneID={SceneID} DocumentGuidPresent={DocumentGuidPresent}",
|
|
projectId,
|
|
bookId,
|
|
chapterId,
|
|
sceneId,
|
|
documentGuid.HasValue);
|
|
|
|
var display = sceneId.HasValue
|
|
? await BuildDisplayAsync(projectId, bookId, chapterId, sceneId, "Manual context")
|
|
: WaitingContextDisplay(projectId, bookId, documentGuid);
|
|
|
|
var model = new WritingContextPageViewModel
|
|
{
|
|
ProjectID = projectId,
|
|
BookID = bookId,
|
|
ChapterID = chapterId,
|
|
SceneID = sceneId,
|
|
DocumentGuid = documentGuid,
|
|
Display = display ?? MissingContextDisplay(projectId, bookId, sceneId)
|
|
};
|
|
|
|
return View(model);
|
|
}
|
|
|
|
[HttpGet("Intent")]
|
|
public async Task<IActionResult> Intent(int? projectId, int? bookId, int? chapterId, int? sceneId, Guid? documentGuid)
|
|
{
|
|
logger.LogInformation(
|
|
"Opening Writing Intent. ProjectID={ProjectID} BookID={BookID} ChapterID={ChapterID} SceneID={SceneID} DocumentGuidPresent={DocumentGuidPresent}",
|
|
projectId,
|
|
bookId,
|
|
chapterId,
|
|
sceneId,
|
|
documentGuid.HasValue);
|
|
|
|
var display = sceneId.HasValue || chapterId.HasValue
|
|
? await BuildIntentDisplayAsync(projectId, bookId, chapterId, sceneId, "Manual intent")
|
|
: WaitingIntentDisplay(projectId, bookId, documentGuid);
|
|
|
|
var model = new WritingIntentPageViewModel
|
|
{
|
|
ProjectID = projectId,
|
|
BookID = bookId,
|
|
ChapterID = chapterId,
|
|
SceneID = sceneId,
|
|
DocumentGuid = documentGuid,
|
|
Display = display ?? MissingIntentDisplay(projectId, bookId, chapterId, sceneId)
|
|
};
|
|
|
|
return View(model);
|
|
}
|
|
|
|
[HttpGet("Context/Scene")]
|
|
public async Task<IActionResult> ContextScene(int? projectId, int? bookId, int? chapterId, int? sceneId)
|
|
{
|
|
if (!sceneId.HasValue)
|
|
{
|
|
logger.LogWarning("Writing Context partial requested without SceneID. ProjectID={ProjectID} BookID={BookID} ChapterID={ChapterID}", projectId, bookId, chapterId);
|
|
return PartialView("~/Views/Writing/_WritingContextContent.cshtml", WaitingContextDisplay(projectId, bookId, null));
|
|
}
|
|
|
|
var model = await BuildDisplayAsync(projectId, bookId, chapterId, sceneId, "Following Word");
|
|
if (model?.HasScene == true)
|
|
{
|
|
return PartialView("~/Views/Writing/_WritingContextContent.cshtml", model);
|
|
}
|
|
|
|
logger.LogWarning("Writing Context could not resolve scene. ProjectID={ProjectID} BookID={BookID} ChapterID={ChapterID} SceneID={SceneID}", projectId, bookId, chapterId, sceneId);
|
|
return PartialView("~/Views/Writing/_WritingContextContent.cshtml", MissingContextDisplay(projectId, bookId, sceneId));
|
|
}
|
|
|
|
[HttpGet("Intent/Scene")]
|
|
public async Task<IActionResult> IntentScene(int? projectId, int? bookId, int? chapterId, int? sceneId)
|
|
{
|
|
if (!sceneId.HasValue && !chapterId.HasValue)
|
|
{
|
|
logger.LogWarning("Writing Intent partial requested without ChapterID or SceneID. ProjectID={ProjectID} BookID={BookID}", projectId, bookId);
|
|
return PartialView("~/Views/Writing/_WritingIntentContent.cshtml", WaitingIntentDisplay(projectId, bookId, null));
|
|
}
|
|
|
|
var model = await BuildIntentDisplayAsync(projectId, bookId, chapterId, sceneId, "Following Word");
|
|
if (model is not null && (model.HasScene || model.HasChapter))
|
|
{
|
|
return PartialView("~/Views/Writing/_WritingIntentContent.cshtml", model);
|
|
}
|
|
|
|
logger.LogWarning("Writing Intent could not resolve chapter/scene. ProjectID={ProjectID} BookID={BookID} ChapterID={ChapterID} SceneID={SceneID}", projectId, bookId, chapterId, sceneId);
|
|
return PartialView("~/Views/Writing/_WritingIntentContent.cshtml", MissingIntentDisplay(projectId, bookId, chapterId, sceneId));
|
|
}
|
|
|
|
private async Task<WritingContextDisplayViewModel?> BuildDisplayAsync(int? projectId, int? bookId, int? chapterId, int? sceneId, string followStatus)
|
|
{
|
|
if (!sceneId.HasValue)
|
|
{
|
|
return new WritingContextDisplayViewModel { FollowStatus = followStatus };
|
|
}
|
|
|
|
var context = await storyContext.GetForSceneAsync(sceneId.Value, projectId, bookId);
|
|
if (context?.Scene is null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var position = context.Position;
|
|
var relationships = await relationshipMap.GetAsync(position, null, true);
|
|
var floorPlan = context.Scene.FloorPlanID.HasValue
|
|
? await floorPlans.GetEditorAsync(context.Scene.FloorPlanID.Value)
|
|
: null;
|
|
|
|
return new WritingContextDisplayViewModel
|
|
{
|
|
Context = context,
|
|
RelationshipMap = relationships,
|
|
FloorPlan = floorPlan,
|
|
FollowStatus = followStatus,
|
|
SceneOptions = BuildSceneOptions(context.Scene)
|
|
};
|
|
}
|
|
|
|
private async Task<WritingIntentDisplayViewModel?> BuildIntentDisplayAsync(int? projectId, int? bookId, int? chapterId, int? sceneId, string followStatus)
|
|
{
|
|
StoryContextViewModel? context = null;
|
|
if (sceneId.HasValue)
|
|
{
|
|
context = await storyContext.GetForSceneAsync(sceneId.Value, projectId, bookId);
|
|
if (context?.Scene is null)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
var resolvedChapterId = context?.Position.ChapterId ?? chapterId;
|
|
var chapterWorkflow = resolvedChapterId.HasValue
|
|
? await writerWorkspace.GetChapterWorkflowAsync(resolvedChapterId.Value)
|
|
: null;
|
|
|
|
if (chapterWorkflow is not null)
|
|
{
|
|
if (projectId.HasValue && chapterWorkflow.Project.ProjectID != projectId.Value)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (bookId.HasValue && chapterWorkflow.Book.BookID != bookId.Value)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
if (context is null && chapterWorkflow is null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return new WritingIntentDisplayViewModel
|
|
{
|
|
Context = context,
|
|
ChapterWorkflow = chapterWorkflow,
|
|
FollowStatus = followStatus,
|
|
SceneOptions = context?.Scene is not null
|
|
? BuildSceneOptions(context.Scene)
|
|
: BuildSceneOptions(chapterWorkflow)
|
|
};
|
|
}
|
|
|
|
private static IReadOnlyList<SelectListItem> BuildSceneOptions(SceneEditViewModel scene)
|
|
{
|
|
var options = scene.SceneOptions
|
|
.Select(x => new SelectListItem(x.Text, x.Value, x.Value == scene.SceneID.ToString()))
|
|
.ToList();
|
|
|
|
if (!options.Any(x => x.Value == scene.SceneID.ToString()))
|
|
{
|
|
options.Insert(0, new SelectListItem(
|
|
$"{scene.Book?.BookDisplayTitle ?? "Book"} / Ch {scene.Chapter?.ChapterNumber:g} / Scene {scene.SceneNumber:g}: {scene.SceneTitle}",
|
|
scene.SceneID.ToString(),
|
|
true));
|
|
}
|
|
|
|
return options
|
|
.OrderByDescending(x => x.Selected)
|
|
.ThenBy(x => x.Text)
|
|
.ToList();
|
|
}
|
|
|
|
private static IReadOnlyList<SelectListItem> BuildSceneOptions(ChapterWorkflowViewModel? chapterWorkflow)
|
|
{
|
|
return chapterWorkflow?.Scenes
|
|
.Select(x => new SelectListItem(
|
|
$"Scene {x.SceneNumber:g}: {x.SceneTitle}",
|
|
x.SceneID.ToString()))
|
|
.OrderBy(x => x.Text)
|
|
.ToList()
|
|
?? [];
|
|
}
|
|
|
|
private static WritingContextDisplayViewModel WaitingContextDisplay(int? projectId, int? bookId, Guid? documentGuid) => new()
|
|
{
|
|
FollowStatus = "No current Word scene received",
|
|
EmptyTitle = "No current Word scene received",
|
|
EmptyMessage = MissingScopeMessage(projectId, bookId, documentGuid, "Context")
|
|
};
|
|
|
|
private static WritingIntentDisplayViewModel WaitingIntentDisplay(int? projectId, int? bookId, Guid? documentGuid) => new()
|
|
{
|
|
FollowStatus = "No current Word scene received",
|
|
EmptyTitle = "No current Word scene received",
|
|
EmptyMessage = MissingScopeMessage(projectId, bookId, documentGuid, "Intent")
|
|
};
|
|
|
|
private static WritingContextDisplayViewModel MissingContextDisplay(int? projectId, int? bookId, int? sceneId) => new()
|
|
{
|
|
FollowStatus = "Context unavailable",
|
|
EmptyTitle = "No Story Intelligence context is available",
|
|
EmptyMessage = $"PlotDirector could not load context for scene {sceneId?.ToString() ?? "unknown"} in project {projectId?.ToString() ?? "unknown"}, book {bookId?.ToString() ?? "unknown"}."
|
|
};
|
|
|
|
private static WritingIntentDisplayViewModel MissingIntentDisplay(int? projectId, int? bookId, int? chapterId, int? sceneId) => new()
|
|
{
|
|
FollowStatus = "Intent unavailable",
|
|
EmptyTitle = "No writing intent is available",
|
|
EmptyMessage = $"PlotDirector could not load writing intent for chapter {chapterId?.ToString() ?? "unknown"}, scene {sceneId?.ToString() ?? "none"} in project {projectId?.ToString() ?? "unknown"}, book {bookId?.ToString() ?? "unknown"}."
|
|
};
|
|
|
|
private static string MissingScopeMessage(int? projectId, int? bookId, Guid? documentGuid, string screen)
|
|
{
|
|
if (!projectId.HasValue || !bookId.HasValue)
|
|
{
|
|
return $"{screen} did not receive a project/book from Word Companion. Reopen it from a linked manuscript, or choose a scene manually.";
|
|
}
|
|
|
|
return $"Waiting for Word Companion to report the current scene for project {projectId}, book {bookId}{(documentGuid.HasValue ? " and this Word document" : "")}.";
|
|
}
|
|
}
|