--- stepsCompleted: [1, 2, 3, 4, 5] workflow_completed: true inputDocuments: - _bmad-output/planning-artifacts/prd-phase1-mvp-ai.md - _bmad-output/planning-artifacts/ux-design-specification.md - _bmad-output/planning-artifacts/architecture.md - _bmad-output/analysis/brainstorming-session-2026-01-09.md workflow_type: 'create-epics-and-stories' project_name: 'Keep (Memento Phase 1 MVP AI)' user_name: 'Ramez' date: '2026-01-10' focus_area: 'Phase 1 MVP AI - AI-Powered Note Taking Features' communication_language: 'French' document_output_language: 'English' status: 'completed' --- # Keep (Memento) - Epic Breakdown - Phase 1 MVP AI ## Overview This document provides the complete epic and story breakdown for **Keep Phase 1 MVP AI**, decomposing the requirements from the Phase 1 PRD, UX Design Specification, and Architecture into implementable stories. **Project Context:** Brownfield extension of existing Keep Notes application with AI-powered features. Zero breaking changes to existing functionality. **Implementation Timeline:** 12 weeks (4 phases) **Target:** Production-ready MVP with 6 core AI features --- ## Requirements Inventory ### Functional Requirements - Phase 1 MVP **Core AI Features:** - **FR6:** Real-time content analysis for concept identification - **FR7:** AI-powered tag suggestions based on content analysis - **FR8:** User control over AI suggestions (accept/modify/reject) - **FR11:** Exact keyword search (title and content) - **FR12:** Semantic search by meaning/intention (natural language) - **FR13:** Hybrid search combining exact + semantic results **Foundation Features (Already Implemented):** - **FR1:** CRUD operations for notes (text and checklist) - **FR2:** Pin notes to top of list - **FR3:** Archive notes - **FR4:** Attach images to notes - **FR5:** Drag-and-drop reordering (Muuri) - **FR9:** Manual tag management - **FR10:** Filter and sort by tags - **FR16:** Optimistic UI for immediate feedback **Configuration & Administration:** - **FR17:** AI provider configuration (OpenAI, Ollama) - **FR18:** Multi-provider support via Vercel AI SDK - **FR19:** Theme customization (dark mode) **Deferred to Phase 2/3:** - **FR14:** Offline PWA mode - **FR15:** Background sync ### Non-Functional Requirements **Performance:** - **NFR1:** Auto-tagging < 1.5s after typing ends - **NFR2:** Semantic search < 300ms for 1000 notes - **NFR3:** Title suggestions < 2s after detection **Security & Privacy:** - **NFR4:** API key isolation (server-side only) - **NFR5:** Local-first privacy (Ollama = 100% local) **Reliability:** - **NFR8:** Vector integrity (automatic background updates) **Portability:** - **NFR9:** Minimal footprint (Zero DevOps) - **NFR10:** Node.js LTS support --- ## Phase 1 MVP AI Epic Mapping ### Epic 1: Intelligent Title Suggestions ⭐ **Focus:** AI-powered title generation for untitled notes **FRs covered:** FR6, FR8 **Architecture Decision:** Decision 1 (Database Schema), Decision 3 (Language Detection) **Priority:** HIGH (Core user experience feature) ### Epic 2: Hybrid Semantic Search 🔍 **Focus:** Keyword + vector search with RRF fusion **FRs covered:** FR11, FR12, FR13 **Architecture Decision:** Decision 1 (Database Schema - reuses Note.embedding) **Priority:** HIGH (Core discovery feature) ### Epic 3: Paragraph-Level Reformulation ✍️ **Focus:** AI-powered text improvement (Clarify, Shorten, Improve Style) **FRs covered:** FR6, FR8 **Architecture Decision:** Decision 1 (Database Schema - no schema change) **Priority:** MEDIUM (User productivity feature) ### Epic 4: Memory Echo (Proactive Connections) 🧠 **Focus:** Daily proactive note connections via cosine similarity **FRs covered:** FR6 **Architecture Decision:** Decision 2 (Memory Echo Architecture) **Priority:** HIGH (Differentiating feature) ### Epic 5: AI Settings Panel ⚙️ **Focus:** Granular ON/OFF controls per feature + provider selection **FRs covered:** FR17, FR18 **Architecture Decision:** Decision 4 (AI Settings Architecture) **Priority:** HIGH (User control requirement) ### Epic 6: Language Detection Service 🌐 **Focus:** Automatic language detection (TinyLD hybrid approach) **FRs covered:** FR6 (Cross-cutting concern) **Architecture Decision:** Decision 3 (Language Detection Strategy) **Priority:** HIGH (Enables multilingual prompts) --- ## Epic 1: Intelligent Title Suggestions ### Overview Generate 3 AI-powered title suggestions when a note reaches 50+ words without a title. User can accept, modify, or reject suggestions. **User Stories:** 3 **Estimated Complexity:** Medium **Dependencies:** Language Detection Service, AI Provider Factory ### Story 1.1: Real-time Word Count Detection **As a user, I want the system to detect when my note reaches 50+ words without a title, so that I can receive title suggestions automatically.** **Acceptance Criteria:** - **Given** an open note editor - **When** I type content and the word count reaches 50+ - **And** the note title field is empty - **Then** the system triggers background title generation - **And** a non-intrusive toast notification appears: "💡 Title suggestions available" **Technical Requirements:** - Word count triggered on `debounce` (300ms after typing stops) - Detection logic: `content.split(/\s+/).length >= 50` - Must not interfere with typing experience (non-blocking) - Toast notification uses Sonner (radix-ui compatible) **Implementation Files:** - Component: `keep-notes/components/ai/ai-suggestion.tsx` (NEW) - Hook: `useWordCountDetection` (NEW utility) - UI: Toast notification with "View" / "Dismiss" actions --- ### Story 1.2: AI Title Generation **As a system, I want to generate 3 relevant title suggestions using AI, so that users can quickly organize their notes.** **Acceptance Criteria:** - **Given** a note with 50+ words of content - **When** title generation is triggered - **Then** the AI generates 3 distinct title suggestions - **And** each title is concise (3-8 words) - **And** titles reflect the main concept of the content - **And** generation completes within < 2 seconds **Technical Requirements:** - Service: `TitleSuggestionService` in `lib/ai/services/title-suggestion.service.ts` - Provider: Uses `getAIProvider()` factory (OpenAI or Ollama) - System Prompt: English (stability) - User Data: Local language (FR, EN, ES, DE, FA, etc.) - Language Detection: Called before generation for multilingual prompts - Storage: Suggestions stored in memory (not persisted until user accepts) **Prompt Engineering:** ``` System: You are a title generator. Generate 3 concise titles (3-8 words each) that capture the main concept. User Language: {detected_language} Content: {note_content} Output format: JSON array of strings ``` **Error Handling:** - If AI fails: Retry once with different provider (if available) - If retry fails: Show toast error "Failed to generate suggestions. Please try again." - Timeout: 5 seconds maximum --- ### Story 1.3: User Interaction & Feedback **As a user, I want to accept, modify, or reject title suggestions, so that I maintain full control over my note organization.** **Acceptance Criteria:** - **Given** 3 AI-generated title suggestions - **When** I click the toast notification - **Then** a modal displays the 3 suggestions - **And** I can click any suggestion to apply it as the note title - **And** I can click "Dismiss" to ignore all suggestions - **And** the modal closes automatically after selection or dismissal **Technical Requirements:** - Component: `AiSuggestionModal` (extends `components/ai/ai-suggestion.tsx`) - Server Action: `updateNote(noteId, { title })` - Feedback: Store user choice in `AiFeedback` table - `feedbackType`: 'thumbs_up' if accepted without modification - `feature`: 'title_suggestion' - `originalContent`: All 3 suggestions (JSON array) - `correctedContent`: User's final choice (or modified title) **UI/UX Requirements (from UX Design Spec):** - Modal design: Clean, centered, with card-style suggestions - Each suggestion: Clickable card with hover effect - "Dismiss" button: Secondary action at bottom - Auto-close after selection (no confirmation dialog) - If user modifies title: Record as 'correction' feedback **Implementation Files:** - Modal: `components/ai/ai-suggestion.tsx` (NEW) - Server Action: `app/actions/ai-suggestions.ts` (NEW) - API Route: `/api/ai/feedback` (NEW) - stores feedback **Database Updates:** ```typescript // When user accepts a title await prisma.note.update({ where: { id: noteId }, data: { title: selectedTitle, autoGenerated: true, aiProvider: currentProvider, aiConfidence: 85, // Placeholder - Phase 3 will calculate lastAiAnalysis: new Date() } }) // Store feedback for Phase 3 trust scoring await prisma.aiFeedback.create({ data: { noteId, userId: session.user.id, feedbackType: 'thumbs_up', feature: 'title_suggestion', originalContent: JSON.stringify(allThreeSuggestions), correctedContent: selectedTitle, metadata: JSON.stringify({ provider: currentProvider, model: modelName, timestamp: new Date() }) } }) ``` --- ## Epic 2: Hybrid Semantic Search ### Overview Combine exact keyword matching with vector similarity search using Reciprocal Rank Fusion (RRF) for comprehensive results. **User Stories:** 3 **Estimated Complexity:** High **Dependencies:** Existing embeddings system, Language Detection (optional) ### Story 2.1: Query Embedding Generation **As a system, I want to generate vector embeddings for user search queries, so that I can find notes by meaning.** **Acceptance Criteria:** - **Given** a user search query - **When** the search is executed - **Then** the system generates a vector embedding for the query - **And** the embedding is stored in memory (not persisted) - **And** generation completes within < 200ms **Technical Requirements:** - Service: `SemanticSearchService` in `lib/ai/services/semantic-search.service.ts` - Provider: Uses `getAIProvider()` factory - Embedding Model: `text-embedding-3-small` (OpenAI) or Ollama equivalent - Language Detection: Optional (can detect query language for better results) - Caching: Query embeddings cached in React Cache (5-minute TTL) **Implementation:** ```typescript // lib/ai/services/semantic-search.service.ts async generateQueryEmbedding(query: string): Promise { const provider = getAIProvider() const embedding = await provider.generateEmbedding(query) return embedding } ``` --- ### Story 2.2: Vector Similarity Calculation **As a system, I want to calculate cosine similarity between query and all user notes, so that I can rank results by meaning.** **Acceptance Criteria:** - **Given** a query embedding and all user note embeddings - **When** similarity calculation runs - **Then** the system calculates cosine similarity for each note - **And** returns notes ranked by similarity score (descending) - **And** calculation completes within < 300ms for 1000 notes **Technical Requirements:** - Algorithm: Cosine similarity - Formula: `similarity = dotProduct(queryEmbedding, noteEmbedding) / (magnitude(query) * magnitude(note))` - Threshold: Notes with similarity < 0.3 are filtered out - Performance: In-memory calculation (no separate vector DB for Phase 1) **Implementation:** ```typescript // lib/ai/services/semantic-search.service.ts async searchBySimilarity( queryEmbedding: number[], userId: string ): Promise> { // Fetch all user notes with embeddings const notes = await prisma.note.findMany({ where: { userId }, select: { id: true, title: true, content: true, embedding: true } }) // Calculate cosine similarity const results = notes .map(note => ({ note, score: cosineSimilarity(queryEmbedding, JSON.parse(note.embedding)) })) .filter(r => r.score > 0.3) // Threshold filter .sort((a, b) => b.score - a.score) return results } ``` --- ### Story 2.3: Hybrid Search with RRF Fusion **As a user, I want to see combined results from keyword search and semantic search, so that I get the most comprehensive results.** **Acceptance Criteria:** - **Given** a search query - **When** I execute the search - **Then** the system performs BOTH keyword search AND semantic search - **And** results are fused using Reciprocal Rank Fusion (RRF) - **And** each result displays a badge: "Exact Match" or "Related" - **And** total time < 300ms for 1000 notes **Technical Requirements:** - Service: `SemanticSearchService` (extends from Story 2.1, 2.2) - Fusion Algorithm: Reciprocal Rank Fusion (RRF) - `RRF(score) = 1 / (k + rank)` where k = 60 (standard value) - Combined score = `RRF(keyword_rank) + RRF(semantic_rank)` - Keyword Search: Existing Prisma query (title/content LIKE `%query%`) - Semantic Search: Cosine similarity from Story 2.2 - Result Limit: Top 20 notes **RRF Implementation:** ```typescript // lib/ai/services/semantic-search.service.ts async hybridSearch( query: string, userId: string ): Promise> { // Parallel execution const [keywordResults, semanticResults] = await Promise.all([ this.keywordSearch(query, userId), // Existing implementation this.searchBySimilarity(query, userId) // Story 2.2 ]) // Calculate RRF scores const k = 60 const scoredNotes = new Map() // Add keyword RRF scores keywordResults.forEach((note, index) => { const rrf = 1 / (k + index + 1) scoredNotes.set(note.id, { note, keywordScore: rrf, semanticScore: 0, combinedScore: rrf }) }) // Add semantic RRF scores and combine semanticResults.forEach(({ note, score }, index) => { const rrf = 1 / (k + index + 1) if (scoredNotes.has(note.id)) { const existing = scoredNotes.get(note.id) existing.semanticScore = rrf existing.combinedScore += rrf } else { scoredNotes.set(note.id, { note, keywordScore: 0, semanticScore: rrf, combinedScore: rrf }) } }) // Convert to array and sort by combined score return Array.from(scoredNotes.values()) .sort((a, b) => b.combinedScore - a.combinedScore) .slice(0, 20) // Top 20 results } ``` **UI Requirements (from UX Design Spec):** - Component: `components/ai/semantic-search-results.tsx` (NEW) - Badge display: - "Exact Match" badge: Blue background, shown if `keywordScore > 0` - "Related" badge: Gray background, shown if `semanticScore > 0` AND `keywordScore === 0` - Both badges can appear if note matches both - Result card: Displays title, content snippet (100 chars), badges - Loading state: Skeleton cards while searching (< 300ms) **API Route:** - Endpoint: `POST /api/ai/search` - Request schema: ```typescript { query: string, userId: string } ``` - Response: ```typescript { success: true, data: { results: Array<{ note: Note, badges: Array<"Exact Match" | "Related"> }>, totalResults: number, searchTime: number // milliseconds } } ``` --- ## Epic 3: Paragraph-Level Reformulation ### Overview AI-powered text improvement with 3 options: Clarify, Shorten, Improve Style. Triggered via context menu on text selection. **User Stories:** 2 **Estimated Complexity:** Medium **Dependencies:** AI Provider Factory ### Story 3.1: Context Menu Integration **As a user, I want to select text and see "Reformulate" options in a context menu, so that I can improve my writing with AI assistance.** **Acceptance Criteria:** - **Given** a note editor with text content - **When** I select one or more paragraphs (50-500 words) - **And** I right-click or long-press - **Then** a context menu appears with "Reformulate" submenu - **And** the submenu shows 3 options: "Clarify", "Shorten", "Improve Style" - **When** I click any option - **Then** the selected text is sent to AI for reformulation - **And** a loading indicator appears on the selected text **Technical Requirements:** - Component: `components/ai/paragraph-refactor.tsx` (NEW) - Context Menu: Extends existing note editor context menu (Radix Dropdown Menu) - Text Selection: `window.getSelection()` API - Word Count Validation: 50-500 words (show error if out of range) - Loading State: Skeleton or spinner overlay on selected text **UI Implementation:** ```typescript // components/ai/paragraph-refactor.tsx 'use client' import { useCallback } from 'react' import { startTransition } from 'react' export function ParagraphRefactor({ noteId, content }: { noteId: string, content: string }) { const handleTextSelection = useCallback(() => { const selection = window.getSelection() const selectedText = selection?.toString() const wordCount = selectedText?.split(/\s+/).length || 0 if (wordCount < 50 || wordCount > 500) { showError('Please select 50-500 words to reformulate') return } // Show context menu at selection position showContextMenu(selection.getRangeAt(0)) }, []) const handleRefactor = async (option: 'clarify' | 'shorten' | 'improve') => { const selectedText = window.getSelection()?.toString() startTransition(async () => { showLoadingState() const result = await refactorParagraph(noteId, selectedText, option) hideLoadingState() showRefactorDialog(result.refactoredText) }) } return ( // Context menu integration Reformulate handleRefactor('clarify')}> Clarify handleRefactor('shorten')}> Shorten handleRefactor('improve')}> Improve Style ) } ``` --- ### Story 3.2: AI Reformulation & Application **As a user, I want to see AI-reformulated text and choose to apply or discard it, so that I can improve my writing while maintaining control.** **Acceptance Criteria:** - **Given** selected text sent for reformulation - **When** AI completes processing (< 2 seconds) - **Then** a modal displays showing: - Original text (left side) - Reformulated text (right side) with diff highlighting - "Apply" and "Discard" buttons - **When** I click "Apply" - **Then** the reformulated text replaces the original in the note - **And** the change is saved automatically - **When** I click "Discard" - **Then** the modal closes and no changes are made **Technical Requirements:** - Service: `ParagraphRefactorService` in `lib/ai/services/paragraph-refactor.service.ts` - Provider: Uses `getAIProvider()` factory - System Prompt: English (stability) - User Data: Local language (respects language detection) - Diff Display: Use `react-diff-viewer` or similar library **Prompt Engineering:** ``` System: You are a text reformulator. Reformulate the text according to the user's chosen option. User Language: {detected_language} Option: {clarify|shorten|improve} Clarify: Make the text clearer and easier to understand Shorten: Reduce word count by 30-50% while keeping key information Improve Style: Enhance readability, flow, and professional tone Original Text: {selected_text} Output: Reformulated text only (no explanations) ``` **UI Implementation:** ```typescript // Modal component (extends paragraph-refactor.tsx) export function RefactorModal({ originalText, refactoredText, onApply, onDiscard }) { return ( Compare & Apply

