Files
brachnha-insight/_bmad-output/implementation-artifacts/2-4-export-copy-actions.md
Max e9e6fadb1d fix: ChatBubble crash and DeepSeek API compatibility
- Fix ChatBubble to handle non-string content with String() wrapper
- Fix API route to use generateText for non-streaming requests
- Add @ai-sdk/openai-compatible for non-OpenAI providers (DeepSeek, etc.)
- Use Chat Completions API instead of Responses API for compatible providers
- Update ChatBubble tests and fix component exports to kebab-case
- Remove stale PascalCase ChatBubble.tsx file
2026-01-26 16:55:05 +07:00

27 KiB

Story 2.4: Export & Copy Actions

Status: review

Story

As a user, I want to copy the text or save the post, So that I can publish it on LinkedIn or save it for later.

Acceptance Criteria

  1. Copy to Clipboard on Thumbs Up

    • Given the user likes the draft
    • When they click "Thumbs Up" or "Copy"
    • Then the full Markdown text is copied to the clipboard
    • And a success toast/animation confirms the action
  2. Save Draft as Completed

    • Given the draft is finalized
    • When the user saves it
    • Then it is marked as "Completed" in the local database
    • And the user is returned to the Home/History screen

Tasks / Subtasks

  • Implement Clipboard Copy Functionality

    • Create copyToClipboard() utility in src/lib/utils/clipboard.ts
    • Support copying Markdown formatted text
    • Handle clipboard API errors gracefully
    • Add fallback for older browsers
  • Create Success Feedback Toast

    • Create CopySuccessToast.tsx component in src/components/features/feedback/
    • Show confetti or success animation on copy
    • Auto-dismiss after 3 seconds
    • Include haptic feedback on mobile devices
    • Accessible: announce to screen readers
  • Extend DraftActions with Copy/Save

    • Add "Copy" button variant (separate from Thumbs Up)
    • Wire Thumbs Up to both copy AND save actions
    • Add "Just Copy" option (copy without closing sheet)
    • Add "Save & Close" action
  • Implement Draft Completion Logic

    • Add completeDraft(draftId: number) action to ChatStore
    • Update draft status from 'draft' to 'completed' in IndexedDB
    • Clear currentDraft from store after completion
    • Close DraftViewSheet after completion
    • Navigate to Home/History screen (deferred to Epic 3)
  • Implement DraftService Completion Method

    • Add markAsCompleted(draftId: string) to src/lib/db/draft-service.ts
    • Update DraftRecord status in IndexedDB
    • Add completedAt timestamp
    • Return updated draft record
  • Create History Screen Navigation

    • Add navigation to Home/History after save
    • Ensure completed draft appears in history feed
    • Scroll to newly saved draft in history
    • Highlight the newly saved entry
  • Add Copy Accessibility Features

    • Add aria-label to all copy buttons
    • Announce "Copied to clipboard" to screen readers
    • Support keyboard shortcuts (Cmd+C / Ctrl+C) (deferred - enhancement)
    • Ensure focus management after copy action (deferred - enhancement)
  • Test Copy & Save End-to-End

    • Unit test: clipboard.copyText() with Markdown (12 tests)
    • Unit test: DraftService.markAsCompleted() updates status (3 tests)
    • Integration test: Thumbs Up -> Copy -> Save flow (DraftViewSheet tests)
    • Integration test: Copy button only (no save) (DraftViewSheet tests)
    • Edge case: Clipboard permission denied (clipboard fallback tests)
    • Edge case: Draft already marked as completed (DraftService tests)
    • Edge case: Copy with very long draft content (deferred - stress test)
  • Test History Integration

    • Integration test: Completed draft appears in history feed (deferred to Epic 3)
    • Integration test: Copy action from history view (Epic 3)
    • Test: Multiple drafts saved in session (deferred to Epic 3)

Dev Notes

Architecture Compliance (CRITICAL)

Logic Sandwich Pattern - DO NOT VIOLATE:

  • UI Components MUST NOT import src/lib/db or clipboard utilities directly
  • All completion logic MUST go through ChatService layer
  • ChatService then calls DraftService for database operations
  • 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 { currentDraft } = useChatStore();

