import { useState, useEffect, useRef } from 'react'; import { Search, BookOpen, Loader2, X, Sparkles, LayoutGrid, CheckCircle, Moon, Sun, Info, Bookmark, ArrowLeft, Users } from 'lucide-react'; import { api } from './api'; import { Loader } from './Loader'; import { BookCover } from './BookCover'; import { BookCard } from './BookCard'; import { DEMO_PERSONAS } from './data/personas'; import type { RecommendationResult, BookCluster } from './types'; function App() { const [query, setQuery] = useState(''); const [results, setResults] = useState([]); const [loading, setLoading] = useState(false); const [longLoading, setLongLoading] = useState(false); const [hasSearched, setHasSearched] = useState(false); // Modal State const [selectedResult, setSelectedResult] = useState(null); const [historyStack, setHistoryStack] = useState([]); const [explanation, setExplanation] = useState<{ summary: string; details: Record } | null>(null); const [explaining, setExplaining] = useState(false); const [showAbout, setShowAbout] = useState(false); const modalRef = useRef(null); // Related Books State const [relatedBooks, setRelatedBooks] = useState([]); const [loadingRelated, setLoadingRelated] = useState(false); // Dynamic Clusters State const [clusters, setClusters] = useState([]); const [loadingClusters, setLoadingClusters] = useState(true); // Toast State const [toast, setToast] = useState<{ message: string; visible: boolean } | null>(null); // Settings / Theme State const [darkMode, setDarkMode] = useState(false); // Personalization / History State const [readHistory, setReadHistory] = useState(() => { try { return JSON.parse(localStorage.getItem('bookfinder_read_history') || '[]'); } catch { return []; } }); useEffect(() => { if (selectedResult && modalRef.current) { modalRef.current.scrollTop = 0; } }, [selectedResult]); useEffect(() => { const savedTheme = localStorage.getItem('bookfinder_theme_mode'); if (savedTheme) { setDarkMode(savedTheme === 'dark'); } else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { setDarkMode(true); } }, []); useEffect(() => { localStorage.setItem('bookfinder_theme_mode', darkMode ? 'dark' : 'light'); if (darkMode) { document.documentElement.classList.add('dark'); } else { document.documentElement.classList.remove('dark'); } }, [darkMode]); useEffect(() => { localStorage.setItem('bookfinder_read_history', JSON.stringify(readHistory)); }, [readHistory]); const toggleReadBook = (title: string) => { setReadHistory(prev => { if (prev.includes(title)) { showToast("Removed from library"); return prev.filter(t => t !== title); } else { showToast("Added to library"); return [...prev, title]; } }); triggerHaptic(); }; const handlePersonalize = async () => { if (readHistory.length === 0) { showToast("Mark some books as read first!"); return; } triggerHaptic(); setLoading(true); setHasSearched(true); setResults([]); setQuery("✨ Selected for you"); try { const data = await api.recommendPersonalized(readHistory); setResults(data); } catch (err) { console.error(err); showToast("Failed to personalize. Try again."); } finally { setLoading(false); } }; const handlePersonaSelect = (persona: typeof DEMO_PERSONAS[0]) => { triggerHaptic(); setReadHistory(persona.history); showToast(`Switched to ${persona.name} mode`); setLoading(true); setHasSearched(true); setResults([]); setQuery(`👤 Demo: ${persona.name}`); api.recommendPersonalized(persona.history) .then(setResults) .catch(err => { console.error(err); showToast("Failed to load persona."); }) .finally(() => setLoading(false)); }; const toggleTheme = async (e: React.MouseEvent) => { const isDark = !darkMode; if (!(document as any).startViewTransition) { setDarkMode(isDark); return; } const x = e.clientX; const y = e.clientY; const endRadius = Math.hypot(Math.max(x, window.innerWidth - x), Math.max(y, window.innerHeight - y)); const transition = (document as any).startViewTransition(() => setDarkMode(isDark)); await transition.ready; const clipPath = [`circle(0px at ${x}px ${y}px)`, `circle(${endRadius}px at ${x}px ${y}px)`]; document.documentElement.animate( { clipPath: isDark ? clipPath : [...clipPath].reverse() }, { duration: 500, easing: 'ease-in-out', pseudoElement: isDark ? '::view-transition-new(root)' : '::view-transition-old(root)' } ); }; const triggerHaptic = () => { if (typeof navigator !== 'undefined' && navigator.vibrate) { navigator.vibrate(10); } }; useEffect(() => { const fetchClusters = async () => { try { const data = await api.getClusters(); setClusters(data.slice(0, 6)); } catch (err) { console.error("Failed to fetch clusters", err); } finally { setLoadingClusters(false); } }; fetchClusters(); }, []); const showToast = (message: string) => { setToast({ message, visible: true }); setTimeout(() => setToast(null), 3000); }; const handleSearch = async (e?: React.FormEvent) => { if (e) e.preventDefault(); if (!query.trim()) return; triggerHaptic(); setLoading(true); setLongLoading(false); setHasSearched(true); setResults([]); const timer = setTimeout(() => setLongLoading(true), 3000); try { const data = await api.recommendByQuery(query); setResults(data); } catch (err) { console.error(err); } finally { clearTimeout(timer); setLoading(false); setLongLoading(false); } }; const handleQuickSearch = (text: string) => { setQuery(text); triggerHaptic(); setLoading(true); setLongLoading(false); setHasSearched(true); setResults([]); const timer = setTimeout(() => setLongLoading(true), 3000); api.recommendByQuery(text).then(setResults).catch(console.error).finally(() => { clearTimeout(timer); setLoading(false); setLongLoading(false); }); }; const handleFeedback = async (bookId: string, type: 'positive' | 'negative') => { triggerHaptic(); try { await api.submitFeedback(query, bookId, type); showToast(type === 'positive' ? "Thanks! We'll show more like this." : "Thanks! We'll tune our results."); } catch (err) { console.error(err); } }; const handleReadMore = async (result: RecommendationResult, isDrillingDown = false) => { triggerHaptic(); if (isDrillingDown && selectedResult) { setHistoryStack(prev => [...prev, selectedResult]); } else if (!isDrillingDown) { setHistoryStack([]); } setSelectedResult(result); setExplaining(true); setExplanation(null); setRelatedBooks([]); setLoadingRelated(true); try { const [expl, related] = await Promise.allSettled([ api.explainRecommendation(query || result.book.title, result.book, result.similarity_score), api.getRelatedBooks(result.book.title) ]); if (expl.status === 'fulfilled') setExplanation(expl.value); if (related.status === 'fulfilled') setRelatedBooks(related.value); } catch (err) { console.error("Failed to fetch details", err); } finally { setExplaining(false); setLoadingRelated(false); } }; const handleBack = () => { triggerHaptic(); if (historyStack.length > 0) { const prev = historyStack[historyStack.length - 1]; setHistoryStack(curr => curr.slice(0, -1)); setSelectedResult(prev); setRelatedBooks([]); setExplanation(null); setLoadingRelated(true); setExplaining(true); Promise.allSettled([ api.explainRecommendation(query || prev.book.title, prev.book, prev.similarity_score), api.getRelatedBooks(prev.book.title) ]).then(([expl, related]) => { if (expl.status === 'fulfilled') setExplanation(expl.value); if (related.status === 'fulfilled') setRelatedBooks(related.value); setExplaining(false); setLoadingRelated(false); }); } }; const closeModal = () => { setSelectedResult(null); setHistoryStack([]); setExplanation(null); setRelatedBooks([]); }; const handleClusterClick = (clusterName: string) => { triggerHaptic(); setQuery(clusterName); setLoading(true); setHasSearched(true); setResults([]); api.recommendByQuery(clusterName).then(setResults).catch(console.error).finally(() => setLoading(false)); }; return (
{toast && (
{toast.message}
)}
setActiveView('search')}>
Serendipity
{!hasSearched && (

Find your next
obsession.

Describe the vibe, plot, or character you're looking for.

)}
setQuery(e.target.value)} placeholder="e.g. A sci-fi thriller about AI consciousness..." className="relative w-full bg-white dark:bg-zinc-900 border-2 border-zinc-200 dark:border-zinc-800 focus:border-indigo-500 dark:focus:border-indigo-500 rounded-full py-4 pl-6 pr-24 text-lg outline-none transition-all placeholder:text-zinc-400 dark:placeholder:text-zinc-600 shadow-xl shadow-zinc-200/50 dark:shadow-black/50 text-zinc-900 dark:text-white" autoFocus /> {query && ( )}
{loading && longLoading && (

Waking up... this might take a moment 😴

)} {!hasSearched && !loading && (
{[ "Cyberpunk noir detective", "Cozy cottagecore mystery", "Space opera with politics", "Psychological horror 1920s" ].map((prompt) => ( ))}
{/* Demo Personas */}
Try a Demo Persona
{DEMO_PERSONAS.map(persona => ( ))}
{/* Personalization Trigger */} {readHistory.length > 0 && ( )}
)} {!hasSearched && (
Curated Collections
{loadingClusters ? (
{[1,2,3,4,5,6].map(i => (
))}
) : (
{clusters.map(cluster => ( ))}
)}
)}
{loading && results.length === 0 ? (
) : ( results.map((result) => ( handleReadMore(result)} onFeedback={handleFeedback} /> )) )}
{hasSearched && results.length === 0 && !loading && (
No books found matching that description.
)}
{selectedResult && (
{historyStack.length > 0 && ( )}

{selectedResult.book.title}

{selectedResult.book.title}

by {selectedResult.book.authors.join(', ')}

{selectedResult.book.description}

Why this match?
{explaining ? (
Reading your mind...
) : explanation ? (

{explanation.summary}

{Object.entries(explanation.details).map(([key, val]) => ( {key} {val}% ))}
) : (

Could not generate explanation.

)}

You might also like

{loadingRelated ? (
{[1, 2, 3].map(i => (
))}
) : (
{relatedBooks.map(rel => (
handleReadMore(rel)} className="group cursor-pointer active:scale-95 transition-transform">

{rel.book.title}

{rel.book.authors[0]}

))}
)}
)} {showAbout && (
setShowAbout(false)} />

About Serendipity

Serendipity is an intelligent book discovery interface powered by the DeepShelf Engine.

Unlike traditional keyword search, DeepShelf uses semantic vector embeddings to understand the meaning and feeling of your request. It connects you with books that match your specific wavelength, even if they don't share a single keyword.

How it works:

  • Semantic Search: Understands the meaning and context of your queries.
  • Vector Embeddings: DeepShelf maps books and queries into a high-dimensional vector space.
  • Neural Retrieval: Finds the nearest neighbors to your thought in the library of 100,000+ books.

Describe your ideal read and let Serendipity uncover your next literary obsession!

)}
); } export default App;