nyxcore-systems
7 min read

NyxBook's Narrative Core: Unblocking Data, Enriching Personas, and Visualizing Arcs

Join me as we dive into a recent NyxBook development session, where we tackled critical data import bugs, enriched our narrative 'bible,' and revamped the UI for a more insightful writing experience.

Next.jsTypeScriptPrismaMarkdown ParsingUI/UXData ModelingDeveloper WorkflowLessons Learned

It was late night, session 8. The kind of deep work where the world outside fades and only the code matters. My mission for NyxBook: fix a frustrating bug causing empty character data after import, breathe life into the dashboard with real-time activity, refine our narrative arc display, and clarify some core concepts.

The goal was ambitious, but the satisfaction of seeing it all come together made the late hours worth it. Today, I'm sharing the journey – the problems, the solutions, and a few valuable lessons learned along the way.

The Big Unblock: From Empty Shells to Enriched Personas

One of NyxBook's core features is importing a writer's "bible" – their structured markdown and JSON files containing world rules, characters, motifs, and influences. But lately, after an import, my Personas tab (formerly Characters) was a ghost town. Empty. Frustrating.

The culprit? A classic parsing gotcha.

The Regex Riddle: ^##\s+ vs. ^#{2,3}\s+

Our parseCharacters() function in src/server/services/nyxbook-import.ts was looking for ^##\s+ (level 2 headings) to identify character profiles in stimmen.md. The problem? The source file had evolved, now using ### (level 3 headings) for individual character sections.

typescript
// Old (buggy) regex:
// const characterHeadingRegex = /^##\s+/;

// New (fixed) regex:
const characterHeadingRegex = /^#{2,3}\s+/; // Matches H2 or H3

A small change, but it unlocked a cascade of data. This is a crucial reminder: always validate your parsing logic against the actual format of your source files, especially in evolving projects. Or better yet, make your parsers more resilient.

Deeper Dive into the Narrative Bible

With the basic character parsing fixed, it was time to enrich NyxBook's understanding of the narrative. Previously, we only grabbed names. Now, we're pulling in the full spectrum of a character's being, plus world-level details.

