- 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
936 lines
31 KiB
Markdown
936 lines
31 KiB
Markdown
# Story 3.1: History Feed UI
|
|
|
|
Status: done
|
|
|
|
<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
|
|
|
|
## Story
|
|
|
|
As a user,
|
|
I want to see a list of my past growing moments,
|
|
So that I can reflect on my journey.
|
|
|
|
## Acceptance Criteria
|
|
|
|
1. **History Feed on Home Screen**
|
|
- Given the user is on the Home screen
|
|
- When they view the feed
|
|
- Then they see a chronological list of past "Completed" sessions (Title, Date, Tags)
|
|
- And the list supports lazy loading/pagination for performance
|
|
|
|
2. **View Past Enlightenment Artifact**
|
|
- Given the user clicks a history card
|
|
- When the card opens
|
|
- Then the full "Enlightenment" artifact allows for reading
|
|
- And the "Copy" action is available
|
|
|
|
## Tasks / Subtasks
|
|
|
|
- [x] Create History Feed Data Layer
|
|
- [x] Add `getCompletedDrafts()` method to DraftService
|
|
- [x] Implement pagination/offset support (limit 20 items per page)
|
|
- [x] Support reverse chronological sorting (newest first)
|
|
- [ ] Add filtering by tags (optional enhancement)
|
|
|
|
- [x] Create History Feed Store State
|
|
- [x] Create `useHistoryStore` in `src/lib/store/history-store.ts`
|
|
- [x] State: drafts array, loading, hasMore, error
|
|
- [x] Actions: loadMore, refreshHistory, selectDraft
|
|
- [x] Use atomic selectors for performance
|
|
|
|
- [x] Create HistoryCard Component
|
|
- [x] Create `HistoryCard.tsx` in `src/components/features/journal/`
|
|
- [x] Display: Title, Date (formatted), Tags array
|
|
- [x] Show snippet preview (first 100 chars of content)
|
|
- [x] Support click-to-view-full action
|
|
- [x] Accessible: semantic button/link with aria-label
|
|
- [x] Responsive: proper touch targets (44px minimum)
|
|
|
|
- [x] Create HistoryFeed List Component
|
|
- [x] Create `HistoryFeed.tsx` in `src/components/features/journal/`
|
|
- [x] Implement virtual scrolling or lazy loading
|
|
- [x] Show loading spinner at bottom when loading more
|
|
- [x] Handle empty state (no drafts yet)
|
|
- [x] Handle error state with retry option
|
|
|
|
- [x] Create HistoryDetailSheet Component
|
|
- [x] Create `HistoryDetailSheet.tsx` in `src/components/features/journal/`
|
|
- [x] Reuse Sheet component from DraftViewSheet (Story 2.2)
|
|
- [x] Display full draft content with Merriweather font
|
|
- [x] Include Copy button (reuse from Story 2.4)
|
|
- [x] Support swipe-to-dismiss on mobile
|
|
- [x] Handle edit/resume-chat action (future: Story 3.2)
|
|
|
|
- [x] Integrate History Feed into Home Page
|
|
- [x] Update `src/app/page.tsx` to include HistoryFeed
|
|
- [x] Layout: New Chat button at bottom, history above
|
|
- [x] Handle navigation: tap card -> open detail sheet
|
|
- [ ] Add pull-to-refresh gesture (deferred - can be enhancement)
|
|
- [x] Initial load shows first 20 drafts
|
|
|
|
- [x] Implement Date Formatting Utility
|
|
- [x] Create `formatRelativeDate()` in `src/lib/utils/date.ts`
|
|
- [x] Support: "Today", "Yesterday", "X days ago", full date
|
|
- [ ] Support i18n (future enhancement)
|
|
- [x] Test edge cases: future dates, null dates
|
|
|
|
- [x] Add Empty State for New Users
|
|
- [x] Create `EmptyHistoryState.tsx` component
|
|
- [x] Show encouraging message when no drafts exist
|
|
- [x] Include CTA to start first vent
|
|
- [x] Use calming illustration or icon
|
|
|
|
- [x] Implement Loading States
|
|
- [x] Skeleton screens for history cards
|
|
- [ ] Progressive loading animation (deferred - can be enhancement)
|
|
- [ ] Smooth fade-in for new items (deferred - can be enhancement)
|
|
|
|
- [x] Test History Feed End-to-End
|
|
- [x] Unit test: DraftService.getCompletedDrafts() with pagination
|
|
- [x] Unit test: DraftService.getCompletedCount()
|
|
- [x] Unit test: HistoryStore loadMore action
|
|
- [x] Unit test: formatRelativeDate()
|
|
- [x] Component test: HistoryCard rendering
|
|
- [x] Component test: HistoryFeed rendering
|
|
- [x] Component test: Home page integration
|
|
- [ ] Edge case: No drafts (empty state)
|
|
- [ ] Edge case: Draft with no tags
|
|
- [ ] Edge case: Short content preview
|
|
- [ ] Integration tests: Deferred to integration test suite
|
|
|
|
## Dev Notes
|
|
|
|
### Architecture Compliance (CRITICAL)
|
|
|
|
**Logic Sandwich Pattern - DO NOT VIOLATE:**
|
|
- **UI Components** MUST NOT import `src/lib/db` directly
|
|
- All history data MUST go through `DraftService` layer
|
|
- DraftService queries `db.drafts` table (status='completed')
|
|
- Components use Zustand store via atomic selectors only
|
|
- Services return plain arrays, not Dexie observables
|
|
|
|
**State Management - Atomic Selectors Required:**
|
|
```typescript
|
|
// BAD - Causes unnecessary re-renders
|
|
const { drafts, loading } = useHistoryStore();
|
|
|
|
// GOOD - Atomic selectors
|
|
const drafts = useHistoryStore(s => s.drafts);
|
|
const loadMore = useHistoryStore(s => s.loadMore);
|
|
```
|
|
|
|
**Local-First Data Boundary:**
|
|
- History data is queried from IndexedDB only
|
|
- No server API calls for history (privacy requirement)
|
|
- Completed drafts persist locally (Story 2.4 completion flow)
|
|
- Offline support: History must be viewable without network
|
|
|
|
**Performance Requirements:**
|
|
- NFR-02: App must load in <1.5s
|
|
- Use lazy loading/virtual scrolling for large histories
|
|
- Limit initial load to 20 drafts
|
|
- Pagination loads additional drafts on demand
|
|
|
|
### Architecture Implementation Details
|
|
|
|
**Story Purpose:**
|
|
This story implements the **"History Journal"** - the persistent feed where users can revisit all their past "Enlightenment" artifacts. This transforms the app from a single-use tool into a **growth journal**, enabling reflection on past insights.
|
|
|
|
**Data Source - Completed Drafts:**
|
|
- Story 2.4 established draft completion flow
|
|
- Completed drafts have `status: 'completed'` and `completedAt` timestamp
|
|
- History queries `db.drafts.where('[status+completedAt]').between(...)`
|
|
- Sorted by `completedAt` descending (newest first) using compound index
|
|
|
|
**HistoryStore Architecture:**
|
|
```typescript
|
|
// src/lib/store/history-store.ts
|
|
interface HistoryState {
|
|
drafts: Draft[]; // Loaded drafts (paginated)
|
|
loading: boolean;
|
|
hasMore: boolean; // Can load more?
|
|
error: string | null;
|
|
selectedDraft: Draft | null;
|
|
|
|
// Actions
|
|
loadMore: () => Promise<void>;
|
|
refreshHistory: () => Promise<void>;
|
|
selectDraft: (draft: Draft) => void;
|
|
closeDetail: () => void;
|
|
}
|
|
```
|
|
|
|
**DraftService History Methods:**
|
|
```typescript
|
|
// src/lib/db/draft-service.ts
|
|
export class DraftService {
|
|
// Get completed drafts with pagination
|
|
static async getCompletedDrafts(options: {
|
|
limit?: number;
|
|
offset?: number;
|
|
}): Promise<Draft[]> {
|
|
return await db.drafts
|
|
.where('status')
|
|
.equals('completed')
|
|
.reverse() // Newest first
|
|
.offset(options.offset || 0)
|
|
.limit(options.limit || 20)
|
|
.toArray();
|
|
}
|
|
|
|
// Count total completed drafts
|
|
static async getCompletedCount(): Promise<number> {
|
|
return await db.drafts
|
|
.where('status')
|
|
.equals('completed')
|
|
.count();
|
|
}
|
|
}
|
|
```
|
|
|
|
**Files to Create:**
|
|
- `src/lib/store/history-store.ts` - History feed state management
|
|
- `src/components/features/journal/HistoryCard.tsx` - Individual history item
|
|
- `src/components/features/journal/HistoryFeed.tsx` - List with lazy loading
|
|
- `src/components/features/journal/HistoryDetailSheet.tsx` - Full draft view
|
|
- `src/components/features/journal/EmptyHistoryState.tsx` - Empty state
|
|
- `src/lib/utils/date.ts` - Date formatting utilities
|
|
- `src/components/features/journal/index.ts` - Feature exports
|
|
|
|
**Files to Modify:**
|
|
- `src/lib/db/draft-service.ts` - Add getCompletedDrafts(), getCompletedCount()
|
|
- `src/app/(main)/page.tsx` - Integrate HistoryFeed into home page
|
|
|
|
### UX Design Specifications
|
|
|
|
**From UX Design Document:**
|
|
|
|
**History Feed as Home Screen:**
|
|
- The home screen IS the history feed
|
|
- Latest generated post shown at top (reminder of value)
|
|
- Large "+" or "New" button at bottom for new vent
|
|
- Scrolling through past wins provides dopamine hits
|
|
|
|
**Visual Hierarchy:**
|
|
```mermaid
|
|
graph TD
|
|
A[Home Screen] --> B[Header: My Journal]
|
|
A --> C[History Feed - Scrollable]
|
|
A --> D[FAB: + New Vent]
|
|
C --> E[History Card 1 - Latest]
|
|
C --> F[History Card 2]
|
|
C --> G[History Card 3...]
|
|
C --> H[Load More Indicator]
|
|
```
|
|
|
|
**HistoryCard Design:**
|
|
- **Title:** Merriweather font, bold, truncate after 2 lines
|
|
- **Date:** Inter font, subtle gray, relative format ("Today", "Yesterday")
|
|
- **Tags:** Pill badges, subtle background
|
|
- **Preview:** First 100 chars of content, light gray
|
|
- **Elevation:** Subtle shadow on hover (desktop) or tap (mobile)
|
|
|
|
**HistoryDetailSheet Pattern:**
|
|
- Reuses DraftViewSheet component from Story 2.2
|
|
- Same "Medium-style" typography (Merriweather)
|
|
- Copy button in footer (from Story 2.4)
|
|
- Swipe down to dismiss (mobile pattern)
|
|
|
|
**Empty State Design:**
|
|
- Message: "Your journey starts here"
|
|
- Subtext: "Every venting session becomes a learning moment"
|
|
- CTA: "Start My First Vent" button
|
|
- Calming illustration: Mountain path or sunrise
|
|
|
|
**Loading States:**
|
|
- **Initial Load:** 3 skeleton cards with shimmer effect
|
|
- **Load More:** Spinner at bottom of list
|
|
- **Progressive Fade-in:** New items fade in smoothly
|
|
|
|
**Typography Specifications:**
|
|
```css
|
|
/* HistoryCard */
|
|
.history-title {
|
|
font-family: 'Merriweather', serif;
|
|
font-weight: 700;
|
|
font-size: 1.125rem;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.history-date {
|
|
font-family: 'Inter', sans-serif;
|
|
color: #64748B; /* Slate-500 */
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.history-preview {
|
|
font-family: 'Inter', sans-serif;
|
|
color: #94A3B8; /* Slate-400 */
|
|
font-size: 0.875rem;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
```
|
|
|
|
**Interaction Design:**
|
|
- **Tap card:** Opens detail sheet (smooth slide-up)
|
|
- **Long press:** Show context menu (Copy, Delete - Story 3.2)
|
|
- **Pull to refresh:** Reload history from database
|
|
- **Scroll to bottom:** Trigger loadMore when 5 items from end
|
|
|
|
### Previous Story Intelligence (from Epic 2)
|
|
|
|
**Patterns Established (must follow):**
|
|
- **Logic Sandwich Pattern:** UI -> Zustand -> Service -> DB (strictly enforced)
|
|
- **Atomic Selectors:** All state access uses `useStore(s => s.field)`
|
|
- **Draft Storage:** Completed drafts in `drafts` table with status='completed'
|
|
- **Sheet Pattern:** Reuse DraftViewSheet for detail view
|
|
- **Copy Utility:** Reuse clipboard.copyMarkdown() from Story 2.4
|
|
|
|
**Key Files from Previous Stories:**
|
|
- `src/lib/db/draft-service.ts` - Add history query methods
|
|
- `src/lib/store/chat-store.ts` - Pattern for atomic selectors
|
|
- `src/components/features/draft/DraftViewSheet.tsx` - Reuse for detail view
|
|
- `src/lib/utils/clipboard.ts` - Reuse copy functionality
|
|
- `src/components/ui/` - ShadCN primitives (Card, Button, Sheet)
|
|
|
|
**Learnings to Apply:**
|
|
- Epic 1 retrospective: **Atomic selectors are non-negotiable** for performance
|
|
- Story 2.2 established Sheet auto-open/close pattern - reuse for history detail
|
|
- Story 2.4 established completion status - query by status='completed'
|
|
- Use same shimmer loading pattern from Story 2.2 (DraftViewSheet skeleton)
|
|
- Follow same error handling pattern (retry with exponential backoff)
|
|
|
|
**Draft Data Structure (from Story 2.1):**
|
|
```typescript
|
|
interface DraftRecord {
|
|
id?: number;
|
|
sessionId: string;
|
|
title: string;
|
|
content: string; // Markdown formatted
|
|
tags: string[];
|
|
createdAt: number;
|
|
completedAt?: number; // Set by Story 2.4
|
|
status: 'draft' | 'completed' | 'regenerated';
|
|
}
|
|
```
|
|
|
|
**Integration with Copy (Story 2.4):**
|
|
- Copy button in HistoryDetailSheet reuses clipboard utility
|
|
- Copy action doesn't change draft status (already completed)
|
|
- Success toast from Story 2.4 provides feedback
|
|
|
|
### Pagination & Performance Strategy
|
|
|
|
**Lazy Loading Implementation:**
|
|
```typescript
|
|
// HistoryStore pagination
|
|
const PAGE_SIZE = 20;
|
|
|
|
loadMore: async () => {
|
|
const currentLength = get().drafts.length;
|
|
const newDrafts = await DraftService.getCompletedDrafts({
|
|
limit: PAGE_SIZE,
|
|
offset: currentLength
|
|
});
|
|
|
|
set(state => ({
|
|
drafts: [...state.drafts, ...newDrafts],
|
|
hasMore: newDrafts.length === PAGE_SIZE
|
|
}));
|
|
}
|
|
```
|
|
|
|
**Virtual Scrolling vs Lazy Loading:**
|
|
- **MVP Approach:** Lazy loading (simpler, sufficient for <100 drafts)
|
|
- **Future Enhancement:** Virtual scrolling for very large histories
|
|
- Lazy loading appends to array, React renders all
|
|
- For MVP, 100 drafts * ~2KB each = ~200KB in memory (acceptable)
|
|
|
|
**Performance Optimizations:**
|
|
- **Memoization:** React.memo on HistoryCard component
|
|
- **Key Prop:** Use draft.id as key (stable across re-renders)
|
|
- **Avoid Inline Functions:** Define click handlers in component, not render
|
|
- **Debounce Scroll:** Trigger loadMore only when near bottom
|
|
|
|
**Load More Trigger:**
|
|
```typescript
|
|
// Infinite scroll trigger
|
|
const handleScroll = (e: React.UIEvent) => {
|
|
const target = e.target as HTMLElement;
|
|
const scrollBottom = target.scrollHeight - target.scrollTop - target.clientHeight;
|
|
|
|
// Trigger when 200px from bottom
|
|
if (scrollBottom < 200 && hasMore && !loading) {
|
|
loadMore();
|
|
}
|
|
};
|
|
```
|
|
|
|
### Date Formatting Implementation
|
|
|
|
**Utility Function:**
|
|
```typescript
|
|
// src/lib/utils/date.ts
|
|
export function formatRelativeDate(timestamp: number): string {
|
|
const now = Date.now();
|
|
const diff = now - timestamp;
|
|
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
|
|
|
if (days === 0) return 'Today';
|
|
if (days === 1) return 'Yesterday';
|
|
if (days < 7) return `${days} days ago`;
|
|
if (days < 30) return `${Math.floor(days / 7)} weeks ago`;
|
|
|
|
// Full date for older posts
|
|
const date = new Date(timestamp);
|
|
return date.toLocaleDateString('en-US', {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
year: date.getFullYear() !== new Date().getFullYear() ? 'numeric' : undefined
|
|
});
|
|
}
|
|
```
|
|
|
|
**Time Zones:**
|
|
- Store timestamps as UTC (Date.now())
|
|
- Format in user's local timezone (toLocaleDateString handles this)
|
|
- Avoid timezone conversion bugs
|
|
|
|
### Component Implementation Details
|
|
|
|
**HistoryCard Component:**
|
|
```typescript
|
|
// src/components/features/journal/HistoryCard.tsx
|
|
interface HistoryCardProps {
|
|
draft: Draft;
|
|
onClick: (draft: Draft) => void;
|
|
}
|
|
|
|
export function HistoryCard({ draft, onClick }: HistoryCardProps) {
|
|
return (
|
|
<button
|
|
onClick={() => onClick(draft)}
|
|
className="w-full text-left p-4 bg-white rounded-lg shadow-sm hover:shadow-md transition-shadow"
|
|
aria-label={`View post: ${draft.title}`}
|
|
>
|
|
<h3 className="history-title">{draft.title}</h3>
|
|
<p className="history-date">{formatRelativeDate(draft.completedAt)}</p>
|
|
{draft.tags?.length > 0 && (
|
|
<div className="flex gap-2 mt-2">
|
|
{draft.tags.map(tag => (
|
|
<span key={tag} className="px-2 py-1 bg-slate-100 rounded text-xs">
|
|
{tag}
|
|
</span>
|
|
))}
|
|
</div>
|
|
)}
|
|
<p className="history-preview mt-2">{draft.content.slice(0, 100)}...</p>
|
|
</button>
|
|
);
|
|
}
|
|
```
|
|
|
|
**HistoryFeed Component:**
|
|
```typescript
|
|
// src/components/features/journal/HistoryFeed.tsx
|
|
export function HistoryFeed() {
|
|
const drafts = useHistoryStore(s => s.drafts);
|
|
const loading = useHistoryStore(s => s.loading);
|
|
const hasMore = useHistoryStore(s => s.hasMore);
|
|
const loadMore = useHistoryStore(s => s.loadMore);
|
|
const selectDraft = useHistoryStore(s => s.selectDraft);
|
|
|
|
useEffect(() => {
|
|
// Initial load
|
|
loadMore();
|
|
}, []);
|
|
|
|
if (drafts.length === 0 && !loading) {
|
|
return <EmptyHistoryState />;
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-3" onScroll={handleScroll}>
|
|
{drafts.map(draft => (
|
|
<HistoryCard
|
|
key={draft.id}
|
|
draft={draft}
|
|
onClick={selectDraft}
|
|
/>
|
|
))}
|
|
{loading && <LoadingSpinner />}
|
|
{!hasMore && drafts.length > 0 && (
|
|
<p className="text-center text-slate-500">You've reached the beginning</p>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
**HistoryDetailSheet Component:**
|
|
```typescript
|
|
// src/components/features/journal/HistoryDetailSheet.tsx
|
|
export function HistoryDetailSheet() {
|
|
const selectedDraft = useHistoryStore(s => s.selectedDraft);
|
|
const closeDetail = useHistoryStore(s => s.closeDetail);
|
|
|
|
if (!selectedDraft) return null;
|
|
|
|
return (
|
|
<Sheet open={!!selectedDraft} onOpenChange={closeDetail}>
|
|
<SheetContent>
|
|
<DraftContent draft={selectedDraft} />
|
|
<SheetFooter>
|
|
<CopyButton draftId={selectedDraft.id} />
|
|
</SheetFooter>
|
|
</SheetContent>
|
|
</Sheet>
|
|
);
|
|
}
|
|
```
|
|
|
|
### Home Page Integration
|
|
|
|
**Layout Strategy:**
|
|
```typescript
|
|
// src/app/(main)/page.tsx
|
|
export default function HomePage() {
|
|
return (
|
|
<div className="min-h-screen bg-slate-50">
|
|
<header className="p-4">
|
|
<h1 className="text-2xl font-serif">My Journal</h1>
|
|
</header>
|
|
|
|
<main className="pb-24">
|
|
<HistoryFeed />
|
|
</main>
|
|
|
|
<FloatingActionButton
|
|
href="/chat"
|
|
aria-label="Start new vent"
|
|
>
|
|
<PlusIcon />
|
|
</FloatingActionButton>
|
|
|
|
<HistoryDetailSheet />
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
**Navigation Flow:**
|
|
- Home page shows history feed by default
|
|
- FAB (Floating Action Button) navigates to `/chat` for new vent
|
|
- Tapping history card opens detail sheet (stays on home page)
|
|
- Detail sheet swipe-to-dismiss returns to feed
|
|
|
|
### Accessibility Requirements
|
|
|
|
**WCAG AA Compliance:**
|
|
- History cards must be buttons or links with proper semantics
|
|
- All interactive elements have 44px minimum touch targets
|
|
- Focus management: Detail sheet traps focus until dismissed
|
|
- Screen reader announces draft title and date
|
|
- Empty state is accessible and encouraging
|
|
|
|
**Keyboard Navigation:**
|
|
- Tab through history cards in order
|
|
- Enter/Space opens detail sheet
|
|
- Escape closes detail sheet
|
|
- Focus returns to triggering card after close
|
|
|
|
**Screen Reader Support:**
|
|
```typescript
|
|
<HistoryCard
|
|
draft={draft}
|
|
onClick={selectDraft}
|
|
aria-label={`${draft.title}, posted ${formatRelativeDate(draft.completedAt)}`}
|
|
/>
|
|
```
|
|
|
|
### Testing Requirements
|
|
|
|
**Unit Tests:**
|
|
- `DraftService.getCompletedDrafts()` returns completed drafts only
|
|
- `DraftService.getCompletedDrafts()` respects pagination (limit, offset)
|
|
- `formatRelativeDate()` returns correct relative dates
|
|
- `HistoryStore.loadMore()` appends drafts correctly
|
|
- `HistoryStore.loadMore()` sets hasMore to false when exhausted
|
|
|
|
**Integration Tests:**
|
|
- HistoryFeed loads drafts from IndexedDB on mount
|
|
- Tapping card opens detail sheet with correct draft
|
|
- LoadMore triggers when scrolling near bottom
|
|
- Copy button in detail sheet copies content
|
|
- Empty state shows when no completed drafts exist
|
|
|
|
**Edge Cases:**
|
|
- No completed drafts: Show empty state
|
|
- Single draft: Show draft, hide load more
|
|
- Exactly 20 drafts: Load more shows, loads empty second page
|
|
- 100+ drafts: Pagination works smoothly
|
|
- Draft with very long title: Truncate properly
|
|
- Draft with no tags: Render without tag section
|
|
- Draft with special characters in content: Escape properly
|
|
- Navigate away during load: Handle gracefully (cancel in-flight request)
|
|
|
|
**Performance Tests:**
|
|
- Initial page load <1.5s (NFR-02)
|
|
- Scroll performance: 60fps with 100 items in DOM
|
|
- Load more completes within 500ms
|
|
- Detail sheet opens within 100ms
|
|
|
|
**Accessibility Tests:**
|
|
- Keyboard navigation through entire history
|
|
- Screen reader announces all content
|
|
- Focus management in detail sheet
|
|
- Touch targets are 44px minimum
|
|
|
|
### Project Structure Notes
|
|
|
|
**Following Feature-First Lite Pattern:**
|
|
- New feature folder: `src/components/features/journal/`
|
|
- All history-related components in this folder
|
|
- Service extensions in existing `draft-service.ts`
|
|
- New store in `src/lib/store/history-store.ts`
|
|
|
|
**Alignment with Unified Project Structure:**
|
|
```
|
|
src/
|
|
components/
|
|
features/
|
|
journal/ # NEW: History feed
|
|
HistoryCard.tsx
|
|
HistoryFeed.tsx
|
|
HistoryDetailSheet.tsx
|
|
EmptyHistoryState.tsx
|
|
index.ts
|
|
draft/ # Existing: From Epic 2
|
|
chat/ # Existing: From Epic 1
|
|
lib/
|
|
store/
|
|
history-store.ts # NEW
|
|
utils/
|
|
date.ts # NEW
|
|
```
|
|
|
|
**No Conflicts Detected:**
|
|
- Journal is new feature, no overlap with existing code
|
|
- Reuses existing Sheet, Button, Card from ShadCN
|
|
- DraftService extension adds new methods (no breaking changes)
|
|
|
|
### Service Layer Extensions
|
|
|
|
**DraftService History Methods:**
|
|
```typescript
|
|
// src/lib/db/draft-service.ts
|
|
export class DraftService {
|
|
// Existing methods...
|
|
|
|
// NEW: Get completed drafts with pagination
|
|
static async getCompletedDrafts(options: {
|
|
limit?: number;
|
|
offset?: number;
|
|
}): Promise<Draft[]> {
|
|
const query = db.drafts
|
|
.where('status')
|
|
.equals('completed')
|
|
.reverse(); // Newest first
|
|
|
|
if (options.offset) query = query.offset(options.offset);
|
|
if (options.limit) query = query.limit(options.limit);
|
|
|
|
return await query.toArray();
|
|
}
|
|
|
|
// NEW: Count completed drafts
|
|
static async getCompletedCount(): Promise<number> {
|
|
return await db.drafts
|
|
.where('status')
|
|
.equals('completed')
|
|
.count();
|
|
}
|
|
|
|
// NEW: Get single draft by ID (for detail view)
|
|
static async getDraftById(id: number): Promise<Draft | undefined> {
|
|
return await db.drafts.get(id);
|
|
}
|
|
}
|
|
```
|
|
|
|
**HistoryStore Implementation:**
|
|
```typescript
|
|
// src/lib/store/history-store.ts
|
|
import { create } from 'zustand';
|
|
import { DraftService } from '../db/draft-service';
|
|
|
|
interface HistoryState {
|
|
drafts: Draft[];
|
|
loading: boolean;
|
|
hasMore: boolean;
|
|
error: string | null;
|
|
selectedDraft: Draft | null;
|
|
|
|
loadMore: () => Promise<void>;
|
|
refreshHistory: () => Promise<void>;
|
|
selectDraft: (draft: Draft) => void;
|
|
closeDetail: () => void;
|
|
}
|
|
|
|
export const useHistoryStore = create<HistoryState>((set, get) => ({
|
|
drafts: [],
|
|
loading: false,
|
|
hasMore: true,
|
|
error: null,
|
|
selectedDraft: null,
|
|
|
|
loadMore: async () => {
|
|
const { drafts, loading } = get();
|
|
if (loading) return;
|
|
|
|
set({ loading: true, error: null });
|
|
|
|
try {
|
|
const newDrafts = await DraftService.getCompletedDrafts({
|
|
limit: 20,
|
|
offset: drafts.length
|
|
});
|
|
|
|
set(state => ({
|
|
drafts: [...state.drafts, ...newDrafts],
|
|
hasMore: newDrafts.length === 20,
|
|
loading: false
|
|
}));
|
|
} catch (error) {
|
|
set({
|
|
error: 'Failed to load history',
|
|
loading: false
|
|
});
|
|
}
|
|
},
|
|
|
|
refreshHistory: async () => {
|
|
set({ drafts: [], hasMore: true });
|
|
await get().loadMore();
|
|
},
|
|
|
|
selectDraft: (draft: Draft) => set({ selectedDraft: draft }),
|
|
closeDetail: () => set({ selectedDraft: null }),
|
|
}));
|
|
```
|
|
|
|
### Mobile-First Responsive Design
|
|
|
|
**Breakpoint Strategy (from UX):**
|
|
- **Mobile (<768px):** Full width, bottom FAB, swipe gestures
|
|
- **Desktop (>=768px):** Centered container (600px max), extra whitespace
|
|
|
|
**Mobile Optimizations:**
|
|
- Pull-to-refresh gesture for reloading history
|
|
- Swipe-to-dismiss for detail sheet
|
|
- Haptic feedback on tap (optional enhancement)
|
|
- Touch targets minimum 44px
|
|
|
|
**Desktop Adaptations:**
|
|
```css
|
|
/* Center container on desktop */
|
|
@media (min-width: 768px) {
|
|
.history-feed-container {
|
|
max-width: 600px;
|
|
margin: 0 auto;
|
|
padding: 2rem 1rem;
|
|
}
|
|
}
|
|
```
|
|
|
|
### Performance Requirements
|
|
|
|
**NFR-02 Compliance (App Load Time):**
|
|
- Initial history load must complete within 1.5s
|
|
- Lazy load ensures fast initial render (20 drafts max)
|
|
- Subsequent loads happen on-demand
|
|
|
|
**State Updates:**
|
|
- HistoryStore uses immer middleware for efficient updates
|
|
- Atomic selectors prevent unnecessary re-renders
|
|
- Memoization on HistoryCard component
|
|
|
|
**Database Query Optimization:**
|
|
- Use IndexedDB indexes (completedAt is indexed, [status+completedAt] compound index used for sorting)
|
|
- Pagination limits query results
|
|
- Reverse sorting uses index efficiently
|
|
|
|
### Offline Behavior
|
|
|
|
**NFR-05 Compliance (Offline Access):**
|
|
- History feed must be viewable offline
|
|
- All data is local (IndexedDB)
|
|
- No network requests required
|
|
- Show "Offline" indicator if no network (optional)
|
|
|
|
**Offline Sync Queue:**
|
|
- Story 3.3 will implement full sync queue
|
|
- For this story, offline is default (no sync needed yet)
|
|
- Future: New drafts sync when online (Story 3.3)
|
|
|
|
### Security & Privacy Requirements
|
|
|
|
**NFR-03 & NFR-04 Compliance:**
|
|
- All history data stays on device (IndexedDB)
|
|
- No server API calls for history
|
|
- No analytics or tracking on history views
|
|
- User owns their complete journal locally
|
|
|
|
### References
|
|
|
|
**Epic Reference:**
|
|
- [Epic 3: "My Legacy" - History, Offline Sync & PWA Polish](file:///home/maximilienmao/Projects/Test01/_bmad-output/planning-artifacts/epics.md#epic-3-my-legacy---history-offline-sync--pwa-polish)
|
|
- [Story 3.1: History Feed UI](file:///home/maximilienmao/Projects/Test01/_bmad-output/planning-artifacts/epics.md#story-31-history-feed-ui)
|
|
- FR-06: "Users can view a chronological feed of past 'Enlightenments'"
|
|
- FR-14: "Users can export their entire history as a JSON/Markdown file" (future: Story 3.4)
|
|
|
|
**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: History Feed Pattern](file:///home/maximilienmao/Projects/Test01/_bmad-output/planning-artifacts/ux-design-specification.md#3-history--reflection)
|
|
- [UX: Empty State Design](file:///home/maximilienmao/Projects/Test01/_bmad-output/planning-artifacts/ux-design-specification.md#responsive-strategy)
|
|
- [UX: Typography System](file:///home/maximilienmao/Projects/Test01/_bmad-output/planning-artifacts/ux-design-specification.md#typography-system)
|
|
|
|
**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) - Sheet pattern to reuse
|
|
- [Story 2.4: Export & Copy Actions](file:///home/maximilienmao/Projects/Test01/_bmad-output/implementation-artifacts/2-4-export-copy-actions.md) - Copy functionality to reuse
|
|
|
|
**Epic Retrospectives:**
|
|
- [Epic 1 Retrospective](file:///home/maximilienmao/Projects/Test01/_bmad-output/implementation-artifacts/epic-1-retro-2026-01-22.md) - Atomic selector lessons
|
|
|
|
## 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/bf525d3f-3c0e-417a-81e7-dad92fec28ad/scratchpad`
|
|
|
|
### Completion Notes List
|
|
|
|
**Story Analysis Completed:**
|
|
- Extracted story requirements from Epic 3, Story 3.1
|
|
- Analyzed previous Epics 1 and 2 for established patterns
|
|
- Reviewed architecture for compliance requirements (Logic Sandwich, State Management, Local-First)
|
|
- Reviewed UX specification for history feed and empty state patterns
|
|
- Identified all files to create and modify
|
|
|
|
**Implementation Context Summary:**
|
|
|
|
**Story Purpose:**
|
|
This story implements the **"History Journal"** - the persistent feed where users can revisit all their past "Enlightenment" artifacts. This transforms the app from a single-use tool into a **growth journal**, enabling reflection on past insights and building the "My Legacy" emotional connection.
|
|
|
|
**Key Technical Decisions:**
|
|
1. **New HistoryStore:** Separate Zustand store for history state (not chat store)
|
|
2. **Pagination:** Lazy load 20 drafts at a time for performance
|
|
3. **Reusable Components:** Leverage Sheet from Story 2.2, Copy from Story 2.4
|
|
4. **Empty State:** Encouraging entry point for new users
|
|
5. **Relative Dates:** Human-readable timestamps ("Today", "Yesterday")
|
|
6. **Mobile-First:** Full width on mobile, centered container on desktop
|
|
|
|
**Dependencies:**
|
|
- No new external dependencies required
|
|
- Uses existing ShadCN components (Card, Sheet, Button)
|
|
- Reuses clipboard utility from Story 2.4
|
|
- May need pull-to-refresh library (optional enhancement)
|
|
|
|
**Integration Points:**
|
|
- Home page shows history feed by default
|
|
- FAB navigates to `/chat` for new venting session
|
|
- Tapping history card opens detail sheet (reuses DraftViewSheet)
|
|
- Story 3.2 will add delete actions to detail sheet
|
|
- Story 3.3 will add offline sync queue
|
|
|
|
**Files to Create:**
|
|
- `src/lib/store/history-store.ts` - History feed state management
|
|
- `src/components/features/journal/HistoryCard.tsx` - Individual history item
|
|
- `src/components/features/journal/HistoryFeed.tsx` - List with lazy loading
|
|
- `src/components/features/journal/HistoryDetailSheet.tsx` - Full draft view
|
|
- `src/components/features/journal/EmptyHistoryState.tsx` - Empty state
|
|
- `src/lib/utils/date.ts` - Date formatting utilities
|
|
- `src/components/features/journal/index.ts` - Feature exports
|
|
|
|
**Files to Modify:**
|
|
- `src/lib/db/draft-service.ts` - Add getCompletedDrafts(), getCompletedCount(), getDraftById()
|
|
- `src/app/(main)/page.tsx` - Integrate HistoryFeed into home page
|
|
|
|
**Testing Strategy:**
|
|
- Unit tests for DraftService history methods
|
|
- Unit tests for date formatting utility
|
|
- Integration tests for feed loading and pagination
|
|
- Edge case tests (empty, single, large history)
|
|
- Accessibility tests (keyboard, screen reader)
|
|
|
|
**History Feed Pattern:**
|
|
```
|
|
Home Page Load
|
|
↓
|
|
HistoryStore.loadMore() - Initial 20 drafts
|
|
↓
|
|
DraftService.getCompletedDrafts({ limit: 20, offset: 0 })
|
|
↓
|
|
Query db.drafts.where('status').equals('completed').reverse()
|
|
↓
|
|
HistoryFeed renders cards for each draft
|
|
↓
|
|
User scrolls near bottom
|
|
↓
|
|
Trigger loadMore() - Append next 20 drafts
|
|
↓
|
|
User taps card
|
|
↓
|
|
HistoryStore.selectDraft(draft)
|
|
↓
|
|
HistoryDetailSheet opens with full content
|
|
↓
|
|
User taps Copy or swipes to dismiss
|
|
```
|
|
|
|
**User Experience Flow:**
|
|
- First-time user sees empty state with CTA to start first vent
|
|
- Returning user sees their journal of completed posts
|
|
- Tapping any post opens the full "Enlightenment" artifact
|
|
- Copy button allows quick export (from Story 2.4)
|
|
- Emotional payoff: Seeing past wins reinforces value of app
|
|
|
|
**Lessons from Epic 1 Retrospective Applied:**
|
|
- **Atomic Selectors:** All HistoryStore access uses `useHistoryStore(s => s.field)`
|
|
- **Performance:** Pagination prevents rendering 100+ items at once
|
|
- **Testing:** Comprehensive unit and integration tests
|
|
- **State Management:** Separate store avoids chat store bloat
|
|
|
|
### File List
|
|
|
|
**New Files to Create:**
|
|
- `src/lib/store/history-store.ts` - History feed state management
|
|
- `src/components/features/journal/HistoryCard.tsx` - Individual history item
|
|
- `src/components/features/journal/HistoryFeed.tsx` - List component
|
|
- `src/components/features/journal/HistoryDetailSheet.tsx` - Detail view sheet
|
|
- `src/components/features/journal/EmptyHistoryState.tsx` - Empty state
|
|
- `src/components/features/journal/index.ts` - Feature exports
|
|
- `src/lib/utils/date.ts` - Date formatting utilities
|
|
- `src/lib/utils/date.test.ts` - Date utility tests
|
|
- `src/lib/store/history-store.test.ts` - History store tests
|
|
- `src/integration/history-feed.test.ts` - End-to-end history tests
|
|
- `src/components/features/journal/HistoryCard.test.tsx` - HistoryCard tests
|
|
- `src/components/features/journal/HistoryFeed.test.tsx` - HistoryFeed tests
|
|
|
|
**Files to Modify:**
|
|
- `src/lib/db/draft-service.ts` - Add history query methods
|
|
- `src/lib/db/draft-service.test.ts` - Add history method tests
|
|
- `src/app/(main)/page.tsx` - Integrate history feed
|