Spaces:
Running
Running
File size: 13,990 Bytes
6fe3275 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 |
import React, { useState } from 'react';
import { BentoCardData } from '../types';
import { Maximize2, Minimize2, BookOpen, MessageSquare, Activity, Quote, Zap, ThumbsUp, ThumbsDown, ArrowRight, FileText, MoveHorizontal, MoveVertical, Plus, Minus } from 'lucide-react';
import ReactMarkdown from 'react-markdown';
import MermaidDiagram from './MermaidDiagram';
interface Props {
card: BentoCardData;
onExpand: (card: BentoCardData) => void;
onRate: (id: string, rating: number) => void;
onResize: (id: string, deltaCol: number, deltaRow: number) => void;
layoutMode: 'auto' | 'grid' | 'list';
}
const BentoCard: React.FC<Props> = ({ card, onExpand, onRate, onResize, layoutMode }) => {
const [isExpanded, setIsExpanded] = useState(false);
// Visual Configurations based on Card Type
const getTypeConfig = () => {
switch (card.type) {
case 'stat':
return {
icon: <Activity className="w-5 h-5" />,
color: 'text-emerald-600 dark:text-emerald-400',
bg: 'bg-emerald-100 dark:bg-emerald-900/30',
border: 'border-emerald-200 dark:border-emerald-800',
gradient: 'from-emerald-500/10 to-teal-500/5',
graphic: (
<svg viewBox="0 0 100 40" className="w-full h-full opacity-20 text-emerald-500" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M0 35 C 20 35, 20 15, 40 15 S 60 25, 80 5 L 100 0" />
<path d="M0 40 L 100 40 L 100 0 L 80 5 C 60 25, 60 15, 40 15 C 20 15, 20 35, 0 35 Z" fill="currentColor" stroke="none" opacity="0.2" />
</svg>
)
};
case 'quote':
return {
icon: <Quote className="w-5 h-5" />,
color: 'text-amber-600 dark:text-amber-400',
bg: 'bg-amber-100 dark:bg-amber-900/30',
border: 'border-amber-200 dark:border-amber-800',
gradient: 'from-amber-500/10 to-orange-500/5',
graphic: (
<svg width="120" height="120" viewBox="0 0 24 24" className="absolute -right-2 -bottom-6 text-amber-500 opacity-10 transform rotate-12" fill="currentColor">
<path d="M14.017 21L14.017 18C14.017 16.8954 14.9124 16 16.017 16H19.017C19.5693 16 20.017 15.5523 20.017 15V9C20.017 8.44772 19.5693 8 19.017 8H15.017C14.4647 8 14.017 7.55228 14.017 7V3H19.017C20.6739 3 22.017 4.34315 22.017 6V15C22.017 16.6569 20.6739 18 19.017 18H16.017V21H14.017ZM5.0166 21L5.0166 18C5.0166 16.8954 5.91203 16 7.0166 16H10.0166C10.5689 16 11.0166 15.5523 11.0166 15V9C11.0166 8.44772 10.5689 8 10.0166 8H6.0166C5.46432 8 5.0166 7.55228 5.0166 7V3H10.0166C11.6735 3 13.0166 4.34315 13.0166 6V15C13.0166 16.6569 11.6735 18 10.0166 18H7.0166V21H5.0166Z" />
</svg>
)
};
case 'process':
return {
icon: <Zap className="w-5 h-5" />,
color: 'text-purple-600 dark:text-purple-400',
bg: 'bg-purple-100 dark:bg-purple-900/30',
border: 'border-purple-200 dark:border-purple-800',
gradient: 'from-purple-500/10 to-indigo-500/5',
graphic: (
<svg viewBox="0 0 100 100" className="w-full h-full opacity-10 text-purple-500 absolute right-0 top-0">
<circle cx="80" cy="20" r="15" stroke="currentColor" strokeWidth="2" fill="none" />
<circle cx="60" cy="50" r="10" stroke="currentColor" strokeWidth="2" fill="none" />
<circle cx="80" cy="80" r="20" stroke="currentColor" strokeWidth="2" fill="none" />
<line x1="75" y1="33" x2="65" y2="42" stroke="currentColor" strokeWidth="2" />
<line x1="65" y1="58" x2="75" y2="65" stroke="currentColor" strokeWidth="2" />
</svg>
)
};
case 'insight':
return {
icon: <MessageSquare className="w-5 h-5" />,
color: 'text-blue-600 dark:text-blue-400',
bg: 'bg-blue-100 dark:bg-blue-900/30',
border: 'border-blue-200 dark:border-blue-800',
gradient: 'from-blue-500/10 to-cyan-500/5',
graphic: (
<div className="absolute top-0 right-0 w-32 h-32 bg-blue-500/10 rounded-bl-full pointer-events-none blur-xl" />
)
};
default:
return {
icon: <BookOpen className="w-5 h-5" />,
color: 'text-gray-600 dark:text-gray-400',
bg: 'bg-gray-100 dark:bg-gray-800',
border: 'border-gray-200 dark:border-gray-700',
gradient: 'from-gray-500/5 to-slate-500/5',
graphic: null
};
}
};
const config = getTypeConfig();
const displayRow = card.rowSpan > 3 ? 3 : card.rowSpan < 1 ? 1 : card.rowSpan;
const getGridClass = () => {
if (layoutMode === 'list') return 'col-span-4 row-span-1';
if (isExpanded) return 'col-span-4 row-span-2 md:col-span-4 md:row-span-3 z-30';
const displayCol = card.colSpan;
// Clamping to grid limits
const cols = displayCol > 4 ? 4 : displayCol < 1 ? 1 : displayCol;
const colClass = cols === 4 ? 'md:col-span-4' : cols === 3 ? 'md:col-span-3' : cols === 2 ? 'md:col-span-2' : 'md:col-span-1';
const rowClass = displayRow === 3 ? 'md:row-span-3' : displayRow === 2 ? 'md:row-span-2' : 'md:row-span-1';
return `col-span-4 ${colClass} ${rowClass}`;
};
const getLineClampClass = () => {
if (isExpanded) return '';
switch (displayRow) {
case 1: return 'line-clamp-4';
case 2: return 'line-clamp-[12]';
case 3: return 'line-clamp-[20]';
default: return 'line-clamp-4';
}
};
const handleExpandClick = (e: React.MouseEvent) => {
if ((e.target as HTMLElement).closest('.control-btn')) return;
e.stopPropagation();
if (!isExpanded && !card.expandedContent) {
onExpand(card);
}
setIsExpanded(!isExpanded);
};
return (
<div
className={`
relative group transition-all duration-500 ease-[cubic-bezier(0.25,0.1,0.25,1.0)]
${getGridClass()}
${isExpanded
? 'scale-[1.02] z-40 shadow-2xl -translate-y-2 h-auto min-h-[400px]'
: 'hover:scale-[1.02] hover:-translate-y-2 hover:shadow-xl hover:shadow-brand-500/10 hover:z-20 h-full'
}
`}
onClick={handleExpandClick}
>
<div className={`
h-full w-full rounded-2xl overflow-hidden flex flex-col
bg-white dark:bg-slate-900
border
${isExpanded ? 'border-brand-500 ring-1 ring-brand-500' : `${config.border} group-hover:border-opacity-0`}
shadow-sm transition-all duration-300
relative
group-hover:ring-1 group-hover:ring-black/5 dark:group-hover:ring-white/10
`}>
{/* Mermaid Diagram (Takes priority over default graphics if present) */}
{card.mermaid && (
<div className={`absolute inset-0 z-0 opacity-20 dark:opacity-30 ${isExpanded ? 'opacity-100 dark:opacity-100 z-10 bg-white/90 dark:bg-slate-900/90' : ''} transition-all duration-500`}>
<MermaidDiagram chart={card.mermaid} theme={document.documentElement.classList.contains('dark') ? 'dark' : 'light'} />
</div>
)}
{/* Default Graphic / Fallback */}
{!card.mermaid && (
<div className="absolute inset-0 overflow-hidden pointer-events-none z-0">
<div className={`absolute inset-0 bg-gradient-to-br ${config.gradient} opacity-100`} />
{config.graphic}
</div>
)}
{/* Layout & Action Controls (Visible on Hover) */}
<div className="absolute top-3 right-12 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity z-20 control-btn">
{/* Resize Width */}
<div className="flex items-center bg-white/90 dark:bg-black/60 rounded-lg backdrop-blur-sm border border-gray-200 dark:border-gray-700 p-1 shadow-sm">
<button onClick={(e) => { e.stopPropagation(); onResize(card.id, -1, 0); }} className="p-1 hover:bg-black/10 dark:hover:bg-white/20 rounded" title="Decrease Width">
<Minus size={12} />
</button>
<span className="px-1.5 text-[10px] font-mono text-gray-500">W</span>
<button onClick={(e) => { e.stopPropagation(); onResize(card.id, 1, 0); }} className="p-1 hover:bg-black/10 dark:hover:bg-white/20 rounded" title="Increase Width">
<Plus size={12} />
</button>
</div>
{/* Resize Height */}
<div className="flex items-center bg-white/90 dark:bg-black/60 rounded-lg backdrop-blur-sm border border-gray-200 dark:border-gray-700 p-1 shadow-sm">
<button onClick={(e) => { e.stopPropagation(); onResize(card.id, 0, -1); }} className="p-1 hover:bg-black/10 dark:hover:bg-white/20 rounded" title="Decrease Height">
<Minus size={12} />
</button>
<span className="px-1.5 text-[10px] font-mono text-gray-500">H</span>
<button onClick={(e) => { e.stopPropagation(); onResize(card.id, 0, 1); }} className="p-1 hover:bg-black/10 dark:hover:bg-white/20 rounded" title="Increase Height">
<Plus size={12} />
</button>
</div>
</div>
<div className={`relative p-6 flex flex-col h-full z-10 ${isExpanded && card.mermaid ? 'bg-white/80 dark:bg-slate-900/80 backdrop-blur-sm' : ''}`}>
{/* Header */}
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${config.bg} ${config.color} backdrop-blur-md shadow-sm transition-transform duration-300 group-hover:scale-110`}>
{config.icon}
</div>
<h3 className="font-display font-bold text-lg leading-tight text-gray-900 dark:text-gray-50 pr-8 drop-shadow-sm">{card.title}</h3>
</div>
<button
onClick={handleExpandClick}
className="control-btn p-2 rounded-full bg-white/50 dark:bg-black/20 hover:bg-white dark:hover:bg-black/40 transition-all opacity-0 group-hover:opacity-100 absolute right-4 top-4 backdrop-blur-md border border-white/20 shadow-sm"
>
{isExpanded ? <Minimize2 size={16} className="text-gray-700 dark:text-gray-200" /> : <Maximize2 size={16} className="text-gray-700 dark:text-gray-200" />}
</button>
</div>
{/* Main Content (Summary) */}
<div className="flex-grow overflow-y-auto custom-scrollbar relative">
{/* Use ReactMarkdown for summary to handle bolding/italics if model sends it, but style as simple text */}
<div className={`text-base leading-relaxed text-gray-700 dark:text-gray-200 font-medium ${getLineClampClass()} drop-shadow-sm prose prose-sm dark:prose-invert max-w-none prose-p:my-0`}>
<ReactMarkdown>{card.summary}</ReactMarkdown>
</div>
{!isExpanded && (
<div className="mt-4 flex items-center gap-2 text-xs font-medium uppercase tracking-wider opacity-60 text-gray-600 dark:text-gray-300 group-hover:translate-x-1 transition-transform">
Click to explore <ArrowRight size={12} />
</div>
)}
</div>
{/* Expanded Content Area */}
{isExpanded && (
<div
className="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700 animate-in fade-in slide-in-from-bottom-4 duration-500 overflow-y-auto custom-scrollbar max-h-[400px]"
onClick={(e) => e.stopPropagation()}
>
<div className="flex justify-between items-center mb-4">
<div className="flex items-center gap-2 text-brand-600 dark:text-brand-400">
<FileText size={16} />
<h4 className="text-sm uppercase tracking-wider font-bold">First Principles Deep Dive</h4>
</div>
</div>
{card.isLoadingDetails ? (
<div className="space-y-3 py-2">
<div className="h-2 bg-gray-200 dark:bg-gray-700 rounded w-full animate-pulse"></div>
<div className="h-2 bg-gray-200 dark:bg-gray-700 rounded w-5/6 animate-pulse delay-75"></div>
<div className="h-2 bg-gray-200 dark:bg-gray-700 rounded w-4/6 animate-pulse delay-150"></div>
<div className="h-2 bg-gray-200 dark:bg-gray-700 rounded w-full animate-pulse delay-100"></div>
</div>
) : (
<div className="prose prose-sm dark:prose-invert max-w-none text-gray-700 dark:text-gray-300 bg-gray-50/50 dark:bg-black/20 p-4 rounded-xl">
<ReactMarkdown>{card.expandedContent || "No detailed content available."}</ReactMarkdown>
</div>
)}
{/* Feedback */}
<div className="flex items-center justify-end gap-2 mt-4 pt-2">
<span className="text-xs font-medium text-gray-500">Helpful?</span>
<button onClick={() => onRate(card.id, 1)} className={`p-1.5 rounded hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors ${card.rating === 1 ? 'text-green-600 bg-green-50 dark:bg-green-900/20' : 'text-gray-400'}`}>
<ThumbsUp size={14} />
</button>
<button onClick={() => onRate(card.id, -1)} className={`p-1.5 rounded hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors ${card.rating === -1 ? 'text-red-600 bg-red-50 dark:bg-red-900/20' : 'text-gray-400'}`}>
<ThumbsDown size={14} />
</button>
</div>
</div>
)}
</div>
</div>
</div>
);
};
export default BentoCard;
|