Smart Notes, Smarter Actions: Elevating Project Management with Granular AI Enrichment & Grouped Insights
Dive into how we empowered users with fine-grained control over AI note enrichment and brought clarity to action points by grouping them by their source notes. A journey through state management, type safety, and database security.
In the fast-paced world of software development, turning raw ideas into actionable tasks is a critical bottleneck. Our project management platform aims to streamline this by leveraging AI to enrich notes and then converting those insights into actionable items. Recently, we embarked on a mission to make this process even more powerful, customizable, and intuitive.
Our latest development sprint focused on two key enhancements:
- Granular AI Enrichment: Giving users the power to choose their AI provider, model, and even a specific persona for note enrichment.
- Organized Action Points: Bringing clarity to project tasks by grouping action points directly under their source notes.
This post will walk you through the features we built, the challenges we faced, and the valuable lessons we learned along the way.
What We Built: Empowering Smart Notes and Smarter Actions
Our core goal was to move beyond a "one-size-fits-all" AI experience and to provide a more structured view of derived action points.
1. Granular AI Enrichment: Your AI, Your Way
Previously, our AI note enrichment was a bit of a black box. While effective, it lacked the flexibility many users desired. Now, we've opened up the controls:
- Provider & Model Selection: Users can now specify which LLM provider (e.g., OpenAI, Claude, Ollama) and even which specific model they want to use for enriching a particular note. This is crucial for balancing cost, performance, and specific AI capabilities.
- On the backend, our
enrichNoteWithWisdom()service insrc/server/services/note-enrichment.tswas extended to acceptproviderName,modelOverride, andpersonaId. - Input validation for the provider is handled robustly via
z.enum(LLM_PROVIDERS)in our tRPCenrichprocedure, ensuring only supported providers are accepted. - We also added a crucial guard for missing
FAST_MODELSentries (e.g., for self-hosted Ollama instances), throwing a clear error if a configured model isn't found.
- On the backend, our
- Persona-Driven Insights: Imagine enriching a note about a user interview from the perspective of a "Product Manager" or a "Marketing Strategist." With persona selection, users can now guide the AI's output to better suit their needs.
- A significant security enhancement here was scoping persona queries to the user's tenant or built-in personas, preventing accidental exposure or misuse.
- Intuitive UI for Control: All these settings are exposed in a sleek, per-note enrichment panel within the
NotesTab. This involved building aMap<noteId, EnrichSettings>to manage the state of enrichment preferences for each individual note, ensuring that changing settings for one note doesn't inadvertently affect others.
2. Action Points, Grouped by Source Note
As projects grow, the list of action points can become overwhelming. Without context, it's hard to remember why a particular action was generated. Our solution: group action points by their originating note.
- Database Foundation: We extended our
prisma/schema.prismaby adding asourceNoteIdforeign key (nullable UUID) to theActionPointmodel, linking it back to theProjectNote. We also added anactionPoints ActionPoint[]back-relation onProjectNotefor easy querying. - Backend Integration: The
applyEnrichmentprocedure now correctly passes thesourceNoteIdwhen creating new action points. Crucially, we added tenant and project ownership checks before any updates to ensure data integrity and security. - Smart Grouping UI: On the frontend, we developed a new
ActionPointGroupscomponent. This component intelligently groups action points by theirsourceNote, displaying them under collapsible headers. To keep the UI snappy, we memoized the grouping logic, preventing unnecessary re-calculations. We also took care to fix React key placement on.map()call sites for optimal performance and stability.
Challenges & Lessons Learned
No development sprint is without its hurdles. These challenges, however, provided valuable learning opportunities that strengthened our architecture and development practices.
1. The Peril of Shared State: Per-Note Settings vs. Global Chaos
- Initial Approach: Our first thought for enrichment settings was to use a shared state variable that would apply across all notes.
- The Problem: During code review, it became immediately apparent that this would lead to a poor user experience. Changing the AI model for one note would unexpectedly change it for all notes currently open or being viewed, leading to confusion and potential data inconsistencies.
- The Fix: We pivoted to a
Map<noteId, EnrichSettings>data structure to manage settings. Each note now maintains its own independent enrichment configuration, accessed and updated viagetEnrichSettings()andpatchEnrichSettings()functions. - Lesson Learned: In dynamic UIs with multiple instances of similar components, carefully consider the scope of your state. Global state is tempting for simplicity but can quickly lead to unintended side effects. Granular, component-specific, or ID-keyed state is often the more robust solution for complex interactions.
2. Tightening Type Safety with Zod: From Arbitrary Strings to Enforced Providers
- Initial Approach: When defining the input for our tRPC
enrichprocedure, we initially usedz.string()for theproviderNamefield. - The Problem: While technically validating the type,
z.string()allows any string. This meant a malicious or mistaken user could sendproviderName: "not-a-real-provider"and potentially cause an unhandled error or expose internal logic. It also made type inference less precise on the frontend. - The Fix: We tightened the validation to
z.enum(LLM_PROVIDERS), whereLLM_PROVIDERSis a constant array of our supported AI provider names. This not only restricts input to valid options but also allowed us to typeEnrichSettings.providerasLLMProviderName, giving us compile-time safety across the stack. - Lesson Learned: Zod's
enumvalidator is incredibly powerful for enforcing strict input constraints against a known set of values. It's a crucial tool for building robust, type-safe APIs that prevent invalid data from reaching your business logic, enhancing both security and developer experience.
3. Ensuring Data Ownership in Prisma: Beyond Simple where Clauses
- Initial Approach: When updating project-related entities like
ActionPointorProjectNote, we initially tried to use a compoundwhereclause in Prisma'supdatemethod, likewhere: { id: actionPointId, tenantId: userTenantId, projectId: userProjectId }. - The Problem: Prisma's
updatemethod typically requires thewhereclause to target fields marked with@idor@@uniquein your schema. OurtenantIdandprojectIdfields were not unique identifiers on their own (as they can appear on many records). This meant our compoundwherefor ownership checks wouldn't work directly withupdate. - The Fix: We implemented a two-step process. First, we perform a
findFirstquery with the compoundwhereclause (id,tenantId,projectId) to explicitly verify that the requesting user owns the record. If the record is found, then we proceed with a standardupdateusing only the@idfield. - Lesson Learned: Always explicitly verify data ownership and permissions before performing sensitive write operations. While ORMs like Prisma are powerful, they don't implicitly handle all security concerns. A separate, explicit ownership check is a critical safeguard against unauthorized data modification, especially in multi-tenant applications.
Immediate Next Steps
With the core features deployed, our immediate focus shifts to ensuring everything works perfectly and planning for future refinements:
- End-to-End Verification: A thorough end-to-end test is essential: expand a note, configure enrichment settings, trigger enrichment, apply the generated action points, and finally, verify that they are correctly grouped in the Actions tab.
- Component Extraction: The
NoteEnrichmentPanelis currently integrated directly into theNotesTab. As the page grows, we'll consider extracting it into a standalone component for better modularity and maintainability. - Local Dev Environment: The
.claude/directory remains untracked, which is by design for local settings.
Conclusion
This sprint has significantly enhanced our platform's intelligence and usability. By giving users fine-grained control over AI enrichment and bringing much-needed structure to action points, we're empowering them to manage projects with unprecedented clarity and efficiency. The journey also reinforced key principles around state management, type safety, and database security – invaluable lessons that will guide our future development. We're excited to see how these features help our users turn insights into impactful actions!