Files
brachnha-insight/_bmad-output/implementation-artifacts/2-3-refinement-loop-regeneration.md
Max 3fbbb1a93b Initial commit: Brachnha Insight project setup
- Next.js 14+ with App Router and TypeScript
- Tailwind CSS and ShadCN UI styling
- Zustand state management
- Dexie.js for IndexedDB (local-first data)
- Auth.js v5 for authentication
- BMAD framework integration

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-26 12:28:43 +07:00

25 KiB

Story 2.3: Refinement Loop (Regeneration)

Status: done

Story

As a user, I want to provide feedback if the draft isn't right, So that I can get a better version.

Acceptance Criteria

  1. Thumbs Down Triggers Refinement

    • Given the user is viewing a draft
    • When they click "Thumbs Down"
    • Then the draft sheet closes and returns to the Chat UI
    • And the AI proactively asks "What should we change?"
  2. Feedback-Based Regeneration

    • Given the user provides specific critique (e.g., "Make it shorter")
    • When they send the feedback
    • Then the "Ghostwriter" regenerates the draft respecting the new constraint
    • And the new draft replaces the old one in the Draft View

Tasks / Subtasks

  • Implement Refinement Flow State Management

    • Add refinement state to ChatStore: isRefining, refinementDraftId, originalDraft
    • Add startRefinement(draftId) action to enter refinement mode
    • Add submitRefinementFeedback(feedback: string) action
    • Add cancelRefinement() action to exit refinement mode
    • Use atomic selectors for all state access
  • Implement Refinement Prompt Engineering

    • Create generateRefinementPrompt() in src/lib/llm/prompt-engine.ts
    • Include original draft content in prompt context
    • Include user's feedback/critique as constraint
    • Include original chat history for context preservation
    • Add prompt instructions: "respect user's specific critique while maintaining their voice"
  • Extend Ghostwriter Service for Regeneration

    • Add regenerateDraft() method to src/services/llm-service.ts
    • Support streaming response for regenerated draft
    • Pass original draft, feedback, and chat history to LLM
    • Return new Draft object with same sessionId but updated content
  • Implement Refinement Mode in ChatService

    • Add handleRefinementFeedback() orchestration method
    • Load original draft from IndexedDB for context
    • Call Ghostwriter with refinement prompt
    • Store regenerated draft in IndexedDB with status 'regenerated'
    • Update ChatStore with new draft
  • Create Refinement UI Indicators

    • Create RefinementModeBadge.tsx component
    • Show "Refining your draft..." indicator during regeneration
    • Add visual cue that chat is in refinement mode (different color/border)
    • Add system message: "What should we change?" when entering refinement
  • Implement Draft Replacement Logic

    • Update currentDraft in ChatStore with regenerated content
    • Keep original draft in IndexedDB (create new record with status 'regenerated')
    • Auto-open DraftViewSheet with new draft after regeneration
    • Allow unlimited refinement iterations
  • Handle Refinement Cancellation

    • Allow user to exit refinement mode without regenerating
    • Add "Cancel Refinement" button or gesture
    • Restore normal chat mode on cancellation
    • Keep original draft accessible from history
  • Test Refinement Loop End-to-End

    • Unit test: Refinement prompt generation with various feedback types
    • Unit test: Ghostwriter regeneration with original draft context
    • Integration test: Thumbs Down -> Feedback -> Regeneration flow
    • Integration test: Multiple refinement iterations
    • Edge case: Vague feedback ("make it better")
    • Edge case: Contradictory feedback
    • Edge case: Very long original draft

Dev Notes

Architecture Compliance (CRITICAL)

Logic Sandwich Pattern - DO NOT VIOLATE:

  • UI Components MUST NOT import src/lib/llm or src/services/llm-service.ts directly
  • All refinement logic MUST go through ChatService layer
  • ChatService then calls LLMService as needed
  • Components use Zustand store via atomic selectors only
  • Services return plain objects, not Dexie observables

