diff --git a/src/Novelly.Api/Agent/NovelAgentToolset.cs b/src/Novelly.Api/Agent/NovelAgentToolset.cs index 85c66c5..4e96927 100644 --- a/src/Novelly.Api/Agent/NovelAgentToolset.cs +++ b/src/Novelly.Api/Agent/NovelAgentToolset.cs @@ -146,11 +146,8 @@ public class NovelAgentToolset( JsonInput.String(input, "appearance"), JsonInput.String(input, "personality"), JsonInput.String(input, "backstory"), - JsonInput.String(input, "want"), - JsonInput.String(input, "need"), - JsonInput.String(input, "internal_conflict"), - JsonInput.String(input, "external_conflict"), - JsonInput.String(input, "arc_summary"), + JsonInput.String(input, "motivation"), + JsonInput.String(input, "conflict"), JsonInput.String(input, "voice"), JsonInput.String(input, "notes"), JsonInput.Strings(input, "tags"), @@ -177,11 +174,8 @@ public class NovelAgentToolset( JsonInput.String(input, "appearance"), JsonInput.String(input, "personality"), JsonInput.String(input, "backstory"), - JsonInput.String(input, "want"), - JsonInput.String(input, "need"), - JsonInput.String(input, "internal_conflict"), - JsonInput.String(input, "external_conflict"), - JsonInput.String(input, "arc_summary"), + JsonInput.String(input, "motivation"), + JsonInput.String(input, "conflict"), JsonInput.String(input, "voice"), JsonInput.String(input, "notes"), JsonInput.Strings(input, "tags"), @@ -667,11 +661,8 @@ public class NovelAgentToolset( .Str("appearance", "How they look.") .Str("personality", "Temperament, habits, how they treat people.") .Str("backstory", "History that shapes who they are now.") - .Str("want", "What they consciously pursue.") - .Str("need", "What they actually need, usually at odds with what they want.") - .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("motivation", "What they consciously pursue, weighed against what they actually need.") + .Str("conflict", "The war inside them and what in the world opposes them.") .Str("voice", "Speech patterns and register that make their dialogue theirs.") .Str("notes", "Anything else worth recording.") .StringArray("tags", "Tags for cross-referencing. Replaces the existing tags.") diff --git a/src/Novelly.Api/Characters/Character.cs b/src/Novelly.Api/Characters/Character.cs index 2c220fc..dadb93f 100644 --- a/src/Novelly.Api/Characters/Character.cs +++ b/src/Novelly.Api/Characters/Character.cs @@ -26,14 +26,9 @@ public class Character public string? Personality { get; set; } public string? Backstory { get; set; } - public string? Want { get; set; } + public string? Motivation { get; set; } - public string? Need { get; set; } - - public string? InternalConflict { get; set; } - public string? ExternalConflict { get; set; } - - public string? ArcSummary { get; set; } + public string? Conflict { get; set; } public string? Voice { get; set; } diff --git a/src/Novelly.Api/Characters/CharacterContracts.cs b/src/Novelly.Api/Characters/CharacterContracts.cs index 258442a..4159773 100644 --- a/src/Novelly.Api/Characters/CharacterContracts.cs +++ b/src/Novelly.Api/Characters/CharacterContracts.cs @@ -16,11 +16,8 @@ public record CharacterResponse( string? Appearance, string? Personality, string? Backstory, - string? Want, - string? Need, - string? InternalConflict, - string? ExternalConflict, - string? ArcSummary, + string? Motivation, + string? Conflict, string? Voice, string? Notes, IReadOnlyList Aliases, @@ -54,11 +51,8 @@ public record CreateCharacterRequest( string? Appearance = null, string? Personality = null, string? Backstory = null, - string? Want = null, - string? Need = null, - string? InternalConflict = null, - string? ExternalConflict = null, - string? ArcSummary = null, + string? Motivation = null, + string? Conflict = null, string? Voice = null, string? Notes = null, IReadOnlyList? Tags = null, @@ -73,7 +67,7 @@ public class CreateCharacterRequestValidator : IModelValidator? Tags = null, @@ -109,7 +100,7 @@ public class UpdateCharacterRequestValidator : IModelValidator? tags, IReadOnlyList? aliases, ValidationResult result) { Cap(age, "Age", 100, result); @@ -129,11 +120,8 @@ file static class CharacterValidation Cap(appearance, "Appearance", 20000, result); Cap(personality, "Personality", 20000, result); Cap(backstory, "Backstory", 20000, result); - Cap(want, "Want", 2000, result); - Cap(need, "Need", 2000, result); - Cap(internalConflict, "InternalConflict", 2000, result); - Cap(externalConflict, "ExternalConflict", 2000, result); - Cap(arcSummary, "ArcSummary", 20000, result); + Cap(motivation, "Motivation", 2000, result); + Cap(conflict, "Conflict", 2000, result); Cap(voice, "Voice", 2000, result); Cap(notes, "Notes", 20000, result); @@ -300,8 +288,7 @@ public static class CharacterMapping { 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.Appearance, c.Personality, c.Backstory, c.Want, c.Need, - c.InternalConflict, c.ExternalConflict, c.ArcSummary, c.Voice, c.Notes, + c.Appearance, c.Personality, c.Backstory, c.Motivation, c.Conflict, c.Voice, c.Notes, [.. c.Aliases], c.SameCharacterAsId, c.SameCharacterAs?.Name, diff --git a/src/Novelly.Api/Characters/CharacterService.cs b/src/Novelly.Api/Characters/CharacterService.cs index dbced19..453c1da 100644 --- a/src/Novelly.Api/Characters/CharacterService.cs +++ b/src/Novelly.Api/Characters/CharacterService.cs @@ -84,11 +84,8 @@ public class CharacterService( Appearance = request.Appearance, Personality = request.Personality, Backstory = request.Backstory, - Want = request.Want, - Need = request.Need, - InternalConflict = request.InternalConflict, - ExternalConflict = request.ExternalConflict, - ArcSummary = request.ArcSummary, + Motivation = request.Motivation, + Conflict = request.Conflict, Voice = request.Voice, Notes = request.Notes }; @@ -134,11 +131,8 @@ public class CharacterService( character.Appearance = Patch.Apply(character.Appearance, request.Appearance); character.Personality = Patch.Apply(character.Personality, request.Personality); character.Backstory = Patch.Apply(character.Backstory, request.Backstory); - character.Want = Patch.Apply(character.Want, request.Want); - character.Need = Patch.Apply(character.Need, request.Need); - 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.Motivation = Patch.Apply(character.Motivation, request.Motivation); + character.Conflict = Patch.Apply(character.Conflict, request.Conflict); character.Voice = Patch.Apply(character.Voice, request.Voice); character.Notes = Patch.Apply(character.Notes, request.Notes); character.UpdatedAt = DateTimeOffset.UtcNow; diff --git a/src/Novelly.Api/Common/NovellyServiceRegistration.cs b/src/Novelly.Api/Common/NovellyServiceRegistration.cs index 9af1603..c1ffdbf 100644 --- a/src/Novelly.Api/Common/NovellyServiceRegistration.cs +++ b/src/Novelly.Api/Common/NovellyServiceRegistration.cs @@ -26,6 +26,8 @@ public static class NovellyServiceRegistration { public static IServiceCollection AddNovelly(this IServiceCollection services, IConfiguration configuration) { + services.Configure(configuration.GetSection(UiSettingsOptions.SectionName)); + var connectionString = configuration.GetConnectionString("Novel") ?? "Data Source=novel.db"; diff --git a/src/Novelly.Api/Common/UiSettingsEndpoints.cs b/src/Novelly.Api/Common/UiSettingsEndpoints.cs new file mode 100644 index 0000000..e732177 --- /dev/null +++ b/src/Novelly.Api/Common/UiSettingsEndpoints.cs @@ -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 options) => + Results.Ok(new UiSettingsResponse(options.Value.ShowPronouns))) + .WithTags("UiSettings") + .AllowAnonymous(); + + return app; + } +} + +public record UiSettingsResponse(bool ShowPronouns); diff --git a/src/Novelly.Api/Common/UiSettingsOptions.cs b/src/Novelly.Api/Common/UiSettingsOptions.cs new file mode 100644 index 0000000..223fdbe --- /dev/null +++ b/src/Novelly.Api/Common/UiSettingsOptions.cs @@ -0,0 +1,8 @@ +namespace Novelly.Api.Common; + +public class UiSettingsOptions +{ + public const string SectionName = "UiSettings"; + + public bool ShowPronouns { get; set; } +} diff --git a/src/Novelly.Api/Data/Migrations/20260819222436_CollapseCharacterMotivationAndConflict.Designer.cs b/src/Novelly.Api/Data/Migrations/20260819222436_CollapseCharacterMotivationAndConflict.Designer.cs new file mode 100644 index 0000000..b0b45de --- /dev/null +++ b/src/Novelly.Api/Data/Migrations/20260819222436_CollapseCharacterMotivationAndConflict.Designer.cs @@ -0,0 +1,1223 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Novelly.Api.Data; + +#nullable disable + +namespace Novelly.Api.Data.Migrations +{ + [DbContext(typeof(NovelDbContext))] + [Migration("20260819222436_CollapseCharacterMotivationAndConflict")] + partial class CollapseCharacterMotivationAndConflict + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.10"); + + modelBuilder.Entity("BeatCharacter", b => + { + b.Property("BeatsId") + .HasColumnType("TEXT"); + + b.Property("CharactersId") + .HasColumnType("TEXT"); + + b.HasKey("BeatsId", "CharactersId"); + + b.HasIndex("CharactersId"); + + b.ToTable("BeatCharacters", (string)null); + }); + + modelBuilder.Entity("BeatCharacterArcStage", b => + { + b.Property("ArcStagesId") + .HasColumnType("TEXT"); + + b.Property("BeatsId") + .HasColumnType("TEXT"); + + b.HasKey("ArcStagesId", "BeatsId"); + + b.HasIndex("BeatsId"); + + b.ToTable("ArcStageBeats", (string)null); + }); + + modelBuilder.Entity("BeatTag", b => + { + b.Property("BeatsId") + .HasColumnType("TEXT"); + + b.Property("TagsId") + .HasColumnType("TEXT"); + + b.HasKey("BeatsId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("BeatTags", (string)null); + }); + + modelBuilder.Entity("ChapterLocation", b => + { + b.Property("ChaptersId") + .HasColumnType("TEXT"); + + b.Property("LocationsId") + .HasColumnType("TEXT"); + + b.HasKey("ChaptersId", "LocationsId"); + + b.HasIndex("LocationsId"); + + b.ToTable("ChapterLocations", (string)null); + }); + + modelBuilder.Entity("ChapterTag", b => + { + b.Property("ChaptersId") + .HasColumnType("TEXT"); + + b.Property("TagsId") + .HasColumnType("TEXT"); + + b.HasKey("ChaptersId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("ChapterTags", (string)null); + }); + + modelBuilder.Entity("CharacterTag", b => + { + b.Property("CharactersId") + .HasColumnType("TEXT"); + + b.Property("TagsId") + .HasColumnType("TEXT"); + + b.HasKey("CharactersId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("CharacterTags", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Novelly.Api.Agent.AgentConversation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("INTEGER"); + + b.Property("NovelId") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("NovelId"); + + b.ToTable("Conversations"); + }); + + modelBuilder.Entity("Novelly.Api.Agent.AgentMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ConversationId") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("INTEGER"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("TEXT"); + + b.Property("Sequence") + .HasColumnType("INTEGER"); + + b.Property("ToolCallsJson") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ConversationId", "Sequence") + .IsUnique(); + + b.ToTable("AgentMessages"); + }); + + modelBuilder.Entity("Novelly.Api.Beats.Beat", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ChapterId") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("INTEGER"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("INTEGER"); + + b.Property("WhatHappened") + .HasColumnType("TEXT"); + + b.Property("WhatsNext") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId", "SortOrder"); + + b.ToTable("Beats"); + }); + + modelBuilder.Entity("Novelly.Api.Chapters.Chapter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("INTEGER"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("NovelId") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("Prose") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("TargetWordCount") + .HasColumnType("INTEGER"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("NovelId", "Number"); + + b.ToTable("Chapters"); + }); + + modelBuilder.Entity("Novelly.Api.Characters.Character", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Age") + .HasColumnType("TEXT"); + + b.PrimitiveCollection("Aliases") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Appearance") + .HasColumnType("TEXT"); + + b.Property("Backstory") + .HasColumnType("TEXT"); + + b.Property("Conflict") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("INTEGER"); + + b.Property("IdentityNote") + .HasColumnType("TEXT"); + + b.Property("Importance") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("Motivation") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("NovelId") + .HasColumnType("TEXT"); + + b.Property("Occupation") + .HasColumnType("TEXT"); + + b.Property("Personality") + .HasColumnType("TEXT"); + + b.Property("Pronouns") + .HasColumnType("TEXT"); + + b.Property("RevealedInChapterId") + .HasColumnType("TEXT"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("SameCharacterAsId") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("INTEGER"); + + b.Property("Voice") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NovelId"); + + b.HasIndex("RevealedInChapterId"); + + b.HasIndex("SameCharacterAsId"); + + b.ToTable("Characters"); + }); + + modelBuilder.Entity("Novelly.Api.Characters.CharacterArcStage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ChapterId") + .HasColumnType("TEXT"); + + b.Property("CharacterId") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("INTEGER"); + + b.Property("Result") + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.HasIndex("CharacterId", "SortOrder"); + + b.ToTable("CharacterArcStages"); + }); + + modelBuilder.Entity("Novelly.Api.Characters.CharacterRelationship", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CharacterId") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("RelatedCharacterId") + .HasColumnType("TEXT"); + + b.Property("RelationshipType") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CharacterId"); + + b.HasIndex("RelatedCharacterId"); + + b.ToTable("CharacterRelationships"); + }); + + modelBuilder.Entity("Novelly.Api.Genres.Genre", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Genres"); + + b.HasData( + new + { + Id = new Guid("b89aadb3-ee96-5a33-897d-94946b037f96"), + Name = "Adventure" + }, + new + { + Id = new Guid("1295b746-5de1-5724-aab8-186d4220c84f"), + Name = "Contemporary Fiction" + }, + new + { + Id = new Guid("786d6d01-be6c-5dff-ab53-17081d2979ed"), + Name = "Crime" + }, + new + { + Id = new Guid("800eea0a-52cb-5e03-8b6f-5e1ceaec8554"), + Name = "Dystopian" + }, + new + { + Id = new Guid("8dbe0291-1ab6-5045-b327-00f2025a7b0a"), + Name = "Fantasy" + }, + new + { + Id = new Guid("93face5a-9a61-5d63-9a8d-7fd5d49eab7d"), + Name = "Historical Fiction" + }, + new + { + Id = new Guid("4eba456f-b706-5f1f-bfc9-5d32cab0da62"), + Name = "Horror" + }, + new + { + Id = new Guid("d49c5adf-3ed9-5bc9-8652-1f7a9a098ecb"), + Name = "Literary Fiction" + }, + new + { + Id = new Guid("f72c6437-c8e7-519f-8d35-5aefeebbff9e"), + Name = "Magical Realism" + }, + new + { + Id = new Guid("1b670010-b4cc-5b22-a879-d36eb1bf3429"), + Name = "Memoir" + }, + new + { + Id = new Guid("03063bbf-de5d-5dd0-af06-0ee939de58bc"), + Name = "Middle Grade" + }, + new + { + Id = new Guid("c22ed045-52e5-54b0-8cdd-cd1d6a699c19"), + Name = "Mystery" + }, + new + { + Id = new Guid("abe2e8bc-a35e-5a30-a07f-7ae30a00d838"), + Name = "Non-Fiction" + }, + new + { + Id = new Guid("f8543db0-c519-56a0-996a-c6028176e57e"), + Name = "Poetry" + }, + new + { + Id = new Guid("b6251b9e-63a1-563f-94c0-834162fb580b"), + Name = "Romance" + }, + new + { + Id = new Guid("4f188842-488e-567a-b31d-831e0c551fa5"), + Name = "Science Fiction" + }, + new + { + Id = new Guid("ae67fc84-1ed9-55ae-8c9f-8a37adb52b57"), + Name = "Thriller" + }, + new + { + Id = new Guid("37956a94-e9c4-5d29-abbc-f121d687f997"), + Name = "Young Adult" + }); + }); + + modelBuilder.Entity("Novelly.Api.Imports.ImportJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ChaptersCompleted") + .HasColumnType("INTEGER"); + + b.Property("ChaptersTotal") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("INTEGER"); + + b.Property("NovelId") + .HasColumnType("TEXT"); + + b.Property("RequestedByUserId") + .HasColumnType("TEXT"); + + b.Property("SourceRoot") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("TEXT"); + + b.Property("StatusMessage") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SourceRoot"); + + b.ToTable("ImportJobs"); + }); + + modelBuilder.Entity("Novelly.Api.Locations.Location", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("TEXT"); + + b.Property("NovelId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NovelId", "Name") + .IsUnique(); + + b.ToTable("Locations"); + }); + + modelBuilder.Entity("Novelly.Api.Novels.Novel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Author") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("INTEGER"); + + b.Property("Genre") + .HasColumnType("TEXT"); + + b.Property("Logline") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnType("TEXT"); + + b.Property("Phase") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("Synopsis") + .HasColumnType("TEXT"); + + b.Property("TargetWordCount") + .HasColumnType("INTEGER"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("Novels"); + }); + + modelBuilder.Entity("Novelly.Api.Questions.OpenQuestion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ChapterId") + .HasColumnType("TEXT"); + + b.Property("CharacterId") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("INTEGER"); + + b.Property("Detail") + .HasColumnType("TEXT"); + + b.Property("NovelId") + .HasColumnType("TEXT"); + + b.Property("Question") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Resolution") + .HasColumnType("TEXT"); + + b.Property("ResolvedAt") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.HasIndex("CharacterId"); + + b.HasIndex("NovelId"); + + b.ToTable("OpenQuestions"); + }); + + modelBuilder.Entity("Novelly.Api.Tags.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Color") + .HasMaxLength(16) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("NovelId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NovelId", "Name") + .IsUnique(); + + b.ToTable("Tags"); + }); + + modelBuilder.Entity("Novelly.Api.Users.NovelMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("GrantedAt") + .HasColumnType("INTEGER"); + + b.Property("GrantedByUserId") + .HasColumnType("TEXT"); + + b.Property("NovelId") + .HasColumnType("TEXT"); + + b.Property("NovelRole") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("NovelId", "UserId") + .IsUnique(); + + b.ToTable("NovelMembers"); + }); + + modelBuilder.Entity("Novelly.Api.Users.NovellyUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("INTEGER"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("GlobalRole") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("INTEGER"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("BeatCharacter", b => + { + b.HasOne("Novelly.Api.Beats.Beat", null) + .WithMany() + .HasForeignKey("BeatsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Novelly.Api.Characters.Character", null) + .WithMany() + .HasForeignKey("CharactersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("BeatCharacterArcStage", b => + { + b.HasOne("Novelly.Api.Characters.CharacterArcStage", null) + .WithMany() + .HasForeignKey("ArcStagesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Novelly.Api.Beats.Beat", null) + .WithMany() + .HasForeignKey("BeatsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("BeatTag", b => + { + b.HasOne("Novelly.Api.Beats.Beat", null) + .WithMany() + .HasForeignKey("BeatsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Novelly.Api.Tags.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterLocation", b => + { + b.HasOne("Novelly.Api.Chapters.Chapter", null) + .WithMany() + .HasForeignKey("ChaptersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Novelly.Api.Locations.Location", null) + .WithMany() + .HasForeignKey("LocationsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterTag", b => + { + b.HasOne("Novelly.Api.Chapters.Chapter", null) + .WithMany() + .HasForeignKey("ChaptersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Novelly.Api.Tags.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("CharacterTag", b => + { + b.HasOne("Novelly.Api.Characters.Character", null) + .WithMany() + .HasForeignKey("CharactersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Novelly.Api.Tags.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Novelly.Api.Users.NovellyUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Novelly.Api.Users.NovellyUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Novelly.Api.Users.NovellyUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Novelly.Api.Agent.AgentConversation", b => + { + b.HasOne("Novelly.Api.Novels.Novel", "Novel") + .WithMany("Conversations") + .HasForeignKey("NovelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Novel"); + }); + + modelBuilder.Entity("Novelly.Api.Agent.AgentMessage", b => + { + b.HasOne("Novelly.Api.Agent.AgentConversation", "Conversation") + .WithMany("Messages") + .HasForeignKey("ConversationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Conversation"); + }); + + modelBuilder.Entity("Novelly.Api.Beats.Beat", b => + { + b.HasOne("Novelly.Api.Chapters.Chapter", "Chapter") + .WithMany("Beats") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + }); + + modelBuilder.Entity("Novelly.Api.Chapters.Chapter", b => + { + b.HasOne("Novelly.Api.Novels.Novel", "Novel") + .WithMany("Chapters") + .HasForeignKey("NovelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Novel"); + }); + + modelBuilder.Entity("Novelly.Api.Characters.Character", b => + { + b.HasOne("Novelly.Api.Novels.Novel", "Novel") + .WithMany("Characters") + .HasForeignKey("NovelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Novelly.Api.Chapters.Chapter", "RevealedInChapter") + .WithMany() + .HasForeignKey("RevealedInChapterId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Novelly.Api.Characters.Character", "SameCharacterAs") + .WithMany("OtherIdentities") + .HasForeignKey("SameCharacterAsId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Novel"); + + b.Navigation("RevealedInChapter"); + + b.Navigation("SameCharacterAs"); + }); + + modelBuilder.Entity("Novelly.Api.Characters.CharacterArcStage", b => + { + b.HasOne("Novelly.Api.Chapters.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Novelly.Api.Characters.Character", "Character") + .WithMany("ArcStages") + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + + b.Navigation("Character"); + }); + + modelBuilder.Entity("Novelly.Api.Characters.CharacterRelationship", b => + { + b.HasOne("Novelly.Api.Characters.Character", "Character") + .WithMany("Relationships") + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Novelly.Api.Characters.Character", "RelatedCharacter") + .WithMany() + .HasForeignKey("RelatedCharacterId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Character"); + + b.Navigation("RelatedCharacter"); + }); + + modelBuilder.Entity("Novelly.Api.Locations.Location", b => + { + b.HasOne("Novelly.Api.Novels.Novel", "Novel") + .WithMany() + .HasForeignKey("NovelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Novel"); + }); + + modelBuilder.Entity("Novelly.Api.Novels.Novel", b => + { + b.HasOne("Novelly.Api.Users.NovellyUser", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Novelly.Api.Questions.OpenQuestion", b => + { + b.HasOne("Novelly.Api.Chapters.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Novelly.Api.Characters.Character", "Character") + .WithMany() + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Novelly.Api.Novels.Novel", "Novel") + .WithMany() + .HasForeignKey("NovelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + + b.Navigation("Character"); + + b.Navigation("Novel"); + }); + + modelBuilder.Entity("Novelly.Api.Tags.Tag", b => + { + b.HasOne("Novelly.Api.Novels.Novel", "Novel") + .WithMany("Tags") + .HasForeignKey("NovelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Novel"); + }); + + modelBuilder.Entity("Novelly.Api.Users.NovelMember", b => + { + b.HasOne("Novelly.Api.Novels.Novel", "Novel") + .WithMany("Members") + .HasForeignKey("NovelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Novelly.Api.Users.NovellyUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Novel"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Novelly.Api.Agent.AgentConversation", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("Novelly.Api.Chapters.Chapter", b => + { + b.Navigation("Beats"); + }); + + modelBuilder.Entity("Novelly.Api.Characters.Character", b => + { + b.Navigation("ArcStages"); + + b.Navigation("OtherIdentities"); + + b.Navigation("Relationships"); + }); + + modelBuilder.Entity("Novelly.Api.Novels.Novel", b => + { + b.Navigation("Chapters"); + + b.Navigation("Characters"); + + b.Navigation("Conversations"); + + b.Navigation("Members"); + + b.Navigation("Tags"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Novelly.Api/Data/Migrations/20260819222436_CollapseCharacterMotivationAndConflict.cs b/src/Novelly.Api/Data/Migrations/20260819222436_CollapseCharacterMotivationAndConflict.cs new file mode 100644 index 0000000..6ecbf59 --- /dev/null +++ b/src/Novelly.Api/Data/Migrations/20260819222436_CollapseCharacterMotivationAndConflict.cs @@ -0,0 +1,86 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Novelly.Api.Data.Migrations +{ + /// + public partial class CollapseCharacterMotivationAndConflict : Migration + { + /// + 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"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "Motivation", + table: "Characters", + newName: "Want"); + + migrationBuilder.RenameColumn( + name: "Conflict", + table: "Characters", + newName: "InternalConflict"); + + migrationBuilder.AddColumn( + name: "ArcSummary", + table: "Characters", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "ExternalConflict", + table: "Characters", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "Need", + table: "Characters", + type: "TEXT", + nullable: true); + } + } +} diff --git a/src/Novelly.Api/Data/Migrations/NovelDbContextModelSnapshot.cs b/src/Novelly.Api/Data/Migrations/NovelDbContextModelSnapshot.cs index b0b6dc9..cb1fbd3 100644 --- a/src/Novelly.Api/Data/Migrations/NovelDbContextModelSnapshot.cs +++ b/src/Novelly.Api/Data/Migrations/NovelDbContextModelSnapshot.cs @@ -333,18 +333,15 @@ namespace Novelly.Api.Data.Migrations b.Property("Appearance") .HasColumnType("TEXT"); - b.Property("ArcSummary") + b.Property("Backstory") .HasColumnType("TEXT"); - b.Property("Backstory") + b.Property("Conflict") .HasColumnType("TEXT"); b.Property("CreatedAt") .HasColumnType("INTEGER"); - b.Property("ExternalConflict") - .HasColumnType("TEXT"); - b.Property("IdentityNote") .HasColumnType("TEXT"); @@ -353,7 +350,7 @@ namespace Novelly.Api.Data.Migrations .HasMaxLength(32) .HasColumnType("TEXT"); - b.Property("InternalConflict") + b.Property("Motivation") .HasColumnType("TEXT"); b.Property("Name") @@ -361,9 +358,6 @@ namespace Novelly.Api.Data.Migrations .HasMaxLength(200) .HasColumnType("TEXT"); - b.Property("Need") - .HasColumnType("TEXT"); - b.Property("Notes") .HasColumnType("TEXT"); @@ -396,9 +390,6 @@ namespace Novelly.Api.Data.Migrations b.Property("Voice") .HasColumnType("TEXT"); - b.Property("Want") - .HasColumnType("TEXT"); - b.HasKey("Id"); b.HasIndex("NovelId"); diff --git a/src/Novelly.Api/Imports/ImportAgentToolset.cs b/src/Novelly.Api/Imports/ImportAgentToolset.cs index b3a1bce..b23f0d0 100644 --- a/src/Novelly.Api/Imports/ImportAgentToolset.cs +++ b/src/Novelly.Api/Imports/ImportAgentToolset.cs @@ -241,7 +241,7 @@ public class ImportAgentToolset( Occupation: JsonInput.String(input, "occupation"), Appearance: JsonInput.String(input, "appearance"), Backstory: JsonInput.String(input, "backstory"), - Want: JsonInput.String(input, "want"), + Motivation: JsonInput.String(input, "motivation"), Notes: JsonInput.String(input, "notes")), ct); return created is null @@ -264,7 +264,7 @@ public class ImportAgentToolset( Occupation: JsonInput.String(input, "occupation"), Appearance: JsonInput.String(input, "appearance"), Backstory: JsonInput.String(input, "backstory"), - Want: JsonInput.String(input, "want"), + Motivation: JsonInput.String(input, "motivation"), Notes: JsonInput.String(input, "notes")), ct); return updated is null @@ -379,6 +379,6 @@ public class ImportAgentToolset( .Str("occupation", "The italic tagline under the heading.") .Str("appearance", "The ## Appearance section.") .Str("backstory", "The ## Background section.") - .Str("want", "The ## Motivation section.") + .Str("motivation", "The ## Motivation section.") .Str("notes", "The ## Notes section, if present."); } diff --git a/src/Novelly.Api/Program.cs b/src/Novelly.Api/Program.cs index 1e87e82..861f1c9 100644 --- a/src/Novelly.Api/Program.cs +++ b/src/Novelly.Api/Program.cs @@ -105,6 +105,7 @@ if (app.Environment.IsDevelopment()) app.MapDefaultEndpoints(); app.MapGet("/api/health", () => Results.Ok(new { status = "ok" })).WithTags("Health").AllowAnonymous(); +app.MapUiSettingsEndpoints(); app.MapUserEndpoints(); app.MapNovelMemberEndpoints(); diff --git a/src/Novelly.Api/appsettings.json b/src/Novelly.Api/appsettings.json index 8addcb6..9d09b10 100644 --- a/src/Novelly.Api/appsettings.json +++ b/src/Novelly.Api/appsettings.json @@ -28,9 +28,12 @@ "Origins": [ "http://localhost:5173" ] }, "Agent": { - "Model": "claude-opus-5", + "Model": "claude-sonnet-5", "MaxTokens": 16000, "Effort": "high", "MaxIterations": 12 + }, + "UiSettings": { + "ShowPronouns": false } } diff --git a/src/Novelly.Mcp/Tools/CharacterTools.cs b/src/Novelly.Mcp/Tools/CharacterTools.cs index d608e6b..f89c51b 100644 --- a/src/Novelly.Mcp/Tools/CharacterTools.cs +++ b/src/Novelly.Mcp/Tools/CharacterTools.cs @@ -41,11 +41,8 @@ public static class CharacterTools [Description("How they look.")] string? appearance = null, [Description("Temperament, habits, how they treat people.")] string? personality = null, [Description("History that shapes who they are now.")] string? backstory = null, - [Description("What they consciously pursue.")] string? want = null, - [Description("What they actually need, usually at odds with what they want.")] string? need = 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("What they consciously pursue, weighed against what they actually need.")] string? motivation = null, + [Description("The war inside them and what in the world opposes them.")] string? conflict = 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, @@ -61,11 +58,8 @@ public static class CharacterTools appearance, personality, backstory, - want, - need, - internalConflict, - externalConflict, - arcSummary, + motivation, + conflict, voice, notes, tags, @@ -89,11 +83,8 @@ public static class CharacterTools [Description("How they look.")] string? appearance = null, [Description("Temperament, habits, how they treat people.")] string? personality = null, [Description("History that shapes who they are now.")] string? backstory = null, - [Description("What they consciously pursue.")] string? want = null, - [Description("What they actually need.")] string? need = 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("What they consciously pursue, weighed against what they actually need.")] string? motivation = null, + [Description("The war inside them and what in the world opposes them.")] string? conflict = 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, @@ -109,11 +100,8 @@ public static class CharacterTools appearance, personality, backstory, - want, - need, - internalConflict, - externalConflict, - arcSummary, + motivation, + conflict, voice, notes, tags, diff --git a/src/Novelly.Web/src/api/hooks.ts b/src/Novelly.Web/src/api/hooks.ts index 0cb818f..ff20e9b 100644 --- a/src/Novelly.Web/src/api/hooks.ts +++ b/src/Novelly.Web/src/api/hooks.ts @@ -23,11 +23,13 @@ import type { NovelSummary, TagReferences, TagSummary, + UiSettings, User, } from './types' export const keys = { me: ['me'] as const, + uiSettings: ['ui-settings'] as const, members: (novelId: string) => ['novels', novelId, 'members'] as const, novels: ['novels'] as const, genres: ['genres'] as const, @@ -46,6 +48,9 @@ export const keys = { importJob: (id: string) => ['imports', id] as const, } +export const useUiSettings = () => + useQuery({ queryKey: keys.uiSettings, queryFn: () => api.get('/api/ui-settings') }) + export const useMe = () => useQuery({ queryKey: keys.me, diff --git a/src/Novelly.Web/src/api/types.ts b/src/Novelly.Web/src/api/types.ts index c4a14c0..c4d1175 100644 --- a/src/Novelly.Web/src/api/types.ts +++ b/src/Novelly.Web/src/api/types.ts @@ -49,6 +49,10 @@ export interface User { globalRole: GlobalRole } +export interface UiSettings { + showPronouns: boolean +} + export interface NovelMember { userId: string email: string @@ -197,11 +201,8 @@ export interface Character { appearance: string | null personality: string | null backstory: string | null - want: string | null - need: string | null - internalConflict: string | null - externalConflict: string | null - arcSummary: string | null + motivation: string | null + conflict: string | null voice: string | null notes: string | null aliases: string[] diff --git a/src/Novelly.Web/src/pages/CharacterDetailPage.tsx b/src/Novelly.Web/src/pages/CharacterDetailPage.tsx index b6a74fd..293adba 100644 --- a/src/Novelly.Web/src/pages/CharacterDetailPage.tsx +++ b/src/Novelly.Web/src/pages/CharacterDetailPage.tsx @@ -9,6 +9,7 @@ import { useNovel, useRemoveRelationship, useTags, + useUiSettings, useUnlinkCharacterIdentity, useUpdateCharacter, } from '../api/hooks' @@ -79,6 +80,7 @@ function CharacterSheet({ canDelete: boolean }) { const navigate = useNavigate() + const { data: uiSettings } = useUiSettings() const { data: allTags } = useTags(novelId) const { data: allCharacters } = useCharacters(novelId) const { data: chapters } = useChapters(novelId) @@ -125,12 +127,14 @@ function CharacterSheet({
patch({ age })} readOnly={!canWrite} /> - patch({ pronouns })} - readOnly={!canWrite} - /> + {uiSettings?.showPronouns && ( + patch({ pronouns })} + readOnly={!canWrite} + /> + )}
-
- t.name) ?? []} - onChange={(tags) => canWrite && patch({ tags })} - /> +
patch({ want })} + placeholder="What they consciously want, and what they need instead." + onCommit={(motivation) => patch({ motivation })} readOnly={!canWrite} /> patch({ need })} - readOnly={!canWrite} - /> - patch({ internalConflict })} - readOnly={!canWrite} - /> - patch({ externalConflict })} - readOnly={!canWrite} - /> - patch({ arcSummary })} + placeholder="The war inside them, and what in the world opposes them." + onCommit={(conflict) => patch({ conflict })} readOnly={!canWrite} /> +
+ t.name) ?? []} + onChange={(tags) => canWrite && patch({ tags })} + /> +
+ {confirmingDelete && ( { Assert.That(renamed.Name, Is.EqualTo("Ines Vell")); - Assert.That(renamed.Want, Is.EqualTo("To find her sister.")); - Assert.That(renamed.Need, Is.EqualTo("To let go of the guilt.")); + Assert.That(renamed.Motivation, Is.EqualTo("To find her sister.")); + 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.That(cleared.Need, Is.Null); - Assert.That(cleared.Want, Is.EqualTo("To find her sister.")); + Assert.That(cleared.Conflict, Is.Null); + Assert.That(cleared.Motivation, Is.EqualTo("To find her sister.")); }); }