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>
This commit is contained in:
775
_bmad-output/implementation-artifacts/2-4-export-copy-actions.md
Normal file
775
_bmad-output/implementation-artifacts/2-4-export-copy-actions.md
Normal file
@@ -0,0 +1,775 @@
|
||||
# Story 2.4: Export & Copy Actions
|
||||
|
||||
Status: review
|
||||
|
||||
<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
|
||||
|
||||
## 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
|
||||
|
||||
- [x] Implement Clipboard Copy Functionality
|
||||
- [x] Create `copyToClipboard()` utility in `src/lib/utils/clipboard.ts`
|
||||
- [x] Support copying Markdown formatted text
|
||||
- [x] Handle clipboard API errors gracefully
|
||||
- [x] Add fallback for older browsers
|
||||
|
||||
- [x] Create Success Feedback Toast
|
||||
- [x] Create `CopySuccessToast.tsx` component in `src/components/features/feedback/`
|
||||
- [x] Show confetti or success animation on copy
|
||||
- [x] Auto-dismiss after 3 seconds
|
||||
- [x] Include haptic feedback on mobile devices
|
||||
- [x] Accessible: announce to screen readers
|
||||
|
||||
- [x] Extend DraftActions with Copy/Save
|
||||
- [x] Add "Copy" button variant (separate from Thumbs Up)
|
||||
- [x] Wire Thumbs Up to both copy AND save actions
|
||||
- [x] Add "Just Copy" option (copy without closing sheet)
|
||||
- [x] Add "Save & Close" action
|
||||
|
||||
- [x] Implement Draft Completion Logic
|
||||
- [x] Add `completeDraft(draftId: number)` action to ChatStore
|
||||
- [x] Update draft status from 'draft' to 'completed' in IndexedDB
|
||||
- [x] Clear `currentDraft` from store after completion
|
||||
- [x] Close DraftViewSheet after completion
|
||||
- [ ] Navigate to Home/History screen (deferred to Epic 3)
|
||||
|
||||
- [x] Implement DraftService Completion Method
|
||||
- [x] Add `markAsCompleted(draftId: string)` to `src/lib/db/draft-service.ts`
|
||||
- [x] Update DraftRecord status in IndexedDB
|
||||
- [x] Add `completedAt` timestamp
|
||||
- [x] 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
|
||||
|
||||
- [x] Add Copy Accessibility Features
|
||||
- [x] Add `aria-label` to all copy buttons
|
||||
- [x] Announce "Copied to clipboard" to screen readers
|
||||
- [ ] Support keyboard shortcuts (Cmd+C / Ctrl+C) (deferred - enhancement)
|
||||
- [ ] Ensure focus management after copy action (deferred - enhancement)
|
||||
|
||||
- [x] Test Copy & Save End-to-End
|
||||
- [x] Unit test: clipboard.copyText() with Markdown (12 tests)
|
||||
- [x] Unit test: DraftService.markAsCompleted() updates status (3 tests)
|
||||
- [x] Integration test: Thumbs Up -> Copy -> Save flow (DraftViewSheet tests)
|
||||
- [x] Integration test: Copy button only (no save) (DraftViewSheet tests)
|
||||
- [x] Edge case: Clipboard permission denied (clipboard fallback tests)
|
||||
- [x] 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:**
|
||||
```typescript
|
||||
// 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:**
|
||||
```typescript
|
||||
// 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:**
|
||||
```typescript
|
||||
// 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:**
|
||||
```typescript
|
||||
// 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:**
|
||||
```mermaid
|
||||
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):**
|
||||
```typescript
|
||||
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:**
|
||||
```typescript
|
||||
// Modern browsers (Chrome 66+, Firefox 63+, Safari 13.1+)
|
||||
navigator.clipboard.writeText(markdown)
|
||||
.then(() => showSuccessToast())
|
||||
.catch(() => showFallbackMessage());
|
||||
```
|
||||
|
||||
**Fallback for Older Browsers:**
|
||||
```typescript
|
||||
// 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:**
|
||||
```typescript
|
||||
<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:**
|
||||
```typescript
|
||||
// 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:**
|
||||
```typescript
|
||||
// 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:**
|
||||
```typescript
|
||||
// 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:**
|
||||
```typescript
|
||||
// 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:**
|
||||
```typescript
|
||||
// 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:**
|
||||
```css
|
||||
/* 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:**
|
||||
```typescript
|
||||
// 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:**
|
||||
```typescript
|
||||
// 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:**
|
||||
- [Epic 2: "The Magic Mirror" - Ghostwriter & Draft Refinement](file:///home/maximilienmao/Projects/Test01/_bmad-output/planning-artifacts/epics.md#epic-2-the-magic-mirror---ghostwriter--draft-refinement)
|
||||
- [Story 2.4: Export & Copy Actions](file:///home/maximilienmao/Projects/Test01/_bmad-output/planning-artifacts/epics.md#story-24-export--copy-actions)
|
||||
- FR-07: "Users can 'One-Click Copy' the formatted text to clipboard"
|
||||
- FR-09: "Users can edit the generated draft manually before exporting" (future enhancement)
|
||||
|
||||
**Architecture Documents:**
|
||||
- [Project Context: Logic Sandwich](file:///home/maximilienmao/Projects/Test01/_bmad-output/project-context.md#1-the-logic-sandwich-pattern-service-layer)
|
||||
- [Project Context: State Management](file:///home/maximilienmao/Projects/Test01/_bmad-output/project-context.md#2-state-management-zustand)
|
||||
- [Project Context: Local-First Boundary](file:///home/maximilienmao/Projects/Test01/_bmad-output/project-context.md#3-local-first-data-boundary)
|
||||
|
||||
**UX Design Specifications:**
|
||||
- [UX: Completion Reward Pattern](file:///home/maximilienmao/Projects/Test01/_bmad-output/planning-artifacts/ux-design-specification.md#24-novel-ux-patterns)
|
||||
- [UX: Success State](file:///home/maximilienmao/Projects/Test01/_bmad-output/planning-artifacts/ux-design-specification.md#5-completion-if-liked)
|
||||
- [UX: Micro-interactions](file:///home/maximilienmao/Projects/Test01/_bmad-output/planning-artifacts/ux-design-specification.md#micro-interactions)
|
||||
|
||||
**Previous Stories:**
|
||||
- [Story 2.1: Ghostwriter Agent & Markdown Generation](file:///home/maximilienmao/Projects/Test01/_bmad-output/implementation-artifacts/2-1-ghostwriter-agent-markdown-generation.md) - Draft data structure
|
||||
- [Story 2.2: Draft View UI (The Slide-Up)](file:///home/maximilienmao/Projects/Test01/_bmad-output/implementation-artifacts/2-2-draft-view-ui-the-slide-up.md) - DraftViewSheet and DraftActions
|
||||
- [Story 2.3: Refinement Loop (Regeneration)](file:///home/maximilienmao/Projects/Test01/_bmad-output/implementation-artifacts/2-3-refinement-loop-regeneration.md) - Draft versioning pattern
|
||||
|
||||
## 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
|
||||
Reference in New Issue
Block a user