Original

{originalText}

Refactored

{refactoredText}
) } ``` **Server Action:** ```typescript // app/actions/ai-suggestions.ts 'use server' import { auth } from '@/auth' import { ParagraphRefactorService } from '@/lib/ai/services/paragraph-refactor.service' import { updateNote } from './notes' export async function refactorParagraph( noteId: string, selectedText: string, option: 'clarify' | 'shorten' | 'improve' ) { const session = await auth() if (!session?.user?.id) throw new Error('Unauthorized') const service = new ParagraphRefactorService() const refactoredText = await service.refactor(selectedText, option) return { success: true, originalText: selectedText, refactoredText } } export async function applyRefactoring( noteId: string, originalText: string, refactoredText: string ) { const session = await auth() if (!session?.user?.id) throw new Error('Unauthorized') // Get current note content const note = await prisma.note.findUnique({ where: { id: noteId } }) if (!note?.userId || note.userId !== session.user.id) { throw new Error('Note not found') } // Replace original text with refactored text const newContent = note.content.replace(originalText, refactoredText) await updateNote(noteId, { content: newContent }) return { success: true } } ``` **Feedback Collection:** ```typescript // Track which reformulation option users prefer await prisma.aiFeedback.create({ data: { noteId, userId: session.user.id, feedbackType: 'correction', // User chose to apply feature: 'paragraph_refactor', originalContent: originalText, correctedContent: refactoredText, metadata: JSON.stringify({ option, // 'clarify' | 'shorten' | 'improve' provider: currentProvider, timestamp: new Date() }) } }) ``` --- ## Epic 4: Memory Echo (Proactive Connections) ### Overview Background process that identifies connections between notes using cosine similarity. Displays 1 insight per day (max similarity > 0.75). **User Stories:** 2 **Estimated Complexity:** High **Dependencies:** Existing embeddings system, Decision 2 (Server Action + Queue pattern) ### Story 4.1: Background Insight Generation **As a system, I want to analyze all user note embeddings daily to find connections, so that I can proactively suggest related notes.** **Acceptance Criteria:** - **Given** a user with 10+ notes (each with embeddings) - **When** the user logs in - **And** no insight has been generated today - **Then** the system triggers background analysis - **And** calculates cosine similarity between all note pairs - **And** finds the top pair with similarity > 0.75 - **And** stores the insight in `MemoryEchoInsight` table - **And** UI freeze is < 100ms (only DB check, background processing) **Technical Requirements:** - Server Action: `app/actions/ai-memory-echo.ts` (NEW) - Service: `MemoryEchoService` in `lib/ai/services/memory-echo.service.ts` (NEW) - Trigger: User login check (in layout or dashboard) - Constraint: Max 1 insight per user per day (enforced via DB unique constraint) - Performance: < 100ms UI freeze (async processing) **Implementation:** ```typescript // app/actions/ai-memory-echo.ts 'use server' import { auth } from '@/auth' import { prisma } from '@/lib/prisma' import { MemoryEchoService } from '@/lib/ai/services/memory-echo.service' export async function generateMemoryEcho() { const session = await auth() if (!session?.user?.id) { return { success: false, error: 'Unauthorized' } } // Check if already generated today const today = new Date() today.setHours(0, 0, 0, 0) const existing = await prisma.memoryEchoInsight.findFirst({ where: { userId: session.user.id, insightDate: { gte: today } } }) if (existing) { return { success: true, insight: existing, alreadyGenerated: true } } // Generate new insight (non-blocking background task) generateInBackground(session.user.id) // Return immediately (UI doesn't wait) return { success: true, insight: null, alreadyGenerated: false } } async function generateInBackground(userId: string) { const service = new MemoryEchoService() try { const insight = await service.findTopConnection(userId) if (insight) { await prisma.memoryEchoInsight.create({ data: { userId, note1Id: insight.note1Id, note2Id: insight.note2Id, similarityScore: insight.score } }) } } catch (error) { console.error('Memory Echo background generation error:', error) } } ``` **Service Implementation:** ```typescript // lib/ai/services/memory-echo.service.ts export class MemoryEchoService { async findTopConnection( userId: string ): Promise<{ note1Id: string, note2Id: string, score: number } | null> { // Fetch all user notes with embeddings const notes = await prisma.note.findMany({ where: { userId }, select: { id: true, embedding: true, title: true, content: true } }) if (notes.length < 2) return null // Calculate pairwise cosine similarities const insights = [] const threshold = 0.75 for (let i = 0; i < notes.length; i++) { for (let j = i + 1; j < notes.length; j++) { const embedding1 = JSON.parse(notes[i].embedding) const embedding2 = JSON.parse(notes[j].embedding) const similarity = cosineSimilarity(embedding1, embedding2) if (similarity > threshold) { insights.push({ note1Id: notes[i].id, note2Id: notes[j].id, score: similarity }) } } } // Return top insight (highest similarity) if (insights.length === 0) return null insights.sort((a, b) => b.score - a.score) return insights[0] } } // Cosine similarity utility function cosineSimilarity(vecA: number[], vecB: number[]): number { const dotProduct = vecA.reduce((sum, a, i) => sum + a * vecB[i], 0) const magnitudeA = Math.sqrt(vecA.reduce((sum, a) => sum + a * a, 0)) const magnitudeB = Math.sqrt(vecB.reduce((sum, b) => sum + b * b, 0)) return dotProduct / (magnitudeA * magnitudeB) } ``` --- ### Story 4.2: Insight Display & Feedback **As a user, I want to see daily note connections and provide feedback, so that I can discover relationships in my knowledge base.** **Acceptance Criteria:** - **Given** a stored Memory Echo insight - **When** I log in (or navigate to dashboard) - **Then** a toast notification appears: "💡 Memory Echo: Note X relates to Note Y (85% match)" - **When** I click the toast - **Then** a modal displays both notes side-by-side - **And** I can click each note to view it in editor - **And** I can provide feedback via 👍 / 👎 buttons - **When** I click feedback - **Then** the feedback is stored in `MemoryEchoInsight.feedback` field **Technical Requirements:** - Component: `components/ai/memory-echo-notification.tsx` (NEW) - Trigger: Check on page load (dashboard layout) - UI: Toast notification with Sonner - Modal: Side-by-side note comparison - Feedback: Updates `MemoryEchoInsight.feedback` field **UI Implementation:** ```typescript // components/ai/memory-echo-notification.tsx 'use client' import { useEffect, useState } from 'react' import { useRouter } from 'next/navigation' import { toast } from 'sonner' import { Bell, X, ThumbsUp, ThumbsDown } from 'lucide-react' import { generateMemoryEcho } from '@/app/actions/ai-memory-echo' export function MemoryEchoNotification() { const router = useRouter() const [insight, setInsight] = useState(null) const [viewed, setViewed] = useState(false) useEffect(() => { checkForInsight() }, []) const checkForInsight = async () => { const result = await generateMemoryEcho() if (result.success && result.insight && !result.alreadyGenerated) { // Show toast notification toast('💡 Memory Echo', { description: `Note "${insight.note1.title}" relates to "${insight.note2.title}" (${Math.round(insight.similarityScore * 100)}% match)`, action: { label: 'View', onClick: () => showInsightModal(result.insight) } }) } if (result.success && result.insight) { setInsight(result.insight) } } const showInsightModal = (insightData: any) => { // Open modal with both notes side-by-side setViewed(true) markAsViewed(insightData.id) } const handleFeedback = async (feedback: 'thumbs_up' | 'thumbs_down') => { await updateMemoryEchoFeedback(insight.id, feedback) toast(feedback === 'thumbs_up' ? 'Thanks for your feedback!' : 'We\'ll improve next time') // Close modal or hide toast } if (!insight) return null return ( // Modal implementation with feedback buttons Memory Echo Discovery
{/* Note 1 */} router.push(`/notes/${insight.note1.id}`)} /> {/* Note 2 */} router.push(`/notes/${insight.note2.id}`)} />
Similarity: {Math.round(insight.similarityScore * 100)}%
) } ``` **Server Action for Feedback:** ```typescript // app/actions/ai-memory-echo.ts export async function updateMemoryEchoFeedback( insightId: string, feedback: 'thumbs_up' | 'thumbs_down' ) { const session = await auth() if (!session?.user?.id) throw new Error('Unauthorized') await prisma.memoryEchoInsight.update({ where: { id: insightId }, data: { feedback } }) return { success: true } } ``` **Database Schema (from Architecture Decision 2):** ```prisma model MemoryEchoInsight { id String @id @default(cuid()) userId String? note1Id String note2Id String similarityScore Float insightDate DateTime @default(now()) viewed Boolean @default(false) feedback String? note1 Note @relation("EchoNote1", fields: [note1Id], references: [id]) note2 Note @relation("EchoNote2", fields: [note2Id], references: [id]) user User? @relation(fields: [userId], references: [id]) @@unique([userId, insightDate]) @@index([userId, insightDate]) } ``` --- ## Epic 5: AI Settings Panel ### Overview Dedicated settings page at `/settings/ai` with granular ON/OFF controls for each AI feature and provider selection. **User Stories:** 2 **Estimated Complexity:** Medium **Dependencies:** Decision 4 (UserAISettings table), AI Provider Factory ### Story 5.1: Granular Feature Toggles **As a user, I want to enable/disable individual AI features, so that I can control which AI assistance I receive.** **Acceptance Criteria:** - **Given** the AI Settings page at `/settings/ai` - **When** I navigate to the page - **Then** I see toggles for each AI feature: - Title Suggestions (default: ON) - Semantic Search (default: ON) - Paragraph Reformulation (default: ON) - Memory Echo (default: ON) - **When** I toggle any feature OFF - **Then** the setting is saved to `UserAISettings` table - **And** the feature is immediately disabled in the UI - **When** I toggle any feature ON - **Then** the feature is re-enabled immediately **Technical Requirements:** - Page: `app/(main)/settings/ai/page.tsx` (NEW) - Component: `components/ai/ai-settings-panel.tsx` (NEW) - Server Action: `app/actions/ai-settings.ts` (NEW) - Database: `UserAISettings` table (from Decision 4) **UI Implementation:** ```typescript // app/(main)/settings/ai/page.tsx import { AISettingsPanel } from '@/components/ai/ai-settings-panel' import { getAISettings } from '@/lib/ai/settings' export default async function AISettingsPage() { const settings = await getAISettings() return (

