Phase 16K - Live Asset Detection While Writing
This commit is contained in:
parent
df03f46ca8
commit
c8bae2d1d1
@ -156,6 +156,20 @@ public sealed class WordCompanionController(
|
||||
return response is null ? BadRequest() : Ok(response);
|
||||
}
|
||||
|
||||
[HttpGet("runtime/assets")]
|
||||
public async Task<IActionResult> GetRuntimeAssets([FromQuery] int projectId, [FromQuery] Guid documentGuid)
|
||||
{
|
||||
var response = await wordCompanion.ListRuntimeAssetAliasesAsync(projectId, documentGuid);
|
||||
return response is null ? BadRequest() : Ok(response);
|
||||
}
|
||||
|
||||
[HttpPost("runtime/asset-suggestions")]
|
||||
public async Task<IActionResult> CreateRuntimeAssetSuggestions(WordCompanionRuntimeAssetSuggestionsRequest request)
|
||||
{
|
||||
var response = await wordCompanion.CreateRuntimeAssetSuggestionsAsync(request);
|
||||
return response is null ? BadRequest() : Ok(response);
|
||||
}
|
||||
|
||||
[HttpGet("manuscript/book/{bookId:int}")]
|
||||
public async Task<IActionResult> GetManuscriptForBook(int bookId)
|
||||
{
|
||||
|
||||
@ -23,6 +23,8 @@ public interface IWordCompanionRepository
|
||||
Task<WordCompanionRuntimeCurrentSceneResponse?> ValidateRuntimeCurrentSceneAsync(int userId, WordCompanionRuntimeCurrentSceneRequest request);
|
||||
Task<IReadOnlyList<WordCompanionRuntimeCharacterAliasDto>> ListRuntimeCharacterAliasesAsync(int userId, int projectId, Guid documentGuid);
|
||||
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<WordCompanionManuscriptAnalyseResponse?> AnalyseManuscriptAsync(int userId, WordCompanionManuscriptAnalyseRequest request);
|
||||
Task<WordCompanionManuscriptImportResponse?> ImportManuscriptAsync(int userId, WordCompanionManuscriptImportRequest request);
|
||||
Task<bool> HasProjectAccessAsync(int projectId, int userId);
|
||||
@ -298,6 +300,31 @@ public sealed class WordCompanionRepository(ISqlConnectionFactory connectionFact
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<WordCompanionRuntimeAssetAliasDto>> ListRuntimeAssetAliasesAsync(int userId, int projectId, Guid documentGuid)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
var rows = await connection.QueryAsync<WordCompanionRuntimeAssetAliasDto>(
|
||||
"dbo.WordCompanion_Runtime_AssetAlias_List",
|
||||
new { UserID = userId, ProjectID = projectId, DocumentGuid = documentGuid },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
return rows.ToList();
|
||||
}
|
||||
|
||||
public async Task<WordCompanionRuntimeAssetSuggestionsResponse?> CreateRuntimeAssetSuggestionsAsync(int userId, WordCompanionRuntimeAssetSuggestionsRequest request)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleOrDefaultAsync<WordCompanionRuntimeAssetSuggestionsResponse>(
|
||||
"dbo.WordCompanion_Runtime_AssetSuggestions_Create",
|
||||
new
|
||||
{
|
||||
UserID = userId,
|
||||
request.DocumentGuid,
|
||||
request.SceneId,
|
||||
AssetIds = ToCsv(request.AssetIds)
|
||||
},
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<WordCompanionManuscriptAnalyseResponse?> AnalyseManuscriptAsync(int userId, WordCompanionManuscriptAnalyseRequest request)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
|
||||
@ -275,6 +275,35 @@ public sealed class WordCompanionRuntimeCharacterSuggestionsResponse
|
||||
public string Message { get; init; } = "No new character suggestions.";
|
||||
}
|
||||
|
||||
public sealed class WordCompanionRuntimeAssetAliasDto
|
||||
{
|
||||
public int AssetId { get; init; }
|
||||
public string Name { get; init; } = string.Empty;
|
||||
public string MatchText { get; init; } = string.Empty;
|
||||
public bool IsDisplayName { get; init; }
|
||||
}
|
||||
|
||||
public sealed class WordCompanionRuntimeAssetAliasResponse
|
||||
{
|
||||
public int ProjectId { get; init; }
|
||||
public IReadOnlyList<WordCompanionRuntimeAssetAliasDto> Assets { get; init; } = [];
|
||||
}
|
||||
|
||||
public sealed class WordCompanionRuntimeAssetSuggestionsRequest
|
||||
{
|
||||
public Guid DocumentGuid { get; set; }
|
||||
public int SceneId { get; set; }
|
||||
public IReadOnlyList<int> AssetIds { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class WordCompanionRuntimeAssetSuggestionsResponse
|
||||
{
|
||||
public int SceneId { get; init; }
|
||||
public int CreatedCount { get; init; }
|
||||
public int SkippedCount { get; init; }
|
||||
public string Message { get; init; } = "No new asset suggestions.";
|
||||
}
|
||||
|
||||
public sealed class WordCompanionManuscriptDocumentDto
|
||||
{
|
||||
public int ManuscriptDocumentId { get; init; }
|
||||
|
||||
@ -23,6 +23,8 @@ public interface IWordCompanionService
|
||||
Task<WordCompanionRuntimeCurrentSceneResponse?> ValidateRuntimeCurrentSceneAsync(WordCompanionRuntimeCurrentSceneRequest request);
|
||||
Task<WordCompanionRuntimeCharacterAliasResponse?> ListRuntimeCharacterAliasesAsync(int projectId, Guid documentGuid);
|
||||
Task<WordCompanionRuntimeCharacterSuggestionsResponse?> CreateRuntimeCharacterSuggestionsAsync(WordCompanionRuntimeCharacterSuggestionsRequest request);
|
||||
Task<WordCompanionRuntimeAssetAliasResponse?> ListRuntimeAssetAliasesAsync(int projectId, Guid documentGuid);
|
||||
Task<WordCompanionRuntimeAssetSuggestionsResponse?> CreateRuntimeAssetSuggestionsAsync(WordCompanionRuntimeAssetSuggestionsRequest request);
|
||||
Task<WordCompanionRuntimeBookResponse?> GetRuntimeBookAsync(int bookId, Guid documentGuid);
|
||||
Task<WordCompanionManuscriptBookResponse?> GetManuscriptForBookAsync(int bookId);
|
||||
Task<WordCompanionManuscriptLinkResponse?> LinkManuscriptAsync(WordCompanionManuscriptLinkRequest request);
|
||||
@ -278,6 +280,36 @@ public sealed class WordCompanionService(
|
||||
: repository.CreateRuntimeCharacterSuggestionsAsync(RequireUserId(), request);
|
||||
}
|
||||
|
||||
public async Task<WordCompanionRuntimeAssetAliasResponse?> ListRuntimeAssetAliasesAsync(int projectId, Guid documentGuid)
|
||||
{
|
||||
if (projectId <= 0 || documentGuid == Guid.Empty)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var aliases = await repository.ListRuntimeAssetAliasesAsync(RequireUserId(), projectId, documentGuid);
|
||||
return new WordCompanionRuntimeAssetAliasResponse
|
||||
{
|
||||
ProjectId = projectId,
|
||||
Assets = aliases
|
||||
};
|
||||
}
|
||||
|
||||
public Task<WordCompanionRuntimeAssetSuggestionsResponse?> CreateRuntimeAssetSuggestionsAsync(WordCompanionRuntimeAssetSuggestionsRequest request)
|
||||
{
|
||||
request.AssetIds = request.AssetIds
|
||||
.Where(id => id > 0)
|
||||
.Distinct()
|
||||
.Take(CharacterDiscoverySuggestionLimit)
|
||||
.ToList();
|
||||
|
||||
return request.DocumentGuid == Guid.Empty
|
||||
|| request.SceneId <= 0
|
||||
|| request.AssetIds.Count == 0
|
||||
? Task.FromResult<WordCompanionRuntimeAssetSuggestionsResponse?>(null)
|
||||
: repository.CreateRuntimeAssetSuggestionsAsync(RequireUserId(), request);
|
||||
}
|
||||
|
||||
public async Task<WordCompanionRuntimeBookResponse?> GetRuntimeBookAsync(int bookId, Guid documentGuid)
|
||||
{
|
||||
if (bookId <= 0 || documentGuid == Guid.Empty)
|
||||
|
||||
189
PlotLine/Sql/103_Phase16K_WordCompanionAssetSuggestions.sql
Normal file
189
PlotLine/Sql/103_Phase16K_WordCompanionAssetSuggestions.sql
Normal file
@ -0,0 +1,189 @@
|
||||
IF OBJECT_ID(N'dbo.SceneAssetSuggestions', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.SceneAssetSuggestions
|
||||
(
|
||||
SceneAssetSuggestionID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_SceneAssetSuggestions PRIMARY KEY,
|
||||
SceneID int NOT NULL,
|
||||
AssetID int NOT NULL,
|
||||
SuggestionSource nvarchar(50) NOT NULL CONSTRAINT DF_SceneAssetSuggestions_Source DEFAULT (N'Word Companion'),
|
||||
DetectedUtc datetime2 NOT NULL CONSTRAINT DF_SceneAssetSuggestions_DetectedUtc DEFAULT SYSUTCDATETIME(),
|
||||
Status nvarchar(20) NOT NULL CONSTRAINT DF_SceneAssetSuggestions_Status DEFAULT (N'Pending'),
|
||||
Confidence nvarchar(20) NOT NULL CONSTRAINT DF_SceneAssetSuggestions_Confidence DEFAULT (N'High'),
|
||||
Reason nvarchar(200) NULL,
|
||||
CreatedByUserID int NOT NULL,
|
||||
ReviewedByUserID int NULL,
|
||||
ReviewedUtc datetime2 NULL,
|
||||
CONSTRAINT FK_SceneAssetSuggestions_Scenes FOREIGN KEY (SceneID) REFERENCES dbo.Scenes(SceneID),
|
||||
CONSTRAINT FK_SceneAssetSuggestions_StoryAssets FOREIGN KEY (AssetID) REFERENCES dbo.StoryAssets(StoryAssetID),
|
||||
CONSTRAINT FK_SceneAssetSuggestions_CreatedByUser FOREIGN KEY (CreatedByUserID) REFERENCES dbo.AppUser(UserID),
|
||||
CONSTRAINT FK_SceneAssetSuggestions_ReviewedByUser FOREIGN KEY (ReviewedByUserID) REFERENCES dbo.AppUser(UserID),
|
||||
CONSTRAINT CK_SceneAssetSuggestions_Status CHECK (Status IN (N'Pending', N'Accepted', N'Rejected', N'Dismissed')),
|
||||
CONSTRAINT CK_SceneAssetSuggestions_Confidence CHECK (Confidence IN (N'High', N'Medium', N'Low'))
|
||||
);
|
||||
END;
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'UX_SceneAssetSuggestions_Pending' AND object_id = OBJECT_ID(N'dbo.SceneAssetSuggestions'))
|
||||
CREATE UNIQUE INDEX UX_SceneAssetSuggestions_Pending ON dbo.SceneAssetSuggestions(SceneID, AssetID) WHERE Status = N'Pending';
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_SceneAssetSuggestions_SceneStatus' AND object_id = OBJECT_ID(N'dbo.SceneAssetSuggestions'))
|
||||
CREATE INDEX IX_SceneAssetSuggestions_SceneStatus ON dbo.SceneAssetSuggestions(SceneID, Status, AssetID);
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.WordCompanion_Runtime_AssetAlias_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 sa.StoryAssetID AS AssetId,
|
||||
sa.AssetName AS Name,
|
||||
sa.AssetName AS MatchText,
|
||||
CAST(1 AS bit) AS IsDisplayName
|
||||
FROM dbo.StoryAssets sa
|
||||
WHERE sa.ProjectID = @ProjectID
|
||||
AND sa.IsArchived = 0
|
||||
AND NULLIF(LTRIM(RTRIM(sa.AssetName)), N'') IS NOT NULL
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT sa.StoryAssetID AS AssetId,
|
||||
sa.AssetName AS Name,
|
||||
aa.Alias AS MatchText,
|
||||
CAST(0 AS bit) AS IsDisplayName
|
||||
FROM dbo.StoryAssets sa
|
||||
INNER JOIN dbo.AssetAliases aa ON aa.StoryAssetID = sa.StoryAssetID
|
||||
WHERE sa.ProjectID = @ProjectID
|
||||
AND sa.IsArchived = 0
|
||||
AND NULLIF(LTRIM(RTRIM(aa.Alias)), N'') IS NOT NULL
|
||||
ORDER BY Name, AssetId, IsDisplayName DESC, MatchText;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.WordCompanion_Runtime_AssetSuggestions_Create
|
||||
@UserID int,
|
||||
@DocumentGuid uniqueidentifier,
|
||||
@SceneID int,
|
||||
@AssetIds 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 (AssetID int NOT NULL PRIMARY KEY);
|
||||
|
||||
INSERT @Requested (AssetID)
|
||||
SELECT DISTINCT TRY_CONVERT(int, LTRIM(RTRIM(value)))
|
||||
FROM STRING_SPLIT(COALESCE(@AssetIds, 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 asset suggestions detected.' AS Message;
|
||||
RETURN;
|
||||
END;
|
||||
|
||||
IF EXISTS
|
||||
(
|
||||
SELECT 1
|
||||
FROM @Requested requested
|
||||
WHERE NOT EXISTS
|
||||
(
|
||||
SELECT 1
|
||||
FROM dbo.StoryAssets sa
|
||||
WHERE sa.StoryAssetID = requested.AssetID
|
||||
AND sa.ProjectID = @ProjectID
|
||||
AND sa.IsArchived = 0
|
||||
)
|
||||
)
|
||||
RETURN;
|
||||
|
||||
DECLARE @Inserted table (AssetID int NOT NULL PRIMARY KEY);
|
||||
|
||||
INSERT dbo.SceneAssetSuggestions
|
||||
(SceneID, AssetID, SuggestionSource, Status, Confidence, Reason, CreatedByUserID)
|
||||
OUTPUT inserted.AssetID INTO @Inserted
|
||||
SELECT @SceneID,
|
||||
requested.AssetID,
|
||||
N'Word Companion',
|
||||
N'Pending',
|
||||
N'High',
|
||||
N'Detected in manuscript text',
|
||||
@UserID
|
||||
FROM @Requested requested
|
||||
WHERE NOT EXISTS
|
||||
(SELECT 1 FROM dbo.AssetEvents ae WHERE ae.SceneID = @SceneID AND ae.StoryAssetID = requested.AssetID)
|
||||
AND NOT EXISTS
|
||||
(SELECT 1 FROM dbo.SceneAssetLocations sal WHERE sal.SceneID = @SceneID AND sal.StoryAssetID = requested.AssetID)
|
||||
AND NOT EXISTS
|
||||
(SELECT 1 FROM dbo.AssetCustodyEvents ace WHERE ace.SceneID = @SceneID AND ace.StoryAssetID = requested.AssetID)
|
||||
AND NOT EXISTS
|
||||
(SELECT 1 FROM dbo.SceneAssetSuggestions suggestion WHERE suggestion.SceneID = @SceneID AND suggestion.AssetID = requested.AssetID);
|
||||
|
||||
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 asset suggestion detected.'
|
||||
WHEN (SELECT COUNT(*) FROM @Inserted) > 1 THEN CONCAT((SELECT COUNT(*) FROM @Inserted), N' asset suggestions detected.')
|
||||
ELSE N'No new asset suggestions.'
|
||||
END AS Message;
|
||||
END;
|
||||
GO
|
||||
@ -215,6 +215,11 @@
|
||||
let characterAliasCache = [];
|
||||
let lastSuggestedSceneId = null;
|
||||
let lastSuggestedCharacterSignature = "";
|
||||
let assetAliasCacheProjectId = null;
|
||||
let assetAliasCacheDocumentGuid = "";
|
||||
let assetAliasCache = [];
|
||||
let lastSuggestedAssetSceneId = null;
|
||||
let lastSuggestedAssetSignature = "";
|
||||
let lastFollowSceneNotificationSignature = "";
|
||||
let lastLinkedSceneTitle = "";
|
||||
let lastLinkedChapterTitle = "";
|
||||
@ -577,6 +582,7 @@
|
||||
plotDirectorStructureCache = null;
|
||||
plotDirectorStructureBookId = null;
|
||||
resetRuntimeCharacterAliasCache();
|
||||
resetRuntimeAssetAliasCache();
|
||||
setRuntimeBookContext(null);
|
||||
};
|
||||
|
||||
@ -2986,7 +2992,10 @@
|
||||
setDocumentMessage("Reading manuscript position...");
|
||||
try {
|
||||
clearPlotDirectorStructureCache();
|
||||
await loadRuntimeCharacterAliases(true);
|
||||
await Promise.all([
|
||||
loadRuntimeCharacterAliases(true),
|
||||
loadRuntimeAssetAliases(true)
|
||||
]);
|
||||
await getBookStructureChapters(book.bookId);
|
||||
const structure = await window.Word.run(async (context) => await readRuntimeDocumentStructure(context, !!book.usesExplicitScenes));
|
||||
renderOutline(structure);
|
||||
@ -3896,6 +3905,14 @@
|
||||
clearPendingCharacterSuggestionDetection();
|
||||
};
|
||||
|
||||
const resetRuntimeAssetAliasCache = () => {
|
||||
assetAliasCacheProjectId = null;
|
||||
assetAliasCacheDocumentGuid = "";
|
||||
assetAliasCache = [];
|
||||
lastSuggestedAssetSceneId = null;
|
||||
lastSuggestedAssetSignature = "";
|
||||
};
|
||||
|
||||
const loadRuntimeCharacterAliases = async (force = false) => {
|
||||
const project = selectedProject();
|
||||
const documentGuid = currentDocumentGuid();
|
||||
@ -3919,6 +3936,29 @@
|
||||
return characterAliasCache;
|
||||
};
|
||||
|
||||
const loadRuntimeAssetAliases = async (force = false) => {
|
||||
const project = selectedProject();
|
||||
const documentGuid = currentDocumentGuid();
|
||||
if (!project || !documentGuid) {
|
||||
assetAliasCache = [];
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!force
|
||||
&& assetAliasCacheProjectId === project.projectId
|
||||
&& assetAliasCacheDocumentGuid === documentGuid
|
||||
&& assetAliasCache.length > 0) {
|
||||
return assetAliasCache;
|
||||
}
|
||||
|
||||
const response = await fetchJson(`/api/word-companion/runtime/assets?projectId=${encodeURIComponent(project.projectId)}&documentGuid=${encodeURIComponent(documentGuid)}`);
|
||||
assetAliasCacheProjectId = project.projectId;
|
||||
assetAliasCacheDocumentGuid = documentGuid;
|
||||
assetAliasCache = Array.isArray(response?.assets) ? response.assets : [];
|
||||
runtimeDebug(`Asset cache loaded: ${assetAliasCache.length} aliases.`);
|
||||
return assetAliasCache;
|
||||
};
|
||||
|
||||
const escapeRegex = (value) => String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
|
||||
const hasWholeWordMatch = (text, matchText) => {
|
||||
@ -3951,6 +3991,26 @@
|
||||
return [...detected.entries()].map(([characterId, name]) => ({ characterId, name }));
|
||||
};
|
||||
|
||||
const detectAssetsInSceneText = (sceneText, aliases) => {
|
||||
if (!sceneText || !Array.isArray(aliases) || aliases.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const detected = new Map();
|
||||
for (const item of aliases) {
|
||||
const assetId = Number.parseInt(item.assetId || "", 10);
|
||||
if (!Number.isInteger(assetId) || assetId <= 0 || detected.has(assetId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hasWholeWordMatch(sceneText, item.matchText)) {
|
||||
detected.set(assetId, item.name || item.matchText || "Asset");
|
||||
}
|
||||
}
|
||||
|
||||
return [...detected.entries()].map(([assetId, name]) => ({ assetId, name }));
|
||||
};
|
||||
|
||||
const finishCharacterSuggestionDetection = () => {
|
||||
characterSuggestionInFlight = false;
|
||||
if (characterSuggestionPending) {
|
||||
@ -3975,18 +4035,21 @@
|
||||
characterSuggestionPending = false;
|
||||
|
||||
try {
|
||||
const [sceneText, aliases] = await Promise.all([
|
||||
const [sceneText, characterAliases, assetAliases] = await Promise.all([
|
||||
readCachedCurrentSceneText(),
|
||||
loadRuntimeCharacterAliases()
|
||||
loadRuntimeCharacterAliases(),
|
||||
loadRuntimeAssetAliases()
|
||||
]);
|
||||
const detected = detectCharactersInSceneText(sceneText, aliases);
|
||||
const detected = detectCharactersInSceneText(sceneText, characterAliases);
|
||||
const characterIds = detected.map((item) => item.characterId);
|
||||
const signature = characterIds.slice().sort((left, right) => left - right).join(",");
|
||||
const detectedAssets = detectAssetsInSceneText(sceneText, assetAliases);
|
||||
const assetIds = detectedAssets.map((item) => item.assetId);
|
||||
const assetSignature = assetIds.slice().sort((left, right) => left - right).join(",");
|
||||
|
||||
if (!signature || (lastSuggestedSceneId === resolvedSceneId && lastSuggestedCharacterSignature === signature)) {
|
||||
runtimeDebug("Character suggestions skipped.");
|
||||
return;
|
||||
}
|
||||
|
||||
} else {
|
||||
const response = await postJson("/api/word-companion/runtime/character-suggestions", {
|
||||
documentGuid: currentDocumentGuid(),
|
||||
sceneId: resolvedSceneId,
|
||||
@ -4001,8 +4064,29 @@
|
||||
setDocumentMessage(names ? `Characters detected: ${names}${suffix}` : response.message);
|
||||
}
|
||||
runtimeDebug(response?.message || "Character suggestions submitted.");
|
||||
}
|
||||
|
||||
if (!assetSignature || (lastSuggestedAssetSceneId === resolvedSceneId && lastSuggestedAssetSignature === assetSignature)) {
|
||||
runtimeDebug("Asset suggestions skipped.");
|
||||
} else {
|
||||
runtimeDebug(`Asset matches found: ${detectedAssets.map((item) => item.name).join(", ")}`);
|
||||
const assetResponse = await postJson("/api/word-companion/runtime/asset-suggestions", {
|
||||
documentGuid: currentDocumentGuid(),
|
||||
sceneId: resolvedSceneId,
|
||||
assetIds
|
||||
});
|
||||
|
||||
lastSuggestedAssetSceneId = resolvedSceneId;
|
||||
lastSuggestedAssetSignature = assetSignature;
|
||||
if ((assetResponse?.createdCount || 0) > 0) {
|
||||
const names = detectedAssets.slice(0, 3).map((item) => item.name).join(", ");
|
||||
const suffix = detectedAssets.length > 3 ? ` +${detectedAssets.length - 3}` : "";
|
||||
setDocumentMessage(names ? `Assets detected: ${names}${suffix}` : assetResponse.message);
|
||||
}
|
||||
runtimeDebug(assetResponse?.message || "Asset suggestions submitted.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Unable to detect character suggestions.", error);
|
||||
console.error("Unable to detect runtime suggestions.", error);
|
||||
setDiagnostics({ lastError: errorText(error) });
|
||||
} finally {
|
||||
finishCharacterSuggestionDetection();
|
||||
@ -4480,7 +4564,10 @@
|
||||
setRefreshCurrentSceneBusy(true);
|
||||
setSceneTrackingState("Updating...");
|
||||
try {
|
||||
await loadRuntimeCharacterAliases(true);
|
||||
await Promise.all([
|
||||
loadRuntimeCharacterAliases(true),
|
||||
loadRuntimeAssetAliases(true)
|
||||
]);
|
||||
const scanned = await refreshDocumentStructure();
|
||||
if (!scanned) {
|
||||
return;
|
||||
|
||||
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user