Add character aliases/identity links, move-beats, keyboard help overlay
- Characters can carry aliases and be linked as the same underlying person (canonical SameCharacterAsId, optional reveal chapter/note), surfaced through the API, MCP tools, agent toolset, and web UI. - Characters page redesigned as a filterable/sortable table (name+ aliases, role, importance, occupation, tags) instead of a sidebar list, to stay usable as the cast grows. - Beats can be moved between chapters (BeatService.MoveAsync + MCP/ agent tool + endpoint). - Add a keyboard-shortcuts help overlay (HelpButton/HelpOverlayContext) wired into the project layout. - CLAUDE.md: require every frontend component to carry a unique id attribute; apply it to CharacterMultiSelect and MarkdownEditor.
This commit is contained in:
@@ -27,6 +27,12 @@ public class NovelApiClient(HttpClient http, ILogger<NovelApiClient> logger)
|
||||
Content = JsonContent.Create(body, options: Options)
|
||||
}, ct);
|
||||
|
||||
public Task<CallToolResult> PutAsync(string path, object body, CancellationToken ct = default) =>
|
||||
SendAsync(new HttpRequestMessage(HttpMethod.Put, path)
|
||||
{
|
||||
Content = JsonContent.Create(body, options: Options)
|
||||
}, ct);
|
||||
|
||||
public Task<CallToolResult> DeleteAsync(string path, CancellationToken ct = default) => SendAsync(new HttpRequestMessage(HttpMethod.Delete, path), ct);
|
||||
|
||||
private async Task<CallToolResult> SendAsync(HttpRequestMessage request, CancellationToken ct)
|
||||
|
||||
@@ -78,4 +78,15 @@ public static class BeatTools
|
||||
[Description("Beat ids in their new order.")] Guid[] beatIds,
|
||||
CancellationToken ct) =>
|
||||
api.PostAsync($"/api/chapters/{chapterId}/beats/reorder", new { beatIds }, ct);
|
||||
|
||||
[McpServerTool(Name = "move_beats")]
|
||||
[Description("Move one or more beats from one chapter to another, appending them to the "
|
||||
+ "target chapter's end in the order given.")]
|
||||
public static Task<CallToolResult> MoveBeats(
|
||||
NovelApiClient api,
|
||||
[Description("The beats' current chapter id.")] Guid chapterId,
|
||||
[Description("Id of the chapter to move the beats into.")] Guid targetChapterId,
|
||||
[Description("Ids of the beats to move.")] Guid[] beatIds,
|
||||
CancellationToken ct) =>
|
||||
api.PostAsync($"/api/chapters/{chapterId}/beats/move", new { targetChapterId, beatIds }, ct);
|
||||
}
|
||||
|
||||
@@ -48,7 +48,8 @@ public static class CharacterTools
|
||||
[Description("How they change over the course of the book.")] string? arcSummary = null,
|
||||
[Description("Speech patterns and register that make their dialogue theirs.")] string? voice = null,
|
||||
[Description("Anything else worth recording.")] string? notes = null,
|
||||
[Description("Tags for cross-referencing. Unknown tags are created.")] string[]? tags = null) =>
|
||||
[Description("Tags for cross-referencing. Unknown tags are created.")] string[]? tags = null,
|
||||
[Description("Other names this character is known by.")] string[]? aliases = null) =>
|
||||
api.PostAsync($"/api/projects/{projectId}/characters", new
|
||||
{
|
||||
name,
|
||||
@@ -67,7 +68,8 @@ public static class CharacterTools
|
||||
arcSummary,
|
||||
voice,
|
||||
notes,
|
||||
tags
|
||||
tags,
|
||||
aliases
|
||||
}, ct);
|
||||
|
||||
[McpServerTool(Name = "update_character")]
|
||||
@@ -94,7 +96,8 @@ public static class CharacterTools
|
||||
[Description("How they change over the course of the book.")] string? arcSummary = null,
|
||||
[Description("Speech patterns and register.")] string? voice = null,
|
||||
[Description("Anything else worth recording.")] string? notes = null,
|
||||
[Description("Tags for cross-referencing. Replaces the existing tags.")] string[]? tags = null) =>
|
||||
[Description("Tags for cross-referencing. Replaces the existing tags.")] string[]? tags = null,
|
||||
[Description("Other names this character is known by. Replaces the existing aliases.")] string[]? aliases = null) =>
|
||||
api.PatchAsync($"/api/characters/{characterId}", new
|
||||
{
|
||||
name,
|
||||
@@ -113,7 +116,8 @@ public static class CharacterTools
|
||||
arcSummary,
|
||||
voice,
|
||||
notes,
|
||||
tags
|
||||
tags,
|
||||
aliases
|
||||
}, ct);
|
||||
|
||||
[McpServerTool(Name = "get_character_beats")]
|
||||
@@ -191,4 +195,27 @@ public static class CharacterTools
|
||||
[Description("What the relationship is like, and where it is headed.")] string? description = null) =>
|
||||
api.PostAsync($"/api/characters/{characterId}/relationships",
|
||||
new { relatedCharacterId, relationshipType, description }, ct);
|
||||
|
||||
[McpServerTool(Name = "link_character_identity")]
|
||||
[Description("Record that this character is really another character — e.g. a character introduced "
|
||||
+ "under one name who is later revealed to be a character already in the project under "
|
||||
+ "another name. Both characters keep their own dossier and beats; the canonical identity "
|
||||
+ "is whichever character you link to.")]
|
||||
public static Task<CallToolResult> LinkCharacterIdentity(
|
||||
NovelApiClient api,
|
||||
[Description("Id of the character being revealed as someone else.")] Guid characterId,
|
||||
[Description("Id of the character this one really is.")] Guid sameCharacterAsId,
|
||||
CancellationToken ct,
|
||||
[Description("Id of the chapter where the reveal happens, if any.")] Guid? revealedInChapterId = null,
|
||||
[Description("Context on the reveal, e.g. how and why the disguise held.")] string? note = null) =>
|
||||
api.PutAsync($"/api/characters/{characterId}/identity",
|
||||
new { sameCharacterAsId, revealedInChapterId, note }, ct);
|
||||
|
||||
[McpServerTool(Name = "unlink_character_identity")]
|
||||
[Description("Remove a character's identity link, restoring it to its own separate identity.")]
|
||||
public static Task<CallToolResult> UnlinkCharacterIdentity(
|
||||
NovelApiClient api,
|
||||
[Description("The character's id.")] Guid characterId,
|
||||
CancellationToken ct) =>
|
||||
api.DeleteAsync($"/api/characters/{characterId}/identity", ct);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user