// GOOD - Atomic selectors
const currentDraft = useChatStore(s => s.currentDraft);
const completeDraft = useChatStore(s => s.completeDraft);

Local-First Data Boundary:

  • Completed drafts MUST be stored in IndexedDB
  • Status change from 'draft' to 'completed' persists locally
  • Clipboard operations are client-side only (no server interaction)
  • Draft history is queryable from history view (Epic 3)

Edge Runtime Constraint:

  • This story does NOT require Edge API calls (clipboard is client-side only)
  • All operations happen in the browser for privacy and speed

Architecture Implementation Details

Issue Resolution (Review Fixes):

  • Logic Sandwich Violation: Fixed ChatStore.completeDraft to call ChatService.approveDraft instead of direct DB acton.
  • Missing Component: Created CopyButton.tsx and refactored DraftActions to use it.
  • Redundancy: Consolidated approveDraft and completeDraft in Store.

Story Purpose: This story implements the "Export & Completion" flow - the final step where users copy their polished draft to clipboard and save it to their local history. This completes the core value loop: Vent -> Generate -> Refine -> Export.

State Management Extensions:

// Add to ChatStore (src/lib/store/chat-store.ts)
interface ChatStore {
  // Existing state
  currentDraft: Draft | null;

  // New actions for this story
  completeDraft: (draftId: string) => Promise<void>;
  copyDraftToClipboard: (draftId: string) => Promise<void>;
  copyAndSaveDraft: (draftId: string) => Promise<void>;
}

Completion Logic Flow:

  1. User views draft in DraftViewSheet (Story 2.2)
  2. User taps Thumbs Up OR Copy button
  3. Thumbs Up path:
    • Copy Markdown to clipboard
    • Show success toast with confetti
    • Mark draft as 'completed' in IndexedDB
    • Clear currentDraft from store
    • Close DraftViewSheet
    • Navigate to Home/History
    • Scroll to/highlight newly saved entry
  4. Copy Only path:
    • Copy Markdown to clipboard
    • Show success toast
    • Keep sheet open (user can continue viewing)

Clipboard Utility Pattern:

// src/lib/utils/clipboard.ts
export class ClipboardUtil {
  static async copyMarkdown(markdown: string): Promise<boolean> {
    try {
      await navigator.clipboard.writeText(markdown);
      return true;
    } catch (error) {
      // Fallback for older browsers
      return this.fallbackCopy(markdown);
    }
  }

  private static fallbackCopy(text: string): boolean {
    const textarea = document.createElement('textarea');
    textarea.value = text;
    document.body.appendChild(textarea);
    textarea.select();
    const success = document.execCommand('copy');
    document.body.removeChild(textarea);
    return success;
  }
}

Draft Completion in Database:

// src/lib/db/draft-service.ts
export class DraftService {
  static async markAsCompleted(draftId: string): Promise<Draft> {
    await db.drafts.update(draftId, {
      status: 'completed',
      completedAt: Date.now()
    });
    return this.getDraftById(draftId);
  }
}

Files to Create:

  • src/lib/utils/clipboard.ts - Clipboard utility with fallback
  • src/components/features/feedback/CopySuccessToast.tsx - Success feedback
  • src/components/features/draft/CopyButton.tsx - Standalone copy button

Files to Modify:

  • src/lib/store/chat-store.ts - Add completion actions
  • src/lib/db/draft-service.ts - Add markAsCompleted() method
  • src/services/chat-service.ts - Add completion orchestration
  • src/components/features/draft/DraftActions.tsx - Add Copy/Save buttons
  • src/app/(main)/page.tsx - Navigate to history after save (optional scroll)

UX Design Specifications

From UX Design Document:

Completion Reward Pattern:

  • Thumbs Up triggers "You're done!" animation
  • Confetti or haptic feedback provides dopamine hit
  • Success toast confirms copy action
  • Auto-navigation to history reinforces "saved to your journal"

Visual Feedback - Success State:

