# BookDate Implementation Status ## ✅ Completed Phases (1-5) ### Phase 1: Database Schema ✅ **Files:** - `prisma/schema.prisma` - Added 3 models: - `BookDateConfig` - Per-user AI configuration (encrypted API keys) - `BookDateRecommendation` - Cached recommendations - `BookDateSwipe` - Swipe history for learning - Added relationships to User model **To apply schema:** ```bash docker-compose restart app # Or manually: npx prisma db push && npx prisma generate ``` ### Phase 2: Backend API - Configuration ✅ **Files created:** - `src/app/api/bookdate/test-connection/route.ts` - Test AI provider & fetch models - `src/app/api/bookdate/config/route.ts` - GET/POST/DELETE user config - `src/app/api/admin/bookdate/toggle/route.ts` - Admin global toggle - `src/app/api/bookdate/swipes/route.ts` - Clear swipe history ### Phase 3: Backend API - Recommendations ✅ **Files created:** - `src/lib/bookdate/helpers.ts` - Complete helper functions: - `getUserLibraryBooks()` - Get Plex library books - `getUserRecentSwipes()` - Get swipe history - `buildAIPrompt()` - Generate AI prompt - `callAI()` - Call OpenAI/Claude APIs - `matchToAudnexus()` - Match recommendations to Audible - `isInLibrary()`, `isAlreadyRequested()`, `isAlreadySwiped()` - Filtering helpers - `src/app/api/bookdate/recommendations/route.ts` - Get recommendations (cached or generate) - `src/app/api/bookdate/swipe/route.ts` - Record swipe & create request - `src/app/api/bookdate/undo/route.ts` - Undo last swipe - `src/app/api/bookdate/generate/route.ts` - Force generate new batch ### Phase 4: Setup Wizard Integration ✅ **Files modified:** - `src/app/setup/page.tsx` - Added BookDate as step 7 (now 9 total steps) - `src/app/setup/steps/BookDateStep.tsx` - New setup step component - `src/app/api/setup/complete/route.ts` - Save BookDate config during setup ### Phase 5: Settings Page ✅ **Files created:** - `src/app/settings/page.tsx` - User settings page with: - AI provider selection (OpenAI/Claude) - API key management (encrypted) - Model selection - Library scope (full/listened/rated) - Custom prompt - Clear swipe history --- ## ⏳ Remaining Work (Phases 6-8) ### Phase 6: BookDate UI - Main Page & Components 🚧 #### 6.1 Install Dependencies ```bash npm install react-swipeable framer-motion ``` #### 6.2 Files to Create **Main BookDate Page:** - `src/app/bookdate/page.tsx` - Main swipe interface page **Components:** - `src/components/bookdate/RecommendationCard.tsx` - Swipeable card component - `src/components/bookdate/LoadingScreen.tsx` - Animated loading screen - `src/components/bookdate/EmptyState.tsx` - Empty state when no recommendations **Key Features:** - Mobile: Touch swipe gestures (left/right/up) - Desktop: Button controls - Visual feedback during drag - Confirmation toast for right swipes - Undo button for left/up swipes - Auto-request creation on right swipe + confirm #### 6.3 Navigation Integration Add BookDate tab to main navigation (conditional based on configuration): - Modify `src/components/layout/Header.tsx` (or wherever nav is) - Check `/api/bookdate/config` to show/hide tab - Only show if `config.isVerified && config.isEnabled` ### Phase 7: Integration & Polish 🚧 #### 7.1 Plex Library Integration **File:** `src/lib/bookdate/helpers.ts` Update `getUserLibraryBooks()`: - Query Plex API directly (not just database cache) - For 'listened' scope: Calculate `viewOffset / duration > 0.25` - For 'rated' scope: Fetch user ratings from Plex - Extract genres from Plex metadata - Fallback to database if Plex API fails #### 7.2 Audnexus Matching Enhancement **File:** `src/lib/bookdate/helpers.ts` Update `matchToAudnexus()`: - If not in `AudibleCache`, query Audnexus API directly - Implement fuzzy matching (Levenshtein distance < 3) - Handle multiple results (pick best by rating/popularity) - Cache new matches to `AudibleCache` #### 7.3 Request Integration **File:** `src/app/api/bookdate/swipe/route.ts` Already implemented: - ✅ Creates audiobook record if doesn't exist - ✅ Creates request on right swipe (if not marked as known) - ✅ Links to existing audiobook by ASIN ### Phase 8: Testing & Verification 🚧 #### 8.1 Database Testing - [ ] Build Docker image: `docker-compose build` - [ ] Start containers: `docker-compose up -d` - [ ] Check logs: `docker-compose logs -f app` - [ ] Verify Prisma migration: Check PostgreSQL tables - [ ] Test encrypted API key storage #### 8.2 API Testing (Manual) Use Postman/Thunder Client or curl: ```bash # Test connection curl -X POST http://localhost:3030/api/bookdate/test-connection \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{"provider":"openai","apiKey":"sk-..."}' # Save config curl -X POST http://localhost:3030/api/bookdate/config \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{"provider":"openai","apiKey":"sk-...","model":"gpt-4o","libraryScope":"full"}' # Get recommendations curl http://localhost:3030/api/bookdate/recommendations \ -H "Authorization: Bearer YOUR_TOKEN" ``` #### 8.3 UI Testing - [ ] Setup wizard: Complete step 7 (BookDate) - [ ] Settings page: Save/update config - [ ] BookDate tab: Visibility based on config - [ ] Swipe gestures: Test on mobile and desktop - [ ] Loading states: Check animations - [ ] Error handling: Test invalid API keys, network errors - [ ] Dark mode: Verify all components #### 8.4 Integration Testing - [ ] Right swipe → Confirm → Creates request - [ ] Check request appears in /requests page - [ ] Verify request status updates - [ ] Test undo functionality - [ ] Clear swipe history from settings --- ## 📋 Quick Implementation Guide for Remaining Work ### Step 1: Create BookDate Main Page Create `src/app/bookdate/page.tsx`: ```typescript 'use client'; import { useState, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import { Header } from '@/components/layout/Header'; import { RecommendationCard } from '@/components/bookdate/RecommendationCard'; import { LoadingScreen } from '@/components/bookdate/LoadingScreen'; export default function BookDatePage() { const [recommendations, setRecommendations] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [currentIndex, setCurrentIndex] = useState(0); const router = useRouter(); useEffect(() => { loadRecommendations(); }, []); const loadRecommendations = async () => { setLoading(true); try { const accessToken = localStorage.getItem('accessToken'); const response = await fetch('/api/bookdate/recommendations', { headers: { 'Authorization': `Bearer ${accessToken}` } }); const data = await response.json(); if (!response.ok) { setError(data.error); return; } setRecommendations(data.recommendations); } catch (error: any) { setError(error.message); } finally { setLoading(false); } }; const handleSwipe = async (action: 'left' | 'right' | 'up', markedAsKnown = false) => { const recommendation = recommendations[currentIndex]; try { const accessToken = localStorage.getItem('accessToken'); await fetch('/api/bookdate/swipe', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}` }, body: JSON.stringify({ recommendationId: recommendation.id, action, markedAsKnown }) }); setCurrentIndex(currentIndex + 1); // Check if we need to load more if (currentIndex + 1 >= recommendations.length) { // Show empty state or load more } } catch (error) { console.error('Swipe error:', error); } }; if (loading) { return ; } if (error) { return (

