Spaces:
Sleeping
Sleeping
| 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; | |