Improve Story Intelligence visualisation assignments
This commit is contained in:
parent
9bb738f07e
commit
7dd14f13a9
@ -1026,7 +1026,7 @@ static void Phase21TRepairsDurableCharacterUnderstanding()
|
|||||||
var snapshot = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine/Services/StoryIntelligenceVisualisationSnapshotService.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("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(service.Contains("ExtensionEvidence(sceneCharacter.ExtensionData)", StringComparison.Ordinal), "Saved scene appearance extension data must be included in durable evidence.");
|
||||||
Assert(snapshot.Contains("NonPovRole(memoryCharacter.Role)", StringComparison.Ordinal), "Carried memory characters must not keep POV role outside the resolved current POV.");
|
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(snapshot.Contains("ApplyPersistedStoryMemoryAssignmentsAsync(importSessionId, model)", StringComparison.Ordinal), "Live and replay must apply durable Story Memory assignments.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1094,6 +1094,8 @@ static void Phase21UEnforcesAliasAgeSemanticAndUiResetRules()
|
|||||||
var alisonName = StoryIntelligenceIllustrationCompatibility.InferCharacterEvidence("Alison", ["Alison"], []);
|
var alisonName = StoryIntelligenceIllustrationCompatibility.InferCharacterEvidence("Alison", ["Alison"], []);
|
||||||
var sophie = StoryIntelligenceIllustrationCompatibility.InferCharacterEvidence("Sophie", ["Sophie glared at me. She called the pub hers."], ["Sophie is Rebecca's sister."]);
|
var sophie = StoryIntelligenceIllustrationCompatibility.InferCharacterEvidence("Sophie", ["Sophie glared at me. She called the pub hers."], ["Sophie is Rebecca's sister."]);
|
||||||
var gregFromRosieAction = StoryIntelligenceIllustrationCompatibility.InferCharacterEvidence("Greg", ["Greg offers to take Rosie to hospital."], []);
|
var gregFromRosieAction = StoryIntelligenceIllustrationCompatibility.InferCharacterEvidence("Greg", ["Greg offers to take Rosie to hospital."], []);
|
||||||
|
var judyName = StoryIntelligenceIllustrationCompatibility.InferCharacterEvidence("Judy", ["Judy"], []);
|
||||||
|
var philName = StoryIntelligenceIllustrationCompatibility.InferCharacterEvidence("Phil", ["Phil"], []);
|
||||||
var childCandidate = Metadata("char-child", "Child", "Feminine");
|
var childCandidate = Metadata("char-child", "Child", "Feminine");
|
||||||
var darkRedCandidate = Metadata("char-red-dark", "OlderTeen", "Feminine", hairColour: "Red", skinTone: "Dark");
|
var darkRedCandidate = Metadata("char-red-dark", "OlderTeen", "Feminine", hairColour: "Red", skinTone: "Dark");
|
||||||
Assert(bethFromLeakyEvidence.AgeBand != StoryIntelligenceIllustrationCompatibility.AgeBands.MatureAdult, $"Leaked family-role evidence should not age Beth up, got {bethFromLeakyEvidence.AgeBand}.");
|
Assert(bethFromLeakyEvidence.AgeBand != StoryIntelligenceIllustrationCompatibility.AgeBands.MatureAdult, $"Leaked family-role evidence should not age Beth up, got {bethFromLeakyEvidence.AgeBand}.");
|
||||||
@ -1104,6 +1106,8 @@ static void Phase21UEnforcesAliasAgeSemanticAndUiResetRules()
|
|||||||
Assert(alisonName.Presentation == StoryIntelligenceIllustrationCompatibility.Presentations.Feminine, $"Common feminine name Alison should infer Feminine, got {alisonName.Presentation}.");
|
Assert(alisonName.Presentation == StoryIntelligenceIllustrationCompatibility.Presentations.Feminine, $"Common feminine name Alison should infer Feminine, got {alisonName.Presentation}.");
|
||||||
Assert(sophie.Presentation == StoryIntelligenceIllustrationCompatibility.Presentations.Feminine, $"Sophie/she/sister evidence should infer Feminine, got {sophie.Presentation}.");
|
Assert(sophie.Presentation == StoryIntelligenceIllustrationCompatibility.Presentations.Feminine, $"Sophie/she/sister evidence should infer Feminine, got {sophie.Presentation}.");
|
||||||
Assert(gregFromRosieAction.Presentation != StoryIntelligenceIllustrationCompatibility.Presentations.Feminine, "Greg must not inherit feminine presentation from action text mentioning Rosie.");
|
Assert(gregFromRosieAction.Presentation != StoryIntelligenceIllustrationCompatibility.Presentations.Feminine, "Greg must not inherit feminine presentation from action text mentioning Rosie.");
|
||||||
|
Assert(judyName.Presentation == StoryIntelligenceIllustrationCompatibility.Presentations.Feminine, $"Common feminine name Judy should infer Feminine, got {judyName.Presentation}.");
|
||||||
|
Assert(philName.Presentation == StoryIntelligenceIllustrationCompatibility.Presentations.Masculine, $"Common masculine name Phil should infer Masculine, got {philName.Presentation}.");
|
||||||
Assert(Rejected(mum, childCandidate).Any(reason => reason.Contains("age hard mismatch", StringComparison.OrdinalIgnoreCase)), "Parent role should reject child portraits.");
|
Assert(Rejected(mum, childCandidate).Any(reason => reason.Contains("age hard mismatch", StringComparison.OrdinalIgnoreCase)), "Parent role should reject child portraits.");
|
||||||
Assert(Rejected(redHairedMaggie, darkRedCandidate).Any(reason => reason.Contains("skin tone hard mismatch", StringComparison.OrdinalIgnoreCase)), "Red-haired light-skin evidence must reject dark-skin generated portraits.");
|
Assert(Rejected(redHairedMaggie, darkRedCandidate).Any(reason => reason.Contains("skin tone hard mismatch", StringComparison.OrdinalIgnoreCase)), "Red-haired light-skin evidence must reject dark-skin generated portraits.");
|
||||||
Assert(examiner.AgeBand == StoryIntelligenceIllustrationCompatibility.AgeBands.Adult, $"Driving examiner should infer Adult, got {examiner.AgeBand}.");
|
Assert(examiner.AgeBand == StoryIntelligenceIllustrationCompatibility.AgeBands.Adult, $"Driving examiner should infer Adult, got {examiner.AgeBand}.");
|
||||||
@ -1127,7 +1131,12 @@ static void Phase21UEnforcesAliasAgeSemanticAndUiResetRules()
|
|||||||
|
|
||||||
var root = Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine");
|
var root = Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine");
|
||||||
var onboarding = File.ReadAllText(Path.Combine(root, "Views/Onboarding/StoryIntelligence.cshtml"));
|
var onboarding = File.ReadAllText(Path.Combine(root, "Views/Onboarding/StoryIntelligence.cshtml"));
|
||||||
|
var adminController = File.ReadAllText(Path.Combine(root, "Controllers/AdminController.cs"));
|
||||||
|
var adminDiagnostics = File.ReadAllText(Path.Combine(root, "Views/Admin/StoryIntelligenceDiagnostics.cshtml"));
|
||||||
|
var program = File.ReadAllText(Path.Combine(root, "Program.cs"));
|
||||||
var storyMemory = File.ReadAllText(Path.Combine(root, "Services/StoryMemoryServices.cs"));
|
var storyMemory = File.ReadAllText(Path.Combine(root, "Services/StoryMemoryServices.cs"));
|
||||||
|
var snapshot = File.ReadAllText(Path.Combine(root, "Services/StoryIntelligenceVisualisationSnapshotService.cs"));
|
||||||
|
var developmentReset = File.ReadAllText(Path.Combine(root, "Services/StoryIntelligenceDevelopmentResetService.cs"));
|
||||||
var persistedRunner = File.ReadAllText(Path.Combine(root, "Services/PersistedStoryIntelligenceRunner.cs"));
|
var persistedRunner = File.ReadAllText(Path.Combine(root, "Services/PersistedStoryIntelligenceRunner.cs"));
|
||||||
var script = File.ReadAllText(Path.Combine(root, "wwwroot/js/story-intelligence-experience-prototype.js"));
|
var script = File.ReadAllText(Path.Combine(root, "wwwroot/js/story-intelligence-experience-prototype.js"));
|
||||||
var css = File.ReadAllText(Path.Combine(root, "wwwroot/css/story-intelligence-experience-prototype.css"));
|
var css = File.ReadAllText(Path.Combine(root, "wwwroot/css/story-intelligence-experience-prototype.css"));
|
||||||
@ -1158,6 +1167,20 @@ static void Phase21UEnforcesAliasAgeSemanticAndUiResetRules()
|
|||||||
Assert(storyMemory.Contains("semanticType is \"Road\" or \"Lane\" or \"Street\"", StringComparison.Ordinal), "Road-family locations must be treated as exterior scenes.");
|
Assert(storyMemory.Contains("semanticType is \"Road\" or \"Lane\" or \"Street\"", StringComparison.Ordinal), "Road-family locations must be treated as exterior scenes.");
|
||||||
Assert(storyMemory.Contains("evidence.HairColour", StringComparison.Ordinal), "Character demand keys/specs must include durable hair colour evidence.");
|
Assert(storyMemory.Contains("evidence.HairColour", StringComparison.Ordinal), "Character demand keys/specs must include durable hair colour evidence.");
|
||||||
Assert(storyMemory.Contains("Maximum one primary POV", StringComparison.Ordinal) || storyMemory.Contains("ResolveScenePov", StringComparison.Ordinal), "Story Memory must use a single POV resolver.");
|
Assert(storyMemory.Contains("Maximum one primary POV", StringComparison.Ordinal) || storyMemory.Contains("ResolveScenePov", StringComparison.Ordinal), "Story Memory must use a single POV resolver.");
|
||||||
|
Assert(!snapshot.Contains(".Where(character => !sceneCharacterNames.Contains(character.Name))", StringComparison.Ordinal), "Visualisation must not fill scene character slots from memory-only characters.");
|
||||||
|
Assert(snapshot.Contains("PovResolutionReason = resolved is null ? \"unresolved narrator\" : \"inherited same-chapter POV\"", StringComparison.Ordinal), "Generic narrators should inherit only within the current chapter.");
|
||||||
|
Assert(!snapshot.Contains("?? bookNarratorName\r\n ?? narratorResolvedName", StringComparison.Ordinal), "Generic narrator resolution must not fall through to stale book-level narrator identity.");
|
||||||
|
Assert(snapshot.Contains("BuildObservations(scene, resolvedPovName)", StringComparison.Ordinal), "Observation cards should receive the resolved POV identity.");
|
||||||
|
Assert(snapshot.Contains("NamedPovText", StringComparison.Ordinal), "Observation card text should replace generic narrator wording when POV is known.");
|
||||||
|
Assert(program.Contains("IStoryIntelligenceDevelopmentResetService", StringComparison.Ordinal), "Admin reset service must be registered.");
|
||||||
|
Assert(adminController.Contains("ResetStoryIntelligenceImports", StringComparison.Ordinal), "Admin diagnostics must expose the reset post action.");
|
||||||
|
Assert(adminDiagnostics.Contains("Reset my Story Intelligence data", StringComparison.Ordinal), "Admin diagnostics page must show the reset button.");
|
||||||
|
Assert(developmentReset.Contains("PreservedIllustrationItems", StringComparison.Ordinal), "Admin reset result must report preserved generated images.");
|
||||||
|
Assert(!developmentReset.Contains("DELETE FROM dbo.IllustrationLibraryItems", StringComparison.Ordinal), "Admin reset must preserve generated illustration library records.");
|
||||||
|
Assert(script.Contains("renderPresenceCallout", StringComparison.Ordinal), "Stage should show a presence callout for characters in the active location.");
|
||||||
|
Assert(script.Contains("drawConnection(id, cluster, locationPoint, \"presence-cluster\", 54, \"\"", StringComparison.Ordinal), "Presence connector should not show a tiny count label.");
|
||||||
|
Assert(css.Contains(".story-exp-presence-callout", StringComparison.Ordinal), "Presence callout styling is missing.");
|
||||||
|
Assert(css.Contains("white-space: normal", StringComparison.Ordinal), "Location captions should wrap rather than ellipsise.");
|
||||||
Assert(!script.Contains("renderOverflowSummary", StringComparison.Ordinal), "Side panels must not use hidden '+N more' overflow controls.");
|
Assert(!script.Contains("renderOverflowSummary", StringComparison.Ordinal), "Side panels must not use hidden '+N more' overflow controls.");
|
||||||
Assert(css.Contains("-webkit-line-clamp: 3", StringComparison.Ordinal), "Insight body text should be clamped inside its card.");
|
Assert(css.Contains("-webkit-line-clamp: 3", StringComparison.Ordinal), "Insight body text should be clamped inside its card.");
|
||||||
Assert(resetSql.Contains("StoryIntelligence_DevelopmentAccountReset", StringComparison.Ordinal), "Phase 21U reset procedure is missing.");
|
Assert(resetSql.Contains("StoryIntelligence_DevelopmentAccountReset", StringComparison.Ordinal), "Phase 21U reset procedure is missing.");
|
||||||
@ -1189,8 +1212,8 @@ static void Phase21RSceneBrowserAndPanelsAreReplayFriendly()
|
|||||||
Assert(css.Contains("--story-side-body-font-size: 0.72rem;", StringComparison.Ordinal), "Left and right side panels should share body text sizing.");
|
Assert(css.Contains("--story-side-body-font-size: 0.72rem;", StringComparison.Ordinal), "Left and right side panels should share body text sizing.");
|
||||||
Assert(!view.Contains("data-assets", StringComparison.Ordinal), "Asset visualisation lane should not be rendered on the main stage.");
|
Assert(!view.Contains("data-assets", StringComparison.Ordinal), "Asset visualisation lane should not be rendered on the main stage.");
|
||||||
Assert(script.Contains("Assets are recorded in insights/diagnostics but not rendered as stage nodes.", StringComparison.Ordinal), "Asset data should remain available without drawing asset stage nodes.");
|
Assert(script.Contains("Assets are recorded in insights/diagnostics but not rendered as stage nodes.", StringComparison.Ordinal), "Asset data should remain available without drawing asset stage nodes.");
|
||||||
Assert(script.Contains("width * 0.84 - size / 2", StringComparison.Ordinal), "Location should be a compact right-side anchor once asset nodes are removed.");
|
Assert(script.Contains("width * 0.82 - size / 2", StringComparison.Ordinal), "Location should be a larger right-side anchor once asset nodes are removed.");
|
||||||
Assert(script.Contains("{ x: 0.60, y: 0.66, role: \"context\" }", StringComparison.Ordinal), "Character layout should use more of the stage width.");
|
Assert(script.Contains("{ x: 0.68, y: 0.26, role: \"context\" }", StringComparison.Ordinal), "Character layout should use more of the stage width.");
|
||||||
Assert(script.Contains("presence-cluster", StringComparison.Ordinal), "Presence should be shown as a single location callout instead of many confusing location lines.");
|
Assert(script.Contains("presence-cluster", StringComparison.Ordinal), "Presence should be shown as a single location callout instead of many confusing location lines.");
|
||||||
Assert(css.Contains(".story-exp-zone--location .story-exp-zone-label {\n left: 80%;", StringComparison.Ordinal), "Location lane label should follow the compact right-side anchor.");
|
Assert(css.Contains(".story-exp-zone--location .story-exp-zone-label {\n left: 80%;", StringComparison.Ordinal), "Location lane label should follow the compact right-side anchor.");
|
||||||
Assert(!css.Contains("grid-template-rows: minmax(218px, 1.35fr) minmax(185px, 1.08fr) auto", StringComparison.Ordinal), "Right rail must not use fixed-height panel tracks.");
|
Assert(!css.Contains("grid-template-rows: minmax(218px, 1.35fr) minmax(185px, 1.08fr) auto", StringComparison.Ordinal), "Right rail must not use fixed-height panel tracks.");
|
||||||
|
|||||||
@ -13,6 +13,7 @@ public sealed class AdminController(
|
|||||||
IFeatureRequestService featureRequests,
|
IFeatureRequestService featureRequests,
|
||||||
IEmailService emails,
|
IEmailService emails,
|
||||||
IStoryIntelligenceDiagnosticsService storyIntelligenceDiagnostics,
|
IStoryIntelligenceDiagnosticsService storyIntelligenceDiagnostics,
|
||||||
|
IStoryIntelligenceDevelopmentResetService storyIntelligenceDevelopmentReset,
|
||||||
IStoryIntelligenceDryRunService storyIntelligenceDryRun,
|
IStoryIntelligenceDryRunService storyIntelligenceDryRun,
|
||||||
IChapterStructureDryRunService chapterStructureDryRun,
|
IChapterStructureDryRunService chapterStructureDryRun,
|
||||||
IChapterStoryIntelligenceDryRunService chapterStoryIntelligenceDryRun,
|
IChapterStoryIntelligenceDryRunService chapterStoryIntelligenceDryRun,
|
||||||
@ -159,6 +160,20 @@ public sealed class AdminController(
|
|||||||
return View(await storyIntelligenceDiagnostics.GetDiagnosticsAsync(cancellationToken));
|
return View(await storyIntelligenceDiagnostics.GetDiagnosticsAsync(cancellationToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("story-intelligence-diagnostics/reset-imports")]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> ResetStoryIntelligenceImports(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (currentUser.UserId is not int userId)
|
||||||
|
{
|
||||||
|
return Forbid();
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await storyIntelligenceDevelopmentReset.ResetOwnedProjectsAndImportsAsync(userId, cancellationToken);
|
||||||
|
TempData[result.Success ? "AdminMessage" : "AdminError"] = result.Message;
|
||||||
|
return RedirectToAction(nameof(StoryIntelligenceDiagnostics));
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("story-intelligence-test")]
|
[HttpGet("story-intelligence-test")]
|
||||||
public IActionResult StoryIntelligenceTest()
|
public IActionResult StoryIntelligenceTest()
|
||||||
{
|
{
|
||||||
|
|||||||
@ -207,6 +207,7 @@ public class Program
|
|||||||
builder.Services.AddScoped<IStorySceneValidator, StorySceneValidator>();
|
builder.Services.AddScoped<IStorySceneValidator, StorySceneValidator>();
|
||||||
builder.Services.AddScoped<IChapterStructureValidator, ChapterStructureValidator>();
|
builder.Services.AddScoped<IChapterStructureValidator, ChapterStructureValidator>();
|
||||||
builder.Services.AddScoped<IStoryIntelligenceDiagnosticsService, StoryIntelligenceDiagnosticsService>();
|
builder.Services.AddScoped<IStoryIntelligenceDiagnosticsService, StoryIntelligenceDiagnosticsService>();
|
||||||
|
builder.Services.AddScoped<IStoryIntelligenceDevelopmentResetService, StoryIntelligenceDevelopmentResetService>();
|
||||||
builder.Services.AddScoped<IStoryIntelligenceDryRunService, StoryIntelligenceDryRunService>();
|
builder.Services.AddScoped<IStoryIntelligenceDryRunService, StoryIntelligenceDryRunService>();
|
||||||
builder.Services.AddScoped<IChapterStructureDryRunService, ChapterStructureDryRunService>();
|
builder.Services.AddScoped<IChapterStructureDryRunService, ChapterStructureDryRunService>();
|
||||||
builder.Services.AddScoped<IChapterStoryIntelligenceDryRunService, ChapterStoryIntelligenceDryRunService>();
|
builder.Services.AddScoped<IChapterStoryIntelligenceDryRunService, ChapterStoryIntelligenceDryRunService>();
|
||||||
|
|||||||
249
PlotLine/Services/StoryIntelligenceDevelopmentResetService.cs
Normal file
249
PlotLine/Services/StoryIntelligenceDevelopmentResetService.cs
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
using Dapper;
|
||||||
|
using PlotLine.Data;
|
||||||
|
|
||||||
|
namespace PlotLine.Services;
|
||||||
|
|
||||||
|
public interface IStoryIntelligenceDevelopmentResetService
|
||||||
|
{
|
||||||
|
Task<StoryIntelligenceDevelopmentResetResult> ResetOwnedProjectsAndImportsAsync(int userId, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class StoryIntelligenceDevelopmentResetService(
|
||||||
|
ISqlConnectionFactory connectionFactory,
|
||||||
|
ILogger<StoryIntelligenceDevelopmentResetService> logger) : IStoryIntelligenceDevelopmentResetService
|
||||||
|
{
|
||||||
|
public async Task<StoryIntelligenceDevelopmentResetResult> ResetOwnedProjectsAndImportsAsync(int userId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
var command = new CommandDefinition(PrepareResetSql, new { UserID = userId }, cancellationToken: cancellationToken, commandTimeout: 120);
|
||||||
|
var summary = await connection.QuerySingleAsync<PreparedResetSummary>(command);
|
||||||
|
|
||||||
|
foreach (var projectId in summary.ProjectIdList)
|
||||||
|
{
|
||||||
|
var deleteCommand = new CommandDefinition(
|
||||||
|
"dbo.ProjectHardDelete_ForOwner",
|
||||||
|
new { ProjectID = projectId, UserID = userId },
|
||||||
|
commandType: System.Data.CommandType.StoredProcedure,
|
||||||
|
cancellationToken: cancellationToken,
|
||||||
|
commandTimeout: 120);
|
||||||
|
await connection.ExecuteAsync(deleteCommand);
|
||||||
|
}
|
||||||
|
|
||||||
|
var verificationCommand = new CommandDefinition(VerifyResetSql, new { UserID = userId }, cancellationToken: cancellationToken, commandTimeout: 120);
|
||||||
|
var verification = await connection.QuerySingleAsync<ResetVerification>(verificationCommand);
|
||||||
|
var result = new StoryIntelligenceDevelopmentResetResult(
|
||||||
|
summary.ProjectIdList.Count,
|
||||||
|
summary.ImportSessionCount,
|
||||||
|
summary.RunCount,
|
||||||
|
verification.RemainingProjects,
|
||||||
|
verification.RemainingImports,
|
||||||
|
verification.RemainingRuns,
|
||||||
|
verification.PreservedIllustrationItems);
|
||||||
|
|
||||||
|
logger.LogWarning(
|
||||||
|
"Admin Story Intelligence reset completed for user {UserId}. DeletedProjects={DeletedProjects} DeletedImports={DeletedImports} DeletedRuns={DeletedRuns} PreservedIllustrationItems={PreservedIllustrationItems}.",
|
||||||
|
userId,
|
||||||
|
result.DeletedProjects,
|
||||||
|
result.DeletedImportSessions,
|
||||||
|
result.DeletedRuns,
|
||||||
|
result.PreservedIllustrationItems);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private const string PrepareResetSql = """
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
DECLARE @ProjectIDs TABLE (ProjectID int NOT NULL PRIMARY KEY);
|
||||||
|
DECLARE @BookIDs TABLE (BookID int NOT NULL PRIMARY KEY);
|
||||||
|
DECLARE @ImportSessionIDs TABLE (ImportSessionID int NOT NULL PRIMARY KEY);
|
||||||
|
DECLARE @RunIDs TABLE (StoryIntelligenceRunID int NOT NULL PRIMARY KEY);
|
||||||
|
|
||||||
|
INSERT @ProjectIDs (ProjectID)
|
||||||
|
SELECT DISTINCT p.ProjectID
|
||||||
|
FROM dbo.Projects p
|
||||||
|
INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = p.ProjectID
|
||||||
|
WHERE pua.UserID = @UserID
|
||||||
|
AND pua.AccessRole = N'Owner'
|
||||||
|
AND pua.IsActive = 1;
|
||||||
|
|
||||||
|
INSERT @BookIDs (BookID)
|
||||||
|
SELECT b.BookID
|
||||||
|
FROM dbo.Books b
|
||||||
|
INNER JOIN @ProjectIDs p ON p.ProjectID = b.ProjectID;
|
||||||
|
|
||||||
|
INSERT @ImportSessionIDs (ImportSessionID)
|
||||||
|
SELECT StoryIntelligenceBookPipelineID
|
||||||
|
FROM dbo.StoryIntelligenceBookPipelines pipeline
|
||||||
|
WHERE EXISTS (SELECT 1 FROM @ProjectIDs p WHERE p.ProjectID = pipeline.ProjectID)
|
||||||
|
OR EXISTS (SELECT 1 FROM @BookIDs b WHERE b.BookID = pipeline.BookID);
|
||||||
|
|
||||||
|
INSERT @RunIDs (StoryIntelligenceRunID)
|
||||||
|
SELECT StoryIntelligenceRunID
|
||||||
|
FROM dbo.StoryIntelligenceRuns run
|
||||||
|
WHERE run.UserID = @UserID
|
||||||
|
AND (
|
||||||
|
run.ProjectID IS NULL
|
||||||
|
OR EXISTS (SELECT 1 FROM @ProjectIDs p WHERE p.ProjectID = run.ProjectID)
|
||||||
|
OR EXISTS (SELECT 1 FROM @BookIDs b WHERE b.BookID = run.BookID)
|
||||||
|
);
|
||||||
|
|
||||||
|
DECLARE @ImportSessionCount int = (SELECT COUNT(*) FROM @ImportSessionIDs);
|
||||||
|
DECLARE @RunCount int = (SELECT COUNT(*) FROM @RunIDs);
|
||||||
|
|
||||||
|
UPDATE dbo.StoryIntelligenceRuns
|
||||||
|
SET Status = CASE WHEN Status IN (N'Pending', N'Running') THEN N'Cancelled' ELSE Status END,
|
||||||
|
CurrentStage = N'Cancelled before admin reset',
|
||||||
|
CurrentMessage = N'Admin reset invalidated this run while preserving generated illustrations.',
|
||||||
|
CancellationRequestedUtc = COALESCE(CancellationRequestedUtc, SYSUTCDATETIME()),
|
||||||
|
CancelledUtc = COALESCE(CancelledUtc, SYSUTCDATETIME()),
|
||||||
|
CompletedUtc = COALESCE(CompletedUtc, SYSUTCDATETIME()),
|
||||||
|
UpdatedUtc = SYSUTCDATETIME()
|
||||||
|
WHERE EXISTS (SELECT 1 FROM @RunIDs ids WHERE ids.StoryIntelligenceRunID = StoryIntelligenceRuns.StoryIntelligenceRunID)
|
||||||
|
AND Status IN (N'Pending', N'Running');
|
||||||
|
|
||||||
|
DELETE FROM dbo.StoryMemoryIllustrationAssignments
|
||||||
|
WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryMemoryIllustrationAssignments.ImportSessionID);
|
||||||
|
|
||||||
|
DELETE FROM dbo.StoryMemoryIllustrationDemands
|
||||||
|
WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryMemoryIllustrationDemands.ImportSessionID);
|
||||||
|
|
||||||
|
DELETE FROM dbo.StoryMemoryRelationships
|
||||||
|
WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryMemoryRelationships.ImportSessionID);
|
||||||
|
|
||||||
|
DELETE FROM dbo.StoryMemoryCharacterAttributes
|
||||||
|
WHERE EXISTS
|
||||||
|
(
|
||||||
|
SELECT 1
|
||||||
|
FROM dbo.StoryMemoryCharacters c
|
||||||
|
INNER JOIN @ImportSessionIDs ids ON ids.ImportSessionID = c.ImportSessionID
|
||||||
|
WHERE c.StoryMemoryCharacterID = StoryMemoryCharacterAttributes.StoryMemoryCharacterID
|
||||||
|
);
|
||||||
|
|
||||||
|
DELETE FROM dbo.StoryMemoryCharacterAliases
|
||||||
|
WHERE EXISTS
|
||||||
|
(
|
||||||
|
SELECT 1
|
||||||
|
FROM dbo.StoryMemoryCharacters c
|
||||||
|
INNER JOIN @ImportSessionIDs ids ON ids.ImportSessionID = c.ImportSessionID
|
||||||
|
WHERE c.StoryMemoryCharacterID = StoryMemoryCharacterAliases.StoryMemoryCharacterID
|
||||||
|
);
|
||||||
|
|
||||||
|
DELETE FROM dbo.StoryMemoryAssets
|
||||||
|
WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryMemoryAssets.ImportSessionID);
|
||||||
|
|
||||||
|
DELETE FROM dbo.StoryMemoryLocations
|
||||||
|
WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryMemoryLocations.ImportSessionID);
|
||||||
|
|
||||||
|
DELETE FROM dbo.StoryMemoryCharacters
|
||||||
|
WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryMemoryCharacters.ImportSessionID);
|
||||||
|
|
||||||
|
DELETE FROM dbo.StoryMemoryProcessedSceneResults
|
||||||
|
WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryMemoryProcessedSceneResults.ImportSessionID);
|
||||||
|
|
||||||
|
DELETE FROM dbo.StoryMemoryImportPreferences
|
||||||
|
WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryMemoryImportPreferences.ImportSessionID);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
DELETE FROM dbo.StoryIntelligenceIllustrationDemands
|
||||||
|
WHERE EXISTS (SELECT 1 FROM @ProjectIDs p WHERE p.ProjectID = StoryIntelligenceIllustrationDemands.ProjectID);
|
||||||
|
|
||||||
|
DELETE FROM dbo.StoryIntelligenceBookPipelines
|
||||||
|
WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryIntelligenceBookPipelines.StoryIntelligenceBookPipelineID);
|
||||||
|
|
||||||
|
DELETE FROM dbo.StoryIntelligenceSceneResults
|
||||||
|
WHERE EXISTS (SELECT 1 FROM @RunIDs ids WHERE ids.StoryIntelligenceRunID = StoryIntelligenceSceneResults.StoryIntelligenceRunID);
|
||||||
|
|
||||||
|
DELETE FROM dbo.StoryIntelligenceChapterResults
|
||||||
|
WHERE EXISTS (SELECT 1 FROM @RunIDs ids WHERE ids.StoryIntelligenceRunID = StoryIntelligenceChapterResults.StoryIntelligenceRunID);
|
||||||
|
|
||||||
|
DELETE FROM dbo.StoryIntelligenceImportCommits
|
||||||
|
WHERE EXISTS (SELECT 1 FROM @RunIDs ids WHERE ids.StoryIntelligenceRunID = StoryIntelligenceImportCommits.StoryIntelligenceRunID);
|
||||||
|
|
||||||
|
DELETE FROM dbo.StoryIntelligenceRuns
|
||||||
|
WHERE EXISTS (SELECT 1 FROM @RunIDs ids WHERE ids.StoryIntelligenceRunID = StoryIntelligenceRuns.StoryIntelligenceRunID);
|
||||||
|
|
||||||
|
UPDATE dbo.Projects
|
||||||
|
SET IsArchived = 1,
|
||||||
|
ArchivedDate = COALESCE(ArchivedDate, SYSUTCDATETIME()),
|
||||||
|
ArchivedReason = N'Admin Story Intelligence reset preserving generated illustrations',
|
||||||
|
UpdatedDate = SYSUTCDATETIME()
|
||||||
|
WHERE EXISTS (SELECT 1 FROM @ProjectIDs ids WHERE ids.ProjectID = Projects.ProjectID);
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
ProjectIds = COALESCE(STRING_AGG(CONVERT(varchar(20), ProjectID), ','), ''),
|
||||||
|
ImportSessionCount = @ImportSessionCount,
|
||||||
|
RunCount = @RunCount
|
||||||
|
FROM @ProjectIDs;
|
||||||
|
""";
|
||||||
|
|
||||||
|
private const string VerifyResetSql = """
|
||||||
|
SELECT
|
||||||
|
RemainingProjects = (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM dbo.Projects p
|
||||||
|
INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = p.ProjectID
|
||||||
|
WHERE pua.UserID = @UserID AND pua.AccessRole = N'Owner' AND pua.IsActive = 1
|
||||||
|
),
|
||||||
|
RemainingImports = (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM dbo.StoryIntelligenceBookPipelines pipeline
|
||||||
|
INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = pipeline.ProjectID
|
||||||
|
WHERE pua.UserID = @UserID AND pua.AccessRole = N'Owner' AND pua.IsActive = 1
|
||||||
|
),
|
||||||
|
RemainingRuns = (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM dbo.StoryIntelligenceRuns
|
||||||
|
WHERE UserID = @UserID
|
||||||
|
),
|
||||||
|
PreservedIllustrationItems = (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM dbo.IllustrationLibraryItems
|
||||||
|
WHERE Category IN (N'Character', N'Location', N'Asset')
|
||||||
|
);
|
||||||
|
""";
|
||||||
|
|
||||||
|
private sealed class PreparedResetSummary
|
||||||
|
{
|
||||||
|
public string ProjectIds { get; init; } = string.Empty;
|
||||||
|
public int ImportSessionCount { get; init; }
|
||||||
|
public int RunCount { get; init; }
|
||||||
|
|
||||||
|
public IReadOnlyList<int> ProjectIdList => SplitProjectIds(ProjectIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class ResetVerification
|
||||||
|
{
|
||||||
|
public int RemainingProjects { get; init; }
|
||||||
|
public int RemainingImports { get; init; }
|
||||||
|
public int RemainingRuns { get; init; }
|
||||||
|
public int PreservedIllustrationItems { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<int> SplitProjectIds(string projectIds)
|
||||||
|
=> string.IsNullOrWhiteSpace(projectIds)
|
||||||
|
? []
|
||||||
|
: projectIds.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||||
|
.Select(value => int.TryParse(value, out var id) ? id : 0)
|
||||||
|
.Where(id => id > 0)
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record StoryIntelligenceDevelopmentResetResult(
|
||||||
|
int DeletedProjects,
|
||||||
|
int DeletedImportSessions,
|
||||||
|
int DeletedRuns,
|
||||||
|
int RemainingProjects,
|
||||||
|
int RemainingImportSessions,
|
||||||
|
int RemainingRuns,
|
||||||
|
int PreservedIllustrationItems)
|
||||||
|
{
|
||||||
|
public bool Success => RemainingProjects == 0 && RemainingImportSessions == 0 && RemainingRuns == 0;
|
||||||
|
|
||||||
|
public string Message => Success
|
||||||
|
? $"Reset complete. Deleted {DeletedProjects:N0} project(s), {DeletedImportSessions:N0} import(s) and {DeletedRuns:N0} run(s). Preserved {PreservedIllustrationItems:N0} generated image record(s)."
|
||||||
|
: $"Reset finished with remaining data: {RemainingProjects:N0} project(s), {RemainingImportSessions:N0} import(s), {RemainingRuns:N0} run(s). Preserved {PreservedIllustrationItems:N0} generated image record(s).";
|
||||||
|
}
|
||||||
@ -522,8 +522,8 @@ public static class StoryIntelligenceIllustrationCompatibility
|
|||||||
|
|
||||||
private static EvidenceSignal NamePresentation(string text)
|
private static EvidenceSignal NamePresentation(string text)
|
||||||
{
|
{
|
||||||
if (ContainsAny(text, " alison ", " sophie ", " beth ", " maggie ", " rosie ", " grace ", " zoe ", " rebecca ", " annie ", " mary ", " sharon ", " catherine ", " susan ", " helen ", " elen ")) return new(Presentations.Feminine, 0.82m);
|
if (ContainsAny(text, " alison ", " annie ", " anna ", " anne ", " beth ", " catherine ", " chrissy ", " grace ", " helen ", " elen ", " judy ", " katherine ", " maggie ", " mary ", " rebecca ", " rosie ", " sharon ", " sophie ", " susan ", " zoe ")) return new(Presentations.Feminine, 0.82m);
|
||||||
if (ContainsAny(text, " adam ", " colin ", " david ", " kevin ", " simon ", " graham ", " gareth ", " john ", " michael ", " peter ", " richard ", " rick ", " george ")) return new(Presentations.Masculine, 0.82m);
|
if (ContainsAny(text, " adam ", " colin ", " david ", " gareth ", " george ", " graham ", " greg ", " john ", " kevin ", " michael ", " peter ", " phil ", " richard ", " rick ", " rob ", " roger ", " simon ")) return new(Presentations.Masculine, 0.82m);
|
||||||
return EvidenceSignal.Unknown;
|
return EvidenceSignal.Unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -559,13 +559,13 @@ public static class StoryIntelligenceIllustrationCompatibility
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (StartsWithAny(clean, "mr", "father", "dad", "uncle", "brother", "son", "boy", "man", "male")
|
if (StartsWithAny(clean, "mr", "father", "dad", "uncle", "brother", "son", "boy", "man", "male")
|
||||||
|| clean is "adam" or "colin" or "david" or "kevin" or "simon" or "graham" or "gareth" or "john" or "michael" or "peter" or "george" or "richard" or "rick")
|
|| clean is "adam" or "colin" or "david" or "gareth" or "george" or "graham" or "greg" or "john" or "kevin" or "michael" or "peter" or "phil" or "richard" or "rick" or "rob" or "roger" or "simon")
|
||||||
{
|
{
|
||||||
return new(Presentations.Masculine, 0.98m);
|
return new(Presentations.Masculine, 0.98m);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (StartsWithAny(clean, "mrs", "miss", "ms", "mother", "mum", "mom", "aunt", "sister", "daughter", "girl", "woman", "female")
|
if (StartsWithAny(clean, "mrs", "miss", "ms", "mother", "mum", "mom", "aunt", "sister", "daughter", "girl", "woman", "female")
|
||||||
|| clean is "alison" or "sophie" or "beth" or "maggie" or "grace" or "rosie" or "rebecca" or "annie" or "mary" or "susan" or "helen" or "elen" or "zoe" or "sharon" or "catherine")
|
|| clean is "alison" or "annie" or "anna" or "anne" or "beth" or "catherine" or "chrissy" or "grace" or "helen" or "elen" or "judy" or "katherine" or "maggie" or "mary" or "rebecca" or "rosie" or "sharon" or "sophie" or "susan" or "zoe")
|
||||||
{
|
{
|
||||||
return new(Presentations.Feminine, 0.98m);
|
return new(Presentations.Feminine, 0.98m);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -591,8 +591,8 @@ public sealed class StoryIntelligenceIllustrationMatchingService(
|
|||||||
Code = $"char-demand-{StableHash(demand.DemandKey)}",
|
Code = $"char-demand-{StableHash(demand.DemandKey)}",
|
||||||
Title = $"{age} {presentation.ToLowerInvariant()} character",
|
Title = $"{age} {presentation.ToLowerInvariant()} character",
|
||||||
Description = "Demand-created reusable character illustration for Story Intelligence matching.",
|
Description = "Demand-created reusable character illustration for Story Intelligence matching.",
|
||||||
VisualSubject = $"{age} {presentation.ToLowerInvariant()} character with {hairLength.ToLowerInvariant()} {hairColour.ToLowerInvariant()} hair. Skin tone requirement: {SkinToneInstruction(skinTone)}.",
|
VisualSubject = $"{age} {presentation.ToLowerInvariant()} character with {hairLength.ToLowerInvariant()} {hairColour.ToLowerInvariant()} hair. Skin tone requirement: {SkinToneInstruction(skinTone)}. The face must read immediately as {PresentationPrompt(presentation)} with distinct facial structure, expression, jaw, eyes, nose and hairstyle silhouette.",
|
||||||
Composition = "clear head-and-shoulders cinematic portrait with readable face, subtle atmospheric story background, clean separation, suitable for circular cropping",
|
Composition = "clear head-and-shoulders cinematic portrait with readable face, subtle atmospheric story background, clean separation, suitable for circular cropping, visibly different from other reusable portraits",
|
||||||
Mood = "neutral, story-ready, emotionally readable, subdued and cinematic",
|
Mood = "neutral, story-ready, emotionally readable, subdued and cinematic",
|
||||||
ApparentAgeBand = age,
|
ApparentAgeBand = age,
|
||||||
Presentation = presentation,
|
Presentation = presentation,
|
||||||
@ -697,6 +697,14 @@ public sealed class StoryIntelligenceIllustrationMatchingService(
|
|||||||
_ => "not specified"
|
_ => "not specified"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static string PresentationPrompt(string presentation)
|
||||||
|
=> presentation switch
|
||||||
|
{
|
||||||
|
StoryIntelligenceIllustrationCompatibility.Presentations.Feminine => "a recognisably feminine woman or girl, not androgynous and not masculine",
|
||||||
|
StoryIntelligenceIllustrationCompatibility.Presentations.Masculine => "a recognisably masculine man or boy, not androgynous and not feminine",
|
||||||
|
_ => "the requested presentation"
|
||||||
|
};
|
||||||
|
|
||||||
private static Dictionary<string, CharacterObservation> BuildCharacterObservations(StoryIntelligenceExperiencePrototypeViewModel prototype)
|
private static Dictionary<string, CharacterObservation> BuildCharacterObservations(StoryIntelligenceExperiencePrototypeViewModel prototype)
|
||||||
{
|
{
|
||||||
var observations = new Dictionary<string, CharacterObservation>(StringComparer.OrdinalIgnoreCase);
|
var observations = new Dictionary<string, CharacterObservation>(StringComparer.OrdinalIgnoreCase);
|
||||||
@ -781,12 +789,12 @@ public sealed class StoryIntelligenceIllustrationMatchingService(
|
|||||||
|
|
||||||
private static EvidenceSignal NamePresentation(string text)
|
private static EvidenceSignal NamePresentation(string text)
|
||||||
{
|
{
|
||||||
if (ContainsAny(text, " beth ", " maggie ", " rosie ", " grace ", " annie ", " helen ", " elen "))
|
if (ContainsAny(text, " alison ", " annie ", " anna ", " anne ", " beth ", " chrissy ", " grace ", " helen ", " elen ", " judy ", " katherine ", " maggie ", " mary ", " rebecca ", " rosie ", " sharon ", " sophie "))
|
||||||
{
|
{
|
||||||
return new("Feminine", 0.82m);
|
return new("Feminine", 0.82m);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ContainsAny(text, " graham ", " george ", " john ", " david ", " simon ", " kevin ", " colin "))
|
if (ContainsAny(text, " colin ", " david ", " george ", " graham ", " greg ", " john ", " kevin ", " phil ", " roger ", " rick ", " rob ", " simon "))
|
||||||
{
|
{
|
||||||
return new("Masculine", 0.82m);
|
return new("Masculine", 0.82m);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -349,6 +349,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var isCurrent = current?.Result.SceneResultID == item.Result.SceneResultID;
|
var isCurrent = current?.Result.SceneResultID == item.Result.SceneResultID;
|
||||||
|
var resolvedPovName = storyMemory.ResolvePointOfView(scene);
|
||||||
yield return new StoryIntelligenceExperienceSceneState
|
yield return new StoryIntelligenceExperienceSceneState
|
||||||
{
|
{
|
||||||
Id = $"scene-result-{item.Result.SceneResultID}",
|
Id = $"scene-result-{item.Result.SceneResultID}",
|
||||||
@ -373,13 +374,13 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
SceneTotal = Math.Max(1, overallTotalScenes ?? totalScenes),
|
SceneTotal = Math.Max(1, overallTotalScenes ?? totalScenes),
|
||||||
ElapsedTime = overallElapsed ?? EstimateElapsed(run),
|
ElapsedTime = overallElapsed ?? EstimateElapsed(run),
|
||||||
EstimatedRemaining = overallRemaining ?? EstimateRemaining(run, completedScenes, totalScenes),
|
EstimatedRemaining = overallRemaining ?? EstimateRemaining(run, completedScenes, totalScenes),
|
||||||
PovCharacterId = CharacterId(storyMemory.ResolvePointOfView(scene)),
|
PovCharacterId = CharacterId(resolvedPovName),
|
||||||
Location = BuildLocation(scene),
|
Location = BuildLocation(scene),
|
||||||
Characters = BuildCharacters(scene, storyMemory),
|
Characters = BuildCharacters(scene, storyMemory),
|
||||||
Assets = BuildAssets(scene),
|
Assets = BuildAssets(scene),
|
||||||
Relationships = BuildRelationships(scene, storyMemory),
|
Relationships = BuildRelationships(scene, storyMemory),
|
||||||
KnowledgeThreads = BuildKnowledge(scene),
|
KnowledgeThreads = BuildKnowledge(scene),
|
||||||
Observations = BuildObservations(scene),
|
Observations = BuildObservations(scene, resolvedPovName),
|
||||||
LatestDiscoveries = isCurrent ? BuildDiscoveries(scene).ToList() : [],
|
LatestDiscoveries = isCurrent ? BuildDiscoveries(scene).ToList() : [],
|
||||||
ActivityFeed = isCurrent ? BuildActivity(run, sceneNumber).ToList() : []
|
ActivityFeed = isCurrent ? BuildActivity(run, sceneNumber).ToList() : []
|
||||||
};
|
};
|
||||||
@ -478,7 +479,6 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
var povName = Clean(narratorResolution ?? scene.PointOfView?.CharacterName);
|
var povName = Clean(narratorResolution ?? scene.PointOfView?.CharacterName);
|
||||||
var states = new List<StoryIntelligenceExperienceCharacterState>();
|
var states = new List<StoryIntelligenceExperienceCharacterState>();
|
||||||
var reservedCodes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
var reservedCodes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
var sceneCharacterNames = new HashSet<string>((scene.Characters ?? []).Select(character => Clean(character.Name)), StringComparer.OrdinalIgnoreCase);
|
|
||||||
foreach (var group in (scene.Characters ?? [])
|
foreach (var group in (scene.Characters ?? [])
|
||||||
.Where(character => !string.IsNullOrWhiteSpace(character.Name))
|
.Where(character => !string.IsNullOrWhiteSpace(character.Name))
|
||||||
.GroupBy(character => Clean(character.Name), StringComparer.OrdinalIgnoreCase)
|
.GroupBy(character => Clean(character.Name), StringComparer.OrdinalIgnoreCase)
|
||||||
@ -520,30 +520,6 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (storyMemory is not null)
|
|
||||||
{
|
|
||||||
foreach (var memoryCharacter in storyMemory.Characters
|
|
||||||
.Where(character => !sceneCharacterNames.Contains(character.Name))
|
|
||||||
.OrderByDescending(character => character.Weight)
|
|
||||||
.Take(Math.Max(0, 5 - states.Count)))
|
|
||||||
{
|
|
||||||
var libraryCode = CharacterLibraryCode(memoryCharacter.Name, null, scene, reservedCodes);
|
|
||||||
reservedCodes.Add(libraryCode);
|
|
||||||
states.Add(new StoryIntelligenceExperienceCharacterState
|
|
||||||
{
|
|
||||||
Id = CharacterId(memoryCharacter.Name),
|
|
||||||
LibraryCode = libraryCode,
|
|
||||||
Name = memoryCharacter.Name,
|
|
||||||
Role = string.Equals(CharacterId(memoryCharacter.Name), CharacterId(povName), StringComparison.OrdinalIgnoreCase)
|
|
||||||
? "POV character"
|
|
||||||
: NonPovRole(memoryCharacter.Role),
|
|
||||||
Relevance = memoryCharacter.Relevance,
|
|
||||||
Weight = Math.Max(38, memoryCharacter.Weight - 18),
|
|
||||||
ImagePath = CharacterFallback(memoryCharacter.Name)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return states
|
return states
|
||||||
.OrderByDescending(character => character.Weight)
|
.OrderByDescending(character => character.Weight)
|
||||||
.GroupBy(character => character.Id, StringComparer.OrdinalIgnoreCase)
|
.GroupBy(character => character.Id, StringComparer.OrdinalIgnoreCase)
|
||||||
@ -778,17 +754,36 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
return questions.Concat(knowledge).Take(4).ToList();
|
return questions.Concat(knowledge).Take(4).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IReadOnlyList<StoryIntelligenceExperienceTextItem> BuildObservations(SceneIntelligenceScene scene)
|
private static IReadOnlyList<StoryIntelligenceExperienceTextItem> BuildObservations(SceneIntelligenceScene scene, string? povName)
|
||||||
=> (scene.Observations ?? []).Select(observation => new StoryIntelligenceExperienceTextItem
|
=> (scene.Observations ?? []).Select(observation => new StoryIntelligenceExperienceTextItem
|
||||||
{
|
{
|
||||||
Id = $"observation-result-{StableKey(observation.ObservationType)}-{StableKey(observation.SubjectName)}-{StableKey(observation.Predicate)}",
|
Id = $"observation-result-{StableKey(observation.ObservationType)}-{StableKey(observation.SubjectName)}-{StableKey(observation.Predicate)}",
|
||||||
Title = Trim(FirstConfigured(observation.Description, observation.Predicate, observation.ObservationType, "Story observation"), 74),
|
Title = Trim(NamedPovText(FirstConfigured(observation.Description, observation.Predicate, observation.ObservationType, "Story observation"), povName), 74),
|
||||||
Detail = Trim(FirstConfigured(observation.Evidence, $"{observation.SubjectName} {observation.Predicate} {observation.ObjectName}".Trim(), "Observed in current scene"), 128),
|
Detail = Trim(NamedPovText(FirstConfigured(observation.Evidence, $"{observation.SubjectName} {observation.Predicate} {observation.ObjectName}".Trim(), "Observed in current scene"), povName), 128),
|
||||||
Tone = "insight"
|
Tone = "insight"
|
||||||
})
|
})
|
||||||
.Take(4)
|
.Take(4)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
private static string NamedPovText(string text, string? povName)
|
||||||
|
{
|
||||||
|
var name = Clean(povName);
|
||||||
|
if (string.IsNullOrWhiteSpace(text) || string.IsNullOrWhiteSpace(name) || IsGenericNarratorName(name))
|
||||||
|
{
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
return text
|
||||||
|
.Replace("The narrator's", $"{name}'s", StringComparison.OrdinalIgnoreCase)
|
||||||
|
.Replace("the narrator's", $"{name}'s", StringComparison.OrdinalIgnoreCase)
|
||||||
|
.Replace("The narrator", name, StringComparison.OrdinalIgnoreCase)
|
||||||
|
.Replace("the narrator", name, StringComparison.OrdinalIgnoreCase)
|
||||||
|
.Replace("Narrator's", $"{name}'s", StringComparison.OrdinalIgnoreCase)
|
||||||
|
.Replace("narrator's", $"{name}'s", StringComparison.OrdinalIgnoreCase)
|
||||||
|
.Replace("Narrator", name, StringComparison.OrdinalIgnoreCase)
|
||||||
|
.Replace("narrator", name, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
private static IEnumerable<string> BuildDiscoveries(SceneIntelligenceScene scene)
|
private static IEnumerable<string> BuildDiscoveries(SceneIntelligenceScene scene)
|
||||||
{
|
{
|
||||||
foreach (var character in (scene.Characters ?? []).Where(item => !string.IsNullOrWhiteSpace(item.Name)).Take(2))
|
foreach (var character in (scene.Characters ?? []).Where(item => !string.IsNullOrWhiteSpace(item.Name)).Take(2))
|
||||||
@ -1125,19 +1120,15 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
if (IsGenericNarratorName(raw))
|
if (IsGenericNarratorName(raw))
|
||||||
{
|
{
|
||||||
var resolved = chapterPov.GetValueOrDefault(CurrentChapterKey)?.Name
|
var resolved = chapterPov.GetValueOrDefault(CurrentChapterKey)?.Name
|
||||||
?? (string.Equals(previousChapterKey, CurrentChapterKey, StringComparison.OrdinalIgnoreCase) ? previousPovName : null)
|
?? (string.Equals(previousChapterKey, CurrentChapterKey, StringComparison.OrdinalIgnoreCase) ? previousPovName : null);
|
||||||
?? bookNarratorName
|
|
||||||
?? narratorResolvedName;
|
|
||||||
ResolvedPovIdentity = resolved ?? raw;
|
ResolvedPovIdentity = resolved ?? raw;
|
||||||
PovResolutionReason = resolved is null ? "unresolved narrator" : "inherited resolved chapter POV";
|
PovResolutionReason = resolved is null ? "unresolved narrator" : "inherited same-chapter POV";
|
||||||
PovConfidence = chapterPov.GetValueOrDefault(CurrentChapterKey)?.Confidence ?? (resolved is null ? 0.35m : 0.74m);
|
PovConfidence = chapterPov.GetValueOrDefault(CurrentChapterKey)?.Confidence ?? (resolved is null ? 0.35m : 0.74m);
|
||||||
return resolved ?? raw;
|
return resolved ?? raw;
|
||||||
}
|
}
|
||||||
|
|
||||||
var fallback = chapterPov.GetValueOrDefault(CurrentChapterKey)?.Name
|
var fallback = chapterPov.GetValueOrDefault(CurrentChapterKey)?.Name
|
||||||
?? (string.Equals(previousChapterKey, CurrentChapterKey, StringComparison.OrdinalIgnoreCase) ? previousPovName : null)
|
?? (string.Equals(previousChapterKey, CurrentChapterKey, StringComparison.OrdinalIgnoreCase) ? previousPovName : null);
|
||||||
?? bookNarratorName
|
|
||||||
?? narratorResolvedName;
|
|
||||||
ResolvedPovIdentity = string.IsNullOrWhiteSpace(raw) ? fallback ?? string.Empty : raw;
|
ResolvedPovIdentity = string.IsNullOrWhiteSpace(raw) ? fallback ?? string.Empty : raw;
|
||||||
PovResolutionReason = string.IsNullOrWhiteSpace(raw) ? "inherited previous POV" : "explicit named POV";
|
PovResolutionReason = string.IsNullOrWhiteSpace(raw) ? "inherited previous POV" : "explicit named POV";
|
||||||
PovConfidence = string.IsNullOrWhiteSpace(raw) ? 0.72m : 0.92m;
|
PovConfidence = string.IsNullOrWhiteSpace(raw) ? 0.72m : 0.92m;
|
||||||
@ -1166,8 +1157,6 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
|
|
||||||
return chapterPov.GetValueOrDefault(CurrentChapterKey)?.Name
|
return chapterPov.GetValueOrDefault(CurrentChapterKey)?.Name
|
||||||
?? (string.Equals(previousChapterKey, CurrentChapterKey, StringComparison.OrdinalIgnoreCase) ? previousPovName : null)
|
?? (string.Equals(previousChapterKey, CurrentChapterKey, StringComparison.OrdinalIgnoreCase) ? previousPovName : null)
|
||||||
?? bookNarratorName
|
|
||||||
?? narratorResolvedName
|
|
||||||
?? name;
|
?? name;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1373,12 +1362,12 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
private static string? NamePresentation(string name)
|
private static string? NamePresentation(string name)
|
||||||
{
|
{
|
||||||
var key = name.ToLowerInvariant();
|
var key = name.ToLowerInvariant();
|
||||||
if (key is "beth" or "rosie" or "maggie" or "grace" or "annie" or "anna" or "anne" or "helen" or "elen")
|
if (key is "alison" or "annie" or "anna" or "anne" or "beth" or "chrissy" or "grace" or "helen" or "elen" or "judy" or "katherine" or "maggie" or "mary" or "rebecca" or "rosie" or "sharon" or "sophie")
|
||||||
{
|
{
|
||||||
return "Feminine";
|
return "Feminine";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key is "graham" or "simon" or "kevin" or "colin" or "john" or "david")
|
if (key is "colin" or "david" or "george" or "graham" or "greg" or "john" or "kevin" or "phil" or "roger" or "rick" or "rob" or "simon")
|
||||||
{
|
{
|
||||||
return "Masculine";
|
return "Masculine";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -996,8 +996,8 @@ public sealed class StoryMemoryService(
|
|||||||
Code = code,
|
Code = code,
|
||||||
Title = $"{age} {presentation} character",
|
Title = $"{age} {presentation} character",
|
||||||
Description = "Reusable character portrait generated from durable Story Memory appearance evidence. Not canonical to a specific manuscript.",
|
Description = "Reusable character portrait generated from durable Story Memory appearance evidence. Not canonical to a specific manuscript.",
|
||||||
VisualSubject = $"A {agePrompt} {presentation.ToLowerInvariant()} person from {era} {location} with unmistakable {hairLength.ToLowerInvariant()} {hairColour.ToLowerInvariant()} hair. Skin tone requirement: {skinInstruction}. Give this person a distinct face shape, nose, eyes, jaw, expression, and hairstyle silhouette so they do not resemble other reusable portraits.",
|
VisualSubject = $"A {agePrompt} {presentation.ToLowerInvariant()} person from {era} {location} with unmistakable {hairLength.ToLowerInvariant()} {hairColour.ToLowerInvariant()} hair. Skin tone requirement: {skinInstruction}. The face must read immediately as {PresentationPrompt(presentation)}. Give this person a distinct face shape, nose, eyes, jaw, expression, and hairstyle silhouette so they do not resemble other reusable portraits.",
|
||||||
Composition = "single head-and-shoulders portrait with clear facial lighting, neutral story background, strong thumbnail readability, and visibly individual facial features",
|
Composition = "single head-and-shoulders portrait with clear facial lighting, neutral story background, strong thumbnail readability, visibly individual facial features, clear gender presentation when specified",
|
||||||
Mood = "present, natural, story-aware",
|
Mood = "present, natural, story-aware",
|
||||||
ApparentAgeBand = age,
|
ApparentAgeBand = age,
|
||||||
Presentation = presentation,
|
Presentation = presentation,
|
||||||
@ -1123,6 +1123,14 @@ public sealed class StoryMemoryService(
|
|||||||
_ => "not specified"
|
_ => "not specified"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static string PresentationPrompt(string presentation)
|
||||||
|
=> presentation switch
|
||||||
|
{
|
||||||
|
StoryIntelligenceIllustrationCompatibility.Presentations.Feminine => "a recognisably feminine woman or girl, not androgynous and not masculine",
|
||||||
|
StoryIntelligenceIllustrationCompatibility.Presentations.Masculine => "a recognisably masculine man or boy, not androgynous and not feminine",
|
||||||
|
_ => "the requested presentation"
|
||||||
|
};
|
||||||
|
|
||||||
private static string SplitWords(string? value)
|
private static string SplitWords(string? value)
|
||||||
{
|
{
|
||||||
var clean = UnknownToDefault(value, "Unknown");
|
var clean = UnknownToDefault(value, "Unknown");
|
||||||
|
|||||||
@ -23,6 +23,16 @@
|
|||||||
<div class="alert alert-warning">@Model.ErrorMessage</div>
|
<div class="alert alert-warning">@Model.ErrorMessage</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@if (TempData["AdminMessage"] is string message)
|
||||||
|
{
|
||||||
|
<div class="alert alert-success">@message</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (TempData["AdminError"] is string error)
|
||||||
|
{
|
||||||
|
<div class="alert alert-danger">@error</div>
|
||||||
|
}
|
||||||
|
|
||||||
<section class="edit-panel">
|
<section class="edit-panel">
|
||||||
<h2>OpenAI configuration</h2>
|
<h2>OpenAI configuration</h2>
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
@ -59,6 +69,14 @@
|
|||||||
</dl>
|
</dl>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="edit-panel">
|
||||||
|
<h2>Development reset</h2>
|
||||||
|
<p>Delete your projects, books, Story Intelligence imports, runs and story memory. Generated image library records and files are preserved.</p>
|
||||||
|
<form asp-action="ResetStoryIntelligenceImports" method="post">
|
||||||
|
<button class="btn btn-outline-danger" type="submit">Reset my Story Intelligence data</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="edit-panel">
|
<section class="edit-panel">
|
||||||
<h2>Prompt repository</h2>
|
<h2>Prompt repository</h2>
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
|
|||||||
@ -792,6 +792,37 @@ input:focus-visible {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.story-exp-presence-callout {
|
||||||
|
position: absolute;
|
||||||
|
left: var(--presence-left, 28px);
|
||||||
|
top: var(--presence-top, 58px);
|
||||||
|
z-index: 78;
|
||||||
|
width: var(--presence-width, 520px);
|
||||||
|
height: var(--presence-height, 320px);
|
||||||
|
border: 1px solid rgba(125, 193, 255, 0.2);
|
||||||
|
border-radius: 36px;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 65% 45%, rgba(87, 149, 205, 0.18), transparent 52%),
|
||||||
|
linear-gradient(135deg, rgba(17, 41, 70, 0.22), rgba(4, 10, 20, 0.06));
|
||||||
|
box-shadow: inset 0 0 44px rgba(125, 193, 255, 0.08), 0 22px 80px rgba(0, 0, 0, 0.16);
|
||||||
|
pointer-events: none;
|
||||||
|
transition: left 850ms cubic-bezier(.16, .84, .28, 1), top 850ms cubic-bezier(.16, .84, .28, 1), width 850ms cubic-bezier(.16, .84, .28, 1), height 850ms cubic-bezier(.16, .84, .28, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-presence-callout::after {
|
||||||
|
position: absolute;
|
||||||
|
right: -34px;
|
||||||
|
top: 48%;
|
||||||
|
width: 70px;
|
||||||
|
height: 44px;
|
||||||
|
border-top: 1px solid rgba(125, 193, 255, 0.2);
|
||||||
|
border-right: 1px solid rgba(125, 193, 255, 0.2);
|
||||||
|
border-radius: 0 28px 0 0;
|
||||||
|
background: linear-gradient(90deg, rgba(87, 149, 205, 0.14), transparent);
|
||||||
|
content: "";
|
||||||
|
transform: skewX(-18deg);
|
||||||
|
}
|
||||||
|
|
||||||
.story-character {
|
.story-character {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: block;
|
display: block;
|
||||||
@ -966,8 +997,8 @@ input:focus-visible {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
display: grid;
|
display: grid;
|
||||||
justify-items: center;
|
justify-items: center;
|
||||||
gap: 10px;
|
gap: 12px;
|
||||||
width: var(--node-size, 190px);
|
width: min(292px, calc(var(--node-size, 190px) + 52px));
|
||||||
transform: translate(var(--x, 0), var(--y, 0));
|
transform: translate(var(--x, 0), var(--y, 0));
|
||||||
z-index: 42;
|
z-index: 42;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
@ -998,7 +1029,8 @@ input:focus-visible {
|
|||||||
display: grid;
|
display: grid;
|
||||||
justify-items: center;
|
justify-items: center;
|
||||||
gap: 3px;
|
gap: 3px;
|
||||||
max-width: min(244px, calc(100vw - 32px));
|
width: min(292px, calc(var(--node-size, 190px) + 54px));
|
||||||
|
max-width: min(292px, calc(100vw - 32px));
|
||||||
border: 1px solid rgba(125, 193, 255, 0.12);
|
border: 1px solid rgba(125, 193, 255, 0.12);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 7px 10px;
|
padding: 7px 10px;
|
||||||
@ -1008,23 +1040,24 @@ input:focus-visible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.story-location h2 {
|
.story-location h2 {
|
||||||
overflow: hidden;
|
overflow: visible;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
font-size: clamp(1.12rem, 1.55vw, 1.55rem);
|
font-size: clamp(1.02rem, 1.18vw, 1.24rem);
|
||||||
|
line-height: 1.06;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
text-overflow: ellipsis;
|
overflow-wrap: anywhere;
|
||||||
white-space: nowrap;
|
white-space: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-location p {
|
.story-location p {
|
||||||
overflow: hidden;
|
overflow: visible;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
color: var(--story-muted);
|
color: var(--story-muted);
|
||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
line-height: 1.15;
|
line-height: 1.15;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
text-overflow: ellipsis;
|
overflow-wrap: anywhere;
|
||||||
white-space: nowrap;
|
white-space: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-exp-ribbon-wrap {
|
.story-exp-ribbon-wrap {
|
||||||
|
|||||||
@ -682,38 +682,56 @@
|
|||||||
const activeIds = new Set();
|
const activeIds = new Set();
|
||||||
const occupiedRects = layoutRects(stageRect);
|
const occupiedRects = layoutRects(stageRect);
|
||||||
|
|
||||||
scene.relationships
|
|
||||||
.filter((relationship) => relationship.weight >= 72)
|
|
||||||
.slice(0, 2)
|
|
||||||
.forEach((relationship, index) => {
|
|
||||||
const source = dom.visualStage.querySelector(`[data-character-id="${cssEscape(relationship.sourceId)}"]`);
|
|
||||||
const target = dom.visualStage.querySelector(`[data-character-id="${cssEscape(relationship.targetId)}"]`);
|
|
||||||
if (!source || !target) return;
|
|
||||||
|
|
||||||
const start = connectionPoint(source, stageRect, target);
|
|
||||||
const end = connectionPoint(target, stageRect, source);
|
|
||||||
const obstacles = connectionObstacles(stageRect, source, target);
|
|
||||||
const id = relationship.id || `relationship-${relationship.sourceId}-${relationship.targetId}`;
|
|
||||||
activeIds.add(id);
|
|
||||||
drawn.push(drawConnection(id, start, end, relationshipTone(relationship), relationship.weight, "", index, obstacles, occupiedRects));
|
|
||||||
});
|
|
||||||
|
|
||||||
const location = dom.visualStage.querySelector("[data-location-id]");
|
const location = dom.visualStage.querySelector("[data-location-id]");
|
||||||
const presentCharacters = scene.characters
|
const presentCharacters = scene.characters
|
||||||
.filter((character) => !character.status || !character.status.toLowerCase().includes("absent"))
|
.filter((character) => !character.status || !character.status.toLowerCase().includes("absent"))
|
||||||
.map((character) => dom.visualStage.querySelector(`[data-character-id="${cssEscape(character.id)}"]`))
|
.map((character) => dom.visualStage.querySelector(`[data-character-id="${cssEscape(character.id)}"]`))
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
const cluster = characterClusterPoint(presentCharacters, stageRect);
|
const cluster = characterClusterPoint(presentCharacters, stageRect);
|
||||||
|
renderPresenceCallout(presentCharacters, location, stageRect);
|
||||||
if (cluster && location) {
|
if (cluster && location) {
|
||||||
const id = `presence-cluster-${location.dataset.locationId}`;
|
const id = `presence-cluster-${location.dataset.locationId}`;
|
||||||
activeIds.add(id);
|
activeIds.add(id);
|
||||||
const locationPoint = connectionPoint(location, stageRect, { getBoundingClientRect: () => ({ left: stageRect.left + cluster.x, top: stageRect.top + cluster.y, width: 0, height: 0 }) });
|
const locationPoint = connectionPoint(location, stageRect, { getBoundingClientRect: () => ({ left: stageRect.left + cluster.x, top: stageRect.top + cluster.y, width: 0, height: 0 }) });
|
||||||
drawn.push(drawConnection(id, cluster, locationPoint, "presence-cluster", 54, `${presentCharacters.length} here`, drawn.length, [], occupiedRects));
|
drawn.push(drawConnection(id, cluster, locationPoint, "presence-cluster", 54, "", drawn.length, [], occupiedRects));
|
||||||
}
|
}
|
||||||
|
|
||||||
removeInactiveSvgConnections(activeIds);
|
removeInactiveSvgConnections(activeIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderPresenceCallout(characterNodes, locationNode, stageRect) {
|
||||||
|
let callout = dom.visualStage.querySelector("[data-presence-callout]");
|
||||||
|
if (!characterNodes.length || !locationNode) {
|
||||||
|
callout?.remove();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!callout) {
|
||||||
|
callout = document.createElement("div");
|
||||||
|
callout.className = "story-exp-presence-callout";
|
||||||
|
callout.dataset.presenceCallout = "true";
|
||||||
|
dom.visualStage.append(callout);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rects = characterNodes
|
||||||
|
.map((node) => node.getBoundingClientRect())
|
||||||
|
.filter((rect) => rect.width > 0 && rect.height > 0);
|
||||||
|
if (!rects.length) {
|
||||||
|
callout.remove();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const left = Math.min(...rects.map((rect) => rect.left)) - stageRect.left;
|
||||||
|
const top = Math.min(...rects.map((rect) => rect.top)) - stageRect.top;
|
||||||
|
const right = Math.max(...rects.map((rect) => rect.right)) - stageRect.left;
|
||||||
|
const bottom = Math.max(...rects.map((rect) => rect.bottom)) - stageRect.top;
|
||||||
|
const padding = Math.max(28, Math.min(54, stageRect.width * 0.035));
|
||||||
|
callout.style.setProperty("--presence-left", `${Math.max(12, left - padding)}px`);
|
||||||
|
callout.style.setProperty("--presence-top", `${Math.max(46, top - padding)}px`);
|
||||||
|
callout.style.setProperty("--presence-width", `${Math.min(stageRect.width * 0.72, right - left + padding * 2)}px`);
|
||||||
|
callout.style.setProperty("--presence-height", `${Math.min(stageRect.height * 0.82, bottom - top + padding * 2 + 62)}px`);
|
||||||
|
}
|
||||||
|
|
||||||
function drawConnection(id, start, end, tone, weight, label, index, obstacles, occupiedRects) {
|
function drawConnection(id, start, end, tone, weight, label, index, obstacles, occupiedRects) {
|
||||||
let group = dom.connections.querySelector(`[data-connection-id="${cssEscape(id)}"]`);
|
let group = dom.connections.querySelector(`[data-connection-id="${cssEscape(id)}"]`);
|
||||||
const isNew = !group;
|
const isNew = !group;
|
||||||
@ -807,7 +825,7 @@
|
|||||||
const povSize = Math.round(Math.min(188, Math.max(150, width * 0.142)));
|
const povSize = Math.round(Math.min(188, Math.max(150, width * 0.142)));
|
||||||
if (pov) {
|
if (pov) {
|
||||||
positions.set(pov.id, {
|
positions.set(pov.id, {
|
||||||
x: clamp(Math.round(width * 0.28 - povSize / 2), 28, width * 0.58 - povSize),
|
x: clamp(Math.round(width * 0.28 - povSize / 2), 28, width * 0.66 - povSize),
|
||||||
y: clamp(Math.round(height * 0.28 - povSize / 2), 54, height * 0.54 - povSize),
|
y: clamp(Math.round(height * 0.28 - povSize / 2), 54, height * 0.54 - povSize),
|
||||||
size: povSize,
|
size: povSize,
|
||||||
scale: 1,
|
scale: 1,
|
||||||
@ -819,14 +837,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const slots = [
|
const slots = [
|
||||||
{ x: 0.55, y: 0.46, role: "partner" },
|
{ x: 0.57, y: 0.46, role: "partner" },
|
||||||
{ x: 0.14, y: 0.58, role: "support" },
|
{ x: 0.14, y: 0.58, role: "support" },
|
||||||
{ x: 0.12, y: 0.18, role: "support" },
|
{ x: 0.12, y: 0.18, role: "support" },
|
||||||
{ x: 0.42, y: 0.72, role: "context" },
|
{ x: 0.47, y: 0.72, role: "context" },
|
||||||
{ x: 0.34, y: 0.46, role: "context" },
|
{ x: 0.38, y: 0.47, role: "context" },
|
||||||
{ x: 0.60, y: 0.66, role: "context" },
|
{ x: 0.64, y: 0.66, role: "context" },
|
||||||
{ x: 0.06, y: 0.38, role: "context" },
|
{ x: 0.06, y: 0.38, role: "context" },
|
||||||
{ x: 0.24, y: 0.76, role: "context" }
|
{ x: 0.28, y: 0.76, role: "context" },
|
||||||
|
{ x: 0.68, y: 0.26, role: "context" },
|
||||||
|
{ x: 0.50, y: 0.20, role: "context" }
|
||||||
];
|
];
|
||||||
const placed = [...positions.values()];
|
const placed = [...positions.values()];
|
||||||
others.forEach((character, index) => {
|
others.forEach((character, index) => {
|
||||||
@ -904,9 +924,9 @@
|
|||||||
const bounds = dom.visualStage?.getBoundingClientRect();
|
const bounds = dom.visualStage?.getBoundingClientRect();
|
||||||
const width = Math.max(820, bounds?.width || 1040);
|
const width = Math.max(820, bounds?.width || 1040);
|
||||||
const height = Math.max(500, bounds?.height || 560);
|
const height = Math.max(500, bounds?.height || 560);
|
||||||
const size = Math.min(188, Math.max(154, width * 0.14));
|
const size = Math.min(246, Math.max(200, width * 0.185));
|
||||||
return {
|
return {
|
||||||
x: Math.round(width * 0.84 - size / 2),
|
x: Math.round(width * 0.82 - size / 2),
|
||||||
y: Math.round(height * 0.46 - size / 2),
|
y: Math.round(height * 0.46 - size / 2),
|
||||||
size
|
size
|
||||||
};
|
};
|
||||||
@ -1136,7 +1156,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function conciseLocationLabel(location) {
|
function conciseLocationLabel(location) {
|
||||||
return compactPhrase(location.label, 30);
|
return location.label || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function conciseAssetLabel(asset) {
|
function conciseAssetLabel(asset) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user