nyxcore-systems
10 min read

Architecting Narrative: A Deep Dive into nyxBook's AI-Powered Story Pipeline

Join me on a late-night journey as we transform nyxBook into an even more intelligent writing companion, building an AI-driven idea pipeline, a dynamic beat board, and a rich narrative dashboard from the ground up.

TypeScriptNext.jstRPCPrismaLLMUI/UXStorytellingFullstackData Visualization

It was late. The kind of late where the only sounds are the hum of your server and the click of your mechanical keyboard. But in that quiet, a significant leap for nyxBook was taking shape. This wasn't just another feature sprint; it was an ambitious overhaul aimed at deeply integrating AI into the creative process, giving writers unprecedented control and insight into their stories.

Our goal was clear: build a seamless pipeline for raw ideas, auto-suggest their placement using AI, refactor the beat board for chapter-grouped progression, and introduce a rich book dashboard with data visualizations. All while restructuring the sidebar for intuitive navigation. It was a massive undertaking, culminating in a final push that left everything type-checked, lint-clean, and ready for deployment.

Let's unpack how we got there.

The Vision: From Scattered Thoughts to Structured Story

At its heart, nyxBook aims to be the ultimate storytelling companion. The biggest challenge for any writer is often the sheer volume of ideas – some brilliant, some half-baked, all needing a home. We wanted to build a system that acts as a co-pilot, helping writers organize, refine, and integrate these ideas directly into their narrative structure.

1. The Data Foundation: Prisma Schema Evolution

Every ambitious feature starts with data. For our idea pipeline, we needed a new model: BookIdea.

typescript
// prisma/schema.prisma
model BookIdea {
  id               String      @id @default(cuid())
  content          String
  tags             String[]
  status           IdeaStatus  @default(loose) // loose | suggested | assigned | incorporated

  // AI suggestion fields
  suggestedBeatId  String?
  suggestedChapter Int?
  suggestionReason String?

  // Writer's decision fields
  assignedBeatId   String?
  assignedChapter  Int?

  bookId           String
  book             Book        @relation(fields: [bookId], references: [id], onDelete: Cascade)

  // ... other fields like createdAt, updatedAt
}

enum IdeaStatus {
  loose
  suggested
  assigned
  incorporated
}

This model provides the necessary structure to track an idea from its initial "loose" state, through AI "suggestion," to being "assigned" by the writer, and finally "incorporated" into a beat. We also linked BookIdea to Book and Tenant to ensure proper data ownership and querying. After a db:push, our database was ready.

2. The AI Brain: nyxbook-idea-suggest.ts

This is where the magic happens. We created a dedicated service layer to encapsulate our LLM interactions.

typescript
// src/server/services/nyxbook-idea-suggest.ts
import { OpenAI } from 'openai'; // or your BYOK LLM client

export async function suggestIdeaPlacement(ideaContent: string, existingBeats: Beat[], bookContext: BookContext) {
  // Construct a detailed prompt including book genre, existing characters, themes, current plot
  // ...
  const prompt = `Given the following idea: "${ideaContent}", and the current narrative structure of the book '${bookContext.title}' (genre: ${bookContext.genre}), suggest the most suitable beat and chapter for it. Consider character arcs, motif development, and dramatic pacing.
  Existing beats: ${JSON.stringify(existingBeats.map(b => ({ id: b.id, chapter: b.chapterNum, summary: b.summary })))}
  Return a JSON object: { suggestedBeatId: string, suggestedChapter: number, reason: string }`;

  const response = await openai.chat.completions.create({ /* ... */ });
  // Parse and return
}

export async function suggestBeatRefactoring(currentBeats: Beat[], characters: Character[], motifs: Motif[], bookContext: BookContext) {
  const prompt = `Analyze the following narrative elements for the book '${bookContext.title}':
  - Current Beats: ${JSON.stringify(currentBeats)}
  - Characters: ${JSON.stringify(characters.map(c => ({ name: c.name, arcSummary: c.arcSummary})))}
  - Motifs: ${JSON.stringify(motifs.map(m => ({ name: m.name, development: m.developmentNotes})))}
  - Overall Dramatic Structure: ${bookContext.dramaticStructureNotes}

  Suggest concrete refactoring actions for the beats (add, reorder, merge, split, modify summary) to improve character arc progression, motif development curves, world evolution, dramatic structure, and pacing.
  Return a JSON array of suggested actions.`;

  const response = await openai.chat.completions.create({ /* ... */ });
  // Parse and return
}

