nyxcore-systems
6 min read

Intelligent Notes & Action Points: Building a Smarter Productivity Workflow

We just wrapped up a session focused on supercharging our project notes with intelligent AI enrichment and a more organized way to track action points. Dive into how we built per-note AI settings and grouped action items, tackling common state management and React challenges along the way.

Next.jsTypeScriptPrismatRPCAIProductivityFrontendBackendLessons Learned

Every development session brings a mix of focused implementation, problem-solving, and a dash of "aha!" moments. This past session was no different, as we pushed forward on two significant features designed to make our project management tool even more powerful:

  1. Per-Note AI Enrichment Settings: Giving users granular control over how AI models enrich their individual notes.
  2. Grouped Action Points: Organizing action items by their source note for better context and clarity.

Both features are now fully implemented, type-checked, and lint-clean, ready to be integrated into the main branch. Let's break down how we got there, the architecture involved, and the valuable lessons we learned along the way.

What We Built: Elevating Note Intelligence and Organization

Our primary goal was to empower users with more control over AI-driven note enrichment and to provide a clearer overview of action points. This required a full-stack approach, touching our database, backend services, and frontend UI.

1. Database & Backend Foundations

To link action points back to their origin, we needed a strong data model.

  • Schema Evolution: We introduced a sourceNoteId foreign key to our ActionPoint model in prisma/schema.prisma, linking it directly to ProjectNote. We also added a back-relation (actionPoints ActionPoint[]) to ProjectNote, allowing us to easily retrieve all action points associated with a specific note. After defining the schema, npm run db:push && npm run db:generate ensured our database and Prisma client were perfectly in sync.
  • Enrichment Service Enhancements: The core enrichNoteWithWisdom() service in src/server/services/note-enrichment.ts was extended. It now accepts optional providerName, modelOverride, and personaId parameters. This flexibility is key to allowing users to customize their enrichment experience.
  • Secure Persona Handling: A critical security consideration was ensuring that personaId queries are scoped to the user's tenant or to built-in personas. This prevents any cross-tenant information disclosure, keeping user data isolated and secure.
  • tRPC Procedure Updates: Our src/server/trpc/routers/projects.ts router's enrich procedure was updated to accept the new provider, modelOverride, and personaId inputs. Crucially, the applyEnrichment procedure was also modified to pass sourceNoteId: input.noteId when creating new action points, establishing that vital link between an action and its source.
  • Action Point Listing with Context: To support the new grouped view, our list procedure in src/server/trpc/routers/action-points.ts was updated to include the sourceNote: { select: { id, title } } relation. This allows the frontend to display the note's title alongside its associated action points.

2. Frontend User Experience

The magic truly comes alive in the user interface, where these backend changes translate into tangible improvements.

  • Per-Note Enrichment Settings UI: In the project detail page (src/app/(dashboard)/dashboard/projects/[id]/page.tsx), we built a clean, intuitive UI within each note's section. This includes:
    • A collapsible "Enrichment Settings" panel.
    • Provider buttons (e.g., OpenAI, Anthropic).
    • A model grid for selecting specific LLMs.
    • A PersonaPicker for choosing an AI persona.
    • The settings are passed directly to the enrichNote.mutate() call, giving users immediate feedback and control.
  • Intelligent Action Point Grouping: The ActionPointGroups component in the ActionPointsTab is a game-changer for organization.
    • It intelligently groups action points by their sourceNoteId, displaying them under collapsible headers that show the source note's title and an action point count badge.
    • A dedicated "Manual / Other" section catches any ungrouped items (e.g., action points created manually without a source note).
    • A flat list fallback ensures a good user experience even when no grouping is present.
    • Careful attention was paid to React key placement on .map() call sites to ensure efficient rendering.

Lessons Learned: Navigating the Challenges

Development isn't always a smooth road. We hit a couple of common snags, but overcoming them provided valuable insights.

Challenge 1: Shared State vs. Per-Note State

  • The Problem: Our initial approach for the enrichment settings UI was to use a single useState hook for provider, model, and persona across all notes on the page.
  • The Flaw: During code review, it became immediately clear that opening the settings for one note would inadvertently affect the displayed settings for all other notes simultaneously. Not ideal for a per-note configuration!
  • The Solution: We refactored to use a Map<noteId, EnrichSettings> to store the enrichment settings state. This allowed each note to maintain its own isolated configuration, providing a much better and more intuitive user experience. Helper functions like getEnrichSettings() and patchEnrichSettings() streamlined interaction with this map. This is a classic example of why local, isolated state is often preferable for UI components that need independent configurations.

Challenge 2: React key Prop Placement

  • The Problem: In our ActionPointGroups component, we initially tried setting the React key prop inside a renderItem helper function, which was then called within a .map() loop.
  • The Flaw: React's reconciliation algorithm expects the key prop to be on the element directly returned by the .map() callback. Placing it inside a helper function, on an inner <Card> for instance, meant React wouldn't recognize it for list optimization, potentially leading to inefficient re-renders or unexpected behavior.
  • The Solution: The fix was simple but crucial: we moved the key prop to the immediate child of the .map() call: {items.map((ap) => <div key={ap.id}>{renderItem(ap)}</div>)}. This ensures React can correctly identify and track each item in the list.

What's Next: Refining and Expanding

With these features implemented, our immediate next steps involve getting them properly integrated and considering further refinements:

  1. Commit and Integrate: The first order of business is to commit all changes (a mix of these features and some prior work) and push them for review.
  2. Robust Input Validation: We'll enhance the enrich procedure by validating the provider input against a known LLM_PROVIDERS enum using z.enum() instead of a free-form z.string(). This adds an extra layer of type safety and prevents unexpected provider names.
  3. Performance Optimization: For the ActionPointGroups component, we'll consider adding useMemo to the grouping logic. This can prevent unnecessary re-calculations of groups when the underlying action points haven't changed, improving performance on larger datasets.
  4. LLM Model Constants: We noticed that ollama is currently missing from our FAST_MODELS constant. A quick addition will ensure it's correctly recognized and utilized where appropriate.
  5. Enhanced Security Scoping: The applyEnrichment procedure could benefit from an additional tenant/project scope check on the projectNote.update where clause. Currently, it only filters by id, and adding this extra layer of verification would bolster data integrity and security.
  6. End-to-End Testing: Finally, a thorough end-to-end test is essential: expanding a note, configuring enrichment settings, triggering enrichment, applying the results, verifying sourceNoteId population on new action points, and confirming the grouped view in the Actions tab.

This session was a productive leap forward, adding significant value to our project management tool. By focusing on both intelligent functionality and a seamless user experience, we're continuously striving to build a platform that truly empowers our users.