graph TD
    A[User Taps Thumbs Up] --> B[Copy to Clipboard]
    B --> C[Show Success Toast]
    C --> D[Confetti Animation]
    D --> E[Mark as Completed]
    E --> F[Close Sheet]
    F --> G[Navigate to History]
    G --> H[Highlight New Entry]

Toast Design Specifications:

  • Position: Top-center or bottom-center (mobile)
  • Duration: 3 seconds auto-dismiss
  • Icon: Checkmark or clipboard icon
  • Text: "Copied to clipboard!" or "Draft saved!"
  • Animation: Fade in + slide up
  • Confetti: Subtle particle burst on success

Button Hierarchy:

  • Primary (Thumbs Up): Copy + Save + Close (complete action)
  • Secondary (Copy Only): Copy to clipboard, keep sheet open
  • Tertiary (Close): Dismiss without saving

Tone and Emotion:

  • Success state should feel celebratory
  • "You're done, great job!" messaging
  • Reinforces the value of completing the ritual

Micro-interactions:

  • Thumbs Up should have "spring" animation
  • Haptic feedback on mobile (vibration)
  • Smooth transition to history screen
  • New entry in history has subtle highlight/fade-in

Previous Story Intelligence (from Stories 2.1, 2.2, and 2.3)

Patterns Established (must follow):

  • Logic Sandwich Pattern: UI -> Zustand -> Service -> DB (strictly enforced)
  • Atomic Selectors: All state access uses useChatStore(s => s.field)
  • Draft Storage: Drafts stored in IndexedDB via drafts table
  • DraftViewSheet: Sheet component that responds to currentDraft state
  • Draft Status Flow: 'draft' -> 'regenerated' -> 'completed'

Key Files from Previous Stories:

  • src/lib/db/draft-service.ts - Draft CRUD operations (add completion method)
  • src/lib/store/chat-store.ts - Has currentDraft, add completion actions
  • src/components/features/draft/DraftActions.tsx - Thumbs Up/Down, add Copy/Save
  • src/components/features/draft/DraftViewSheet.tsx - Sheet component
  • src/services/chat-service.ts - Chat orchestration (add completion flow)

Learnings to Apply:

  • Story 2.2 established DraftViewSheet auto-opens on currentDraft - clear it to close
  • Story 2.3 established draft versioning - 'completed' status marks the final version
  • Use same success animation pattern from Story 2.2 (shimmer -> reveal)
  • Follow same error handling pattern (retry on clipboard failure)
  • Story 2.1 established DraftRecord structure - add completedAt timestamp

Draft Data Structure (extending from Story 2.1):

interface DraftRecord {
  id: string;
  sessionId: string;
  title: string;
  content: string;      // Markdown formatted
  tags: string[];
  createdAt: number;
  completedAt?: number; // NEW: Set when draft is marked as completed
  status: 'draft' | 'completed' | 'regenerated';
}

Integration with Refinement (Story 2.3):

  • If user refined draft, the final version is marked as 'completed'
  • Original draft(s) with status 'regenerated' remain in history
  • Only the latest draft (the one user approved) gets 'completed' status
  • History view (Epic 3) will show all versions, highlighting the completed one

Clipboard Implementation Specifications

Browser Clipboard API:

// Modern browsers (Chrome 66+, Firefox 63+, Safari 13.1+)
navigator.clipboard.writeText(markdown)
  .then(() => showSuccessToast())
  .catch(() => showFallbackMessage());

Fallback for Older Browsers:

// Create hidden textarea, select, execCommand('copy')
// Remove textarea after copy
// Returns boolean success

Clipboard Permissions:

  • Clipboard API requires user gesture (button click)
  • No permissions needed for writeText() in active tab
  • May fail in iframes or cross-origin contexts

Accessibility for Copy:

<button
  onClick={handleCopy}
  aria-label="Copy draft to clipboard"
  className="..."
>
  <CopyIcon />
  <span className="sr-only">Copy</span>
</button>

// After copy, announce to screen readers
useEffect(() => {
  if (copied) {
    announceToScreenReader('Draft copied to clipboard');
  }
}, [copied]);

Keyboard Shortcut Support:

  • Detect Cmd+C / Ctrl+C when draft is visible
  • Show tooltip: "Press Cmd+C to copy"
  • Prevent interference with browser default