AI Settings

) } // components/ai/ai-settings-panel.tsx 'use client' import { useState } from 'react' import { Switch } from '@/components/ui/switch' import { Label } from '@/components/ui/label' import { Card } from '@/components/ui/card' import { updateAISettings } from '@/app/actions/ai-settings' export function AISettingsPanel({ initialSettings }: { initialSettings: any }) { const [settings, setSettings] = useState(initialSettings) const handleToggle = async (feature: string, value: boolean) => { // Optimistic update setSettings(prev => ({ ...prev, [feature]: value })) // Server update await updateAISettings({ [feature]: value }) } return (
handleToggle('titleSuggestions', checked)} /> handleToggle('semanticSearch', checked)} /> handleToggle('paragraphRefactor', checked)} /> handleToggle('memoryEcho', checked)} /> {settings.memoryEcho && ( handleToggle('memoryEchoFrequency', value)} options={['daily', 'weekly', 'custom']} /> )}
) } function FeatureToggle({ name, label, description, checked, onChange }: { name: string label: string description: string checked: boolean onChange: (checked: boolean) => void }) { return (

{description}

) } ``` **Server Action:** ```typescript // app/actions/ai-settings.ts 'use server' import { auth } from '@/auth' import { prisma } from '@/lib/prisma' export async function updateAISettings(settings: Partial) { const session = await auth() if (!session?.user?.id) throw new Error('Unauthorized') // Upsert settings (create if not exists) await prisma.userAISettings.upsert({ where: { userId: session.user.id }, create: { userId: session.user.id, ...settings }, update: settings }) revalidatePath('/settings/ai') return { success: true } } export async function getAISettings() { const session = await auth() if (!session?.user?.id) { // Return defaults for non-logged-in users return { titleSuggestions: true, semanticSearch: true, paragraphRefactor: true, memoryEcho: true, memoryEchoFrequency: 'daily', aiProvider: 'auto' } } const settings = await prisma.userAISettings.findUnique({ where: { userId: session.user.id } }) return settings || { titleSuggestions: true, semanticSearch: true, paragraphRefactor: true, memoryEcho: true, memoryEchoFrequency: 'daily', aiProvider: 'auto' } } ``` --- ### Story 5.2: AI Provider Selection **As a user, I want to choose my AI provider (Auto, OpenAI, or Ollama), so that I can control cost and privacy.** **Acceptance Criteria:** - **Given** the AI Settings page - **When** I scroll to the "AI Provider" section - **Then** I see 3 provider options: - **Auto (Recommended)** - Ollama when available, OpenAI fallback - **Ollama (Local)** - 100% private, runs locally - **OpenAI (Cloud)** - Most accurate, requires API key - **When** I select a provider - **Then** the selection is saved to `UserAISettings.aiProvider` - **And** the AI provider factory uses my preference **Technical Requirements:** - Component: Extends `AISettingsPanel` with provider selector - Integration: `getAIProvider()` factory respects user selection - Validation: API key required for OpenAI (stored in SystemConfig) **UI Implementation:** ```typescript // components/ai/ai-settings-panel.tsx (extend existing component) function ProviderSelector({ value, onChange }: { value: 'auto' | 'openai' | 'ollama' onChange: (value: 'auto' | 'openai' | 'ollama') => void }) { const providers = [ { value: 'auto', label: 'Auto (Recommended)', description: 'Ollama when available, OpenAI fallback' }, { value: 'ollama', label: 'Ollama (Local)', description: '100% private, runs locally on your machine' }, { value: 'openai', label: 'OpenAI (Cloud)', description: 'Most accurate, requires API key' } ] return ( {providers.map(provider => (

