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.
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:
- Per-Note AI Enrichment Settings: Giving users granular control over how AI models enrich their individual notes.
- 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
sourceNoteIdforeign key to ourActionPointmodel inprisma/schema.prisma, linking it directly toProjectNote. We also added a back-relation (actionPoints ActionPoint[]) toProjectNote, allowing us to easily retrieve all action points associated with a specific note. After defining the schema,npm run db:push && npm run db:generateensured our database and Prisma client were perfectly in sync. - Enrichment Service Enhancements: The core
enrichNoteWithWisdom()service insrc/server/services/note-enrichment.tswas extended. It now accepts optionalproviderName,modelOverride, andpersonaIdparameters. This flexibility is key to allowing users to customize their enrichment experience. - Secure Persona Handling: A critical security consideration was ensuring that
personaIdqueries 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.tsrouter'senrichprocedure was updated to accept the newprovider,modelOverride, andpersonaIdinputs. Crucially, theapplyEnrichmentprocedure was also modified to passsourceNoteId: input.noteIdwhen 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
listprocedure insrc/server/trpc/routers/action-points.tswas updated to include thesourceNote: { 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
PersonaPickerfor 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
ActionPointGroupscomponent 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
keyplacement on.map()call sites to ensure efficient rendering.
- It intelligently groups action points by their
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
useStatehook forprovider,model, andpersonaacross 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 likegetEnrichSettings()andpatchEnrichSettings()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
ActionPointGroupscomponent, we initially tried setting the Reactkeyprop inside arenderItemhelper function, which was then called within a.map()loop. - The Flaw: React's reconciliation algorithm expects the
keyprop 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
keyprop 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:
- 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.
- Robust Input Validation: We'll enhance the
enrichprocedure by validating theproviderinput against a knownLLM_PROVIDERSenum usingz.enum()instead of a free-formz.string(). This adds an extra layer of type safety and prevents unexpected provider names. - Performance Optimization: For the
ActionPointGroupscomponent, we'll consider addinguseMemoto the grouping logic. This can prevent unnecessary re-calculations of groups when the underlying action points haven't changed, improving performance on larger datasets. - LLM Model Constants: We noticed that
ollamais currently missing from ourFAST_MODELSconstant. A quick addition will ensure it's correctly recognized and utilized where appropriate. - Enhanced Security Scoping: The
applyEnrichmentprocedure could benefit from an additional tenant/project scope check on theprojectNote.updatewhere clause. Currently, it only filters byid, and adding this extra layer of verification would bolster data integrity and security. - 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
sourceNoteIdpopulation 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.