The suggestIdeaPlacement() function takes a loose idea and, armed with the context of the entire book, proposes a beat and chapter. The suggestBeatRefactoring() is a real beast – it analyzes the entire narrative (beats, characters, motifs, dramatic structure) and suggests actionable changes to improve the story's flow. We spent extra time refining this prompt to truly consider the complex interplay of narrative elements.

3. The API Gateway: tRPC Router

Exposing these powerful services to the frontend required extending our tRPC router.

typescript
// src/server/trpc/routers/nyxbook.ts
export const nyxbookRouter = t.router({
  // ... existing procedures
  ideas: t.router({
    list: publicProcedure.input(z.object({ bookId: z.string() })).query(/* ... */),
    create: publicProcedure.input(createIdeaSchema).mutation(async ({ input }) => {
      // Create idea, then call suggestIdeaPlacement()
      // ...
    }),
    update: publicProcedure.input(updateIdeaSchema).mutation(/* ... */),
    assign: publicProcedure.input(assignIdeaSchema).mutation(/* ... */),
    incorporate: publicProcedure.input(incorporateIdeaSchema).mutation(/* ... */),
    remove: publicProcedure.input(removeIdeaSchema).mutation(/* ... */),
    suggest: publicProcedure.input(suggestIdeaSchema).mutation(async ({ input }) => {
      // Re-run AI suggestion for an existing idea
      // ...
    }),
  }),
  beats: t.router({
    // ... existing beat procedures
    refactor: publicProcedure.input(z.object({ bookId: z.string() })).mutation(async ({ input }) => {
      // Call suggestBeatRefactoring()
      // ...
    }),
  }),
  // ...
});

We created a nested ideas router with 7 procedures to manage the full lifecycle of an idea. Crucially, create automatically triggers an AI suggestion, and suggest allows writers to re-run the AI if they've updated the idea or the surrounding context. We also added beats.refactor to expose the LLM-powered beat restructuring.

4. Visualizing the Narrative: The Beat Board Refactor

The old beat board was a flat grid – functional, but not inspiring. We completely rewrote src/components/nyxbook/beat-board.tsx to transform it into a chapter-grouped, timeline-like progression.

Key enhancements:

  • Chapter Grouping: Beats are now explicitly grouped by chapter, making the narrative flow much clearer.
  • Progression Dividers: Between chapters, we added visual cues: green indicators for new characters, strikethrough for absent characters, accent colors for new motifs, and strikethrough for resolved motifs. This provides an immediate sense of narrative progression and change.
  • Character & Motif Indicators: Each beat card now features consistent character color dots and, when collapsed, motif chips, offering a quick glance at who and what is active in that beat.
  • Unassigned Beats: A dedicated section at the bottom with a dashed border now houses beats not yet assigned to a chapter, making them easy to spot and integrate.
  • Ideas Integration: Idea count badges and linked ideas in the expanded view connect the pipeline directly to the beats.

This was a significant UI/UX lift, but the result is a dynamic, informative overview that truly helps writers understand their story's rhythm.

5. The Narrative Compass: Book Dashboard

A brand new file, src/components/nyxbook/book-dashboard.tsx, became the central hub for book insights.

  • Hero Header: A visually striking header with the book title, status badge, and an SVG chapter progress ring.
  • Quick Stats: Essential metrics at a glance: Chapters, Beats, Characters, Words, Motifs, Influences, Ideas.
  • Story Arc (SVG Curve): A beautiful bezier curve showing beat density per chapter, indicating pacing and dramatic highs/lows.
  • Character Presence (Heatmap): A "Stimmen-Präsenz" (Character Presence) heatmap, coloring cells based on a character's activity in a given chapter.
  • Motif Threads (Visualization): A "Motiv-Fäden" (Motif Threads) visualization, showing dots and connecting lines to illustrate motif continuity across chapters.
  • Chapter Overview: Compact chapter chips with status symbols and narrative indicators.
  • Ideas Pipeline: A stacked progress bar visualizing the loose -> suggested -> assigned -> incorporated journey of ideas.

Every panel on the dashboard is clickable, navigating the writer to the relevant tab for deeper exploration. It's a true "narrative compass," providing both macro and micro insights.

6. Navigational Clarity: Sidebar Restructuring

The old flat sidebar was getting unwieldy. We reorganized it into logical groups:

  • Book: Overview, Chapters, Beat Board, Ideas
  • World: Characters, Motifs, Inspirations
  • Create: Workshop (for generative tools)
  • Import: GitHub, Zip, Server Path (for bringing in existing projects)