{provider.description}

))}
{value === 'openai' && ( )}
) } ``` **Provider Factory Integration:** ```typescript // lib/ai/factory.ts (existing, extend to respect user settings) import { getAIProvider } from './factory' import { getAISettings } from './settings' export async function getUserAIProvider(): Promise { const userSettings = await getAISettings() const systemConfig = await getSystemConfig() let provider = userSettings.aiProvider // 'auto' | 'openai' | 'ollama' // Handle 'auto' mode if (provider === 'auto') { // Check if Ollama is available try { const ollamaStatus = await checkOllamaHealth() provider = ollamaStatus ? 'ollama' : 'openai' } catch { provider = 'openai' // Fallback to OpenAI } } return getAIProvider(provider) } ``` **Database Schema (from Decision 4):** ```prisma model UserAISettings { userId String @id // Feature Flags (granular ON/OFF) titleSuggestions Boolean @default(true) semanticSearch Boolean @default(true) paragraphRefactor Boolean @default(true) memoryEcho Boolean @default(true) // Configuration memoryEchoFrequency String @default("daily") // 'daily' | 'weekly' | 'custom' aiProvider String @default("auto") // 'auto' | 'openai' | 'ollama' // Relation user User @relation(fields: [userId], references: [id]) // Indexes for analytics @@index([memoryEcho]) @@index([aiProvider]) @@index([memoryEchoFrequency]) } ``` --- ## Epic 6: Language Detection Service ### Overview Automatic language detection using TinyLD (62 languages including Persian). Hybrid approach: TinyLD for < 50 words, AI for ≥ 50 words. **User Stories:** 2 **Estimated Complexity:** Medium **Dependencies:** Decision 3 (Language Detection Strategy), TinyLD library ### Story 6.1: TinyLD Integration for Short Notes **As a system, I want to detect note language efficiently for notes < 50 words using TinyLD, so that I can enable multilingual AI processing.** **Acceptance Criteria:** - **Given** a note with < 50 words - **When** the note is saved or analyzed - **Then** the system detects language using TinyLD - **And** detection completes in < 10ms - **And** the detected language is stored in `Note.language` field - **And** confidence score is stored in `Note.languageConfidence` field **Technical Requirements:** - Library: `tinyld` (npm install tinyld) - Service: `LanguageDetectionService` in `lib/ai/services/language-detection.service.ts` - Supported Languages: 62 (including Persian/fa verified) - Output Format: ISO 639-1 codes (fr, en, es, de, fa, etc.) **Implementation:** ```typescript // lib/ai/services/language-detection.service.ts import { tinyld } from 'tinyld' export class LanguageDetectionService { private readonly MIN_WORDS_FOR_AI = 50 private readonly MIN_CONFIDENCE = 0.7 async detectLanguage(content: string): Promise<{ language: string // 'fr' | 'en' | 'es' | 'de' | 'fa' | 'unknown' confidence: number // 0.0-1.0 method: 'tinyld' | 'ai' | 'manual' }> { const wordCount = content.split(/\s+/).length // Short notes: TinyLD (fast, TypeScript native) if (wordCount < this.MIN_WORDS_FOR_AI) { const result = tinyld(content) return { language: this.mapToISO(result.language), confidence: result.confidence || 0.8, method: 'tinyld' } } // Long notes: AI for better accuracy const response = await generateText({ model: openai('gpt-4o-mini'), // or ollama/llama3.2 prompt: `Detect the language of this text. Respond ONLY with ISO 639-1 code (fr, en, es, de, fa):\n\n${content.substring(0, 500)}` }) return { language: response.text.toLowerCase().trim(), confidence: 0.9, method: 'ai' } } private mapToISO(code: string): string { const mapping = { 'fra': 'fr', 'eng': 'en', 'spa': 'es', 'deu': 'de', 'fas': 'fa', 'pes': 'fa', // Persian (Farsi) 'por': 'pt', 'ita': 'it', 'rus': 'ru', 'zho': 'zh' } return mapping[code] || code.substring(0, 2) } } ``` **Trigger Points:** 1. Note creation (on save) 2. Note update (on save) 3. Before AI processing (title generation, reformulation, etc.) **Database Update:** ```typescript // app/actions/notes.ts (extend existing createNote/updateNote) export async function createNote(data: { title: string, content: string }) { const session = await auth() if (!session?.user?.id) throw new Error('Unauthorized') // Detect language const languageService = new LanguageDetectionService() const { language, languageConfidence } = await languageService.detectLanguage(data.content) const note = await prisma.note.create({ data: { ...data, userId: session.user.id, language, languageConfidence } }) return note } ``` --- ### Story 6.2: AI Fallback for Long Notes **As a system, I want to use AI language detection for notes ≥ 50 words, so that I can achieve higher accuracy for longer content.** **Acceptance Criteria:** - **Given** a note with ≥ 50 words - **When** the note is saved or analyzed - **Then** the system detects language using AI (OpenAI or Ollama) - **And** detection completes in < 500ms - **And** the detected language is stored in `Note.language` field - **And** confidence score is 0.9 (AI is more accurate) **Technical Requirements:** - Provider: Uses `getAIProvider()` factory - Model: `gpt-4o-mini` (OpenAI) or `llama3.2` (Ollama) - Prompt: Minimal (only language detection) - Output: ISO 639-1 code only **AI Prompt (from Story 6.1):** ``` Detect the language of this text. Respond ONLY with ISO 639-1 code (fr, en, es, de, fa): {content (first 500 chars)} ``` **Performance Target:** - TinyLD detection: ~8ms for < 50 words ✅ - AI detection: ~200-500ms for ≥ 50 words ✅ - Overall impact: Negligible for UX --- ## Implementation Phases ### Phase 1: Foundation (Week 1-2) **Goal:** Database schema and base infrastructure **Stories:** - Epic 1-6: All Prisma migrations (3 new tables, extend Note model) - Epic 6: Language Detection Service (TinyLD integration) - Epic 5: AI Settings page + UserAISettings table **Deliverables:** - ✅ Prisma migrations created and applied - ✅ `LanguageDetectionService` implemented - ✅ `/settings/ai` page functional - ✅ Base AI service layer structure created --- ### Phase 2: Infrastructure (Week 3-4) **Goal:** Core services and AI provider integration **Stories:** - Epic 1: Title Suggestion Service - Epic 2: Semantic Search Service (part 1 - embeddings) - Epic 3: Paragraph Refactor Service - Epic 4: Memory Echo Service (part 1 - background job) **Deliverables:** - ✅ All AI services implemented - ✅ Provider factory extended for new services - ✅ Server actions created for all features - ✅ Integration tests passing --- ### Phase 3: AI Features (Week 5-9) **Goal:** UI components and user-facing features **Stories:** - Epic 1: Title Suggestions UI (Stories 1.1, 1.2, 1.3) - Epic 2: Semantic Search UI (Stories 2.1, 2.2, 2.3) - Epic 3: Paragraph Reformulation UI (Stories 3.1, 3.2) - Epic 4: Memory Echo UI (Stories 4.1, 4.2) **Deliverables:** - ✅ All AI components implemented - ✅ Toast notifications working - ✅ Modals and dialogs functional - ✅ Feedback collection active --- ### Phase 4: Polish & Testing (Week 10-12) **Goal:** Quality assurance and performance optimization **Stories:** - Epic 1-6: E2E Playwright tests - Epic 1-6: Performance testing and optimization - Epic 1-6: Multi-language testing (FR, EN, ES, DE, FA) - Epic 1-6: Bug fixes and refinement **Deliverables:** - ✅ E2E test coverage for all AI features - ✅ Performance targets met (search < 300ms, titles < 2s, Memory Echo < 100ms UI freeze) - ✅ Multi-language verification complete - ✅ Production deployment ready --- ## Dependencies & Critical Path ### Critical Path Implementation ``` Prisma Migrations → Language Detection Service → AI Settings Page ↓ All AI Services ↓ UI Components ↓ Testing & Polish ``` ### Parallel Development Opportunities - **Week 1-2:** Language Detection + AI Settings (independent) - **Week 3-4:** All AI services (can be developed in parallel) - **Week 5-9:** UI components (can be developed in parallel per epic) - **Week 10-12:** Testing (all features tested together) ### Cross-Epic Dependencies - **All Epics → Epic 6 (Language Detection):** Must detect language before AI processing - **All Epics → Epic 5 (AI Settings):** Must check feature flags before executing - **Epic 2 (Semantic Search) → Existing Embeddings:** Reuses `Note.embedding` field - **Epic 4 (Memory Echo) → Epic 2 (Semantic Search):** Uses cosine similarity from Epic 2 --- ## Definition of Done ### Per Story - [ ] Code implemented following `project-context.md` rules - [ ] TypeScript strict mode compliance - [ ] Server actions have `'use server'` directive - [ ] Components have `'use client'` directive (if interactive) - [ ] All imports use `@/` alias - [ ] Error handling with `try/catch` and `console.error()` - [ ] API responses follow `{success, data, error}` format - [ ] `auth()` check in all server actions - [ ] `revalidatePath('/')` after mutations - [ ] E2E Playwright test written - [ ] Manual testing completed ### Per Epic - [ ] All stories completed - [ ] Integration tests passing - [ ] Performance targets met - [ ] User acceptance criteria validated - [ ] Documentation updated ### Phase 1 MVP AI - [ ] All 6 epics completed - [ ] Zero breaking changes to existing features - [ ] All NFRs met (performance, security, privacy) - [ ] Multi-language verified (FR, EN, ES, DE, FA) - [ ] Production deployment ready - [ ] User feedback collected and analyzed --- *Generated: 2026-01-10* *Author: Winston (Architect Agent) with Create Epics & Stories workflow* *Based on: PRD Phase 1 MVP AI + UX Design Spec + Architecture (2784 lines)* *Status: READY FOR IMPLEMENTATION*