Phase 16N - Conservative Live Location Detection
This commit is contained in:
parent
1f6a88a175
commit
6867dfad77
@ -177,6 +177,13 @@ public sealed class WordCompanionController(
|
|||||||
return response is null ? BadRequest() : Ok(response);
|
return response is null ? BadRequest() : Ok(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("runtime/location-suggestions")]
|
||||||
|
public async Task<IActionResult> CreateRuntimeLocationSuggestions(WordCompanionRuntimeLocationSuggestionsRequest request)
|
||||||
|
{
|
||||||
|
var response = await wordCompanion.CreateRuntimeLocationSuggestionsAsync(request);
|
||||||
|
return response is null ? BadRequest() : Ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("manuscript/book/{bookId:int}")]
|
[HttpGet("manuscript/book/{bookId:int}")]
|
||||||
public async Task<IActionResult> GetManuscriptForBook(int bookId)
|
public async Task<IActionResult> GetManuscriptForBook(int bookId)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -26,6 +26,7 @@ public interface IWordCompanionRepository
|
|||||||
Task<IReadOnlyList<WordCompanionRuntimeAssetAliasDto>> ListRuntimeAssetAliasesAsync(int userId, int projectId, Guid documentGuid);
|
Task<IReadOnlyList<WordCompanionRuntimeAssetAliasDto>> ListRuntimeAssetAliasesAsync(int userId, int projectId, Guid documentGuid);
|
||||||
Task<WordCompanionRuntimeAssetSuggestionsResponse?> CreateRuntimeAssetSuggestionsAsync(int userId, WordCompanionRuntimeAssetSuggestionsRequest request);
|
Task<WordCompanionRuntimeAssetSuggestionsResponse?> CreateRuntimeAssetSuggestionsAsync(int userId, WordCompanionRuntimeAssetSuggestionsRequest request);
|
||||||
Task<IReadOnlyList<WordCompanionRuntimeLocationAliasDto>> ListRuntimeLocationAliasesAsync(int userId, int projectId, Guid documentGuid);
|
Task<IReadOnlyList<WordCompanionRuntimeLocationAliasDto>> ListRuntimeLocationAliasesAsync(int userId, int projectId, Guid documentGuid);
|
||||||
|
Task<WordCompanionRuntimeLocationSuggestionsResponse?> CreateRuntimeLocationSuggestionsAsync(int userId, WordCompanionRuntimeLocationSuggestionsRequest request);
|
||||||
Task<WordCompanionManuscriptAnalyseResponse?> AnalyseManuscriptAsync(int userId, WordCompanionManuscriptAnalyseRequest request);
|
Task<WordCompanionManuscriptAnalyseResponse?> AnalyseManuscriptAsync(int userId, WordCompanionManuscriptAnalyseRequest request);
|
||||||
Task<WordCompanionManuscriptImportResponse?> ImportManuscriptAsync(int userId, WordCompanionManuscriptImportRequest request);
|
Task<WordCompanionManuscriptImportResponse?> ImportManuscriptAsync(int userId, WordCompanionManuscriptImportRequest request);
|
||||||
Task<bool> HasProjectAccessAsync(int projectId, int userId);
|
Task<bool> HasProjectAccessAsync(int projectId, int userId);
|
||||||
@ -336,6 +337,21 @@ public sealed class WordCompanionRepository(ISqlConnectionFactory connectionFact
|
|||||||
return rows.ToList();
|
return rows.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<WordCompanionRuntimeLocationSuggestionsResponse?> CreateRuntimeLocationSuggestionsAsync(int userId, WordCompanionRuntimeLocationSuggestionsRequest request)
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
return await connection.QuerySingleOrDefaultAsync<WordCompanionRuntimeLocationSuggestionsResponse>(
|
||||||
|
"dbo.WordCompanion_Runtime_LocationSuggestions_Create",
|
||||||
|
new
|
||||||
|
{
|
||||||
|
UserID = userId,
|
||||||
|
request.DocumentGuid,
|
||||||
|
request.SceneId,
|
||||||
|
LocationIds = ToCsv(request.LocationIds)
|
||||||
|
},
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<WordCompanionManuscriptAnalyseResponse?> AnalyseManuscriptAsync(int userId, WordCompanionManuscriptAnalyseRequest request)
|
public async Task<WordCompanionManuscriptAnalyseResponse?> AnalyseManuscriptAsync(int userId, WordCompanionManuscriptAnalyseRequest request)
|
||||||
{
|
{
|
||||||
using var connection = connectionFactory.CreateConnection();
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
|||||||
@ -322,6 +322,21 @@ public sealed class WordCompanionRuntimeLocationAliasResponse
|
|||||||
public IReadOnlyList<WordCompanionRuntimeLocationAliasDto> Locations { get; init; } = [];
|
public IReadOnlyList<WordCompanionRuntimeLocationAliasDto> Locations { get; init; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class WordCompanionRuntimeLocationSuggestionsRequest
|
||||||
|
{
|
||||||
|
public Guid DocumentGuid { get; set; }
|
||||||
|
public int SceneId { get; set; }
|
||||||
|
public IReadOnlyList<int> LocationIds { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class WordCompanionRuntimeLocationSuggestionsResponse
|
||||||
|
{
|
||||||
|
public int SceneId { get; init; }
|
||||||
|
public int CreatedCount { get; init; }
|
||||||
|
public int SkippedCount { get; init; }
|
||||||
|
public string Message { get; init; } = "No new location suggestions.";
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class WordCompanionManuscriptDocumentDto
|
public sealed class WordCompanionManuscriptDocumentDto
|
||||||
{
|
{
|
||||||
public int ManuscriptDocumentId { get; init; }
|
public int ManuscriptDocumentId { get; init; }
|
||||||
|
|||||||
@ -26,6 +26,7 @@ public interface IWordCompanionService
|
|||||||
Task<WordCompanionRuntimeAssetAliasResponse?> ListRuntimeAssetAliasesAsync(int projectId, Guid documentGuid);
|
Task<WordCompanionRuntimeAssetAliasResponse?> ListRuntimeAssetAliasesAsync(int projectId, Guid documentGuid);
|
||||||
Task<WordCompanionRuntimeAssetSuggestionsResponse?> CreateRuntimeAssetSuggestionsAsync(WordCompanionRuntimeAssetSuggestionsRequest request);
|
Task<WordCompanionRuntimeAssetSuggestionsResponse?> CreateRuntimeAssetSuggestionsAsync(WordCompanionRuntimeAssetSuggestionsRequest request);
|
||||||
Task<WordCompanionRuntimeLocationAliasResponse?> ListRuntimeLocationAliasesAsync(int projectId, Guid documentGuid);
|
Task<WordCompanionRuntimeLocationAliasResponse?> ListRuntimeLocationAliasesAsync(int projectId, Guid documentGuid);
|
||||||
|
Task<WordCompanionRuntimeLocationSuggestionsResponse?> CreateRuntimeLocationSuggestionsAsync(WordCompanionRuntimeLocationSuggestionsRequest request);
|
||||||
Task<WordCompanionRuntimeBookResponse?> GetRuntimeBookAsync(int bookId, Guid documentGuid);
|
Task<WordCompanionRuntimeBookResponse?> GetRuntimeBookAsync(int bookId, Guid documentGuid);
|
||||||
Task<WordCompanionManuscriptBookResponse?> GetManuscriptForBookAsync(int bookId);
|
Task<WordCompanionManuscriptBookResponse?> GetManuscriptForBookAsync(int bookId);
|
||||||
Task<WordCompanionManuscriptLinkResponse?> LinkManuscriptAsync(WordCompanionManuscriptLinkRequest request);
|
Task<WordCompanionManuscriptLinkResponse?> LinkManuscriptAsync(WordCompanionManuscriptLinkRequest request);
|
||||||
@ -326,6 +327,21 @@ public sealed class WordCompanionService(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task<WordCompanionRuntimeLocationSuggestionsResponse?> CreateRuntimeLocationSuggestionsAsync(WordCompanionRuntimeLocationSuggestionsRequest request)
|
||||||
|
{
|
||||||
|
request.LocationIds = request.LocationIds
|
||||||
|
.Where(id => id > 0)
|
||||||
|
.Distinct()
|
||||||
|
.Take(CharacterDiscoverySuggestionLimit)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return request.DocumentGuid == Guid.Empty
|
||||||
|
|| request.SceneId <= 0
|
||||||
|
|| request.LocationIds.Count == 0
|
||||||
|
? Task.FromResult<WordCompanionRuntimeLocationSuggestionsResponse?>(null)
|
||||||
|
: repository.CreateRuntimeLocationSuggestionsAsync(RequireUserId(), request);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<WordCompanionRuntimeBookResponse?> GetRuntimeBookAsync(int bookId, Guid documentGuid)
|
public async Task<WordCompanionRuntimeBookResponse?> GetRuntimeBookAsync(int bookId, Guid documentGuid)
|
||||||
{
|
{
|
||||||
if (bookId <= 0 || documentGuid == Guid.Empty)
|
if (bookId <= 0 || documentGuid == Guid.Empty)
|
||||||
|
|||||||
141
PlotLine/Sql/106_Phase16N_WordCompanionLocationSuggestions.sql
Normal file
141
PlotLine/Sql/106_Phase16N_WordCompanionLocationSuggestions.sql
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
SET ANSI_NULLS ON;
|
||||||
|
GO
|
||||||
|
SET QUOTED_IDENTIFIER ON;
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF OBJECT_ID(N'dbo.SceneLocationSuggestions', N'U') IS NULL
|
||||||
|
BEGIN
|
||||||
|
CREATE TABLE dbo.SceneLocationSuggestions
|
||||||
|
(
|
||||||
|
SceneLocationSuggestionID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_SceneLocationSuggestions PRIMARY KEY,
|
||||||
|
SceneID int NOT NULL,
|
||||||
|
LocationID int NOT NULL,
|
||||||
|
SuggestionSource nvarchar(50) NOT NULL CONSTRAINT DF_SceneLocationSuggestions_Source DEFAULT (N'Word Companion'),
|
||||||
|
DetectedUtc datetime2 NOT NULL CONSTRAINT DF_SceneLocationSuggestions_DetectedUtc DEFAULT SYSUTCDATETIME(),
|
||||||
|
Status nvarchar(20) NOT NULL CONSTRAINT DF_SceneLocationSuggestions_Status DEFAULT (N'Pending'),
|
||||||
|
Confidence nvarchar(20) NOT NULL CONSTRAINT DF_SceneLocationSuggestions_Confidence DEFAULT (N'High'),
|
||||||
|
Reason nvarchar(200) NULL,
|
||||||
|
CreatedByUserID int NOT NULL,
|
||||||
|
ReviewedByUserID int NULL,
|
||||||
|
ReviewedUtc datetime2 NULL,
|
||||||
|
CONSTRAINT FK_SceneLocationSuggestions_Scenes FOREIGN KEY (SceneID) REFERENCES dbo.Scenes(SceneID),
|
||||||
|
CONSTRAINT FK_SceneLocationSuggestions_Locations FOREIGN KEY (LocationID) REFERENCES dbo.Locations(LocationID),
|
||||||
|
CONSTRAINT FK_SceneLocationSuggestions_CreatedByUser FOREIGN KEY (CreatedByUserID) REFERENCES dbo.AppUser(UserID),
|
||||||
|
CONSTRAINT FK_SceneLocationSuggestions_ReviewedByUser FOREIGN KEY (ReviewedByUserID) REFERENCES dbo.AppUser(UserID),
|
||||||
|
CONSTRAINT CK_SceneLocationSuggestions_Status CHECK (Status IN (N'Pending', N'Accepted', N'Rejected', N'Dismissed')),
|
||||||
|
CONSTRAINT CK_SceneLocationSuggestions_Confidence CHECK (Confidence IN (N'High', N'Medium', N'Low'))
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'UX_SceneLocationSuggestions_Pending' AND object_id = OBJECT_ID(N'dbo.SceneLocationSuggestions'))
|
||||||
|
CREATE UNIQUE INDEX UX_SceneLocationSuggestions_Pending ON dbo.SceneLocationSuggestions(SceneID, LocationID) WHERE Status = N'Pending';
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_SceneLocationSuggestions_SceneStatus' AND object_id = OBJECT_ID(N'dbo.SceneLocationSuggestions'))
|
||||||
|
CREATE INDEX IX_SceneLocationSuggestions_SceneStatus ON dbo.SceneLocationSuggestions(SceneID, Status, LocationID);
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.WordCompanion_Runtime_LocationSuggestions_Create
|
||||||
|
@UserID int,
|
||||||
|
@DocumentGuid uniqueidentifier,
|
||||||
|
@SceneID int,
|
||||||
|
@LocationIds 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 (LocationID int NOT NULL PRIMARY KEY);
|
||||||
|
|
||||||
|
INSERT @Requested (LocationID)
|
||||||
|
SELECT DISTINCT TRY_CONVERT(int, LTRIM(RTRIM(value)))
|
||||||
|
FROM STRING_SPLIT(COALESCE(@LocationIds, 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 location suggestions detected.' AS Message;
|
||||||
|
RETURN;
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF EXISTS
|
||||||
|
(
|
||||||
|
SELECT 1
|
||||||
|
FROM @Requested requested
|
||||||
|
WHERE NOT EXISTS
|
||||||
|
(
|
||||||
|
SELECT 1
|
||||||
|
FROM dbo.Locations location
|
||||||
|
WHERE location.LocationID = requested.LocationID
|
||||||
|
AND location.ProjectID = @ProjectID
|
||||||
|
AND location.IsArchived = 0
|
||||||
|
AND location.ExcludeFromCompanionDetection = 0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
RETURN;
|
||||||
|
|
||||||
|
DECLARE @Inserted table (LocationID int NOT NULL PRIMARY KEY);
|
||||||
|
|
||||||
|
INSERT dbo.SceneLocationSuggestions
|
||||||
|
(SceneID, LocationID, SuggestionSource, Status, Confidence, Reason, CreatedByUserID)
|
||||||
|
OUTPUT inserted.LocationID INTO @Inserted
|
||||||
|
SELECT @SceneID,
|
||||||
|
requested.LocationID,
|
||||||
|
N'Word Companion',
|
||||||
|
N'Pending',
|
||||||
|
N'High',
|
||||||
|
N'Detected in manuscript text',
|
||||||
|
@UserID
|
||||||
|
FROM @Requested requested
|
||||||
|
WHERE NOT EXISTS
|
||||||
|
(SELECT 1 FROM dbo.Scenes s WHERE s.SceneID = @SceneID AND s.PrimaryLocationID = requested.LocationID)
|
||||||
|
AND NOT EXISTS
|
||||||
|
(SELECT 1 FROM dbo.WordCompanionSceneLocations wcsl WHERE wcsl.SceneID = @SceneID AND wcsl.LocationID = requested.LocationID)
|
||||||
|
AND NOT EXISTS
|
||||||
|
(SELECT 1 FROM dbo.SceneLocationSuggestions suggestion WHERE suggestion.SceneID = @SceneID AND suggestion.LocationID = requested.LocationID);
|
||||||
|
|
||||||
|
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 location suggestion detected.'
|
||||||
|
WHEN (SELECT COUNT(*) FROM @Inserted) > 1 THEN CONCAT((SELECT COUNT(*) FROM @Inserted), N' location suggestions detected.')
|
||||||
|
ELSE N'No new location suggestions.'
|
||||||
|
END AS Message;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
@ -220,6 +220,11 @@
|
|||||||
let assetAliasCache = [];
|
let assetAliasCache = [];
|
||||||
let lastSuggestedAssetSceneId = null;
|
let lastSuggestedAssetSceneId = null;
|
||||||
let lastSuggestedAssetSignature = "";
|
let lastSuggestedAssetSignature = "";
|
||||||
|
let locationAliasCacheProjectId = null;
|
||||||
|
let locationAliasCacheDocumentGuid = "";
|
||||||
|
let locationAliasCache = [];
|
||||||
|
let lastSuggestedLocationSceneId = null;
|
||||||
|
let lastSuggestedLocationSignature = "";
|
||||||
let lastFollowSceneNotificationSignature = "";
|
let lastFollowSceneNotificationSignature = "";
|
||||||
let lastLinkedSceneTitle = "";
|
let lastLinkedSceneTitle = "";
|
||||||
let lastLinkedChapterTitle = "";
|
let lastLinkedChapterTitle = "";
|
||||||
@ -583,6 +588,7 @@
|
|||||||
plotDirectorStructureBookId = null;
|
plotDirectorStructureBookId = null;
|
||||||
resetRuntimeCharacterAliasCache();
|
resetRuntimeCharacterAliasCache();
|
||||||
resetRuntimeAssetAliasCache();
|
resetRuntimeAssetAliasCache();
|
||||||
|
resetRuntimeLocationAliasCache();
|
||||||
setRuntimeBookContext(null);
|
setRuntimeBookContext(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -2994,7 +3000,8 @@
|
|||||||
clearPlotDirectorStructureCache();
|
clearPlotDirectorStructureCache();
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
loadRuntimeCharacterAliases(true),
|
loadRuntimeCharacterAliases(true),
|
||||||
loadRuntimeAssetAliases(true)
|
loadRuntimeAssetAliases(true),
|
||||||
|
loadRuntimeLocationAliases(true)
|
||||||
]);
|
]);
|
||||||
await getBookStructureChapters(book.bookId);
|
await getBookStructureChapters(book.bookId);
|
||||||
const structure = await window.Word.run(async (context) => await readRuntimeDocumentStructure(context, !!book.usesExplicitScenes));
|
const structure = await window.Word.run(async (context) => await readRuntimeDocumentStructure(context, !!book.usesExplicitScenes));
|
||||||
@ -3913,6 +3920,14 @@
|
|||||||
lastSuggestedAssetSignature = "";
|
lastSuggestedAssetSignature = "";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resetRuntimeLocationAliasCache = () => {
|
||||||
|
locationAliasCacheProjectId = null;
|
||||||
|
locationAliasCacheDocumentGuid = "";
|
||||||
|
locationAliasCache = [];
|
||||||
|
lastSuggestedLocationSceneId = null;
|
||||||
|
lastSuggestedLocationSignature = "";
|
||||||
|
};
|
||||||
|
|
||||||
const loadRuntimeCharacterAliases = async (force = false) => {
|
const loadRuntimeCharacterAliases = async (force = false) => {
|
||||||
const project = selectedProject();
|
const project = selectedProject();
|
||||||
const documentGuid = currentDocumentGuid();
|
const documentGuid = currentDocumentGuid();
|
||||||
@ -3959,6 +3974,29 @@
|
|||||||
return assetAliasCache;
|
return assetAliasCache;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadRuntimeLocationAliases = async (force = false) => {
|
||||||
|
const project = selectedProject();
|
||||||
|
const documentGuid = currentDocumentGuid();
|
||||||
|
if (!project || !documentGuid) {
|
||||||
|
locationAliasCache = [];
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!force
|
||||||
|
&& locationAliasCacheProjectId === project.projectId
|
||||||
|
&& locationAliasCacheDocumentGuid === documentGuid
|
||||||
|
&& locationAliasCache.length > 0) {
|
||||||
|
return locationAliasCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetchJson(`/api/word-companion/runtime/locations?projectId=${encodeURIComponent(project.projectId)}&documentGuid=${encodeURIComponent(documentGuid)}`);
|
||||||
|
locationAliasCacheProjectId = project.projectId;
|
||||||
|
locationAliasCacheDocumentGuid = documentGuid;
|
||||||
|
locationAliasCache = Array.isArray(response?.locations) ? response.locations : [];
|
||||||
|
runtimeDebug(`Location cache loaded: ${locationAliasCache.length} aliases.`);
|
||||||
|
return locationAliasCache;
|
||||||
|
};
|
||||||
|
|
||||||
const escapeRegex = (value) => String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
const escapeRegex = (value) => String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
|
||||||
const hasWholeWordMatch = (text, matchText) => {
|
const hasWholeWordMatch = (text, matchText) => {
|
||||||
@ -4011,6 +4049,41 @@
|
|||||||
return [...detected.entries()].map(([assetId, name]) => ({ assetId, name }));
|
return [...detected.entries()].map(([assetId, name]) => ({ assetId, name }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const countLocationMatchWords = (value) => String(value || "")
|
||||||
|
.trim()
|
||||||
|
.split(/\s+/)
|
||||||
|
.filter(Boolean)
|
||||||
|
.length;
|
||||||
|
|
||||||
|
const isConservativeLocationCandidate = (item) => {
|
||||||
|
if (item?.excludeFromCompanionDetection) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const priority = Number.parseInt(item?.detectionPriority || "0", 10);
|
||||||
|
return countLocationMatchWords(item?.matchText) >= 2 || (Number.isInteger(priority) && priority >= 75);
|
||||||
|
};
|
||||||
|
|
||||||
|
const detectLocationsInSceneText = (sceneText, aliases) => {
|
||||||
|
if (!sceneText || !Array.isArray(aliases) || aliases.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const detected = new Map();
|
||||||
|
for (const item of aliases) {
|
||||||
|
const locationId = Number.parseInt(item.locationId || "", 10);
|
||||||
|
if (!Number.isInteger(locationId) || locationId <= 0 || detected.has(locationId) || !isConservativeLocationCandidate(item)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasWholeWordMatch(sceneText, item.matchText)) {
|
||||||
|
detected.set(locationId, item.name || item.matchText || "Location");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...detected.entries()].map(([locationId, name]) => ({ locationId, name }));
|
||||||
|
};
|
||||||
|
|
||||||
const finishCharacterSuggestionDetection = () => {
|
const finishCharacterSuggestionDetection = () => {
|
||||||
characterSuggestionInFlight = false;
|
characterSuggestionInFlight = false;
|
||||||
if (characterSuggestionPending) {
|
if (characterSuggestionPending) {
|
||||||
@ -4035,10 +4108,11 @@
|
|||||||
characterSuggestionPending = false;
|
characterSuggestionPending = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [sceneText, characterAliases, assetAliases] = await Promise.all([
|
const [sceneText, characterAliases, assetAliases, locationAliases] = await Promise.all([
|
||||||
readCachedCurrentSceneText(),
|
readCachedCurrentSceneText(),
|
||||||
loadRuntimeCharacterAliases(),
|
loadRuntimeCharacterAliases(),
|
||||||
loadRuntimeAssetAliases()
|
loadRuntimeAssetAliases(),
|
||||||
|
loadRuntimeLocationAliases()
|
||||||
]);
|
]);
|
||||||
const detected = detectCharactersInSceneText(sceneText, characterAliases);
|
const detected = detectCharactersInSceneText(sceneText, characterAliases);
|
||||||
const characterIds = detected.map((item) => item.characterId);
|
const characterIds = detected.map((item) => item.characterId);
|
||||||
@ -4046,6 +4120,9 @@
|
|||||||
const detectedAssets = detectAssetsInSceneText(sceneText, assetAliases);
|
const detectedAssets = detectAssetsInSceneText(sceneText, assetAliases);
|
||||||
const assetIds = detectedAssets.map((item) => item.assetId);
|
const assetIds = detectedAssets.map((item) => item.assetId);
|
||||||
const assetSignature = assetIds.slice().sort((left, right) => left - right).join(",");
|
const assetSignature = assetIds.slice().sort((left, right) => left - right).join(",");
|
||||||
|
const detectedLocations = detectLocationsInSceneText(sceneText, locationAliases);
|
||||||
|
const locationIds = detectedLocations.map((item) => item.locationId);
|
||||||
|
const locationSignature = locationIds.slice().sort((left, right) => left - right).join(",");
|
||||||
|
|
||||||
if (!signature || (lastSuggestedSceneId === resolvedSceneId && lastSuggestedCharacterSignature === signature)) {
|
if (!signature || (lastSuggestedSceneId === resolvedSceneId && lastSuggestedCharacterSignature === signature)) {
|
||||||
runtimeDebug("Character suggestions skipped.");
|
runtimeDebug("Character suggestions skipped.");
|
||||||
@ -4085,6 +4162,26 @@
|
|||||||
}
|
}
|
||||||
runtimeDebug(assetResponse?.message || "Asset suggestions submitted.");
|
runtimeDebug(assetResponse?.message || "Asset suggestions submitted.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!locationSignature || (lastSuggestedLocationSceneId === resolvedSceneId && lastSuggestedLocationSignature === locationSignature)) {
|
||||||
|
runtimeDebug("Location suggestions skipped.");
|
||||||
|
} else {
|
||||||
|
runtimeDebug(`Location matches found: ${detectedLocations.map((item) => item.name).join(", ")}`);
|
||||||
|
const locationResponse = await postJson("/api/word-companion/runtime/location-suggestions", {
|
||||||
|
documentGuid: currentDocumentGuid(),
|
||||||
|
sceneId: resolvedSceneId,
|
||||||
|
locationIds
|
||||||
|
});
|
||||||
|
|
||||||
|
lastSuggestedLocationSceneId = resolvedSceneId;
|
||||||
|
lastSuggestedLocationSignature = locationSignature;
|
||||||
|
if ((locationResponse?.createdCount || 0) > 0) {
|
||||||
|
const names = detectedLocations.slice(0, 3).map((item) => item.name).join(", ");
|
||||||
|
const suffix = detectedLocations.length > 3 ? ` +${detectedLocations.length - 3}` : "";
|
||||||
|
setDocumentMessage(names ? `Locations detected: ${names}${suffix}` : locationResponse.message);
|
||||||
|
}
|
||||||
|
runtimeDebug(locationResponse?.message || "Location suggestions submitted.");
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Unable to detect runtime suggestions.", error);
|
console.error("Unable to detect runtime suggestions.", error);
|
||||||
setDiagnostics({ lastError: errorText(error) });
|
setDiagnostics({ lastError: errorText(error) });
|
||||||
@ -4566,7 +4663,8 @@
|
|||||||
try {
|
try {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
loadRuntimeCharacterAliases(true),
|
loadRuntimeCharacterAliases(true),
|
||||||
loadRuntimeAssetAliases(true)
|
loadRuntimeAssetAliases(true),
|
||||||
|
loadRuntimeLocationAliases(true)
|
||||||
]);
|
]);
|
||||||
const scanned = await refreshDocumentStructure();
|
const scanned = await refreshDocumentStructure();
|
||||||
if (!scanned) {
|
if (!scanned) {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user