File size: 11,422 Bytes
1f7ead8 | 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 | import React, { useState } from 'react';
import { X, ZoomIn, ZoomOut, Download, Loader2, RefreshCw,
Target, Zap, Book, Users, Clock, ArrowRight, CheckCircle,
BarChart, GitBranch, Layers, Layout, Share2, Award, Lightbulb } from 'lucide-react';
import { generateInfographic } from '../api';
import * as htmlToImage from 'html-to-image';
const iconMap = {
'Target': Target, 'Zap': Zap, 'Book': Book, 'Users': Users,
'Clock': Clock, 'ArrowRight': ArrowRight, 'CheckCircle': CheckCircle,
'BarChart': BarChart, 'GitBranch': GitBranch, 'Layers': Layers,
'Layout': Layout, 'Share2': Share2, 'Award': Award, 'Lightbulb': Lightbulb
};
const InfographicModal = ({ isOpen, onClose, notebookId }) => {
const [status, setStatus] = useState('idle'); // idle, loading, ready
const [data, setData] = useState(null);
const [scale, setScale] = useState(1);
if (!isOpen) return null;
const handleGenerate = async () => {
setStatus('loading');
try {
const result = await generateInfographic(notebookId);
setData(result);
setStatus('ready');
} catch (error) {
console.error(error);
setStatus('error');
}
};
// Auto-generate on first open if no data
React.useEffect(() => {
if (isOpen && status === 'idle') {
handleGenerate();
}
}, [isOpen]);
const handleDownload = async () => {
const node = document.getElementById('infographic-canvas');
if (node) {
const dataUrl = await htmlToImage.toPng(node);
const link = document.createElement('a');
link.download = `infographic-${notebookId}.png`;
link.href = dataUrl;
link.click();
}
};
const DynamicIcon = ({ name, size=24, className }) => {
const Icon = iconMap[name] || Lightbulb;
return <Icon size={size} className={className} />;
};
// Render Logic based on Type
const renderLayout = () => {
if (!data) return null;
const { type, title, summary, sections } = data;
// Common Header
const Header = () => (
<div className="text-center mb-12">
<h1 className="text-4xl font-extrabold text-slate-900 mb-4 tracking-tight">{title}</h1>
<p className="text-lg text-slate-500 max-w-2xl mx-auto leading-relaxed">{summary}</p>
<div className="mt-6 flex justify-center">
<div className="h-1 w-24 bg-indigo-500 rounded-full"></div>
</div>
</div>
);
// PROCESS / TIMELINE Layout
if (type === 'PROCESS' || type === 'TIMELINE') {
return (
<div className="p-8 bg-slate-50 min-h-[600px] w-full" id="infographic-canvas">
<Header />
<div className="max-w-4xl mx-auto relative">
{/* Vertical Line */}
<div className="absolute left-1/2 top-0 bottom-0 w-1 bg-indigo-200 transform -translate-x-1/2 hidden md:block"></div>
<div className="space-y-12 relative">
{sections.map((section, idx) => (
<div key={idx} className={`flex items-center gap-8 ${idx % 2 === 0 ? 'md:flex-row' : 'md:flex-row-reverse'} flex-col`}>
{/* Content Card */}
<div className="flex-1 w-full">
<div className="bg-white p-6 rounded-2xl shadow-sm border border-slate-100 hover:shadow-md transition-shadow relative overflow-hidden">
<div className="absolute top-0 left-0 w-2 h-full bg-indigo-500"></div>
<h3 className="text-xl font-bold text-slate-800 mb-3">{section.title}</h3>
<ul className="space-y-2">
{section.points.map((pt, i) => (
<li key={i} className="flex items-start gap-2 text-slate-600 text-sm">
<span className="mt-1.5 w-1.5 h-1.5 bg-indigo-400 rounded-full shrink-0"></span>
{pt}
</li>
))}
</ul>
</div>
</div>
{/* Icon Marker */}
<div className="relative z-10 shrink-0">
<div className="w-16 h-16 rounded-full bg-white border-4 border-indigo-100 flex items-center justify-center text-indigo-600 shadow-sm">
<DynamicIcon name={section.icon} size={28} />
</div>
</div>
{/* Spacer */}
<div className="flex-1 hidden md:block"></div>
</div>
))}
</div>
</div>
{/* Branding */}
<div className="mt-16 text-center text-slate-400 text-sm font-medium uppercase tracking-widest">
Generated by Vertical.ai
</div>
</div>
);
}
// DEFAULT: OVERVIEW Grid Layout
return (
<div className="p-12 bg-white min-h-[600px] w-full" id="infographic-canvas">
<Header />
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-6xl mx-auto">
{sections.map((section, idx) => (
<div key={idx} className="bg-slate-50 rounded-2xl p-8 border border-slate-100 flex flex-col items-start hover:bg-indigo-50/30 transition-colors">
<div className="w-12 h-12 rounded-xl bg-indigo-100 text-indigo-600 flex items-center justify-center mb-6">
<DynamicIcon name={section.icon} size={24} />
</div>
<h3 className="text-xl font-bold text-slate-900 mb-4">{section.title}</h3>
<ul className="space-y-3 w-full">
{section.points.map((pt, i) => (
<li key={i} className="flex items-start gap-3 text-slate-600">
<CheckCircle size={16} className="text-indigo-400 mt-1 shrink-0" />
<span className="text-sm font-medium">{pt}</span>
</li>
))}
</ul>
</div>
))}
</div>
<div className="mt-16 text-center text-slate-400 text-sm font-medium uppercase tracking-widest border-t border-slate-100 pt-8">
Research Summary • Vertical.ai
</div>
</div>
);
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
<div className="bg-slate-100 rounded-2xl shadow-2xl w-full max-w-[90vw] h-[90vh] flex flex-col animate-in fade-in zoom-in duration-200 overflow-hidden">
{/* Header */}
<div className="p-4 bg-white border-b border-slate-200 flex items-center justify-between shrink-0">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-indigo-600 flex items-center justify-center text-white">
<Layout size={20} />
</div>
<div>
<h2 className="text-lg font-bold text-slate-900">Infographic Generator</h2>
<p className="text-sm text-slate-500">Visual Insights</p>
</div>
</div>
<div className="flex items-center gap-2">
<button onClick={() => setScale(s => Math.max(0.5, s - 0.1))} className="p-2 hover:bg-slate-100 rounded-lg"><ZoomOut size={20} /></button>
<span className="text-xs font-mono w-12 text-center">{Math.round(scale * 100)}%</span>
<button onClick={() => setScale(s => Math.min(2, s + 0.1))} className="p-2 hover:bg-slate-100 rounded-lg"><ZoomIn size={20} /></button>
<div className="h-6 w-px bg-slate-200 mx-2"></div>
<button onClick={handleDownload} disabled={status !== 'ready'} className="flex items-center gap-2 px-4 py-2 bg-slate-900 text-white rounded-lg hover:bg-slate-800 transition-colors disabled:opacity-50">
<Download size={18} /> Export PNG
</button>
<button onClick={onClose} className="p-2 hover:bg-slate-100 rounded-full text-slate-400 ml-2">
<X size={20} />
</button>
</div>
</div>
{/* Canvas Area */}
<div className="flex-1 overflow-auto bg-slate-200 p-8 flex items-start justify-center">
{status === 'loading' && (
<div className="flex flex-col items-center justify-center h-full text-slate-500">
<Loader2 size={48} className="animate-spin text-indigo-500 mb-6" />
<h3 className="text-xl font-medium text-slate-800">Designing Layout...</h3>
<p className="mt-2">Analyzing structure and composing visuals.</p>
</div>
)}
{status === 'error' && (
<div className="flex flex-col items-center justify-center h-full text-red-500">
<p>Failed to generate infographic.</p>
<button onClick={handleGenerate} className="mt-4 underline flex items-center gap-2"><RefreshCw size={16}/> Retry</button>
</div>
)}
{status === 'ready' && (
<div
style={{ transform: `scale(${scale})`, transformOrigin: 'top center' }}
className="transition-transform duration-200 shadow-2xl"
>
{renderLayout()}
</div>
)}
</div>
</div>
</div>
);
};
export default InfographicModal;
|