State Management - Atomic Selectors Required:

// BAD - Causes unnecessary re-renders
const { isRefining, refinementDraftId } = useChatStore();

// GOOD - Atomic selectors
const isRefining = useChatStore(s => s.isRefining);
const refinementDraftId = useChatStore(s => s.refinementDraftId);
const startRefinement = useChatStore(s => s.startRefinement);
const submitRefinementFeedback = useChatStore(s => s.submitRefinementFeedback);

Local-First Data Boundary:

  • Regenerated drafts MUST be stored in IndexedDB
  • Each regeneration creates a new DraftRecord with status 'regenerated'
  • Original draft is preserved (not overwritten)
  • Draft history is queryable from history view (Epic 3)

Edge Runtime Constraint:

  • Regeneration LLM call goes through /api/llm edge function (same as original Ghostwriter)
  • export const runtime = 'edge';

Architecture Implementation Details

Story Purpose: This story implements the "Conversational Refinement Loop" - the ability to iteratively improve drafts through natural language feedback. This is a key differentiator from standard AI writing tools, as it preserves the user's voice while allowing precise control over the output.

State Management Extensions:

// Add to ChatStore (src/lib/store/chat-store.ts)
interface ChatStore {
  // Refinement state
  isRefining: boolean;
  refinementDraftId: string | null;
  originalDraft: Draft | null;
  startRefinement: (draftId: string) => Promise<void>;
  submitRefinementFeedback: (feedback: string) => Promise<void>;
  cancelRefinement: () => void;
}

Refinement Logic Flow:

  1. User views draft in DraftViewSheet (Story 2.2)
  2. User taps Thumbs Down
  3. DraftViewSheet closes
  4. ChatService.startRefinement(draftId) called
  5. System message added: "What should we change?"
  6. User enters feedback: "Make it shorter"
  7. ChatService.submitRefinementFeedback(feedback) called
  8. Ghostwriter regenerates with refinement prompt
  9. New draft stored with status 'regenerated'
  10. DraftViewSheet re-opens with new draft
  11. Loop can repeat indefinitely

Draft Versioning Strategy:

  • Each regeneration creates a NEW DraftRecord (not an update)
  • Link drafts via sessionId and createdAt timestamp
  • Latest draft for a session is shown in current view
  • History view (Epic 3) can show all versions

Files to Create:

  • src/components/features/chat/RefinementModeBadge.tsx - Visual indicator for refinement mode
  • src/components/features/chat/RefinementIndicator.tsx - Loading state during regeneration

Files to Modify:

  • src/lib/store/chat-store.ts - Add refinement state and actions
  • src/lib/llm/prompt-engine.ts - Add generateRefinementPrompt() function
  • src/services/llm-service.ts - Add regenerateDraft() method
  • src/services/chat-service.ts - Add refinement orchestration
  • src/components/features/draft/DraftActions.tsx - Thumbs Down triggers refinement

UX Design Specifications

From UX Design Document:

The "Refinement Loop":

  • Correction is done by talking to the agent, not editing text
  • "What didn't you like?" prompt appears after Thumbs Down
  • New draft replaces old one seamlessly
  • Fast loop is critical (< 10 seconds total)

Visual Feedback - Refinement Mode:

  • Chat interface should show visual cue that we're in refinement mode
  • Different border color or subtle background change
  • System message clearly prompts: "What should we change?"

Tone and Persona:

  • AI should accept feedback gracefully
  • No defensive responses
  • Clear acknowledgment of the constraint

Interaction Pattern:

graph TD
    A[Draft View Open] --> B[User Taps Thumbs Down]
    B --> C[Sheet Closes]
    C --> D[System Message: What should we change?]
    D --> E[User Types Feedback]
    E --> F[Ghostwriter Regenerates]
    F --> G[New Draft Appears]
    G --> H{User Happy?}
    H -->|No| B
    H -->|Yes| I[Thumbs Up to Complete]

