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 logger) : Controller { [HttpGet("Context")] public async Task 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 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 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 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 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 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 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 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" : "")}."; } }