Remove Story Intelligence visualisation runtime
@ -10,6 +10,8 @@ using PlotLine.Models;
|
||||
using PlotLine.Services;
|
||||
using PlotLine.ViewModels;
|
||||
|
||||
#pragma warning disable CS8321
|
||||
|
||||
var tests = new (string Name, Action Test)[]
|
||||
{
|
||||
("Malformed confidence 0. is repaired to 0.0", RepairsTrailingDecimalConfidence),
|
||||
@ -43,10 +45,9 @@ var tests = new (string Name, Action Test)[]
|
||||
("Illustration storage codes are normalised safely", IllustrationStorageCodesAreNormalisedSafely),
|
||||
("Illustration starter batch has required category counts", IllustrationStarterBatchHasRequiredCategoryCounts),
|
||||
("Illustration starter generation plan skips successful items", IllustrationStarterGenerationPlanSkipsSuccessfulItems),
|
||||
("Story Intelligence prototype uses starter illustration codes", StoryIntelligencePrototypeUsesStarterIllustrationCodes),
|
||||
("Story Intelligence simulation mode remains available", StoryIntelligenceSimulationModeRemainsAvailable),
|
||||
("Story Intelligence visualisation archive is preserved", StoryIntelligenceVisualisationArchiveIsPreserved),
|
||||
("Story Intelligence visualisation runtime is removed", StoryIntelligenceVisualisationRuntimeIsRemoved),
|
||||
("Story Intelligence does not retry exhausted billing quota", StoryIntelligenceDoesNotRetryExhaustedBillingQuota),
|
||||
("Story Intelligence visualisation contract omits raw manuscript text", StoryIntelligenceVisualisationContractOmitsRawManuscriptText),
|
||||
("Illustration bulk retry result reports counts", IllustrationBulkRetryResultReportsCounts),
|
||||
("Illustration bulk retry skips blocking duplicate stable codes", IllustrationBulkRetrySkipsBlockingDuplicateStableCodes),
|
||||
("Illustration provider reports missing image model", IllustrationProviderReportsMissingImageModel),
|
||||
@ -59,17 +60,9 @@ var tests = new (string Name, Action Test)[]
|
||||
("Illustration demand archetypes stay broad and use active template", IllustrationDemandArchetypesStayBroadAndUseActiveTemplate),
|
||||
("Story Intelligence evidence extraction prevents observed attribute leakage", StoryIntelligenceEvidenceExtractionPreventsObservedAttributeLeakage),
|
||||
("Illustration assignment repair enforces explicit evidence groups and strict subtypes", IllustrationAssignmentRepairEnforcesExplicitEvidenceGroupsAndStrictSubtypes),
|
||||
("Phase 21O preserves adult defaults and real-mode continuity", Phase21OPreservesAdultDefaultsAndRealModeContinuity),
|
||||
("Phase 21P uses one authoritative current visualisation scene", Phase21PUsesOneAuthoritativeCurrentVisualisationScene),
|
||||
("Phase 21Q repairs production visualisation regressions", Phase21QRepairsProductionVisualisationRegressions),
|
||||
("Phase 21Q view keeps panels bounded and real controls hidden", Phase21QViewKeepsPanelsBoundedAndRealControlsHidden),
|
||||
("Phase 21R replay uses persisted data only", Phase21RReplayUsesPersistedDataOnly),
|
||||
("Phase 21R scene browser and panels are replay friendly", Phase21RSceneBrowserAndPanelsAreReplayFriendly),
|
||||
("Phase 21S uses durable Story Memory assignments", Phase21SUsesDurableStoryMemoryAssignments),
|
||||
("Lean import removes replay links while retaining progress", LeanImportRemovesReplayLinksWhileRetainingProgress),
|
||||
("Phase 21T repairs durable character understanding", Phase21TRepairsDurableCharacterUnderstanding),
|
||||
("Phase 21U extracts mandatory character appearance fixtures", Phase21UExtractsMandatoryCharacterAppearanceFixtures),
|
||||
("Phase 21U enforces alias age semantic and UI reset rules", Phase21UEnforcesAliasAgeSemanticAndUiResetRules),
|
||||
("Story Intelligence visual polish keeps labels fixed and semantics specific", StoryIntelligenceVisualPolishKeepsLabelsFixedAndSemanticsSpecific),
|
||||
("Character sex hints are conservative and reviewable", CharacterSexHintsAreConservativeAndReviewable),
|
||||
("Character discovery does not truncate scan review candidates", CharacterDiscoveryDoesNotTruncateScanReviewCandidates),
|
||||
("Character discovery rejects sentence-start words", CharacterDiscoveryRejectsSentenceStartWords),
|
||||
@ -82,9 +75,6 @@ var tests = new (string Name, Action Test)[]
|
||||
("Name Intelligence repository uses stored procedures", NameIntelligenceRepositoryUsesStoredProcedures),
|
||||
("Name Intelligence review warning is wired", NameIntelligenceReviewWarningIsWired),
|
||||
("Scan review post supports full-book form submissions", ScanReviewPostSupportsFullBookFormSubmissions),
|
||||
("Story Intelligence experience boot does not serialise live model", StoryIntelligenceExperienceBootDoesNotSerialiseLiveModel),
|
||||
("Live visualisation strips illustration diagnostics", LiveVisualisationStripsIllustrationDiagnostics),
|
||||
("Illustration evidence trace does not recursively store previous evidence", IllustrationEvidenceTraceDoesNotRecursivelyStorePreviousEvidence),
|
||||
("StoryPosition rejects incomplete or invalid runtime scene positions", StoryPositionRejectsInvalidRuntimeScenePositions),
|
||||
("StoryPosition builds canonical runtime scene navigation", StoryPositionBuildsCanonicalRuntimeSceneNavigation),
|
||||
("StoryPosition derives canonical scene context identity", StoryPositionDerivesCanonicalSceneContextIdentity),
|
||||
@ -957,21 +947,19 @@ static void IllustrationStarterGenerationPlanSkipsSuccessfulItems()
|
||||
Assert(IllustrationStarterBatchDefinition.IsStarterCodeEligibleForGeneration(failedAsset.StableCode, [generatedCharacter, failedAsset]), "Failed starter should be eligible.");
|
||||
}
|
||||
|
||||
static void StoryIntelligencePrototypeUsesStarterIllustrationCodes()
|
||||
static void StoryIntelligenceVisualisationArchiveIsPreserved()
|
||||
{
|
||||
var prototype = StoryIntelligenceExperiencePrototypeData.Build();
|
||||
var codes = prototype.Scenes
|
||||
.SelectMany(scene => scene.Characters.Select(character => character.LibraryCode)
|
||||
.Concat([scene.Location.LibraryCode])
|
||||
.Concat(scene.Assets.Select(asset => asset.LibraryCode)))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
var root = Path.Combine(AppContext.BaseDirectory, "../../../../");
|
||||
var archive = Path.Combine(root, "docs/story-intelligence/import-visualisation-archive.md");
|
||||
var archiveSource = Path.Combine(root, "docs/story-intelligence/visualisation-archive/runtime-source");
|
||||
|
||||
Assert(codes.Count > 0, "Prototype should carry explicit library codes.");
|
||||
Assert(codes.All(code => IllustrationStarterBatchDefinition.StarterCodes.Contains(code)), "Prototype requested a stable code that is not in the starter library.");
|
||||
Assert(prototype.Scenes.SelectMany(scene => scene.Characters).All(character => character.ImagePath.EndsWith(".svg", StringComparison.OrdinalIgnoreCase)), "Character SVG fallback should remain before library resolution.");
|
||||
Assert(prototype.Scenes.All(scene => scene.Location.ImagePath.EndsWith(".svg", StringComparison.OrdinalIgnoreCase)), "Location SVG fallback should remain before library resolution.");
|
||||
Assert(prototype.Scenes.SelectMany(scene => scene.Assets).All(asset => asset.ImagePath.EndsWith(".svg", StringComparison.OrdinalIgnoreCase)), "Asset SVG fallback should remain before library resolution.");
|
||||
Assert(File.Exists(archive), "Visualisation archive document should be preserved.");
|
||||
var text = File.ReadAllText(archive);
|
||||
Assert(text.Contains("must never cause additional analysis or AI work", StringComparison.OrdinalIgnoreCase), "Archive must record the passive-observer architectural lesson.");
|
||||
Assert(File.Exists(Path.Combine(archiveSource, "StoryIntelligenceExperience.cshtml")), "Archived Razor markup should be preserved outside runtime.");
|
||||
Assert(File.Exists(Path.Combine(archiveSource, "story-intelligence-experience-prototype.css")), "Archived CSS should be preserved outside runtime.");
|
||||
Assert(File.Exists(Path.Combine(archiveSource, "story-intelligence-experience-prototype.js")), "Archived JavaScript should be preserved outside runtime.");
|
||||
Assert(Directory.Exists(Path.Combine(archiveSource, "prototype-assets")), "Archived prototype assets should be preserved outside runtime.");
|
||||
}
|
||||
|
||||
static void IllustrationMatchingRejectsAdultMasculineUnknownArtForBeth()
|
||||
@ -1187,12 +1175,32 @@ static StoryMemoryCharacterAttribute Attribute(string type, string value, decima
|
||||
static IReadOnlyList<string> Rejected(CharacterEvidenceProfile evidence, IllustrationCharacterMetadata metadata)
|
||||
=> StoryIntelligenceIllustrationCompatibility.CharacterHardRejections(evidence, metadata, isNamedCharacter: true, alreadyAssignedToAnotherSignificantCharacter: false);
|
||||
|
||||
static void StoryIntelligenceSimulationModeRemainsAvailable()
|
||||
static void StoryIntelligenceVisualisationRuntimeIsRemoved()
|
||||
{
|
||||
var prototype = StoryIntelligenceExperiencePrototypeData.Build();
|
||||
var root = Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine");
|
||||
Assert(!File.Exists(Path.Combine(root, "Controllers/DevelopmentController.cs")), "Development visualisation controller should not be active.");
|
||||
Assert(!File.Exists(Path.Combine(root, "Controllers/StoryIntelligenceVisualisationController.cs")), "Visualisation snapshot API controller should not be active.");
|
||||
Assert(!File.Exists(Path.Combine(root, "Views/Development/StoryIntelligenceExperience.cshtml")), "Visualisation Razor view should not be active.");
|
||||
Assert(!File.Exists(Path.Combine(root, "wwwroot/js/story-intelligence-experience-prototype.js")), "Visualisation JavaScript should not be served from runtime.");
|
||||
Assert(!File.Exists(Path.Combine(root, "wwwroot/css/story-intelligence-experience-prototype.css")), "Visualisation CSS should not be served from runtime.");
|
||||
Assert(!Directory.Exists(Path.Combine(root, "wwwroot/images/story-intelligence/prototype")), "Prototype SVG assets should not be served from runtime.");
|
||||
|
||||
Assert(prototype.Scenes.Count >= 7, "Simulation should keep the existing multi-scene visual dataset.");
|
||||
Assert(prototype.Scenes.Any(scene => scene.PovCharacterId == "rosie"), "Simulation should still exercise POV transitions.");
|
||||
var program = File.ReadAllText(Path.Combine(root, "Program.cs"));
|
||||
Assert(!program.Contains("IStoryIntelligenceVisualisationSnapshotService", StringComparison.Ordinal), "Visualisation snapshot service should not be registered.");
|
||||
Assert(!program.Contains("IStoryIntelligenceIllustrationMatchingService", StringComparison.Ordinal), "Visualisation illustration matching adapter should not be registered.");
|
||||
}
|
||||
|
||||
static void LeanImportRemovesReplayLinksWhileRetainingProgress()
|
||||
{
|
||||
var root = Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine");
|
||||
var progressView = File.ReadAllText(Path.Combine(root, "Views/Onboarding/StoryIntelligenceProgress.cshtml"));
|
||||
var progressScript = Path.Combine(root, "wwwroot/js/story-intelligence-progress.js");
|
||||
var program = File.ReadAllText(Path.Combine(root, "Program.cs"));
|
||||
|
||||
Assert(!progressView.Contains("Replay Story Intelligence", StringComparison.Ordinal), "Ordinary import progress should not link to replay visualisation.");
|
||||
Assert(!progressView.Contains("/Development/StoryIntelligenceExperience", StringComparison.Ordinal), "Ordinary import progress should not expose the development visualisation route.");
|
||||
Assert(File.Exists(progressScript), "Lean import should retain the operational progress script.");
|
||||
Assert(program.Contains("app.MapHub<StoryIntelligenceHub>", StringComparison.Ordinal), "Lean import should retain the shared Story Intelligence progress hub.");
|
||||
}
|
||||
|
||||
static void StoryIntelligenceDoesNotRetryExhaustedBillingQuota()
|
||||
@ -1205,31 +1213,7 @@ static void StoryIntelligenceDoesNotRetryExhaustedBillingQuota()
|
||||
|
||||
static void StoryIntelligenceVisualisationContractOmitsRawManuscriptText()
|
||||
{
|
||||
var model = new StoryIntelligenceExperiencePrototypeViewModel
|
||||
{
|
||||
Mode = "real",
|
||||
RunId = 42,
|
||||
ChangeToken = "stable-token",
|
||||
ProjectName = "Project",
|
||||
BookTitle = "Book",
|
||||
Scenes =
|
||||
[
|
||||
new StoryIntelligenceExperienceSceneState
|
||||
{
|
||||
Id = "scene-result-1",
|
||||
SceneNumber = 1,
|
||||
Title = "Analysed scene",
|
||||
Summary = "Safe summary",
|
||||
Location = new StoryIntelligenceExperienceLocationState { Id = "location-result-room", Name = "Room" }
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(model, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
|
||||
Assert(json.Contains("\"changeToken\":\"stable-token\"", StringComparison.Ordinal), "Snapshot contract should expose a change token.");
|
||||
Assert(!json.Contains("sourceText", StringComparison.OrdinalIgnoreCase), "Snapshot contract should not expose raw manuscript source text.");
|
||||
Assert(!json.Contains("rawResponseJson", StringComparison.OrdinalIgnoreCase), "Snapshot contract should not expose raw AI responses.");
|
||||
Assert(!json.Contains("prompt", StringComparison.OrdinalIgnoreCase), "Snapshot contract should not expose prompts.");
|
||||
Assert(true, "Historical visualisation contract helper retained only to minimise churn after visualisation removal.");
|
||||
}
|
||||
|
||||
static void IllustrationBulkRetryResultReportsCounts()
|
||||
@ -1459,42 +1443,7 @@ static void StoryIntelligenceExperienceBootDoesNotSerialiseLiveModel()
|
||||
|
||||
static void LiveVisualisationStripsIllustrationDiagnostics()
|
||||
{
|
||||
var large = new string('x', 200_000);
|
||||
var model = new StoryIntelligenceExperiencePrototypeViewModel
|
||||
{
|
||||
Diagnostics = new StoryIntelligenceExperienceSnapshotDiagnostics
|
||||
{
|
||||
CharacterConsistencyReport = large
|
||||
},
|
||||
Scenes =
|
||||
[
|
||||
new StoryIntelligenceExperienceSceneState
|
||||
{
|
||||
Id = "scene-result-1",
|
||||
Location = new StoryIntelligenceExperienceLocationState(),
|
||||
Characters =
|
||||
[
|
||||
new StoryIntelligenceExperienceCharacterState
|
||||
{
|
||||
Id = "character-result-beth",
|
||||
Name = "Beth",
|
||||
ImageResolution = new StoryIntelligenceExperienceImageResolution
|
||||
{
|
||||
RejectedCandidates = large,
|
||||
PreviousEvidence = large,
|
||||
CurrentEvidence = large,
|
||||
EvidenceWarnings = large
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
StoryIntelligenceVisualisationSnapshotService.StripLiveDiagnostics(model);
|
||||
var json = JsonSerializer.Serialize(model, JsonOptions());
|
||||
Assert(!json.Contains(large, StringComparison.Ordinal), "Live snapshot should not contain large illustration diagnostic strings.");
|
||||
Assert(json.Length < 20_000, $"Live snapshot remained too large after stripping diagnostics: {json.Length:N0} chars.");
|
||||
Assert(true, "Historical helper retained only to minimise churn after visualisation removal.");
|
||||
}
|
||||
|
||||
static void IllustrationEvidenceTraceDoesNotRecursivelyStorePreviousEvidence()
|
||||
@ -1633,11 +1582,9 @@ static void Phase21TRepairsDurableCharacterUnderstanding()
|
||||
Assert(StoryIntelligenceIllustrationCompatibility.IsGroupEntity("pair of girls"), "Pair of girls should not reserve a unique character portrait.");
|
||||
|
||||
var service = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine/Services/StoryMemoryServices.cs"));
|
||||
var snapshot = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine/Services/StoryIntelligenceVisualisationSnapshotService.cs"));
|
||||
Assert(service.Contains("BuildCrossSceneIdentityAliases", StringComparison.Ordinal), "Rebuild should repair cross-scene descriptive-to-named character identities from saved analysis.");
|
||||
Assert(service.Contains("ExtensionEvidence(sceneCharacter.ExtensionData)", StringComparison.Ordinal), "Saved scene appearance extension data must be included in durable evidence.");
|
||||
Assert(!snapshot.Contains("memoryCharacter", StringComparison.Ordinal), "Visualisation must not carry memory-only characters into scenes without scene evidence.");
|
||||
Assert(snapshot.Contains("ApplyPersistedStoryMemoryAssignmentsAsync(importSessionId, model)", StringComparison.Ordinal), "Live and replay must apply durable Story Memory assignments.");
|
||||
Assert(!File.Exists(Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine/Services/StoryIntelligenceVisualisationSnapshotService.cs")), "Visualisation snapshot service should stay removed.");
|
||||
}
|
||||
|
||||
static void Phase21UExtractsMandatoryCharacterAppearanceFixtures()
|
||||
|
||||
@ -1,193 +0,0 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using PlotLine.Data;
|
||||
using PlotLine.Models;
|
||||
using PlotLine.Services;
|
||||
using PlotLine.ViewModels;
|
||||
|
||||
namespace PlotLine.Controllers;
|
||||
|
||||
[Authorize(Policy = "AdminOnly")]
|
||||
[Route("Development")]
|
||||
public sealed class DevelopmentController(
|
||||
IWebHostEnvironment environment,
|
||||
ICurrentUserService currentUser,
|
||||
IIllustrationLibraryService illustrationLibrary,
|
||||
IStoryMemoryService storyMemory,
|
||||
IStoryMemoryRepository storyMemoryRepository,
|
||||
IStoryIntelligenceVisualisationSnapshotService snapshots) : Controller
|
||||
{
|
||||
[HttpGet("StoryIntelligenceExperience")]
|
||||
public async Task<IActionResult> StoryIntelligenceExperience([FromQuery] string? mode, [FromQuery] int? importSessionId, [FromQuery] int? runId)
|
||||
{
|
||||
if (!environment.IsDevelopment())
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var requestedMode = string.IsNullOrWhiteSpace(mode) ? "real" : mode.Trim();
|
||||
var simulationRequested = string.Equals(requestedMode, "simulation", StringComparison.OrdinalIgnoreCase);
|
||||
var replayRequested = string.Equals(requestedMode, "replay", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (!simulationRequested && importSessionId.HasValue && currentUser.UserId is int userId)
|
||||
{
|
||||
var realModel = replayRequested
|
||||
? await snapshots.BuildReplaySnapshotAsync(importSessionId.Value, userId)
|
||||
: await snapshots.BuildImportSessionSnapshotAsync(importSessionId.Value, userId);
|
||||
if (realModel is null)
|
||||
{
|
||||
return View(ErrorModel(
|
||||
requestedMode,
|
||||
importSessionId,
|
||||
$"Import Session {importSessionId.Value:N0} could not be found for the current user, or it has no persisted chapter analysis runs yet."));
|
||||
}
|
||||
|
||||
return View(realModel);
|
||||
}
|
||||
|
||||
if (!simulationRequested && runId.HasValue && currentUser.UserId is int legacyUserId)
|
||||
{
|
||||
var realModel = await snapshots.BuildSnapshotAsync(runId.Value, legacyUserId);
|
||||
if (realModel is null)
|
||||
{
|
||||
return View(ErrorModel(
|
||||
requestedMode,
|
||||
null,
|
||||
$"Run {runId.Value:N0} could not be found for the current user. Live visualisation URLs should use importSessionId for book imports."));
|
||||
}
|
||||
|
||||
return View(realModel);
|
||||
}
|
||||
|
||||
if (simulationRequested)
|
||||
{
|
||||
var model = StoryIntelligenceExperiencePrototypeData.Build();
|
||||
await illustrationLibrary.ApplyApprovedIllustrationsAsync(model);
|
||||
return View(model);
|
||||
}
|
||||
|
||||
if (currentUser.UserId is int selectorUserId)
|
||||
{
|
||||
var sessions = await snapshots.ListImportSessionOptionsAsync(selectorUserId);
|
||||
return View(new StoryIntelligenceExperiencePrototypeViewModel
|
||||
{
|
||||
Mode = "selector",
|
||||
AvailableRuns = sessions,
|
||||
ProjectName = "Development",
|
||||
BookTitle = "Story Intelligence",
|
||||
SnapshotMessage = sessions.Count == 0
|
||||
? "No persisted Story Intelligence import sessions were found for the current user. Start a real import, then refresh this page."
|
||||
: "Choose a Story Intelligence import session, or explicitly open simulation mode.",
|
||||
Diagnostics = new StoryIntelligenceExperienceSnapshotDiagnostics
|
||||
{
|
||||
RequestedMode = requestedMode,
|
||||
RequestedImportSessionId = importSessionId,
|
||||
SnapshotSource = "Real"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return View(ErrorModel(requestedMode, importSessionId, "No development user is signed in."));
|
||||
}
|
||||
|
||||
[HttpPost("StoryIntelligenceExperience/Rebuild")]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> RebuildStoryIntelligenceExperience(
|
||||
[FromForm] int importSessionId,
|
||||
[FromForm] string? rebuildScope,
|
||||
[FromForm] bool dryRun = false,
|
||||
[FromForm] bool queueDemand = false,
|
||||
[FromForm] int? maxDemandToQueue = null,
|
||||
[FromForm] string? appearancePreference = null)
|
||||
{
|
||||
if (!environment.IsDevelopment())
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (importSessionId <= 0 || currentUser.UserId is not int userId)
|
||||
{
|
||||
return BadRequest("A valid importSessionId is required.");
|
||||
}
|
||||
|
||||
var rebuild = await storyMemory.RebuildImportSessionAsync(
|
||||
importSessionId,
|
||||
userId,
|
||||
new StoryMemoryRebuildOptions
|
||||
{
|
||||
DryRun = dryRun,
|
||||
RebuildAll = !string.Equals(rebuildScope, "incremental", StringComparison.OrdinalIgnoreCase),
|
||||
QueueDemand = queueDemand,
|
||||
MaxDemandToQueue = maxDemandToQueue,
|
||||
AppearancePreference = string.IsNullOrWhiteSpace(appearancePreference)
|
||||
? StoryMemoryAppearancePreferences.Balanced
|
||||
: appearancePreference.Trim()
|
||||
},
|
||||
HttpContext.RequestAborted);
|
||||
if (rebuild.DryRun)
|
||||
{
|
||||
TempData["StoryIntelligenceReplayMessage"] = $"Dry run complete. {rebuild.SceneResultsRead:N0} stored scene result(s), {rebuild.SceneResultsProcessed:N0} parseable. No database changes, AI calls, or image credits.";
|
||||
}
|
||||
else
|
||||
{
|
||||
TempData["StoryIntelligenceReplayMessage"] = $"Durable Story Memory rebuilt. {rebuild.SceneResultsProcessed:N0} scene result(s) processed, {rebuild.AssignmentsCreatedOrValidated:N0} assignment(s), {rebuild.DemandsCreated:N0} demand record(s), {rebuild.AiCalls:N0} AI calls.";
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(StoryIntelligenceExperience), new { importSessionId, mode = "replay" });
|
||||
}
|
||||
|
||||
[HttpGet("StoryMemoryDiagnostics")]
|
||||
public async Task<IActionResult> StoryMemoryDiagnostics([FromQuery] int importSessionId)
|
||||
{
|
||||
if (!environment.IsDevelopment())
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (importSessionId <= 0)
|
||||
{
|
||||
return BadRequest("A valid importSessionId is required.");
|
||||
}
|
||||
|
||||
var diagnostics = await storyMemoryRepository.GetDiagnosticsAsync(importSessionId);
|
||||
return Json(diagnostics);
|
||||
}
|
||||
|
||||
[HttpGet("StoryIntelligenceIllustrationDiagnostics")]
|
||||
public async Task<IActionResult> StoryIntelligenceIllustrationDiagnostics([FromQuery] int importSessionId)
|
||||
{
|
||||
if (!environment.IsDevelopment())
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (importSessionId <= 0 || currentUser.UserId is not int userId)
|
||||
{
|
||||
return BadRequest("A valid importSessionId is required.");
|
||||
}
|
||||
|
||||
var model = await snapshots.BuildImportSessionSnapshotAsync(importSessionId, userId);
|
||||
if (model is null)
|
||||
{
|
||||
return NotFound($"Import Session {importSessionId:N0} could not be found for the current user, or it has no persisted chapter analysis runs yet.");
|
||||
}
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
private static StoryIntelligenceExperiencePrototypeViewModel ErrorModel(string requestedMode, int? importSessionId, string message)
|
||||
=> new()
|
||||
{
|
||||
Mode = "error",
|
||||
ProjectName = "Development",
|
||||
BookTitle = "Story Intelligence",
|
||||
SnapshotMessage = message,
|
||||
Diagnostics = new StoryIntelligenceExperienceSnapshotDiagnostics
|
||||
{
|
||||
RequestedMode = requestedMode,
|
||||
RequestedImportSessionId = importSessionId,
|
||||
SnapshotSource = "Real",
|
||||
Warning = message
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -1,61 +0,0 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using PlotLine.Services;
|
||||
|
||||
namespace PlotLine.Controllers;
|
||||
|
||||
[Authorize]
|
||||
[Route("api/story-intelligence")]
|
||||
public sealed class StoryIntelligenceVisualisationController(
|
||||
ICurrentUserService currentUser,
|
||||
IStoryIntelligenceVisualisationSnapshotService snapshots,
|
||||
ILogger<StoryIntelligenceVisualisationController> logger) : ControllerBase
|
||||
{
|
||||
[HttpGet("runs/{runId:int}/visualisation-snapshot")]
|
||||
public async Task<IActionResult> GetSnapshot(int runId)
|
||||
{
|
||||
if (currentUser.UserId is not int userId)
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var snapshot = await snapshots.BuildSnapshotAsync(runId, userId);
|
||||
if (snapshot is not null)
|
||||
{
|
||||
var current = snapshot.Scenes.FirstOrDefault(scene => scene.IsCurrent) ?? snapshot.Scenes.LastOrDefault();
|
||||
logger.LogInformation(
|
||||
"Story Intelligence browser endpoint synchronisation: import session {ImportSessionId}; run {RunId}; current chapter {CurrentChapter}; current scene {CurrentScene}; scene result {SceneResultId}; snapshot version Endpoint; change token {ChangeToken}.",
|
||||
snapshot.ImportSessionId,
|
||||
snapshot.RunId,
|
||||
current?.ChapterLabel,
|
||||
current?.SceneNumber,
|
||||
snapshot.Diagnostics.SelectedCurrentSceneResultId,
|
||||
snapshot.ChangeToken);
|
||||
}
|
||||
return snapshot is null ? NotFound() : Ok(snapshot);
|
||||
}
|
||||
|
||||
[HttpGet("import-sessions/{importSessionId:int}/visualisation-snapshot")]
|
||||
public async Task<IActionResult> GetImportSessionSnapshot(int importSessionId)
|
||||
{
|
||||
if (currentUser.UserId is not int userId)
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var snapshot = await snapshots.BuildImportSessionSnapshotAsync(importSessionId, userId);
|
||||
if (snapshot is not null)
|
||||
{
|
||||
var current = snapshot.Scenes.FirstOrDefault(scene => scene.IsCurrent) ?? snapshot.Scenes.LastOrDefault();
|
||||
logger.LogInformation(
|
||||
"Story Intelligence browser endpoint synchronisation: import session {ImportSessionId}; run {RunId}; current chapter {CurrentChapter}; current scene {CurrentScene}; scene result {SceneResultId}; snapshot version Endpoint; change token {ChangeToken}.",
|
||||
snapshot.ImportSessionId,
|
||||
snapshot.RunId,
|
||||
current?.ChapterLabel,
|
||||
current?.SceneNumber,
|
||||
snapshot.Diagnostics.SelectedCurrentSceneResultId,
|
||||
snapshot.ChangeToken);
|
||||
}
|
||||
return snapshot is null ? NotFound() : Ok(snapshot);
|
||||
}
|
||||
}
|
||||
@ -1,117 +0,0 @@
|
||||
using Dapper;
|
||||
using PlotLine.Models;
|
||||
|
||||
namespace PlotLine.Data;
|
||||
|
||||
public interface IStoryIntelligenceIllustrationMatchingRepository
|
||||
{
|
||||
Task<IReadOnlyList<StoryIntelligenceCharacterIllustrationAssignment>> ListAssignmentsAsync(int projectId);
|
||||
Task<StoryIntelligenceCharacterIllustrationAssignment> UpsertAssignmentAsync(StoryIntelligenceCharacterIllustrationAssignmentSave request);
|
||||
Task<StoryIntelligenceIllustrationDemand> UpsertDemandAsync(StoryIntelligenceIllustrationDemandRequest request);
|
||||
}
|
||||
|
||||
public sealed class StoryIntelligenceIllustrationMatchingRepository(ISqlConnectionFactory connectionFactory) : IStoryIntelligenceIllustrationMatchingRepository
|
||||
{
|
||||
public async Task<IReadOnlyList<StoryIntelligenceCharacterIllustrationAssignment>> ListAssignmentsAsync(int projectId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
var rows = await connection.QueryAsync<StoryIntelligenceCharacterIllustrationAssignment>(
|
||||
"""
|
||||
SELECT
|
||||
StoryIntelligenceCharacterIllustrationAssignmentID,
|
||||
ProjectID,
|
||||
CharacterKey,
|
||||
CharacterName,
|
||||
IllustrationLibraryItemID,
|
||||
StableCode,
|
||||
MatchConfidence,
|
||||
MatchingScore,
|
||||
CAST(NULL AS nvarchar(max)) AS EvidenceJson,
|
||||
CAST(NULL AS nvarchar(max)) AS CandidateDiagnosticsJson,
|
||||
ReasonChosen,
|
||||
ReasonReassigned,
|
||||
IsActive,
|
||||
CreatedUtc,
|
||||
UpdatedUtc,
|
||||
AssignmentStatus,
|
||||
AssignedUtc,
|
||||
LastValidatedUtc,
|
||||
EvidenceVersion,
|
||||
IsFallback
|
||||
FROM dbo.StoryIntelligenceCharacterIllustrationAssignments
|
||||
WHERE ProjectID = @ProjectID
|
||||
AND IsActive = 1;
|
||||
""",
|
||||
new { ProjectID = projectId });
|
||||
return rows.ToList();
|
||||
}
|
||||
|
||||
public async Task<StoryIntelligenceCharacterIllustrationAssignment> UpsertAssignmentAsync(StoryIntelligenceCharacterIllustrationAssignmentSave request)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleAsync<StoryIntelligenceCharacterIllustrationAssignment>(
|
||||
"""
|
||||
SET XACT_ABORT ON;
|
||||
|
||||
MERGE dbo.StoryIntelligenceCharacterIllustrationAssignments WITH (HOLDLOCK) AS target
|
||||
USING (SELECT @ProjectID AS ProjectID, @CharacterKey AS CharacterKey) AS source
|
||||
ON target.ProjectID = source.ProjectID
|
||||
AND target.CharacterKey = source.CharacterKey
|
||||
AND target.IsActive = 1
|
||||
WHEN MATCHED THEN
|
||||
UPDATE SET
|
||||
CharacterName = @CharacterName,
|
||||
IllustrationLibraryItemID = @IllustrationLibraryItemID,
|
||||
StableCode = @StableCode,
|
||||
MatchConfidence = @MatchConfidence,
|
||||
MatchingScore = @MatchingScore,
|
||||
EvidenceJson = @EvidenceJson,
|
||||
CandidateDiagnosticsJson = @CandidateDiagnosticsJson,
|
||||
ReasonChosen = @ReasonChosen,
|
||||
ReasonReassigned = @ReasonReassigned,
|
||||
AssignmentStatus = CASE WHEN @IllustrationLibraryItemID IS NULL THEN N'Fallback' ELSE N'Assigned' END,
|
||||
LastValidatedUtc = SYSUTCDATETIME(),
|
||||
EvidenceVersion = N'21K',
|
||||
IsFallback = CASE WHEN @IllustrationLibraryItemID IS NULL THEN 1 ELSE 0 END,
|
||||
UpdatedUtc = SYSUTCDATETIME()
|
||||
WHEN NOT MATCHED THEN
|
||||
INSERT (ProjectID, CharacterKey, CharacterName, IllustrationLibraryItemID, StableCode, MatchConfidence, MatchingScore, EvidenceJson, CandidateDiagnosticsJson, ReasonChosen, ReasonReassigned, AssignmentStatus, AssignedUtc, LastValidatedUtc, EvidenceVersion, IsFallback)
|
||||
VALUES (@ProjectID, @CharacterKey, @CharacterName, @IllustrationLibraryItemID, @StableCode, @MatchConfidence, @MatchingScore, @EvidenceJson, @CandidateDiagnosticsJson, @ReasonChosen, @ReasonReassigned, CASE WHEN @IllustrationLibraryItemID IS NULL THEN N'Fallback' ELSE N'Assigned' END, SYSUTCDATETIME(), SYSUTCDATETIME(), N'21K', CASE WHEN @IllustrationLibraryItemID IS NULL THEN 1 ELSE 0 END);
|
||||
|
||||
SELECT *
|
||||
FROM dbo.StoryIntelligenceCharacterIllustrationAssignments
|
||||
WHERE ProjectID = @ProjectID
|
||||
AND CharacterKey = @CharacterKey
|
||||
AND IsActive = 1;
|
||||
""",
|
||||
request);
|
||||
}
|
||||
|
||||
public async Task<StoryIntelligenceIllustrationDemand> UpsertDemandAsync(StoryIntelligenceIllustrationDemandRequest request)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleAsync<StoryIntelligenceIllustrationDemand>(
|
||||
"""
|
||||
SET XACT_ABORT ON;
|
||||
|
||||
MERGE dbo.StoryIntelligenceIllustrationDemands WITH (HOLDLOCK) AS target
|
||||
USING (SELECT @ProjectID AS ProjectID, @DemandKey AS DemandKey) AS source
|
||||
ON ISNULL(target.ProjectID, -1) = ISNULL(source.ProjectID, -1)
|
||||
AND target.DemandKey = source.DemandKey
|
||||
WHEN MATCHED THEN
|
||||
UPDATE SET
|
||||
RequestCount = RequestCount + 1,
|
||||
LastRequestedUtc = SYSUTCDATETIME(),
|
||||
UpdatedUtc = SYSUTCDATETIME()
|
||||
WHEN NOT MATCHED THEN
|
||||
INSERT (ProjectID, DemandKey, AgeBand, Presentation, HairColour, SkinTone, HairLength, Glasses, FacialHair)
|
||||
VALUES (@ProjectID, @DemandKey, @AgeBand, @Presentation, @HairColour, @SkinTone, @HairLength, @Glasses, @FacialHair);
|
||||
|
||||
SELECT *
|
||||
FROM dbo.StoryIntelligenceIllustrationDemands
|
||||
WHERE ISNULL(ProjectID, -1) = ISNULL(@ProjectID, -1)
|
||||
AND DemandKey = @DemandKey;
|
||||
""",
|
||||
request);
|
||||
}
|
||||
}
|
||||
@ -12,6 +12,7 @@ public interface IStoryIntelligencePipelineRepository
|
||||
Task<IReadOnlyList<StoryIntelligenceBookPipelineState>> ListForUserAsync(int userId);
|
||||
Task<StoryIntelligenceBookPipelineState> UpsertAsync(StoryIntelligenceBookPipelineSaveRequest request);
|
||||
Task<IReadOnlyList<StoryIntelligenceCommittedRunSummary>> ListCommittedRunsByBookAsync(int bookId, int userId);
|
||||
Task<StoryIntelligenceImportNotificationTarget?> TryBeginCompletionNotificationAsync(int bookId, int userId);
|
||||
}
|
||||
|
||||
public sealed class StoryIntelligencePipelineRepository(ISqlConnectionFactory connectionFactory) : IStoryIntelligencePipelineRepository
|
||||
@ -81,4 +82,13 @@ public sealed class StoryIntelligencePipelineRepository(ISqlConnectionFactory co
|
||||
commandType: CommandType.StoredProcedure);
|
||||
return rows.ToList();
|
||||
}
|
||||
|
||||
public async Task<StoryIntelligenceImportNotificationTarget?> TryBeginCompletionNotificationAsync(int bookId, int userId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleOrDefaultAsync<StoryIntelligenceImportNotificationTarget>(
|
||||
"dbo.StoryIntelligenceBookPipeline_TryBeginCompletionNotification",
|
||||
new { BookID = bookId, UserID = userId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,70 +0,0 @@
|
||||
namespace PlotLine.Models;
|
||||
|
||||
public sealed class StoryIntelligenceCharacterIllustrationAssignment
|
||||
{
|
||||
public int StoryIntelligenceCharacterIllustrationAssignmentID { get; init; }
|
||||
public int ProjectID { get; init; }
|
||||
public string CharacterKey { get; init; } = string.Empty;
|
||||
public string CharacterName { get; init; } = string.Empty;
|
||||
public int? IllustrationLibraryItemID { get; init; }
|
||||
public string? StableCode { get; init; }
|
||||
public decimal MatchConfidence { get; init; }
|
||||
public int MatchingScore { get; init; }
|
||||
public string? EvidenceJson { get; init; }
|
||||
public string? CandidateDiagnosticsJson { get; init; }
|
||||
public string? ReasonChosen { get; init; }
|
||||
public string? ReasonReassigned { get; init; }
|
||||
public int? ImportSessionID { get; init; }
|
||||
public string AssignmentStatus { get; init; } = "Assigned";
|
||||
public DateTime? AssignedUtc { get; init; }
|
||||
public DateTime? LastValidatedUtc { get; init; }
|
||||
public DateTime? InvalidatedUtc { get; init; }
|
||||
public string? InvalidationReason { get; init; }
|
||||
public string EvidenceVersion { get; init; } = "21K";
|
||||
public bool IsFallback { get; init; }
|
||||
public bool IsActive { get; init; }
|
||||
public DateTime CreatedUtc { get; init; }
|
||||
public DateTime UpdatedUtc { get; init; }
|
||||
}
|
||||
|
||||
public sealed record StoryIntelligenceCharacterIllustrationAssignmentSave(
|
||||
int ProjectID,
|
||||
string CharacterKey,
|
||||
string CharacterName,
|
||||
int? IllustrationLibraryItemID,
|
||||
string? StableCode,
|
||||
decimal MatchConfidence,
|
||||
int MatchingScore,
|
||||
string? EvidenceJson,
|
||||
string? CandidateDiagnosticsJson,
|
||||
string? ReasonChosen,
|
||||
string? ReasonReassigned);
|
||||
|
||||
public sealed class StoryIntelligenceIllustrationDemand
|
||||
{
|
||||
public int StoryIntelligenceIllustrationDemandID { get; init; }
|
||||
public int? ProjectID { get; init; }
|
||||
public string DemandKey { get; init; } = string.Empty;
|
||||
public string AgeBand { get; init; } = string.Empty;
|
||||
public string Presentation { get; init; } = string.Empty;
|
||||
public string HairColour { get; init; } = string.Empty;
|
||||
public string SkinTone { get; init; } = string.Empty;
|
||||
public string? HairLength { get; init; }
|
||||
public string? Glasses { get; init; }
|
||||
public string? FacialHair { get; init; }
|
||||
public int RequestCount { get; init; }
|
||||
public DateTime LastRequestedUtc { get; init; }
|
||||
public DateTime? QueuedUtc { get; init; }
|
||||
public string GenerationSource { get; init; } = "Demand generation";
|
||||
}
|
||||
|
||||
public sealed record StoryIntelligenceIllustrationDemandRequest(
|
||||
int? ProjectID,
|
||||
string DemandKey,
|
||||
string AgeBand,
|
||||
string Presentation,
|
||||
string HairColour,
|
||||
string SkinTone,
|
||||
string? HairLength,
|
||||
string? Glasses,
|
||||
string? FacialHair);
|
||||
@ -69,6 +69,22 @@ public sealed class StoryIntelligenceBookPipelineState
|
||||
&& string.Equals(LastCompletedStage, StoryIntelligencePipelineStages.KnowledgeImport, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public sealed class StoryIntelligenceImportNotificationTarget
|
||||
{
|
||||
public int ImportSessionID { get; init; }
|
||||
public int UserID { get; init; }
|
||||
public int ProjectID { get; init; }
|
||||
public string ProjectName { get; init; } = string.Empty;
|
||||
public int BookID { get; init; }
|
||||
public string BookTitle { get; init; } = string.Empty;
|
||||
public string? BookSubtitle { get; init; }
|
||||
public string BookDisplayTitle => BookTitleFormatter.DisplayTitle(BookTitle, BookSubtitle);
|
||||
public int ChapterCount { get; init; }
|
||||
public int CompletedChapterCount { get; init; }
|
||||
public int FailedChapterCount { get; init; }
|
||||
public string NotificationKind { get; init; } = "Success";
|
||||
}
|
||||
|
||||
public sealed class StoryIntelligenceBookPipelineSaveRequest
|
||||
{
|
||||
public int ProjectID { get; init; }
|
||||
|
||||
@ -148,7 +148,6 @@ public class Program
|
||||
builder.Services.AddScoped<IStoryIntelligenceResultRepository, StoryIntelligenceResultRepository>();
|
||||
builder.Services.AddScoped<IStoryIntelligenceSourceRepository, StoryIntelligenceSourceRepository>();
|
||||
builder.Services.AddScoped<IStoryIntelligencePipelineRepository, StoryIntelligencePipelineRepository>();
|
||||
builder.Services.AddScoped<IStoryIntelligenceIllustrationMatchingRepository, StoryIntelligenceIllustrationMatchingRepository>();
|
||||
builder.Services.AddScoped<IStoryMemoryRepository, StoryMemoryRepository>();
|
||||
builder.Services.AddScoped<ICharacterIntelligenceRepository, CharacterIntelligenceRepository>();
|
||||
builder.Services.AddScoped<INameIntelligenceRepository, NameIntelligenceRepository>();
|
||||
@ -225,12 +224,13 @@ public class Program
|
||||
builder.Services.AddScoped<IStoryIntelligenceAssetImportService, StoryIntelligenceAssetImportService>();
|
||||
builder.Services.AddScoped<IStoryIntelligenceRelationshipImportService, StoryIntelligenceRelationshipImportService>();
|
||||
builder.Services.AddScoped<IStoryIntelligenceKnowledgeImportService, StoryIntelligenceKnowledgeImportService>();
|
||||
builder.Services.AddScoped<IStoryIntelligenceVisualisationSnapshotService, StoryIntelligenceVisualisationSnapshotService>();
|
||||
builder.Services.AddScoped<IStoryMemoryService, StoryMemoryService>();
|
||||
builder.Services.AddScoped<ICharacterIntelligenceService, CharacterIntelligenceService>();
|
||||
builder.Services.AddScoped<INameIntelligenceService, NameIntelligenceService>();
|
||||
builder.Services.AddScoped<IStoryIntelligencePipelineStateService, StoryIntelligencePipelineStateService>();
|
||||
builder.Services.AddScoped<IStoryIntelligenceProgressNotifier, StoryIntelligenceProgressNotifier>();
|
||||
builder.Services.AddScoped<IStoryIntelligenceEntitlementService, StoryIntelligenceEntitlementService>();
|
||||
builder.Services.AddScoped<IStoryIntelligenceCompletionNotificationService, StoryIntelligenceCompletionNotificationService>();
|
||||
builder.Services.AddScoped<ISceneImportResolver, SceneImportResolver>();
|
||||
builder.Services.AddScoped<IPersistedStoryIntelligenceRunner, PersistedStoryIntelligenceRunner>();
|
||||
builder.Services.AddScoped<IIllustrationPromptBuilder, IllustrationPromptBuilder>();
|
||||
@ -238,7 +238,6 @@ public class Program
|
||||
builder.Services.AddHttpClient<IIllustrationImageProvider, OpenAIIllustrationImageProvider>();
|
||||
builder.Services.AddScoped<IIllustrationGenerationRunner, IllustrationGenerationRunner>();
|
||||
builder.Services.AddScoped<IIllustrationLibraryService, IllustrationLibraryService>();
|
||||
builder.Services.AddScoped<IStoryIntelligenceIllustrationMatchingService, StoryIntelligenceIllustrationMatchingService>();
|
||||
builder.Services.AddScoped<IProjectAccessService, ProjectAccessService>();
|
||||
builder.Services.AddScoped<ISubscriptionService, SubscriptionService>();
|
||||
builder.Services.AddScoped<IStripeBillingService, StripeBillingService>();
|
||||
|
||||
@ -50,6 +50,7 @@ public interface IEmailService
|
||||
Task SendFeatureRequestAdminReplyAsync(FeatureRequest request, string messagePreview, string link, string actorEmail);
|
||||
Task SendFeatureRequestStatusChangedAsync(FeatureRequest request, string link, string actorEmail);
|
||||
Task SendFeatureRequestUserReplyAsync(FeatureRequest request, string messagePreview, string link, string actorEmail);
|
||||
Task SendStoryIntelligenceImportNotificationAsync(string email, string displayName, StoryIntelligenceImportNotificationTarget target);
|
||||
}
|
||||
|
||||
public interface ICurrentUserService
|
||||
|
||||
@ -180,6 +180,38 @@ public sealed class EmailService(
|
||||
}
|
||||
}
|
||||
|
||||
public Task SendStoryIntelligenceImportNotificationAsync(string email, string displayName, StoryIntelligenceImportNotificationTarget target)
|
||||
{
|
||||
var failed = string.Equals(target.NotificationKind, "Failure", StringComparison.OrdinalIgnoreCase);
|
||||
var subject = failed
|
||||
? $"PlotDirector import needs attention: {target.BookDisplayTitle}"
|
||||
: $"PlotDirector import complete: {target.BookDisplayTitle}";
|
||||
var statusLine = failed
|
||||
? "The manuscript import has stopped with one or more chapters needing attention."
|
||||
: "The manuscript import has finished and is ready for review.";
|
||||
var actionLine = failed
|
||||
? "Please open PlotDirector to review the failed chapter analysis before continuing."
|
||||
: "Please open PlotDirector to review and commit the imported scenes.";
|
||||
|
||||
var body = $"""
|
||||
Hi {DisplayName(displayName)},
|
||||
|
||||
{statusLine}
|
||||
|
||||
Project: {target.ProjectName}
|
||||
Book: {target.BookDisplayTitle}
|
||||
Chapters processed: {target.CompletedChapterCount:N0} of {target.ChapterCount:N0}
|
||||
Failed chapters: {target.FailedChapterCount:N0}
|
||||
|
||||
{actionLine}
|
||||
|
||||
Thanks,
|
||||
The PlotDirector team
|
||||
""";
|
||||
|
||||
return QueueAsync(email, DisplayName(displayName), subject, body, html: null);
|
||||
}
|
||||
|
||||
private async Task QueueAsync(string toEmail, string displayName, string subject, string plainText, string? html)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(toEmail))
|
||||
|
||||
@ -649,14 +649,12 @@ public interface IIllustrationLibraryService
|
||||
Task<IllustrationLibraryActionResult> RegenerateAsync(int illustrationLibraryItemId, bool confirmed);
|
||||
Task<IllustrationLibraryActionResult> DisableAsync(int illustrationLibraryItemId);
|
||||
Task<IllustrationLibraryActionResult> UpdateMetadataAsync(int illustrationLibraryItemId, IllustrationLibraryMetadataForm form);
|
||||
Task ApplyApprovedIllustrationsAsync(StoryIntelligenceExperiencePrototypeViewModel prototype);
|
||||
}
|
||||
|
||||
public sealed class IllustrationLibraryService(
|
||||
IIllustrationLibraryRepository repository,
|
||||
IIllustrationPromptBuilder promptBuilder,
|
||||
IIllustrationImageProvider provider,
|
||||
IUploadStorageService uploadStorage) : IIllustrationLibraryService
|
||||
IIllustrationImageProvider provider) : IIllustrationLibraryService
|
||||
{
|
||||
private const int MaxGenerationAttempts = 3;
|
||||
|
||||
@ -953,115 +951,4 @@ public sealed class IllustrationLibraryService(
|
||||
return new(true, "Illustration metadata updated.");
|
||||
}
|
||||
|
||||
public async Task ApplyApprovedIllustrationsAsync(StoryIntelligenceExperiencePrototypeViewModel prototype)
|
||||
{
|
||||
var items = await repository.ListAsync(new IllustrationLibraryFilter());
|
||||
var lookup = items
|
||||
.Where(item => item.Status is IllustrationLibraryStatuses.Approved or IllustrationLibraryStatuses.Generated)
|
||||
.Where(item => item.IsActive && !item.IsArchived)
|
||||
.Where(item => !item.IsFallbackClassification)
|
||||
.OrderBy(item => item.Status == IllustrationLibraryStatuses.Approved ? 0 : 1)
|
||||
.ThenByDescending(item => item.UpdatedUtc)
|
||||
.GroupBy(item => item.StableCode, StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(group => group.Key, group => group.ToList(), StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var scene in prototype.Scenes)
|
||||
{
|
||||
var locationFallback = scene.Location.ImagePath;
|
||||
var locationResolution = ResolvePrototypeImage(scene.Location.Id, scene.Location.LibraryCode, locationFallback, lookup);
|
||||
scene.Location.ImageResolution = locationResolution;
|
||||
scene.Location.ImagePath = locationResolution.FinalImageUrl ?? locationFallback;
|
||||
|
||||
foreach (var character in scene.Characters)
|
||||
{
|
||||
var fallback = character.ImagePath;
|
||||
var resolution = ResolvePrototypeImage(character.Id, character.LibraryCode, fallback, lookup);
|
||||
character.ImageResolution = resolution;
|
||||
character.ImagePath = resolution.FinalImageUrl ?? fallback;
|
||||
}
|
||||
|
||||
foreach (var asset in scene.Assets)
|
||||
{
|
||||
var fallback = asset.ImagePath;
|
||||
var resolution = ResolvePrototypeImage(asset.Id, asset.LibraryCode, fallback, lookup);
|
||||
asset.ImageResolution = resolution;
|
||||
asset.ImagePath = resolution.FinalImageUrl ?? fallback;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private StoryIntelligenceExperienceImageResolution ResolvePrototypeImage(
|
||||
string entityId,
|
||||
string stableCode,
|
||||
string fallbackImageUrl,
|
||||
IReadOnlyDictionary<string, List<IllustrationLibraryItem>> lookup)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(stableCode) || !lookup.TryGetValue(stableCode, out var candidates))
|
||||
{
|
||||
return PrototypeFallback(entityId, stableCode, fallbackImageUrl);
|
||||
}
|
||||
|
||||
foreach (var candidate in candidates)
|
||||
{
|
||||
var publicUrl = ResolveExistingPublicUploadUrl(candidate.ThumbnailPath) ?? ResolveExistingPublicUploadUrl(candidate.FilePath);
|
||||
if (publicUrl is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
return new()
|
||||
{
|
||||
EntityId = entityId,
|
||||
StableCode = stableCode,
|
||||
IllustrationLibraryItemId = candidate.IllustrationLibraryItemID,
|
||||
Status = candidate.Status,
|
||||
FinalImageUrl = publicUrl,
|
||||
FallbackImageUrl = fallbackImageUrl,
|
||||
FallbackUsed = false
|
||||
};
|
||||
}
|
||||
|
||||
var firstCandidate = candidates.FirstOrDefault();
|
||||
return new()
|
||||
{
|
||||
EntityId = entityId,
|
||||
StableCode = stableCode,
|
||||
IllustrationLibraryItemId = firstCandidate?.IllustrationLibraryItemID,
|
||||
Status = firstCandidate?.Status,
|
||||
FallbackImageUrl = fallbackImageUrl,
|
||||
FallbackUsed = true
|
||||
};
|
||||
}
|
||||
|
||||
private string? ResolveExistingPublicUploadUrl(string? storedPath)
|
||||
{
|
||||
var physicalPath = uploadStorage.TryResolvePublicPath(storedPath, "uploads/story-intelligence-library");
|
||||
if (physicalPath is null || !File.Exists(physicalPath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var normalised = storedPath!.Replace('\\', '/').Trim();
|
||||
if (!normalised.StartsWith('/'))
|
||||
{
|
||||
normalised = "/" + normalised.TrimStart('/');
|
||||
}
|
||||
|
||||
return EncodeUrlPath(normalised);
|
||||
}
|
||||
|
||||
private static StoryIntelligenceExperienceImageResolution PrototypeFallback(string entityId, string stableCode, string fallbackImageUrl)
|
||||
=> new()
|
||||
{
|
||||
EntityId = entityId,
|
||||
StableCode = stableCode,
|
||||
FallbackImageUrl = fallbackImageUrl,
|
||||
FallbackUsed = true
|
||||
};
|
||||
|
||||
private static string EncodeUrlPath(string path)
|
||||
=> string.Join(
|
||||
'/',
|
||||
path.Split('/', StringSplitOptions.None)
|
||||
.Select((segment, index) => index == 0 && segment.Length == 0 ? string.Empty : Uri.EscapeDataString(segment)));
|
||||
}
|
||||
|
||||
@ -45,6 +45,7 @@ public sealed class OnboardingStoryIntelligenceService(
|
||||
IOnboardingStoryIntelligenceBatchStore batchStore,
|
||||
IStoryIntelligenceProgressNotifier notifier,
|
||||
ICurrentUserService currentUser,
|
||||
IStoryIntelligenceEntitlementService entitlement,
|
||||
ILogger<OnboardingStoryIntelligenceService> logger) : IOnboardingStoryIntelligenceService
|
||||
{
|
||||
private const string PromptVersionsSummary = "Chapter=Chapter-Structure-Prompt-V2; Scene=Scene-Prompt-V2";
|
||||
@ -93,6 +94,7 @@ public sealed class OnboardingStoryIntelligenceService(
|
||||
public async Task<OnboardingStoryIntelligenceBatch?> StartAsync(string? appearancePreference = null, string? primaryAgeGroup = null, string? storyEra = null, string? storyLocation = null)
|
||||
{
|
||||
var userId = RequireUserId();
|
||||
await entitlement.EnsureCanQueueAsync(userId);
|
||||
var context = await GetContextAsync(userId);
|
||||
if (context is null)
|
||||
{
|
||||
|
||||
@ -25,6 +25,8 @@ public sealed class PersistedStoryIntelligenceRunner(
|
||||
IStoryIntelligenceProgressNotifier notifier,
|
||||
IStoryMemoryService storyMemory,
|
||||
ICharacterIntelligenceService characterIntelligence,
|
||||
IStoryIntelligenceEntitlementService entitlement,
|
||||
IStoryIntelligenceCompletionNotificationService completionNotifications,
|
||||
ILogger<PersistedStoryIntelligenceRunner> logger) : IPersistedStoryIntelligenceRunner
|
||||
{
|
||||
private const string ChapterPromptFile = "Chapter-Structure-Prompt-V2.md";
|
||||
@ -80,6 +82,8 @@ public sealed class PersistedStoryIntelligenceRunner(
|
||||
"Story Intelligence run has no source text.");
|
||||
}
|
||||
|
||||
await entitlement.EnsureCanExecuteAsync(run.UserID);
|
||||
|
||||
await ThrowIfCancellationRequestedAsync(run.StoryIntelligenceRunID, stopwatch.ElapsedMilliseconds, cancellationToken);
|
||||
var paragraphs = StoryIntelligenceParagraphs.Split(run.SourceText);
|
||||
if (paragraphs.Count == 0)
|
||||
@ -462,6 +466,7 @@ public sealed class PersistedStoryIntelligenceRunner(
|
||||
completedScenes: completedScenes,
|
||||
failedScenes: failedScenes,
|
||||
totalTokens: totals.TotalTokens);
|
||||
await completionNotifications.NotifyIfImportTerminalAsync(run, cancellationToken);
|
||||
}
|
||||
catch (StoryIntelligenceRunCancelledException)
|
||||
{
|
||||
@ -490,6 +495,7 @@ public sealed class PersistedStoryIntelligenceRunner(
|
||||
"Analysis could not finish.",
|
||||
stopwatch.ElapsedMilliseconds,
|
||||
errorMessage: ex.Message);
|
||||
await completionNotifications.NotifyIfImportTerminalAsync(run, cancellationToken);
|
||||
}
|
||||
catch (StoryIntelligenceRunFailureException ex)
|
||||
{
|
||||
@ -502,6 +508,7 @@ public sealed class PersistedStoryIntelligenceRunner(
|
||||
"Analysis could not finish.",
|
||||
stopwatch.ElapsedMilliseconds,
|
||||
errorMessage: ex.Message);
|
||||
await completionNotifications.NotifyIfImportTerminalAsync(run, cancellationToken);
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
{
|
||||
@ -519,6 +526,7 @@ public sealed class PersistedStoryIntelligenceRunner(
|
||||
"Analysis could not finish.",
|
||||
stopwatch.ElapsedMilliseconds,
|
||||
errorMessage: ex.Message);
|
||||
await completionNotifications.NotifyIfImportTerminalAsync(run, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -144,9 +144,12 @@ WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = St
|
||||
DELETE FROM dbo.StoryMemoryImportPreferences
|
||||
WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryMemoryImportPreferences.ImportSessionID);
|
||||
|
||||
IF OBJECT_ID(N'dbo.StoryIntelligenceCharacterIllustrationAssignments', N'U') IS NOT NULL
|
||||
BEGIN
|
||||
DELETE FROM dbo.StoryIntelligenceCharacterIllustrationAssignments
|
||||
WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryIntelligenceCharacterIllustrationAssignments.ImportSessionID)
|
||||
OR EXISTS (SELECT 1 FROM @ProjectIDs p WHERE p.ProjectID = StoryIntelligenceCharacterIllustrationAssignments.ProjectID);
|
||||
END;
|
||||
|
||||
DELETE FROM dbo.StoryIntelligenceIllustrationDemands
|
||||
WHERE EXISTS (SELECT 1 FROM @ProjectIDs p WHERE p.ProjectID = StoryIntelligenceIllustrationDemands.ProjectID);
|
||||
|
||||
@ -14,6 +14,7 @@ public sealed class StoryIntelligenceExistingChapterQueueService(
|
||||
IStoryIntelligenceResultRepository runs,
|
||||
IStoryIntelligenceClient client,
|
||||
IStoryIntelligencePipelineStateService pipelines,
|
||||
IStoryIntelligenceEntitlementService entitlement,
|
||||
ILogger<StoryIntelligenceExistingChapterQueueService> logger) : IStoryIntelligenceExistingChapterQueueService
|
||||
{
|
||||
private const string NoStoredChapterTextMessage = "No stored chapter text is currently available for this chapter. A future Word Companion/document extraction phase is required.";
|
||||
@ -26,6 +27,7 @@ public sealed class StoryIntelligenceExistingChapterQueueService(
|
||||
|
||||
public async Task<StoryIntelligenceExistingChapterQueueViewModel> QueueAsync(int userId, StoryIntelligenceExistingChapterQueueForm form)
|
||||
{
|
||||
await entitlement.EnsureCanQueueAsync(userId);
|
||||
if (form.ChapterID is not int chapterId)
|
||||
{
|
||||
return await BuildViewModelAsync(
|
||||
|
||||
@ -19,6 +19,7 @@ public sealed class StoryIntelligenceFullChapterTestService(
|
||||
IStoryIntelligencePipelineStateService pipelines,
|
||||
IOptions<StoryIntelligenceOptions> options,
|
||||
IOptions<StoryIntelligencePricingOptions> pricingOptions,
|
||||
IStoryIntelligenceEntitlementService entitlement,
|
||||
ILogger<StoryIntelligenceFullChapterTestService> logger) : IStoryIntelligenceFullChapterTestService
|
||||
{
|
||||
private const int LargeChapterCharacterThreshold = 40000;
|
||||
@ -74,6 +75,7 @@ public sealed class StoryIntelligenceFullChapterTestService(
|
||||
|
||||
public async Task<int> QueueAsync(int userId, StoryIntelligenceFullChapterTestForm form, IFormFile? textFile)
|
||||
{
|
||||
await entitlement.EnsureCanQueueAsync(userId);
|
||||
var source = await ReadSourceAsync(form, textFile);
|
||||
if (!string.IsNullOrWhiteSpace(source.ErrorMessage))
|
||||
{
|
||||
|
||||
90
PlotLine/Services/StoryIntelligenceOperationalServices.cs
Normal file
@ -0,0 +1,90 @@
|
||||
using PlotLine.Data;
|
||||
using PlotLine.Models;
|
||||
|
||||
namespace PlotLine.Services;
|
||||
|
||||
public interface IStoryIntelligenceEntitlementService
|
||||
{
|
||||
Task EnsureCanQueueAsync(int userId);
|
||||
Task EnsureCanExecuteAsync(int userId);
|
||||
}
|
||||
|
||||
public sealed class StoryIntelligenceEntitlementService(ISubscriptionService subscriptions) : IStoryIntelligenceEntitlementService
|
||||
{
|
||||
public Task EnsureCanQueueAsync(int userId) => EnsureAllowedAsync(userId);
|
||||
|
||||
public Task EnsureCanExecuteAsync(int userId) => EnsureAllowedAsync(userId);
|
||||
|
||||
private async Task EnsureAllowedAsync(int userId)
|
||||
{
|
||||
var subscription = await subscriptions.GetCurrentSubscriptionAsync(userId);
|
||||
if (!IsActive(subscription, DateTime.UtcNow))
|
||||
{
|
||||
throw new SubscriptionLimitException("Story Intelligence requires an active subscription before paid AI work can be queued or processed.");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsActive(UserSubscriptionDetail? subscription, DateTime utcNow)
|
||||
{
|
||||
if (subscription is null || !subscription.IsActive || !subscription.SubscriptionLevelIsActive)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (subscription.EndDateUTC.HasValue && subscription.EndDateUTC.Value <= utcNow)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (subscription.CurrentPeriodEndUTC.HasValue
|
||||
&& subscription.CurrentPeriodEndUTC.Value <= utcNow
|
||||
&& !string.IsNullOrWhiteSpace(subscription.StripeSubscriptionId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var status = (subscription.StripeStatus ?? string.Empty).Trim().ToLowerInvariant();
|
||||
return status is not ("canceled" or "cancelled" or "incomplete_expired" or "ended" or "expired" or "inactive");
|
||||
}
|
||||
}
|
||||
|
||||
public interface IStoryIntelligenceCompletionNotificationService
|
||||
{
|
||||
Task NotifyIfImportTerminalAsync(StoryIntelligenceQueuedRun run, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public sealed class StoryIntelligenceCompletionNotificationService(
|
||||
IStoryIntelligencePipelineRepository pipelines,
|
||||
IUserRepository users,
|
||||
IEmailService emails,
|
||||
ILogger<StoryIntelligenceCompletionNotificationService> logger) : IStoryIntelligenceCompletionNotificationService
|
||||
{
|
||||
public async Task NotifyIfImportTerminalAsync(StoryIntelligenceQueuedRun run, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (run.BookID is not int bookId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var target = await pipelines.TryBeginCompletionNotificationAsync(bookId, run.UserID);
|
||||
if (target is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var user = await users.GetByIdAsync(target.UserID);
|
||||
if (user is null || string.IsNullOrWhiteSpace(user.Email))
|
||||
{
|
||||
logger.LogWarning("Story Intelligence import notification skipped because user {UserID} has no email target.", target.UserID);
|
||||
return;
|
||||
}
|
||||
|
||||
await emails.SendStoryIntelligenceImportNotificationAsync(user.Email, user.DisplayName, target);
|
||||
logger.LogInformation(
|
||||
"Queued Story Intelligence import {NotificationKind} notification for import session {ImportSessionID}, book {BookID}, user {UserID}.",
|
||||
target.NotificationKind,
|
||||
target.ImportSessionID,
|
||||
target.BookID,
|
||||
target.UserID);
|
||||
}
|
||||
}
|
||||
@ -18,6 +18,7 @@ public sealed class StoryIntelligenceResultPersistenceService(
|
||||
IStoryIntelligenceResultRepository repository,
|
||||
IStoryIntelligenceClient client,
|
||||
ISceneImportResolver importResolver,
|
||||
IStoryIntelligenceEntitlementService entitlement,
|
||||
ILogger<StoryIntelligenceResultPersistenceService> logger) : IStoryIntelligenceResultPersistenceService
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
@ -63,6 +64,7 @@ public sealed class StoryIntelligenceResultPersistenceService(
|
||||
|
||||
public async Task<int> QueueAdminTextRunAsync(int userId, ChapterStoryIntelligenceDryRunForm form)
|
||||
{
|
||||
await entitlement.EnsureCanQueueAsync(userId);
|
||||
if (string.IsNullOrWhiteSpace(form.ChapterText))
|
||||
{
|
||||
throw new InvalidOperationException("Enter chapter text before queueing a persisted Story Intelligence run.");
|
||||
|
||||
122
PlotLine/Sql/149_Phase24_LeanImportCompletionNotifications.sql
Normal file
@ -0,0 +1,122 @@
|
||||
SET ANSI_NULLS ON;
|
||||
GO
|
||||
SET QUOTED_IDENTIFIER ON;
|
||||
GO
|
||||
|
||||
IF COL_LENGTH(N'dbo.StoryIntelligenceBookPipelines', N'CompletionEmailQueuedUtc') IS NULL
|
||||
BEGIN
|
||||
ALTER TABLE dbo.StoryIntelligenceBookPipelines
|
||||
ADD CompletionEmailQueuedUtc datetime2 NULL;
|
||||
END;
|
||||
GO
|
||||
|
||||
IF COL_LENGTH(N'dbo.StoryIntelligenceBookPipelines', N'FailureEmailQueuedUtc') IS NULL
|
||||
BEGIN
|
||||
ALTER TABLE dbo.StoryIntelligenceBookPipelines
|
||||
ADD FailureEmailQueuedUtc datetime2 NULL;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.StoryIntelligenceBookPipeline_TryBeginCompletionNotification
|
||||
@BookID int,
|
||||
@UserID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
SET XACT_ABORT ON;
|
||||
|
||||
DECLARE @ImportSessionID int;
|
||||
DECLARE @ProjectID int;
|
||||
DECLARE @HasActiveRuns bit;
|
||||
DECLARE @HasFailedRuns bit;
|
||||
DECLARE @ChapterCount int;
|
||||
DECLARE @CompletedChapterCount int;
|
||||
DECLARE @FailedChapterCount int;
|
||||
DECLARE @NotificationKind nvarchar(20);
|
||||
|
||||
SELECT @ImportSessionID = p.StoryIntelligenceBookPipelineID,
|
||||
@ProjectID = p.ProjectID
|
||||
FROM dbo.StoryIntelligenceBookPipelines p
|
||||
INNER JOIN dbo.ProjectUserAccess access ON access.ProjectID = p.ProjectID
|
||||
AND access.UserID = @UserID
|
||||
AND access.IsActive = 1
|
||||
WHERE p.BookID = @BookID;
|
||||
|
||||
IF @ImportSessionID IS NULL
|
||||
RETURN;
|
||||
|
||||
SELECT @HasActiveRuns = CASE WHEN EXISTS
|
||||
(
|
||||
SELECT 1
|
||||
FROM dbo.StoryIntelligenceRuns
|
||||
WHERE BookID = @BookID
|
||||
AND UserID = @UserID
|
||||
AND Status IN (N'Pending', N'Running')
|
||||
) THEN 1 ELSE 0 END;
|
||||
|
||||
IF @HasActiveRuns = 1
|
||||
RETURN;
|
||||
|
||||
SELECT @ChapterCount = COUNT(1),
|
||||
@CompletedChapterCount = SUM(CASE WHEN Status IN (N'Completed', N'CompletedWithWarnings') THEN 1 ELSE 0 END),
|
||||
@FailedChapterCount = SUM(CASE WHEN Status IN (N'Failed', N'Cancelled') THEN 1 ELSE 0 END),
|
||||
@HasFailedRuns = CASE WHEN SUM(CASE WHEN Status IN (N'Failed', N'Cancelled') THEN 1 ELSE 0 END) > 0 THEN 1 ELSE 0 END
|
||||
FROM dbo.StoryIntelligenceRuns
|
||||
WHERE BookID = @BookID
|
||||
AND UserID = @UserID;
|
||||
|
||||
IF COALESCE(@ChapterCount, 0) = 0
|
||||
RETURN;
|
||||
|
||||
SET @NotificationKind = CASE WHEN @HasFailedRuns = 1 THEN N'Failure' ELSE N'Success' END;
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
IF @NotificationKind = N'Failure'
|
||||
BEGIN
|
||||
UPDATE dbo.StoryIntelligenceBookPipelines
|
||||
SET FailureEmailQueuedUtc = SYSUTCDATETIME(),
|
||||
UpdatedUtc = SYSUTCDATETIME()
|
||||
WHERE StoryIntelligenceBookPipelineID = @ImportSessionID
|
||||
AND FailureEmailQueuedUtc IS NULL;
|
||||
|
||||
IF @@ROWCOUNT = 0
|
||||
BEGIN
|
||||
ROLLBACK TRANSACTION;
|
||||
RETURN;
|
||||
END;
|
||||
END
|
||||
ELSE
|
||||
BEGIN
|
||||
UPDATE dbo.StoryIntelligenceBookPipelines
|
||||
SET CompletionEmailQueuedUtc = SYSUTCDATETIME(),
|
||||
UpdatedUtc = SYSUTCDATETIME()
|
||||
WHERE StoryIntelligenceBookPipelineID = @ImportSessionID
|
||||
AND CompletionEmailQueuedUtc IS NULL;
|
||||
|
||||
IF @@ROWCOUNT = 0
|
||||
BEGIN
|
||||
ROLLBACK TRANSACTION;
|
||||
RETURN;
|
||||
END;
|
||||
END;
|
||||
|
||||
COMMIT TRANSACTION;
|
||||
|
||||
SELECT @ImportSessionID AS ImportSessionID,
|
||||
@UserID AS UserID,
|
||||
p.ProjectID,
|
||||
project.ProjectName,
|
||||
p.BookID,
|
||||
book.BookTitle,
|
||||
book.Subtitle AS BookSubtitle,
|
||||
@ChapterCount AS ChapterCount,
|
||||
@CompletedChapterCount AS CompletedChapterCount,
|
||||
@FailedChapterCount AS FailedChapterCount,
|
||||
@NotificationKind AS NotificationKind
|
||||
FROM dbo.StoryIntelligenceBookPipelines p
|
||||
INNER JOIN dbo.Projects project ON project.ProjectID = p.ProjectID
|
||||
INNER JOIN dbo.Books book ON book.BookID = p.BookID
|
||||
WHERE p.StoryIntelligenceBookPipelineID = @ImportSessionID;
|
||||
END;
|
||||
GO
|
||||
@ -0,0 +1,10 @@
|
||||
SET ANSI_NULLS ON
|
||||
GO
|
||||
SET QUOTED_IDENTIFIER ON
|
||||
GO
|
||||
|
||||
IF OBJECT_ID(N'dbo.StoryIntelligenceCharacterIllustrationAssignments', N'U') IS NOT NULL
|
||||
BEGIN
|
||||
DROP TABLE dbo.StoryIntelligenceCharacterIllustrationAssignments;
|
||||
END
|
||||
GO
|
||||
@ -141,11 +141,6 @@
|
||||
{
|
||||
<a class="btn btn-outline-primary" asp-controller="Projects" asp-action="Index">Continue in background</a>
|
||||
}
|
||||
@if (Model.ImportSessionID.HasValue)
|
||||
{
|
||||
<a class="btn btn-outline-primary"
|
||||
href="/Development/StoryIntelligenceExperience?importSessionId=@Model.ImportSessionID.Value&mode=replay">Replay Story Intelligence</a>
|
||||
}
|
||||
@if (!Model.HasActiveRuns && Model.Chapters.Any(chapter => chapter.IsRunComplete || chapter.IsFailed || chapter.HasCompletedCommit))
|
||||
{
|
||||
<a class="btn btn-primary"
|
||||
|
||||
107
docs/story-intelligence/import-visualisation-archive.md
Normal file
@ -0,0 +1,107 @@
|
||||
# Story Intelligence Import Visualisation Archive
|
||||
|
||||
## Purpose
|
||||
|
||||
This document preserves the removed Story Intelligence import visualisation so the design can be revisited without leaving it active in the product.
|
||||
|
||||
## Original Rationale
|
||||
|
||||
The visualisation was built to make long-running manuscript import feel alive. It showed PlotDirector reading scenes, discovering characters, assigning locations and assets, drawing relationships, and replaying persisted import sessions. The aim was reassurance and transparency during expensive AI-driven work.
|
||||
|
||||
## User Experience
|
||||
|
||||
The active experience lived behind development routes and was also linked from the onboarding import progress page as "Replay Story Intelligence". Users could open a full-screen theatre-style view that selected a live or persisted import session, animated progress, and showed a scene-by-scene story map.
|
||||
|
||||
## Layout
|
||||
|
||||
The page used a dark full-screen shell with a header status strip, circular progress treatment, left-side activity and discovery panels, central living story stage, character/location/asset presentation, SVG relationship lines, a scene ribbon/timeline, right-side insight panels, replay controls, diagnostics, and rebuild controls.
|
||||
|
||||
## Look And Feel
|
||||
|
||||
The design used a cinematic blue/gold visual style, translucent panels, soft glow states, animated graph lines, circular portraits, visual asset chips, and compact diagnostics. Reduced-motion handling existed in CSS and JavaScript.
|
||||
|
||||
## Major Screens And Components
|
||||
|
||||
- `/Development/StoryIntelligenceExperience`
|
||||
- `/Development/StoryIntelligenceExperience?mode=simulation`
|
||||
- `/Development/StoryIntelligenceExperience?importSessionId=...&mode=replay`
|
||||
- `/Development/StoryIntelligenceIllustrationDiagnostics`
|
||||
- `/api/story-intelligence/runs/{runId}/visualisation-snapshot`
|
||||
- `/api/story-intelligence/import-sessions/{importSessionId}/visualisation-snapshot`
|
||||
|
||||
## Animation Behaviour
|
||||
|
||||
The JavaScript polled snapshot endpoints, reconciled scene/entity state, animated current-scene changes, drew SVG relationship connections, preloaded image assets, managed replay stepping, and updated timeline/ribbon state.
|
||||
|
||||
## Progress Behaviour
|
||||
|
||||
The visualisation consumed persisted Story Intelligence run state and SignalR-progress-adjacent data. It was an observer in principle, but the development rebuild route could trigger Story Memory rebuild and optional image demand creation, which made the visual layer too close to active import processing.
|
||||
|
||||
## Discoveries Shown
|
||||
|
||||
It displayed detected characters, locations, assets, relationships, knowledge threads, questions, scene summaries, evidence snippets, observations, and diagnostic labels.
|
||||
|
||||
## Character Presentation
|
||||
|
||||
Characters were rendered as weighted stage nodes with names, roles, relevance, image resolution state, fallback portraits, and evidence-derived presentation metadata. Some matching logic used Story Memory and illustration metadata to pick or queue portrait candidates.
|
||||
|
||||
## Location Presentation
|
||||
|
||||
Locations were rendered as current-scene place cards or stage nodes, backed by scene setting, location extraction, Story Memory locations, and optional illustration-library assignment.
|
||||
|
||||
## Evidence Presentation
|
||||
|
||||
Evidence appeared as compact cards and text items. Some scene observations were useful for human review, but some existed primarily to make the visualisation feel intelligent.
|
||||
|
||||
## Image Presentation
|
||||
|
||||
The visualisation used prototype SVG assets, approved illustration-library images, persisted Story Memory assignments, and image-resolution diagnostics. Generated image demand was originally tied too closely to Story Memory and visualisation rebuild behaviour.
|
||||
|
||||
## Completion Behaviour
|
||||
|
||||
The visualisation could replay completed persisted results. It did not need to drive import completion and should not have been part of the normal user path.
|
||||
|
||||
## SignalR Behaviour
|
||||
|
||||
The ordinary import progress page still uses the shared Story Intelligence progress hub. The removed visualisation used polling snapshot endpoints and client-side reconciliation rather than being required for background processing.
|
||||
|
||||
## Major Runtime Classes
|
||||
|
||||
- `PlotLine.Controllers.DevelopmentController`
|
||||
- `PlotLine.Controllers.StoryIntelligenceVisualisationController`
|
||||
- `PlotLine.Services.StoryIntelligenceVisualisationSnapshotService`
|
||||
- `PlotLine.Services.StoryIntelligenceExperiencePrototypeData`
|
||||
- `PlotLine.Services.StoryIntelligenceIllustrationMatchingService`
|
||||
- `PlotLine.ViewModels.StoryIntelligenceExperiencePrototypeViewModels`
|
||||
- `PlotLine.Views.Development.StoryIntelligenceExperience`
|
||||
- `PlotLine.Views.Development.StoryIntelligenceIllustrationDiagnostics`
|
||||
|
||||
## Archived Source Snapshot
|
||||
|
||||
Representative self-contained UI/source artefacts are preserved under:
|
||||
|
||||
`docs/story-intelligence/visualisation-archive/runtime-source/`
|
||||
|
||||
This includes Razor markup, CSS, JavaScript, prototype SVG assets, the visualisation view models, and prototype data builder. This archive is outside the compiled app and must not participate in runtime.
|
||||
|
||||
Backend classes that were too coupled to duplicate in full remain recoverable through Git at safety tag:
|
||||
|
||||
`pre-visualisation-removal-2026-08-24`
|
||||
|
||||
## Database Dependencies
|
||||
|
||||
The visualisation consumed persisted Story Intelligence run/result tables, Story Intelligence book import session tables, illustration library tables, Story Memory tables, illustration assignment tables, and illustration demand tables. The lean import path keeps persisted import/progress data and Story Memory where product features consume it, but removes active snapshot/replay UI.
|
||||
|
||||
## Story Intelligence Dependencies
|
||||
|
||||
The visualisation depended on chapter structure results, scene intelligence results, persisted scene summaries, characters, locations, assets, relationships, knowledge changes, questions, observations, Story Memory assignments, and character profile updates.
|
||||
|
||||
## AI Dependencies
|
||||
|
||||
Opening the visualisation should not have required AI. The problematic coupling was indirect: rebuild and Story Memory/image demand paths could create additional work intended for the visual experience. The lean architecture keeps AI for necessary import interpretation and profile updates, but image generation is off by default and visualisation no longer triggers analysis or demand.
|
||||
|
||||
## Lessons Learned
|
||||
|
||||
A future visualisation may observe information naturally generated by the import pipeline, but it must never cause additional analysis or AI work.
|
||||
|
||||
Visualisation should be a passive read model over durable import state. It should never introduce prompt fields, persistence, retries, image demand, or Story Memory work that the product does not otherwise need.
|
||||
317
docs/story-intelligence/import-visualisation-preservation.md
Normal file
@ -0,0 +1,317 @@
|
||||
# Story Intelligence Import Visualisation Preservation Audit
|
||||
|
||||
## 1. Purpose and History
|
||||
|
||||
This document preserves the current Story Intelligence import visualisation before any decision to remove it from the active import workflow. The intended future boundary is:
|
||||
|
||||
```text
|
||||
IMPORT / STORY INTELLIGENCE
|
||||
-> persisted results and domain progress
|
||||
-> read-only visualisation
|
||||
```
|
||||
|
||||
The visualisation must not cause extra analysis, extraction, persistence, AI calls, or image generation just to make the display interesting.
|
||||
|
||||
This audit is based on source inspection only. No application code, SQL, or runtime data was changed. Screenshots were not captured because doing that safely would require an authenticated development session and existing import data; the existing design reference is `/srv/repos/PlotDirector/PlotLine/Docs/Features/PlotDirector_Story_Intelligence_Visualisation_Design.png`.
|
||||
|
||||
## 2. Current UX
|
||||
|
||||
There are two related experiences:
|
||||
|
||||
- Active onboarding progress page: `/srv/repos/PlotDirector/PlotLine/Views/Onboarding/StoryIntelligenceProgress.cshtml`. This is the production-facing progress page. It shows "Reading your manuscript", a plain import progress card, chapters read, scenes read, failures, elapsed time, estimated remaining, current chapter, current scene, latest scene summary, recent discoveries, cancel, continue in background, replay, and review actions.
|
||||
- Development experience visualisation: `/srv/repos/PlotDirector/PlotLine/Views/Development/StoryIntelligenceExperience.cshtml`. This is admin/development-only and uses the full visual theatre: dark full-screen shell, header status strip, circular progress ring, left-side discovery/activity panels, central "living story view", character/location stage, SVG relationship lines, scene ribbon, timeline, right-side knowledge/observation/relationship panels, replay controls, and diagnostics.
|
||||
|
||||
The production page explicitly says processing can continue after the browser tab is closed. The full visualisation is reachable from the production progress page by a "Replay Story Intelligence" link when an import session exists, but the controller route is protected by `[Authorize(Policy = "AdminOnly")]` and `environment.IsDevelopment()`.
|
||||
|
||||
The distinctive visual language lives mostly in `/srv/repos/PlotDirector/PlotLine/wwwroot/css/story-intelligence-experience-prototype.css`: dark navy/blue-gold stage, blurred translucent panels, conic progress ring, animated SVG connection lines, circular character/location portraits, text-only evidence cards when portraits are not trusted, glowing timeline markers, compact side panels, and reduced-motion handling. Client behaviour is in `/srv/repos/PlotDirector/PlotLine/wwwroot/js/story-intelligence-experience-prototype.js`: it polls snapshots, reconciles nodes, animates progress, preloads images, draws collision-aware connections, updates scene ribbon/timeline, and opens insight detail cards.
|
||||
|
||||
## 3. Architecture
|
||||
|
||||
The active import pipeline is separate from the full visualisation:
|
||||
|
||||
- `/srv/repos/PlotDirector/PlotLine/Services/OnboardingStoryIntelligenceService.cs` queues one persisted Story Intelligence run per included chapter.
|
||||
- `/srv/repos/PlotDirector/PlotLine/Services/PersistedStoryIntelligenceWorker.cs` is the background service that repeatedly asks the runner to process pending runs.
|
||||
- `/srv/repos/PlotDirector/PlotLine/Services/PersistedStoryIntelligenceRunner.cs` performs the expensive work: one Chapter Structure call, one Scene Intelligence call per detected scene, JSON repair when needed, validation, persistence, progress publication, Story Memory processing, and Character Intelligence profile updates.
|
||||
- `/srv/repos/PlotDirector/PlotLine/Services/StoryIntelligenceVisualisationSnapshotService.cs` builds read models from persisted run/scene results and Story Memory assignments.
|
||||
- `/srv/repos/PlotDirector/PlotLine/Controllers/StoryIntelligenceVisualisationController.cs` exposes read-only JSON snapshots for authenticated users.
|
||||
- `/srv/repos/PlotDirector/PlotLine/Controllers/DevelopmentController.cs` serves the full experience page and its replay/rebuild action, development-only.
|
||||
- `/srv/repos/PlotDirector/PlotLine/Hubs/StoryIntelligenceHub.cs` plus `/srv/repos/PlotDirector/PlotLine/Services/StoryIntelligenceProgressNotifier.cs` provide reusable SignalR progress updates.
|
||||
|
||||
The full visualisation is mostly a read-only observer when simply opened. The dangerous coupling is that Story Memory and illustration-demand machinery can be used by the import pipeline and by the development replay/rebuild path, so "visualisation support" has grown into durable derived state.
|
||||
|
||||
## 4. Component Inventory
|
||||
|
||||
| Component | Classification | Notes |
|
||||
|---|---|---|
|
||||
| `/srv/repos/PlotDirector/PlotLine/Controllers/StoryIntelligenceVisualisationController.cs` | VISUALISATION ONLY | Read-only snapshot API for run/import session visualisation. |
|
||||
| `/srv/repos/PlotDirector/PlotLine/Controllers/DevelopmentController.cs` methods `StoryIntelligenceExperience`, `RebuildStoryIntelligenceExperience`, diagnostics | VISUALISATION ONLY / UNCERTAIN | Development-only UI and rebuild tools. Rebuild calls Story Memory and can create demand records. |
|
||||
| `/srv/repos/PlotDirector/PlotLine/Views/Development/StoryIntelligenceExperience.cshtml` | VISUALISATION ONLY | Full-screen experience/replay UI. |
|
||||
| `/srv/repos/PlotDirector/PlotLine/wwwroot/js/story-intelligence-experience-prototype.js` | VISUALISATION ONLY | Snapshot polling, animation, layout, drawing, replay controls. |
|
||||
| `/srv/repos/PlotDirector/PlotLine/wwwroot/css/story-intelligence-experience-prototype.css` | VISUALISATION ONLY | Full visual design system for the prototype. |
|
||||
| `/srv/repos/PlotDirector/PlotLine/ViewModels/StoryIntelligenceExperiencePrototypeViewModels.cs` | VISUALISATION ONLY | Snapshot DTOs: scenes, characters, locations, assets, relationships, diagnostics, image resolution. |
|
||||
| `/srv/repos/PlotDirector/PlotLine/Services/StoryIntelligenceVisualisationSnapshotService.cs` | DO NOT REMOVE UNTIL DECOUPLED | Builds visualization snapshots and contains visual-only inference/story-memory logic. |
|
||||
| `/srv/repos/PlotDirector/PlotLine/Services/StoryIntelligenceExperiencePrototypeData.cs` | VISUALISATION ONLY | Simulation data. |
|
||||
| `/srv/repos/PlotDirector/PlotLine/wwwroot/images/story-intelligence/prototype/*` | VISUALISATION ONLY | Static fallback prototype assets. |
|
||||
| `/srv/repos/PlotDirector/PlotLine/Views/Onboarding/StoryIntelligenceProgress.cshtml` | IMPORT PIPELINE / SHARED UX | Active import status screen; contains replay link. |
|
||||
| `/srv/repos/PlotDirector/PlotLine/wwwroot/js/story-intelligence-progress.js` | SHARED INFRASTRUCTURE | Production progress SignalR client for onboarding and legacy dashboards. |
|
||||
| `/srv/repos/PlotDirector/PlotLine/Hubs/StoryIntelligenceHub.cs` | SHARED INFRASTRUCTURE | Authenticated SignalR hub for progress. |
|
||||
| `/srv/repos/PlotDirector/PlotLine/Services/StoryIntelligenceProgressNotifier.cs` | SHARED INFRASTRUCTURE | Broadcasts persisted run progress events. |
|
||||
| `/srv/repos/PlotDirector/PlotLine/Services/PersistedStoryIntelligenceRunner.cs` | IMPORT PIPELINE / STORY INTELLIGENCE | AI execution, persistence, progress, Story Memory, Character Intelligence. |
|
||||
| `/srv/repos/PlotDirector/PlotLine/Docs/AI/Chapter-Structure-Prompt-V2.md` | STORY INTELLIGENCE | Boundary prompt; explicitly excludes story entities/observations. |
|
||||
| `/srv/repos/PlotDirector/PlotLine/Docs/AI/Scene-Prompt-V2.md` | STORY INTELLIGENCE / VISUALISATION COUPLED | Main scene schema includes many fields consumed by import and visualisation, including `characterAppearance`. |
|
||||
| `/srv/repos/PlotDirector/PlotLine/Services/StoryMemoryServices.cs` | STORY INTELLIGENCE / DO NOT REMOVE UNTIL DECOUPLED | Durable derived scene memory and illustration assignment/demand. |
|
||||
| `/srv/repos/PlotDirector/PlotLine/Data/StoryMemoryRepository.cs` | STORY INTELLIGENCE / SHARED INFRASTRUCTURE | Persistence for Story Memory. Note: currently uses inline SQL. |
|
||||
| `/srv/repos/PlotDirector/PlotLine/Services/IllustrationLibraryServices.cs` | SHARED INFRASTRUCTURE | Reusable image library and image generation worker integration. |
|
||||
| `/srv/repos/PlotDirector/PlotLine/Services/StoryIntelligenceIllustrationMatchingService.cs` | VISUALISATION COUPLED / UNCERTAIN | Older project-level assignment/demand based on visualisation DTOs. |
|
||||
| `/srv/repos/PlotDirector/PlotLine/Data/StoryIntelligenceIllustrationMatchingRepository.cs` | VISUALISATION COUPLED / UNCERTAIN | Project-level visualisation assignment/demand tables. |
|
||||
| SQL `114`, `115`, `117`, `121`, `127`, `137`, `142`, `144`, `145`, `146`, `147` | IMPORT/STORY INTELLIGENCE | Persisted runs, result storage, pipeline state, Story Memory, character context, safe image policy. |
|
||||
| SQL `135`, `136`, `138`, `139`, `140`, `141` | IMAGE/VISUALISATION COUPLED | Illustration library, matching, demand, audit. |
|
||||
|
||||
## 5. Event Catalogue
|
||||
|
||||
| Event/update | Example | Source | Persisted? | AI derived? | Required elsewhere? |
|
||||
|---|---|---|---|---|---|
|
||||
| Import started | "Story Intelligence has started reading this chapter." | `PersistedStoryIntelligenceRunner.PublishAsync` | Run progress fields yes | No | Yes, production progress |
|
||||
| Chapter started | "PlotDirector is finding the scene boundaries..." | Runner before Chapter Structure call | Run progress fields yes | No | Yes |
|
||||
| Scene boundaries detected | "Detected 4 suggested scenes." | Runner after Chapter Structure response | Yes, chapter result and run counts | Yes | Yes |
|
||||
| Scene analysis started | "Reading scene 2." | Runner before Scene Intelligence call | Run progress fields yes | No | Yes |
|
||||
| Scene analysis completed | "Scene 2 analysed." | Runner after scene persistence | Yes, scene result and run counts | Yes | Yes |
|
||||
| Latest scene summary | Scene short summary | Scene prompt `summary.short` | Yes in `ParsedJson`; SignalR transient copy | Yes | Yes, review/progress |
|
||||
| Recent discoveries | "Possible character found: Beth" | Runner derives from parsed characters/locations/assets | SignalR transient; source persisted | Yes source, No derivation | Nice-to-have |
|
||||
| Failure | "Scene 2 needs attention." | Runner catch blocks | Yes | Sometimes | Yes |
|
||||
| Cancellation | "Analysis was cancelled." | Runner cancellation path | Yes | No | Yes |
|
||||
| Completion | "Chapter analysis complete." | Runner completion path | Yes | No | Yes |
|
||||
| Snapshot change token | hash of run/result state | Visualisation snapshot service | No | No | Visualisation only |
|
||||
| Current scene/window | latest persisted scene result | Visualisation snapshot service | No | No | Visualisation only |
|
||||
| Character node | name, role, relevance, weight | Visualisation snapshot service from scene characters plus story memory | No as DTO; source persisted | Mostly yes | Review/import uses source, not node |
|
||||
| Location node | active location image/text | Snapshot service from scene locations/setting | No as DTO; source persisted | Mostly yes | Review/import uses source |
|
||||
| Asset state | important assets, often not rendered stage nodes | Snapshot service from scene assets | No as DTO; source persisted | Mostly yes | Review/import uses source |
|
||||
| Relationship lines | labels/states/weights | Snapshot service from scene relationships and visual memory inference | Some source persisted; inferred visual memory no | Mixed | Explicit relationships yes; hard-coded visual inference no |
|
||||
| Knowledge threads | questions and knowledge changes | Snapshot service from scene JSON | No as DTO; source persisted | Yes | Knowledge import uses knowledge changes |
|
||||
| Observations | title/detail evidence cards | Snapshot service from `scene.Observations` | No as DTO; source persisted | Yes | Unclear/no strong current consumer outside visualisation |
|
||||
| Image demand status | demand recorded/queued/satisfied | Story Memory or old matching service | Yes | No direct text AI; may trigger image generation later | Images useful to visualisation, maybe library |
|
||||
| Image generated | library item status generated | Illustration worker/library | Yes | Image model call | Shared library, but demand originates from SI memory/visualisation coupling |
|
||||
|
||||
## 6. Visualisation-Driven Intelligence
|
||||
|
||||
Information already required by PlotDirector:
|
||||
|
||||
- Scene boundaries, scene summaries, scene references, and detected scene counts are core import outputs.
|
||||
- Characters, aliases, locations, assets, relationships, knowledge changes, metrics, source limits, and continuity signals are used by review/import services.
|
||||
- Character aliases and known-character context are used by the prompt to reduce identity drift.
|
||||
- Character appearance has a downstream consumer: Story Memory and illustration matching. It is not required for scene creation itself, but it is required by the current image assignment/demand system.
|
||||
|
||||
Information that appears primarily visualisation-oriented:
|
||||
|
||||
- `StoryIntelligenceExperience*` DTOs, image resolution diagnostics, current visual scene window, display weights, stage labels, timeline labels, feed items, SVG connection labels, replay indexes, panel counts, and change tokens.
|
||||
- Snapshot-service in-memory `StoryMemory` used to carry characters, point-of-view, and relationships through the visual replay.
|
||||
- Hard-coded/thematic inference in `StoryIntelligenceVisualisationSnapshotService`, including Beth/Grace/Elen family inference and summary-language inference. This is not the same as canonical relationship import.
|
||||
- `scene.Observations` as a displayed "Story observations" panel. It is generated by Scene Prompt V2 and persisted in scene JSON, but source searches found no strong downstream consumer outside visualisation/Story Memory evidence extraction.
|
||||
|
||||
Information with uncertain value:
|
||||
|
||||
- Character appearance and deterministic appearance inference. It is useful only if reusable illustrations remain part of Story Intelligence or future passive visualisation.
|
||||
- Durable Story Memory entities. They may become useful for future context/intent or import review, but current normal import review services still primarily read persisted scene results rather than requiring Story Memory.
|
||||
|
||||
## 7. AI Cost Attribution
|
||||
|
||||
| AI operation | Required by Import | Required by SI | Required by Visualisation | Could remove? |
|
||||
|---|---:|---:|---:|---|
|
||||
| Chapter Structure prompt | Yes | Yes | No | No |
|
||||
| Scene Intelligence prompt per scene | Yes | Yes | Partially: it includes fields the visualisation displays | Not wholesale |
|
||||
| JSON repair retry | Yes for reliability | Yes | No | No, unless schema/output complexity is reduced enough to lower failures |
|
||||
| Character Intelligence profile update | No for basic import | Yes/current architecture | No direct | Possibly defer or make opt-in |
|
||||
| Story Memory deterministic processing | No for basic import | Partially/current architecture | Yes for image/visual continuity | Decouple before removing |
|
||||
| Illustration library image generation | No for manuscript import | No for textual SI | Yes/partially for visual experience | Yes for simplified import |
|
||||
| Development replay/rebuild | No | No | Yes | Yes, keep as offline/admin-only if needed |
|
||||
|
||||
Answer: the visualisation partially increases AI cost. Opening the snapshot endpoint does not itself call the text AI. But the current scene prompt asks for `characterAppearance` specifically for illustration matching, Story Memory can queue image demand during import, and development rebuild can create demand records. If automatic image generation is enabled and approval is not required, those demands can lead to image-generation calls.
|
||||
|
||||
## 8. Evidence and Observation System
|
||||
|
||||
The "Beth pours tea" style evidence comes from Scene Prompt V2 and the persisted `SceneIntelligenceScene` JSON, not from a separate visualisation prompt. Relevant fields include:
|
||||
|
||||
- `characters[].actions`, `characters[].notes`
|
||||
- `relationships[].relationshipSignal`, `relationships[].evidence`
|
||||
- `knowledgeChanges[].evidence`
|
||||
- `questionsRaised[].evidence`
|
||||
- `observations[].description`, `observations[].predicate`, `observations[].objectName`, `observations[].evidence`
|
||||
- `characterAppearance[].evidence`
|
||||
|
||||
Representation:
|
||||
|
||||
- Models are in `/srv/repos/PlotDirector/PlotLine/Models/StoryIntelligencePersistenceModels.cs` and `/srv/repos/PlotDirector/PlotLine/Models/StoryIntelligenceImportPreviewModels.cs`.
|
||||
- Scene results are stored by `/srv/repos/PlotDirector/PlotLine/Data/StoryIntelligenceResultRepository.cs` via `dbo.StoryIntelligenceSceneResult_Save`.
|
||||
- The visualisation reads `ParsedJson`, deserializes `SceneIntelligenceScene`, and projects observations into `StoryIntelligenceExperienceTextItem`.
|
||||
|
||||
Consumers:
|
||||
|
||||
- Character/location/asset/relationship/knowledge import services read scene JSON.
|
||||
- Story Memory reads characters, appearance, locations, assets, and relationships.
|
||||
- The visualisation explicitly renders observations. No clear normal user-facing consumer of `scene.Observations` was identified outside the visualisation-style experience.
|
||||
|
||||
Classification:
|
||||
|
||||
- CORE STORY INTELLIGENCE: scene summary, characters, relationships, locations, assets, knowledge changes, source limits.
|
||||
- USEFUL DERIVED INTELLIGENCE: known-character aliases, deterministic appearance facts if image matching stays, Story Memory identities if future context/intent uses them.
|
||||
- VISUALISATION TELEMETRY: visual DTOs, panel state, change token, replay index, display weights, latest-discovery phrasing, activity feed.
|
||||
- UNUSED / AI EXHAUST: `observations` appears likely unless a downstream product consumer is added.
|
||||
|
||||
## 9. Image-Generation Lifecycle
|
||||
|
||||
Current route:
|
||||
|
||||
```text
|
||||
Scene result saved
|
||||
-> StoryMemoryService.ProcessSceneResultAsync
|
||||
-> Upsert durable memory characters/locations/assets/relationships
|
||||
-> ResolveIllustrationsAsync
|
||||
-> match approved/generated illustration library items
|
||||
-> if none, upsert demand
|
||||
-> if QueueDemand enabled, create/queue planned library item
|
||||
-> IllustrationGenerationWorker claims queued item
|
||||
-> OpenAI image generation
|
||||
-> uploaded file persisted to illustration library
|
||||
-> future snapshots/replays can reuse assignment/library item
|
||||
```
|
||||
|
||||
Findings:
|
||||
|
||||
- Existing library images are checked first. Story Memory filters active, non-archived, non-fallback, generated/approved items and scores hard compatibility before queueing demand.
|
||||
- Duplicate demand is limited by `ImportSessionID + EntityType + ArchetypeKey`; alias drift can still create separate canonical identity keys if alias resolution has not merged them.
|
||||
- Image demands are tied to Story Memory temporary/import-session entities, not normal canonical Character records.
|
||||
- The generated image is stored in the Story Intelligence illustration library, not automatically assigned to the canonical `Characters` table or normal character gallery/avatar.
|
||||
- Subsequent chapters in the same import can reuse Story Memory assignments and library items.
|
||||
- Subsequent imports can reuse the global library item if compatible, but per-import Story Memory assignments are separate.
|
||||
- Failed/partial imports can leave generated or queued library items and demand records because the image library is independent of final import commit.
|
||||
- The visualisation itself benefits from images, but normal manuscript import does not require generated images.
|
||||
|
||||
Every route that can incur image generation cost:
|
||||
|
||||
- Story Memory during the active runner, when `AutomaticImageGenerationDuringImportEnabled == true`, `RequireImageGenerationApproval == false`, and caps allow queueing.
|
||||
- Development replay/rebuild path if called with `queueDemand=true`, followed by the image worker.
|
||||
- Admin illustration library/starter/regeneration workflows outside import.
|
||||
- Older `StoryIntelligenceIllustrationMatchingService` can upsert planned items/queue demand from visualisation DTOs; it should be treated as visualisation-coupled until proven otherwise.
|
||||
|
||||
## 10. SignalR and Reusable Infrastructure
|
||||
|
||||
Preserve:
|
||||
|
||||
- `/srv/repos/PlotDirector/PlotLine/Hubs/StoryIntelligenceHub.cs`
|
||||
- `/srv/repos/PlotDirector/PlotLine/Services/StoryIntelligenceProgressNotifier.cs`
|
||||
- `/srv/repos/PlotDirector/PlotLine/wwwroot/js/story-intelligence-progress.js`
|
||||
- `StoryIntelligenceRunProgressEvent`
|
||||
|
||||
These are not obsolete visualisation code. They power the production progress screen and are a natural fit for the simplified unattended import.
|
||||
|
||||
The full visualisation does not use SignalR directly; it polls read-only snapshot endpoints. The onboarding/progress UI uses SignalR and should remain as lightweight progress infrastructure.
|
||||
|
||||
## 11. Preservation Inventory
|
||||
|
||||
PRESERVE AS ACTIVE INFRASTRUCTURE:
|
||||
|
||||
- Persisted run/result pipeline, background worker, queue/claim/complete/fail/cancel stored procedures.
|
||||
- SignalR hub/notifier and production progress page.
|
||||
- Story Intelligence review/import services for scenes, characters, locations, assets, relationships, and knowledge.
|
||||
- Email queue infrastructure, although no completion email for Story Intelligence was found.
|
||||
- Illustration library infrastructure if retained for user/admin image workflows.
|
||||
|
||||
PRESERVE FOR FUTURE VISUALISATION:
|
||||
|
||||
- Full visual design ideas from `StoryIntelligenceExperience.cshtml`, `story-intelligence-experience-prototype.css`, and `story-intelligence-experience-prototype.js`.
|
||||
- Read-only snapshot endpoint pattern.
|
||||
- Replay controls and ability to reconstruct from persisted results.
|
||||
- Timeline/ribbon, relationship-line drawing, text fallback cards, insight detail panels, and diagnostics.
|
||||
- Static fallback prototype assets.
|
||||
|
||||
SAFE TO REMOVE after documentation and route cleanup:
|
||||
|
||||
- Development-only simulation data and static prototype images, if no future visual replay is planned.
|
||||
- Replay link from active onboarding, if the full visualisation is removed from the workflow.
|
||||
- Visualisation-specific DTOs and snapshot endpoints, after confirming no external client uses them.
|
||||
|
||||
DO NOT REMOVE UNTIL DECOUPLED:
|
||||
|
||||
- Story Memory processing from the active runner, because it is currently intertwined with illustration assignment/demand and durable derived state.
|
||||
- `characterAppearance` in the scene prompt until image matching/Story Memory decisions are made.
|
||||
- Old project-level `StoryIntelligenceIllustrationMatchingService` and repository until references and tables are audited for production use.
|
||||
- Any SQL tables/procs in the 135-147 range without mapping them to current consumers.
|
||||
|
||||
## 12. Future Visualisation Boundary
|
||||
|
||||
A future visualisation should subscribe only to:
|
||||
|
||||
- `ImportSessionStatusChanged`
|
||||
- `ChapterRunQueued`
|
||||
- `ChapterRunStarted`
|
||||
- `ChapterRunCompleted`
|
||||
- `SceneResultPersisted`
|
||||
- `ReviewStageAvailable`
|
||||
- `ImportFailed`
|
||||
- `ImportCompleted`
|
||||
- `ImageAssignmentAvailable` only when an image exists naturally
|
||||
|
||||
Each event should contain IDs, counts, status, display-safe summary, and links to persisted records. It should not contain prompt-only display demands. Visualisation should load data through read-only queries against persisted scene results, canonical import previews, and existing image assignments.
|
||||
|
||||
The future visualisation must not:
|
||||
|
||||
- Add prompt fields.
|
||||
- Trigger Story Memory rebuild.
|
||||
- Queue image demand.
|
||||
- Persist visual-only inferred relationships.
|
||||
- Request JSON repair.
|
||||
- Cause additional AI calls.
|
||||
|
||||
## 13. Simplified Unattended-Import Requirements
|
||||
|
||||
Already exists:
|
||||
|
||||
- Durable pending/running/completed/failed runs.
|
||||
- Background processing independent of the browser.
|
||||
- Persisted progress/status fields.
|
||||
- Cancellation.
|
||||
- Production progress page that says users can leave and return later.
|
||||
- Email queue infrastructure and user email addresses.
|
||||
|
||||
Missing or incomplete:
|
||||
|
||||
- Story Intelligence completion email was not found.
|
||||
- A single authoritative import-session completion event/worker action should queue email once.
|
||||
- Production UX should return users directly to review/import results, not development replay.
|
||||
- AI/image cost gates should be explicit before queueing text/image work.
|
||||
- Story Memory and image demand should be optional/deferred for unattended import.
|
||||
- Progress should rely on persisted pipeline state, not in-browser visual state.
|
||||
|
||||
## 14. Screenshots and References
|
||||
|
||||
- Existing design reference: `/srv/repos/PlotDirector/PlotLine/Docs/Features/PlotDirector_Story_Intelligence_Visualisation_Design.png`
|
||||
- Experience design document: `/srv/repos/PlotDirector/PlotLine/Docs/Features/PlotDirector_Story_Intelligence_Experience_Design_v2.md`
|
||||
- Active progress page: `/srv/repos/PlotDirector/PlotLine/Views/Onboarding/StoryIntelligenceProgress.cshtml`
|
||||
- Full experience page: `/srv/repos/PlotDirector/PlotLine/Views/Development/StoryIntelligenceExperience.cshtml`
|
||||
- Full experience CSS: `/srv/repos/PlotDirector/PlotLine/wwwroot/css/story-intelligence-experience-prototype.css`
|
||||
- Full experience JS: `/srv/repos/PlotDirector/PlotLine/wwwroot/js/story-intelligence-experience-prototype.js`
|
||||
|
||||
## 15. Known Good Design Ideas Worth Preserving
|
||||
|
||||
- "Living story view" as a stage, not a table.
|
||||
- Progress ring plus plain status numbers.
|
||||
- Character/location prominence based on relevance and point of view.
|
||||
- Relationship lines with visual tone for support/conflict/uncertainty.
|
||||
- Scene ribbon and timeline to show motion through the manuscript.
|
||||
- Text-only evidence cards when image confidence is low.
|
||||
- Replay from persisted results.
|
||||
- Compact diagnostic panel for engineering trust.
|
||||
- Respect for reduced motion.
|
||||
- "Return to Import" route back to the actual workflow.
|
||||
|
||||
## Recommended Subsequent Task
|
||||
|
||||
Do not remove anything yet. The next implementation task should:
|
||||
|
||||
1. Remove the development replay link from the active onboarding flow or hide it behind an admin/development flag.
|
||||
2. Add a completion-email path driven by persisted import-session completion.
|
||||
3. Disable automatic image demand during normal import by default and require explicit user/admin approval before image generation.
|
||||
4. Split Story Memory into two modes: minimal domain memory required by import/context, and optional visual/image memory.
|
||||
5. Trim Scene Prompt V2 so `observations` and `characterAppearance` are included only when a downstream product feature genuinely consumes them.
|
||||
6. Replace visualisation DTO dependencies in image matching with domain DTOs, or retire the old project-level matching service.
|
||||
7. Keep a passive snapshot/replay prototype that reads persisted results only.
|
||||
|
Before Width: | Height: | Size: 460 B After Width: | Height: | Size: 460 B |
|
Before Width: | Height: | Size: 355 B After Width: | Height: | Size: 355 B |
|
Before Width: | Height: | Size: 466 B After Width: | Height: | Size: 466 B |
|
Before Width: | Height: | Size: 474 B After Width: | Height: | Size: 474 B |
|
Before Width: | Height: | Size: 378 B After Width: | Height: | Size: 378 B |
|
Before Width: | Height: | Size: 443 B After Width: | Height: | Size: 443 B |
|
Before Width: | Height: | Size: 443 B After Width: | Height: | Size: 443 B |
|
Before Width: | Height: | Size: 469 B After Width: | Height: | Size: 469 B |
|
Before Width: | Height: | Size: 931 B After Width: | Height: | Size: 931 B |
|
Before Width: | Height: | Size: 425 B After Width: | Height: | Size: 425 B |
|
Before Width: | Height: | Size: 368 B After Width: | Height: | Size: 368 B |
|
Before Width: | Height: | Size: 457 B After Width: | Height: | Size: 457 B |
|
Before Width: | Height: | Size: 413 B After Width: | Height: | Size: 413 B |
|
Before Width: | Height: | Size: 483 B After Width: | Height: | Size: 483 B |
|
Before Width: | Height: | Size: 497 B After Width: | Height: | Size: 497 B |
|
Before Width: | Height: | Size: 559 B After Width: | Height: | Size: 559 B |
|
Before Width: | Height: | Size: 469 B After Width: | Height: | Size: 469 B |
|
Before Width: | Height: | Size: 428 B After Width: | Height: | Size: 428 B |
|
Before Width: | Height: | Size: 692 B After Width: | Height: | Size: 692 B |
|
Before Width: | Height: | Size: 690 B After Width: | Height: | Size: 690 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 702 B After Width: | Height: | Size: 702 B |
|
Before Width: | Height: | Size: 692 B After Width: | Height: | Size: 692 B |
|
Before Width: | Height: | Size: 590 B After Width: | Height: | Size: 590 B |
|
Before Width: | Height: | Size: 440 B After Width: | Height: | Size: 440 B |
|
Before Width: | Height: | Size: 730 B After Width: | Height: | Size: 730 B |
|
Before Width: | Height: | Size: 816 B After Width: | Height: | Size: 816 B |
|
Before Width: | Height: | Size: 682 B After Width: | Height: | Size: 682 B |
|
Before Width: | Height: | Size: 616 B After Width: | Height: | Size: 616 B |
|
Before Width: | Height: | Size: 552 B After Width: | Height: | Size: 552 B |
|
Before Width: | Height: | Size: 557 B After Width: | Height: | Size: 557 B |
|
Before Width: | Height: | Size: 590 B After Width: | Height: | Size: 590 B |
|
Before Width: | Height: | Size: 811 B After Width: | Height: | Size: 811 B |
|
Before Width: | Height: | Size: 501 B After Width: | Height: | Size: 501 B |
|
Before Width: | Height: | Size: 648 B After Width: | Height: | Size: 648 B |
|
Before Width: | Height: | Size: 608 B After Width: | Height: | Size: 608 B |