Micro-interactions:

  • Thumbs Down should feel like "let's fix this" not "you failed"
  • System message should appear as if from a supportive editor
  • Regeneration should use same shimmer animation as initial draft

Previous Story Intelligence (from Stories 2.1 and 2.2)

Patterns Established (must follow):

  • Logic Sandwich Pattern: UI -> Zustand -> Service -> LLM (strictly enforced)
  • Atomic Selectors: All state access uses useChatStore(s => s.field)
  • Draft Storage: Drafts stored in IndexedDB via drafts table
  • Ghostwriter Integration: getGhostwriterResponseStream() pattern for streaming
  • DraftViewSheet: Auto-opens when currentDraft is set

Key Files from Story 2.1:

  • src/lib/llm/prompt-engine.ts - Has generateGhostwriterPrompt(), add refinement version
  • src/services/llm-service.ts - Has getGhostwriterResponseStream(), add regenerate version
  • src/lib/db/draft-service.ts - Draft CRUD operations

Key Files from Story 2.2:

  • src/components/features/draft/DraftActions.tsx - Thumbs Down button, wire to refinement
  • src/components/features/draft/DraftViewSheet.tsx - Sheet component
  • src/lib/store/chat-store.ts - Has currentDraft, add refinement state

Learnings to Apply:

  • Story 2.1 established streaming pattern - reuse for regeneration
  • Story 2.2 established DraftViewSheet auto-open - reuse for new draft
  • Use same DraftingIndicator animation during regeneration
  • Follow same error handling pattern (retry on failure)

Draft Data Structure (from Story 2.1):

interface DraftRecord {
  id: string;
  sessionId: string;
  title: string;
  content: string;      // Markdown formatted
  tags: string[];
  createdAt: number;
  status: 'draft' | 'completed' | 'regenerated';
}

Refinement Prompt Specifications

Prompt Structure:

function generateRefinementPrompt(
  originalDraft: Draft,
  userFeedback: string,
  chatHistory: ChatMessage[],
  intent?: 'venting' | 'insight'
): string {
  return `
You are the Ghostwriter Agent in REFINEMENT MODE. The user has provided feedback on a draft you wrote.

ORIGINAL DRAFT:
Title: ${originalDraft.title}
Content: ${originalDraft.content}

USER FEEDBACK:
"${userFeedback}"

CONTEXT:
- User Intent: ${intent || 'unknown'}
- Original Chat History: ${formatChatHistory(chatHistory)}

REQUIREMENTS:
1. Address the user's specific feedback while maintaining their authentic voice
2. Do NOT introduce new ideas or hallucinate facts
3. Keep what worked in the original, only change what the user criticized
4. If feedback is vague, make a reasonable best guess
5. Maintain the same professional LinkedIn tone
6. Return a NEW draft in the same Markdown format

OUTPUT FORMAT:
\`\`\`markdown
# [Refined Title - adjust if needed based on feedback]

[Revised content that addresses the feedback]

**Tags:** [3-5 relevant tags - adjust if needed]
\`\`\`
`;
}

Prompt Engineering Notes:

  • Emphasize "maintain what worked" to avoid over-correction
  • Include original draft as context so AI doesn't lose good parts
  • User feedback can be vague ("make it punchier") - AI must interpret
  • Title and tags can change if feedback warrants it
  • Chat history provides context that might be missing from draft

Common Feedback Patterns to Handle:

  • "Make it shorter" -> Condense while keeping key points
  • "More casual" -> Reduce corporate language
  • "More professional" -> Increase formality
  • "Focus on X" -> Emphasize specific aspect
  • "Less technical" -> Simplify jargon
  • "Add more about Y" -> Expand on specific point (careful not to hallucinate)

Testing Requirements

