329 lines
14 KiB
C#
329 lines
14 KiB
C#
using System.Collections.Concurrent;
|
|
using PlotLine.Models;
|
|
|
|
namespace PlotLine.Services;
|
|
|
|
public interface IManuscriptScanPreviewStore
|
|
{
|
|
Task<ManuscriptScanState> StartAsync(int userId, int onboardingId, int projectId, int bookId);
|
|
Task ClearAsync(int userId, int onboardingId);
|
|
Task<ManuscriptScanState> ProgressAsync(int userId, ManuscriptScanProgress progress);
|
|
Task<ManuscriptScanState> CompleteAsync(int userId, ManuscriptScanPreview preview);
|
|
Task<ManuscriptScanState> FailAsync(int userId, int onboardingId, string message);
|
|
Task<IReadOnlyList<ManuscriptScanState>> FailRunningForUserAsync(int userId, string message);
|
|
Task<ManuscriptScanState> GetStateAsync(int userId, int onboardingId);
|
|
Task<ManuscriptScanPreview?> GetLatestPreviewAsync(int userId, int onboardingId);
|
|
Task<ManuscriptScanPreview?> GetPreviewAsync(int userId, Guid previewId);
|
|
Task<ManuscriptScanReviewDecision?> GetReviewAsync(int userId, Guid previewId);
|
|
Task<ManuscriptScanReviewDecision?> SaveReviewAsync(int userId, ManuscriptScanReviewDecision review);
|
|
Task<OnboardingBuildProgress> BuildProgressAsync(int userId, Guid previewId, string message, int percentComplete, OnboardingManuscriptBuildResult? result = null);
|
|
Task<OnboardingManuscriptBuildResult?> GetBuildResultAsync(int userId, Guid previewId);
|
|
Task<OnboardingManuscriptBuildResult?> SaveBuildResultAsync(int userId, OnboardingManuscriptBuildResult result);
|
|
}
|
|
|
|
public sealed class ManuscriptScanPreviewStore : IManuscriptScanPreviewStore
|
|
{
|
|
private readonly ConcurrentDictionary<(int UserId, int OnboardingId), ScanSession> sessions = new();
|
|
private readonly ConcurrentDictionary<Guid, ManuscriptScanPreview> previews = new();
|
|
private readonly ConcurrentDictionary<Guid, ManuscriptScanReviewDecision> reviews = new();
|
|
private readonly ConcurrentDictionary<Guid, OnboardingManuscriptBuildResult> buildResults = new();
|
|
private readonly ConcurrentDictionary<Guid, OnboardingBuildProgress> buildProgress = new();
|
|
|
|
public Task<ManuscriptScanState> StartAsync(int userId, int onboardingId, int projectId, int bookId)
|
|
{
|
|
var session = new ScanSession
|
|
{
|
|
UserID = userId,
|
|
OnboardingID = onboardingId,
|
|
ProjectID = projectId,
|
|
BookID = bookId,
|
|
Status = ManuscriptScanStatuses.Running,
|
|
Message = "Preparing document scan",
|
|
PercentComplete = 0,
|
|
UpdatedUtc = DateTime.UtcNow
|
|
};
|
|
sessions[(userId, onboardingId)] = session;
|
|
return Task.FromResult(ToState(session));
|
|
}
|
|
|
|
public Task ClearAsync(int userId, int onboardingId)
|
|
{
|
|
sessions.TryRemove((userId, onboardingId), out _);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task<ManuscriptScanState> ProgressAsync(int userId, ManuscriptScanProgress progress)
|
|
{
|
|
var key = (userId, progress.OnboardingID);
|
|
var session = sessions.GetOrAdd(key, _ => new ScanSession
|
|
{
|
|
UserID = userId,
|
|
OnboardingID = progress.OnboardingID,
|
|
Status = ManuscriptScanStatuses.Running
|
|
});
|
|
|
|
session.Status = ManuscriptScanStatuses.Running;
|
|
session.Message = Clean(progress.Message, "Scanning manuscript");
|
|
session.PercentComplete = progress.PercentComplete;
|
|
session.ChapterCount = progress.ChapterCount;
|
|
session.SceneCount = progress.SceneCount;
|
|
session.CharacterCandidateCount = progress.CharacterCandidateCount;
|
|
session.TotalWordCount = progress.TotalWordCount;
|
|
session.UpdatedUtc = DateTime.UtcNow;
|
|
return Task.FromResult(ToState(session));
|
|
}
|
|
|
|
public Task<ManuscriptScanState> CompleteAsync(int userId, ManuscriptScanPreview preview)
|
|
{
|
|
var stored = preview.WithUser(userId);
|
|
previews[stored.PreviewID] = stored;
|
|
var session = sessions.GetOrAdd((userId, stored.OnboardingID), _ => new ScanSession
|
|
{
|
|
UserID = userId,
|
|
OnboardingID = stored.OnboardingID,
|
|
ProjectID = stored.ProjectID,
|
|
BookID = stored.BookID
|
|
});
|
|
|
|
session.Status = ManuscriptScanStatuses.Complete;
|
|
session.ReviewStatus = ManuscriptScanReviewStatuses.ScanComplete;
|
|
session.Message = "Scan complete";
|
|
session.PercentComplete = 100;
|
|
session.ChapterCount = stored.ChapterCount;
|
|
session.SceneCount = stored.SceneCount;
|
|
session.CharacterCandidateCount = stored.CharacterCandidateCount;
|
|
session.TotalWordCount = stored.TotalWordCount;
|
|
session.PreviewID = stored.PreviewID;
|
|
session.UpdatedUtc = stored.CreatedUtc;
|
|
return Task.FromResult(ToState(session));
|
|
}
|
|
|
|
public Task<ManuscriptScanState> FailAsync(int userId, int onboardingId, string message)
|
|
{
|
|
var session = sessions.GetOrAdd((userId, onboardingId), _ => new ScanSession
|
|
{
|
|
UserID = userId,
|
|
OnboardingID = onboardingId
|
|
});
|
|
session.Status = ManuscriptScanStatuses.Failed;
|
|
session.Message = Clean(message, "The scan could not finish. Try reconnecting the Word Companion.");
|
|
session.UpdatedUtc = DateTime.UtcNow;
|
|
return Task.FromResult(ToState(session));
|
|
}
|
|
|
|
public Task<IReadOnlyList<ManuscriptScanState>> FailRunningForUserAsync(int userId, string message)
|
|
{
|
|
var failed = new List<ManuscriptScanState>();
|
|
foreach (var pair in sessions)
|
|
{
|
|
if (pair.Key.UserId != userId || !string.Equals(pair.Value.Status, ManuscriptScanStatuses.Running, StringComparison.Ordinal))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
pair.Value.Status = ManuscriptScanStatuses.Failed;
|
|
pair.Value.Message = Clean(message, "The Word Companion disconnected before the scan finished. Reopen Word and try again.");
|
|
pair.Value.UpdatedUtc = DateTime.UtcNow;
|
|
failed.Add(ToState(pair.Value));
|
|
}
|
|
|
|
return Task.FromResult<IReadOnlyList<ManuscriptScanState>>(failed);
|
|
}
|
|
|
|
public Task<ManuscriptScanState> GetStateAsync(int userId, int onboardingId)
|
|
=> Task.FromResult(sessions.TryGetValue((userId, onboardingId), out var session)
|
|
? ToState(session)
|
|
: new ManuscriptScanState());
|
|
|
|
public Task<ManuscriptScanPreview?> GetLatestPreviewAsync(int userId, int onboardingId)
|
|
{
|
|
if (!sessions.TryGetValue((userId, onboardingId), out var session) || !session.PreviewID.HasValue)
|
|
{
|
|
return Task.FromResult<ManuscriptScanPreview?>(null);
|
|
}
|
|
|
|
return GetPreviewAsync(userId, session.PreviewID.Value);
|
|
}
|
|
|
|
public Task<ManuscriptScanPreview?> GetPreviewAsync(int userId, Guid previewId)
|
|
=> Task.FromResult(previews.TryGetValue(previewId, out var preview) && preview.UserID == userId ? preview : null);
|
|
|
|
public Task<ManuscriptScanReviewDecision?> GetReviewAsync(int userId, Guid previewId)
|
|
{
|
|
if (!previews.TryGetValue(previewId, out var preview) || preview.UserID != userId)
|
|
{
|
|
return Task.FromResult<ManuscriptScanReviewDecision?>(null);
|
|
}
|
|
|
|
var review = reviews.GetOrAdd(previewId, _ => BuildDefaultReview(preview));
|
|
return Task.FromResult<ManuscriptScanReviewDecision?>(review);
|
|
}
|
|
|
|
public Task<ManuscriptScanReviewDecision?> SaveReviewAsync(int userId, ManuscriptScanReviewDecision review)
|
|
{
|
|
if (!previews.TryGetValue(review.PreviewID, out var preview) || preview.UserID != userId)
|
|
{
|
|
return Task.FromResult<ManuscriptScanReviewDecision?>(null);
|
|
}
|
|
|
|
var stored = new ManuscriptScanReviewDecision
|
|
{
|
|
PreviewID = review.PreviewID,
|
|
Status = review.Status,
|
|
UpdatedUtc = DateTime.UtcNow,
|
|
Chapters = review.Chapters,
|
|
Scenes = review.Scenes,
|
|
Characters = review.Characters
|
|
};
|
|
reviews[review.PreviewID] = stored;
|
|
if (sessions.TryGetValue((userId, preview.OnboardingID), out var session))
|
|
{
|
|
session.ReviewStatus = stored.Status;
|
|
session.UpdatedUtc = stored.UpdatedUtc;
|
|
}
|
|
|
|
return Task.FromResult<ManuscriptScanReviewDecision?>(stored);
|
|
}
|
|
|
|
public Task<OnboardingBuildProgress> BuildProgressAsync(int userId, Guid previewId, string message, int percentComplete, OnboardingManuscriptBuildResult? result = null)
|
|
{
|
|
if (!previews.TryGetValue(previewId, out var preview) || preview.UserID != userId)
|
|
{
|
|
return Task.FromResult(new OnboardingBuildProgress { Status = "Failed", Message = "The scan preview could not be found." });
|
|
}
|
|
|
|
var progress = new OnboardingBuildProgress
|
|
{
|
|
Status = result is null ? "Running" : "Complete",
|
|
Message = message,
|
|
PercentComplete = percentComplete,
|
|
ChaptersCreated = result?.ChaptersCreated ?? 0,
|
|
ScenesCreated = result?.ScenesCreated ?? 0,
|
|
CharactersCreated = (result?.CharactersCreated ?? 0) + (result?.CharactersReused ?? 0),
|
|
SceneAppearancesCreated = result?.SceneAppearancesCreated ?? 0,
|
|
PreviewID = previewId
|
|
};
|
|
buildProgress[previewId] = progress;
|
|
return Task.FromResult(progress);
|
|
}
|
|
|
|
public Task<OnboardingManuscriptBuildResult?> GetBuildResultAsync(int userId, Guid previewId)
|
|
=> Task.FromResult(previews.TryGetValue(previewId, out var preview)
|
|
&& preview.UserID == userId
|
|
&& buildResults.TryGetValue(previewId, out var result)
|
|
? result
|
|
: null);
|
|
|
|
public Task<OnboardingManuscriptBuildResult?> SaveBuildResultAsync(int userId, OnboardingManuscriptBuildResult result)
|
|
{
|
|
if (!previews.TryGetValue(result.PreviewID, out var preview) || preview.UserID != userId)
|
|
{
|
|
return Task.FromResult<OnboardingManuscriptBuildResult?>(null);
|
|
}
|
|
|
|
buildResults[result.PreviewID] = result;
|
|
return Task.FromResult<OnboardingManuscriptBuildResult?>(result);
|
|
}
|
|
|
|
private static ManuscriptScanState ToState(ScanSession session)
|
|
=> new()
|
|
{
|
|
Status = session.Status,
|
|
ReviewStatus = session.ReviewStatus,
|
|
Message = session.Message,
|
|
PercentComplete = session.PercentComplete,
|
|
ChapterCount = session.ChapterCount,
|
|
SceneCount = session.SceneCount,
|
|
CharacterCandidateCount = session.CharacterCandidateCount,
|
|
TotalWordCount = session.TotalWordCount,
|
|
PreviewID = session.PreviewID,
|
|
UpdatedUtc = session.UpdatedUtc
|
|
};
|
|
|
|
private static string Clean(string? value, string fallback)
|
|
{
|
|
var cleaned = (value ?? string.Empty).Trim();
|
|
return string.IsNullOrWhiteSpace(cleaned) ? fallback : cleaned;
|
|
}
|
|
|
|
private static ManuscriptScanReviewDecision BuildDefaultReview(ManuscriptScanPreview preview)
|
|
=> new()
|
|
{
|
|
PreviewID = preview.PreviewID,
|
|
Status = ManuscriptScanReviewStatuses.ScanComplete,
|
|
Chapters = preview.Chapters
|
|
.OrderBy(chapter => chapter.ChapterNumber)
|
|
.Select(chapter => new ManuscriptScanChapterReviewDecision
|
|
{
|
|
TemporaryChapterKey = chapter.TemporaryChapterKey,
|
|
Include = !string.IsNullOrWhiteSpace(chapter.Title) && chapter.WordCount > 0,
|
|
Title = chapter.Title,
|
|
ChapterNumber = chapter.ChapterNumber
|
|
})
|
|
.ToList(),
|
|
Scenes = preview.Scenes
|
|
.OrderBy(scene => scene.TemporaryChapterKey)
|
|
.ThenBy(scene => scene.SceneNumberWithinChapter)
|
|
.Select(scene => new ManuscriptScanSceneReviewDecision
|
|
{
|
|
TemporarySceneKey = scene.TemporarySceneKey,
|
|
TemporaryChapterKey = scene.TemporaryChapterKey,
|
|
Include = true,
|
|
Title = string.IsNullOrWhiteSpace(scene.Title) ? $"Scene {scene.SceneNumberWithinChapter}" : scene.Title,
|
|
SceneNumberWithinChapter = scene.SceneNumberWithinChapter
|
|
})
|
|
.ToList(),
|
|
Characters = preview.CharacterCandidates
|
|
.Select(candidate => new ManuscriptScanCharacterReviewDecision
|
|
{
|
|
TemporaryCharacterKey = candidate.TemporaryCharacterKey,
|
|
Include = string.Equals(candidate.Category, "ProbableCharacter", StringComparison.OrdinalIgnoreCase),
|
|
Name = candidate.Name,
|
|
Category = candidate.Category
|
|
})
|
|
.ToList()
|
|
};
|
|
|
|
private sealed class ScanSession
|
|
{
|
|
public int UserID { get; init; }
|
|
public int OnboardingID { get; init; }
|
|
public int ProjectID { get; init; }
|
|
public int BookID { get; init; }
|
|
public string Status { get; set; } = ManuscriptScanStatuses.NotStarted;
|
|
public string ReviewStatus { get; set; } = ManuscriptScanReviewStatuses.ScanComplete;
|
|
public string Message { get; set; } = string.Empty;
|
|
public int? PercentComplete { get; set; }
|
|
public int ChapterCount { get; set; }
|
|
public int SceneCount { get; set; }
|
|
public int CharacterCandidateCount { get; set; }
|
|
public int TotalWordCount { get; set; }
|
|
public Guid? PreviewID { get; set; }
|
|
public DateTime? UpdatedUtc { get; set; }
|
|
}
|
|
}
|
|
|
|
file static class ManuscriptScanPreviewExtensions
|
|
{
|
|
public static ManuscriptScanPreview WithUser(this ManuscriptScanPreview preview, int userId)
|
|
=> new()
|
|
{
|
|
PreviewID = preview.PreviewID == Guid.Empty ? Guid.NewGuid() : preview.PreviewID,
|
|
UserID = userId,
|
|
OnboardingID = preview.OnboardingID,
|
|
ProjectID = preview.ProjectID,
|
|
BookID = preview.BookID,
|
|
Source = preview.Source,
|
|
DocumentTitle = preview.DocumentTitle,
|
|
CompanionDocumentIdentifier = preview.CompanionDocumentIdentifier,
|
|
TotalWordCount = preview.TotalWordCount,
|
|
ChapterCount = preview.ChapterCount,
|
|
SceneCount = preview.SceneCount,
|
|
CharacterCandidateCount = preview.CharacterCandidateCount,
|
|
CreatedUtc = preview.CreatedUtc == default ? DateTime.UtcNow : preview.CreatedUtc,
|
|
Chapters = preview.Chapters,
|
|
Scenes = preview.Scenes,
|
|
CharacterCandidates = preview.CharacterCandidates
|
|
};
|
|
}
|