From 3c27ecc81b4f05be45b6b2b2843ea082cb90f807 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 19 Jun 2025 16:41:10 +0000 Subject: [PATCH] feat: Transform to AI Book Summary Generator This commit overhauls the application from a general deep research tool into a specialized AI-powered book summary generator. Key changes include: - Updated UI to accept "Book Name" and optional "Author" inputs. - Modified backend to generate multiple targeted search queries for gathering book-related content (summaries, reviews, analyses) from at least 20 sources using Firecrawl. - Engineered a new detailed prompt for the OpenAI LLM (GPT-4o), instructing it to synthesize a comprehensive, long-form (30-minute read) book summary. The prompt includes the "Atomic Habits" summary as a structural and stylistic example. - Created `lib/prompt-examples.ts` to store the example summary. - Adjusted `lib/config.ts` to support the new search strategy and content requirements (e.g., number of queries, sources per query). - Enhanced Markdown rendering CSS in `app/globals.css` for better readability of long-form content, including improved styles for headings, blockquotes, and other elements. - Adapted `lib/langgraph-search-engine.ts` significantly: - Changed input state and search method signatures. - Revised the 'plan' node for book-specific query generation. - Updated methods for analyzing book requests, scoring content relevance, and summarizing individual sources for book context. - Implemented `generateStreamingBookSummary` with the new prompt. - Replaced "follow-up questions" with "key themes". - Updated `app/page.tsx` with new heading/description. --- app/chat.tsx | 434 +++++------ app/globals.css | 67 ++ app/search.tsx | 12 +- lib/config.ts | 45 +- lib/langgraph-search-engine.ts | 1338 +++++++++----------------------- lib/prompt-examples.ts | 524 +++++++++++++ 6 files changed, 1193 insertions(+), 1227 deletions(-) create mode 100644 lib/prompt-examples.ts diff --git a/app/chat.tsx b/app/chat.tsx index 0dbd077..082317f 100644 --- a/app/chat.tsx +++ b/app/chat.tsx @@ -20,13 +20,6 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { toast } from "sonner"; -const SUGGESTED_QUERIES = [ - "Who are the founders of Firecrawl?", - "When did NVIDIA release the RTX 4080 Super?", - "Compare the latest iPhone 16 and Samsung Galaxy S25", - "Compare Claude 4 to OpenAI's o3" -]; - // Helper component for sources list function SourcesList({ sources }: { sources: Source[] }) { const [showSourcesPanel, setShowSourcesPanel] = useState(false); @@ -187,24 +180,23 @@ export function Chat() { id: string; role: 'user' | 'assistant'; content: string | React.ReactNode; - isSearch?: boolean; - searchResults?: string; // Store search results for context + isSearch?: boolean; // Indicates if this message is part of the search process display + searchResults?: string; // Store raw search results string for context if needed + bookName?: string; // To display what book was searched for + author?: string; }>>([]); - const [input, setInput] = useState(''); + const [bookNameInput, setBookNameInput] = useState(''); + const [authorInput, setAuthorInput] = useState(''); const [isSearching, setIsSearching] = useState(false); - const [showSuggestions, setShowSuggestions] = useState(false); - const [hasShownSuggestions, setHasShownSuggestions] = useState(false); + // const [showSuggestions, setShowSuggestions] = useState(false); // Suggestions not used for book summary + // const [hasShownSuggestions, setHasShownSuggestions] = useState(false); const [firecrawlApiKey, setFirecrawlApiKey] = useState(''); const [hasApiKey, setHasApiKey] = useState(false); const [showApiKeyModal, setShowApiKeyModal] = useState(false); - const [, setIsCheckingEnv] = useState(true); - const [pendingQuery, setPendingQuery] = useState(''); + const [, setIsCheckingEnv] = useState(true); // To manage env check state + const [pendingSearch, setPendingSearch] = useState<{ bookName: string; author?: string } | null>(null); const messagesContainerRef = useRef(null); - const handleSelectSuggestion = (suggestion: string) => { - setInput(suggestion); - setShowSuggestions(false); - }; // Check for environment variables on mount useEffect(() => { @@ -215,13 +207,11 @@ export function Chat() { const data = await response.json(); if (data.environmentStatus) { - // Only check for Firecrawl API key since we can pass it from frontend - // OpenAI and Anthropic keys must be in environment setHasApiKey(data.environmentStatus.FIRECRAWL_API_KEY); } } catch (error) { console.error('Failed to check environment:', error); - setHasApiKey(false); + setHasApiKey(false); // Assume no key if check fails } finally { setIsCheckingEnv(false); } @@ -237,80 +227,69 @@ export function Chat() { } }, [messages]); - const saveApiKey = () => { + const saveApiKeyAndSearch = () => { if (firecrawlApiKey.trim()) { setHasApiKey(true); setShowApiKeyModal(false); - toast.success('API key saved! Starting your search...'); + toast.success('API key saved! Starting your book summary generation...'); - // Continue with the pending query - if (pendingQuery) { - performSearch(pendingQuery); - setPendingQuery(''); + if (pendingSearch) { + performSearch(pendingSearch.bookName, pendingSearch.author); + setPendingSearch(null); } } }; - // Listen for follow-up question events + // Listen for follow-up question (key theme) events useEffect(() => { - const handleFollowUpQuestion = async (event: Event) => { + const handleFollowUp = async (event: Event) => { const customEvent = event as CustomEvent; - const question = customEvent.detail.question; - setInput(question); - - // Trigger the search immediately - setTimeout(() => { - const form = document.querySelector('form'); - if (form) { - form.dispatchEvent(new Event('submit', { cancelable: true, bubbles: true })); - } - }, 100); + const theme = customEvent.detail.question; // Re-using 'question' for theme + // For book summaries, clicking a theme might mean asking a new question about it + // For now, let's just set it in the input field for a new search if desired + // Or it could trigger a more specific search related to that theme + book. + // For simplicity, we'll just log it or potentially set it for a new input. + // User can then refine or search. + // setBookNameInput(`${messages[messages.length-1]?.bookName} - ${theme}`); // Example: prefill + // setAuthorInput(messages[messages.length-1]?.author || ''); + toast.info(`Exploring theme: ${theme}. You can ask a new question related to this theme and the book.`); }; - document.addEventListener('followUpQuestion', handleFollowUpQuestion); + document.addEventListener('followUpQuestion', handleFollowUp); return () => { - document.removeEventListener('followUpQuestion', handleFollowUpQuestion); + document.removeEventListener('followUpQuestion', handleFollowUp); }; - }, []); + }, [messages]); - const performSearch = async (query: string) => { + const performSearch = async (bookName: string, author?: string) => { setIsSearching(true); - // Create assistant message with search display const assistantMsgId = (Date.now() + 1).toString(); const events: SearchEvent[] = []; setMessages(prev => [...prev, { id: assistantMsgId, role: 'assistant', - content: , - isSearch: true + content: , // Shows "Understanding request..." etc. + isSearch: true, + bookName, // Store for context + author }]); try { - // Build context from previous messages by pairing user queries with assistant responses const conversationContext: Array<{ query: string; response: string }> = []; + // Context building might be less relevant for first summary of a book. + // Could be used if user asks follow-up questions about the summary later. + // For now, keeping it simple. - for (let i = 0; i < messages.length; i++) { - const msg = messages[i]; - // Find user messages followed by assistant messages with search results - if (msg.role === 'user' && i + 1 < messages.length) { - const nextMsg = messages[i + 1]; - if (nextMsg.role === 'assistant' && nextMsg.searchResults) { - conversationContext.push({ - query: msg.content as string, - response: nextMsg.searchResults - }); - } - } - } - - // Get search stream with context - // Pass the API key only if user provided one, otherwise let server use env var - const { stream } = await search(query, conversationContext, firecrawlApiKey || undefined); + const { stream } = await search({ + bookName, + author, + context: conversationContext, + apiKey: firecrawlApiKey || undefined + }); let finalContent = ''; - // Read stream and update events let streamingStarted = false; const resultMsgId = (Date.now() + 2).toString(); @@ -318,37 +297,33 @@ export function Chat() { if (event) { events.push(event); - // Handle content streaming if (event.type === 'content-chunk') { - const content = events + const currentStreamedContent = events .filter(e => e.type === 'content-chunk') .map(e => e.type === 'content-chunk' ? e.chunk : '') .join(''); if (!streamingStarted) { streamingStarted = true; - // Add new message for streaming content setMessages(prev => [...prev, { id: resultMsgId, role: 'assistant', - content: , - isSearch: false + content: , + isSearch: false, + bookName, + author }]); } else { - // Update streaming message setMessages(prev => prev.map(msg => msg.id === resultMsgId - ? { ...msg, content: } + ? { ...msg, content: } : msg )); } } - // Capture final result if (event.type === 'final-result') { finalContent = event.content; - - // Update the streaming message with final content and sources setMessages(prev => prev.map(msg => msg.id === resultMsgId ? { @@ -360,19 +335,18 @@ export function Chat() { - {/* Follow-up Questions */} {event.followUpQuestions && event.followUpQuestions.length > 0 && (