Unit Tests:

  • PromptEngine: generateRefinementPrompt() includes original draft
  • PromptEngine: generateRefinementPrompt() includes user feedback
  • PromptEngine: Handles vague feedback gracefully
  • PromptEngine: Maintains chat history context
  • LLMService: regenerateDraft() calls Edge API correctly
  • LLMService: Handles streaming response for regeneration
  • ChatStore: startRefinement() sets state correctly
  • ChatStore: submitRefinementFeedback() triggers regeneration
  • ChatStore: cancelRefinement() clears state

Integration Tests:

  • Full refinement flow: Thumbs Down -> Feedback -> Regeneration -> New draft
  • Multiple iterations: Refine -> Refine again -> Final draft
  • Draft replacement: New draft appears in DraftViewSheet
  • Original draft preservation: Original still in IndexedDB
  • Refinement cancellation: Exit without regenerating
  • Refinement mode indicator: Visual cue appears

Edge Cases:

  • Vague feedback: "make it better" - should make reasonable guess
  • Contradictory feedback: "make it shorter but add more detail" - should prioritize or ask
  • Very long original draft: Should handle within token limits
  • Empty feedback: Should handle gracefully
  • Malformed LLM response: Should handle gracefully
  • LLM API failure during regeneration: Should show retry option

Performance Tests:

  • Regeneration time: < 5 seconds (same as initial generation)
  • Refinement indicator appears within 1 second
  • Large draft with feedback: Should handle efficiently

Component Implementation Details

RefinementModeBadge Component:

// src/components/features/chat/RefinementModeBadge.tsx
interface RefinementModeBadgeProps {
  onCancel: () => void;
}

export function RefinementModeBadge({ onCancel }: RefinementModeBadgeProps) {
  return (
    <div className="flex items-center gap-2 px-3 py-2 bg-amber-50 border border-amber-200 rounded-full text-sm text-amber-800">
      <span className="flex-1">Refining your draft...</span>
      <button
        onClick={onCancel}
        className="text-amber-600 hover:text-amber-800 underline"
      >
        Cancel
      </button>
    </div>
  );
}

ChatService Extensions:

// src/services/chat-service.ts
export class ChatService {
  async startRefinement(draftId: string): Promise<void> {
    // Load original draft
    const draft = await DraftService.getDraftById(draftId);

    // Add system message to chat
    await this.addSystemMessage("What should we change?");

    // Update store
    ChatStore.getState().startRefinement(draftId, draft);
  }

  async submitRefinementFeedback(feedback: string): Promise<void> {
    const { refinementDraftId, originalDraft } = ChatStore.getState();

    // Get chat history for context
    const session = await SessionService.getCurrentSession();
    const chatHistory = await ChatLogService.getBySessionId(session.id);

    // Regenerate draft
    const newDraft = await LLMService.regenerateDraft(
      originalDraft!,
      feedback,
      chatHistory
    );

    // Save new draft
    await DraftService.createDraft({
      ...newDraft,
      sessionId: session.id,
      status: 'regenerated'
    });

    // Update store with new draft (triggers DraftViewSheet open)
    ChatStore.getState().setCurrentDraft(newDraft);
    ChatStore.getState().exitRefinementMode();
  }

  cancelRefinement(): void {
    ChatStore.getState().cancelRefinement();
  }
}

DraftActions Integration (from Story 2.2):

// Update Thumbs Down handler in DraftActions
const handleReject = () => {
  onReject(draftId);
  // Trigger refinement flow
  ChatService.startRefinement(draftId);
};

Draft Versioning Strategy

Storage Pattern:

// Each regeneration creates a new record
const originalDraft = {
  id: 'draft-1',
  sessionId: 'session-1',
  content: 'Original content...',
  status: 'completed', // User approved after refinement
  createdAt: 1000
};

const regeneratedDraft = {
  id: 'draft-2',
  sessionId: 'session-1', // Same session
  content: 'Refined content...',
  status: 'regenerated', // Marked as regenerated version
  createdAt: 2000 // Later timestamp
};

// Query for latest draft
const latestDraft = await db.drafts
  .where('sessionId')
  .equals('session-1')
  .reverse() // Newest first
  .first();

