Phase 16L - Review and Accept Word Companion Asset Suggestions
This commit is contained in:
parent
c8bae2d1d1
commit
c8ae73a954
@ -275,6 +275,38 @@ public sealed class ScenesController(
|
|||||||
return RedirectForScene(sceneId, projectId, bookId);
|
return RedirectForScene(sceneId, projectId, bookId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> AcceptAssetSuggestion(int id, int sceneId, int? projectId, int? bookId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await assets.AcceptSceneAssetSuggestionAsync(id);
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
TempData["SceneAssetSuggestionError"] = ex.Message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return RedirectForScene(sceneId, projectId, bookId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> RejectAssetSuggestion(int id, int sceneId, int? projectId, int? bookId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await assets.RejectSceneAssetSuggestionAsync(id);
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
TempData["SceneAssetSuggestionError"] = ex.Message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return RedirectForScene(sceneId, projectId, bookId);
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[ValidateAntiForgeryToken]
|
[ValidateAntiForgeryToken]
|
||||||
public async Task<IActionResult> AddSceneDependency(SceneDependencyCreateViewModel model)
|
public async Task<IActionResult> AddSceneDependency(SceneDependencyCreateViewModel model)
|
||||||
|
|||||||
@ -179,6 +179,10 @@ public interface IAssetRepository
|
|||||||
Task<IReadOnlyList<AssetEvent>> ListEventsByAssetAsync(int storyAssetId);
|
Task<IReadOnlyList<AssetEvent>> ListEventsByAssetAsync(int storyAssetId);
|
||||||
Task<int> SaveEventAsync(AssetEvent assetEvent);
|
Task<int> SaveEventAsync(AssetEvent assetEvent);
|
||||||
Task DeleteEventAsync(int assetEventId);
|
Task DeleteEventAsync(int assetEventId);
|
||||||
|
Task<IReadOnlyList<SceneAssetSuggestion>> ListSceneAssetSuggestionsAsync(int sceneId, int userId);
|
||||||
|
Task<SceneAssetSuggestionReviewResult?> AcceptSceneAssetSuggestionAsync(int suggestionId, int userId);
|
||||||
|
Task<SceneAssetSuggestionReviewResult?> RejectSceneAssetSuggestionAsync(int suggestionId, int userId);
|
||||||
|
Task<IReadOnlyList<SceneAssetSuggestionCount>> CountSceneAssetSuggestionsByChapterAsync(int chapterId, int userId);
|
||||||
Task<IReadOnlyList<AssetDependency>> ListDependenciesByAssetAsync(int storyAssetId);
|
Task<IReadOnlyList<AssetDependency>> ListDependenciesByAssetAsync(int storyAssetId);
|
||||||
Task<int> SaveDependencyAsync(AssetDependency dependency);
|
Task<int> SaveDependencyAsync(AssetDependency dependency);
|
||||||
Task DeleteDependencyAsync(int assetDependencyId);
|
Task DeleteDependencyAsync(int assetDependencyId);
|
||||||
@ -3201,6 +3205,44 @@ public sealed class AssetRepository(ISqlConnectionFactory connectionFactory) : I
|
|||||||
await connection.ExecuteAsync("dbo.AssetEvent_Delete", new { AssetEventID = assetEventId }, commandType: CommandType.StoredProcedure);
|
await connection.ExecuteAsync("dbo.AssetEvent_Delete", new { AssetEventID = assetEventId }, commandType: CommandType.StoredProcedure);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<SceneAssetSuggestion>> ListSceneAssetSuggestionsAsync(int sceneId, int userId)
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
var rows = await connection.QueryAsync<SceneAssetSuggestion>(
|
||||||
|
"dbo.SceneAssetSuggestions_ListByScene",
|
||||||
|
new { SceneID = sceneId, UserID = userId },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
return rows.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SceneAssetSuggestionReviewResult?> AcceptSceneAssetSuggestionAsync(int suggestionId, int userId)
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
return await connection.QuerySingleOrDefaultAsync<SceneAssetSuggestionReviewResult>(
|
||||||
|
"dbo.SceneAssetSuggestion_Accept",
|
||||||
|
new { SceneAssetSuggestionID = suggestionId, UserID = userId },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SceneAssetSuggestionReviewResult?> RejectSceneAssetSuggestionAsync(int suggestionId, int userId)
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
return await connection.QuerySingleOrDefaultAsync<SceneAssetSuggestionReviewResult>(
|
||||||
|
"dbo.SceneAssetSuggestion_Reject",
|
||||||
|
new { SceneAssetSuggestionID = suggestionId, UserID = userId },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<SceneAssetSuggestionCount>> CountSceneAssetSuggestionsByChapterAsync(int chapterId, int userId)
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
var rows = await connection.QueryAsync<SceneAssetSuggestionCount>(
|
||||||
|
"dbo.SceneAssetSuggestions_CountByChapter",
|
||||||
|
new { ChapterID = chapterId, UserID = userId },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
return rows.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<IReadOnlyList<AssetDependency>> ListDependenciesByAssetAsync(int storyAssetId)
|
public async Task<IReadOnlyList<AssetDependency>> ListDependenciesByAssetAsync(int storyAssetId)
|
||||||
{
|
{
|
||||||
using var connection = connectionFactory.CreateConnection();
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
|||||||
@ -402,6 +402,7 @@ public sealed class Scene
|
|||||||
public int WarningCount { get; set; }
|
public int WarningCount { get; set; }
|
||||||
public int DependencyCount { get; set; }
|
public int DependencyCount { get; set; }
|
||||||
public int PendingCharacterSuggestionCount { get; set; }
|
public int PendingCharacterSuggestionCount { get; set; }
|
||||||
|
public int PendingAssetSuggestionCount { get; set; }
|
||||||
|
|
||||||
public string TimeLabel
|
public string TimeLabel
|
||||||
{
|
{
|
||||||
@ -1009,6 +1010,33 @@ public sealed class SceneCharacterSuggestionReviewResult
|
|||||||
public bool SceneCharacterCreated { get; set; }
|
public bool SceneCharacterCreated { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class SceneAssetSuggestion
|
||||||
|
{
|
||||||
|
public int SceneAssetSuggestionID { get; set; }
|
||||||
|
public int SceneID { get; set; }
|
||||||
|
public int AssetID { get; set; }
|
||||||
|
public string AssetName { 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 SceneAssetSuggestionCount
|
||||||
|
{
|
||||||
|
public int SceneID { get; set; }
|
||||||
|
public int PendingSuggestionCount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class SceneAssetSuggestionReviewResult
|
||||||
|
{
|
||||||
|
public int SceneAssetSuggestionID { get; set; }
|
||||||
|
public int SceneID { get; set; }
|
||||||
|
public int AssetID { get; set; }
|
||||||
|
public string Status { get; set; } = string.Empty;
|
||||||
|
public bool SceneAssetCreated { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class CharacterAttributeType
|
public sealed class CharacterAttributeType
|
||||||
{
|
{
|
||||||
public int CharacterAttributeTypeID { get; set; }
|
public int CharacterAttributeTypeID { get; set; }
|
||||||
|
|||||||
@ -174,6 +174,8 @@ public interface IAssetService
|
|||||||
Task ArchiveAssetAsync(int storyAssetId);
|
Task ArchiveAssetAsync(int storyAssetId);
|
||||||
Task<int> AddAssetEventAsync(AssetEventCreateViewModel model);
|
Task<int> AddAssetEventAsync(AssetEventCreateViewModel model);
|
||||||
Task DeleteAssetEventAsync(int assetEventId);
|
Task DeleteAssetEventAsync(int assetEventId);
|
||||||
|
Task AcceptSceneAssetSuggestionAsync(int suggestionId);
|
||||||
|
Task RejectSceneAssetSuggestionAsync(int suggestionId);
|
||||||
Task<int> AddCustodyEventAsync(AssetCustodyCreateViewModel model);
|
Task<int> AddCustodyEventAsync(AssetCustodyCreateViewModel model);
|
||||||
Task DeleteCustodyEventAsync(int assetCustodyEventId);
|
Task DeleteCustodyEventAsync(int assetCustodyEventId);
|
||||||
Task<int> SaveDependencyAsync(AssetDependencyEditViewModel model);
|
Task<int> SaveDependencyAsync(AssetDependencyEditViewModel model);
|
||||||
@ -1290,6 +1292,7 @@ public sealed class ChapterService(
|
|||||||
IChapterRepository chapters,
|
IChapterRepository chapters,
|
||||||
ISceneRepository scenes,
|
ISceneRepository scenes,
|
||||||
ICharacterRepository characters,
|
ICharacterRepository characters,
|
||||||
|
IAssetRepository assets,
|
||||||
ILookupRepository lookups,
|
ILookupRepository lookups,
|
||||||
ISubscriptionService subscriptions,
|
ISubscriptionService subscriptions,
|
||||||
IProjectActivityService activity,
|
IProjectActivityService activity,
|
||||||
@ -1315,9 +1318,12 @@ public sealed class ChapterService(
|
|||||||
{
|
{
|
||||||
var suggestionCounts = await characters.CountSceneCharacterSuggestionsByChapterAsync(chapterId, userId);
|
var suggestionCounts = await characters.CountSceneCharacterSuggestionsByChapterAsync(chapterId, userId);
|
||||||
var countsByScene = suggestionCounts.ToDictionary(x => x.SceneID, x => x.PendingSuggestionCount);
|
var countsByScene = suggestionCounts.ToDictionary(x => x.SceneID, x => x.PendingSuggestionCount);
|
||||||
|
var assetSuggestionCounts = await assets.CountSceneAssetSuggestionsByChapterAsync(chapterId, userId);
|
||||||
|
var assetCountsByScene = assetSuggestionCounts.ToDictionary(x => x.SceneID, x => x.PendingSuggestionCount);
|
||||||
foreach (var scene in sceneRows)
|
foreach (var scene in sceneRows)
|
||||||
{
|
{
|
||||||
scene.PendingCharacterSuggestionCount = countsByScene.GetValueOrDefault(scene.SceneID);
|
scene.PendingCharacterSuggestionCount = countsByScene.GetValueOrDefault(scene.SceneID);
|
||||||
|
scene.PendingAssetSuggestionCount = assetCountsByScene.GetValueOrDefault(scene.SceneID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1327,7 +1333,8 @@ public sealed class ChapterService(
|
|||||||
Book = book,
|
Book = book,
|
||||||
Chapter = chapter,
|
Chapter = chapter,
|
||||||
Scenes = sceneRows,
|
Scenes = sceneRows,
|
||||||
PendingCharacterSuggestionCount = sceneRows.Sum(x => x.PendingCharacterSuggestionCount)
|
PendingCharacterSuggestionCount = sceneRows.Sum(x => x.PendingCharacterSuggestionCount),
|
||||||
|
PendingAssetSuggestionCount = sceneRows.Sum(x => x.PendingAssetSuggestionCount)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1651,6 +1658,7 @@ public sealed class SceneService(
|
|||||||
model.SceneAssetLocations = model.SceneID == 0 ? [] : await locations.ListSceneAssetLocationsAsync(model.SceneID);
|
model.SceneAssetLocations = model.SceneID == 0 ? [] : await locations.ListSceneAssetLocationsAsync(model.SceneID);
|
||||||
model.SceneCharacters = model.SceneID == 0 ? [] : await characters.ListSceneCharactersAsync(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.CharacterSuggestions = model.SceneID == 0 || currentUser.UserId is not int userId ? [] : await characters.ListSceneCharacterSuggestionsAsync(model.SceneID, userId);
|
||||||
|
model.AssetSuggestions = model.SceneID == 0 || currentUser.UserId is not int assetUserId ? [] : await assets.ListSceneAssetSuggestionsAsync(model.SceneID, assetUserId);
|
||||||
model.CharacterAttributeEvents = model.SceneID == 0 ? [] : await characters.ListAttributeEventsBySceneAsync(model.SceneID);
|
model.CharacterAttributeEvents = model.SceneID == 0 ? [] : await characters.ListAttributeEventsBySceneAsync(model.SceneID);
|
||||||
model.Warnings = model.SceneID == 0 ? [] : await warnings.ListBySceneAsync(model.SceneID);
|
model.Warnings = model.SceneID == 0 ? [] : await warnings.ListBySceneAsync(model.SceneID);
|
||||||
model.AgeContinuityWarnings = await BuildAgeContinuityWarningsAsync(model);
|
model.AgeContinuityWarnings = await BuildAgeContinuityWarningsAsync(model);
|
||||||
@ -7459,7 +7467,8 @@ public sealed class AssetService(
|
|||||||
IAssetRepository assets,
|
IAssetRepository assets,
|
||||||
ILocationRepository locations,
|
ILocationRepository locations,
|
||||||
IProjectActivityService activity,
|
IProjectActivityService activity,
|
||||||
IVisualIdentityImageService visualIdentityImages) : IAssetService
|
IVisualIdentityImageService visualIdentityImages,
|
||||||
|
ICurrentUserService currentUser) : IAssetService
|
||||||
{
|
{
|
||||||
public async Task<StoryAssetListViewModel?> GetAssetsAsync(int projectId)
|
public async Task<StoryAssetListViewModel?> GetAssetsAsync(int projectId)
|
||||||
{
|
{
|
||||||
@ -7663,6 +7672,34 @@ public sealed class AssetService(
|
|||||||
|
|
||||||
public Task DeleteAssetEventAsync(int assetEventId) => assets.DeleteEventAsync(assetEventId);
|
public Task DeleteAssetEventAsync(int assetEventId) => assets.DeleteEventAsync(assetEventId);
|
||||||
|
|
||||||
|
public async Task AcceptSceneAssetSuggestionAsync(int suggestionId)
|
||||||
|
{
|
||||||
|
if (currentUser.UserId is not int userId)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Sign in to review asset suggestions.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await assets.AcceptSceneAssetSuggestionAsync(suggestionId, userId);
|
||||||
|
if (result is null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("That asset suggestion is no longer available.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RejectSceneAssetSuggestionAsync(int suggestionId)
|
||||||
|
{
|
||||||
|
if (currentUser.UserId is not int userId)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Sign in to review asset suggestions.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await assets.RejectSceneAssetSuggestionAsync(suggestionId, userId);
|
||||||
|
if (result is null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("That asset suggestion is no longer available.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public Task<int> AddCustodyEventAsync(AssetCustodyCreateViewModel model) => assets.SaveCustodyEventAsync(
|
public Task<int> AddCustodyEventAsync(AssetCustodyCreateViewModel model) => assets.SaveCustodyEventAsync(
|
||||||
new AssetCustodyEvent
|
new AssetCustodyEvent
|
||||||
{
|
{
|
||||||
|
|||||||
198
PlotLine/Sql/104_Phase16L_WordCompanionAssetSuggestionReview.sql
Normal file
198
PlotLine/Sql/104_Phase16L_WordCompanionAssetSuggestionReview.sql
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
SET ANSI_NULLS ON;
|
||||||
|
GO
|
||||||
|
SET QUOTED_IDENTIFIER ON;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.SceneAssetSuggestions_ListByScene
|
||||||
|
@SceneID int,
|
||||||
|
@UserID int
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
SELECT suggestion.SceneAssetSuggestionID,
|
||||||
|
suggestion.SceneID,
|
||||||
|
suggestion.AssetID,
|
||||||
|
sa.AssetName,
|
||||||
|
suggestion.Status,
|
||||||
|
suggestion.Confidence,
|
||||||
|
suggestion.Reason,
|
||||||
|
suggestion.DetectedUtc
|
||||||
|
FROM dbo.SceneAssetSuggestions 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.StoryAssets sa ON sa.StoryAssetID = suggestion.AssetID
|
||||||
|
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 sa.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,
|
||||||
|
sa.AssetName;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.SceneAssetSuggestion_Accept
|
||||||
|
@SceneAssetSuggestionID int,
|
||||||
|
@UserID int
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
DECLARE @SceneID int;
|
||||||
|
DECLARE @AssetID int;
|
||||||
|
DECLARE @AssetEventTypeID int;
|
||||||
|
DECLARE @SceneAssetCreated bit = 0;
|
||||||
|
|
||||||
|
SELECT @SceneID = suggestion.SceneID,
|
||||||
|
@AssetID = suggestion.AssetID
|
||||||
|
FROM dbo.SceneAssetSuggestions 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.StoryAssets sa ON sa.StoryAssetID = suggestion.AssetID AND sa.ProjectID = p.ProjectID
|
||||||
|
WHERE suggestion.SceneAssetSuggestionID = @SceneAssetSuggestionID
|
||||||
|
AND suggestion.Status = N'Pending'
|
||||||
|
AND s.IsArchived = 0
|
||||||
|
AND c.IsArchived = 0
|
||||||
|
AND b.IsArchived = 0
|
||||||
|
AND p.IsArchived = 0
|
||||||
|
AND sa.IsArchived = 0
|
||||||
|
AND pua.UserID = @UserID
|
||||||
|
AND pua.IsActive = 1;
|
||||||
|
|
||||||
|
IF @SceneID IS NULL OR @AssetID IS NULL
|
||||||
|
RETURN;
|
||||||
|
|
||||||
|
SELECT TOP (1) @AssetEventTypeID = AssetEventTypeID
|
||||||
|
FROM dbo.AssetEventTypes
|
||||||
|
WHERE TypeName = N'Used'
|
||||||
|
AND IsActive = 1
|
||||||
|
ORDER BY SortOrder, AssetEventTypeID;
|
||||||
|
|
||||||
|
IF @AssetEventTypeID IS NULL
|
||||||
|
SELECT TOP (1) @AssetEventTypeID = AssetEventTypeID
|
||||||
|
FROM dbo.AssetEventTypes
|
||||||
|
WHERE IsActive = 1
|
||||||
|
ORDER BY SortOrder, AssetEventTypeID;
|
||||||
|
|
||||||
|
IF @AssetEventTypeID IS NULL
|
||||||
|
RETURN;
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM dbo.AssetEvents WHERE SceneID = @SceneID AND StoryAssetID = @AssetID)
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM dbo.SceneAssetLocations WHERE SceneID = @SceneID AND StoryAssetID = @AssetID)
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM dbo.AssetCustodyEvents WHERE SceneID = @SceneID AND StoryAssetID = @AssetID)
|
||||||
|
BEGIN
|
||||||
|
INSERT dbo.AssetEvents (StoryAssetID, SceneID, AssetEventTypeID, FromStateID, ToStateID, EventTitle, EventDescription)
|
||||||
|
VALUES (@AssetID, @SceneID, @AssetEventTypeID, NULL, NULL, N'Used', NULL);
|
||||||
|
|
||||||
|
SET @SceneAssetCreated = 1;
|
||||||
|
END;
|
||||||
|
|
||||||
|
UPDATE dbo.SceneAssetSuggestions
|
||||||
|
SET Status = N'Accepted',
|
||||||
|
ReviewedByUserID = @UserID,
|
||||||
|
ReviewedUtc = SYSUTCDATETIME()
|
||||||
|
WHERE SceneAssetSuggestionID = @SceneAssetSuggestionID
|
||||||
|
AND Status = N'Pending';
|
||||||
|
|
||||||
|
SELECT @SceneAssetSuggestionID AS SceneAssetSuggestionID,
|
||||||
|
@SceneID AS SceneID,
|
||||||
|
@AssetID AS AssetID,
|
||||||
|
N'Accepted' AS Status,
|
||||||
|
@SceneAssetCreated AS SceneAssetCreated;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.SceneAssetSuggestion_Reject
|
||||||
|
@SceneAssetSuggestionID int,
|
||||||
|
@UserID int
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
DECLARE @SceneID int;
|
||||||
|
DECLARE @AssetID int;
|
||||||
|
|
||||||
|
SELECT @SceneID = suggestion.SceneID,
|
||||||
|
@AssetID = suggestion.AssetID
|
||||||
|
FROM dbo.SceneAssetSuggestions 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.StoryAssets sa ON sa.StoryAssetID = suggestion.AssetID AND sa.ProjectID = p.ProjectID
|
||||||
|
WHERE suggestion.SceneAssetSuggestionID = @SceneAssetSuggestionID
|
||||||
|
AND suggestion.Status = N'Pending'
|
||||||
|
AND s.IsArchived = 0
|
||||||
|
AND c.IsArchived = 0
|
||||||
|
AND b.IsArchived = 0
|
||||||
|
AND p.IsArchived = 0
|
||||||
|
AND sa.IsArchived = 0
|
||||||
|
AND pua.UserID = @UserID
|
||||||
|
AND pua.IsActive = 1;
|
||||||
|
|
||||||
|
IF @SceneID IS NULL OR @AssetID IS NULL
|
||||||
|
RETURN;
|
||||||
|
|
||||||
|
UPDATE dbo.SceneAssetSuggestions
|
||||||
|
SET Status = N'Rejected',
|
||||||
|
ReviewedByUserID = @UserID,
|
||||||
|
ReviewedUtc = SYSUTCDATETIME()
|
||||||
|
WHERE SceneAssetSuggestionID = @SceneAssetSuggestionID
|
||||||
|
AND Status = N'Pending';
|
||||||
|
|
||||||
|
SELECT @SceneAssetSuggestionID AS SceneAssetSuggestionID,
|
||||||
|
@SceneID AS SceneID,
|
||||||
|
@AssetID AS AssetID,
|
||||||
|
N'Rejected' AS Status,
|
||||||
|
CAST(0 AS bit) AS SceneAssetCreated;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.SceneAssetSuggestions_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.SceneAssetSuggestionID) AS PendingSuggestionCount
|
||||||
|
FROM dbo.Scenes s
|
||||||
|
INNER JOIN dbo.SceneAssetSuggestions suggestion ON suggestion.SceneID = s.SceneID
|
||||||
|
INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = suggestion.AssetID
|
||||||
|
WHERE s.ChapterID = @ChapterID
|
||||||
|
AND s.IsArchived = 0
|
||||||
|
AND sa.IsArchived = 0
|
||||||
|
AND suggestion.Status = N'Pending'
|
||||||
|
GROUP BY s.SceneID;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
@ -267,6 +267,7 @@ public sealed class ChapterDetailViewModel
|
|||||||
public Chapter Chapter { get; set; } = new();
|
public Chapter Chapter { get; set; } = new();
|
||||||
public IReadOnlyList<Scene> Scenes { get; set; } = [];
|
public IReadOnlyList<Scene> Scenes { get; set; } = [];
|
||||||
public int PendingCharacterSuggestionCount { get; set; }
|
public int PendingCharacterSuggestionCount { get; set; }
|
||||||
|
public int PendingAssetSuggestionCount { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class SceneEditViewModel
|
public sealed class SceneEditViewModel
|
||||||
@ -340,6 +341,7 @@ public sealed class SceneEditViewModel
|
|||||||
public SceneAssetLocationCreateViewModel NewSceneAssetLocation { get; set; } = new();
|
public SceneAssetLocationCreateViewModel NewSceneAssetLocation { get; set; } = new();
|
||||||
public IReadOnlyList<SceneCharacter> SceneCharacters { get; set; } = [];
|
public IReadOnlyList<SceneCharacter> SceneCharacters { get; set; } = [];
|
||||||
public IReadOnlyList<SceneCharacterSuggestion> CharacterSuggestions { get; set; } = [];
|
public IReadOnlyList<SceneCharacterSuggestion> CharacterSuggestions { get; set; } = [];
|
||||||
|
public IReadOnlyList<SceneAssetSuggestion> AssetSuggestions { get; set; } = [];
|
||||||
public IReadOnlyList<CharacterAttributeEvent> CharacterAttributeEvents { get; set; } = [];
|
public IReadOnlyList<CharacterAttributeEvent> CharacterAttributeEvents { get; set; } = [];
|
||||||
public IReadOnlyList<CharacterKnowledgeItem> CharacterKnowledge { get; set; } = [];
|
public IReadOnlyList<CharacterKnowledgeItem> CharacterKnowledge { get; set; } = [];
|
||||||
public IReadOnlyList<RelationshipEvent> RelationshipEvents { get; set; } = [];
|
public IReadOnlyList<RelationshipEvent> RelationshipEvents { get; set; } = [];
|
||||||
|
|||||||
@ -51,11 +51,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (Model.PendingCharacterSuggestionCount > 0)
|
@if (Model.PendingCharacterSuggestionCount > 0 || Model.PendingAssetSuggestionCount > 0)
|
||||||
{
|
{
|
||||||
<section class="list-section word-companion-suggestion-summary">
|
<section class="list-section word-companion-suggestion-summary">
|
||||||
<h2>Word Companion Suggestions</h2>
|
<h2>Word Companion Suggestions</h2>
|
||||||
<p class="muted">@Model.PendingCharacterSuggestionCount character suggestion@(Model.PendingCharacterSuggestionCount == 1 ? "" : "s") awaiting review in this chapter.</p>
|
@if (Model.PendingCharacterSuggestionCount > 0)
|
||||||
|
{
|
||||||
|
<p class="muted">@Model.PendingCharacterSuggestionCount character suggestion@(Model.PendingCharacterSuggestionCount == 1 ? "" : "s") awaiting review in this chapter.</p>
|
||||||
|
}
|
||||||
|
@if (Model.PendingAssetSuggestionCount > 0)
|
||||||
|
{
|
||||||
|
<p class="muted">@Model.PendingAssetSuggestionCount asset suggestion@(Model.PendingAssetSuggestionCount == 1 ? "" : "s") awaiting review in this chapter.</p>
|
||||||
|
}
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,9 +95,18 @@
|
|||||||
<td class="narrow">@scene.SceneNumber</td>
|
<td class="narrow">@scene.SceneNumber</td>
|
||||||
<td>
|
<td>
|
||||||
<a asp-controller="Scenes" asp-action="Edit" asp-route-id="@scene.SceneID">@scene.SceneTitle</a>
|
<a asp-controller="Scenes" asp-action="Edit" asp-route-id="@scene.SceneID">@scene.SceneTitle</a>
|
||||||
@if (scene.PendingCharacterSuggestionCount > 0)
|
@if (scene.PendingCharacterSuggestionCount > 0 || scene.PendingAssetSuggestionCount > 0)
|
||||||
{
|
{
|
||||||
<span class="status-pill word-companion-suggestion-pill">@scene.PendingCharacterSuggestionCount suggestion@(scene.PendingCharacterSuggestionCount == 1 ? "" : "s")</span>
|
var suggestionParts = new List<string>();
|
||||||
|
if (scene.PendingCharacterSuggestionCount > 0)
|
||||||
|
{
|
||||||
|
suggestionParts.Add($"{scene.PendingCharacterSuggestionCount} Character{(scene.PendingCharacterSuggestionCount == 1 ? "" : "s")}");
|
||||||
|
}
|
||||||
|
if (scene.PendingAssetSuggestionCount > 0)
|
||||||
|
{
|
||||||
|
suggestionParts.Add($"{scene.PendingAssetSuggestionCount} Asset{(scene.PendingAssetSuggestionCount == 1 ? "" : "s")}");
|
||||||
|
}
|
||||||
|
<span class="status-pill word-companion-suggestion-pill">@string.Join(" • ", suggestionParts)</span>
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
<td><span class="status-pill">@scene.RevisionStatusName</span></td>
|
<td><span class="status-pill">@scene.RevisionStatusName</span></td>
|
||||||
|
|||||||
@ -361,7 +361,7 @@
|
|||||||
<!-- Characters -->
|
<!-- Characters -->
|
||||||
<section class="inspector-section character-section" data-section-title="Characters" data-section-id="inspector-characters">
|
<section class="inspector-section character-section" data-section-title="Characters" data-section-id="inspector-characters">
|
||||||
<h3>Characters <help-icon key="sceneInspector.characters" /></h3>
|
<h3>Characters <help-icon key="sceneInspector.characters" /></h3>
|
||||||
@if (Model.CharacterSuggestions.Any())
|
@if (Model.CharacterSuggestions.Any() || Model.AssetSuggestions.Any())
|
||||||
{
|
{
|
||||||
<div class="word-companion-suggestion-panel">
|
<div class="word-companion-suggestion-panel">
|
||||||
<h4 class="inspector-subheading">Word Companion Suggestions</h4>
|
<h4 class="inspector-subheading">Word Companion Suggestions</h4>
|
||||||
@ -369,36 +369,78 @@
|
|||||||
{
|
{
|
||||||
<p class="text-danger form-helper-message">@suggestionError</p>
|
<p class="text-danger form-helper-message">@suggestionError</p>
|
||||||
}
|
}
|
||||||
|
@if (TempData["SceneAssetSuggestionError"] is string assetSuggestionError)
|
||||||
|
{
|
||||||
|
<p class="text-danger form-helper-message">@assetSuggestionError</p>
|
||||||
|
}
|
||||||
<div class="word-companion-suggestion-list">
|
<div class="word-companion-suggestion-list">
|
||||||
@foreach (var suggestion in Model.CharacterSuggestions)
|
@if (Model.CharacterSuggestions.Any())
|
||||||
{
|
{
|
||||||
<article class="word-companion-suggestion-item">
|
<h5 class="inspector-subheading">Characters</h5>
|
||||||
<div>
|
@foreach (var suggestion in Model.CharacterSuggestions)
|
||||||
<strong>@suggestion.CharacterName</strong>
|
{
|
||||||
<span class="muted">@suggestion.Confidence confidence</span>
|
<article class="word-companion-suggestion-item">
|
||||||
@if (!string.IsNullOrWhiteSpace(suggestion.Reason))
|
<div>
|
||||||
{
|
<strong>@suggestion.CharacterName</strong>
|
||||||
<span class="muted">@suggestion.Reason</span>
|
<span class="muted">@suggestion.Confidence confidence</span>
|
||||||
}
|
@if (!string.IsNullOrWhiteSpace(suggestion.Reason))
|
||||||
<span class="muted">Detected @suggestion.DetectedUtc.ToLocalTime().ToString("d MMM yyyy HH:mm")</span>
|
{
|
||||||
</div>
|
<span class="muted">@suggestion.Reason</span>
|
||||||
<div class="character-appearance-actions">
|
}
|
||||||
<form asp-controller="Scenes" asp-action="AcceptCharacterSuggestion" method="post">
|
<span class="muted">Detected @suggestion.DetectedUtc.ToLocalTime().ToString("d MMM yyyy HH:mm")</span>
|
||||||
<input type="hidden" name="id" value="@suggestion.SceneCharacterSuggestionID" />
|
</div>
|
||||||
<input type="hidden" name="sceneId" value="@Model.SceneID" />
|
<div class="character-appearance-actions">
|
||||||
<input type="hidden" name="projectId" value="@Model.ReturnProjectID" />
|
<form asp-controller="Scenes" asp-action="AcceptCharacterSuggestion" method="post">
|
||||||
<input type="hidden" name="bookId" value="@Model.ReturnBookID" />
|
<input type="hidden" name="id" value="@suggestion.SceneCharacterSuggestionID" />
|
||||||
<button class="btn btn-outline-primary btn-sm" type="submit">Accept</button>
|
<input type="hidden" name="sceneId" value="@Model.SceneID" />
|
||||||
</form>
|
<input type="hidden" name="projectId" value="@Model.ReturnProjectID" />
|
||||||
<form asp-controller="Scenes" asp-action="RejectCharacterSuggestion" method="post">
|
<input type="hidden" name="bookId" value="@Model.ReturnBookID" />
|
||||||
<input type="hidden" name="id" value="@suggestion.SceneCharacterSuggestionID" />
|
<button class="btn btn-outline-primary btn-sm" type="submit">Accept</button>
|
||||||
<input type="hidden" name="sceneId" value="@Model.SceneID" />
|
</form>
|
||||||
<input type="hidden" name="projectId" value="@Model.ReturnProjectID" />
|
<form asp-controller="Scenes" asp-action="RejectCharacterSuggestion" method="post">
|
||||||
<input type="hidden" name="bookId" value="@Model.ReturnBookID" />
|
<input type="hidden" name="id" value="@suggestion.SceneCharacterSuggestionID" />
|
||||||
<button class="btn btn-outline-secondary btn-sm" type="submit">Reject</button>
|
<input type="hidden" name="sceneId" value="@Model.SceneID" />
|
||||||
</form>
|
<input type="hidden" name="projectId" value="@Model.ReturnProjectID" />
|
||||||
</div>
|
<input type="hidden" name="bookId" value="@Model.ReturnBookID" />
|
||||||
</article>
|
<button class="btn btn-outline-secondary btn-sm" type="submit">Reject</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@if (Model.AssetSuggestions.Any())
|
||||||
|
{
|
||||||
|
<h5 class="inspector-subheading">Assets</h5>
|
||||||
|
@foreach (var suggestion in Model.AssetSuggestions)
|
||||||
|
{
|
||||||
|
<article class="word-companion-suggestion-item">
|
||||||
|
<div>
|
||||||
|
<strong>@suggestion.AssetName</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="AcceptAssetSuggestion" method="post">
|
||||||
|
<input type="hidden" name="id" value="@suggestion.SceneAssetSuggestionID" />
|
||||||
|
<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="RejectAssetSuggestion" method="post">
|
||||||
|
<input type="hidden" name="id" value="@suggestion.SceneAssetSuggestionID" />
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user