Improve post-import review defaults
This commit is contained in:
parent
1dfc620e00
commit
a90b98f8f0
@ -74,12 +74,17 @@ var tests = new (string Name, Action Test)[]
|
|||||||
("Name Intelligence reference data is copied on publish", NameIntelligenceReferenceDataIsCopiedOnPublish),
|
("Name Intelligence reference data is copied on publish", NameIntelligenceReferenceDataIsCopiedOnPublish),
|
||||||
("Name Intelligence repository uses stored procedures", NameIntelligenceRepositoryUsesStoredProcedures),
|
("Name Intelligence repository uses stored procedures", NameIntelligenceRepositoryUsesStoredProcedures),
|
||||||
("Name Intelligence review warning is wired", NameIntelligenceReviewWarningIsWired),
|
("Name Intelligence review warning is wired", NameIntelligenceReviewWarningIsWired),
|
||||||
|
("Name Intelligence library defaults to all locales", NameIntelligenceLibraryDefaultsToAllLocales),
|
||||||
("Story Intelligence character defaults link proposed existing matches", StoryIntelligenceCharacterDefaultsLinkProposedExistingMatches),
|
("Story Intelligence character defaults link proposed existing matches", StoryIntelligenceCharacterDefaultsLinkProposedExistingMatches),
|
||||||
("Story Intelligence character defaults ignore generic references", StoryIntelligenceCharacterDefaultsIgnoreGenericReferences),
|
("Story Intelligence character defaults ignore generic references", StoryIntelligenceCharacterDefaultsIgnoreGenericReferences),
|
||||||
("Story Intelligence character defaults keep descriptive identities", StoryIntelligenceCharacterDefaultsKeepDescriptiveIdentities),
|
("Story Intelligence character defaults keep descriptive identities", StoryIntelligenceCharacterDefaultsKeepDescriptiveIdentities),
|
||||||
("Story Intelligence character review posts selected existing match", StoryIntelligenceCharacterReviewPostsSelectedExistingMatch),
|
("Story Intelligence character review posts selected existing match", StoryIntelligenceCharacterReviewPostsSelectedExistingMatch),
|
||||||
("Story Intelligence character default changes preserve persisted results", StoryIntelligenceCharacterDefaultChangesPreservePersistedResults),
|
("Story Intelligence character default changes preserve persisted results", StoryIntelligenceCharacterDefaultChangesPreservePersistedResults),
|
||||||
("Story Intelligence character review reuses structured appearance aliases", StoryIntelligenceCharacterReviewReusesStructuredAppearanceAliases),
|
("Story Intelligence character review reuses structured appearance aliases", StoryIntelligenceCharacterReviewReusesStructuredAppearanceAliases),
|
||||||
|
("Story Intelligence review evidence includes chapter scene labels", StoryIntelligenceReviewEvidenceIncludesChapterSceneLabels),
|
||||||
|
("Story Intelligence import enables project metric defaults before save", StoryIntelligenceImportEnablesProjectMetricDefaultsBeforeSave),
|
||||||
|
("Project metrics add defaults backfills scene values", ProjectMetricsAddDefaultsBackfillsSceneValues),
|
||||||
|
("Word Companion replays live current scene to writing panes", WordCompanionReplaysLiveCurrentSceneToWritingPanes),
|
||||||
("Scan review post supports full-book form submissions", ScanReviewPostSupportsFullBookFormSubmissions),
|
("Scan review post supports full-book form submissions", ScanReviewPostSupportsFullBookFormSubmissions),
|
||||||
("StoryPosition rejects incomplete or invalid runtime scene positions", StoryPositionRejectsInvalidRuntimeScenePositions),
|
("StoryPosition rejects incomplete or invalid runtime scene positions", StoryPositionRejectsInvalidRuntimeScenePositions),
|
||||||
("StoryPosition builds canonical runtime scene navigation", StoryPositionBuildsCanonicalRuntimeSceneNavigation),
|
("StoryPosition builds canonical runtime scene navigation", StoryPositionBuildsCanonicalRuntimeSceneNavigation),
|
||||||
@ -1932,6 +1937,20 @@ static void NameIntelligenceReviewWarningIsWired()
|
|||||||
Assert(view.Contains("Name warning", StringComparison.Ordinal), "Character review view should render the name warning.");
|
Assert(view.Contains("Name warning", StringComparison.Ordinal), "Character review view should render the name warning.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void NameIntelligenceLibraryDefaultsToAllLocales()
|
||||||
|
{
|
||||||
|
var root = Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine");
|
||||||
|
var model = File.ReadAllText(Path.Combine(root, "Models/NameIntelligenceModels.cs"));
|
||||||
|
var view = File.ReadAllText(Path.Combine(root, "Views/Names/Index.cshtml"));
|
||||||
|
var migration = File.ReadAllText(Path.Combine(root, "Sql/152_NameLibraryAllLocalesDefault.sql"));
|
||||||
|
|
||||||
|
Assert(model.Contains("public string? LocaleCode { get; set; }", StringComparison.Ordinal), "Name library filter should not default LocaleCode to a specific country.");
|
||||||
|
Assert(!model.Contains("LocaleCode { get; set; } = \"en-US\"", StringComparison.Ordinal), "Name library must not default to the United States locale.");
|
||||||
|
Assert(view.Contains(">All locales</option>", StringComparison.Ordinal), "Name library locale dropdown should expose an All locales option.");
|
||||||
|
Assert(migration.Contains("DECLARE @NameLocaleID int = NULL", StringComparison.Ordinal), "Name search procedure should keep null locale as all locales.");
|
||||||
|
Assert(!migration.Contains("COALESCE(@LocaleCode, N'en-US')", StringComparison.Ordinal), "Name search procedure must not convert null locale back to en-US.");
|
||||||
|
}
|
||||||
|
|
||||||
static void StoryIntelligenceCharacterDefaultsLinkProposedExistingMatches()
|
static void StoryIntelligenceCharacterDefaultsLinkProposedExistingMatches()
|
||||||
{
|
{
|
||||||
Assert(StoryIntelligenceCharacterReviewDefaults.DetermineAction("Beth", 42) == StoryIntelligenceCharacterImportActions.LinkExisting,
|
Assert(StoryIntelligenceCharacterReviewDefaults.DetermineAction("Beth", 42) == StoryIntelligenceCharacterImportActions.LinkExisting,
|
||||||
@ -1993,6 +2012,68 @@ static void StoryIntelligenceCharacterReviewReusesStructuredAppearanceAliases()
|
|||||||
Assert(service.Contains("Aliases = appearance.Aliases", StringComparison.Ordinal), "Structured appearance aliases should be reused without another AI call.");
|
Assert(service.Contains("Aliases = appearance.Aliases", StringComparison.Ordinal), "Structured appearance aliases should be reused without another AI call.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void StoryIntelligenceReviewEvidenceIncludesChapterSceneLabels()
|
||||||
|
{
|
||||||
|
var root = Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine");
|
||||||
|
var characterService = File.ReadAllText(Path.Combine(root, "Services/StoryIntelligenceCharacterImportService.cs"));
|
||||||
|
var locationService = File.ReadAllText(Path.Combine(root, "Services/StoryIntelligenceLocationImportService.cs"));
|
||||||
|
var assetService = File.ReadAllText(Path.Combine(root, "Services/StoryIntelligenceAssetImportService.cs"));
|
||||||
|
var relationshipService = File.ReadAllText(Path.Combine(root, "Services/StoryIntelligenceRelationshipImportService.cs"));
|
||||||
|
var knowledgeService = File.ReadAllText(Path.Combine(root, "Services/StoryIntelligenceKnowledgeImportService.cs"));
|
||||||
|
var locationView = File.ReadAllText(Path.Combine(root, "Views/Locations/Details.cshtml"));
|
||||||
|
|
||||||
|
Assert(characterService.Contains("$\"Chapter {scene.ChapterNumber:g}, Scene {scene.SceneNumber:g}\"", StringComparison.Ordinal), "Character review first appearances should include chapter and scene.");
|
||||||
|
Assert(locationService.Contains("$\"Chapter {scene.ChapterNumber:g}, Scene {scene.SceneNumber:g}\"", StringComparison.Ordinal), "Location review first appearances should include chapter and scene.");
|
||||||
|
Assert(assetService.Contains("$\"Chapter {scene.ChapterNumber:g}, Scene {scene.SceneNumber:g}\"", StringComparison.Ordinal), "Asset review first appearances should include chapter and scene.");
|
||||||
|
Assert(relationshipService.Contains("$\"Chapter {scene.ChapterNumber:g}, Scene {scene.SceneNumber:g}\"", StringComparison.Ordinal), "Relationship review first appearances should include chapter and scene.");
|
||||||
|
Assert(knowledgeService.Contains("$\"Chapter {scene.ChapterNumber:g}, Scene {scene.SceneNumber:g}\"", StringComparison.Ordinal), "Knowledge review first appearances should include chapter and scene.");
|
||||||
|
Assert(locationView.Contains("SceneLabel(scene.ChapterNumber, scene.SceneNumber)", StringComparison.Ordinal), "Location detail scene links should include chapter context.");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void StoryIntelligenceImportEnablesProjectMetricDefaultsBeforeSave()
|
||||||
|
{
|
||||||
|
var root = Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine");
|
||||||
|
var service = File.ReadAllText(Path.Combine(root, "Services/StoryIntelligenceImportCommitService.cs"));
|
||||||
|
|
||||||
|
Assert(service.Contains("await metricTypes.AddMissingDefaultsAsync(run.ProjectID.Value)", StringComparison.Ordinal), "Story Intelligence import should enable default metric types for the project before committing scenes.");
|
||||||
|
Assert(service.Contains("metric.IsActive && metric.IsEnabledForProject", StringComparison.Ordinal), "Story Intelligence import should only save metrics enabled for the project.");
|
||||||
|
Assert(service.IndexOf("await metricTypes.AddMissingDefaultsAsync(run.ProjectID.Value)", StringComparison.Ordinal)
|
||||||
|
< service.IndexOf("ListForManagementAsync(run.ProjectID.Value)", StringComparison.Ordinal), "Metric defaults must be enabled before metric types are loaded.");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ProjectMetricsAddDefaultsBackfillsSceneValues()
|
||||||
|
{
|
||||||
|
var root = Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine");
|
||||||
|
var core = File.ReadAllText(Path.Combine(root, "Services/CoreServices.cs"));
|
||||||
|
var repository = File.ReadAllText(Path.Combine(root, "Data/Repositories.cs"));
|
||||||
|
var controller = File.ReadAllText(Path.Combine(root, "Controllers/ProjectMetricsController.cs"));
|
||||||
|
var migration = File.ReadAllText(Path.Combine(root, "Sql/151_BackfillDefaultSceneMetricValues.sql"));
|
||||||
|
var selfHealingDefaults = File.ReadAllText(Path.Combine(root, "Sql/153_SelfHealingProjectMetricDefaults.sql"));
|
||||||
|
|
||||||
|
Assert(core.Contains("Task<int> AddMissingDefaultsAsync(int projectId)", StringComparison.Ordinal), "Metric defaults service should report inserted scene values.");
|
||||||
|
Assert(core.Contains("BackfillDefaultValuesForProjectAsync(projectId)", StringComparison.Ordinal), "Metric defaults service should backfill scene metric values.");
|
||||||
|
Assert(repository.Contains("SceneMetric_BackfillDefaultsForProject", StringComparison.Ordinal), "Metric repository should call the scene metric backfill procedure.");
|
||||||
|
Assert(controller.Contains("backfilled", StringComparison.Ordinal), "Metric controller should tell users when scene values were backfilled.");
|
||||||
|
Assert(migration.Contains("NOT EXISTS", StringComparison.Ordinal), "Metric backfill must not overwrite existing scene metric values.");
|
||||||
|
Assert(selfHealingDefaults.Contains("MERGE dbo.SceneMetricTypes", StringComparison.Ordinal), "Add missing defaults should seed missing global metric types.");
|
||||||
|
Assert(selfHealingDefaults.Contains("Overall Intensity", StringComparison.Ordinal), "Core metric seed should include the default PlotDirector metrics.");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void WordCompanionReplaysLiveCurrentSceneToWritingPanes()
|
||||||
|
{
|
||||||
|
var root = Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine");
|
||||||
|
var controller = File.ReadAllText(Path.Combine(root, "Controllers/WordCompanionController.cs"));
|
||||||
|
var presence = File.ReadAllText(Path.Combine(root, "Services/WordCompanionPresenceService.cs"));
|
||||||
|
var workspace = File.ReadAllText(Path.Combine(root, "Services/WritingWorkspacePresenceService.cs"));
|
||||||
|
var script = File.ReadAllText(Path.Combine(root, "wwwroot/js/writing-follow.js"));
|
||||||
|
|
||||||
|
Assert(controller.Contains("presence.UpdateCurrentStoryPositionAsync", StringComparison.Ordinal), "Word current-scene updates should update live presence.");
|
||||||
|
Assert(presence.Contains("CurrentStoryPosition = connected ? record.CurrentStoryPosition : null", StringComparison.Ordinal), "Presence should only replay current scene while Word is live.");
|
||||||
|
Assert(workspace.Contains("CurrentStoryPosition = wordStatus.CurrentStoryPosition", StringComparison.Ordinal), "Workspace status should carry Word's current scene.");
|
||||||
|
Assert(script.Contains("storyPositionFromWorkspaceStatus", StringComparison.Ordinal), "Writing panes should read the current scene from workspace status.");
|
||||||
|
Assert(script.Contains("WatchWritingWorkspace", StringComparison.Ordinal), "Writing panes should request scoped workspace status on startup.");
|
||||||
|
}
|
||||||
|
|
||||||
static int CountCsvRows(string path)
|
static int CountCsvRows(string path)
|
||||||
=> File.ReadLines(path).Skip(1).Count(line => !string.IsNullOrWhiteSpace(line));
|
=> File.ReadLines(path).Skip(1).Count(line => !string.IsNullOrWhiteSpace(line));
|
||||||
|
|
||||||
|
|||||||
@ -50,8 +50,10 @@ public sealed class ProjectMetricsController(ISceneMetricTypeService metrics) :
|
|||||||
[ValidateAntiForgeryToken]
|
[ValidateAntiForgeryToken]
|
||||||
public async Task<IActionResult> AddMissingDefaults(int projectId)
|
public async Task<IActionResult> AddMissingDefaults(int projectId)
|
||||||
{
|
{
|
||||||
await metrics.AddMissingDefaultsAsync(projectId);
|
var backfilled = await metrics.AddMissingDefaultsAsync(projectId);
|
||||||
TempData["ProjectMetricMessage"] = "Missing default scene metrics added.";
|
TempData["ProjectMetricMessage"] = backfilled > 0
|
||||||
|
? $"Missing default scene metrics added. {backfilled:N0} scene metric value(s) backfilled."
|
||||||
|
: "Missing default scene metrics added. Existing scenes already had values for the enabled defaults.";
|
||||||
return RedirectToAction(nameof(Index), new { projectId });
|
return RedirectToAction(nameof(Index), new { projectId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,7 @@ namespace PlotLine.Controllers;
|
|||||||
[Route("api/word-companion")]
|
[Route("api/word-companion")]
|
||||||
public sealed class WordCompanionController(
|
public sealed class WordCompanionController(
|
||||||
IWordCompanionService wordCompanion,
|
IWordCompanionService wordCompanion,
|
||||||
|
IWordCompanionPresenceService presence,
|
||||||
IHubContext<WordCompanionFollowHub> followHub) : ControllerBase
|
IHubContext<WordCompanionFollowHub> followHub) : ControllerBase
|
||||||
{
|
{
|
||||||
[HttpGet("projects")]
|
[HttpGet("projects")]
|
||||||
@ -130,6 +131,8 @@ public sealed class WordCompanionController(
|
|||||||
return BadRequest();
|
return BadRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await presence.UpdateCurrentStoryPositionAsync(RequireUserId(), response.Position);
|
||||||
|
|
||||||
await BroadcastToCurrentUserAsync("wordCompanionSceneChanged", new
|
await BroadcastToCurrentUserAsync("wordCompanionSceneChanged", new
|
||||||
{
|
{
|
||||||
response.ProjectId,
|
response.ProjectId,
|
||||||
@ -267,4 +270,12 @@ public sealed class WordCompanionController(
|
|||||||
await followHub.Clients.User(userId).SendAsync(eventName, payload);
|
await followHub.Clients.User(userId).SendAsync(eventName, payload);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int RequireUserId()
|
||||||
|
{
|
||||||
|
var raw = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
|
return int.TryParse(raw, out var userId) && userId > 0
|
||||||
|
? userId
|
||||||
|
: throw new InvalidOperationException("The current user could not be identified.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -141,6 +141,7 @@ public interface ISceneMetricTypeRepository
|
|||||||
Task<bool> SetActiveAsync(int metricTypeId, int projectId, bool isActive);
|
Task<bool> SetActiveAsync(int metricTypeId, int projectId, bool isActive);
|
||||||
Task<bool> MoveAsync(int metricTypeId, int projectId, string direction);
|
Task<bool> MoveAsync(int metricTypeId, int projectId, string direction);
|
||||||
Task AddMissingDefaultsAsync(int projectId);
|
Task AddMissingDefaultsAsync(int projectId);
|
||||||
|
Task<int> BackfillDefaultValuesForProjectAsync(int projectId);
|
||||||
Task<bool> RemoveUnusedAsync(int metricTypeId, int projectId);
|
Task<bool> RemoveUnusedAsync(int metricTypeId, int projectId);
|
||||||
Task<bool> HasProjectMetricsAsync(int projectId);
|
Task<bool> HasProjectMetricsAsync(int projectId);
|
||||||
}
|
}
|
||||||
@ -2864,6 +2865,15 @@ public sealed class SceneMetricTypeRepository(ISqlConnectionFactory connectionFa
|
|||||||
await connection.ExecuteAsync("dbo.SceneMetricType_AddMissingDefaultsForProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
|
await connection.ExecuteAsync("dbo.SceneMetricType_AddMissingDefaultsForProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<int> BackfillDefaultValuesForProjectAsync(int projectId)
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
return await connection.QuerySingleAsync<int>(
|
||||||
|
"dbo.SceneMetric_BackfillDefaultsForProject",
|
||||||
|
new { ProjectID = projectId },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<bool> RemoveUnusedAsync(int metricTypeId, int projectId)
|
public async Task<bool> RemoveUnusedAsync(int metricTypeId, int projectId)
|
||||||
{
|
{
|
||||||
using var connection = connectionFactory.CreateConnection();
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
|||||||
@ -97,7 +97,7 @@ public sealed class NameSearchFilter
|
|||||||
public int ProjectID { get; set; }
|
public int ProjectID { get; set; }
|
||||||
public int? BookID { get; set; }
|
public int? BookID { get; set; }
|
||||||
public string? Search { get; set; }
|
public string? Search { get; set; }
|
||||||
public string? LocaleCode { get; set; } = "en-US";
|
public string? LocaleCode { get; set; }
|
||||||
public int? BirthYear { get; set; }
|
public int? BirthYear { get; set; }
|
||||||
public string? GenderPreference { get; set; }
|
public string? GenderPreference { get; set; }
|
||||||
public decimal? GenderThreshold { get; set; }
|
public decimal? GenderThreshold { get; set; }
|
||||||
|
|||||||
@ -39,6 +39,7 @@ public sealed class WordCompanionPresenceStatus
|
|||||||
public string? CurrentDocumentName { get; init; }
|
public string? CurrentDocumentName { get; init; }
|
||||||
public int? LinkedProjectID { get; init; }
|
public int? LinkedProjectID { get; init; }
|
||||||
public int? LinkedBookID { get; init; }
|
public int? LinkedBookID { get; init; }
|
||||||
|
public StoryPosition? CurrentStoryPosition { get; init; }
|
||||||
public DateTime? ConnectedUtc { get; init; }
|
public DateTime? ConnectedUtc { get; init; }
|
||||||
public DateTime? LastHeartbeatUtc { get; init; }
|
public DateTime? LastHeartbeatUtc { get; init; }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -309,7 +309,7 @@ public interface ISceneMetricTypeService
|
|||||||
Task<int> SaveAsync(SceneMetricTypeEditViewModel model);
|
Task<int> SaveAsync(SceneMetricTypeEditViewModel model);
|
||||||
Task<bool> SetActiveAsync(int projectId, int metricTypeId, bool isActive);
|
Task<bool> SetActiveAsync(int projectId, int metricTypeId, bool isActive);
|
||||||
Task<bool> MoveAsync(int projectId, int metricTypeId, string direction);
|
Task<bool> MoveAsync(int projectId, int metricTypeId, string direction);
|
||||||
Task AddMissingDefaultsAsync(int projectId);
|
Task<int> AddMissingDefaultsAsync(int projectId);
|
||||||
Task<bool> RemoveUnusedAsync(int projectId, int metricTypeId);
|
Task<bool> RemoveUnusedAsync(int projectId, int metricTypeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -5247,7 +5247,11 @@ public sealed class SceneMetricTypeService(IProjectRepository projects, ISceneMe
|
|||||||
|
|
||||||
public Task<bool> MoveAsync(int projectId, int metricTypeId, string direction) => metricTypes.MoveAsync(metricTypeId, projectId, direction);
|
public Task<bool> MoveAsync(int projectId, int metricTypeId, string direction) => metricTypes.MoveAsync(metricTypeId, projectId, direction);
|
||||||
|
|
||||||
public Task AddMissingDefaultsAsync(int projectId) => metricTypes.AddMissingDefaultsAsync(projectId);
|
public async Task<int> AddMissingDefaultsAsync(int projectId)
|
||||||
|
{
|
||||||
|
await metricTypes.AddMissingDefaultsAsync(projectId);
|
||||||
|
return await metricTypes.BackfillDefaultValuesForProjectAsync(projectId);
|
||||||
|
}
|
||||||
|
|
||||||
public Task<bool> RemoveUnusedAsync(int projectId, int metricTypeId) => metricTypes.RemoveUnusedAsync(metricTypeId, projectId);
|
public Task<bool> RemoveUnusedAsync(int projectId, int metricTypeId) => metricTypes.RemoveUnusedAsync(metricTypeId, projectId);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -687,10 +687,13 @@ public sealed class StoryIntelligenceAssetImportService(
|
|||||||
{
|
{
|
||||||
var summary = Clean(parsed.Summary?.Short);
|
var summary = Clean(parsed.Summary?.Short);
|
||||||
return string.IsNullOrWhiteSpace(summary)
|
return string.IsNullOrWhiteSpace(summary)
|
||||||
? $"Scene {scene.SceneNumber:g}: {assetName}"
|
? $"{BuildSceneReference(scene)}: {assetName}"
|
||||||
: $"Scene {scene.SceneNumber:g}: {summary}";
|
: $"{BuildSceneReference(scene)}: {summary}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string BuildSceneReference(Scene scene)
|
||||||
|
=> $"Chapter {scene.ChapterNumber:g}, Scene {scene.SceneNumber:g}";
|
||||||
|
|
||||||
private static void AddDecision(OnboardingStoryIntelligenceBatch batch, string key, string action, string? assetName, int? storyAssetId, bool createdOrLinked)
|
private static void AddDecision(OnboardingStoryIntelligenceBatch batch, string key, string action, string? assetName, int? storyAssetId, bool createdOrLinked)
|
||||||
{
|
{
|
||||||
batch.AssetDecisions.RemoveAll(decision => string.Equals(decision.Key, key, StringComparison.OrdinalIgnoreCase));
|
batch.AssetDecisions.RemoveAll(decision => string.Equals(decision.Key, key, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|||||||
@ -725,13 +725,16 @@ public sealed class StoryIntelligenceCharacterImportService(
|
|||||||
var note = Clean(character.Notes);
|
var note = Clean(character.Notes);
|
||||||
if (!string.IsNullOrWhiteSpace(note))
|
if (!string.IsNullOrWhiteSpace(note))
|
||||||
{
|
{
|
||||||
return $"Scene {scene.SceneNumber:g}: {note}";
|
return $"{BuildSceneReference(scene)}: {note}";
|
||||||
}
|
}
|
||||||
|
|
||||||
var summary = Clean(parsed.Summary?.Short);
|
var summary = Clean(parsed.Summary?.Short);
|
||||||
return string.IsNullOrWhiteSpace(summary) ? $"Scene {scene.SceneNumber:g}: {scene.SceneTitle}" : $"Scene {scene.SceneNumber:g}: {summary}";
|
return string.IsNullOrWhiteSpace(summary) ? $"{BuildSceneReference(scene)}: {scene.SceneTitle}" : $"{BuildSceneReference(scene)}: {summary}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string BuildSceneReference(Scene scene)
|
||||||
|
=> $"Chapter {scene.ChapterNumber:g}, Scene {scene.SceneNumber:g}";
|
||||||
|
|
||||||
private static decimal? Max(decimal? current, decimal? next)
|
private static decimal? Max(decimal? current, decimal? next)
|
||||||
=> current.HasValue && next.HasValue ? Math.Max(current.Value, next.Value) : current ?? next;
|
=> current.HasValue && next.HasValue ? Math.Max(current.Value, next.Value) : current ?? next;
|
||||||
|
|
||||||
|
|||||||
@ -148,8 +148,13 @@ public sealed class StoryIntelligenceImportCommitService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var lookupData = await lookups.GetAllAsync();
|
var lookupData = await lookups.GetAllAsync();
|
||||||
|
if (run.ProjectID.HasValue)
|
||||||
|
{
|
||||||
|
await metricTypes.AddMissingDefaultsAsync(run.ProjectID.Value);
|
||||||
|
}
|
||||||
|
|
||||||
var activeMetricTypes = run.ProjectID.HasValue
|
var activeMetricTypes = run.ProjectID.HasValue
|
||||||
? (await metricTypes.ListForManagementAsync(run.ProjectID.Value)).Where(metric => metric.IsActive).ToList()
|
? (await metricTypes.ListForManagementAsync(run.ProjectID.Value)).Where(metric => metric.IsActive && metric.IsEnabledForProject).ToList()
|
||||||
: [];
|
: [];
|
||||||
var noteTypes = await writerWorkspace.ListNoteTypesAsync();
|
var noteTypes = await writerWorkspace.ListNoteTypesAsync();
|
||||||
var characterMap = run.ProjectID.HasValue
|
var characterMap = run.ProjectID.HasValue
|
||||||
|
|||||||
@ -608,7 +608,7 @@ public sealed class StoryIntelligenceKnowledgeImportService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static string BuildSceneReference(Scene scene)
|
private static string BuildSceneReference(Scene scene)
|
||||||
=> $"Scene {scene.SceneNumber:g}";
|
=> $"Chapter {scene.ChapterNumber:g}, Scene {scene.SceneNumber:g}";
|
||||||
|
|
||||||
private static bool IsCharacterEntity(string? entityType)
|
private static bool IsCharacterEntity(string? entityType)
|
||||||
=> string.Equals(Clean(entityType), "Character", StringComparison.OrdinalIgnoreCase);
|
=> string.Equals(Clean(entityType), "Character", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|||||||
@ -730,10 +730,13 @@ public sealed class StoryIntelligenceLocationImportService(
|
|||||||
{
|
{
|
||||||
var summary = Clean(parsed.Summary?.Short);
|
var summary = Clean(parsed.Summary?.Short);
|
||||||
return string.IsNullOrWhiteSpace(summary)
|
return string.IsNullOrWhiteSpace(summary)
|
||||||
? $"Scene {scene.SceneNumber:g}: {locationName}"
|
? $"{BuildSceneReference(scene)}: {locationName}"
|
||||||
: $"Scene {scene.SceneNumber:g}: {summary}";
|
: $"{BuildSceneReference(scene)}: {summary}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string BuildSceneReference(Scene scene)
|
||||||
|
=> $"Chapter {scene.ChapterNumber:g}, Scene {scene.SceneNumber:g}";
|
||||||
|
|
||||||
private static string FirstConfigured(params string?[] values)
|
private static string FirstConfigured(params string?[] values)
|
||||||
=> values.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value))?.Trim() ?? string.Empty;
|
=> values.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value))?.Trim() ?? string.Empty;
|
||||||
|
|
||||||
|
|||||||
@ -623,7 +623,7 @@ public sealed class StoryIntelligenceRelationshipImportService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static string BuildSceneReference(Scene scene)
|
private static string BuildSceneReference(Scene scene)
|
||||||
=> $"Scene {scene.SceneNumber:g}";
|
=> $"Chapter {scene.ChapterNumber:g}, Scene {scene.SceneNumber:g}";
|
||||||
|
|
||||||
private static void AddDecision(OnboardingStoryIntelligenceBatch batch, string key, string action, int? relationshipId, string? label, bool createdOrLinked)
|
private static void AddDecision(OnboardingStoryIntelligenceBatch batch, string key, string action, int? relationshipId, string? label, bool createdOrLinked)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -9,6 +9,7 @@ public interface IWordCompanionPresenceService
|
|||||||
Task<WordCompanionPresenceStatus> HeartbeatAsync(int userId, string connectionId, WordCompanionPresenceHeartbeat heartbeat);
|
Task<WordCompanionPresenceStatus> HeartbeatAsync(int userId, string connectionId, WordCompanionPresenceHeartbeat heartbeat);
|
||||||
Task<WordCompanionPresenceStatus?> DisconnectAsync(string connectionId);
|
Task<WordCompanionPresenceStatus?> DisconnectAsync(string connectionId);
|
||||||
Task<WordCompanionPresenceStatus> GetStatusAsync(int userId);
|
Task<WordCompanionPresenceStatus> GetStatusAsync(int userId);
|
||||||
|
Task<WordCompanionPresenceStatus> UpdateCurrentStoryPositionAsync(int userId, StoryPosition? position);
|
||||||
Task<bool> IsConnectedAsync(int userId);
|
Task<bool> IsConnectedAsync(int userId);
|
||||||
Task<IReadOnlyList<WordCompanionPresenceStatus>> MarkStaleOfflineAsync();
|
Task<IReadOnlyList<WordCompanionPresenceStatus>> MarkStaleOfflineAsync();
|
||||||
Task<string?> GetCompanionConnectionIdAsync(int userId);
|
Task<string?> GetCompanionConnectionIdAsync(int userId);
|
||||||
@ -68,6 +69,11 @@ public sealed class WordCompanionPresenceService : IWordCompanionPresenceService
|
|||||||
record.LinkedProjectID = Positive(heartbeat.LinkedProjectID);
|
record.LinkedProjectID = Positive(heartbeat.LinkedProjectID);
|
||||||
record.LinkedBookID = Positive(heartbeat.LinkedBookID);
|
record.LinkedBookID = Positive(heartbeat.LinkedBookID);
|
||||||
record.LastHeartbeatUtc = now;
|
record.LastHeartbeatUtc = now;
|
||||||
|
if (record.CurrentStoryPosition is not null
|
||||||
|
&& (record.LinkedProjectID != record.CurrentStoryPosition.ProjectId || record.LinkedBookID != record.CurrentStoryPosition.BookId))
|
||||||
|
{
|
||||||
|
record.CurrentStoryPosition = null;
|
||||||
|
}
|
||||||
|
|
||||||
usersByConnection[connectionId] = userId;
|
usersByConnection[connectionId] = userId;
|
||||||
return Task.FromResult(ToStatus(record, now));
|
return Task.FromResult(ToStatus(record, now));
|
||||||
@ -97,6 +103,28 @@ public sealed class WordCompanionPresenceService : IWordCompanionPresenceService
|
|||||||
return Task.FromResult(ToStatus(record, DateTime.UtcNow));
|
return Task.FromResult(ToStatus(record, DateTime.UtcNow));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task<WordCompanionPresenceStatus> UpdateCurrentStoryPositionAsync(int userId, StoryPosition? position)
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
var record = recordsByUser.AddOrUpdate(
|
||||||
|
userId,
|
||||||
|
_ => new PresenceRecord
|
||||||
|
{
|
||||||
|
UserID = userId,
|
||||||
|
ConnectedUtc = now
|
||||||
|
},
|
||||||
|
(_, existing) => existing);
|
||||||
|
|
||||||
|
if (position?.IsValid == true)
|
||||||
|
{
|
||||||
|
record.CurrentStoryPosition = position;
|
||||||
|
record.LinkedProjectID = position.ProjectId;
|
||||||
|
record.LinkedBookID = position.BookId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(ToStatus(record, now));
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<bool> IsConnectedAsync(int userId)
|
public async Task<bool> IsConnectedAsync(int userId)
|
||||||
=> (await GetStatusAsync(userId)).IsConnected;
|
=> (await GetStatusAsync(userId)).IsConnected;
|
||||||
|
|
||||||
@ -156,6 +184,7 @@ public sealed class WordCompanionPresenceService : IWordCompanionPresenceService
|
|||||||
CurrentDocumentName = record.CurrentDocumentName,
|
CurrentDocumentName = record.CurrentDocumentName,
|
||||||
LinkedProjectID = record.LinkedProjectID,
|
LinkedProjectID = record.LinkedProjectID,
|
||||||
LinkedBookID = record.LinkedBookID,
|
LinkedBookID = record.LinkedBookID,
|
||||||
|
CurrentStoryPosition = connected ? record.CurrentStoryPosition : null,
|
||||||
ConnectedUtc = record.ConnectedUtc,
|
ConnectedUtc = record.ConnectedUtc,
|
||||||
LastHeartbeatUtc = record.LastHeartbeatUtc
|
LastHeartbeatUtc = record.LastHeartbeatUtc
|
||||||
};
|
};
|
||||||
@ -181,6 +210,7 @@ public sealed class WordCompanionPresenceService : IWordCompanionPresenceService
|
|||||||
public string? CurrentDocumentName { get; set; }
|
public string? CurrentDocumentName { get; set; }
|
||||||
public int? LinkedProjectID { get; set; }
|
public int? LinkedProjectID { get; set; }
|
||||||
public int? LinkedBookID { get; set; }
|
public int? LinkedBookID { get; set; }
|
||||||
|
public StoryPosition? CurrentStoryPosition { get; set; }
|
||||||
public DateTime? ConnectedUtc { get; set; }
|
public DateTime? ConnectedUtc { get; set; }
|
||||||
public DateTime? LastHeartbeatUtc { get; set; }
|
public DateTime? LastHeartbeatUtc { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -147,6 +147,7 @@ public sealed class WritingWorkspacePresenceService : IWritingWorkspacePresenceS
|
|||||||
WordDocumentOpen = wordStatus.DocumentOpen,
|
WordDocumentOpen = wordStatus.DocumentOpen,
|
||||||
WordProjectID = wordStatus.LinkedProjectID,
|
WordProjectID = wordStatus.LinkedProjectID,
|
||||||
WordBookID = wordStatus.LinkedBookID,
|
WordBookID = wordStatus.LinkedBookID,
|
||||||
|
CurrentStoryPosition = wordStatus.CurrentStoryPosition,
|
||||||
Windows = windows,
|
Windows = windows,
|
||||||
Launcher = LatestRole(windows, WritingWorkspaceRoles.Launcher),
|
Launcher = LatestRole(windows, WritingWorkspaceRoles.Launcher),
|
||||||
Context = LatestRole(windows, WritingWorkspaceRoles.Context),
|
Context = LatestRole(windows, WritingWorkspaceRoles.Context),
|
||||||
|
|||||||
@ -25,6 +25,25 @@ AS
|
|||||||
BEGIN
|
BEGIN
|
||||||
SET NOCOUNT ON;
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
MERGE dbo.SceneMetricTypes AS target
|
||||||
|
USING (VALUES
|
||||||
|
(N'Overall Intensity', N'General dramatic intensity for the scene.', 1, 10, 5, 10),
|
||||||
|
(N'Tension', N'Pressure, suspense, uncertainty, or conflict.', 1, 10, 5, 20),
|
||||||
|
(N'Emotional Weight', N'Emotional load carried by the scene.', 1, 10, 5, 30),
|
||||||
|
(N'Action', N'Physical action, movement, or event energy.', 1, 10, 3, 40),
|
||||||
|
(N'Darkness', N'Darkness, bleakness, fear, or threat.', 1, 10, 3, 50),
|
||||||
|
(N'Romance', N'Romantic focus or intimacy.', 1, 10, 1, 60),
|
||||||
|
(N'Sexual Charge', N'Sexual tension or sensual charge.', 1, 10, 1, 70),
|
||||||
|
(N'Violence', N'Violent action, injury, or threat.', 1, 10, 1, 80),
|
||||||
|
(N'Hope / Lightness', N'Hope, relief, warmth, or tonal lightness.', 1, 10, 4, 90),
|
||||||
|
(N'Comedy', N'Humour or comic relief.', 1, 10, 1, 100),
|
||||||
|
(N'Mystery', N'Questions, clue energy, and mystery pressure.', 1, 10, 5, 110)
|
||||||
|
) AS source (MetricName, Description, MinValue, MaxValue, DefaultValue, SortOrder)
|
||||||
|
ON target.ProjectID IS NULL AND target.MetricName = source.MetricName
|
||||||
|
WHEN NOT MATCHED THEN
|
||||||
|
INSERT (ProjectID, MetricName, Description, MinValue, MaxValue, DefaultValue, SortOrder, IsActive)
|
||||||
|
VALUES (NULL, source.MetricName, source.Description, source.MinValue, source.MaxValue, source.DefaultValue, source.SortOrder, 1);
|
||||||
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM dbo.ProjectTimelineSettings WHERE ProjectID = @ProjectID)
|
IF NOT EXISTS (SELECT 1 FROM dbo.ProjectTimelineSettings WHERE ProjectID = @ProjectID)
|
||||||
BEGIN
|
BEGIN
|
||||||
INSERT dbo.ProjectTimelineSettings
|
INSERT dbo.ProjectTimelineSettings
|
||||||
|
|||||||
@ -484,8 +484,11 @@ BEGIN
|
|||||||
IF @PageSize < 1 SET @PageSize = 25;
|
IF @PageSize < 1 SET @PageSize = 25;
|
||||||
IF @PageSize > 100 SET @PageSize = 100;
|
IF @PageSize > 100 SET @PageSize = 100;
|
||||||
|
|
||||||
DECLARE @NameLocaleID int;
|
DECLARE @NameLocaleID int = NULL;
|
||||||
SELECT TOP (1) @NameLocaleID = NameLocaleID FROM dbo.NameLocales WHERE LocaleCode = COALESCE(@LocaleCode, N'en-US') AND IsActive = 1;
|
IF @LocaleCode IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
SELECT TOP (1) @NameLocaleID = NameLocaleID FROM dbo.NameLocales WHERE LocaleCode = @LocaleCode AND IsActive = 1;
|
||||||
|
END;
|
||||||
|
|
||||||
;WITH UsageChoice AS
|
;WITH UsageChoice AS
|
||||||
(
|
(
|
||||||
|
|||||||
38
PlotLine/Sql/151_BackfillDefaultSceneMetricValues.sql
Normal file
38
PlotLine/Sql/151_BackfillDefaultSceneMetricValues.sql
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
SET ANSI_NULLS ON;
|
||||||
|
GO
|
||||||
|
SET QUOTED_IDENTIFIER ON;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.SceneMetric_BackfillDefaultsForProject
|
||||||
|
@ProjectID int
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
DECLARE @Inserted table (SceneID int NOT NULL, MetricTypeID int NOT NULL);
|
||||||
|
|
||||||
|
INSERT dbo.SceneMetricValues (SceneID, MetricTypeID, Value, Notes)
|
||||||
|
OUTPUT inserted.SceneID, inserted.MetricTypeID INTO @Inserted
|
||||||
|
SELECT s.SceneID, mt.MetricTypeID, mt.DefaultValue, NULL
|
||||||
|
FROM dbo.Scenes s
|
||||||
|
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
|
||||||
|
INNER JOIN dbo.Books b ON b.BookID = c.BookID
|
||||||
|
INNER JOIN dbo.ProjectTimelineMetricSettings ptms ON ptms.ProjectID = b.ProjectID
|
||||||
|
INNER JOIN dbo.SceneMetricTypes mt ON mt.MetricTypeID = ptms.MetricTypeID
|
||||||
|
WHERE b.ProjectID = @ProjectID
|
||||||
|
AND b.IsArchived = 0
|
||||||
|
AND c.IsArchived = 0
|
||||||
|
AND s.IsArchived = 0
|
||||||
|
AND mt.IsActive = 1
|
||||||
|
AND (mt.ProjectID IS NULL OR mt.ProjectID = @ProjectID)
|
||||||
|
AND NOT EXISTS
|
||||||
|
(
|
||||||
|
SELECT 1
|
||||||
|
FROM dbo.SceneMetricValues existing
|
||||||
|
WHERE existing.SceneID = s.SceneID
|
||||||
|
AND existing.MetricTypeID = mt.MetricTypeID
|
||||||
|
);
|
||||||
|
|
||||||
|
SELECT COUNT(*) FROM @Inserted;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
131
PlotLine/Sql/152_NameLibraryAllLocalesDefault.sql
Normal file
131
PlotLine/Sql/152_NameLibraryAllLocalesDefault.sql
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
SET ANSI_NULLS ON;
|
||||||
|
GO
|
||||||
|
SET QUOTED_IDENTIFIER ON;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.GivenName_Search
|
||||||
|
@UserID int,
|
||||||
|
@ProjectID int,
|
||||||
|
@BookID int = NULL,
|
||||||
|
@Search nvarchar(120) = NULL,
|
||||||
|
@LocaleCode nvarchar(16) = NULL,
|
||||||
|
@BirthYear int = NULL,
|
||||||
|
@GenderPreference nvarchar(12) = NULL,
|
||||||
|
@GenderThreshold decimal(9,4) = NULL,
|
||||||
|
@NameType nvarchar(40) = NULL,
|
||||||
|
@MinimumUsage decimal(9,4) = NULL,
|
||||||
|
@UsedScope nvarchar(20) = N'Project',
|
||||||
|
@UsedOnly bit = NULL,
|
||||||
|
@Page int = 1,
|
||||||
|
@PageSize int = 25
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM dbo.ProjectUserAccess WHERE ProjectID = @ProjectID AND UserID = @UserID AND IsActive = 1)
|
||||||
|
BEGIN
|
||||||
|
SELECT TOP (0) CAST(NULL AS int) AS GivenNameID;
|
||||||
|
RETURN;
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF @Page < 1 SET @Page = 1;
|
||||||
|
IF @PageSize < 1 SET @PageSize = 25;
|
||||||
|
IF @PageSize > 100 SET @PageSize = 100;
|
||||||
|
|
||||||
|
DECLARE @NameLocaleID int = NULL;
|
||||||
|
IF @LocaleCode IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
SELECT TOP (1) @NameLocaleID = NameLocaleID FROM dbo.NameLocales WHERE LocaleCode = @LocaleCode AND IsActive = 1;
|
||||||
|
END;
|
||||||
|
|
||||||
|
;WITH UsageChoice AS
|
||||||
|
(
|
||||||
|
SELECT
|
||||||
|
u.*,
|
||||||
|
l.LocaleCode,
|
||||||
|
l.DisplayName AS LocaleDisplayName,
|
||||||
|
ROW_NUMBER() OVER (PARTITION BY u.GivenNameID ORDER BY
|
||||||
|
CASE WHEN @BirthYear IS NOT NULL AND @BirthYear BETWEEN u.BirthYearFrom AND u.BirthYearTo THEN 0 ELSE 1 END,
|
||||||
|
CASE WHEN @BirthYear IS NULL THEN ABS(1960 - ((u.BirthYearFrom + u.BirthYearTo) / 2)) ELSE ABS(@BirthYear - ((u.BirthYearFrom + u.BirthYearTo) / 2)) END,
|
||||||
|
u.UsageWeight DESC) AS rn
|
||||||
|
FROM dbo.NameUsages u
|
||||||
|
INNER JOIN dbo.NameLocales l ON l.NameLocaleID = u.NameLocaleID
|
||||||
|
WHERE u.IsActive = 1
|
||||||
|
AND (@NameLocaleID IS NULL OR u.NameLocaleID = @NameLocaleID)
|
||||||
|
),
|
||||||
|
Used AS
|
||||||
|
(
|
||||||
|
SELECT g.GivenNameID, COUNT(DISTINCT c.CharacterID) AS UsedCount,
|
||||||
|
STRING_AGG(CONVERT(nvarchar(max), c.CharacterName + N' (' + p.ProjectName + COALESCE(N' / ' + b.BookTitle, N'') + N')'), N'; ') AS UsedSummary
|
||||||
|
FROM dbo.GivenNames g
|
||||||
|
INNER JOIN
|
||||||
|
(
|
||||||
|
SELECT c.CharacterID, c.ProjectID, c.CharacterName, c.CharacterName AS MatchedName
|
||||||
|
FROM dbo.Characters c
|
||||||
|
WHERE c.IsArchived = 0
|
||||||
|
UNION ALL
|
||||||
|
SELECT c.CharacterID, c.ProjectID, c.CharacterName, ca.Alias AS MatchedName
|
||||||
|
FROM dbo.CharacterAliases ca
|
||||||
|
INNER JOIN dbo.Characters c ON c.CharacterID = ca.CharacterID
|
||||||
|
WHERE c.IsArchived = 0
|
||||||
|
) c ON UPPER(LTRIM(RTRIM(c.MatchedName))) = g.NormalisedName COLLATE Latin1_General_100_CI_AI_SC
|
||||||
|
INNER JOIN dbo.Projects p ON p.ProjectID = c.ProjectID
|
||||||
|
INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = c.ProjectID AND pua.UserID = @UserID AND pua.IsActive = 1
|
||||||
|
LEFT JOIN dbo.Books b ON b.ProjectID = c.ProjectID AND b.IsArchived = 0
|
||||||
|
WHERE (@UsedScope = N'Library' OR c.ProjectID = @ProjectID)
|
||||||
|
AND (@BookID IS NULL OR @UsedScope <> N'Book' OR b.BookID = @BookID)
|
||||||
|
GROUP BY g.GivenNameID
|
||||||
|
),
|
||||||
|
Related AS
|
||||||
|
(
|
||||||
|
SELECT x.GivenNameID,
|
||||||
|
STRING_AGG(CONVERT(nvarchar(max), x.DisplayName), N', ') AS RelatedForms
|
||||||
|
FROM
|
||||||
|
(
|
||||||
|
SELECT r.FromGivenNameID AS GivenNameID, t.DisplayName
|
||||||
|
FROM dbo.NameRelationships r
|
||||||
|
INNER JOIN dbo.GivenNames t ON t.GivenNameID = r.ToGivenNameID
|
||||||
|
WHERE r.IsActive = 1
|
||||||
|
UNION
|
||||||
|
SELECT r.ToGivenNameID AS GivenNameID, f.DisplayName
|
||||||
|
FROM dbo.NameRelationships r
|
||||||
|
INNER JOIN dbo.GivenNames f ON f.GivenNameID = r.FromGivenNameID
|
||||||
|
WHERE r.IsActive = 1 AND r.IsBidirectional = 1
|
||||||
|
) x
|
||||||
|
GROUP BY x.GivenNameID
|
||||||
|
),
|
||||||
|
Filtered AS
|
||||||
|
(
|
||||||
|
SELECT
|
||||||
|
g.GivenNameID, g.GivenNameKey, g.DisplayName, g.NormalisedName, g.NameType, g.IsWordAmbiguous,
|
||||||
|
u.MaleWeight, u.FemaleWeight, u.UsageWeight, u.Confidence, u.BirthYearFrom, u.BirthYearTo, u.LocaleCode, u.LocaleDisplayName, u.SourceType,
|
||||||
|
rel.RelatedForms,
|
||||||
|
COALESCE(used.UsedSummary, N'') AS UsedSummary,
|
||||||
|
COALESCE(used.UsedCount, 0) AS UsedCount,
|
||||||
|
COUNT(1) OVER() AS TotalRows
|
||||||
|
FROM dbo.GivenNames g
|
||||||
|
LEFT JOIN UsageChoice u ON u.GivenNameID = g.GivenNameID AND u.rn = 1
|
||||||
|
LEFT JOIN Used used ON used.GivenNameID = g.GivenNameID
|
||||||
|
LEFT JOIN Related rel ON rel.GivenNameID = g.GivenNameID
|
||||||
|
WHERE g.IsActive = 1
|
||||||
|
AND (@Search IS NULL OR g.NormalisedName LIKE UPPER(LTRIM(RTRIM(@Search))) + N'%' COLLATE Latin1_General_100_CI_AI_SC OR g.DisplayName LIKE @Search + N'%')
|
||||||
|
AND (@NameType IS NULL OR g.NameType = @NameType)
|
||||||
|
AND (@MinimumUsage IS NULL OR u.UsageWeight >= @MinimumUsage)
|
||||||
|
AND (@UsedOnly IS NULL OR (@UsedOnly = 1 AND COALESCE(used.UsedCount, 0) > 0) OR (@UsedOnly = 0 AND COALESCE(used.UsedCount, 0) = 0))
|
||||||
|
AND (
|
||||||
|
@GenderPreference IS NULL
|
||||||
|
OR @GenderThreshold IS NULL
|
||||||
|
OR (@GenderPreference = N'Female' AND u.FemaleWeight >= @GenderThreshold)
|
||||||
|
OR (@GenderPreference = N'Male' AND u.MaleWeight >= @GenderThreshold)
|
||||||
|
OR (@GenderPreference = N'Mixed' AND u.MaleWeight >= (1 - @GenderThreshold) AND u.FemaleWeight >= (1 - @GenderThreshold))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
SELECT *
|
||||||
|
FROM Filtered
|
||||||
|
ORDER BY
|
||||||
|
CASE WHEN @Search IS NOT NULL AND NormalisedName = UPPER(LTRIM(RTRIM(@Search))) COLLATE Latin1_General_100_CI_AI_SC THEN 0 ELSE 1 END,
|
||||||
|
UsageWeight DESC,
|
||||||
|
DisplayName
|
||||||
|
OFFSET (@Page - 1) * @PageSize ROWS FETCH NEXT @PageSize ROWS ONLY;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
58
PlotLine/Sql/153_SelfHealingProjectMetricDefaults.sql
Normal file
58
PlotLine/Sql/153_SelfHealingProjectMetricDefaults.sql
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
SET ANSI_NULLS ON;
|
||||||
|
GO
|
||||||
|
SET QUOTED_IDENTIFIER ON;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.SceneMetricType_AddMissingDefaultsForProject
|
||||||
|
@ProjectID int
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
MERGE dbo.SceneMetricTypes AS target
|
||||||
|
USING (VALUES
|
||||||
|
(N'Overall Intensity', N'General dramatic intensity for the scene.', 1, 10, 5, 10),
|
||||||
|
(N'Tension', N'Pressure, suspense, uncertainty, or conflict.', 1, 10, 5, 20),
|
||||||
|
(N'Emotional Weight', N'Emotional load carried by the scene.', 1, 10, 5, 30),
|
||||||
|
(N'Action', N'Physical action, movement, or event energy.', 1, 10, 3, 40),
|
||||||
|
(N'Darkness', N'Darkness, bleakness, fear, or threat.', 1, 10, 3, 50),
|
||||||
|
(N'Romance', N'Romantic focus or intimacy.', 1, 10, 1, 60),
|
||||||
|
(N'Sexual Charge', N'Sexual tension or sensual charge.', 1, 10, 1, 70),
|
||||||
|
(N'Violence', N'Violent action, injury, or threat.', 1, 10, 1, 80),
|
||||||
|
(N'Hope / Lightness', N'Hope, relief, warmth, or tonal lightness.', 1, 10, 4, 90),
|
||||||
|
(N'Comedy', N'Humour or comic relief.', 1, 10, 1, 100),
|
||||||
|
(N'Mystery', N'Questions, clue energy, and mystery pressure.', 1, 10, 5, 110)
|
||||||
|
) AS source (MetricName, Description, MinValue, MaxValue, DefaultValue, SortOrder)
|
||||||
|
ON target.ProjectID IS NULL AND target.MetricName = source.MetricName
|
||||||
|
WHEN NOT MATCHED THEN
|
||||||
|
INSERT (ProjectID, MetricName, Description, MinValue, MaxValue, DefaultValue, SortOrder, IsActive)
|
||||||
|
VALUES (NULL, source.MetricName, source.Description, source.MinValue, source.MaxValue, source.DefaultValue, source.SortOrder, 1);
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM dbo.ProjectTimelineSettings WHERE ProjectID = @ProjectID)
|
||||||
|
BEGIN
|
||||||
|
INSERT dbo.ProjectTimelineSettings
|
||||||
|
(ProjectID, ShowSceneCards, ShowMetricShape, ShowPlotLines, ShowStoryAssets, ShowCharacterAppearances, ShowWarnings)
|
||||||
|
VALUES
|
||||||
|
(@ProjectID, 1, 1, 1, 1, 1, 1);
|
||||||
|
END;
|
||||||
|
|
||||||
|
INSERT dbo.ProjectTimelineMetricSettings (ProjectID, MetricTypeID, SortOrder)
|
||||||
|
SELECT @ProjectID, mt.MetricTypeID, mt.SortOrder
|
||||||
|
FROM dbo.SceneMetricTypes mt
|
||||||
|
WHERE mt.ProjectID IS NULL
|
||||||
|
AND mt.IsActive = 1
|
||||||
|
AND mt.MetricName IN
|
||||||
|
(
|
||||||
|
N'Overall Intensity', N'Tension', N'Emotional Weight', N'Action', N'Darkness',
|
||||||
|
N'Romance', N'Sexual Charge', N'Violence', N'Hope / Lightness', N'Comedy', N'Mystery'
|
||||||
|
)
|
||||||
|
AND NOT EXISTS
|
||||||
|
(
|
||||||
|
SELECT 1
|
||||||
|
FROM dbo.ProjectTimelineMetricSettings ptms
|
||||||
|
INNER JOIN dbo.SceneMetricTypes existing ON existing.MetricTypeID = ptms.MetricTypeID
|
||||||
|
WHERE ptms.ProjectID = @ProjectID
|
||||||
|
AND LOWER(existing.MetricName) = LOWER(mt.MetricName)
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
GO
|
||||||
@ -137,7 +137,7 @@
|
|||||||
<h3>Scenes</h3>
|
<h3>Scenes</h3>
|
||||||
@foreach (var scene in Model.Scenes)
|
@foreach (var scene in Model.Scenes)
|
||||||
{
|
{
|
||||||
<p><strong>Scene @scene.SceneNumber</strong> <a asp-controller="Timeline" asp-action="Index" asp-route-ProjectID="@Model.Project.ProjectID" asp-route-SelectedSceneID="@scene.SceneID" asp-route-FocusType="location" asp-route-FocusID="@Model.Location.LocationID">@scene.SceneTitle</a></p>
|
<p><strong>@SceneLabel(scene.ChapterNumber, scene.SceneNumber)</strong> <a asp-controller="Timeline" asp-action="Index" asp-route-ProjectID="@Model.Project.ProjectID" asp-route-SelectedSceneID="@scene.SceneID" asp-route-FocusType="location" asp-route-FocusID="@Model.Location.LocationID">@scene.SceneTitle</a></p>
|
||||||
}
|
}
|
||||||
@if (!Model.Scenes.Any()) { <p class="muted">No scenes set here.</p> }
|
@if (!Model.Scenes.Any()) { <p class="muted">No scenes set here.</p> }
|
||||||
</div>
|
</div>
|
||||||
@ -145,7 +145,7 @@
|
|||||||
<h3>Characters</h3>
|
<h3>Characters</h3>
|
||||||
@foreach (var character in Model.Characters)
|
@foreach (var character in Model.Characters)
|
||||||
{
|
{
|
||||||
<p><strong>@character.CharacterName</strong> <span class="muted">Scene @character.SceneNumber</span></p>
|
<p><strong>@character.CharacterName</strong> <span class="muted">@SceneLabel(character.ChapterNumber, character.SceneNumber)</span></p>
|
||||||
}
|
}
|
||||||
@if (!Model.Characters.Any()) { <p class="muted">No character placements yet.</p> }
|
@if (!Model.Characters.Any()) { <p class="muted">No character placements yet.</p> }
|
||||||
</div>
|
</div>
|
||||||
@ -154,7 +154,12 @@
|
|||||||
@foreach (var asset in Model.Assets)
|
@foreach (var asset in Model.Assets)
|
||||||
{
|
{
|
||||||
<p><strong>@asset.AssetName</strong> <span class="muted">@asset.Description</span></p>
|
<p><strong>@asset.AssetName</strong> <span class="muted">@asset.Description</span></p>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@functions {
|
||||||
|
private static string SceneLabel(decimal chapterNumber, decimal sceneNumber)
|
||||||
|
=> chapterNumber > 0 ? $"Chapter {chapterNumber:g}, Scene {sceneNumber:g}" : $"Scene {sceneNumber:g}";
|
||||||
|
}
|
||||||
@if (!Model.Assets.Any()) { <p class="muted">No asset placements yet.</p> }
|
@if (!Model.Assets.Any()) { <p class="muted">No asset placements yet.</p> }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -73,6 +73,7 @@
|
|||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<label class="form-label" for="LocaleCode">Locale</label>
|
<label class="form-label" for="LocaleCode">Locale</label>
|
||||||
<select class="form-select" id="LocaleCode" name="LocaleCode">
|
<select class="form-select" id="LocaleCode" name="LocaleCode">
|
||||||
|
<option value="" selected="@string.IsNullOrWhiteSpace(filter.LocaleCode)">All locales</option>
|
||||||
@foreach (var locale in Model.Locales)
|
@foreach (var locale in Model.Locales)
|
||||||
{
|
{
|
||||||
<option value="@locale.LocaleCode" selected="@(string.Equals(filter.LocaleCode, locale.LocaleCode, StringComparison.OrdinalIgnoreCase))">@locale.DisplayName</option>
|
<option value="@locale.LocaleCode" selected="@(string.Equals(filter.LocaleCode, locale.LocaleCode, StringComparison.OrdinalIgnoreCase))">@locale.DisplayName</option>
|
||||||
|
|||||||
@ -67,6 +67,15 @@
|
|||||||
}).catch(() => null);
|
}).catch(() => null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const storyPositionFromWorkspaceStatus = (status) => {
|
||||||
|
const position = status?.currentStoryPosition || status?.CurrentStoryPosition;
|
||||||
|
if (!position) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return window.PlotDirectorStoryPosition?.normalise(position, "WordCompanion") || position;
|
||||||
|
};
|
||||||
|
|
||||||
const readStoredTab = () => {
|
const readStoredTab = () => {
|
||||||
try {
|
try {
|
||||||
return localStorage.getItem(config.storageKey) || config.defaultTab;
|
return localStorage.getItem(config.storageKey) || config.defaultTab;
|
||||||
@ -230,8 +239,17 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
connection.start()
|
connection.start()
|
||||||
.then(() => {
|
.then(async () => {
|
||||||
if (!currentSceneId) {
|
const status = await connection.invoke("WatchWritingWorkspace", {
|
||||||
|
projectID: scopedProjectId,
|
||||||
|
bookID: scopedBookId
|
||||||
|
}).catch(() => null);
|
||||||
|
const position = storyPositionFromWorkspaceStatus(status);
|
||||||
|
if (position?.sceneId || (config.allowChapterOnly && position?.chapterId)) {
|
||||||
|
await loadPosition(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentSceneId && !(config.allowChapterOnly && currentChapterId)) {
|
||||||
setStatus(config.waitingStatus);
|
setStatus(config.waitingStatus);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user