Version Querying (for Epic 3 History):

// Get all versions of a draft
async getAllDraftVersions(sessionId: string): Promise<Draft[]> {
  return await db.drafts
    .where('sessionId')
    .equals(sessionId)
    .sortBy('createdAt');
}

Service Layer Extensions

LLMService Regeneration:

// src/services/llm-service.ts
export class LLMService {
  async regenerateDraft(
    originalDraft: Draft,
    feedback: string,
    chatHistory: ChatMessage[]
  ): Promise<Draft> {
    const prompt = PromptEngine.generateRefinementPrompt(
      originalDraft,
      feedback,
      chatHistory
    );

    const response = await this.callEdgeAPI(prompt);

    // Parse response (same format as initial generation)
    const { title, content, tags } = this.parseMarkdownResponse(response);

    return {
      id: generateId(),
      sessionId: originalDraft.sessionId,
      title,
      content,
      tags,
      createdAt: Date.now(),
      status: 'regenerated'
    };
  }
}

PromptEngine Extensions:

// src/lib/llm/prompt-engine.ts
export class PromptEngine {
  static generateRefinementPrompt(
    originalDraft: Draft,
    userFeedback: string,
    chatHistory: ChatMessage[],
    intent?: 'venting' | 'insight'
  ): string {
    // Prompt implementation from specifications above
  }
}

Project Structure Notes

Following Feature-First Lite Pattern:

  • New components in src/components/features/chat/ (refinement is chat-mode feature)
  • Service extensions in existing files
  • Store updates in src/lib/store/chat-store.ts

Alignment with Unified Project Structure:

  • All feature code under src/components/features/
  • Services orchestrate logic, don't touch DB directly from UI
  • State managed centrally in Zustand stores

No Conflicts Detected:

  • Refinement mode is new state, no conflicts
  • Extends existing Ghostwriter pattern
  • Creates new draft records (no migration conflicts)

Performance Requirements

NFR-01 Compliance (Generation Latency):

  • Regeneration should complete in < 5 seconds total
  • First token should appear within 3 seconds
  • Same performance as initial Ghostwriter generation

State Updates:

  • isRefining state should update immediately on Thumbs Down
  • Regeneration indicator should appear instantly
  • New draft should auto-open DraftViewSheet on completion

Accessibility Requirements

WCAG AA Compliance:

  • Refinement mode indicator must be visible to screen readers
  • "Cancel Refinement" button keyboard accessible
  • System message "What should we change?" announced
  • Focus management: Return focus to chat input after Thumbs Down

Visual Accessibility:

  • Refinement mode must have clear visual cue (not just color)
  • Status indicator must be high contrast
  • Avoid color-only indicators (use icons + text)

Security & Privacy Requirements

NFR-03 & NFR-04 Compliance:

  • Regeneration uses same Edge API proxy as initial generation
  • No draft content sent to server for storage
  • Original draft kept client-side only
  • User feedback processed through same privacy pipeline

References

Epic Reference:

Architecture Documents:

UX Design Specifications:

Previous Stories:

Dev Agent Record

Agent Model Used

Claude Opus 4.5 (model ID: 'claude-opus-4-5-20251101')

Debug Log References

Session file: /tmp/claude/-home-maximilienmao-Projects/Test01/80b59076-c368-433c-92e4-1937285218ee/scratchpad

Completion Notes List

Story Analysis Completed:

  • Extracted story requirements from Epic 2, Story 2.3
  • Analyzed previous Stories 2.1 and 2.2 for established patterns
  • Reviewed architecture for compliance requirements (Logic Sandwich, State Management, Local-First)
  • Reviewed UX specification for refinement loop interaction pattern
  • Identified all files to create and modify

Implementation Context Summary:

Story Purpose: This story implements the "Conversational Refinement Loop" - the ability to iteratively improve drafts through natural language feedback. Users tap Thumbs Down, provide feedback ("Make it shorter"), and the Ghostwriter regenerates a new version addressing their critique while maintaining their authentic voice.