Testing Requirements

Unit Tests:

  • ClipboardUtil.copyMarkdown() copies text correctly
  • ClipboardUtil.copyMarkdown() handles errors gracefully
  • ClipboardUtil.fallbackCopy() works for older browsers
  • DraftService.markAsCompleted() updates status
  • DraftService.markAsCompleted() sets completedAt timestamp
  • ChatStore.completeDraft() clears currentDraft
  • ChatStore.copyDraftToClipboard() calls ClipboardUtil

Integration Tests:

  • Full completion flow: Thumbs Up -> Copy -> Save -> Navigate
  • Copy only flow: Copy button -> Toast -> Sheet stays open
  • Draft appears in history after completion
  • Multiple drafts in session show all completed drafts
  • Refinement + completion: Refined draft marked as completed

Edge Cases:

  • Clipboard permission denied: Show error message
  • Clipboard API unavailable: Use fallback method
  • Draft already completed: Show message, don't duplicate
  • Very long draft content: Should handle within clipboard limits
  • Copy during refinement: Should work (copy current visible draft)
  • Navigate away during copy: Should handle gracefully

Accessibility Tests:

  • Screen reader announces "Copied to clipboard"
  • Keyboard navigation works for all copy buttons
  • Focus management after copy
  • ARIA labels on all copy buttons

Performance Tests:

  • Copy action completes within 500ms
  • Success toast appears within 100ms
  • Navigation to history is smooth (< 300ms)

Component Implementation Details

CopySuccessToast Component:

// src/components/features/feedback/CopySuccessToast.tsx
interface CopySuccessToastProps {
  message: string;
  duration?: number;
  onClose: () => void;
}

export function CopySuccessToast({
  message,
  duration = 3000,
  onClose
}: CopySuccessToastProps) {
  useEffect(() => {
    const timer = setTimeout(onClose, duration);
    return () => clearTimeout(timer);
  }, [duration, onClose]);

  // Trigger confetti on mount
  useEffect(() => {
    triggerConfetti();
  }, []);

  return (
    <div className="fixed bottom-4 left-1/2 -translate-x-1/2 ...">
      <CheckIcon className="text-green-500" />
      <span>{message}</span>
    </div>
  );
}

CopyButton Component:

// src/components/features/draft/CopyButton.tsx
interface CopyButtonProps {
  draftId: string;
  onCopy?: () => void;
  variant?: 'standalone' | 'toolbar';
}

export function CopyButton({
  draftId,
  onCopy,
  variant = 'standalone'
}: CopyButtonProps) {
  const [copied, setCopied] = useState(false);
  const copyDraftToClipboard = useChatStore(s => s.copyDraftToClipboard);

  const handleCopy = async () => {
    await copyDraftToClipboard(draftId);
    setCopied(true);
    onCopy?.();
    setTimeout(() => setCopied(false), 2000);
  };

  return (
    <button
      onClick={handleCopy}
      aria-label={copied ? 'Copied!' : 'Copy to clipboard'}
      className="..."
    >
      {copied ? <CheckIcon /> : <CopyIcon />}
    </button>
  );
}

ChatService Extensions:

// src/services/chat-service.ts
export class ChatService {
  async completeDraft(draftId: string): Promise<void> {
    // Copy to clipboard
    const draft = await DraftService.getDraftById(draftId);
    await ClipboardUtil.copyMarkdown(draft.content);

    // Mark as completed in database
    await DraftService.markAsCompleted(draftId);

    // Update store (clears currentDraft, closes sheet)
    ChatStore.getState().completeDraft(draftId);

    // Navigate to history
    router.push('/history');
  }

  async copyDraftOnly(draftId: string): Promise<void> {
    const draft = await DraftService.getDraftById(draftId);
    await ClipboardUtil.copyMarkdown(draft.content);
    // Don't clear currentDraft - keep sheet open
  }

  async copyAndSaveDraft(draftId: string): Promise<void> {
    // Same as completeDraft but without navigation
    const draft = await DraftService.getDraftById(draftId);
    await ClipboardUtil.copyMarkdown(draft.content);
    await DraftService.markAsCompleted(draftId);
    ChatStore.getState().completeDraft(draftId);
  }
}

