Phase 16I - Review and Accept Word Companion Character Suggestions
This commit is contained in:
parent
f42a65b04a
commit
429cd5beb5
@ -243,6 +243,38 @@ public sealed class ScenesController(
|
||||
return RedirectForScene(sceneId, projectId, bookId);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> AcceptCharacterSuggestion(int id, int sceneId, int? projectId, int? bookId)
|
||||
{
|
||||
try
|
||||
{
|
||||
await characters.AcceptSceneCharacterSuggestionAsync(id);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
TempData["SceneCharacterSuggestionError"] = ex.Message;
|
||||
}
|
||||
|
||||
return RedirectForScene(sceneId, projectId, bookId);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> RejectCharacterSuggestion(int id, int sceneId, int? projectId, int? bookId)
|
||||
{
|
||||
try
|
||||
{
|
||||
await characters.RejectSceneCharacterSuggestionAsync(id);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
TempData["SceneCharacterSuggestionError"] = ex.Message;
|
||||
}
|
||||
|
||||
return RedirectForScene(sceneId, projectId, bookId);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> AddSceneDependency(SceneDependencyCreateViewModel model)
|
||||
|
||||
@ -214,6 +214,10 @@ public interface ICharacterRepository
|
||||
Task<IReadOnlyList<SceneCharacter>> ListSceneCharactersAsync(int sceneId);
|
||||
Task<int> SaveSceneCharacterAsync(SceneCharacter sceneCharacter);
|
||||
Task DeleteSceneCharacterAsync(int sceneCharacterId);
|
||||
Task<IReadOnlyList<SceneCharacterSuggestion>> ListSceneCharacterSuggestionsAsync(int sceneId, int userId);
|
||||
Task<SceneCharacterSuggestionReviewResult?> AcceptSceneCharacterSuggestionAsync(int suggestionId, int userId);
|
||||
Task<SceneCharacterSuggestionReviewResult?> RejectSceneCharacterSuggestionAsync(int suggestionId, int userId);
|
||||
Task<IReadOnlyList<SceneCharacterSuggestionCount>> CountSceneCharacterSuggestionsByChapterAsync(int chapterId, int userId);
|
||||
Task<IReadOnlyList<CharacterAttributeEvent>> ListAttributeEventsBySceneAsync(int sceneId);
|
||||
Task<IReadOnlyList<CharacterAttributeEvent>> ListAttributeEventsByCharacterAsync(int characterId);
|
||||
Task<int> SaveAttributeEventAsync(CharacterAttributeEvent attributeEvent);
|
||||
@ -3571,6 +3575,44 @@ public sealed class CharacterRepository(ISqlConnectionFactory connectionFactory)
|
||||
await connection.ExecuteAsync("dbo.SceneCharacter_Delete", new { SceneCharacterID = sceneCharacterId }, commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<SceneCharacterSuggestion>> ListSceneCharacterSuggestionsAsync(int sceneId, int userId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
var rows = await connection.QueryAsync<SceneCharacterSuggestion>(
|
||||
"dbo.SceneCharacterSuggestions_ListByScene",
|
||||
new { SceneID = sceneId, UserID = userId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
return rows.ToList();
|
||||
}
|
||||
|
||||
public async Task<SceneCharacterSuggestionReviewResult?> AcceptSceneCharacterSuggestionAsync(int suggestionId, int userId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleOrDefaultAsync<SceneCharacterSuggestionReviewResult>(
|
||||
"dbo.SceneCharacterSuggestion_Accept",
|
||||
new { SceneCharacterSuggestionID = suggestionId, UserID = userId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<SceneCharacterSuggestionReviewResult?> RejectSceneCharacterSuggestionAsync(int suggestionId, int userId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleOrDefaultAsync<SceneCharacterSuggestionReviewResult>(
|
||||
"dbo.SceneCharacterSuggestion_Reject",
|
||||
new { SceneCharacterSuggestionID = suggestionId, UserID = userId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<SceneCharacterSuggestionCount>> CountSceneCharacterSuggestionsByChapterAsync(int chapterId, int userId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
var rows = await connection.QueryAsync<SceneCharacterSuggestionCount>(
|
||||
"dbo.SceneCharacterSuggestions_CountByChapter",
|
||||
new { ChapterID = chapterId, UserID = userId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
return rows.ToList();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<CharacterAttributeEvent>> ListAttributeEventsByCharacterAsync(int characterId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
|
||||
@ -401,6 +401,7 @@ public sealed class Scene
|
||||
public List<SceneMetricValue> Metrics { get; set; } = [];
|
||||
public int WarningCount { get; set; }
|
||||
public int DependencyCount { get; set; }
|
||||
public int PendingCharacterSuggestionCount { get; set; }
|
||||
|
||||
public string TimeLabel
|
||||
{
|
||||
@ -981,6 +982,33 @@ public sealed class SceneCharacter
|
||||
public DateTime UpdatedDate { get; set; }
|
||||
}
|
||||
|
||||
public sealed class SceneCharacterSuggestion
|
||||
{
|
||||
public int SceneCharacterSuggestionID { get; set; }
|
||||
public int SceneID { get; set; }
|
||||
public int CharacterID { get; set; }
|
||||
public string CharacterName { get; set; } = string.Empty;
|
||||
public string Status { get; set; } = string.Empty;
|
||||
public string Confidence { get; set; } = string.Empty;
|
||||
public string? Reason { get; set; }
|
||||
public DateTime DetectedUtc { get; set; }
|
||||
}
|
||||
|
||||
public sealed class SceneCharacterSuggestionCount
|
||||
{
|
||||
public int SceneID { get; set; }
|
||||
public int PendingSuggestionCount { get; set; }
|
||||
}
|
||||
|
||||
public sealed class SceneCharacterSuggestionReviewResult
|
||||
{
|
||||
public int SceneCharacterSuggestionID { get; set; }
|
||||
public int SceneID { get; set; }
|
||||
public int CharacterID { get; set; }
|
||||
public string Status { get; set; } = string.Empty;
|
||||
public bool SceneCharacterCreated { get; set; }
|
||||
}
|
||||
|
||||
public sealed class CharacterAttributeType
|
||||
{
|
||||
public int CharacterAttributeTypeID { get; set; }
|
||||
|
||||
@ -251,6 +251,8 @@ public interface ICharacterService
|
||||
Task<int> AddSceneCharacterAsync(SceneCharacterCreateViewModel model);
|
||||
Task<int> UpdateSceneCharacterAsync(SceneCharacterEditViewModel model);
|
||||
Task DeleteSceneCharacterAsync(int sceneCharacterId);
|
||||
Task AcceptSceneCharacterSuggestionAsync(int suggestionId);
|
||||
Task RejectSceneCharacterSuggestionAsync(int suggestionId);
|
||||
Task<int> AddAttributeEventAsync(CharacterAttributeEventCreateViewModel model);
|
||||
Task<int> AddKnowledgeAsync(CharacterKnowledgeCreateViewModel model);
|
||||
Task DeleteKnowledgeAsync(int characterKnowledgeId);
|
||||
@ -1287,6 +1289,7 @@ public sealed class ChapterService(
|
||||
IBookRepository books,
|
||||
IChapterRepository chapters,
|
||||
ISceneRepository scenes,
|
||||
ICharacterRepository characters,
|
||||
ILookupRepository lookups,
|
||||
ISubscriptionService subscriptions,
|
||||
IProjectActivityService activity,
|
||||
@ -1307,12 +1310,24 @@ public sealed class ChapterService(
|
||||
return null;
|
||||
}
|
||||
|
||||
var sceneRows = await scenes.ListByChapterAsync(chapterId);
|
||||
if (currentUser.UserId is int userId)
|
||||
{
|
||||
var suggestionCounts = await characters.CountSceneCharacterSuggestionsByChapterAsync(chapterId, userId);
|
||||
var countsByScene = suggestionCounts.ToDictionary(x => x.SceneID, x => x.PendingSuggestionCount);
|
||||
foreach (var scene in sceneRows)
|
||||
{
|
||||
scene.PendingCharacterSuggestionCount = countsByScene.GetValueOrDefault(scene.SceneID);
|
||||
}
|
||||
}
|
||||
|
||||
return new ChapterDetailViewModel
|
||||
{
|
||||
Project = project,
|
||||
Book = book,
|
||||
Chapter = chapter,
|
||||
Scenes = await scenes.ListByChapterAsync(chapterId)
|
||||
Scenes = sceneRows,
|
||||
PendingCharacterSuggestionCount = sceneRows.Sum(x => x.PendingCharacterSuggestionCount)
|
||||
};
|
||||
}
|
||||
|
||||
@ -1635,6 +1650,7 @@ public sealed class SceneService(
|
||||
model.AssetCustodyEvents = model.SceneID == 0 ? [] : await assets.ListCustodyBySceneAsync(model.SceneID);
|
||||
model.SceneAssetLocations = model.SceneID == 0 ? [] : await locations.ListSceneAssetLocationsAsync(model.SceneID);
|
||||
model.SceneCharacters = model.SceneID == 0 ? [] : await characters.ListSceneCharactersAsync(model.SceneID);
|
||||
model.CharacterSuggestions = model.SceneID == 0 || currentUser.UserId is not int userId ? [] : await characters.ListSceneCharacterSuggestionsAsync(model.SceneID, userId);
|
||||
model.CharacterAttributeEvents = model.SceneID == 0 ? [] : await characters.ListAttributeEventsBySceneAsync(model.SceneID);
|
||||
model.Warnings = model.SceneID == 0 ? [] : await warnings.ListBySceneAsync(model.SceneID);
|
||||
model.AgeContinuityWarnings = await BuildAgeContinuityWarningsAsync(model);
|
||||
@ -9374,6 +9390,34 @@ public sealed class CharacterService(
|
||||
|
||||
public Task DeleteSceneCharacterAsync(int sceneCharacterId) => characters.DeleteSceneCharacterAsync(sceneCharacterId);
|
||||
|
||||
public async Task AcceptSceneCharacterSuggestionAsync(int suggestionId)
|
||||
{
|
||||
if (currentUser.UserId is not int userId)
|
||||
{
|
||||
throw new InvalidOperationException("Sign in to review character suggestions.");
|
||||
}
|
||||
|
||||
var result = await characters.AcceptSceneCharacterSuggestionAsync(suggestionId, userId);
|
||||
if (result is null)
|
||||
{
|
||||
throw new InvalidOperationException("That character suggestion is no longer available.");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RejectSceneCharacterSuggestionAsync(int suggestionId)
|
||||
{
|
||||
if (currentUser.UserId is not int userId)
|
||||
{
|
||||
throw new InvalidOperationException("Sign in to review character suggestions.");
|
||||
}
|
||||
|
||||
var result = await characters.RejectSceneCharacterSuggestionAsync(suggestionId, userId);
|
||||
if (result is null)
|
||||
{
|
||||
throw new InvalidOperationException("That character suggestion is no longer available.");
|
||||
}
|
||||
}
|
||||
|
||||
public Task<int> AddAttributeEventAsync(CharacterAttributeEventCreateViewModel model) => characters.SaveAttributeEventAsync(new CharacterAttributeEvent
|
||||
{
|
||||
CharacterID = model.CharacterID,
|
||||
|
||||
@ -0,0 +1,281 @@
|
||||
SET ANSI_NULLS ON;
|
||||
GO
|
||||
SET QUOTED_IDENTIFIER ON;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.SceneCharacterSuggestions_ListByScene
|
||||
@SceneID int,
|
||||
@UserID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT suggestion.SceneCharacterSuggestionID,
|
||||
suggestion.SceneID,
|
||||
suggestion.CharacterID,
|
||||
ch.CharacterName,
|
||||
suggestion.Status,
|
||||
suggestion.Confidence,
|
||||
suggestion.Reason,
|
||||
suggestion.DetectedUtc
|
||||
FROM dbo.SceneCharacterSuggestions suggestion
|
||||
INNER JOIN dbo.Scenes s ON s.SceneID = suggestion.SceneID
|
||||
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
|
||||
INNER JOIN dbo.Books b ON b.BookID = c.BookID
|
||||
INNER JOIN dbo.Projects p ON p.ProjectID = b.ProjectID
|
||||
INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = p.ProjectID
|
||||
INNER JOIN dbo.Characters ch ON ch.CharacterID = suggestion.CharacterID
|
||||
WHERE suggestion.SceneID = @SceneID
|
||||
AND suggestion.Status = N'Pending'
|
||||
AND s.IsArchived = 0
|
||||
AND c.IsArchived = 0
|
||||
AND b.IsArchived = 0
|
||||
AND p.IsArchived = 0
|
||||
AND ch.IsArchived = 0
|
||||
AND pua.UserID = @UserID
|
||||
AND pua.IsActive = 1
|
||||
ORDER BY
|
||||
CASE suggestion.Confidence WHEN N'High' THEN 0 WHEN N'Medium' THEN 1 ELSE 2 END,
|
||||
ch.CharacterName;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.SceneCharacterSuggestion_Accept
|
||||
@SceneCharacterSuggestionID int,
|
||||
@UserID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
DECLARE @SceneID int;
|
||||
DECLARE @CharacterID int;
|
||||
DECLARE @SceneCharacterCreated bit = 0;
|
||||
|
||||
SELECT @SceneID = suggestion.SceneID,
|
||||
@CharacterID = suggestion.CharacterID
|
||||
FROM dbo.SceneCharacterSuggestions suggestion
|
||||
INNER JOIN dbo.Scenes s ON s.SceneID = suggestion.SceneID
|
||||
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
|
||||
INNER JOIN dbo.Books b ON b.BookID = c.BookID
|
||||
INNER JOIN dbo.Projects p ON p.ProjectID = b.ProjectID
|
||||
INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = p.ProjectID
|
||||
INNER JOIN dbo.Characters ch ON ch.CharacterID = suggestion.CharacterID AND ch.ProjectID = p.ProjectID
|
||||
WHERE suggestion.SceneCharacterSuggestionID = @SceneCharacterSuggestionID
|
||||
AND suggestion.Status = N'Pending'
|
||||
AND s.IsArchived = 0
|
||||
AND c.IsArchived = 0
|
||||
AND b.IsArchived = 0
|
||||
AND p.IsArchived = 0
|
||||
AND ch.IsArchived = 0
|
||||
AND pua.UserID = @UserID
|
||||
AND pua.IsActive = 1;
|
||||
|
||||
IF @SceneID IS NULL OR @CharacterID IS NULL
|
||||
RETURN;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.SceneCharacters WHERE SceneID = @SceneID AND CharacterID = @CharacterID)
|
||||
BEGIN
|
||||
INSERT dbo.SceneCharacters (SceneID, CharacterID)
|
||||
VALUES (@SceneID, @CharacterID);
|
||||
|
||||
SET @SceneCharacterCreated = 1;
|
||||
END;
|
||||
|
||||
UPDATE dbo.SceneCharacterSuggestions
|
||||
SET Status = N'Accepted',
|
||||
ReviewedByUserID = @UserID,
|
||||
ReviewedUtc = SYSUTCDATETIME()
|
||||
WHERE SceneCharacterSuggestionID = @SceneCharacterSuggestionID
|
||||
AND Status = N'Pending';
|
||||
|
||||
SELECT @SceneCharacterSuggestionID AS SceneCharacterSuggestionID,
|
||||
@SceneID AS SceneID,
|
||||
@CharacterID AS CharacterID,
|
||||
N'Accepted' AS Status,
|
||||
@SceneCharacterCreated AS SceneCharacterCreated;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.SceneCharacterSuggestion_Reject
|
||||
@SceneCharacterSuggestionID int,
|
||||
@UserID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
DECLARE @SceneID int;
|
||||
DECLARE @CharacterID int;
|
||||
|
||||
SELECT @SceneID = suggestion.SceneID,
|
||||
@CharacterID = suggestion.CharacterID
|
||||
FROM dbo.SceneCharacterSuggestions suggestion
|
||||
INNER JOIN dbo.Scenes s ON s.SceneID = suggestion.SceneID
|
||||
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
|
||||
INNER JOIN dbo.Books b ON b.BookID = c.BookID
|
||||
INNER JOIN dbo.Projects p ON p.ProjectID = b.ProjectID
|
||||
INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = p.ProjectID
|
||||
INNER JOIN dbo.Characters ch ON ch.CharacterID = suggestion.CharacterID AND ch.ProjectID = p.ProjectID
|
||||
WHERE suggestion.SceneCharacterSuggestionID = @SceneCharacterSuggestionID
|
||||
AND suggestion.Status = N'Pending'
|
||||
AND s.IsArchived = 0
|
||||
AND c.IsArchived = 0
|
||||
AND b.IsArchived = 0
|
||||
AND p.IsArchived = 0
|
||||
AND ch.IsArchived = 0
|
||||
AND pua.UserID = @UserID
|
||||
AND pua.IsActive = 1;
|
||||
|
||||
IF @SceneID IS NULL OR @CharacterID IS NULL
|
||||
RETURN;
|
||||
|
||||
UPDATE dbo.SceneCharacterSuggestions
|
||||
SET Status = N'Rejected',
|
||||
ReviewedByUserID = @UserID,
|
||||
ReviewedUtc = SYSUTCDATETIME()
|
||||
WHERE SceneCharacterSuggestionID = @SceneCharacterSuggestionID
|
||||
AND Status = N'Pending';
|
||||
|
||||
SELECT @SceneCharacterSuggestionID AS SceneCharacterSuggestionID,
|
||||
@SceneID AS SceneID,
|
||||
@CharacterID AS CharacterID,
|
||||
N'Rejected' AS Status,
|
||||
CAST(0 AS bit) AS SceneCharacterCreated;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.SceneCharacterSuggestions_CountByChapter
|
||||
@ChapterID int,
|
||||
@UserID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
IF NOT EXISTS
|
||||
(
|
||||
SELECT 1
|
||||
FROM dbo.Chapters c
|
||||
INNER JOIN dbo.Books b ON b.BookID = c.BookID
|
||||
INNER JOIN dbo.Projects p ON p.ProjectID = b.ProjectID
|
||||
INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = p.ProjectID
|
||||
WHERE c.ChapterID = @ChapterID
|
||||
AND c.IsArchived = 0
|
||||
AND b.IsArchived = 0
|
||||
AND p.IsArchived = 0
|
||||
AND pua.UserID = @UserID
|
||||
AND pua.IsActive = 1
|
||||
)
|
||||
RETURN;
|
||||
|
||||
SELECT s.SceneID,
|
||||
COUNT(suggestion.SceneCharacterSuggestionID) AS PendingSuggestionCount
|
||||
FROM dbo.Scenes s
|
||||
INNER JOIN dbo.SceneCharacterSuggestions suggestion ON suggestion.SceneID = s.SceneID
|
||||
INNER JOIN dbo.Characters ch ON ch.CharacterID = suggestion.CharacterID
|
||||
WHERE s.ChapterID = @ChapterID
|
||||
AND s.IsArchived = 0
|
||||
AND ch.IsArchived = 0
|
||||
AND suggestion.Status = N'Pending'
|
||||
GROUP BY s.SceneID;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.WordCompanion_Runtime_CharacterSuggestions_Create
|
||||
@UserID int,
|
||||
@DocumentGuid uniqueidentifier,
|
||||
@SceneID int,
|
||||
@CharacterIds nvarchar(max)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
IF @DocumentGuid IS NULL
|
||||
OR @DocumentGuid = '00000000-0000-0000-0000-000000000000'
|
||||
OR @SceneID <= 0
|
||||
RETURN;
|
||||
|
||||
DECLARE @ProjectID int;
|
||||
DECLARE @ManuscriptDocumentID int;
|
||||
|
||||
SELECT @ProjectID = b.ProjectID,
|
||||
@ManuscriptDocumentID = md.ManuscriptDocumentID
|
||||
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.Projects p ON p.ProjectID = b.ProjectID
|
||||
INNER JOIN dbo.ManuscriptDocuments md ON md.BookID = b.BookID
|
||||
INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = b.ProjectID
|
||||
WHERE s.SceneID = @SceneID
|
||||
AND s.IsArchived = 0
|
||||
AND c.IsArchived = 0
|
||||
AND b.IsArchived = 0
|
||||
AND p.IsArchived = 0
|
||||
AND md.DocumentGuid = @DocumentGuid
|
||||
AND md.IsActive = 1
|
||||
AND s.IsLinkedToManuscript = 1
|
||||
AND s.ManuscriptDocumentID = md.ManuscriptDocumentID
|
||||
AND pua.UserID = @UserID
|
||||
AND pua.IsActive = 1;
|
||||
|
||||
IF @ProjectID IS NULL
|
||||
RETURN;
|
||||
|
||||
DECLARE @Requested table (CharacterID int NOT NULL PRIMARY KEY);
|
||||
|
||||
INSERT @Requested (CharacterID)
|
||||
SELECT DISTINCT TRY_CONVERT(int, LTRIM(RTRIM(value)))
|
||||
FROM STRING_SPLIT(COALESCE(@CharacterIds, N''), N',')
|
||||
WHERE TRY_CONVERT(int, LTRIM(RTRIM(value))) IS NOT NULL
|
||||
AND TRY_CONVERT(int, LTRIM(RTRIM(value))) > 0;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM @Requested)
|
||||
BEGIN
|
||||
SELECT @SceneID AS SceneId,
|
||||
0 AS CreatedCount,
|
||||
0 AS SkippedCount,
|
||||
N'No character suggestions detected.' AS Message;
|
||||
RETURN;
|
||||
END;
|
||||
|
||||
IF EXISTS
|
||||
(
|
||||
SELECT 1
|
||||
FROM @Requested requested
|
||||
WHERE NOT EXISTS
|
||||
(
|
||||
SELECT 1
|
||||
FROM dbo.Characters ch
|
||||
WHERE ch.CharacterID = requested.CharacterID
|
||||
AND ch.ProjectID = @ProjectID
|
||||
AND ch.IsArchived = 0
|
||||
)
|
||||
)
|
||||
RETURN;
|
||||
|
||||
DECLARE @Inserted table (CharacterID int NOT NULL PRIMARY KEY);
|
||||
|
||||
INSERT dbo.SceneCharacterSuggestions
|
||||
(SceneID, CharacterID, SuggestionSource, Status, Confidence, Reason, CreatedByUserID)
|
||||
OUTPUT inserted.CharacterID INTO @Inserted
|
||||
SELECT @SceneID,
|
||||
requested.CharacterID,
|
||||
N'Word Companion',
|
||||
N'Pending',
|
||||
N'High',
|
||||
N'Detected in manuscript text',
|
||||
@UserID
|
||||
FROM @Requested requested
|
||||
WHERE NOT EXISTS
|
||||
(SELECT 1 FROM dbo.SceneCharacters sc WHERE sc.SceneID = @SceneID AND sc.CharacterID = requested.CharacterID)
|
||||
AND NOT EXISTS
|
||||
(SELECT 1 FROM dbo.SceneCharacterSuggestions suggestion WHERE suggestion.SceneID = @SceneID AND suggestion.CharacterID = requested.CharacterID);
|
||||
|
||||
SELECT @SceneID AS SceneId,
|
||||
(SELECT COUNT(*) FROM @Inserted) AS CreatedCount,
|
||||
(SELECT COUNT(*) FROM @Requested) - (SELECT COUNT(*) FROM @Inserted) AS SkippedCount,
|
||||
CASE
|
||||
WHEN (SELECT COUNT(*) FROM @Inserted) = 1 THEN N'1 character suggestion detected.'
|
||||
WHEN (SELECT COUNT(*) FROM @Inserted) > 1 THEN CONCAT((SELECT COUNT(*) FROM @Inserted), N' character suggestions detected.')
|
||||
ELSE N'No new character suggestions.'
|
||||
END AS Message;
|
||||
END;
|
||||
GO
|
||||
@ -266,6 +266,7 @@ public sealed class ChapterDetailViewModel
|
||||
public Book Book { get; set; } = new();
|
||||
public Chapter Chapter { get; set; } = new();
|
||||
public IReadOnlyList<Scene> Scenes { get; set; } = [];
|
||||
public int PendingCharacterSuggestionCount { get; set; }
|
||||
}
|
||||
|
||||
public sealed class SceneEditViewModel
|
||||
@ -338,6 +339,7 @@ public sealed class SceneEditViewModel
|
||||
public AssetCustodyCreateViewModel NewAssetCustodyEvent { get; set; } = new();
|
||||
public SceneAssetLocationCreateViewModel NewSceneAssetLocation { get; set; } = new();
|
||||
public IReadOnlyList<SceneCharacter> SceneCharacters { get; set; } = [];
|
||||
public IReadOnlyList<SceneCharacterSuggestion> CharacterSuggestions { get; set; } = [];
|
||||
public IReadOnlyList<CharacterAttributeEvent> CharacterAttributeEvents { get; set; } = [];
|
||||
public IReadOnlyList<CharacterKnowledgeItem> CharacterKnowledge { get; set; } = [];
|
||||
public IReadOnlyList<RelationshipEvent> RelationshipEvents { get; set; } = [];
|
||||
|
||||
@ -51,6 +51,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (Model.PendingCharacterSuggestionCount > 0)
|
||||
{
|
||||
<section class="list-section word-companion-suggestion-summary">
|
||||
<h2>Word Companion Suggestions</h2>
|
||||
<p class="muted">@Model.PendingCharacterSuggestionCount character suggestion@(Model.PendingCharacterSuggestionCount == 1 ? "" : "s") awaiting review in this chapter.</p>
|
||||
</section>
|
||||
}
|
||||
|
||||
<section class="list-section">
|
||||
<h2>Scenes <help-icon key="chapters.scenes" /></h2>
|
||||
@if (!Model.Scenes.Any())
|
||||
@ -78,7 +86,13 @@
|
||||
{
|
||||
<tr>
|
||||
<td class="narrow">@scene.SceneNumber</td>
|
||||
<td><a asp-controller="Scenes" asp-action="Edit" asp-route-id="@scene.SceneID">@scene.SceneTitle</a></td>
|
||||
<td>
|
||||
<a asp-controller="Scenes" asp-action="Edit" asp-route-id="@scene.SceneID">@scene.SceneTitle</a>
|
||||
@if (scene.PendingCharacterSuggestionCount > 0)
|
||||
{
|
||||
<span class="status-pill word-companion-suggestion-pill">@scene.PendingCharacterSuggestionCount suggestion@(scene.PendingCharacterSuggestionCount == 1 ? "" : "s")</span>
|
||||
}
|
||||
</td>
|
||||
<td><span class="status-pill">@scene.RevisionStatusName</span></td>
|
||||
<td><span class="status-pill">@(scene.IsLinkedToManuscript ? "🔗 Linked" : "📝 Planned")</span></td>
|
||||
<td>@scene.TimeLabel</td>
|
||||
|
||||
@ -361,6 +361,48 @@
|
||||
<!-- Characters -->
|
||||
<section class="inspector-section character-section" data-section-title="Characters" data-section-id="inspector-characters">
|
||||
<h3>Characters <help-icon key="sceneInspector.characters" /></h3>
|
||||
@if (Model.CharacterSuggestions.Any())
|
||||
{
|
||||
<div class="word-companion-suggestion-panel">
|
||||
<h4 class="inspector-subheading">Word Companion Suggestions</h4>
|
||||
@if (TempData["SceneCharacterSuggestionError"] is string suggestionError)
|
||||
{
|
||||
<p class="text-danger form-helper-message">@suggestionError</p>
|
||||
}
|
||||
<div class="word-companion-suggestion-list">
|
||||
@foreach (var suggestion in Model.CharacterSuggestions)
|
||||
{
|
||||
<article class="word-companion-suggestion-item">
|
||||
<div>
|
||||
<strong>@suggestion.CharacterName</strong>
|
||||
<span class="muted">@suggestion.Confidence confidence</span>
|
||||
@if (!string.IsNullOrWhiteSpace(suggestion.Reason))
|
||||
{
|
||||
<span class="muted">@suggestion.Reason</span>
|
||||
}
|
||||
<span class="muted">Detected @suggestion.DetectedUtc.ToLocalTime().ToString("d MMM yyyy HH:mm")</span>
|
||||
</div>
|
||||
<div class="character-appearance-actions">
|
||||
<form asp-controller="Scenes" asp-action="AcceptCharacterSuggestion" method="post">
|
||||
<input type="hidden" name="id" value="@suggestion.SceneCharacterSuggestionID" />
|
||||
<input type="hidden" name="sceneId" value="@Model.SceneID" />
|
||||
<input type="hidden" name="projectId" value="@Model.ReturnProjectID" />
|
||||
<input type="hidden" name="bookId" value="@Model.ReturnBookID" />
|
||||
<button class="btn btn-outline-primary btn-sm" type="submit">Accept</button>
|
||||
</form>
|
||||
<form asp-controller="Scenes" asp-action="RejectCharacterSuggestion" method="post">
|
||||
<input type="hidden" name="id" value="@suggestion.SceneCharacterSuggestionID" />
|
||||
<input type="hidden" name="sceneId" value="@Model.SceneID" />
|
||||
<input type="hidden" name="projectId" value="@Model.ReturnProjectID" />
|
||||
<input type="hidden" name="bookId" value="@Model.ReturnBookID" />
|
||||
<button class="btn btn-outline-secondary btn-sm" type="submit">Reject</button>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (!Model.SceneCharacters.Any())
|
||||
{
|
||||
<p class="muted">No characters attached to this scene yet. Add the people who appear, are mentioned, or meaningfully affect the scene.</p>
|
||||
|
||||
@ -1704,6 +1704,42 @@ body.timeline-inspector-resizing {
|
||||
border-top: 1px solid var(--plotline-line);
|
||||
}
|
||||
|
||||
.word-companion-suggestion-panel {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
border: 1px solid rgba(138, 111, 61, 0.24);
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
background: rgba(255, 248, 232, 0.72);
|
||||
}
|
||||
|
||||
.word-companion-suggestion-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.word-companion-suggestion-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
border: 1px solid #ded9ea;
|
||||
border-left: 4px solid #8a6f3d;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.word-companion-suggestion-item > div:first-child {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.word-companion-suggestion-pill {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.form-helper-message {
|
||||
margin: 0 0 8px;
|
||||
font-size: 0.86rem;
|
||||
|
||||
2
PlotLine/wwwroot/css/site.min.css
vendored
2
PlotLine/wwwroot/css/site.min.css
vendored
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user