; notableConceptsStudies: string"> ; notableConceptsStudies: string"> ; notableConceptsStudies: string">
'use client'
import { useEffect, useState, useRef, useCallback } from 'react'
import { useParams } from 'next/navigation'
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Loader2, AlertCircle, BookX, Image } from "lucide-react"
import { Textarea } from "@/components/ui/textarea"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { toast } from "react-hot-toast"
import { fal } from "@fal-ai/client"
import axios from 'axios';
interface BookSummary {
id: string;
title: string;
author: string;
summary: string;
detailedSummary: {
briefOverview: string;
chapterSummaries: { id?: string; title: string; summary: string; image?: string }[]; // Added image property
keyIdeas: Array<{ idea: string; explanation: string }>;
notableConceptsStudies: string[];
actionableTakeaways: Array<{ takeaway: string; explanation: string }>;
criticalAnalysis: string;
conclusion: string;
};
}
// Configure fal client
fal.config({
credentials: process.env.NEXT_PUBLIC_FAL_KEY
})
export default function SummaryPage() {
const params = useParams()
const [bookSummary, setBookSummary] = useState<BookSummary | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState('')
const [audioSrc, setAudioSrc] = useState<string | null>(null)
const [selectedVoice, setSelectedVoice] = useState('')
const [isAudioLoading, setIsAudioLoading] = useState<Record<string, boolean>>({})
const [editMode, setEditMode] = useState<Record<string, boolean>>({})
const [editedSummary, setEditedSummary] = useState<BookSummary | null>(null)
const textareaRefs = useRef<{ [key: string]: HTMLTextAreaElement | null }>({});
const [audioSrcs, setAudioSrcs] = useState<Record<string, string>>({})
const [isImageLoading, setIsImageLoading] = useState<Record<string, boolean>>({})
const [imageSrcs, setImageSrcs] = useState<Record<string, string>>({})
const fetchSummary = useCallback(async () => {
try {
const response = await fetch(`/api/blinkist/summary/${params.slug}`)
if (!response.ok) {
throw new Error('Failed to fetch summary')
}
const data = await response.json()
setBookSummary(data)
setImageSrcs({
briefOverview: data.detailedSummary.briefOverviewImage || '',
criticalAnalysis: data.detailedSummary.criticalAnalysisImage || '',
conclusion: data.detailedSummary.conclusionImage || '',
...data.detailedSummary.chapterSummaries.reduce((acc: Record<string, string>, chapter: { image?: string }, index: number) => {
if (chapter.image) {
acc[`chapterSummaries[${index}]`] = chapter.image;
}
return acc;
}, {}),
})
setAudioSrcs({
briefOverview: data.detailedSummary.briefOverviewAudio || '',
criticalAnalysis: data.detailedSummary.criticalAnalysisAudio || '',
conclusion: data.detailedSummary.conclusionAudio || '',
...data.detailedSummary.chapterSummaries.reduce((acc: Record<string, string>, chapter: { audio?: string }, index: number) => {
if (chapter.audio) {
acc[`chapterSummaries[${index}]`] = chapter.audio;
}
return acc;
}, {}),
})
} catch (err) {
setError('Failed to load summary')
console.error(err)
} finally {
setIsLoading(false)
}
}, [params.slug])
useEffect(() => {
fetchSummary()
}, [fetchSummary])
const generateAudio = async () => {
if (!bookSummary) return;
setIsAudioLoading(prev => ({ ...prev, fullAudio: true }));
setError('');
setAudioSrc(null);
try {
const fullText = `
Brief Overview: ${bookSummary.detailedSummary.briefOverview}
Chapter Summaries:
${bookSummary.detailedSummary.chapterSummaries.map((chapter, index) =>
`Chapter ${index + 1}: ${chapter.title}
${chapter.summary}`
).join('\\n\\n')}
Key Ideas:
${bookSummary.detailedSummary.keyIdeas.map((idea, index) =>
`${index + 1}. ${idea.idea}: ${idea.explanation}`
).join('\\n')}
Notable Concepts and Studies:
${bookSummary.detailedSummary.notableConceptsStudies.map((concept, index) =>
`${index + 1}. ${concept}`
).join('\\n')}
Actionable Takeaways:
${bookSummary.detailedSummary.actionableTakeaways.map((takeaway, index) =>
`${index + 1}. ${takeaway.takeaway}: ${takeaway.explanation}`
).join('\\n')}
Critical Analysis:
${bookSummary.detailedSummary.criticalAnalysis}
Conclusion:
${bookSummary.detailedSummary.conclusion}
`;
// Split the text into chunks of 5000 characters
const chunkSize = 5000;
const textChunks = [];
for (let i = 0; i < fullText.length; i += chunkSize) {
textChunks.push(fullText.slice(i, i + chunkSize));
}
const audioChunks = [];
for (const chunk of textChunks) {
const response = await fetch('/api/elevenlabs/text-to-speech', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
text: chunk,
voice_id: selectedVoice,
}),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
audioChunks.push(data.audio);
}
// Combine all audio chunks
const combinedAudio = audioChunks.join('');
setAudioSrc(`data:audio/mpeg;base64,${combinedAudio}`);
} catch (err) {
setError('An error occurred while generating the audio. Please try again.');
console.error('Error:', err);
} finally {
setIsAudioLoading(prev => ({ ...prev, fullAudio: false }));
}
}
const toggleEditMode = (section: string) => {
setEditMode(prev => ({ ...prev, [section]: !prev[section] }))
}
const handleEdit = (section: string, value: string | { title: string; summary: string }) => {
if (!editedSummary) return
if (section.startsWith('chapterSummaries[')) {
const index = parseInt(section.match(/\\d+/)?.[0] ?? '0', 10)
setEditedSummary(prev => {
if (!prev) return prev
const newChapterSummaries = [...prev.detailedSummary.chapterSummaries]
newChapterSummaries[index] = value as { title: string; summary: string }
return {
...prev,
detailedSummary: {
...prev.detailedSummary,
chapterSummaries: newChapterSummaries
}
}
})
} else {
setEditedSummary(prev => {
if (!prev) return prev
return {
...prev,
detailedSummary: {
...prev.detailedSummary,
[section]: value
}
}
})
}
}
const saveEdits = async () => {
if (!editedSummary) return
try {
const response = await fetch(`/api/blinkist/summary/${params.slug}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(editedSummary),
})
if (!response.ok) throw new Error('Failed to save edits')
setBookSummary(editedSummary)
setEditMode({})
} catch (err) {
console.error('Error saving edits:', err)
setError('Failed to save edits')
}
}
const saveSection = async (section: string) => {
if (!editedSummary) return
try {
const savingToast = toast.loading('Saving changes...')
console.log('Saving section:', section)
console.log('Current editedSummary:', JSON.stringify(editedSummary, null, 2))
let content: any
if (section.startsWith('chapterSummaries[')) {
const index = parseInt(section.match(/\\d+/)?.[0] ?? '0', 10)
content = editedSummary.detailedSummary.chapterSummaries[index]
} else {
content = editedSummary.detailedSummary[section as keyof typeof editedSummary.detailedSummary]
}
console.log('Content to save:', JSON.stringify(content, null, 2))
const response = await fetch(`/api/blinkist/summary/${params.slug}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
section,
content
}),
})
if (!response.ok) throw new Error('Failed to save edits')
const updatedBook = await response.json()
console.log('Updated book:', JSON.stringify(updatedBook, null, 2))
setEditedSummary(updatedBook)
setBookSummary(updatedBook)
setEditMode(prevEditMode => ({
...prevEditMode,
[section]: false
}))
toast.success('Changes saved successfully', { id: savingToast })
} catch (err) {
console.error('Error saving edits:', err)
setError('Failed to save edits')
toast.error('Failed to save changes')
}
}
useEffect(() => {
if (bookSummary) setEditedSummary(bookSummary)
}, [bookSummary])
// Add this new useEffect hook
useEffect(() => {
console.log('bookSummary updated:', JSON.stringify(bookSummary, null, 2))
console.log('editedSummary updated:', JSON.stringify(editedSummary, null, 2))
}, [bookSummary, editedSummary])
const adjustTextareaHeight = (key: string) => {
const textarea = textareaRefs.current[key];
if (textarea) {
textarea.style.height = 'auto';
textarea.style.height = `${textarea.scrollHeight}px`;
}
};
useEffect(() => {
Object.keys(editMode).forEach((key) => {
if (editMode[key]) {
adjustTextareaHeight(key);
}
});
}, [editMode]);
const generateAudioForSection = async (section: string, text: string) => {
setIsAudioLoading(prev => ({ ...prev, [section]: true }))
setError('')
try {
if (!selectedVoice) {
throw new Error('No voice selected. Please select a voice before generating audio.');
}
if (!text.trim()) {
throw new Error('The text for this section is empty. Please add some content before generating audio.');
}
const response = await fetch('/api/elevenlabs/text-to-speech', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
text,
voice_id: selectedVoice,
}),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(`HTTP error! status: ${response.status}, message: ${errorData.message || response.statusText}`);
}
const data = await response.json();
if (!data.audio) {
throw new Error('No audio data received from the server.');
}
// Save the audio data to the database
const saveResponse = await fetch(`/api/blinkist/summary/${params.slug}/audio`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
section,
audioData: `data:audio/mpeg;base64,${data.audio}`,
}),
});
if (!saveResponse.ok) {
throw new Error('Failed to save audio data to the database');
}
const savedData = await saveResponse.json();
setAudioSrcs(prev => ({ ...prev, [section]: savedData.audioUrl }));
// Refresh the book summary data to get the updated audio URLs
fetchSummary();
} catch (err) {
console.error('Error in generateAudioForSection:', err);
setError(`An error occurred while generating the audio for ${section}: ${err instanceof Error ? err.message : 'Unknown error'}`);
} finally {
setIsAudioLoading(prev => ({ ...prev, [section]: false }));
}
}
const generateImageForSection = async (section: string, text: string) => {
setIsImageLoading(prev => ({ ...prev, [section]: true }))
setError('')
try {
let imagePrompt = '';
switch(section) {
case 'briefOverview':
imagePrompt = `Create an abstract image representing the main theme of this book: ${text}`;
break;
case 'criticalAnalysis':
imagePrompt = `Create an image symbolizing critical analysis and evaluation, inspired by: ${text}`;
break;
case 'conclusion':
imagePrompt = `Create an image that captures the essence of this book's conclusion: ${text}`;
break;
default:
if (section.startsWith('chapterSummaries[')) {
imagePrompt = `Create an image representing the key concepts from this chapter: ${text}`;
} else {
imagePrompt = `Create an image inspired by these ideas: ${text}`;
}
}
console.log('Sending request to generate image for section:', section);
const response = await fetch('/api/fal/generate-image', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
prompt: imagePrompt,
image_size: "landscape_4_3",
num_images: 1,
enable_safety_checker: true,
bookId: params.slug,
section: section,
}),
});
if (!response.ok) {
const errorText = await response.text();
let errorData;
try {
errorData = JSON.parse(errorText);
} catch {
errorData = { error: errorText };
}
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('Received image generation response:', data);
if (data.imageUrl) {
setImageSrcs(prev => ({ ...prev, [section]: data.imageUrl }));
// Refresh the book summary data to get the updated image URLs
fetchSummary();
} else {
throw new Error('No image URL in the response');
}
} catch (err) {
console.error('Error in generateImageForSection:', err);
setError(`An error occurred while generating the image for ${section}: ${err instanceof Error ? err.message : 'Unknown error'}`);
} finally {
setIsImageLoading(prev => ({ ...prev, [section]: false }));
}
}
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center">
<Card className="w-96 text-center">
<CardContent className="pt-6">
<Loader2 className="h-12 w-12 animate-spin mx-auto mb-4" />
<p className="text-lg font-semibold">Loading summary...</p>
<p className="text-sm text-muted-foreground">This may take a few moments</p>
</CardContent>
</Card>
</div>
)
}
if (error) {
return (
<div className="min-h-screen flex items-center justify-center">
<Alert variant="destructive" className="w-96">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>
{error}
<Button
className="mt-4 w-full"
onClick={() => window.location.reload()}
>
Try Again
</Button>
</AlertDescription>
</Alert>
</div>
)
}
if (!bookSummary || !editedSummary || !editedSummary.detailedSummary) {
return (
<div className="min-h-screen flex items-center justify-center">
<Card className="w-96 text-center">
<CardContent className="pt-6">
<BookX className="h-12 w-12 mx-auto mb-4" />
<p className="text-lg font-semibold">No summary found</p>
<p className="text-sm text-muted-foreground mb-4">We couldn't find a summary for this book</p>
<Button
className="w-full"
onClick={() => window.history.back()}
>
Go Back
</Button>
</CardContent>
</Card>
</div>
)
}
return (
<div className="min-h-screen py-8 px-4 sm:px-6 lg:px-8">
<div className="max-w-3xl mx-auto space-y-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-2xl font-bold">{bookSummary.title}</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">Author: {bookSummary.author}</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Voice Selection</CardTitle>
</CardHeader>
<CardContent>
<Select onValueChange={setSelectedVoice} value={selectedVoice}>
<SelectTrigger className="w-[200px]">
<SelectValue placeholder="Select a voice" />
</SelectTrigger>
<SelectContent>
<SelectItem value="21m00Tcm4TlvDq8ikWAM">Rachel</SelectItem>
<SelectItem value="AZnzlk1XvdvUeBnXmlld">Domi</SelectItem>
<SelectItem value="EXAVITQu4vr4xnSDxMaL">Bella</SelectItem>
<SelectItem value="ErXwobaYiN019PkySvjV">Antoni</SelectItem>
<SelectItem value="MF3mGyEYCl7XYWbV9V6O">Elli</SelectItem>
<SelectItem value="Mu5jxyqZOLIGltFpfalg">Jameson - Guided Meditation & Narration</SelectItem>
</SelectContent>
</Select>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle>Brief Overview</CardTitle>
<div className="flex space-x-2">
<Button
onClick={() => editMode.briefOverview ? saveSection('briefOverview') : toggleEditMode('briefOverview')}
variant="outline"
size="sm"
>
{editMode.briefOverview ? 'Save' : 'Edit'}
</Button>
<Button
onClick={() => generateAudioForSection('briefOverview', editedSummary.detailedSummary?.briefOverview || '')}
disabled={isAudioLoading['briefOverview'] || !selectedVoice}
size="sm"
>
{isAudioLoading['briefOverview'] ? 'Generating...' : 'Generate Audio'}
</Button>
<Button
onClick={() => generateImageForSection('briefOverview', editedSummary.detailedSummary?.briefOverview || '')}
disabled={isImageLoading['briefOverview']}
size="sm"
>
{isImageLoading['briefOverview'] ? 'Generating...' : <Image className="h-4 w-4" />}
</Button>
</div>
</CardHeader>
<CardContent>
{editMode.briefOverview ? (
<Textarea
ref={(el) => {
if (el) textareaRefs.current['briefOverview'] = el;
}}
value={editedSummary.detailedSummary?.briefOverview || ''}
onChange={(e) => {
handleEdit('briefOverview', e.target.value);
adjustTextareaHeight('briefOverview');
}}
className="w-full min-h-[200px] resize-none"
/>
) : (
<p>{bookSummary.detailedSummary?.briefOverview || 'No brief overview available.'}</p>
)}
{audioSrcs['briefOverview'] && (
<audio controls src={audioSrcs['briefOverview']} className="w-full mt-4" />
)}
{imageSrcs['briefOverview'] && (
<img src={imageSrcs['briefOverview']} alt="Brief Overview" className="w-full mt-4 rounded-md" />
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Chapter Summaries</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{Array.isArray(editedSummary.detailedSummary.chapterSummaries) &&
editedSummary.detailedSummary.chapterSummaries.length > 0 ? (
editedSummary.detailedSummary.chapterSummaries.map((chapter, index) => (
<div key={index} className="border-b pb-4 last:border-b-0 last:pb-0">
<div className="flex justify-between items-center mb-2">
<h4 className="font-medium text-lg">{chapter.title}</h4>
<div className="flex space-x-2">
<Button
onClick={() => editMode[`chapterSummaries[${index}]`] ? saveSection(`chapterSummaries[${index}]`) : toggleEditMode(`chapterSummaries[${index}]`)}
variant="outline"
size="sm"
>
{editMode[`chapterSummaries[${index}]`] ? 'Save' : 'Edit'}
</Button>
<Button
onClick={() => generateAudioForSection(`chapterSummaries[${index}]`, chapter.summary)}
disabled={isAudioLoading[`chapterSummaries[${index}]`] || !selectedVoice}
size="sm"
>
{isAudioLoading[`chapterSummaries[${index}]`] ? 'Generating...' : 'Generate Audio'}
</Button>
<Button
onClick={() => generateImageForSection(`chapterSummaries[${index}]`, chapter.summary)}
disabled={isImageLoading[`chapterSummaries[${index}]`]}
size="sm"
>
{isImageLoading[`chapterSummaries[${index}]`] ? 'Generating...' : <Image className="h-4 w-4" />}
</Button>
</div>
</div>
{audioSrcs[`chapterSummaries[${index}]`] && (
<audio controls src={audioSrcs[`chapterSummaries[${index}]`]} className="w-full mb-4" />
)}
{chapter.image && (
<img src={chapter.image} alt={`Chapter ${index + 1}`} className="w-full mb-4 rounded-md" />
)}
{editMode[`chapterSummaries[${index}]`] ? (
<Textarea
ref={(el) => {
if (el) textareaRefs.current[`chapterSummaries[${index}]`] = el;
}}
value={chapter.summary}
onChange={(e) => {
handleEdit(`chapterSummaries[${index}]`, { ...chapter, summary: e.target.value });
adjustTextareaHeight(`chapterSummaries[${index}]`);
}}
className="w-full min-h-[150px] resize-none"
placeholder="Chapter Summary"
/>
) : (
<p>{chapter.summary}</p>
)}
</div>
))
) : (
<p>No chapter summaries available.</p>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Key Ideas</CardTitle>
</CardHeader>
<CardContent>
<ul className="list-disc pl-5 space-y-2">
{editedSummary.detailedSummary.keyIdeas.map((idea, index) => (
<li key={index}>
<strong>{idea.idea}</strong>: {idea.explanation}
</li>
))}
</ul>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Notable Concepts and Studies</CardTitle>
</CardHeader>
<CardContent>
<ul className="list-disc pl-5">
{editedSummary.detailedSummary.notableConceptsStudies.map((concept, index) => (
<li key={index}>{concept}</li>
))}
</ul>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Actionable Takeaways</CardTitle>
</CardHeader>
<CardContent>
<ul className="list-disc pl-5 space-y-2">
{editedSummary.detailedSummary.actionableTakeaways.map((takeaway, index) => (
<li key={index}>
<strong>{takeaway.takeaway}</strong>: {takeaway.explanation}
</li>
))}
</ul>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Critical Analysis</CardTitle>
</CardHeader>
<CardContent>
<p>{editedSummary.detailedSummary.criticalAnalysis}</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Conclusion</CardTitle>
</CardHeader>
<CardContent>
<p>{editedSummary.detailedSummary.conclusion}</p>
</CardContent>
</Card>
<Button onClick={saveEdits} className="w-full">Save All Edits</Button>
</div>
</div>
)
}