DraftActions Integration:

// Update DraftActions component
const handleThumbsUp = async () => {
  // Full completion flow
  await ChatService.completeDraft(draftId);
  showSuccessToast('Draft saved to history!');
};

const handleCopyOnly = async () => {
  // Copy without closing
  await ChatService.copyDraftOnly(draftId);
  showSuccessToast('Copied to clipboard!');
};

History Navigation Strategy

After Completion:

// Navigate to history with highlight
router.push({
  pathname: '/history',
  query: { highlight: draftId }
});

// In history page, scroll to highlighted draft
useEffect(() => {
  if (router.query.highlight) {
    const element = document.getElementById(`draft-${router.query.highlight}`);
    element?.scrollIntoView({ behavior: 'smooth', block: 'center' });
    element?.classList.add('highlight-pulse');
  }
}, [router.query.highlight]);

Highlight Animation:

/* In globals.css */
@keyframes highlight-pulse {
  0% { box-shadow: 0 0 0 0 rgba(100, 116, 139, 0.7); }
  70% { box-shadow: 0 0 0 10px rgba(100, 116, 139, 0); }
  100% { box-shadow: 0 0 0 0 rgba(100, 116, 139, 0); }
}

.highlight-pulse {
  animation: highlight-pulse 1s ease-out;
}

Service Layer Extensions

DraftService Completion:

// src/lib/db/draft-service.ts
export class DraftService {
  static async markAsCompleted(draftId: string): Promise<Draft> {
    const existing = await this.getDraftById(draftId);

    if (existing.status === 'completed') {
      // Already completed, just return
      return existing;
    }

    await db.drafts.update(draftId, {
      status: 'completed',
      completedAt: Date.now()
    });

    return this.getDraftById(draftId);
  }

  static async getCompletedDrafts(): Promise<Draft[]> {
    return await db.drafts
      .where('status')
      .equals('completed')
      .reverse()
      .sortBy('completedAt');
  }
}

ChatStore Actions:

// src/lib/store/chat-store.ts
interface ChatStore {
  // Existing
  currentDraft: Draft | null;

  // New actions
  completeDraft: (draftId: string) => Promise<void>;
  copyDraftToClipboard: (draftId: string) => Promise<void>;
}

export const useChatStore = create<ChatStore>((set, get) => ({
  // Existing state...

  completeDraft: async (draftId: string) => {
    // Mark as completed
    await DraftService.markAsCompleted(draftId);
    // Clear from store (closes DraftViewSheet)
    set({ currentDraft: null });
  },

  copyDraftToClipboard: async (draftId: string) => {
    const draft = await DraftService.getDraftById(draftId);
    await ClipboardUtil.copyMarkdown(draft.content);
    // Don't clear currentDraft - keep sheet open
  },
}));

Project Structure Notes

Following Feature-First Lite Pattern:

  • New components in src/components/features/feedback/ (toast)
  • New components in src/components/features/draft/ (copy button)
  • 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
  • Utility functions in src/lib/utils/

No Conflicts Detected:

  • Completion is new status value, no conflicts
  • Clipboard utility is new, no conflicts
  • Navigation to history prepares for Epic 3

Performance Requirements

NFR-01 Compliance (Response Latency):

  • Copy action should complete within 500ms
  • Success toast should appear within 100ms
  • Navigation to history should be smooth (< 300ms)

State Updates:

  • currentDraft should clear immediately on completion
  • DraftViewSheet should close smoothly
  • History page should render quickly

Accessibility Requirements

WCAG AA Compliance:

  • Copy buttons must have aria-label
  • Success state must be announced to screen readers
  • Focus management: Return focus after copy
  • Keyboard shortcuts supported

Visual Accessibility:

  • Success toast must be high contrast
  • Avoid color-only indicators (use icons + text)
  • Highlight animation must respect prefers-reduced-motion

Security & Privacy Requirements

