- 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
25 KiB
Story 2.2: Draft View UI (The Slide-Up)
Status: done
Story
As a user, I want to view the generated draft in a clean, reading-focused interface, So that I can review it without the distraction of the chat.
Acceptance Criteria
-
Draft View Slide-Up Display
- Given the draft generation is complete
- When the result is ready
- Then a "Sheet" or modal slides up from the bottom
- And it displays the post in "Medium-style" typography (Merriweather font)
-
Comfortable Reading Experience
- Given the draft view is open
- When the user scrolls
- Then the reading experience is comfortable with appropriate whitespace
- And the "Thumbs Up" and "Thumbs Down" actions are sticky or easily accessible
Tasks / Subtasks
-
Implement Draft View Sheet Component
- Create
Sheet.tsxslide-up component (custom implementation, no ShadCN) - Configure slide-up animation from bottom (300ms ease-out)
- Set up responsive behavior (full-screen on mobile, centered card on desktop)
- Implement backdrop/dim overlay with tap-to-close
- Create
-
Implement Draft Content Display
- Create
DraftContent.tsxcomponent for Markdown rendering - Apply Merriweather serif font for content (UI font stays Inter)
- Add generous whitespace and line-height for readability
- Style headings, paragraphs, and code blocks following Medium-style
- Add tag display section
- Create
-
Implement Action Bar (Thumbs Up/Down)
- Create
DraftActions.tsxcomponent with sticky footer - Add Thumbs Up button (approve/copy)
- Add Thumbs Down button (regenerate feedback)
- Style buttons to be touch-friendly (44px min height)
- Add proper ARIA labels for accessibility
- Create
-
Integrate with ChatStore
- Connect
currentDraftstate to DraftViewSheet - Auto-open sheet when
currentDrafttransitions from null to populated - Handle sheet close action (clear currentDraft and showDraftView)
- Use atomic selectors for state access
- Connect
-
Implement Copy to Clipboard (Thumbs Up)
- Implement
copyToClipboard()utility inline in ChatStore - Copy to clipboard with fallback for older browsers
- Mark draft as 'completed' in IndexedDB
- Implement
-
Implement Regeneration Flow (Thumbs Down)
- Close DraftViewSheet on Thumbs Down tap
- System message to chat: "What should we change?" (Story 2.3 will add this)
- Set chat input focus for user feedback
- Preserve draft context for regeneration in Story 2.3
-
Implement Responsive Layout
- Mobile (< 768px): Full-screen sheet
- Desktop (>= 768px): Centered card layout (max-width ~600px)
- Ensure sheet doesn't stretch too wide on large screens
- Test keyboard navigation (Escape to close)
-
Test Draft View End-to-End
- Unit test: Sheet renders in closed state by default
- Unit test: Sheet opens when open prop is true
- Unit test: Markdown rendering handles code blocks, lists, links
- Unit test: DraftContent handles empty tags array
- Integration test: Sheet auto-opens after Ghostwriter completes
- Integration test: Thumbs Up copies to clipboard and marks completed
- Integration test: Thumbs Down returns to chat with feedback prompt
- Integration test: DraftViewSheet full flow with all components
Dev Notes
Architecture Compliance (CRITICAL)
Logic Sandwich Pattern - DO NOT VIOLATE:
- UI Components MUST NOT import
src/lib/dbor touch IndexedDB directly - Draft status updates MUST go through
ChatService->DraftService - 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, showDraftView } = useChatStore();
// GOOD - Atomic selectors
const currentDraft = useChatStore(s => s.currentDraft);
const showDraftView = useChatStore(s => s.showDraftView);
const closeDraftView = useChatStore(s => s.closeDraftView);
const approveDraft = useChatStore(s => s.approveDraft);
const rejectDraft = useChatStore(s => s.rejectDraft);
Local-First Data Boundary:
- Draft content already stored in IndexedDB (from Story 2.1)
- Thumbs Up action updates draft status to 'completed'
- Draft content remains in IndexedDB for history access (Epic 3)
- No draft content sent to server for any reason
Architecture Implementation Details
Story Purpose: This story implements the "Magic Moment" UI - the visual transformation from casual chat to polished artifact. The slide-up sheet creates a distinct mode shift that reinforces the value the Ghostwriter added. This is the climax of the user journey.
State Management Extensions:
// Add to ChatStore (src/lib/store/chat-store.ts)
interface ChatStore {
// Draft view state
showDraftView: boolean;
closeDraftView: () => void;
approveDraft: (draftId: string) => Promise<void>;
rejectDraft: (draftId: string, feedback?: string) => void;
}
Component Architecture:
DraftViewSheet (ShadCN Sheet wrapper)
├── DraftHeader (Title + Close button)
├── DraftContent (Markdown rendering with Merriweather font)
│ └── TagList (Tag chips)
└── DraftActions (Sticky footer with Thumbs Up/Down)
Logic Flow:
- Ghostwriter completes generation (Story 2.1)
- ChatStore sets
currentDraftwith generated draft showDraftViewbecomestrue(auto-trigger)- DraftViewSheet slides up from bottom
- User reviews content in comfortable reading view
- User taps Thumbs Up OR Thumbs Down:
- Thumbs Up: Copy to clipboard, mark as 'completed', show success animation
- Thumbs Down: Close sheet, return to chat, prompt for feedback
- Sheet closes and flow continues
Responsive Behavior:
- Mobile (< 768px): Full-screen sheet (covers chat completely)
- Desktop (>= 768px): Centered card (max-width 600px) with visible backdrop
- This "Centered App" pattern keeps the mobile-first feel on desktop
Files to Create:
src/components/features/draft/DraftViewSheet.tsx- Main sheet componentsrc/components/features/draft/DraftContent.tsx- Markdown renderer with Merriweathersrc/components/features/draft/DraftActions.tsx- Thumbs Up/Down footersrc/lib/utils/clipboard.ts- Copy to clipboard utility
Files to Modify:
src/lib/store/chat-store.ts- Add draft view state and actionssrc/services/draft-service.ts- AddupdateDraftStatus()methodsrc/services/chat-service.ts- AddapproveDraft()andrejectDraft()orchestration
UX Design Specifications
From UX Design Document:
The "Magic Moment" Visualization:
- Clear visual shift from "Chat" (casual) to "Draft" (professional)
- Sheet slide-up animation is critical for emotional payoff
- Chat should remain visible underneath (context preserved)
Typography - The "Split-Personality" UI:
- Chat UI (Input): Inter font (sans-serif) - casual, fast
- Draft UI (Output): Merriweather font (serif) - published, authoritative
- This font change signals the transformation value
Visual Design - "Morning Mist" Theme:
/* Draft Content Styling */
.draft-content {
font-family: 'Merriweather', serif;
line-height: 1.8;
color: #334155; /* Deep Slate */
padding: 24px;
}
.draft-title {
font-size: 1.75rem;
font-weight: 700;
margin-bottom: 1.5rem;
color: #1E293B;
}
.draft-body {
font-size: 1.125rem;
margin-bottom: 2rem;
}
/* Tags */
.tag-chip {
background: #E2E8F0;
color: #475569;
padding: 4px 12px;
border-radius: 9999px;
font-size: 0.875rem;
font-family: 'Inter', sans-serif;
}
Action Bar - Sticky Footer:
- Thumbs Up (Approve): Primary action, brand color (Slate Blue #64748B)
- Thumbs Down (Reject): Secondary action, outline style
- Minimum 44px touch targets (WCAG AA)
- Position: sticky at bottom of sheet
- ARIA labels: "Approve and copy to clipboard", "Request changes"
Responsive - "Centered App" Pattern:
- Desktop view should NOT stretch to full width
- Centered card container with max-width ~600px
- Generous whitespace background (Morning Mist #F8FAFC)
Animation - Slide-Up Transition:
- Duration: 300ms (ease-out)
- Should feel like "unveiling" the result
- No bounce or overshoot (feels professional, not playful)
Previous Story Intelligence (from Story 2.1)
Patterns Established (must follow):
- Logic Sandwich Pattern: UI -> Zustand -> Service -> Database (strictly enforced)
- Atomic Selectors: All state access uses
useChatStore(s => s.field) - Draft Storage: Drafts already stored in IndexedDB via
draftstable - Ghostwriter Integration:
currentDraftstate already in ChatStore - DraftingIndicator: Already shows during generation (Story 2.1)
Key Files from Story 2.1 (Reference):
src/lib/db/draft-service.ts- Draft CRUD operations (add status update method)src/lib/db/index.ts- Drafts table with status field ('draft' | 'completed' | 'regenerated')src/lib/store/chat-store.ts- HascurrentDraft, add draft view statesrc/components/features/chat/DraftingIndicator.tsx- Pattern for loading states
Learnings to Apply:
- Story 2.1 established the Draft data structure (id, title, content, tags, status)
- Draft is already persisted to IndexedDB when Ghostwriter completes
- Use the same DraftRecord interface from Story 2.1
- Follow the same testing pattern: unit tests for components, integration tests for flow
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';
}
Integration with Ghostwriter:
- When Ghostwriter completes,
currentDraftis already set in ChatStore - This story should auto-open DraftViewSheet when
currentDraftchanges - Use
useEffectto watch forcurrentDraftand setshowDraftView = true
Testing Requirements
Unit Tests:
DraftViewSheet: Renders draft content correctlyDraftViewSheet: Renders in closed state by defaultDraftViewSheet: Opens when showDraftView is trueDraftContent: Renders Markdown with proper stylingDraftContent: Handles code blocks, lists, links correctlyDraftActions: Thumbs Up button calls approveDraft callbackDraftActions: Thumbs Down button calls rejectDraft callbackClipboardUtils: copyToClipboard() writes to clipboard correctly
Integration Tests:
- Auto-open: Sheet opens when Ghostwriter completes (currentDraft populated)
- Thumbs Up: Copies to clipboard, marks draft as completed, closes sheet
- Thumbs Down: Closes sheet, adds feedback prompt to chat
- Close button: Closes sheet without changing draft status
- Escape key: Closes sheet on desktop
- Backdrop click: Closes sheet on desktop
Edge Cases:
- Very long draft (>2000 words): Scrolling works, actions remain accessible
- Draft with code blocks: Markdown renders correctly with syntax highlighting
- Draft with no tags: Tag section doesn't break
- Draft with special characters: Emojis, unicode render correctly
- Empty draft: Shows placeholder or error state
Accessibility Tests:
- Keyboard navigation: Tab through actions, Enter to activate
- Screen reader: ARIA labels on all buttons
- Focus trap: Focus stays in sheet when open
- Focus management: Focus returns to chat trigger after close
- Color contrast: All text passes WCAG AA (4.5:1)
Component Implementation Details
DraftViewSheet Component:
// src/components/features/draft/DraftViewSheet.tsx
import { Sheet, SheetContent, SheetHeader } from '@/components/ui/sheet';
import { DraftContent } from './DraftContent';
import { DraftActions } from './DraftActions';
interface DraftViewSheetProps {
draft: Draft | null;
open: boolean;
onClose: () => void;
onApprove: (draftId: string) => void;
onReject: (draftId: string) => void;
}
export function DraftViewSheet({
draft,
open,
onClose,
onApprove,
onReject
}: DraftViewSheetProps) {
if (!draft) return null;
return (
<Sheet open={open} onOpenChange={onClose}>
<SheetContent
side="bottom"
className="h-[85vh] sm:h-auto sm:max-h-[85vh] sm:max-w-[600px] sm:mx-auto"
>
<SheetHeader>
{/* Close button, Title */}
</SheetHeader>
<DraftContent draft={draft} />
<DraftActions
onApprove={() => onApprove(draft.id)}
onReject={() => onReject(draft.id)}
/>
</SheetContent>
</Sheet>
);
}
DraftContent Component:
// src/components/features/draft/DraftContent.tsx
import ReactMarkdown from 'react-markdown';
interface DraftContentProps {
draft: Draft;
}
export function DraftContent({ draft }: DraftContentProps) {
return (
<div className="draft-content font-merriweather prose prose-slate max-w-none">
<h2 className="draft-title">{draft.title}</h2>
<ReactMarkdown className="draft-body">
{draft.content}
</ReactMarkdown>
{draft.tags && draft.tags.length > 0 && (
<div className="flex flex-wrap gap-2 mt-4">
{draft.tags.map(tag => (
<span key={tag} className="tag-chip">#{tag}</span>
))}
</div>
)}
</div>
);
}
DraftActions Component:
// src/components/features/draft/DraftActions.tsx
import { Button } from '@/components/ui/button';
import { ThumbsUp, ThumbsDown } from 'lucide-react';
interface DraftActionsProps {
onApprove: () => void;
onReject: () => void;
}
export function DraftActions({ onApprove, onReject }: DraftActionsProps) {
return (
<div className="sticky bottom-0 flex gap-3 p-4 bg-white border-t">
<Button
onClick={onReject}
variant="outline"
className="flex-1 min-h-[44px]"
aria-label="Request changes to this draft"
>
<ThumbsDown className="mr-2 h-5 w-5" />
Not Quite
</Button>
<Button
onClick={onApprove}
className="flex-1 min-h-[44px] bg-slate-700 hover:bg-slate-800"
aria-label="Approve and copy to clipboard"
>
<ThumbsUp className="mr-2 h-5 w-5" />
Copy
</Button>
</div>
);
}
Markdown Rendering Strategy
Library Choice:
- Use
react-markdownfor Markdown parsing and rendering - Add
remark-gfmfor GitHub Flavored Markdown support (tables, strikethrough) - Add
rehype-highlightfor syntax highlighting in code blocks
Styling Approach:
- Use Tailwind's
@tailwindcss/typographyplugin for prose styling - Override prose styles to match "Morning Mist" theme
- Apply Merriweather font to prose elements only
- Keep UI elements (buttons, tags) in Inter font
Configuration:
// tailwind.config.js
module.exports = {
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'sans-serif'],
serif: ['Merriweather', 'serif'],
},
},
},
plugins: [
require('@tailwindcss/typography'),
],
};
Clipboard Copy Implementation
Utility Function:
// src/lib/utils/clipboard.ts
export async function copyToClipboard(text: string): Promise<boolean> {
try {
await navigator.clipboard.writeText(text);
return true;
} catch (err) {
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = text;
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy');
document.body.removeChild(textArea);
return true;
} catch (fallbackErr) {
document.body.removeChild(textArea);
return false;
}
}
}
Success Feedback:
- Use ShadCN
useToast()hook for success notification - Toast message: "Copied to clipboard!"
- Optional: Add confetti animation using
canvas-confettilibrary
Service Layer Extensions
DraftService Updates:
// src/services/draft-service.ts
export class DraftService {
// ... existing methods from Story 2.1
async updateDraftStatus(
draftId: string,
status: 'draft' | 'completed' | 'regenerated'
): Promise<void> {
await db.drafts.update(draftId, { status });
}
}
ChatService Orchestration:
// src/services/chat-service.ts
export class ChatService {
async approveDraft(draftId: string): Promise<void> {
// Update draft status to completed
await DraftService.updateDraftStatus(draftId, 'completed');
// Copy to clipboard
const draft = await DraftService.getDraftById(draftId);
const markdown = this.formatDraftAsMarkdown(draft);
await copyToClipboard(markdown);
}
rejectDraft(draftId: string): void {
// Just close the view and return to chat
// Story 2.3 will handle the regeneration flow
}
private formatDraftAsMarkdown(draft: Draft): string {
return `# ${draft.title}\n\n${draft.content}\n\nTags: ${draft.tags.join(', ')}`;
}
}
Project Structure Notes
Following Feature-First Lite Pattern:
- New feature folder:
src/components/features/draft/ - Draft components co-located for easy discovery
- Shares
src/components/ui/ShadCN primitives
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
- Utilities in
src/lib/utils/
No Conflicts Detected:
- DraftViewSheet is new UI layer, no conflicts with existing code
- Extends existing DraftService with status update method
- Adds to ChatStore state, following established patterns
Performance Requirements
NFR-02 Compliance (App Load Time):
- DraftViewSheet should animate smoothly (< 300ms transition)
- Markdown rendering should not block the main thread
- Use React.memo for DraftContent to prevent unnecessary re-renders
Rendering Performance:
- Large drafts (>2000 words) should render without jank
- Code blocks with syntax highlighting should load quickly
- Lazy load syntax highlighting library only when needed
Memory Management:
- Clear draft from currentDraft state after approval/rejection
- Don't keep multiple drafts in memory simultaneously
- IndexedDB is the source of truth, not component state
Accessibility Requirements
WCAG AA Compliance:
- Color contrast: All text meets 4.5:1 ratio (Morning Mist palette enforces this)
- Touch targets: Minimum 44px for all buttons
- Keyboard navigation: Full keyboard support (Tab, Enter, Escape)
- Screen reader: ARIA labels on all interactive elements
- Focus management: Focus trap in sheet, proper focus return on close
Focus Management:
- When sheet opens, focus moves to close button
- Focus is trapped inside sheet while open
- When sheet closes, focus returns to trigger element
- Thumbs Up/Down buttons are keyboard accessible
Semantic HTML:
- Use
<article>for draft content - Use
<nav>for action bar - Use proper heading hierarchy (h2 for draft title)
- Use
aria-labelfor icon-only buttons
Security & Privacy Requirements
NFR-03 & NFR-04 Compliance:
- Clipboard copy is local-only (client-side API)
- No draft content transmitted to server on approval
- Draft status update is a local IndexedDB operation
- Copy to clipboard does not expose content to external services
Content Safety:
- Draft content is user-generated, no sanitization needed for local display
- Markdown rendering should not execute arbitrary scripts
- Use
react-markdownwhich sanitizes by default
References
Epic Reference:
Architecture Documents:
- Project Context: Logic Sandwich
- Project Context: State Management
- Project Context: Local-First Boundary
- Architecture: Service Boundaries
UX Design Specifications:
- UX: The "Magic Moment" Visualization
- UX: Typography System
- UX: Component Strategy - DraftCard
- UX: Experience Mechanics - The Magic
- UX: Responsive Strategy
Previous Stories:
- Story 2.1: Ghostwriter Agent & Markdown Generation - Ghostwriter generates drafts stored in IndexedDB
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.2
- Analyzed previous Story 2.1 for established patterns (Ghostwriter, DraftRecord, DraftService)
- Reviewed architecture for compliance requirements (Logic Sandwich, State Management, Local-First)
- Reviewed UX specification for "Magic Moment" visualization, typography, responsive design
- Identified all files to create and modify
Implementation Context Summary:
Story Purpose: This story implements the "Magic Moment" UI - the slide-up sheet that displays the Ghostwriter's draft in a polished, reading-focused interface. This is the emotional climax of the user journey where raw chat is transformed into a tangible artifact.
Key Technical Decisions:
- Component Structure: DraftViewSheet (Sheet wrapper) -> DraftContent (Markdown) + DraftActions (Footer)
- Typography Split: Merriweather (serif) for draft content, Inter (sans-serif) for UI - reinforces transformation value
- Responsive Design: Full-screen on mobile, centered 600px card on desktop ("Centered App" pattern)
- Markdown Rendering: react-markdown + @tailwindcss/typography for prose styling
- State Management: Add draft view state to ChatStore (showDraftView, closeDraftView, approveDraft, rejectDraft)
- Service Extensions: DraftService.updateDraftStatus() for marking drafts as completed
Dependencies:
- New dependencies to add:
react-markdown- Markdown parsing and renderingremark-gfm- GitHub Flavored Markdown supportrehype-highlight- Syntax highlighting for code blocks@tailwindcss/typography- Tailwind prose styling plugincanvas-confetti(optional) - Success animation on copy
- Reuses existing: Zustand, Dexie, ShadCN Sheet component
Integration Points:
- Auto-opens when Ghostwriter completes (watch currentDraft state in ChatStore)
- Thumbs Up: Copies to clipboard, marks draft as 'completed' in IndexedDB
- Thumbs Down: Closes sheet, returns to chat (Story 2.3 handles regeneration flow)
- Draft content already stored in IndexedDB from Story 2.1
Files to Create:
src/components/features/draft/DraftViewSheet.tsx- Main sheet component with ShadCN Sheet wrappersrc/components/features/draft/DraftContent.tsx- Markdown renderer with Merriweather fontsrc/components/features/draft/DraftActions.tsx- Thumbs Up/Down sticky footersrc/lib/utils/clipboard.ts- Copy to clipboard utility function
Files to Modify:
src/lib/store/chat-store.ts- Add draft view state and actionssrc/services/draft-service.ts- Add updateDraftStatus() methodsrc/services/chat-service.ts- Add approveDraft() and rejectDraft() orchestrationtailwind.config.js- Add Merriweather font and typography plugin
Testing Strategy:
- Unit tests for each component (DraftViewSheet, DraftContent, DraftActions)
- Integration tests for full flow (Ghostwriter -> DraftView -> Copy/Reject)
- Edge case tests (long drafts, code blocks, special characters)
- Accessibility tests (keyboard nav, screen reader, focus management)
File List
New Files to Create:
src/components/features/draft/DraftViewSheet.tsx- Main sheet componentsrc/components/features/draft/DraftContent.tsx- Markdown renderer with Merriweathersrc/components/features/draft/DraftActions.tsx- Thumbs Up/Down footersrc/lib/utils/clipboard.ts- Copy to clipboard utilitysrc/components/features/draft/draft-view-sheet.test.tsx- Component testssrc/components/features/draft/draft-content.test.tsx- Markdown rendering testssrc/components/features/draft/draft-actions.test.tsx- Action button testssrc/integration/draft-view-flow.test.ts- End-to-end flow tests