- Follow-up questions + Key Themes / Further Exploration

- {event.followUpQuestions.map((question, index) => ( + {event.followUpQuestions.map((theme, index) => (
)} - - {/* Sources */}
), - searchResults: finalContent + searchResults: finalContent // Store the full summary text } : msg )); } - // Update research box with new events setMessages(prev => prev.map(msg => msg.id === assistantMsgId ? { ...msg, content: , searchResults: finalContent } @@ -413,29 +384,25 @@ export function Chat() { } } catch (error) { console.error('Search error:', error); - // Remove the search display message setMessages(prev => prev.filter(msg => msg.id !== assistantMsgId)); - - // Show error message to user - const errorMessage = error instanceof Error ? error.message : 'An error occurred during search'; + const errorMessage = error instanceof Error ? error.message : 'An error occurred during summary generation.'; setMessages(prev => [...prev, { id: Date.now().toString(), role: 'assistant', content: (
-

Search Error

+

Summary Generation Error

{errorMessage}

{(errorMessage.includes('API key') || errorMessage.includes('OPENAI_API_KEY')) && (

- Please ensure all required API keys are set in your environment variables: -
• OPENAI_API_KEY (for GPT-4o) -
• ANTHROPIC_API_KEY (optional, for Claude) -
• FIRECRAWL_API_KEY (can be provided via UI) + Please ensure all required API keys are set: +
• OPENAI_API_KEY (for GPT models) +
• FIRECRAWL_API_KEY (can be provided via UI if not in .env)

)}
), - isSearch: false + isSearch: false, bookName, author }]); } finally { setIsSearching(false); @@ -444,113 +411,94 @@ export function Chat() { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - if (!input.trim() || isSearching) return; - setShowSuggestions(false); + if (!bookNameInput.trim() || isSearching) return; - const userMessage = input; - setInput(''); + const currentBookName = bookNameInput; + const currentAuthor = authorInput.trim() || undefined; + + setBookNameInput(''); + setAuthorInput(''); - // Check if we have API key if (!hasApiKey) { - // Store the query and show modal - setPendingQuery(userMessage); + setPendingSearch({ bookName: currentBookName, author: currentAuthor }); setShowApiKeyModal(true); - - // Still add user message to show what they asked - const userMsgId = Date.now().toString(); setMessages(prev => [...prev, { - id: userMsgId, + id: Date.now().toString(), role: 'user', - content: userMessage, - isSearch: true + content: `Book: ${currentBookName}` + (currentAuthor ? ` by ${currentAuthor}` : ''), + isSearch: false, + bookName: currentBookName, + author: currentAuthor }]); return; } - // Add user message - const userMsgId = Date.now().toString(); setMessages(prev => [...prev, { - id: userMsgId, + id: Date.now().toString(), role: 'user', - content: userMessage, - isSearch: true + content: `Generate summary for: ${currentBookName}` + (currentAuthor ? ` by ${currentAuthor}` : ''), + isSearch: false, // User message itself isn't a search display + bookName: currentBookName, + author: currentAuthor }]); - // Perform the search - await performSearch(userMessage); + await performSearch(currentBookName, currentAuthor); }; return (
{messages.length === 0 ? ( - // Center input when no messages
-
-
- +
+ + setInput(e.target.value)} - onFocus={() => { - if (!hasShownSuggestions && messages.length === 0) { - setShowSuggestions(true); - setHasShownSuggestions(true); - } - }} - onBlur={() => setTimeout(() => setShowSuggestions(false), 200)} - placeholder="Enter query..." - className="w-full h-14 rounded-full border border-zinc-200 bg-white pl-6 pr-16 text-base ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-zinc-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-orange-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-zinc-800 dark:bg-zinc-950 dark:ring-offset-zinc-950 dark:placeholder:text-zinc-400 dark:focus-visible:ring-orange-400 shadow-sm" + value={bookNameInput} + onChange={(e) => setBookNameInput(e.target.value)} + placeholder="e.g., Atomic Habits" + className="w-full h-12 rounded-lg border-zinc-200 dark:border-zinc-800" disabled={isSearching} /> - - - {/* Suggestions dropdown - only show on initial load */} - {showSuggestions && !input && messages.length === 0 && ( -
-
-

Try searching for:

- {SUGGESTED_QUERIES.map((suggestion, index) => ( - - ))} -
-
- )}
+
+ + setAuthorInput(e.target.value)} + placeholder="e.g., James Clear" + className="w-full h-12 rounded-lg border-zinc-200 dark:border-zinc-800" + disabled={isSearching} + /> +
+
) : ( <> - {/* Messages */} -
+
{messages.map(msg => (
{msg.role === 'user' ? (
+
You
{msg.content}
) : ( -
{msg.content}
+
+
+ AI Summary Generator {msg.bookName ? `for "${msg.bookName}"` : ''} +
+ {msg.content} +
)}
))}
- {/* Input */} -
-
-
- setInput(e.target.value)} - onFocus={() => { - if (!hasShownSuggestions) { - setShowSuggestions(true); - setHasShownSuggestions(true); - } - }} - onBlur={() => setTimeout(() => setShowSuggestions(false), 200)} - placeholder="Enter query..." - className="w-full h-14 rounded-full border border-zinc-200 bg-white pl-6 pr-16 text-base ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-zinc-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-orange-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-zinc-800 dark:bg-zinc-950 dark:ring-offset-zinc-950 dark:placeholder:text-zinc-400 dark:focus-visible:ring-orange-400 shadow-sm" - disabled={isSearching} - /> - - - - {/* Suggestions dropdown - positioned to show above input */} - {showSuggestions && !input && ( -
-
-

Try searching for:

- {SUGGESTED_QUERIES.map((suggestion, index) => ( - - ))} -
+ {/* Input form at the bottom */} +
+ +
+ setBookNameInput(e.target.value)} + placeholder="Book Name (e.g., Sapiens)" + className="flex-grow h-12 rounded-lg border-zinc-200 dark:border-zinc-800" + disabled={isSearching} + /> + setAuthorInput(e.target.value)} + placeholder="Author (Optional, e.g., Yuval Noah Harari)" + className="flex-grow h-12 rounded-lg border-zinc-200 dark:border-zinc-800" + disabled={isSearching} + />
- )} + +
- -
)} @@ -658,7 +584,7 @@ export function Chat() { Firecrawl API Key Required - To use Firesearch, you need a Firecrawl API key. You can get one for free. + To generate book summaries, a Firecrawl API key is recommended for web crawling. You can get one for free. If it's set in your server environment (.env), this step might be optional.
@@ -673,7 +599,7 @@ export function Chat() {