Simplify character dossier fields, add ShowPronouns setting
Collapse Want/Need into Motivation and Internal/External Conflict into Conflict, drop Arc summary field, and move tags below open questions on the character page. Gate Pronouns display behind new UiSettings:ShowPronouns config (default off).
This commit is contained in:
@@ -146,11 +146,8 @@ public class NovelAgentToolset(
|
|||||||
JsonInput.String(input, "appearance"),
|
JsonInput.String(input, "appearance"),
|
||||||
JsonInput.String(input, "personality"),
|
JsonInput.String(input, "personality"),
|
||||||
JsonInput.String(input, "backstory"),
|
JsonInput.String(input, "backstory"),
|
||||||
JsonInput.String(input, "want"),
|
JsonInput.String(input, "motivation"),
|
||||||
JsonInput.String(input, "need"),
|
JsonInput.String(input, "conflict"),
|
||||||
JsonInput.String(input, "internal_conflict"),
|
|
||||||
JsonInput.String(input, "external_conflict"),
|
|
||||||
JsonInput.String(input, "arc_summary"),
|
|
||||||
JsonInput.String(input, "voice"),
|
JsonInput.String(input, "voice"),
|
||||||
JsonInput.String(input, "notes"),
|
JsonInput.String(input, "notes"),
|
||||||
JsonInput.Strings(input, "tags"),
|
JsonInput.Strings(input, "tags"),
|
||||||
@@ -177,11 +174,8 @@ public class NovelAgentToolset(
|
|||||||
JsonInput.String(input, "appearance"),
|
JsonInput.String(input, "appearance"),
|
||||||
JsonInput.String(input, "personality"),
|
JsonInput.String(input, "personality"),
|
||||||
JsonInput.String(input, "backstory"),
|
JsonInput.String(input, "backstory"),
|
||||||
JsonInput.String(input, "want"),
|
JsonInput.String(input, "motivation"),
|
||||||
JsonInput.String(input, "need"),
|
JsonInput.String(input, "conflict"),
|
||||||
JsonInput.String(input, "internal_conflict"),
|
|
||||||
JsonInput.String(input, "external_conflict"),
|
|
||||||
JsonInput.String(input, "arc_summary"),
|
|
||||||
JsonInput.String(input, "voice"),
|
JsonInput.String(input, "voice"),
|
||||||
JsonInput.String(input, "notes"),
|
JsonInput.String(input, "notes"),
|
||||||
JsonInput.Strings(input, "tags"),
|
JsonInput.Strings(input, "tags"),
|
||||||
@@ -667,11 +661,8 @@ public class NovelAgentToolset(
|
|||||||
.Str("appearance", "How they look.")
|
.Str("appearance", "How they look.")
|
||||||
.Str("personality", "Temperament, habits, how they treat people.")
|
.Str("personality", "Temperament, habits, how they treat people.")
|
||||||
.Str("backstory", "History that shapes who they are now.")
|
.Str("backstory", "History that shapes who they are now.")
|
||||||
.Str("want", "What they consciously pursue.")
|
.Str("motivation", "What they consciously pursue, weighed against what they actually need.")
|
||||||
.Str("need", "What they actually need, usually at odds with what they want.")
|
.Str("conflict", "The war inside them and what in the world opposes them.")
|
||||||
.Str("internal_conflict", "The war inside them.")
|
|
||||||
.Str("external_conflict", "What in the world opposes them.")
|
|
||||||
.Str("arc_summary", "How they change over the course of the book.")
|
|
||||||
.Str("voice", "Speech patterns and register that make their dialogue theirs.")
|
.Str("voice", "Speech patterns and register that make their dialogue theirs.")
|
||||||
.Str("notes", "Anything else worth recording.")
|
.Str("notes", "Anything else worth recording.")
|
||||||
.StringArray("tags", "Tags for cross-referencing. Replaces the existing tags.")
|
.StringArray("tags", "Tags for cross-referencing. Replaces the existing tags.")
|
||||||
|
|||||||
@@ -26,14 +26,9 @@ public class Character
|
|||||||
public string? Personality { get; set; }
|
public string? Personality { get; set; }
|
||||||
public string? Backstory { get; set; }
|
public string? Backstory { get; set; }
|
||||||
|
|
||||||
public string? Want { get; set; }
|
public string? Motivation { get; set; }
|
||||||
|
|
||||||
public string? Need { get; set; }
|
public string? Conflict { get; set; }
|
||||||
|
|
||||||
public string? InternalConflict { get; set; }
|
|
||||||
public string? ExternalConflict { get; set; }
|
|
||||||
|
|
||||||
public string? ArcSummary { get; set; }
|
|
||||||
|
|
||||||
public string? Voice { get; set; }
|
public string? Voice { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -16,11 +16,8 @@ public record CharacterResponse(
|
|||||||
string? Appearance,
|
string? Appearance,
|
||||||
string? Personality,
|
string? Personality,
|
||||||
string? Backstory,
|
string? Backstory,
|
||||||
string? Want,
|
string? Motivation,
|
||||||
string? Need,
|
string? Conflict,
|
||||||
string? InternalConflict,
|
|
||||||
string? ExternalConflict,
|
|
||||||
string? ArcSummary,
|
|
||||||
string? Voice,
|
string? Voice,
|
||||||
string? Notes,
|
string? Notes,
|
||||||
IReadOnlyList<string> Aliases,
|
IReadOnlyList<string> Aliases,
|
||||||
@@ -54,11 +51,8 @@ public record CreateCharacterRequest(
|
|||||||
string? Appearance = null,
|
string? Appearance = null,
|
||||||
string? Personality = null,
|
string? Personality = null,
|
||||||
string? Backstory = null,
|
string? Backstory = null,
|
||||||
string? Want = null,
|
string? Motivation = null,
|
||||||
string? Need = null,
|
string? Conflict = null,
|
||||||
string? InternalConflict = null,
|
|
||||||
string? ExternalConflict = null,
|
|
||||||
string? ArcSummary = null,
|
|
||||||
string? Voice = null,
|
string? Voice = null,
|
||||||
string? Notes = null,
|
string? Notes = null,
|
||||||
IReadOnlyList<string>? Tags = null,
|
IReadOnlyList<string>? Tags = null,
|
||||||
@@ -73,7 +67,7 @@ public class CreateCharacterRequestValidator : IModelValidator<CreateCharacterRe
|
|||||||
result.AddRequiredTextErrors("Name", "Name", model.Name, 200);
|
result.AddRequiredTextErrors("Name", "Name", model.Name, 200);
|
||||||
CharacterValidation.OptionalFields(
|
CharacterValidation.OptionalFields(
|
||||||
model.Age, model.Pronouns, model.Occupation, model.Appearance, model.Personality, model.Backstory,
|
model.Age, model.Pronouns, model.Occupation, model.Appearance, model.Personality, model.Backstory,
|
||||||
model.Want, model.Need, model.InternalConflict, model.ExternalConflict, model.ArcSummary, model.Voice,
|
model.Motivation, model.Conflict, model.Voice,
|
||||||
model.Notes, model.Tags, model.Aliases, result);
|
model.Notes, model.Tags, model.Aliases, result);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -90,11 +84,8 @@ public record UpdateCharacterRequest(
|
|||||||
string? Appearance = null,
|
string? Appearance = null,
|
||||||
string? Personality = null,
|
string? Personality = null,
|
||||||
string? Backstory = null,
|
string? Backstory = null,
|
||||||
string? Want = null,
|
string? Motivation = null,
|
||||||
string? Need = null,
|
string? Conflict = null,
|
||||||
string? InternalConflict = null,
|
|
||||||
string? ExternalConflict = null,
|
|
||||||
string? ArcSummary = null,
|
|
||||||
string? Voice = null,
|
string? Voice = null,
|
||||||
string? Notes = null,
|
string? Notes = null,
|
||||||
IReadOnlyList<string>? Tags = null,
|
IReadOnlyList<string>? Tags = null,
|
||||||
@@ -109,7 +100,7 @@ public class UpdateCharacterRequestValidator : IModelValidator<UpdateCharacterRe
|
|||||||
result.AddUnclearableTextErrors("Name", "Name", model.Name, "a character", 200);
|
result.AddUnclearableTextErrors("Name", "Name", model.Name, "a character", 200);
|
||||||
CharacterValidation.OptionalFields(
|
CharacterValidation.OptionalFields(
|
||||||
model.Age, model.Pronouns, model.Occupation, model.Appearance, model.Personality, model.Backstory,
|
model.Age, model.Pronouns, model.Occupation, model.Appearance, model.Personality, model.Backstory,
|
||||||
model.Want, model.Need, model.InternalConflict, model.ExternalConflict, model.ArcSummary, model.Voice,
|
model.Motivation, model.Conflict, model.Voice,
|
||||||
model.Notes, model.Tags, model.Aliases, result);
|
model.Notes, model.Tags, model.Aliases, result);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -120,7 +111,7 @@ file static class CharacterValidation
|
|||||||
{
|
{
|
||||||
public static void OptionalFields(
|
public static void OptionalFields(
|
||||||
string? age, string? pronouns, string? occupation, string? appearance, string? personality, string? backstory,
|
string? age, string? pronouns, string? occupation, string? appearance, string? personality, string? backstory,
|
||||||
string? want, string? need, string? internalConflict, string? externalConflict, string? arcSummary, string? voice,
|
string? motivation, string? conflict, string? voice,
|
||||||
string? notes, IReadOnlyList<string>? tags, IReadOnlyList<string>? aliases, ValidationResult result)
|
string? notes, IReadOnlyList<string>? tags, IReadOnlyList<string>? aliases, ValidationResult result)
|
||||||
{
|
{
|
||||||
Cap(age, "Age", 100, result);
|
Cap(age, "Age", 100, result);
|
||||||
@@ -129,11 +120,8 @@ file static class CharacterValidation
|
|||||||
Cap(appearance, "Appearance", 20000, result);
|
Cap(appearance, "Appearance", 20000, result);
|
||||||
Cap(personality, "Personality", 20000, result);
|
Cap(personality, "Personality", 20000, result);
|
||||||
Cap(backstory, "Backstory", 20000, result);
|
Cap(backstory, "Backstory", 20000, result);
|
||||||
Cap(want, "Want", 2000, result);
|
Cap(motivation, "Motivation", 2000, result);
|
||||||
Cap(need, "Need", 2000, result);
|
Cap(conflict, "Conflict", 2000, result);
|
||||||
Cap(internalConflict, "InternalConflict", 2000, result);
|
|
||||||
Cap(externalConflict, "ExternalConflict", 2000, result);
|
|
||||||
Cap(arcSummary, "ArcSummary", 20000, result);
|
|
||||||
Cap(voice, "Voice", 2000, result);
|
Cap(voice, "Voice", 2000, result);
|
||||||
Cap(notes, "Notes", 20000, result);
|
Cap(notes, "Notes", 20000, result);
|
||||||
|
|
||||||
@@ -300,8 +288,7 @@ public static class CharacterMapping
|
|||||||
{
|
{
|
||||||
public static CharacterResponse ToResponse(this Character c) => new(
|
public static CharacterResponse ToResponse(this Character c) => new(
|
||||||
c.Id, c.NovelId, c.Name, c.Role, c.Importance, c.Age, c.Pronouns, c.Occupation,
|
c.Id, c.NovelId, c.Name, c.Role, c.Importance, c.Age, c.Pronouns, c.Occupation,
|
||||||
c.Appearance, c.Personality, c.Backstory, c.Want, c.Need,
|
c.Appearance, c.Personality, c.Backstory, c.Motivation, c.Conflict, c.Voice, c.Notes,
|
||||||
c.InternalConflict, c.ExternalConflict, c.ArcSummary, c.Voice, c.Notes,
|
|
||||||
[.. c.Aliases],
|
[.. c.Aliases],
|
||||||
c.SameCharacterAsId,
|
c.SameCharacterAsId,
|
||||||
c.SameCharacterAs?.Name,
|
c.SameCharacterAs?.Name,
|
||||||
|
|||||||
@@ -84,11 +84,8 @@ public class CharacterService(
|
|||||||
Appearance = request.Appearance,
|
Appearance = request.Appearance,
|
||||||
Personality = request.Personality,
|
Personality = request.Personality,
|
||||||
Backstory = request.Backstory,
|
Backstory = request.Backstory,
|
||||||
Want = request.Want,
|
Motivation = request.Motivation,
|
||||||
Need = request.Need,
|
Conflict = request.Conflict,
|
||||||
InternalConflict = request.InternalConflict,
|
|
||||||
ExternalConflict = request.ExternalConflict,
|
|
||||||
ArcSummary = request.ArcSummary,
|
|
||||||
Voice = request.Voice,
|
Voice = request.Voice,
|
||||||
Notes = request.Notes
|
Notes = request.Notes
|
||||||
};
|
};
|
||||||
@@ -134,11 +131,8 @@ public class CharacterService(
|
|||||||
character.Appearance = Patch.Apply(character.Appearance, request.Appearance);
|
character.Appearance = Patch.Apply(character.Appearance, request.Appearance);
|
||||||
character.Personality = Patch.Apply(character.Personality, request.Personality);
|
character.Personality = Patch.Apply(character.Personality, request.Personality);
|
||||||
character.Backstory = Patch.Apply(character.Backstory, request.Backstory);
|
character.Backstory = Patch.Apply(character.Backstory, request.Backstory);
|
||||||
character.Want = Patch.Apply(character.Want, request.Want);
|
character.Motivation = Patch.Apply(character.Motivation, request.Motivation);
|
||||||
character.Need = Patch.Apply(character.Need, request.Need);
|
character.Conflict = Patch.Apply(character.Conflict, request.Conflict);
|
||||||
character.InternalConflict = Patch.Apply(character.InternalConflict, request.InternalConflict);
|
|
||||||
character.ExternalConflict = Patch.Apply(character.ExternalConflict, request.ExternalConflict);
|
|
||||||
character.ArcSummary = Patch.Apply(character.ArcSummary, request.ArcSummary);
|
|
||||||
character.Voice = Patch.Apply(character.Voice, request.Voice);
|
character.Voice = Patch.Apply(character.Voice, request.Voice);
|
||||||
character.Notes = Patch.Apply(character.Notes, request.Notes);
|
character.Notes = Patch.Apply(character.Notes, request.Notes);
|
||||||
character.UpdatedAt = DateTimeOffset.UtcNow;
|
character.UpdatedAt = DateTimeOffset.UtcNow;
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ public static class NovellyServiceRegistration
|
|||||||
{
|
{
|
||||||
public static IServiceCollection AddNovelly(this IServiceCollection services, IConfiguration configuration)
|
public static IServiceCollection AddNovelly(this IServiceCollection services, IConfiguration configuration)
|
||||||
{
|
{
|
||||||
|
services.Configure<UiSettingsOptions>(configuration.GetSection(UiSettingsOptions.SectionName));
|
||||||
|
|
||||||
var connectionString = configuration.GetConnectionString("Novel")
|
var connectionString = configuration.GetConnectionString("Novel")
|
||||||
?? "Data Source=novel.db";
|
?? "Data Source=novel.db";
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Novelly.Api.Common;
|
||||||
|
|
||||||
|
public static class UiSettingsEndpoints
|
||||||
|
{
|
||||||
|
public static IEndpointRouteBuilder MapUiSettingsEndpoints(this IEndpointRouteBuilder app)
|
||||||
|
{
|
||||||
|
app.MapGet("/api/ui-settings", (IOptions<UiSettingsOptions> options) =>
|
||||||
|
Results.Ok(new UiSettingsResponse(options.Value.ShowPronouns)))
|
||||||
|
.WithTags("UiSettings")
|
||||||
|
.AllowAnonymous();
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record UiSettingsResponse(bool ShowPronouns);
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Novelly.Api.Common;
|
||||||
|
|
||||||
|
public class UiSettingsOptions
|
||||||
|
{
|
||||||
|
public const string SectionName = "UiSettings";
|
||||||
|
|
||||||
|
public bool ShowPronouns { get; set; }
|
||||||
|
}
|
||||||
Generated
+1223
File diff suppressed because it is too large
Load Diff
+86
@@ -0,0 +1,86 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Novelly.Api.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class CollapseCharacterMotivationAndConflict : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.Sql(
|
||||||
|
"""
|
||||||
|
UPDATE Characters SET Want = CASE
|
||||||
|
WHEN Want IS NULL OR Want = '' THEN Need
|
||||||
|
WHEN Need IS NULL OR Need = '' THEN Want
|
||||||
|
ELSE Want || char(10) || char(10) || Need
|
||||||
|
END;
|
||||||
|
""");
|
||||||
|
|
||||||
|
migrationBuilder.Sql(
|
||||||
|
"""
|
||||||
|
UPDATE Characters SET InternalConflict = CASE
|
||||||
|
WHEN InternalConflict IS NULL OR InternalConflict = '' THEN ExternalConflict
|
||||||
|
WHEN ExternalConflict IS NULL OR ExternalConflict = '' THEN InternalConflict
|
||||||
|
ELSE InternalConflict || char(10) || char(10) || ExternalConflict
|
||||||
|
END;
|
||||||
|
""");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ArcSummary",
|
||||||
|
table: "Characters");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ExternalConflict",
|
||||||
|
table: "Characters");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Need",
|
||||||
|
table: "Characters");
|
||||||
|
|
||||||
|
migrationBuilder.RenameColumn(
|
||||||
|
name: "Want",
|
||||||
|
table: "Characters",
|
||||||
|
newName: "Motivation");
|
||||||
|
|
||||||
|
migrationBuilder.RenameColumn(
|
||||||
|
name: "InternalConflict",
|
||||||
|
table: "Characters",
|
||||||
|
newName: "Conflict");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.RenameColumn(
|
||||||
|
name: "Motivation",
|
||||||
|
table: "Characters",
|
||||||
|
newName: "Want");
|
||||||
|
|
||||||
|
migrationBuilder.RenameColumn(
|
||||||
|
name: "Conflict",
|
||||||
|
table: "Characters",
|
||||||
|
newName: "InternalConflict");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "ArcSummary",
|
||||||
|
table: "Characters",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "ExternalConflict",
|
||||||
|
table: "Characters",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Need",
|
||||||
|
table: "Characters",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -333,18 +333,15 @@ namespace Novelly.Api.Data.Migrations
|
|||||||
b.Property<string>("Appearance")
|
b.Property<string>("Appearance")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("ArcSummary")
|
b.Property<string>("Backstory")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("Backstory")
|
b.Property<string>("Conflict")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<long>("CreatedAt")
|
b.Property<long>("CreatedAt")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<string>("ExternalConflict")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("IdentityNote")
|
b.Property<string>("IdentityNote")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
@@ -353,7 +350,7 @@ namespace Novelly.Api.Data.Migrations
|
|||||||
.HasMaxLength(32)
|
.HasMaxLength(32)
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("InternalConflict")
|
b.Property<string>("Motivation")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("Name")
|
b.Property<string>("Name")
|
||||||
@@ -361,9 +358,6 @@ namespace Novelly.Api.Data.Migrations
|
|||||||
.HasMaxLength(200)
|
.HasMaxLength(200)
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("Need")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Notes")
|
b.Property<string>("Notes")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
@@ -396,9 +390,6 @@ namespace Novelly.Api.Data.Migrations
|
|||||||
b.Property<string>("Voice")
|
b.Property<string>("Voice")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("Want")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("NovelId");
|
b.HasIndex("NovelId");
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ public class ImportAgentToolset(
|
|||||||
Occupation: JsonInput.String(input, "occupation"),
|
Occupation: JsonInput.String(input, "occupation"),
|
||||||
Appearance: JsonInput.String(input, "appearance"),
|
Appearance: JsonInput.String(input, "appearance"),
|
||||||
Backstory: JsonInput.String(input, "backstory"),
|
Backstory: JsonInput.String(input, "backstory"),
|
||||||
Want: JsonInput.String(input, "want"),
|
Motivation: JsonInput.String(input, "motivation"),
|
||||||
Notes: JsonInput.String(input, "notes")), ct);
|
Notes: JsonInput.String(input, "notes")), ct);
|
||||||
|
|
||||||
return created is null
|
return created is null
|
||||||
@@ -264,7 +264,7 @@ public class ImportAgentToolset(
|
|||||||
Occupation: JsonInput.String(input, "occupation"),
|
Occupation: JsonInput.String(input, "occupation"),
|
||||||
Appearance: JsonInput.String(input, "appearance"),
|
Appearance: JsonInput.String(input, "appearance"),
|
||||||
Backstory: JsonInput.String(input, "backstory"),
|
Backstory: JsonInput.String(input, "backstory"),
|
||||||
Want: JsonInput.String(input, "want"),
|
Motivation: JsonInput.String(input, "motivation"),
|
||||||
Notes: JsonInput.String(input, "notes")), ct);
|
Notes: JsonInput.String(input, "notes")), ct);
|
||||||
|
|
||||||
return updated is null
|
return updated is null
|
||||||
@@ -379,6 +379,6 @@ public class ImportAgentToolset(
|
|||||||
.Str("occupation", "The italic tagline under the heading.")
|
.Str("occupation", "The italic tagline under the heading.")
|
||||||
.Str("appearance", "The ## Appearance section.")
|
.Str("appearance", "The ## Appearance section.")
|
||||||
.Str("backstory", "The ## Background section.")
|
.Str("backstory", "The ## Background section.")
|
||||||
.Str("want", "The ## Motivation section.")
|
.Str("motivation", "The ## Motivation section.")
|
||||||
.Str("notes", "The ## Notes section, if present.");
|
.Str("notes", "The ## Notes section, if present.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ if (app.Environment.IsDevelopment())
|
|||||||
app.MapDefaultEndpoints();
|
app.MapDefaultEndpoints();
|
||||||
|
|
||||||
app.MapGet("/api/health", () => Results.Ok(new { status = "ok" })).WithTags("Health").AllowAnonymous();
|
app.MapGet("/api/health", () => Results.Ok(new { status = "ok" })).WithTags("Health").AllowAnonymous();
|
||||||
|
app.MapUiSettingsEndpoints();
|
||||||
|
|
||||||
app.MapUserEndpoints();
|
app.MapUserEndpoints();
|
||||||
app.MapNovelMemberEndpoints();
|
app.MapNovelMemberEndpoints();
|
||||||
|
|||||||
@@ -28,9 +28,12 @@
|
|||||||
"Origins": [ "http://localhost:5173" ]
|
"Origins": [ "http://localhost:5173" ]
|
||||||
},
|
},
|
||||||
"Agent": {
|
"Agent": {
|
||||||
"Model": "claude-opus-5",
|
"Model": "claude-sonnet-5",
|
||||||
"MaxTokens": 16000,
|
"MaxTokens": 16000,
|
||||||
"Effort": "high",
|
"Effort": "high",
|
||||||
"MaxIterations": 12
|
"MaxIterations": 12
|
||||||
|
},
|
||||||
|
"UiSettings": {
|
||||||
|
"ShowPronouns": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,11 +41,8 @@ public static class CharacterTools
|
|||||||
[Description("How they look.")] string? appearance = null,
|
[Description("How they look.")] string? appearance = null,
|
||||||
[Description("Temperament, habits, how they treat people.")] string? personality = null,
|
[Description("Temperament, habits, how they treat people.")] string? personality = null,
|
||||||
[Description("History that shapes who they are now.")] string? backstory = null,
|
[Description("History that shapes who they are now.")] string? backstory = null,
|
||||||
[Description("What they consciously pursue.")] string? want = null,
|
[Description("What they consciously pursue, weighed against what they actually need.")] string? motivation = null,
|
||||||
[Description("What they actually need, usually at odds with what they want.")] string? need = null,
|
[Description("The war inside them and what in the world opposes them.")] string? conflict = null,
|
||||||
[Description("The war inside them.")] string? internalConflict = null,
|
|
||||||
[Description("What in the world opposes them.")] string? externalConflict = null,
|
|
||||||
[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("Speech patterns and register that make their dialogue theirs.")] string? voice = null,
|
||||||
[Description("Anything else worth recording.")] string? notes = 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,
|
||||||
@@ -61,11 +58,8 @@ public static class CharacterTools
|
|||||||
appearance,
|
appearance,
|
||||||
personality,
|
personality,
|
||||||
backstory,
|
backstory,
|
||||||
want,
|
motivation,
|
||||||
need,
|
conflict,
|
||||||
internalConflict,
|
|
||||||
externalConflict,
|
|
||||||
arcSummary,
|
|
||||||
voice,
|
voice,
|
||||||
notes,
|
notes,
|
||||||
tags,
|
tags,
|
||||||
@@ -89,11 +83,8 @@ public static class CharacterTools
|
|||||||
[Description("How they look.")] string? appearance = null,
|
[Description("How they look.")] string? appearance = null,
|
||||||
[Description("Temperament, habits, how they treat people.")] string? personality = null,
|
[Description("Temperament, habits, how they treat people.")] string? personality = null,
|
||||||
[Description("History that shapes who they are now.")] string? backstory = null,
|
[Description("History that shapes who they are now.")] string? backstory = null,
|
||||||
[Description("What they consciously pursue.")] string? want = null,
|
[Description("What they consciously pursue, weighed against what they actually need.")] string? motivation = null,
|
||||||
[Description("What they actually need.")] string? need = null,
|
[Description("The war inside them and what in the world opposes them.")] string? conflict = null,
|
||||||
[Description("The war inside them.")] string? internalConflict = null,
|
|
||||||
[Description("What in the world opposes them.")] string? externalConflict = null,
|
|
||||||
[Description("How they change over the course of the book.")] string? arcSummary = null,
|
|
||||||
[Description("Speech patterns and register.")] string? voice = null,
|
[Description("Speech patterns and register.")] string? voice = null,
|
||||||
[Description("Anything else worth recording.")] string? notes = 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,
|
||||||
@@ -109,11 +100,8 @@ public static class CharacterTools
|
|||||||
appearance,
|
appearance,
|
||||||
personality,
|
personality,
|
||||||
backstory,
|
backstory,
|
||||||
want,
|
motivation,
|
||||||
need,
|
conflict,
|
||||||
internalConflict,
|
|
||||||
externalConflict,
|
|
||||||
arcSummary,
|
|
||||||
voice,
|
voice,
|
||||||
notes,
|
notes,
|
||||||
tags,
|
tags,
|
||||||
|
|||||||
@@ -23,11 +23,13 @@ import type {
|
|||||||
NovelSummary,
|
NovelSummary,
|
||||||
TagReferences,
|
TagReferences,
|
||||||
TagSummary,
|
TagSummary,
|
||||||
|
UiSettings,
|
||||||
User,
|
User,
|
||||||
} from './types'
|
} from './types'
|
||||||
|
|
||||||
export const keys = {
|
export const keys = {
|
||||||
me: ['me'] as const,
|
me: ['me'] as const,
|
||||||
|
uiSettings: ['ui-settings'] as const,
|
||||||
members: (novelId: string) => ['novels', novelId, 'members'] as const,
|
members: (novelId: string) => ['novels', novelId, 'members'] as const,
|
||||||
novels: ['novels'] as const,
|
novels: ['novels'] as const,
|
||||||
genres: ['genres'] as const,
|
genres: ['genres'] as const,
|
||||||
@@ -46,6 +48,9 @@ export const keys = {
|
|||||||
importJob: (id: string) => ['imports', id] as const,
|
importJob: (id: string) => ['imports', id] as const,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useUiSettings = () =>
|
||||||
|
useQuery({ queryKey: keys.uiSettings, queryFn: () => api.get<UiSettings>('/api/ui-settings') })
|
||||||
|
|
||||||
export const useMe = () =>
|
export const useMe = () =>
|
||||||
useQuery({
|
useQuery({
|
||||||
queryKey: keys.me,
|
queryKey: keys.me,
|
||||||
|
|||||||
@@ -49,6 +49,10 @@ export interface User {
|
|||||||
globalRole: GlobalRole
|
globalRole: GlobalRole
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UiSettings {
|
||||||
|
showPronouns: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export interface NovelMember {
|
export interface NovelMember {
|
||||||
userId: string
|
userId: string
|
||||||
email: string
|
email: string
|
||||||
@@ -197,11 +201,8 @@ export interface Character {
|
|||||||
appearance: string | null
|
appearance: string | null
|
||||||
personality: string | null
|
personality: string | null
|
||||||
backstory: string | null
|
backstory: string | null
|
||||||
want: string | null
|
motivation: string | null
|
||||||
need: string | null
|
conflict: string | null
|
||||||
internalConflict: string | null
|
|
||||||
externalConflict: string | null
|
|
||||||
arcSummary: string | null
|
|
||||||
voice: string | null
|
voice: string | null
|
||||||
notes: string | null
|
notes: string | null
|
||||||
aliases: string[]
|
aliases: string[]
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
useNovel,
|
useNovel,
|
||||||
useRemoveRelationship,
|
useRemoveRelationship,
|
||||||
useTags,
|
useTags,
|
||||||
|
useUiSettings,
|
||||||
useUnlinkCharacterIdentity,
|
useUnlinkCharacterIdentity,
|
||||||
useUpdateCharacter,
|
useUpdateCharacter,
|
||||||
} from '../api/hooks'
|
} from '../api/hooks'
|
||||||
@@ -79,6 +80,7 @@ function CharacterSheet({
|
|||||||
canDelete: boolean
|
canDelete: boolean
|
||||||
}) {
|
}) {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const { data: uiSettings } = useUiSettings()
|
||||||
const { data: allTags } = useTags(novelId)
|
const { data: allTags } = useTags(novelId)
|
||||||
const { data: allCharacters } = useCharacters(novelId)
|
const { data: allCharacters } = useCharacters(novelId)
|
||||||
const { data: chapters } = useChapters(novelId)
|
const { data: chapters } = useChapters(novelId)
|
||||||
@@ -125,12 +127,14 @@ function CharacterSheet({
|
|||||||
|
|
||||||
<div className="grid gap-4 sm:grid-cols-3">
|
<div className="grid gap-4 sm:grid-cols-3">
|
||||||
<AutoField label="Age" value={character.age} onCommit={(age) => patch({ age })} readOnly={!canWrite} />
|
<AutoField label="Age" value={character.age} onCommit={(age) => patch({ age })} readOnly={!canWrite} />
|
||||||
|
{uiSettings?.showPronouns && (
|
||||||
<AutoField
|
<AutoField
|
||||||
label="Pronouns"
|
label="Pronouns"
|
||||||
value={character.pronouns}
|
value={character.pronouns}
|
||||||
onCommit={(pronouns) => patch({ pronouns })}
|
onCommit={(pronouns) => patch({ pronouns })}
|
||||||
readOnly={!canWrite}
|
readOnly={!canWrite}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
<AutoField
|
<AutoField
|
||||||
label="Occupation"
|
label="Occupation"
|
||||||
value={character.occupation}
|
value={character.occupation}
|
||||||
@@ -139,13 +143,7 @@ function CharacterSheet({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-5 grid gap-4 sm:grid-cols-2">
|
<div className="mt-5">
|
||||||
<TagEditor
|
|
||||||
label="Tags"
|
|
||||||
tags={character.tags}
|
|
||||||
suggestions={allTags?.map((t) => t.name) ?? []}
|
|
||||||
onChange={(tags) => canWrite && patch({ tags })}
|
|
||||||
/>
|
|
||||||
<AliasEditor
|
<AliasEditor
|
||||||
label="Also known as"
|
label="Also known as"
|
||||||
aliases={character.aliases}
|
aliases={character.aliases}
|
||||||
@@ -156,42 +154,20 @@ function CharacterSheet({
|
|||||||
|
|
||||||
<div className="mt-6 grid gap-4 lg:grid-cols-2">
|
<div className="mt-6 grid gap-4 lg:grid-cols-2">
|
||||||
<AutoField
|
<AutoField
|
||||||
label="Wants"
|
label="Motivation"
|
||||||
value={character.want}
|
value={character.motivation}
|
||||||
multiline
|
multiline
|
||||||
rows={3}
|
rows={3}
|
||||||
placeholder="What they are consciously chasing."
|
placeholder="What they consciously want, and what they need instead."
|
||||||
onCommit={(want) => patch({ want })}
|
onCommit={(motivation) => patch({ motivation })}
|
||||||
readOnly={!canWrite}
|
readOnly={!canWrite}
|
||||||
/>
|
/>
|
||||||
<AutoField
|
<AutoField
|
||||||
label="Needs"
|
label="Conflict"
|
||||||
value={character.need}
|
value={character.conflict}
|
||||||
multiline
|
multiline
|
||||||
rows={3}
|
placeholder="The war inside them, and what in the world opposes them."
|
||||||
placeholder="What the story will make them face instead."
|
onCommit={(conflict) => patch({ conflict })}
|
||||||
onCommit={(need) => patch({ need })}
|
|
||||||
readOnly={!canWrite}
|
|
||||||
/>
|
|
||||||
<AutoField
|
|
||||||
label="Internal conflict"
|
|
||||||
value={character.internalConflict}
|
|
||||||
multiline
|
|
||||||
onCommit={(internalConflict) => patch({ internalConflict })}
|
|
||||||
readOnly={!canWrite}
|
|
||||||
/>
|
|
||||||
<AutoField
|
|
||||||
label="External conflict"
|
|
||||||
value={character.externalConflict}
|
|
||||||
multiline
|
|
||||||
onCommit={(externalConflict) => patch({ externalConflict })}
|
|
||||||
readOnly={!canWrite}
|
|
||||||
/>
|
|
||||||
<AutoField
|
|
||||||
label="Arc"
|
|
||||||
value={character.arcSummary}
|
|
||||||
multiline
|
|
||||||
onCommit={(arcSummary) => patch({ arcSummary })}
|
|
||||||
readOnly={!canWrite}
|
readOnly={!canWrite}
|
||||||
/>
|
/>
|
||||||
<AutoField
|
<AutoField
|
||||||
@@ -304,6 +280,15 @@ function CharacterSheet({
|
|||||||
canDelete={canDelete}
|
canDelete={canDelete}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<section id="character-tags" className="card mt-6 p-5">
|
||||||
|
<TagEditor
|
||||||
|
label="Tags"
|
||||||
|
tags={character.tags}
|
||||||
|
suggestions={allTags?.map((t) => t.name) ?? []}
|
||||||
|
onChange={(tags) => canWrite && patch({ tags })}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
{confirmingDelete && (
|
{confirmingDelete && (
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
title="Delete character"
|
title="Delete character"
|
||||||
|
|||||||
@@ -48,23 +48,23 @@ public class CharacterServiceTests : ServiceTestFixture
|
|||||||
public async Task Updating_leaves_omitted_fields_alone_and_clears_on_empty_string()
|
public async Task Updating_leaves_omitted_fields_alone_and_clears_on_empty_string()
|
||||||
{
|
{
|
||||||
var character = await Characters.CreateAsync(_novelId, new CreateCharacterRequest(
|
var character = await Characters.CreateAsync(_novelId, new CreateCharacterRequest(
|
||||||
"Ines", Want: "To find her sister.", Need: "To let go of the guilt."));
|
"Ines", Motivation: "To find her sister.", Conflict: "Torn between vengeance and letting go."));
|
||||||
|
|
||||||
var renamed = (await Characters.UpdateAsync(character.Id, new UpdateCharacterRequest(Name: "Ines Vell")))!;
|
var renamed = (await Characters.UpdateAsync(character.Id, new UpdateCharacterRequest(Name: "Ines Vell")))!;
|
||||||
|
|
||||||
Assert.Multiple(() =>
|
Assert.Multiple(() =>
|
||||||
{
|
{
|
||||||
Assert.That(renamed.Name, Is.EqualTo("Ines Vell"));
|
Assert.That(renamed.Name, Is.EqualTo("Ines Vell"));
|
||||||
Assert.That(renamed.Want, Is.EqualTo("To find her sister."));
|
Assert.That(renamed.Motivation, Is.EqualTo("To find her sister."));
|
||||||
Assert.That(renamed.Need, Is.EqualTo("To let go of the guilt."));
|
Assert.That(renamed.Conflict, Is.EqualTo("Torn between vengeance and letting go."));
|
||||||
});
|
});
|
||||||
|
|
||||||
var cleared = (await Characters.UpdateAsync(character.Id, new UpdateCharacterRequest(Need: "")))!;
|
var cleared = (await Characters.UpdateAsync(character.Id, new UpdateCharacterRequest(Conflict: "")))!;
|
||||||
|
|
||||||
Assert.Multiple(() =>
|
Assert.Multiple(() =>
|
||||||
{
|
{
|
||||||
Assert.That(cleared.Need, Is.Null);
|
Assert.That(cleared.Conflict, Is.Null);
|
||||||
Assert.That(cleared.Want, Is.EqualTo("To find her sister."));
|
Assert.That(cleared.Motivation, Is.EqualTo("To find her sister."));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user