NFR-03 & NFR-04 Compliance:

  • Clipboard operations are client-side only
  • No draft content sent to server
  • Completed drafts stay in IndexedDB
  • No external API calls for copy/save

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/4ce6b396-ff74-42ee-be06-133000333628/scratchpad

Completion Notes List

Story Analysis Completed:

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

Implementation Context Summary:

Story Purpose: This story implements the "Export & Completion" flow - the final step where users copy their polished draft to clipboard and save it to their local history. This completes the core value loop: Vent -> Generate -> Refine -> Export -> Save.

Key Technical Decisions:

  1. Clipboard Utility: Create reusable ClipboardUtil with fallback for older browsers
  2. Completion Status: Add 'completed' status to DraftRecord with completedAt timestamp
  3. Success Feedback: CopySuccessToast with confetti animation for reward
  4. Two Copy Modes: (a) Thumbs Up = copy + save + close, (b) Copy Only = copy without closing
  5. Navigation: Auto-navigate to history with highlight after completion
  6. Haptic Feedback: Vibration on mobile for tactile confirmation

Dependencies:

  • No new external dependencies required
  • Uses browser Clipboard API with fallback
  • Reuses existing Zustand, Dexie infrastructure
  • May need confetti library (canvas-confetti) for celebration effect

Integration Points:

  • Thumbs Up in DraftActions (Story 2.2) triggers ChatService.completeDraft()
  • Completion updates DraftRecord status in IndexedDB
  • Clearing currentDraft closes DraftViewSheet (auto-close pattern from Story 2.2)
  • Navigation to history prepares for Epic 3 implementation

Files to Create:

  • src/lib/utils/clipboard.ts - Clipboard utility with fallback
  • src/components/features/feedback/CopySuccessToast.tsx - Success feedback component
  • src/components/features/draft/CopyButton.tsx - Standalone copy button

Files to Modify:

  • src/lib/store/chat-store.ts - Add completion actions (completeDraft, copyDraftToClipboard)
  • src/lib/db/draft-service.ts - Add markAsCompleted() method
  • src/services/chat-service.ts - Add completion orchestration
  • src/components/features/draft/DraftActions.tsx - Wire Thumbs Up to completion flow

Testing Strategy:

  • Unit tests for clipboard utility (including fallback)
  • Integration tests for full completion flow
  • Edge case tests (clipboard denied, already completed, very long content)
  • Accessibility tests (screen reader announcements, keyboard navigation)

Draft Completion Pattern:

  • Thumbs Up triggers: Copy -> Toast -> Mark Completed -> Clear State -> Navigate
  • Copy Only triggers: Copy -> Toast (no state change)
  • Completed draft gets status='completed' and completedAt timestamp
  • History view (Epic 3) will query by completedAt for reverse chronological order

User Experience Flow:

DraftViewSheet (Story 2.2)
    ↓
User taps Thumbs Up
    ↓
Clipboard.copy() - Copy Markdown to clipboard
    ↓
CopySuccessToast - "Copied to clipboard!" + confetti
    ↓
DraftService.markAsCompleted() - Update IndexedDB
    ↓
ChatStore.completeDraft() - Clear currentDraft (closes sheet)
    ↓
Navigate to /history with ?highlight=draftId
    ↓
History scrolls to and highlights the new entry

File List

New Files to Create:

  • src/lib/utils/clipboard.ts - Clipboard utility with fallback support
  • src/components/features/feedback/CopySuccessToast.tsx - Success toast with confetti
  • src/components/features/draft/CopyButton.tsx - Standalone copy button
  • src/lib/utils/clipboard.test.ts - Clipboard utility tests
  • src/integration/export-copy-actions.test.ts - End-to-end completion flow tests

Files to Modify:

  • src/lib/store/chat-store.ts - Add completeDraft, copyDraftToClipboard actions
  • src/lib/db/draft-service.ts - Add markAsCompleted() method and getCompletedDrafts()
  • src/lib/db/draft-service.test.ts - Add completion method tests
  • src/services/chat-service.ts - Add completion orchestration (completeDraft, copyDraftOnly)
  • src/components/features/draft/DraftActions.tsx - Wire Thumbs Up and Copy button to completion