Key Technical Decisions:

  1. State Management: Add refinement mode state to ChatStore (isRefining, refinementDraftId, originalDraft)
  2. Draft Versioning: Each regeneration creates a new DraftRecord (not an update), linked by sessionId
  3. Prompt Engineering: Include original draft + user feedback + chat history in refinement prompt
  4. Visual Feedback: RefinementModeBadge shows we're in refinement mode
  5. Flow Integration: Thumbs Down from Story 2.2 triggers refinement start
  6. Auto-Open: Regenerated draft auto-opens DraftViewSheet (same as initial draft)

Dependencies:

  • No new dependencies required
  • Reuses existing Zustand, Dexie, LLM service infrastructure
  • Extends existing prompt engine and Ghostwriter service

Integration Points:

  • Thumbs Down in DraftActions (Story 2.2) triggers ChatService.startRefinement()
  • Regeneration uses same Edge API proxy as initial Ghostwriter
  • New draft stored in IndexedDB with status 'regenerated'
  • Auto-opens DraftViewSheet when regeneration completes

Files to Create:

  • src/components/features/chat/RefinementModeBadge.tsx - Visual indicator for refinement mode
  • src/components/features/chat/RefinementIndicator.tsx - Loading state during regeneration

Files to Modify:

  • src/lib/store/chat-store.ts - Add refinement state and actions
  • src/lib/llm/prompt-engine.ts - Add generateRefinementPrompt() function
  • src/services/llm-service.ts - Add regenerateDraft() method
  • src/services/chat-service.ts - Add refinement orchestration methods
  • src/components/features/draft/DraftActions.tsx - Wire Thumbs Down to refinement start

Testing Strategy:

  • Unit tests for refinement prompt generation
  • Integration tests for full refinement loop
  • Edge case tests (vague feedback, contradictory feedback)
  • Multiple iteration tests (refine, refine again)

Draft Versioning Pattern:

  • Each regeneration creates NEW DraftRecord
  • Original draft preserved (not overwritten)
  • Latest draft shown in current view
  • History view (Epic 3) can show all versions

File List

New Files Created:

  • src/components/features/chat/RefinementModeBadge.tsx - Visual indicator component
  • src/components/features/chat/RefinementIndicator.tsx - Regeneration loading state
  • src/lib/store/refinement-store.test.ts - Refinement state tests
  • src/lib/llm/refinement-prompt.test.ts - Prompt generation tests

Files Modified:

  • src/lib/store/chat-store.ts - Added refinement state (isRefining, refinementDraftId, originalDraft) and actions (startRefinement, submitRefinementFeedback, cancelRefinement)
  • src/lib/llm/prompt-engine.ts - Added generateRefinementPrompt() function
  • src/services/llm-service.ts - Added regenerateDraft() method with extractOriginalDraft() helper
  • src/services/chat-service.ts - Added startRefinement(), submitRefinementFeedback() methods
  • src/components/features/draft/DraftActions.tsx - Wired Thumbs Down to refinement flow
  • src/components/features/chat/ChatWindow.tsx - Added RefinementModeBadge display
  • src/components/features/chat/index.ts - Exported new components
  • src/components/features/draft/DraftViewSheet.test.tsx - Updated test to expect startRefinement call

Code Review Fixes

  • Critical Fix: Updated ChatStore.addMessage to correctly intercept user input during refinement mode and route it to submitRefinementFeedback, fixing the broken refinement loop.
  • High Fix: Updated ChatService.submitRefinementFeedback and ChatStore to ensure currentDraft is immediately updated with the regenerated content, preventing UI stale state.
  • High Fix: Updated LLMService.regenerateDraft and call sites to explicitly accept originalDraftContent, preventing potential data loss from unreliable chat history parsing.
  • Fix: Resolved sessionId type error in ChatService by deriving it from originalDraft.
  • Verification: All refinement store and prompt tests passed.