This hierarchical structure significantly improves discoverability. We also added dedicated MotifsTab and InspirationsTab to display relevant JSON data as interactive cards.

7. Bringing Ideas to Life: The IdeasTab

Finally, a dedicated IdeasTab was built for managing the idea pipeline. It features a quick-add input, status filter tabs (loose, suggested, assigned, incorporated), and idea cards. Each card provides AI suggestions, along with actions to accept, reassign, re-suggest, incorporate, or delete the idea.

Lessons Learned from the Trenches

No ambitious development sprint is without its snags. These were two notable ones:

  • TypeScript's --downlevelIteration and Sets:

    • The Problem: I tried iterating directly over a Set<string> using for...of in beat-board.tsx, which resulted in a TS2802 error: "Type 'Set' can only be iterated with the '--downlevelIteration' flag enabled."
    • The Nuance: This TypeScript error occurs when your target in tsconfig.json is set to an older ECMAScript version (e.g., ES5) that doesn't natively support iteration protocols (Symbol.iterator) for certain built-in types like Set or Map. for...of relies on these protocols.
    • The Workaround: The quickest fix without changing tsconfig (which can have broader implications) was to convert the Set to an array: Array.from(mySet).forEach(...). This is a common pattern when working with older TS targets or when you need array-specific methods. It's a good reminder to be mindful of your tsconfig and the implications of modern JavaScript syntax.
  • undefined vs. null in TypeScript Props:

    • The Problem: I was passing pipeline (which could be undefined from a useQuery hook) directly to a BookDashboard prop typed as PipelineData | null. TypeScript correctly flagged TS2322: Type 'undefined' is not assignable to type 'null'.
    • The Nuance: In TypeScript's strict mode, undefined and null are distinct types. undefined typically means "not initialized" or "value doesn't exist," while null means "explicitly empty" or "no value." A prop typed T | null means it must be T or null; undefined is not allowed unless explicitly included (T | null | undefined).
    • The Workaround: The fix was simple but important for type correctness: pipeline={pipeline ?? null}. The nullish coalescing operator (??) ensures that if pipeline is undefined (or null), it defaults to null, satisfying the prop's type definition. This is a common pattern when dealing with optional data from hooks or APIs.

What's Next? The Road Ahead

With the core features now complete, the immediate next steps are crucial for validation and refinement:

  1. Commit and Push: Get these changes into the main branch!
  2. Real Data Verification: Import an existing book to see the dashboard visualizations come alive with real-world data. This will highlight any edge cases.
  3. End-to-End Ideas Flow: Test creating an idea, observing the AI suggestion, accepting it, and seeing it correctly linked on a beat card.
  4. Beat Refactoring Test: Click the "refactor" button and evaluate the LLM's suggestions for narrative improvement.
  5. Motifs/Inspirations CRUD: Currently read-only, adding full CRUD capabilities for these elements is a natural evolution.
  6. E2E Testing: Comprehensive end-to-end tests for the entire ideas pipeline and the beat board's new chapter-grouped progression.

This session was a monumental effort, pushing the boundaries of what nyxBook can do. Shipping features like these, that genuinely empower writers with intelligent tools, is incredibly rewarding. The journey continues, always striving to make the process of storytelling more intuitive, insightful, and inspiring.

json
{
  "thingsDone": [
    "Prisma schema updated with BookIdea model and relationships",
    "Service layer created for LLM-powered idea placement and beat refactoring suggestions",
    "tRPC router extended with ideas (7 procedures) and beat refactoring endpoints",
    "Beat Board completely rewritten for chapter-grouped timeline view with rich visual cues",
    "New Book Dashboard component built with multiple data visualizations (story arc, character presence, motif threads, idea pipeline)",
    "Sidebar navigation restructured into logical groups",
    "Dedicated IdeasTab created for managing AI-suggested ideas"
  ],
  "pains": [
    "TypeScript TS2802: `for...of` on Set without `--downlevelIteration`",
    "TypeScript TS2322: `undefined` not assignable to `null` in props"
  ],
  "successes": [
    "Successful integration of LLM for complex narrative analysis and suggestions",
    "Complete rewrite of a major UI component (Beat Board) with significant UX improvements",
    "Development of a comprehensive data visualization dashboard from scratch",
    "Clean type-checking and linting across a large feature set",
    "Achieved all major goals for the session"
  ],
  "techStack": [
    "TypeScript",
    "Next.js",
    "tRPC",
    "Prisma",
    "PostgreSQL",
    "React",
    "Tailwind CSS",
    "OpenAI (or similar BYOK LLM)",
    "Docker (for dev environment)"
  ]
}