I refactored the import process to:

  1. Parse Individual Character Files: Instead of just stimmen.md, NyxBook now reads individual files like 02_bible/characters/oli.md, nia.md, etc. This allows for more structured data for each persona.
    • I created parseCharacterFile() which intelligently extracts fields like wound, desire, mask, trigger words, arc, and tells using helper functions like extractBoldField() and extractSection(). This leverages common markdown patterns (**Field:** Value or ## Section).
  2. Motifs, Influences, and Canon Access:
    • parseMotifs() now processes 02_bible/motifs.md (## M1 – Name) into structured MotifRule[].
    • parseInfluences() reads 02_bible/influences.map.json to bring in external inspirations.
    • parseCanonAccess() parses 02_bible/canon_access.md for our world's internal rules.

The result? Our Book record now stores worldRules, influences, and canonRules, providing a much richer context for the story.

Chapter Status: From Draft to Final

A minor but important detail: newly imported chapters from book/chapters/ were defaulting to "draft". Since these are canonical source files, they should represent the "final" state of the chapter. A quick update in both GitHub and filesystem import paths fixed this, ensuring Chapter records reflect a final status.

A Semantic Shift: Characters → Personas

In NyxBook, "Characters" felt a bit flat. The term Persona better encapsulates the depth we're aiming for – not just a name, but a complex interplay of traits, motivations, and roles.

This involved a sweeping rename across the UI:

  • Sidebar Tab: "Characters" → "Personas"
  • Tab Header: "Personas" with the clarifying subtitle: "CORS Framework — Context, Objective, Role & Rules, Safety"
  • Dashboard Stats: "Stimmen" → "Personas"
  • Heatmap: "Stimmen-Präsenz" → "Persona-Präsenz"
  • Empty States: Updated messaging to reflect the new terminology.

This wasn't just a rename; it was a conceptual alignment, reinforcing how we think about the entities driving the narrative.

Bringing the UI to Life

Data is great, but without a compelling way to visualize and interact with it, it loses impact. This session also focused heavily on UI/UX enhancements.

The Dashboard Activity Panel

The old dashboard had a generic "X aktive Workflows" card. That's not helpful. I replaced it with a detailed Aktivität panel (src/components/nyxbook/book-dashboard.tsx).

  • I extended PipelineData with workflows?: WorkflowInfo[] and an ImportStatus prop.
  • The new panel now features:
    • A clear import status indicator (GitHub/Zip/Path icon + spinner + label).
    • Individual workflow rows, each with its status (spinning for active, paused, completed, failed), name, and relative time.
    • Active workflows are highlighted, and all are clickable to jump to the workshop tab.
  • The importStatus is computed dynamically in page.tsx from the various mutation loading states, providing real-time feedback.

This panel transforms the dashboard from a static summary to a dynamic command center, giving writers immediate insight into NyxBook's background operations.

Visualizing Narrative Arcs: The Arc Timeline

A flat text display of a character's arc doesn't convey its journey effectively. Enter the ArcTimeline component (src/components/nyxbook/character-card.tsx).

This new component replaces plain text with a vertical timeline, marking key phases with distinct colors:

  • Anfang (blue): The beginning
  • Konflikt (amber): Rising action, conflict
  • Kippmoment (rose): The turning point
  • Später (emerald): The aftermath, resolution

The component parses - Label: text lines from the arc data, falling back to a plain text display if the data isn't structured this way. It's a significant step towards more intuitive narrative visualization.

Enriched Inspirations

The Inspirations tab also got an upgrade. The Influence interface was extended with type? and motifs? fields. Now, influence cards proudly display a type badge and relevant motif tags below their description, adding more context at a glance.

Lessons Learned (The "Pain Log" Transformed)

Every development session has its snags. Here are a few "aha!" moments from this one:

  1. JSON Field Type Mismatches in Prisma:
    • Problem: I tried directly assigning a MotifRule[] (a typed array) to a Prisma JSON field.
    • Failure: TS2322 – TypeScript complained about an index signature mismatch. Prisma's JSON fields expect plain JSON objects or arrays, not deeply typed custom classes/interfaces that might carry methods or non-serializable properties.
    • Workaround: JSON.parse(JSON.stringify(data)) was my quick fix. This strips all type information, converting the typed array into a pure JSON array that Prisma happily accepts.
    • Takeaway: When working with Prisma's JSON fields, always ensure your data is plain JSON. If you have complex types, be explicit about serialization/deserialization.
  2. UI Component Prop Type Strictness:
    • Problem: I wanted an outline variant for a badge component.
    • Failure: The component's variant prop only accepted "default" | "accent" | "success" | "warning" | "danger". "outline" wasn't in the union type.
    • Workaround: Used variant="default" for now.
    • Takeaway: Always consult component documentation or prop types. Don't assume variants exist; they often need to be explicitly defined in the component's design system.
  3. Regex Resilience:
    • Problem: The initial ^##\s+ regex for markdown headings.
    • Constraint: This regex only matches H2. It does not match H3.
    • Takeaway: For markdown parsing, use more flexible regex patterns like ^#{2,3}\s+ (for H2 or H3) or even ^#+\s+ if you want to match any heading level, followed by more specific checks if needed. This prevents silent failures when source file formats subtly change.

A Clean Slate and What's Next

To ensure a pristine testing environment, I performed a database cleanup, deleting all existing test books and orphaned personas. It's a fresh start for the re-import.

The immediate next steps are validation: re-import inselwerk from GitHub and verify all the new features:

  • 6 personas with full bible data.
  • 7 motifs and ~10 influences with their new badges/tags.
  • All 13 chapters marked as "final".
  • Correct rendering of the arc timeline on persona cards.

Looking further ahead, the vision for NyxBook includes exciting possibilities: LLM-powered consistency checks between chapters and the bible, extracting entities from chapter text for cross-validation, motif heatmaps, voice drift detection, and narrative tension curves.

This session was a significant step forward, unblocking critical data flows and laying the groundwork for a truly intelligent narrative assistant. The journey continues!

json
{
  "thingsDone": [
    "Fixed empty personas/motifs/inspirations after import",
    "Added detailed dashboard activity panel",
    "Improved arc display with new ArcTimeline component",
    "Renamed 'Characters' to 'Personas' across UI",
    "Parsed full narrative bible (characters, motifs, influences, canon access)",
    "Set chapter status to 'final' on import",
    "Enhanced inspirations tab with type badges and motif tags",
    "Database cleanup for fresh re-import"
  ],
  "pains": [
    "TS2322 - Prisma JSON field type mismatch with typed arrays",
    "UI component badge variant not available ('outline')",
    "Markdown regex `^##\\s+` not matching `###`"
  ],
  "successes": [
    "Successful parsing of complex markdown structures for persona data",
    "Real-time activity panel provides crucial user feedback",
    "Arc timeline significantly improves narrative visualization",
    "Conceptual clarity achieved with 'Personas' rename",
    "All features completed, committed, and pushed"
  ],
  "techStack": [
    "Next.js",
    "TypeScript",
    "Prisma",
    "PostgreSQL",
    "React",
    "Markdown Parsing",
    "UI Components (internal design system)"
  ]
}