PaperStack / components /ProgressiveContent.tsx
Akhil-Theerthala's picture
Upload 32 files
46a757e verified
import React, { useState } from 'react';
import ReactMarkdown from 'react-markdown';
import { ChevronDown, ChevronUp, Layers, Zap, List, FileText, Quote, Sparkles } from 'lucide-react';
import { ContentLayers, ConfidenceLevel } from '../types';
interface Props {
layers: ContentLayers;
eli5Mode?: boolean;
showConfidenceBadges?: boolean;
}
type LayerLevel = 1 | 2 | 3;
const layerInfo = {
1: {
name: 'TL;DR',
icon: Zap,
description: 'Core thesis in one sentence',
color: 'brand'
},
2: {
name: 'Key Takeaways',
icon: List,
description: '3-5 essential points',
color: 'purple'
},
3: {
name: 'Full Details',
icon: FileText,
description: 'Complete explanation',
color: 'emerald'
}
};
const ProgressiveContent: React.FC<Props> = ({
layers,
eli5Mode = false,
showConfidenceBadges = true
}) => {
const [expandedLayer, setExpandedLayer] = useState<LayerLevel>(1);
const [isAnimating, setIsAnimating] = useState(false);
const handleLayerChange = (level: LayerLevel) => {
if (level === expandedLayer) return;
setIsAnimating(true);
setTimeout(() => {
setExpandedLayer(level);
setIsAnimating(false);
}, 150);
};
const content = eli5Mode && layers.eli5Content ? layers.eli5Content : layers.detailed;
return (
<div className="space-y-4">
{/* Layer Selector Pills */}
<div className="flex flex-wrap items-center gap-2 mb-6">
<span className="text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400 mr-2">
Depth:
</span>
{([1, 2, 3] as LayerLevel[]).map((level) => {
const info = layerInfo[level];
const Icon = info.icon;
const isActive = expandedLayer === level;
return (
<button
key={level}
onClick={() => handleLayerChange(level)}
className={`
flex items-center gap-2 px-3 py-1.5 rounded-full text-sm font-medium transition-all duration-300
${isActive
? `bg-${info.color}-100 dark:bg-${info.color}-900/30 text-${info.color}-700 dark:text-${info.color}-300 ring-2 ring-${info.color}-500/30 shadow-sm`
: 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700'
}
`}
title={info.description}
>
<Icon size={14} />
<span>{info.name}</span>
{isActive && level < 3 && (
<span className="text-[10px] opacity-60 ml-1">
Click {level + 1} for more
</span>
)}
</button>
);
})}
</div>
{/* Content Display */}
<div className={`transition-all duration-300 ${isAnimating ? 'opacity-0 scale-98' : 'opacity-100 scale-100'}`}>
{/* Layer 1: Thesis */}
{expandedLayer >= 1 && (
<div className="mb-6">
<div className={`
relative p-6 rounded-2xl border-2 transition-all duration-500
${expandedLayer === 1
? 'bg-brand-50 dark:bg-brand-900/20 border-brand-200 dark:border-brand-800 shadow-lg shadow-brand-500/10'
: 'bg-gray-50 dark:bg-gray-800/50 border-gray-200 dark:border-gray-700'
}
`}>
<div className="flex items-start gap-4">
<div className={`
flex-shrink-0 p-2 rounded-xl
${expandedLayer === 1
? 'bg-brand-500 text-white'
: 'bg-gray-200 dark:bg-gray-700 text-gray-500'
}
`}>
<Zap size={20} />
</div>
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<span className="text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400">
Core Thesis
</span>
{showConfidenceBadges && (
<ConfidenceBadge level="synthesis" />
)}
</div>
<p className={`
text-lg md:text-xl font-medium leading-relaxed
${expandedLayer === 1
? 'text-gray-900 dark:text-white'
: 'text-gray-600 dark:text-gray-400'
}
`}>
{layers.thesis}
</p>
</div>
</div>
{expandedLayer === 1 && (
<button
onClick={() => handleLayerChange(2)}
className="absolute -bottom-3 left-1/2 -translate-x-1/2 flex items-center gap-1 px-3 py-1 rounded-full bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 text-xs font-medium text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors shadow-sm"
>
<span>Want more detail?</span>
<ChevronDown size={14} />
</button>
)}
</div>
</div>
)}
{/* Layer 2: Key Takeaways */}
{expandedLayer >= 2 && (
<div className={`mb-6 transition-all duration-500 ${expandedLayer >= 2 ? 'animate-in fade-in slide-in-from-top-4' : ''}`}>
<div className={`
relative p-6 rounded-2xl border-2 transition-all duration-500
${expandedLayer === 2
? 'bg-purple-50 dark:bg-purple-900/20 border-purple-200 dark:border-purple-800 shadow-lg shadow-purple-500/10'
: 'bg-gray-50 dark:bg-gray-800/50 border-gray-200 dark:border-gray-700'
}
`}>
<div className="flex items-start gap-4">
<div className={`
flex-shrink-0 p-2 rounded-xl
${expandedLayer === 2
? 'bg-purple-500 text-white'
: 'bg-gray-200 dark:bg-gray-700 text-gray-500'
}
`}>
<List size={20} />
</div>
<div className="flex-1">
<div className="flex items-center gap-2 mb-3">
<span className="text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400">
Key Takeaways
</span>
</div>
<ul className="space-y-3">
{layers.takeaways.map((takeaway, idx) => (
<li
key={idx}
className={`
flex items-start gap-3 animate-in fade-in slide-in-from-left-2
${expandedLayer === 2
? 'text-gray-800 dark:text-gray-200'
: 'text-gray-600 dark:text-gray-400'
}
`}
style={{ animationDelay: `${idx * 100}ms` }}
>
<span className={`
flex-shrink-0 w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold mt-0.5
${expandedLayer === 2
? 'bg-purple-200 dark:bg-purple-800 text-purple-700 dark:text-purple-300'
: 'bg-gray-200 dark:bg-gray-700 text-gray-500'
}
`}>
{idx + 1}
</span>
<span className="font-medium">{takeaway}</span>
</li>
))}
</ul>
</div>
</div>
{expandedLayer === 2 && (
<button
onClick={() => handleLayerChange(3)}
className="absolute -bottom-3 left-1/2 -translate-x-1/2 flex items-center gap-1 px-3 py-1 rounded-full bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 text-xs font-medium text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors shadow-sm"
>
<span>Read full details</span>
<ChevronDown size={14} />
</button>
)}
</div>
</div>
)}
{/* Layer 3: Full Details */}
{expandedLayer === 3 && (
<div className="animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="relative p-6 rounded-2xl bg-emerald-50 dark:bg-emerald-900/20 border-2 border-emerald-200 dark:border-emerald-800">
<div className="flex items-center gap-2 mb-4">
<div className="p-2 rounded-xl bg-emerald-500 text-white">
<FileText size={20} />
</div>
<span className="text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400">
Full Explanation
</span>
{eli5Mode && layers.eli5Content && (
<span className="ml-2 px-2 py-0.5 rounded-full bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-300 text-xs font-medium">
Simple Mode
</span>
)}
</div>
<div className="prose prose-lg dark:prose-invert max-w-none">
<ReactMarkdown>{content}</ReactMarkdown>
</div>
<button
onClick={() => handleLayerChange(1)}
className="mt-6 flex items-center gap-1 text-sm font-medium text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 transition-colors"
>
<ChevronUp size={14} />
<span>Collapse to summary</span>
</button>
</div>
</div>
)}
</div>
</div>
);
};
// Confidence Badge Component
const ConfidenceBadge: React.FC<{ level: ConfidenceLevel }> = ({ level }) => {
const config = {
'direct-quote': {
label: 'Direct Quote',
color: 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300 border-green-200 dark:border-green-800',
icon: Quote
},
'paraphrase': {
label: 'Paraphrased',
color: 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 border-blue-200 dark:border-blue-800',
icon: Sparkles
},
'synthesis': {
label: 'Synthesized',
color: 'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 border-purple-200 dark:border-purple-800',
icon: Layers
},
'interpretation': {
label: 'AI Interpretation',
color: 'bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-300 border-amber-200 dark:border-amber-800',
icon: Sparkles
}
};
const { label, color, icon: Icon } = config[level];
return (
<span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-medium border ${color}`}>
<Icon size={10} />
{label}
</span>
);
};
export { ConfidenceBadge };
export default ProgressiveContent;