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, "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.")
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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<string> 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<string>? Tags = null,
|
||||
@@ -73,7 +67,7 @@ public class CreateCharacterRequestValidator : IModelValidator<CreateCharacterRe
|
||||
result.AddRequiredTextErrors("Name", "Name", model.Name, 200);
|
||||
CharacterValidation.OptionalFields(
|
||||
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);
|
||||
|
||||
return result;
|
||||
@@ -90,11 +84,8 @@ public record UpdateCharacterRequest(
|
||||
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<string>? Tags = null,
|
||||
@@ -109,7 +100,7 @@ public class UpdateCharacterRequestValidator : IModelValidator<UpdateCharacterRe
|
||||
result.AddUnclearableTextErrors("Name", "Name", model.Name, "a character", 200);
|
||||
CharacterValidation.OptionalFields(
|
||||
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);
|
||||
|
||||
return result;
|
||||
@@ -120,7 +111,7 @@ file static class CharacterValidation
|
||||
{
|
||||
public static void OptionalFields(
|
||||
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)
|
||||
{
|
||||
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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -26,6 +26,8 @@ public static class NovellyServiceRegistration
|
||||
{
|
||||
public static IServiceCollection AddNovelly(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.Configure<UiSettingsOptions>(configuration.GetSection(UiSettingsOptions.SectionName));
|
||||
|
||||
var connectionString = configuration.GetConnectionString("Novel")
|
||||
?? "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")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ArcSummary")
|
||||
b.Property<string>("Backstory")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Backstory")
|
||||
b.Property<string>("Conflict")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("CreatedAt")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ExternalConflict")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("IdentityNote")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@@ -353,7 +350,7 @@ namespace Novelly.Api.Data.Migrations
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("InternalConflict")
|
||||
b.Property<string>("Motivation")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
@@ -361,9 +358,6 @@ namespace Novelly.Api.Data.Migrations
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Need")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@@ -396,9 +390,6 @@ namespace Novelly.Api.Data.Migrations
|
||||
b.Property<string>("Voice")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Want")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NovelId");
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<UiSettings>('/api/ui-settings') })
|
||||
|
||||
export const useMe = () =>
|
||||
useQuery({
|
||||
queryKey: keys.me,
|
||||
|
||||
@@ -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[]
|
||||
|
||||
@@ -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({
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-3">
|
||||
<AutoField label="Age" value={character.age} onCommit={(age) => patch({ age })} readOnly={!canWrite} />
|
||||
<AutoField
|
||||
label="Pronouns"
|
||||
value={character.pronouns}
|
||||
onCommit={(pronouns) => patch({ pronouns })}
|
||||
readOnly={!canWrite}
|
||||
/>
|
||||
{uiSettings?.showPronouns && (
|
||||
<AutoField
|
||||
label="Pronouns"
|
||||
value={character.pronouns}
|
||||
onCommit={(pronouns) => patch({ pronouns })}
|
||||
readOnly={!canWrite}
|
||||
/>
|
||||
)}
|
||||
<AutoField
|
||||
label="Occupation"
|
||||
value={character.occupation}
|
||||
@@ -139,13 +143,7 @@ function CharacterSheet({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 grid gap-4 sm:grid-cols-2">
|
||||
<TagEditor
|
||||
label="Tags"
|
||||
tags={character.tags}
|
||||
suggestions={allTags?.map((t) => t.name) ?? []}
|
||||
onChange={(tags) => canWrite && patch({ tags })}
|
||||
/>
|
||||
<div className="mt-5">
|
||||
<AliasEditor
|
||||
label="Also known as"
|
||||
aliases={character.aliases}
|
||||
@@ -156,42 +154,20 @@ function CharacterSheet({
|
||||
|
||||
<div className="mt-6 grid gap-4 lg:grid-cols-2">
|
||||
<AutoField
|
||||
label="Wants"
|
||||
value={character.want}
|
||||
label="Motivation"
|
||||
value={character.motivation}
|
||||
multiline
|
||||
rows={3}
|
||||
placeholder="What they are consciously chasing."
|
||||
onCommit={(want) => patch({ want })}
|
||||
placeholder="What they consciously want, and what they need instead."
|
||||
onCommit={(motivation) => patch({ motivation })}
|
||||
readOnly={!canWrite}
|
||||
/>
|
||||
<AutoField
|
||||
label="Needs"
|
||||
value={character.need}
|
||||
label="Conflict"
|
||||
value={character.conflict}
|
||||
multiline
|
||||
rows={3}
|
||||
placeholder="What the story will make them face instead."
|
||||
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 })}
|
||||
placeholder="The war inside them, and what in the world opposes them."
|
||||
onCommit={(conflict) => patch({ conflict })}
|
||||
readOnly={!canWrite}
|
||||
/>
|
||||
<AutoField
|
||||
@@ -304,6 +280,15 @@ function CharacterSheet({
|
||||
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 && (
|
||||
<ConfirmModal
|
||||
title="Delete character"
|
||||
|
||||
@@ -48,23 +48,23 @@ public class CharacterServiceTests : ServiceTestFixture
|
||||
public async Task Updating_leaves_omitted_fields_alone_and_clears_on_empty_string()
|
||||
{
|
||||
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")))!;
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
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."));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user