From c8ae73a954eb470b2c4699e3b2f319e1dce892ea Mon Sep 17 00:00:00 2001 From: Nick Beckley Date: Sun, 28 Jun 2026 21:58:54 +0100 Subject: [PATCH] Phase 16L - Review and Accept Word Companion Asset Suggestions --- PlotLine/Controllers/ScenesController.cs | 32 +++ PlotLine/Data/Repositories.cs | 42 ++++ PlotLine/Models/CoreModels.cs | 28 +++ PlotLine/Services/CoreServices.cs | 41 +++- ...16L_WordCompanionAssetSuggestionReview.sql | 198 ++++++++++++++++++ PlotLine/ViewModels/CoreViewModels.cs | 2 + PlotLine/Views/Chapters/Details.cshtml | 24 ++- PlotLine/Views/Scenes/_SceneInspector.cshtml | 100 ++++++--- 8 files changed, 432 insertions(+), 35 deletions(-) create mode 100644 PlotLine/Sql/104_Phase16L_WordCompanionAssetSuggestionReview.sql diff --git a/PlotLine/Controllers/ScenesController.cs b/PlotLine/Controllers/ScenesController.cs index 8501659..7f8f934 100644 --- a/PlotLine/Controllers/ScenesController.cs +++ b/PlotLine/Controllers/ScenesController.cs @@ -275,6 +275,38 @@ public sealed class ScenesController( return RedirectForScene(sceneId, projectId, bookId); } + [HttpPost] + [ValidateAntiForgeryToken] + public async Task 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 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] [ValidateAntiForgeryToken] public async Task AddSceneDependency(SceneDependencyCreateViewModel model) diff --git a/PlotLine/Data/Repositories.cs b/PlotLine/Data/Repositories.cs index 8b9cc0c..f5af755 100644 --- a/PlotLine/Data/Repositories.cs +++ b/PlotLine/Data/Repositories.cs @@ -179,6 +179,10 @@ public interface IAssetRepository Task> ListEventsByAssetAsync(int storyAssetId); Task SaveEventAsync(AssetEvent assetEvent); Task DeleteEventAsync(int assetEventId); + Task> ListSceneAssetSuggestionsAsync(int sceneId, int userId); + Task AcceptSceneAssetSuggestionAsync(int suggestionId, int userId); + Task RejectSceneAssetSuggestionAsync(int suggestionId, int userId); + Task> CountSceneAssetSuggestionsByChapterAsync(int chapterId, int userId); Task> ListDependenciesByAssetAsync(int storyAssetId); Task SaveDependencyAsync(AssetDependency dependency); 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); } + public async Task> ListSceneAssetSuggestionsAsync(int sceneId, int userId) + { + using var connection = connectionFactory.CreateConnection(); + var rows = await connection.QueryAsync( + "dbo.SceneAssetSuggestions_ListByScene", + new { SceneID = sceneId, UserID = userId }, + commandType: CommandType.StoredProcedure); + return rows.ToList(); + } + + public async Task AcceptSceneAssetSuggestionAsync(int suggestionId, int userId) + { + using var connection = connectionFactory.CreateConnection(); + return await connection.QuerySingleOrDefaultAsync( + "dbo.SceneAssetSuggestion_Accept", + new { SceneAssetSuggestionID = suggestionId, UserID = userId }, + commandType: CommandType.StoredProcedure); + } + + public async Task RejectSceneAssetSuggestionAsync(int suggestionId, int userId) + { + using var connection = connectionFactory.CreateConnection(); + return await connection.QuerySingleOrDefaultAsync( + "dbo.SceneAssetSuggestion_Reject", + new { SceneAssetSuggestionID = suggestionId, UserID = userId }, + commandType: CommandType.StoredProcedure); + } + + public async Task> CountSceneAssetSuggestionsByChapterAsync(int chapterId, int userId) + { + using var connection = connectionFactory.CreateConnection(); + var rows = await connection.QueryAsync( + "dbo.SceneAssetSuggestions_CountByChapter", + new { ChapterID = chapterId, UserID = userId }, + commandType: CommandType.StoredProcedure); + return rows.ToList(); + } + public async Task> ListDependenciesByAssetAsync(int storyAssetId) { using var connection = connectionFactory.CreateConnection(); diff --git a/PlotLine/Models/CoreModels.cs b/PlotLine/Models/CoreModels.cs index ff84c31..6ee678d 100644 --- a/PlotLine/Models/CoreModels.cs +++ b/PlotLine/Models/CoreModels.cs @@ -402,6 +402,7 @@ public sealed class Scene public int WarningCount { get; set; } public int DependencyCount { get; set; } public int PendingCharacterSuggestionCount { get; set; } + public int PendingAssetSuggestionCount { get; set; } public string TimeLabel { @@ -1009,6 +1010,33 @@ public sealed class SceneCharacterSuggestionReviewResult 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 int CharacterAttributeTypeID { get; set; } diff --git a/PlotLine/Services/CoreServices.cs b/PlotLine/Services/CoreServices.cs index 8a38a75..4fbd94d 100644 --- a/PlotLine/Services/CoreServices.cs +++ b/PlotLine/Services/CoreServices.cs @@ -174,6 +174,8 @@ public interface IAssetService Task ArchiveAssetAsync(int storyAssetId); Task AddAssetEventAsync(AssetEventCreateViewModel model); Task DeleteAssetEventAsync(int assetEventId); + Task AcceptSceneAssetSuggestionAsync(int suggestionId); + Task RejectSceneAssetSuggestionAsync(int suggestionId); Task AddCustodyEventAsync(AssetCustodyCreateViewModel model); Task DeleteCustodyEventAsync(int assetCustodyEventId); Task SaveDependencyAsync(AssetDependencyEditViewModel model); @@ -1290,6 +1292,7 @@ public sealed class ChapterService( IChapterRepository chapters, ISceneRepository scenes, ICharacterRepository characters, + IAssetRepository assets, ILookupRepository lookups, ISubscriptionService subscriptions, IProjectActivityService activity, @@ -1315,9 +1318,12 @@ public sealed class ChapterService( { var suggestionCounts = await characters.CountSceneCharacterSuggestionsByChapterAsync(chapterId, userId); 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) { scene.PendingCharacterSuggestionCount = countsByScene.GetValueOrDefault(scene.SceneID); + scene.PendingAssetSuggestionCount = assetCountsByScene.GetValueOrDefault(scene.SceneID); } } @@ -1327,7 +1333,8 @@ public sealed class ChapterService( Book = book, Chapter = chapter, 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.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.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.Warnings = model.SceneID == 0 ? [] : await warnings.ListBySceneAsync(model.SceneID); model.AgeContinuityWarnings = await BuildAgeContinuityWarningsAsync(model); @@ -7459,7 +7467,8 @@ public sealed class AssetService( IAssetRepository assets, ILocationRepository locations, IProjectActivityService activity, - IVisualIdentityImageService visualIdentityImages) : IAssetService + IVisualIdentityImageService visualIdentityImages, + ICurrentUserService currentUser) : IAssetService { public async Task GetAssetsAsync(int projectId) { @@ -7663,6 +7672,34 @@ public sealed class AssetService( 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 AddCustodyEventAsync(AssetCustodyCreateViewModel model) => assets.SaveCustodyEventAsync( new AssetCustodyEvent { diff --git a/PlotLine/Sql/104_Phase16L_WordCompanionAssetSuggestionReview.sql b/PlotLine/Sql/104_Phase16L_WordCompanionAssetSuggestionReview.sql new file mode 100644 index 0000000..5034f0e --- /dev/null +++ b/PlotLine/Sql/104_Phase16L_WordCompanionAssetSuggestionReview.sql @@ -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 diff --git a/PlotLine/ViewModels/CoreViewModels.cs b/PlotLine/ViewModels/CoreViewModels.cs index 5fa5566..cb0ed4c 100644 --- a/PlotLine/ViewModels/CoreViewModels.cs +++ b/PlotLine/ViewModels/CoreViewModels.cs @@ -267,6 +267,7 @@ public sealed class ChapterDetailViewModel public Chapter Chapter { get; set; } = new(); public IReadOnlyList Scenes { get; set; } = []; public int PendingCharacterSuggestionCount { get; set; } + public int PendingAssetSuggestionCount { get; set; } } public sealed class SceneEditViewModel @@ -340,6 +341,7 @@ public sealed class SceneEditViewModel public SceneAssetLocationCreateViewModel NewSceneAssetLocation { get; set; } = new(); public IReadOnlyList SceneCharacters { get; set; } = []; public IReadOnlyList CharacterSuggestions { get; set; } = []; + public IReadOnlyList AssetSuggestions { get; set; } = []; public IReadOnlyList CharacterAttributeEvents { get; set; } = []; public IReadOnlyList CharacterKnowledge { get; set; } = []; public IReadOnlyList RelationshipEvents { get; set; } = []; diff --git a/PlotLine/Views/Chapters/Details.cshtml b/PlotLine/Views/Chapters/Details.cshtml index 85d81cd..f6e05f1 100644 --- a/PlotLine/Views/Chapters/Details.cshtml +++ b/PlotLine/Views/Chapters/Details.cshtml @@ -51,11 +51,18 @@ -@if (Model.PendingCharacterSuggestionCount > 0) +@if (Model.PendingCharacterSuggestionCount > 0 || Model.PendingAssetSuggestionCount > 0) {

Word Companion Suggestions

-

@Model.PendingCharacterSuggestionCount character suggestion@(Model.PendingCharacterSuggestionCount == 1 ? "" : "s") awaiting review in this chapter.

+ @if (Model.PendingCharacterSuggestionCount > 0) + { +

@Model.PendingCharacterSuggestionCount character suggestion@(Model.PendingCharacterSuggestionCount == 1 ? "" : "s") awaiting review in this chapter.

+ } + @if (Model.PendingAssetSuggestionCount > 0) + { +

@Model.PendingAssetSuggestionCount asset suggestion@(Model.PendingAssetSuggestionCount == 1 ? "" : "s") awaiting review in this chapter.

+ }
} @@ -88,9 +95,18 @@ @scene.SceneNumber @scene.SceneTitle - @if (scene.PendingCharacterSuggestionCount > 0) + @if (scene.PendingCharacterSuggestionCount > 0 || scene.PendingAssetSuggestionCount > 0) { - @scene.PendingCharacterSuggestionCount suggestion@(scene.PendingCharacterSuggestionCount == 1 ? "" : "s") + var suggestionParts = new List(); + 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")}"); + } + @string.Join(" • ", suggestionParts) } @scene.RevisionStatusName diff --git a/PlotLine/Views/Scenes/_SceneInspector.cshtml b/PlotLine/Views/Scenes/_SceneInspector.cshtml index d81eac0..561b43f 100644 --- a/PlotLine/Views/Scenes/_SceneInspector.cshtml +++ b/PlotLine/Views/Scenes/_SceneInspector.cshtml @@ -361,7 +361,7 @@

Characters

- @if (Model.CharacterSuggestions.Any()) + @if (Model.CharacterSuggestions.Any() || Model.AssetSuggestions.Any()) {

Word Companion Suggestions

@@ -369,36 +369,78 @@ {

@suggestionError

} + @if (TempData["SceneAssetSuggestionError"] is string assetSuggestionError) + { +

@assetSuggestionError

+ }
- @foreach (var suggestion in Model.CharacterSuggestions) + @if (Model.CharacterSuggestions.Any()) { -
-
- @suggestion.CharacterName - @suggestion.Confidence confidence - @if (!string.IsNullOrWhiteSpace(suggestion.Reason)) - { - @suggestion.Reason - } - Detected @suggestion.DetectedUtc.ToLocalTime().ToString("d MMM yyyy HH:mm") -
-
-
- - - - - -
-
- - - - - -
-
-
+
Characters
+ @foreach (var suggestion in Model.CharacterSuggestions) + { +
+
+ @suggestion.CharacterName + @suggestion.Confidence confidence + @if (!string.IsNullOrWhiteSpace(suggestion.Reason)) + { + @suggestion.Reason + } + Detected @suggestion.DetectedUtc.ToLocalTime().ToString("d MMM yyyy HH:mm") +
+
+
+ + + + + +
+
+ + + + + +
+
+
+ } + } + @if (Model.AssetSuggestions.Any()) + { +
Assets
+ @foreach (var suggestion in Model.AssetSuggestions) + { +
+
+ @suggestion.AssetName + @suggestion.Confidence confidence + @if (!string.IsNullOrWhiteSpace(suggestion.Reason)) + { + @suggestion.Reason + } + Detected @suggestion.DetectedUtc.ToLocalTime().ToString("d MMM yyyy HH:mm") +
+
+
+ + + + + +
+
+ + + + + +
+
+
+ } }