889 lines
34 KiB
C#
889 lines
34 KiB
C#
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using PlotLine.Models;
|
|
using PlotLine.Services;
|
|
using PlotLine.ViewModels;
|
|
|
|
namespace PlotLine.Controllers;
|
|
|
|
[Authorize]
|
|
[Route("onboarding")]
|
|
public sealed class OnboardingController(IOnboardingService onboarding, IOnboardingStoryIntelligenceService storyIntelligence) : Controller
|
|
{
|
|
[HttpGet("")]
|
|
public async Task<IActionResult> Index()
|
|
{
|
|
var model = await onboarding.GetWizardAsync();
|
|
return View(model);
|
|
}
|
|
|
|
[HttpPost("start-manual")]
|
|
[ValidateAntiForgeryToken]
|
|
public async Task<IActionResult> StartManual()
|
|
{
|
|
await onboarding.StartManualSetupAsync();
|
|
return RedirectToAction(nameof(Index));
|
|
}
|
|
|
|
[HttpPost("start-import")]
|
|
[ValidateAntiForgeryToken]
|
|
public async Task<IActionResult> StartImport()
|
|
{
|
|
await onboarding.StartManuscriptImportSetupAsync();
|
|
return RedirectToAction(nameof(Index));
|
|
}
|
|
|
|
[HttpGet("scan-review")]
|
|
public async Task<IActionResult> ScanReview(Guid? previewId)
|
|
{
|
|
var model = await onboarding.GetScanReviewAsync(previewId)
|
|
?? (previewId.HasValue ? await onboarding.GetScanReviewAsync() : null);
|
|
if (model is null)
|
|
{
|
|
TempData["ArchiveError"] = "The scan review could not be opened. Please scan the manuscript again.";
|
|
await onboarding.SetCurrentStepAsync(OnboardingSteps.NextPathPreview);
|
|
return RedirectToAction(nameof(Index));
|
|
}
|
|
|
|
return View(model);
|
|
}
|
|
|
|
[HttpGet("scan-characters")]
|
|
public async Task<IActionResult> ScanCharacters(Guid? previewId, int page = 1)
|
|
{
|
|
var model = await onboarding.GetScanReviewAsync(previewId)
|
|
?? (previewId.HasValue ? await onboarding.GetScanReviewAsync() : null);
|
|
if (model is null)
|
|
{
|
|
TempData["ArchiveError"] = "The character review could not be opened. Please scan the manuscript again.";
|
|
await onboarding.SetCurrentStepAsync(OnboardingSteps.NextPathPreview);
|
|
return RedirectToAction(nameof(Index));
|
|
}
|
|
|
|
var build = await onboarding.GetBuildResultAsync(model.PreviewID);
|
|
if (build is null || build.SceneMappings.Count == 0)
|
|
{
|
|
TempData["ArchiveError"] = "Import the approved chapters and scenes before reviewing characters.";
|
|
await onboarding.SetCurrentStepAsync(OnboardingSteps.NextPathPreview);
|
|
return RedirectToAction(nameof(Index));
|
|
}
|
|
|
|
ViewData["ScanReviewStep"] = "characters";
|
|
ViewData["CharacterReviewPage"] = Math.Max(1, page);
|
|
return View("ScanReview", model);
|
|
}
|
|
|
|
[HttpGet("build-complete")]
|
|
public async Task<IActionResult> BuildComplete(Guid previewId)
|
|
{
|
|
var model = await onboarding.GetBuildResultAsync(previewId);
|
|
return model is null ? NotFound() : View(model);
|
|
}
|
|
|
|
[HttpGet("story-intelligence")]
|
|
public async Task<IActionResult> StoryIntelligence()
|
|
{
|
|
var model = await storyIntelligence.GetOverviewAsync();
|
|
return View(model);
|
|
}
|
|
|
|
[HttpGet("story-intelligence/context")]
|
|
public async Task<IActionResult> StoryIntelligenceContext()
|
|
{
|
|
var model = await storyIntelligence.GetOverviewAsync();
|
|
return View(model);
|
|
}
|
|
|
|
[HttpGet("story-intelligence/book/{bookId:int}/continue")]
|
|
public async Task<IActionResult> ContinueStoryIntelligence(int bookId)
|
|
{
|
|
var target = await storyIntelligence.ResumeBookAsync(bookId);
|
|
if (target is null)
|
|
{
|
|
TempData["ArchiveError"] = "Story Intelligence could not find saved analysis for that book.";
|
|
return RedirectToAction("Details", "Books", new { id = bookId });
|
|
}
|
|
|
|
return target.Route switch
|
|
{
|
|
StoryIntelligenceResumeRoutes.Complete => RedirectToAction(nameof(StoryIntelligenceComplete), new { batchId = target.BatchID }),
|
|
StoryIntelligenceResumeRoutes.Narrative => RedirectToAction(nameof(StoryIntelligenceNarrative), new { batchId = target.BatchID }),
|
|
StoryIntelligenceResumeRoutes.Knowledge => RedirectToAction(nameof(StoryIntelligenceKnowledge), new { batchId = target.BatchID }),
|
|
StoryIntelligenceResumeRoutes.Relationships => RedirectToAction(nameof(StoryIntelligenceRelationships), new { batchId = target.BatchID }),
|
|
StoryIntelligenceResumeRoutes.Assets => RedirectToAction(nameof(StoryIntelligenceAssets), new { batchId = target.BatchID }),
|
|
StoryIntelligenceResumeRoutes.Locations => RedirectToAction(nameof(StoryIntelligenceLocations), new { batchId = target.BatchID }),
|
|
StoryIntelligenceResumeRoutes.Characters => RedirectToAction(nameof(StoryIntelligenceCharacters), new { batchId = target.BatchID }),
|
|
_ => RedirectToAction(nameof(StoryIntelligenceReview), new { batchId = target.BatchID })
|
|
};
|
|
}
|
|
|
|
[HttpPost("story-intelligence/start")]
|
|
[ValidateAntiForgeryToken]
|
|
public async Task<IActionResult> StartStoryIntelligence(
|
|
[FromForm] string? appearancePreference,
|
|
[FromForm] string? primaryAgeGroup,
|
|
[FromForm] string? storyEra,
|
|
[FromForm] string? storyLocation)
|
|
{
|
|
var job = await storyIntelligence.StartAsync(appearancePreference, primaryAgeGroup, storyEra, storyLocation);
|
|
if (job is null)
|
|
{
|
|
TempData["OnboardingStoryIntelligenceError"] = "Review the manuscript scan before starting Story Intelligence.";
|
|
return RedirectToAction(nameof(StoryIntelligenceContext));
|
|
}
|
|
|
|
return RedirectToAction(nameof(StoryIntelligenceProgress), new { batchId = job.BatchID });
|
|
}
|
|
|
|
[HttpPost("story-intelligence/skip")]
|
|
[ValidateAntiForgeryToken]
|
|
public async Task<IActionResult> SkipStoryIntelligence()
|
|
{
|
|
await onboarding.MarkCompletedAsync();
|
|
TempData["ArchiveMessage"] = "Story Intelligence skipped for now. You can return to it from your dashboard.";
|
|
return RedirectToAction("Index", "Projects");
|
|
}
|
|
|
|
[HttpPost("scan-manuscript")]
|
|
[ValidateAntiForgeryToken]
|
|
public async Task<IActionResult> ScanManuscript()
|
|
{
|
|
await onboarding.SetCurrentStepAsync(OnboardingSteps.NextPathPreview);
|
|
return RedirectToAction(nameof(Index));
|
|
}
|
|
|
|
[HttpGet("story-intelligence/progress")]
|
|
public async Task<IActionResult> StoryIntelligenceProgress(Guid batchId)
|
|
{
|
|
var model = await storyIntelligence.GetProgressAsync(batchId);
|
|
return model is null ? NotFound() : View(model);
|
|
}
|
|
|
|
[HttpGet("story-intelligence/review")]
|
|
public async Task<IActionResult> StoryIntelligenceReview(Guid batchId)
|
|
{
|
|
var model = await storyIntelligence.GetProgressAsync(batchId);
|
|
if (model is null)
|
|
{
|
|
return NotFound();
|
|
}
|
|
|
|
if (model.HasActiveRuns)
|
|
{
|
|
return RedirectToAction(nameof(StoryIntelligenceProgress), new { batchId });
|
|
}
|
|
|
|
return model.AllCommitted
|
|
? RedirectToAction(nameof(StoryIntelligenceCharacters), new { batchId })
|
|
: View(model);
|
|
}
|
|
|
|
[HttpGet("story-intelligence/characters")]
|
|
public async Task<IActionResult> StoryIntelligenceCharacters(Guid batchId)
|
|
{
|
|
var model = await storyIntelligence.GetProgressAsync(batchId);
|
|
if (model is null)
|
|
{
|
|
return NotFound();
|
|
}
|
|
|
|
if (model.HasActiveRuns)
|
|
{
|
|
return RedirectToAction(nameof(StoryIntelligenceProgress), new { batchId });
|
|
}
|
|
|
|
return !model.AllCommitted
|
|
? RedirectToAction(nameof(StoryIntelligenceReview), new { batchId })
|
|
: View(model);
|
|
}
|
|
|
|
[HttpGet("story-intelligence/locations")]
|
|
public async Task<IActionResult> StoryIntelligenceLocations(Guid batchId)
|
|
{
|
|
var model = await storyIntelligence.GetProgressAsync(batchId);
|
|
if (model is null)
|
|
{
|
|
return NotFound();
|
|
}
|
|
|
|
if (model.HasActiveRuns)
|
|
{
|
|
return RedirectToAction(nameof(StoryIntelligenceProgress), new { batchId });
|
|
}
|
|
|
|
if (!model.AllCommitted)
|
|
{
|
|
return RedirectToAction(nameof(StoryIntelligenceReview), new { batchId });
|
|
}
|
|
|
|
return !model.PipelineDashboard.CharacterStageComplete
|
|
? RedirectToAction(nameof(StoryIntelligenceCharacters), new { batchId })
|
|
: View(model);
|
|
}
|
|
|
|
[HttpGet("story-intelligence/assets")]
|
|
public async Task<IActionResult> StoryIntelligenceAssets(Guid batchId)
|
|
{
|
|
var model = await storyIntelligence.GetProgressAsync(batchId);
|
|
if (model is null)
|
|
{
|
|
return NotFound();
|
|
}
|
|
|
|
if (model.HasActiveRuns)
|
|
{
|
|
return RedirectToAction(nameof(StoryIntelligenceProgress), new { batchId });
|
|
}
|
|
|
|
if (!model.AllCommitted)
|
|
{
|
|
return RedirectToAction(nameof(StoryIntelligenceReview), new { batchId });
|
|
}
|
|
|
|
if (!model.PipelineDashboard.CharacterStageComplete)
|
|
{
|
|
return RedirectToAction(nameof(StoryIntelligenceCharacters), new { batchId });
|
|
}
|
|
|
|
return !model.PipelineDashboard.LocationStageComplete
|
|
? RedirectToAction(nameof(StoryIntelligenceLocations), new { batchId })
|
|
: View(model);
|
|
}
|
|
|
|
[HttpGet("story-intelligence/relationships")]
|
|
public async Task<IActionResult> StoryIntelligenceRelationships(Guid batchId)
|
|
{
|
|
var model = await storyIntelligence.GetProgressAsync(batchId);
|
|
if (model is null)
|
|
{
|
|
return NotFound();
|
|
}
|
|
|
|
if (model.HasActiveRuns)
|
|
{
|
|
return RedirectToAction(nameof(StoryIntelligenceProgress), new { batchId });
|
|
}
|
|
|
|
if (!model.AllCommitted)
|
|
{
|
|
return RedirectToAction(nameof(StoryIntelligenceReview), new { batchId });
|
|
}
|
|
|
|
if (!model.PipelineDashboard.CharacterStageComplete)
|
|
{
|
|
return RedirectToAction(nameof(StoryIntelligenceCharacters), new { batchId });
|
|
}
|
|
|
|
if (!model.PipelineDashboard.LocationStageComplete)
|
|
{
|
|
return RedirectToAction(nameof(StoryIntelligenceLocations), new { batchId });
|
|
}
|
|
|
|
return !model.PipelineDashboard.AssetStageComplete
|
|
? RedirectToAction(nameof(StoryIntelligenceAssets), new { batchId })
|
|
: View(model);
|
|
}
|
|
|
|
[HttpPost("story-intelligence/cancel")]
|
|
[ValidateAntiForgeryToken]
|
|
public async Task<IActionResult> CancelStoryIntelligence(Guid batchId)
|
|
{
|
|
var progress = await storyIntelligence.CancelAsync(batchId);
|
|
if (progress is null)
|
|
{
|
|
return NotFound();
|
|
}
|
|
|
|
TempData["OnboardingStoryIntelligenceMessage"] = "Analysis cancellation requested.";
|
|
return RedirectToAction(nameof(StoryIntelligenceReview), new { batchId });
|
|
}
|
|
|
|
[HttpPost("story-intelligence/commit")]
|
|
[ValidateAntiForgeryToken]
|
|
public async Task<IActionResult> CommitStoryIntelligence(Guid batchId, int runId)
|
|
{
|
|
var (progress, result) = await storyIntelligence.CommitAsync(batchId, runId);
|
|
if (progress is null)
|
|
{
|
|
return NotFound();
|
|
}
|
|
|
|
TempData[result.Success ? "OnboardingStoryIntelligenceMessage" : "OnboardingStoryIntelligenceError"] = result.Message;
|
|
if (result.Success && progress.AllCommitted)
|
|
{
|
|
TempData["WriterWorkspaceMessage"] = $"{progress.CommittedChapterCount} chapter(s) and {progress.Chapters.Sum(chapter => chapter.ScenesCreated)} scene(s) created from your manuscript.";
|
|
return RedirectToAction("Index", "Writer", new { projectId = progress.ProjectID });
|
|
}
|
|
|
|
return RedirectToAction(nameof(StoryIntelligenceReview), new { batchId });
|
|
}
|
|
|
|
[HttpPost("story-intelligence/commit-all")]
|
|
[ValidateAntiForgeryToken]
|
|
public async Task<IActionResult> CommitAllStoryIntelligence(Guid batchId)
|
|
{
|
|
var (progress, result) = await storyIntelligence.CommitAllAsync(batchId);
|
|
if (progress is null)
|
|
{
|
|
return NotFound();
|
|
}
|
|
|
|
TempData[result.Success ? "OnboardingStoryIntelligenceMessage" : "OnboardingStoryIntelligenceError"] = result.Message;
|
|
return result.Success && progress.AllCommitted
|
|
? RedirectToAction(nameof(StoryIntelligenceCharacters), new { batchId })
|
|
: RedirectToAction(nameof(StoryIntelligenceReview), new { batchId });
|
|
}
|
|
|
|
[HttpPost("story-intelligence/characters")]
|
|
[ValidateAntiForgeryToken]
|
|
public async Task<IActionResult> ImportStoryIntelligenceCharacters(StoryIntelligenceCharacterImportForm form)
|
|
{
|
|
var (progress, result) = await storyIntelligence.ImportCharactersAsync(form.BatchID, form);
|
|
if (progress is null)
|
|
{
|
|
return NotFound();
|
|
}
|
|
|
|
TempData[result.Success ? "OnboardingStoryIntelligenceMessage" : "OnboardingStoryIntelligenceError"] = result.Message;
|
|
return result.Success
|
|
? RedirectToAction(nameof(StoryIntelligenceCharacterComplete), new { batchId = form.BatchID })
|
|
: RedirectToAction(nameof(StoryIntelligenceCharacters), new { batchId = form.BatchID });
|
|
}
|
|
|
|
[HttpGet("story-intelligence/characters/complete")]
|
|
public async Task<IActionResult> StoryIntelligenceCharacterComplete(Guid batchId)
|
|
{
|
|
var model = await storyIntelligence.GetCharacterImportResultAsync(batchId);
|
|
return model is null ? NotFound() : View(model);
|
|
}
|
|
|
|
[HttpPost("story-intelligence/locations")]
|
|
[ValidateAntiForgeryToken]
|
|
public async Task<IActionResult> ImportStoryIntelligenceLocations(StoryIntelligenceLocationImportForm form)
|
|
{
|
|
var (progress, result) = await storyIntelligence.ImportLocationsAsync(form.BatchID, form);
|
|
if (progress is null)
|
|
{
|
|
return NotFound();
|
|
}
|
|
|
|
TempData[result.Success ? "OnboardingStoryIntelligenceMessage" : "OnboardingStoryIntelligenceError"] = result.Message;
|
|
return result.Success
|
|
? RedirectToAction(nameof(StoryIntelligenceLocationComplete), new { batchId = form.BatchID })
|
|
: RedirectToAction(nameof(StoryIntelligenceLocations), new { batchId = form.BatchID });
|
|
}
|
|
|
|
[HttpGet("story-intelligence/locations/complete")]
|
|
public async Task<IActionResult> StoryIntelligenceLocationComplete(Guid batchId)
|
|
{
|
|
var model = await storyIntelligence.GetLocationImportResultAsync(batchId);
|
|
return model is null ? NotFound() : View(model);
|
|
}
|
|
|
|
[HttpPost("story-intelligence/assets")]
|
|
[ValidateAntiForgeryToken]
|
|
public async Task<IActionResult> ImportStoryIntelligenceAssets(StoryIntelligenceAssetImportForm form)
|
|
{
|
|
var (progress, result) = await storyIntelligence.ImportAssetsAsync(form.BatchID, form);
|
|
if (progress is null)
|
|
{
|
|
return NotFound();
|
|
}
|
|
|
|
TempData[result.Success ? "OnboardingStoryIntelligenceMessage" : "OnboardingStoryIntelligenceError"] = result.Message;
|
|
return result.Success
|
|
? RedirectToAction(nameof(StoryIntelligenceAssetComplete), new { batchId = form.BatchID })
|
|
: RedirectToAction(nameof(StoryIntelligenceAssets), new { batchId = form.BatchID });
|
|
}
|
|
|
|
[HttpGet("story-intelligence/assets/complete")]
|
|
public async Task<IActionResult> StoryIntelligenceAssetComplete(Guid batchId)
|
|
{
|
|
var model = await storyIntelligence.GetAssetImportResultAsync(batchId);
|
|
return model is null ? NotFound() : View(model);
|
|
}
|
|
|
|
[HttpPost("story-intelligence/relationships")]
|
|
[ValidateAntiForgeryToken]
|
|
public async Task<IActionResult> ImportStoryIntelligenceRelationships(StoryIntelligenceRelationshipImportForm form)
|
|
{
|
|
var (progress, result) = await storyIntelligence.ImportRelationshipsAsync(form.BatchID, form);
|
|
if (progress is null)
|
|
{
|
|
return NotFound();
|
|
}
|
|
|
|
TempData[result.Success ? "OnboardingStoryIntelligenceMessage" : "OnboardingStoryIntelligenceError"] = result.Message;
|
|
return result.Success
|
|
? RedirectToAction(nameof(StoryIntelligenceRelationshipComplete), new { batchId = form.BatchID })
|
|
: RedirectToAction(nameof(StoryIntelligenceRelationships), new { batchId = form.BatchID });
|
|
}
|
|
|
|
[HttpGet("story-intelligence/relationships/complete")]
|
|
public async Task<IActionResult> StoryIntelligenceRelationshipComplete(Guid batchId)
|
|
{
|
|
var model = await storyIntelligence.GetRelationshipImportResultAsync(batchId);
|
|
return model is null ? NotFound() : View(model);
|
|
}
|
|
|
|
[HttpGet("story-intelligence/knowledge")]
|
|
public async Task<IActionResult> StoryIntelligenceKnowledge(Guid batchId)
|
|
{
|
|
var model = await storyIntelligence.GetProgressAsync(batchId);
|
|
if (model is null)
|
|
{
|
|
return NotFound();
|
|
}
|
|
|
|
if (model.HasActiveRuns)
|
|
{
|
|
return RedirectToAction(nameof(StoryIntelligenceProgress), new { batchId });
|
|
}
|
|
|
|
if (!model.AllCommitted)
|
|
{
|
|
return RedirectToAction(nameof(StoryIntelligenceReview), new { batchId });
|
|
}
|
|
|
|
if (!model.PipelineDashboard.CharacterStageComplete)
|
|
{
|
|
return RedirectToAction(nameof(StoryIntelligenceCharacters), new { batchId });
|
|
}
|
|
|
|
if (!model.PipelineDashboard.LocationStageComplete)
|
|
{
|
|
return RedirectToAction(nameof(StoryIntelligenceLocations), new { batchId });
|
|
}
|
|
|
|
if (!model.PipelineDashboard.AssetStageComplete)
|
|
{
|
|
return RedirectToAction(nameof(StoryIntelligenceAssets), new { batchId });
|
|
}
|
|
|
|
return !model.PipelineDashboard.RelationshipStageComplete
|
|
? RedirectToAction(nameof(StoryIntelligenceRelationships), new { batchId })
|
|
: View(model);
|
|
}
|
|
|
|
[HttpPost("story-intelligence/knowledge")]
|
|
[ValidateAntiForgeryToken]
|
|
public async Task<IActionResult> ImportStoryIntelligenceKnowledge(StoryIntelligenceKnowledgeImportForm form)
|
|
{
|
|
var (progress, result) = await storyIntelligence.ImportKnowledgeAsync(form.BatchID, form);
|
|
if (progress is null)
|
|
{
|
|
return NotFound();
|
|
}
|
|
|
|
TempData[result.Success ? "OnboardingStoryIntelligenceMessage" : "OnboardingStoryIntelligenceError"] = result.Message;
|
|
return result.Success
|
|
? RedirectToAction(nameof(StoryIntelligenceKnowledgeComplete), new { batchId = form.BatchID })
|
|
: RedirectToAction(nameof(StoryIntelligenceKnowledge), new { batchId = form.BatchID });
|
|
}
|
|
|
|
[HttpGet("story-intelligence/knowledge/complete")]
|
|
public async Task<IActionResult> StoryIntelligenceKnowledgeComplete(Guid batchId)
|
|
{
|
|
var model = await storyIntelligence.GetKnowledgeImportResultAsync(batchId);
|
|
return model is null ? NotFound() : View(model);
|
|
}
|
|
|
|
[HttpGet("story-intelligence/narrative")]
|
|
public async Task<IActionResult> StoryIntelligenceNarrative(Guid batchId)
|
|
{
|
|
var model = await storyIntelligence.GetProgressAsync(batchId);
|
|
if (model is null)
|
|
{
|
|
return NotFound();
|
|
}
|
|
|
|
if (model.HasActiveRuns)
|
|
{
|
|
return RedirectToAction(nameof(StoryIntelligenceProgress), new { batchId });
|
|
}
|
|
|
|
if (!model.AllCommitted)
|
|
{
|
|
return RedirectToAction(nameof(StoryIntelligenceReview), new { batchId });
|
|
}
|
|
|
|
if (!model.PipelineDashboard.CharacterStageComplete)
|
|
{
|
|
return RedirectToAction(nameof(StoryIntelligenceCharacters), new { batchId });
|
|
}
|
|
|
|
if (!model.PipelineDashboard.LocationStageComplete)
|
|
{
|
|
return RedirectToAction(nameof(StoryIntelligenceLocations), new { batchId });
|
|
}
|
|
|
|
if (!model.PipelineDashboard.AssetStageComplete)
|
|
{
|
|
return RedirectToAction(nameof(StoryIntelligenceAssets), new { batchId });
|
|
}
|
|
|
|
if (!model.PipelineDashboard.RelationshipStageComplete)
|
|
{
|
|
return RedirectToAction(nameof(StoryIntelligenceRelationships), new { batchId });
|
|
}
|
|
|
|
return !model.PipelineDashboard.KnowledgeStageComplete
|
|
? RedirectToAction(nameof(StoryIntelligenceKnowledge), new { batchId })
|
|
: View(model);
|
|
}
|
|
|
|
[HttpPost("story-intelligence/narrative")]
|
|
[ValidateAntiForgeryToken]
|
|
public async Task<IActionResult> ImportStoryIntelligenceNarrative(StoryIntelligenceNarrativeImportForm form)
|
|
{
|
|
var (progress, result) = await storyIntelligence.ImportNarrativeAsync(form.BatchID, form);
|
|
if (progress is null)
|
|
{
|
|
return NotFound();
|
|
}
|
|
|
|
TempData[result.Success ? "OnboardingStoryIntelligenceMessage" : "OnboardingStoryIntelligenceError"] = result.Message;
|
|
return result.Success
|
|
? RedirectToAction(nameof(StoryIntelligenceNarrativeComplete), new { batchId = form.BatchID })
|
|
: RedirectToAction(nameof(StoryIntelligenceNarrative), new { batchId = form.BatchID });
|
|
}
|
|
|
|
[HttpGet("story-intelligence/narrative/complete")]
|
|
public async Task<IActionResult> StoryIntelligenceNarrativeComplete(Guid batchId)
|
|
{
|
|
var model = await storyIntelligence.GetNarrativeImportResultAsync(batchId);
|
|
return model is null ? NotFound() : View(model);
|
|
}
|
|
|
|
[HttpGet("story-intelligence/complete")]
|
|
public async Task<IActionResult> StoryIntelligenceComplete(Guid batchId)
|
|
{
|
|
var progress = await storyIntelligence.GetProgressAsync(batchId);
|
|
if (progress is null)
|
|
{
|
|
return NotFound();
|
|
}
|
|
|
|
if (!progress.PipelineDashboard.CharacterStageComplete)
|
|
{
|
|
return RedirectToAction(nameof(StoryIntelligenceCharacters), new { batchId });
|
|
}
|
|
|
|
if (!progress.PipelineDashboard.LocationStageComplete)
|
|
{
|
|
return RedirectToAction(nameof(StoryIntelligenceLocations), new { batchId });
|
|
}
|
|
|
|
if (!progress.PipelineDashboard.AssetStageComplete)
|
|
{
|
|
return RedirectToAction(nameof(StoryIntelligenceAssets), new { batchId });
|
|
}
|
|
|
|
if (!progress.PipelineDashboard.RelationshipStageComplete)
|
|
{
|
|
return RedirectToAction(nameof(StoryIntelligenceRelationships), new { batchId });
|
|
}
|
|
|
|
if (!progress.PipelineDashboard.KnowledgeStageComplete)
|
|
{
|
|
return RedirectToAction(nameof(StoryIntelligenceKnowledge), new { batchId });
|
|
}
|
|
|
|
if (!progress.PipelineDashboard.NarrativeStageComplete)
|
|
{
|
|
return RedirectToAction(nameof(StoryIntelligenceNarrative), new { batchId });
|
|
}
|
|
|
|
var model = await storyIntelligence.GetCompletionAsync(batchId);
|
|
return model is null ? NotFound() : View(model);
|
|
}
|
|
|
|
[HttpPost("scan-review")]
|
|
[RequestSizeLimit(25 * 1024 * 1024)]
|
|
[RequestFormLimits(ValueCountLimit = 20000, KeyLengthLimit = 4096, ValueLengthLimit = 1024 * 1024)]
|
|
[ValidateAntiForgeryToken]
|
|
public async Task<IActionResult> SaveScanReview(ManuscriptScanReviewForm form, string intent = "save", int characterPage = 1)
|
|
{
|
|
var continueToCharacters = string.Equals(intent, "characters", StringComparison.OrdinalIgnoreCase);
|
|
var continueCharacterReview = string.Equals(intent, "character-next", StringComparison.OrdinalIgnoreCase);
|
|
var backCharacterReview = string.Equals(intent, "character-back", StringComparison.OrdinalIgnoreCase);
|
|
var readyToImport = string.Equals(intent, "structure", StringComparison.OrdinalIgnoreCase);
|
|
var completeCoreImport = string.Equals(intent, "continue", StringComparison.OrdinalIgnoreCase);
|
|
var nextCharacterPage = Math.Max(1, characterPage + (backCharacterReview ? -1 : 1));
|
|
try
|
|
{
|
|
if (completeCoreImport)
|
|
{
|
|
var result = await onboarding.FinaliseCharacterReviewAsync(form);
|
|
if (result is null)
|
|
{
|
|
return NotFound();
|
|
}
|
|
|
|
TempData["OnboardingReviewMessage"] = "Characters saved. Core manuscript import is complete.";
|
|
return RedirectToAction(nameof(BuildComplete), new { previewId = form.PreviewID });
|
|
}
|
|
|
|
var model = await onboarding.SaveScanReviewAsync(form, readyToImport);
|
|
if (model is null)
|
|
{
|
|
return NotFound();
|
|
}
|
|
|
|
if (continueToCharacters)
|
|
{
|
|
var build = await onboarding.GetBuildResultAsync(form.PreviewID);
|
|
if (build is null || build.SceneMappings.Count == 0)
|
|
{
|
|
TempData["ArchiveError"] = "Import the approved chapters and scenes before reviewing characters.";
|
|
await onboarding.SetCurrentStepAsync(OnboardingSteps.NextPathPreview);
|
|
return RedirectToAction(nameof(Index));
|
|
}
|
|
}
|
|
|
|
TempData["OnboardingReviewMessage"] = readyToImport
|
|
? "Chapter review saved. Next: import scenes and update Word."
|
|
: continueToCharacters ? "Chapter and scene review saved. Next: review detected characters."
|
|
: continueCharacterReview || backCharacterReview ? "Character review choices saved."
|
|
: "Review choices saved.";
|
|
return readyToImport
|
|
? RedirectToAction(nameof(Index))
|
|
: continueCharacterReview || backCharacterReview ? RedirectToAction(nameof(ScanCharacters), new { previewId = form.PreviewID, page = nextCharacterPage })
|
|
: continueToCharacters ? RedirectToAction(nameof(ScanCharacters), new { previewId = form.PreviewID })
|
|
: RedirectToAction(nameof(ScanReview), new { previewId = form.PreviewID });
|
|
}
|
|
catch (InvalidOperationException ex)
|
|
{
|
|
ModelState.AddModelError(string.Empty, ex.Message);
|
|
var model = await onboarding.GetScanReviewAsync(form.PreviewID);
|
|
if (model is null)
|
|
{
|
|
return NotFound();
|
|
}
|
|
|
|
if (continueToCharacters || continueCharacterReview || backCharacterReview || completeCoreImport)
|
|
{
|
|
ViewData["ScanReviewStep"] = "characters";
|
|
ViewData["CharacterReviewPage"] = readyToImport
|
|
? FirstInvalidCharacterPage(model)
|
|
: Math.Max(1, characterPage);
|
|
}
|
|
|
|
return View("ScanReview", model);
|
|
}
|
|
}
|
|
|
|
private static int FirstInvalidCharacterPage(ManuscriptScanReviewViewModel model)
|
|
{
|
|
const int pageSize = 10;
|
|
var characters = model.CharacterCandidates
|
|
.Where(character => !string.Equals(character.Category, "Excluded", StringComparison.OrdinalIgnoreCase))
|
|
.OrderBy(character => character.ReviewName)
|
|
.ThenBy(character => character.Name)
|
|
.ToList();
|
|
var canonicalNames = characters
|
|
.Where(IsIncludedCanonicalCharacter)
|
|
.Select(character => character.ReviewName.Trim())
|
|
.Where(name => !string.IsNullOrWhiteSpace(name))
|
|
.GroupBy(name => name, StringComparer.OrdinalIgnoreCase)
|
|
.Where(group => group.Count() > 1)
|
|
.SelectMany(group => group)
|
|
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
|
|
|
for (var index = 0; index < characters.Count; index++)
|
|
{
|
|
var character = characters[index];
|
|
if (character.Include && string.IsNullOrWhiteSpace(character.ReviewName))
|
|
{
|
|
return (index / pageSize) + 1;
|
|
}
|
|
|
|
if (IsIncludedCanonicalCharacter(character) && canonicalNames.Contains(character.ReviewName.Trim()))
|
|
{
|
|
return (index / pageSize) + 1;
|
|
}
|
|
|
|
if (character.Include && !string.IsNullOrWhiteSpace(character.AliasTargetTemporaryCharacterKey))
|
|
{
|
|
var target = characters.FirstOrDefault(candidate => string.Equals(candidate.TemporaryCharacterKey, character.AliasTargetTemporaryCharacterKey, StringComparison.Ordinal));
|
|
if (target is null || !IsIncludedCanonicalCharacter(target))
|
|
{
|
|
return (index / pageSize) + 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
private static bool IsIncludedCanonicalCharacter(ManuscriptScanReviewCharacterViewModel character)
|
|
=> character.Include
|
|
&& string.IsNullOrWhiteSpace(character.AliasTargetTemporaryCharacterKey)
|
|
&& !string.Equals(character.Category, "Excluded", StringComparison.OrdinalIgnoreCase);
|
|
|
|
[HttpPost("welcome")]
|
|
[ValidateAntiForgeryToken]
|
|
public async Task<IActionResult> Welcome()
|
|
{
|
|
await onboarding.MoveToNextStepAsync();
|
|
return RedirectToAction(nameof(Index));
|
|
}
|
|
|
|
[HttpPost("writing-journey")]
|
|
[ValidateAntiForgeryToken]
|
|
public async Task<IActionResult> WritingJourney(WritingJourneyOnboardingForm form)
|
|
{
|
|
if (!ModelState.IsValid || string.IsNullOrWhiteSpace(form.WritingJourney))
|
|
{
|
|
var model = await onboarding.GetWizardAsync();
|
|
model.CurrentStep = OnboardingSteps.WritingJourney;
|
|
model.StepNumber = 2;
|
|
return View(nameof(Index), model);
|
|
}
|
|
|
|
await onboarding.SaveWritingJourneyAsync(form.WritingJourney);
|
|
return RedirectToAction(nameof(Index));
|
|
}
|
|
|
|
[HttpPost("writing-software")]
|
|
[ValidateAntiForgeryToken]
|
|
public async Task<IActionResult> WritingSoftware(WritingSoftwareOnboardingForm form)
|
|
{
|
|
if (!ModelState.IsValid || string.IsNullOrWhiteSpace(form.WritingSoftware))
|
|
{
|
|
var model = await onboarding.GetWizardAsync();
|
|
model.CurrentStep = OnboardingSteps.WritingSoftware;
|
|
model.StepNumber = 3;
|
|
return View(nameof(Index), model);
|
|
}
|
|
|
|
await onboarding.SaveWritingSoftwareAsync(form.WritingSoftware);
|
|
return RedirectToAction(nameof(Index));
|
|
}
|
|
|
|
[HttpPost("project")]
|
|
[ValidateAntiForgeryToken]
|
|
public async Task<IActionResult> Project(ProjectOnboardingForm form)
|
|
{
|
|
if (string.Equals(form.Mode, OnboardingSelectionModes.Create, StringComparison.Ordinal)
|
|
&& string.IsNullOrWhiteSpace(form.ProjectName))
|
|
{
|
|
ModelState.AddModelError(nameof(form.ProjectName), "Enter a project name.");
|
|
}
|
|
|
|
if (string.Equals(form.Mode, OnboardingSelectionModes.Select, StringComparison.Ordinal)
|
|
&& !form.SelectedProjectID.HasValue)
|
|
{
|
|
ModelState.AddModelError(nameof(form.SelectedProjectID), "Choose one of your active projects.");
|
|
}
|
|
|
|
if (!ModelState.IsValid)
|
|
{
|
|
var invalidModel = await onboarding.GetWizardAsync();
|
|
invalidModel.CurrentStep = OnboardingSteps.Project;
|
|
invalidModel.StepNumber = 4;
|
|
invalidModel.ProjectForm = form;
|
|
return View(nameof(Index), invalidModel);
|
|
}
|
|
|
|
try
|
|
{
|
|
await onboarding.SaveProjectAsync(form);
|
|
}
|
|
catch (InvalidOperationException ex)
|
|
{
|
|
ModelState.AddModelError(string.Equals(form.Mode, OnboardingSelectionModes.Select, StringComparison.Ordinal)
|
|
? nameof(form.SelectedProjectID)
|
|
: nameof(form.ProjectName), ex.Message);
|
|
var invalidModel = await onboarding.GetWizardAsync();
|
|
invalidModel.CurrentStep = OnboardingSteps.Project;
|
|
invalidModel.StepNumber = 4;
|
|
invalidModel.ProjectForm = form;
|
|
return View(nameof(Index), invalidModel);
|
|
}
|
|
|
|
return RedirectToAction(nameof(Index));
|
|
}
|
|
|
|
[HttpPost("book")]
|
|
[ValidateAntiForgeryToken]
|
|
public async Task<IActionResult> Book(BookOnboardingForm form)
|
|
{
|
|
if (string.Equals(form.Mode, OnboardingSelectionModes.Create, StringComparison.Ordinal)
|
|
&& string.IsNullOrWhiteSpace(form.BookTitle))
|
|
{
|
|
ModelState.AddModelError(nameof(form.BookTitle), "Enter a book title.");
|
|
}
|
|
|
|
if (string.Equals(form.Mode, OnboardingSelectionModes.Select, StringComparison.Ordinal)
|
|
&& !form.SelectedBookID.HasValue)
|
|
{
|
|
ModelState.AddModelError(nameof(form.SelectedBookID), "Choose a book from the selected project.");
|
|
}
|
|
|
|
if (!ModelState.IsValid)
|
|
{
|
|
var invalidModel = await onboarding.GetWizardAsync();
|
|
invalidModel.CurrentStep = OnboardingSteps.Book;
|
|
invalidModel.StepNumber = 5;
|
|
invalidModel.BookForm = form;
|
|
return View(nameof(Index), invalidModel);
|
|
}
|
|
|
|
try
|
|
{
|
|
await onboarding.SaveBookAsync(form);
|
|
}
|
|
catch (SubscriptionLimitException ex)
|
|
{
|
|
ModelState.AddModelError(nameof(form.BookTitle), ex.Message);
|
|
var invalidModel = await onboarding.GetWizardAsync();
|
|
invalidModel.CurrentStep = OnboardingSteps.Book;
|
|
invalidModel.StepNumber = 5;
|
|
invalidModel.BookForm = form;
|
|
return View(nameof(Index), invalidModel);
|
|
}
|
|
catch (InvalidOperationException ex)
|
|
{
|
|
ModelState.AddModelError(string.Equals(form.Mode, OnboardingSelectionModes.Select, StringComparison.Ordinal)
|
|
? nameof(form.SelectedBookID)
|
|
: nameof(form.BookTitle), ex.Message);
|
|
var invalidModel = await onboarding.GetWizardAsync();
|
|
invalidModel.CurrentStep = OnboardingSteps.Book;
|
|
invalidModel.StepNumber = 5;
|
|
invalidModel.BookForm = form;
|
|
return View(nameof(Index), invalidModel);
|
|
}
|
|
|
|
return RedirectToAction(nameof(Index));
|
|
}
|
|
|
|
[HttpPost("finish")]
|
|
[ValidateAntiForgeryToken]
|
|
public async Task<IActionResult> Finish()
|
|
{
|
|
await onboarding.MarkCompletedAsync();
|
|
TempData["ArchiveMessage"] = "Setup saved. You can return to setup preferences from your account menu.";
|
|
return RedirectToAction("Index", "Projects");
|
|
}
|
|
|
|
[HttpPost("back")]
|
|
[ValidateAntiForgeryToken]
|
|
public async Task<IActionResult> Back(string currentStep)
|
|
{
|
|
var targetStep = currentStep switch
|
|
{
|
|
OnboardingSteps.WritingJourney => OnboardingSteps.Welcome,
|
|
OnboardingSteps.WritingSoftware => OnboardingSteps.WritingJourney,
|
|
OnboardingSteps.Project => OnboardingSteps.WritingSoftware,
|
|
OnboardingSteps.Book => OnboardingSteps.Project,
|
|
OnboardingSteps.NextPathPreview => OnboardingSteps.Book,
|
|
_ => OnboardingSteps.Welcome
|
|
};
|
|
|
|
await onboarding.SetCurrentStepAsync(targetStep);
|
|
|
|
return RedirectToAction(nameof(Index));
|
|
}
|
|
}
|