Phase 16M - Location Detection Foundation and Alias Improvements
This commit is contained in:
parent
749b671834
commit
1f6a88a175
@ -52,6 +52,8 @@ public sealed class LocationsController(ILocationService locations) : Controller
|
||||
editModel.LocationTypeID = model.LocationTypeID;
|
||||
editModel.Description = model.Description;
|
||||
editModel.ShowInQuickAddBar = model.ShowInQuickAddBar;
|
||||
editModel.ExcludeFromCompanionDetection = model.ExcludeFromCompanionDetection;
|
||||
editModel.DetectionPriority = model.DetectionPriority;
|
||||
return View("Edit", editModel);
|
||||
}
|
||||
|
||||
|
||||
@ -170,6 +170,13 @@ public sealed class WordCompanionController(
|
||||
return response is null ? BadRequest() : Ok(response);
|
||||
}
|
||||
|
||||
[HttpGet("runtime/locations")]
|
||||
public async Task<IActionResult> GetRuntimeLocations([FromQuery] int projectId, [FromQuery] Guid documentGuid)
|
||||
{
|
||||
var response = await wordCompanion.ListRuntimeLocationAliasesAsync(projectId, documentGuid);
|
||||
return response is null ? BadRequest() : Ok(response);
|
||||
}
|
||||
|
||||
[HttpGet("manuscript/book/{bookId:int}")]
|
||||
public async Task<IActionResult> GetManuscriptForBook(int bookId)
|
||||
{
|
||||
|
||||
@ -502,7 +502,9 @@ public sealed class LocationRepository(ISqlConnectionFactory connectionFactory)
|
||||
location.LocationName,
|
||||
location.LocationTypeID,
|
||||
location.Description,
|
||||
location.ShowInQuickAddBar
|
||||
location.ShowInQuickAddBar,
|
||||
location.ExcludeFromCompanionDetection,
|
||||
location.DetectionPriority
|
||||
},
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
@ -25,6 +25,7 @@ public interface IWordCompanionRepository
|
||||
Task<WordCompanionRuntimeCharacterSuggestionsResponse?> CreateRuntimeCharacterSuggestionsAsync(int userId, WordCompanionRuntimeCharacterSuggestionsRequest request);
|
||||
Task<IReadOnlyList<WordCompanionRuntimeAssetAliasDto>> ListRuntimeAssetAliasesAsync(int userId, int projectId, Guid documentGuid);
|
||||
Task<WordCompanionRuntimeAssetSuggestionsResponse?> CreateRuntimeAssetSuggestionsAsync(int userId, WordCompanionRuntimeAssetSuggestionsRequest request);
|
||||
Task<IReadOnlyList<WordCompanionRuntimeLocationAliasDto>> ListRuntimeLocationAliasesAsync(int userId, int projectId, Guid documentGuid);
|
||||
Task<WordCompanionManuscriptAnalyseResponse?> AnalyseManuscriptAsync(int userId, WordCompanionManuscriptAnalyseRequest request);
|
||||
Task<WordCompanionManuscriptImportResponse?> ImportManuscriptAsync(int userId, WordCompanionManuscriptImportRequest request);
|
||||
Task<bool> HasProjectAccessAsync(int projectId, int userId);
|
||||
@ -325,6 +326,16 @@ public sealed class WordCompanionRepository(ISqlConnectionFactory connectionFact
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<WordCompanionRuntimeLocationAliasDto>> ListRuntimeLocationAliasesAsync(int userId, int projectId, Guid documentGuid)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
var rows = await connection.QueryAsync<WordCompanionRuntimeLocationAliasDto>(
|
||||
"dbo.WordCompanion_Runtime_LocationAlias_List",
|
||||
new { UserID = userId, ProjectID = projectId, DocumentGuid = documentGuid },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
return rows.ToList();
|
||||
}
|
||||
|
||||
public async Task<WordCompanionManuscriptAnalyseResponse?> AnalyseManuscriptAsync(int userId, WordCompanionManuscriptAnalyseRequest request)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
|
||||
@ -1246,6 +1246,8 @@ public sealed class LocationItem
|
||||
public string LocationPath { get; set; } = string.Empty;
|
||||
public int Depth { get; set; }
|
||||
public bool ShowInQuickAddBar { get; set; }
|
||||
public bool ExcludeFromCompanionDetection { get; set; }
|
||||
public int DetectionPriority { get; set; } = 50;
|
||||
public DateTime CreatedDate { get; set; }
|
||||
public DateTime UpdatedDate { get; set; }
|
||||
public bool IsArchived { get; set; }
|
||||
|
||||
@ -304,6 +304,24 @@ public sealed class WordCompanionRuntimeAssetSuggestionsResponse
|
||||
public string Message { get; init; } = "No new asset suggestions.";
|
||||
}
|
||||
|
||||
public sealed class WordCompanionRuntimeLocationAliasDto
|
||||
{
|
||||
public int LocationId { get; init; }
|
||||
public int? ParentLocationId { get; init; }
|
||||
public string Name { get; init; } = string.Empty;
|
||||
public string LocationPath { get; init; } = string.Empty;
|
||||
public string MatchText { get; init; } = string.Empty;
|
||||
public bool IsDisplayName { get; init; }
|
||||
public bool ExcludeFromCompanionDetection { get; init; }
|
||||
public int DetectionPriority { get; init; } = 50;
|
||||
}
|
||||
|
||||
public sealed class WordCompanionRuntimeLocationAliasResponse
|
||||
{
|
||||
public int ProjectId { get; init; }
|
||||
public IReadOnlyList<WordCompanionRuntimeLocationAliasDto> Locations { get; init; } = [];
|
||||
}
|
||||
|
||||
public sealed class WordCompanionManuscriptDocumentDto
|
||||
{
|
||||
public int ManuscriptDocumentId { get; init; }
|
||||
|
||||
@ -7815,6 +7815,8 @@ public sealed class LocationService(IProjectRepository projects, ILocationReposi
|
||||
LocationTypeID = location.LocationTypeID,
|
||||
Description = location.Description,
|
||||
ShowInQuickAddBar = location.ShowInQuickAddBar,
|
||||
ExcludeFromCompanionDetection = location.ExcludeFromCompanionDetection,
|
||||
DetectionPriority = location.DetectionPriority,
|
||||
Project = await projects.GetAsync(location.ProjectID)
|
||||
});
|
||||
}
|
||||
@ -7868,7 +7870,9 @@ public sealed class LocationService(IProjectRepository projects, ILocationReposi
|
||||
LocationName = model.LocationName,
|
||||
LocationTypeID = model.LocationTypeID,
|
||||
Description = model.Description,
|
||||
ShowInQuickAddBar = model.ShowInQuickAddBar
|
||||
ShowInQuickAddBar = model.ShowInQuickAddBar,
|
||||
ExcludeFromCompanionDetection = model.ExcludeFromCompanionDetection,
|
||||
DetectionPriority = model.DetectionPriority
|
||||
});
|
||||
await SyncLocationAliasesAsync(locationId, model.Aliases);
|
||||
await activity.RecordAsync(model.ProjectID, isNew ? "Created" : "Updated", "Location", locationId, model.LocationName);
|
||||
|
||||
@ -25,6 +25,7 @@ public interface IWordCompanionService
|
||||
Task<WordCompanionRuntimeCharacterSuggestionsResponse?> CreateRuntimeCharacterSuggestionsAsync(WordCompanionRuntimeCharacterSuggestionsRequest request);
|
||||
Task<WordCompanionRuntimeAssetAliasResponse?> ListRuntimeAssetAliasesAsync(int projectId, Guid documentGuid);
|
||||
Task<WordCompanionRuntimeAssetSuggestionsResponse?> CreateRuntimeAssetSuggestionsAsync(WordCompanionRuntimeAssetSuggestionsRequest request);
|
||||
Task<WordCompanionRuntimeLocationAliasResponse?> ListRuntimeLocationAliasesAsync(int projectId, Guid documentGuid);
|
||||
Task<WordCompanionRuntimeBookResponse?> GetRuntimeBookAsync(int bookId, Guid documentGuid);
|
||||
Task<WordCompanionManuscriptBookResponse?> GetManuscriptForBookAsync(int bookId);
|
||||
Task<WordCompanionManuscriptLinkResponse?> LinkManuscriptAsync(WordCompanionManuscriptLinkRequest request);
|
||||
@ -310,6 +311,21 @@ public sealed class WordCompanionService(
|
||||
: repository.CreateRuntimeAssetSuggestionsAsync(RequireUserId(), request);
|
||||
}
|
||||
|
||||
public async Task<WordCompanionRuntimeLocationAliasResponse?> ListRuntimeLocationAliasesAsync(int projectId, Guid documentGuid)
|
||||
{
|
||||
if (projectId <= 0 || documentGuid == Guid.Empty)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var aliases = await repository.ListRuntimeLocationAliasesAsync(RequireUserId(), projectId, documentGuid);
|
||||
return new WordCompanionRuntimeLocationAliasResponse
|
||||
{
|
||||
ProjectId = projectId,
|
||||
Locations = aliases
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<WordCompanionRuntimeBookResponse?> GetRuntimeBookAsync(int bookId, Guid documentGuid)
|
||||
{
|
||||
if (bookId <= 0 || documentGuid == Guid.Empty)
|
||||
|
||||
172
PlotLine/Sql/105_Phase16M_LocationDetectionMetadata.sql
Normal file
172
PlotLine/Sql/105_Phase16M_LocationDetectionMetadata.sql
Normal file
@ -0,0 +1,172 @@
|
||||
SET ANSI_NULLS ON;
|
||||
GO
|
||||
SET QUOTED_IDENTIFIER ON;
|
||||
GO
|
||||
|
||||
IF COL_LENGTH(N'dbo.Locations', N'ExcludeFromCompanionDetection') IS NULL
|
||||
ALTER TABLE dbo.Locations ADD ExcludeFromCompanionDetection bit NOT NULL CONSTRAINT DF_Locations_ExcludeFromCompanionDetection DEFAULT (0);
|
||||
GO
|
||||
|
||||
IF COL_LENGTH(N'dbo.Locations', N'DetectionPriority') IS NULL
|
||||
ALTER TABLE dbo.Locations ADD DetectionPriority int NOT NULL CONSTRAINT DF_Locations_DetectionPriority DEFAULT (50);
|
||||
GO
|
||||
|
||||
CREATE OR ALTER VIEW dbo.LocationPaths
|
||||
AS
|
||||
WITH LocationTree AS
|
||||
(
|
||||
SELECT LocationID, ProjectID, ParentLocationID, LocationName, LocationTypeID, Description, ShowInQuickAddBar,
|
||||
ExcludeFromCompanionDetection, DetectionPriority,
|
||||
CAST(LocationName AS nvarchar(max)) AS LocationPath,
|
||||
CAST(RIGHT('000000' + CAST(LocationID AS varchar(6)), 6) AS nvarchar(max)) AS SortPath,
|
||||
0 AS Depth
|
||||
FROM dbo.Locations
|
||||
WHERE ParentLocationID IS NULL AND IsArchived = 0
|
||||
UNION ALL
|
||||
SELECT child.LocationID, child.ProjectID, child.ParentLocationID, child.LocationName, child.LocationTypeID, child.Description, child.ShowInQuickAddBar,
|
||||
child.ExcludeFromCompanionDetection, child.DetectionPriority,
|
||||
CAST(parent.LocationPath + N' > ' + child.LocationName AS nvarchar(max)),
|
||||
CAST(parent.SortPath + N'.' + RIGHT('000000' + CAST(child.LocationID AS varchar(6)), 6) AS nvarchar(max)),
|
||||
parent.Depth + 1
|
||||
FROM dbo.Locations child
|
||||
INNER JOIN LocationTree parent ON parent.LocationID = child.ParentLocationID
|
||||
WHERE child.IsArchived = 0
|
||||
)
|
||||
SELECT LocationID, ProjectID, ParentLocationID, LocationName, LocationTypeID, Description, ShowInQuickAddBar,
|
||||
ExcludeFromCompanionDetection, DetectionPriority, LocationPath, SortPath, Depth
|
||||
FROM LocationTree;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.Location_ListByProject
|
||||
@ProjectID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT lp.LocationID, lp.ProjectID, lp.ParentLocationID, lp.LocationName, lp.LocationTypeID, lt.TypeName AS LocationTypeName,
|
||||
lp.Description, lp.ShowInQuickAddBar, lp.ExcludeFromCompanionDetection, lp.DetectionPriority,
|
||||
lp.LocationPath, lp.Depth, l.CreatedDate, l.UpdatedDate, l.IsArchived
|
||||
FROM dbo.LocationPaths lp
|
||||
INNER JOIN dbo.Locations l ON l.LocationID = lp.LocationID
|
||||
LEFT JOIN dbo.LocationTypes lt ON lt.LocationTypeID = lp.LocationTypeID
|
||||
WHERE lp.ProjectID = @ProjectID
|
||||
ORDER BY lp.SortPath;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.Location_Get
|
||||
@LocationID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT lp.LocationID, lp.ProjectID, lp.ParentLocationID, parent.LocationName AS ParentLocationName,
|
||||
lp.LocationName, lp.LocationTypeID, lt.TypeName AS LocationTypeName, lp.Description, lp.ShowInQuickAddBar,
|
||||
lp.ExcludeFromCompanionDetection, lp.DetectionPriority, lp.LocationPath, lp.Depth,
|
||||
l.CreatedDate, l.UpdatedDate, l.IsArchived
|
||||
FROM dbo.LocationPaths lp
|
||||
INNER JOIN dbo.Locations l ON l.LocationID = lp.LocationID
|
||||
LEFT JOIN dbo.Locations parent ON parent.LocationID = lp.ParentLocationID
|
||||
LEFT JOIN dbo.LocationTypes lt ON lt.LocationTypeID = lp.LocationTypeID
|
||||
WHERE lp.LocationID = @LocationID;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.Location_Save
|
||||
@LocationID int = NULL,
|
||||
@ProjectID int,
|
||||
@ParentLocationID int = NULL,
|
||||
@LocationName nvarchar(200),
|
||||
@LocationTypeID int = NULL,
|
||||
@Description nvarchar(max) = NULL,
|
||||
@ShowInQuickAddBar bit = 0,
|
||||
@ExcludeFromCompanionDetection bit = 0,
|
||||
@DetectionPriority int = 50
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SET @DetectionPriority = COALESCE(@DetectionPriority, 50);
|
||||
|
||||
IF @LocationID IS NULL OR @LocationID = 0
|
||||
BEGIN
|
||||
INSERT dbo.Locations (ProjectID, ParentLocationID, LocationName, LocationTypeID, Description, ShowInQuickAddBar, ExcludeFromCompanionDetection, DetectionPriority)
|
||||
VALUES (@ProjectID, @ParentLocationID, @LocationName, @LocationTypeID, @Description, @ShowInQuickAddBar, @ExcludeFromCompanionDetection, @DetectionPriority);
|
||||
|
||||
SET @LocationID = CAST(SCOPE_IDENTITY() AS int);
|
||||
END
|
||||
ELSE
|
||||
BEGIN
|
||||
UPDATE dbo.Locations
|
||||
SET ParentLocationID = @ParentLocationID,
|
||||
LocationName = @LocationName,
|
||||
LocationTypeID = @LocationTypeID,
|
||||
Description = @Description,
|
||||
ShowInQuickAddBar = @ShowInQuickAddBar,
|
||||
ExcludeFromCompanionDetection = @ExcludeFromCompanionDetection,
|
||||
DetectionPriority = @DetectionPriority,
|
||||
UpdatedDate = SYSUTCDATETIME()
|
||||
WHERE LocationID = @LocationID;
|
||||
END
|
||||
|
||||
SELECT @LocationID;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.WordCompanion_Runtime_LocationAlias_List
|
||||
@UserID int,
|
||||
@ProjectID int,
|
||||
@DocumentGuid uniqueidentifier
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
IF @DocumentGuid IS NULL
|
||||
OR @DocumentGuid = '00000000-0000-0000-0000-000000000000'
|
||||
OR @ProjectID <= 0
|
||||
RETURN;
|
||||
|
||||
IF NOT EXISTS
|
||||
(
|
||||
SELECT 1
|
||||
FROM dbo.ManuscriptDocuments md
|
||||
INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = md.ProjectID
|
||||
INNER JOIN dbo.Projects p ON p.ProjectID = md.ProjectID
|
||||
WHERE md.DocumentGuid = @DocumentGuid
|
||||
AND md.ProjectID = @ProjectID
|
||||
AND md.IsActive = 1
|
||||
AND p.IsArchived = 0
|
||||
AND pua.UserID = @UserID
|
||||
AND pua.IsActive = 1
|
||||
)
|
||||
RETURN;
|
||||
|
||||
SELECT lp.LocationID AS LocationId,
|
||||
lp.ParentLocationID AS ParentLocationId,
|
||||
lp.LocationName AS Name,
|
||||
lp.LocationPath,
|
||||
lp.LocationName AS MatchText,
|
||||
CAST(1 AS bit) AS IsDisplayName,
|
||||
lp.ExcludeFromCompanionDetection,
|
||||
lp.DetectionPriority
|
||||
FROM dbo.LocationPaths lp
|
||||
WHERE lp.ProjectID = @ProjectID
|
||||
AND NULLIF(LTRIM(RTRIM(lp.LocationName)), N'') IS NOT NULL
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT lp.LocationID AS LocationId,
|
||||
lp.ParentLocationID AS ParentLocationId,
|
||||
lp.LocationName AS Name,
|
||||
lp.LocationPath,
|
||||
la.Alias AS MatchText,
|
||||
CAST(0 AS bit) AS IsDisplayName,
|
||||
lp.ExcludeFromCompanionDetection,
|
||||
lp.DetectionPriority
|
||||
FROM dbo.LocationPaths lp
|
||||
INNER JOIN dbo.LocationAliases la ON la.LocationID = lp.LocationID
|
||||
WHERE lp.ProjectID = @ProjectID
|
||||
AND NULLIF(LTRIM(RTRIM(la.Alias)), N'') IS NOT NULL
|
||||
ORDER BY DetectionPriority DESC, Name, LocationId, IsDisplayName DESC, MatchText;
|
||||
END;
|
||||
GO
|
||||
@ -1937,6 +1937,11 @@ public sealed class LocationEditViewModel
|
||||
public string? Description { get; set; }
|
||||
[Display(Name = "Show in Quick Add Bar")]
|
||||
public bool ShowInQuickAddBar { get; set; }
|
||||
[Display(Name = "Exclude from Word Companion detection")]
|
||||
public bool ExcludeFromCompanionDetection { get; set; }
|
||||
[Display(Name = "Detection priority")]
|
||||
[Range(0, 1000, ErrorMessage = "Detection priority must be between 0 and 1000.")]
|
||||
public int DetectionPriority { get; set; } = 50;
|
||||
public Project? Project { get; set; }
|
||||
public IReadOnlyList<SelectListItem> ParentLocationOptions { get; set; } = [];
|
||||
public IReadOnlyList<SelectListItem> LocationTypeOptions { get; set; } = [];
|
||||
|
||||
@ -69,6 +69,20 @@
|
||||
<div class="form-text">Makes this item available as a draggable shortcut on the Timeline page.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<section class="list-section p-3">
|
||||
<h2 class="h5">Companion Detection</h2>
|
||||
<div class="form-check mb-3">
|
||||
<input asp-for="ExcludeFromCompanionDetection" class="form-check-input" />
|
||||
<label asp-for="ExcludeFromCompanionDetection" class="form-check-label"></label>
|
||||
<div class="form-text">Use this for generic places that would create noisy suggestions, such as Home, Kitchen, Road or Shop.</div>
|
||||
</div>
|
||||
<label asp-for="DetectionPriority" class="form-label"></label>
|
||||
<input asp-for="DetectionPriority" class="form-control form-control-sm" type="number" min="0" max="1000" />
|
||||
<span asp-validation-for="DetectionPriority" class="text-danger"></span>
|
||||
<div class="form-text">Higher values make this location stronger evidence in future Word Companion detection. The default is 50.</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label asp-for="Description" class="form-label"></label>
|
||||
<textarea asp-for="Description" class="form-control" rows="5"></textarea>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user