Implement a progressive text segmentation strategy. The first few segments are intentionally kept very short to allow playback to start almost immediately, creating a more responsive feel. As more segments are processed, their length gradually increases to optimize audio generation efficiency for the remainder of the article. Additionally, the title is now prepended as the very first segment. The buffer ahead is also increased to 5 segments to ensure content is ready. Further refinements include: - Enhanced voice descriptions in constants. - Improved segment styling in ReaderView for better visual active state indication.
84 lines
3.0 KiB
TypeScript
84 lines
3.0 KiB
TypeScript
|
|
import React, { useEffect, useRef } from 'react';
|
|
import { Article } from '../types';
|
|
import { FileText } from 'lucide-react';
|
|
|
|
interface ReaderViewProps {
|
|
article?: Article | null;
|
|
}
|
|
|
|
export const ReaderView: React.FC<ReaderViewProps> = ({ article }) => {
|
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
|
|
// Auto-scroll to active segment
|
|
useEffect(() => {
|
|
if (!article || article.status !== 'PLAYING') return;
|
|
|
|
const activeEl = document.getElementById(`segment-${article.currentSegmentIndex}`);
|
|
if (activeEl && scrollRef.current) {
|
|
activeEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
}
|
|
}, [article?.currentSegmentIndex, article?.status]);
|
|
|
|
if (!article) {
|
|
return (
|
|
<div className="h-full flex flex-col items-center justify-center text-slate-400 p-12 border-2 border-dashed border-slate-200 rounded-2xl bg-slate-50/50">
|
|
<FileText className="w-12 h-12 mb-4 opacity-50" />
|
|
<p className="text-lg font-medium">Select an article to read along</p>
|
|
<p className="text-sm">The text will appear here while you listen.</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden h-[calc(100vh-12rem)] flex flex-col">
|
|
<div className="p-6 border-b border-slate-100 bg-white sticky top-0 z-10">
|
|
<h2 className="text-2xl font-bold text-slate-900 leading-tight">
|
|
{article.title}
|
|
</h2>
|
|
<a
|
|
href={article.url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-sm text-blue-600 hover:underline mt-2 inline-block"
|
|
>
|
|
{new URL(article.url).hostname}
|
|
</a>
|
|
</div>
|
|
|
|
<div ref={scrollRef} className="flex-grow overflow-y-auto p-6 sm:p-8 space-y-6 custom-scrollbar bg-white">
|
|
{article.segments.length > 0 ? (
|
|
article.segments.map((segment, idx) => {
|
|
const isActive = article.currentSegmentIndex === idx;
|
|
return (
|
|
<div
|
|
key={segment.id}
|
|
id={`segment-${idx}`}
|
|
className={`text-lg leading-relaxed font-serif transition-colors duration-300 whitespace-pre-wrap ${
|
|
isActive
|
|
? 'text-slate-900 bg-blue-50 p-4 rounded-lg -mx-4 border-l-4 border-blue-500'
|
|
: 'text-slate-700'
|
|
}`}
|
|
>
|
|
{segment.text}
|
|
</div>
|
|
);
|
|
})
|
|
) : (
|
|
// Loading State skeleton
|
|
<div className="space-y-4 animate-pulse">
|
|
{[1,2,3,4].map(i => (
|
|
<div key={i} className="space-y-2">
|
|
<div className="h-4 bg-slate-100 rounded w-full"></div>
|
|
<div className="h-4 bg-slate-100 rounded w-full"></div>
|
|
<div className="h-4 bg-slate-100 rounded w-3/4"></div>
|
|
</div>
|
|
))}
|
|
<p className="text-slate-400 italic mt-4">Extracting article content...</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|