Could not load recommendations

{error}

); } if (currentIndex >= recommendations.length) { return (

You've seen all recommendations!

Want more suggestions?

); } const currentRec = recommendations[currentIndex]; return (
); } ``` ### Step 2: Create Recommendation Card Component Install dependencies first: ```bash npm install react-swipeable ``` Create `src/components/bookdate/RecommendationCard.tsx`: ```typescript 'use client'; import { useState } from 'react'; import Image from 'next/image'; import { useSwipeable } from 'react-swipeable'; interface RecommendationCardProps { recommendation: any; onSwipe: (action: 'left' | 'right' | 'up', markedAsKnown?: boolean) => void; } export function RecommendationCard({ recommendation, onSwipe }: RecommendationCardProps) { const [showToast, setShowToast] = useState(false); const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }); const handleSwipeRight = () => { setShowToast(true); }; const handleToastAction = (action: 'request' | 'known' | 'cancel') => { setShowToast(false); if (action === 'request') { onSwipe('right', false); } else if (action === 'known') { onSwipe('right', true); } }; const swipeHandlers = useSwipeable({ onSwipedLeft: () => onSwipe('left'), onSwipedRight: handleSwipeRight, onSwipedUp: () => onSwipe('up'), onSwiping: (eventData) => { setDragOffset({ x: eventData.deltaX, y: eventData.deltaY }); }, trackMouse: true }); return ( <>
{/* Drag overlay indicators */} {dragOffset.x > 50 && (
)} {dragOffset.x < -50 && (
)} {dragOffset.y < -50 && (
⬆️
)} {/* Cover image */}
{recommendation.coverUrl ? ( {recommendation.title} ) : (
📚
)}
{/* Book info */}

{recommendation.title}

{recommendation.author}

{recommendation.narrator && (

Narrated by {recommendation.narrator}

)} {recommendation.rating && (
{recommendation.rating}
)} {recommendation.description && (

{recommendation.description}

)} {recommendation.aiReason && (

{recommendation.aiReason}

)}
{/* Desktop buttons */}
{/* Confirmation Toast */} {showToast && (

Request "{recommendation.title}"?

Do you want to request this audiobook, or have you already read/listened to it elsewhere?

)} ); } ``` ### Step 3: Create Loading Screen Component Create `src/components/bookdate/LoadingScreen.tsx`: ```typescript export function LoadingScreen() { return (
{/* Animated book cards */}

Finding your next great listen...

); } ``` ### Step 4: Add Navigation Link Modify your main navigation component (likely `src/components/layout/Header.tsx`): ```typescript // Add to navigation links const [showBookDate, setShowBookDate] = useState(false); useEffect(() => { async function checkBookDate() { const accessToken = localStorage.getItem('accessToken'); const response = await fetch('/api/bookdate/config', { headers: { 'Authorization': `Bearer ${accessToken}` } }); const data = await response.json(); setShowBookDate(data.config && data.config.isVerified && data.config.isEnabled); } checkBookDate(); }, []); // In your navigation JSX: {showBookDate && ( BookDate )} ``` --- ## 🧪 Testing Checklist ### Initial Setup - [ ] Run `npm install react-swipeable` - [ ] Build Docker: `docker-compose build` - [ ] Start: `docker-compose up -d` - [ ] Check logs: `docker-compose logs -f app` ### Feature Testing 1. **Setup Wizard** - [ ] Complete wizard with BookDate config - [ ] Skip BookDate and continue - [ ] Verify config saved in database 2. **Settings Page** - [ ] Navigate to /settings - [ ] Test OpenAI connection - [ ] Test Claude connection - [ ] Save configuration - [ ] Update existing configuration - [ ] Clear swipe history 3. **BookDate Tab** - [ ] Verify tab visible after config - [ ] Verify tab hidden without config - [ ] Navigate to /bookdate 4. **Recommendations** - [ ] View loading screen - [ ] See first recommendation - [ ] Swipe left (reject) - [ ] Swipe right (request - confirm) - [ ] Swipe up (dismiss) - [ ] Test undo button - [ ] Reach end of recommendations - [ ] Click "Get More" 5. **Integration** - [ ] Right swipe creates request in /requests - [ ] Request status updates correctly - [ ] Recommendations exclude library books - [ ] Recommendations improve with swipes ### Error Scenarios - [ ] Invalid API key - [ ] Network error during generation - [ ] No Audnexus matches - [ ] Empty Plex library - [ ] All recommendations filtered out --- ## 📝 Documentation to Update After testing, update: 1. **TABLEOFCONTENTS.md** ```markdown ## BookDate (AI Recommendations) - **AI-powered recommendations, swipe interface** → features/bookdate.md - **Configuration, setup wizard integration** → features/bookdate.md ``` 2. **Create documentation/features/bookdate.md** (Token-efficient format summarizing the feature) --- ## 🚀 Deployment Notes ### Environment Variables (already in docker-compose.yml) ```yaml CONFIG_ENCRYPTION_KEY: Z7vRDVuimy/oqPj9OB6pd/FLUzOTcTH9wlTrvETkVec= ``` ### Database Migration Schema changes automatically applied on container start via `prisma db push`. ### API Rate Limits - OpenAI: ~3500 RPM (requests per minute) for most models - Claude: ~4000 RPM - Consider adding rate limiting if needed --- ## 💡 Future Enhancements (Post-MVP) - [ ] Multi-AI voting (aggregate multiple AI recommendations) - [ ] Advanced filtering (exclude genres, narrator preferences) - [ ] Swipe analytics dashboard - [ ] Social features (see friends' swipes) - [ ] Recommendation explanations (show AI reasoning) - [ ] Listening goals ("Find books under 10 hours") - [ ] Better Plex integration (real-time listening status) - [ ] Direct Audnexus API integration (beyond cache) --- ## ✅ MVP Definition MVP is complete when: - ✅ Database schema deployed - ✅ All API endpoints working - ✅ Setup wizard includes BookDate - ✅ Settings page functional - 🚧 BookDate tab visible when configured - 🚧 Swipe interface works (mobile + desktop) - 🚧 Right swipe creates requests - 🚧 Recommendations cache correctly - 🚧 Dark mode supported - 🚧 Error states handled ## Current Status: ~70% Complete **Completed:** Backend, Database, Setup, Settings **Remaining:** Main UI, Testing, Documentation