GrantForge Bot
Deploy to Hugging Face
afd56bc
import React, { useState, useEffect, useMemo } from 'react';
import { motion } from 'framer-motion';
import { X, Printer, Loader2, GitCommit, FileText } from 'lucide-react';
import { previewProject } from '../../api/client';
import ReactMarkdown from 'react-markdown';
import * as Diff from 'diff';
interface LivePreviewProps {
projectId: string;
onClose: () => void;
}
const LivePreview: React.FC<LivePreviewProps> = ({ projectId, onClose }) => {
const [markdownData, setMarkdownData] = useState<string>('');
const [oldMarkdownData, setOldMarkdownData] = useState<string>('');
const [loading, setLoading] = useState(true);
const [viewMode, setViewMode] = useState<'current' | 'diff'>('current');
useEffect(() => {
previewProject(projectId).then(res => {
setMarkdownData(res.markdown || '');
setOldMarkdownData(res.old_markdown || '');
setLoading(false);
}).catch(() => setLoading(false));
}, [projectId]);
const handlePrint = () => {
window.print();
};
const renderDiff = () => {
if (!oldMarkdownData) return <ReactMarkdown>{markdownData}</ReactMarkdown>;
const diffResult = Diff.diffWordsWithSpace(oldMarkdownData, markdownData);
return (
<div style={{ whiteSpace: 'pre-wrap' }}>
{diffResult.map((part, index) => {
if (part.added) {
return <span key={index} style={{ backgroundColor: '#dcfce7', color: '#166534', textDecoration: 'none' }}>{part.value}</span>;
}
if (part.removed) {
return <del key={index} style={{ backgroundColor: '#fee2e2', color: '#991b1b', textDecoration: 'line-through' }}>{part.value}</del>;
}
return <span key={index}>{part.value}</span>;
})}
</div>
);
};
return (
<div style={{ position: 'fixed', inset: 0, zIndex: 9999, background: 'rgba(0,0,0,0.6)', backdropFilter: 'blur(5px)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '2rem' }}>
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
style={{ width: '100%', maxWidth: '900px', height: '100%', background: 'var(--bg-secondary)', borderRadius: '16px', display: 'flex', flexDirection: 'column', overflow: 'hidden', border: '1px solid rgba(255,255,255,0.1)', boxShadow: '0 20px 40px rgba(0,0,0,0.3)' }}
>
{/* Header */}
<div style={{ padding: '1.5rem 2rem', background: 'rgba(0,0,0,0.3)', borderBottom: '1px solid rgba(255,255,255,0.05)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ display: 'flex', gap: '2rem', alignItems: 'center' }}>
<h2 style={{ fontSize: '1.25rem', margin: 0, color: 'var(--text-primary)' }}>Podgląd Głównego Dokumentu</h2>
<div style={{ display: 'flex', background: 'rgba(0,0,0,0.5)', padding: '0.25rem', borderRadius: '8px' }} className="hide-on-print">
<button
onClick={() => setViewMode('current')}
style={{ padding: '0.4rem 0.8rem', background: viewMode === 'current' ? 'rgba(59, 130, 246, 0.2)' : 'transparent', color: viewMode === 'current' ? '#60A5FA' : 'var(--text-secondary)', border: 'none', borderRadius: '6px', fontSize: '0.85rem', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '0.4rem', transition: '0.2s' }}
>
<FileText size={14} /> Aktualna wersja
</button>
<button
onClick={() => setViewMode('diff')}
style={{ padding: '0.4rem 0.8rem', background: viewMode === 'diff' ? 'rgba(16, 185, 129, 0.2)' : 'transparent', color: viewMode === 'diff' ? 'var(--accent-green)' : 'var(--text-secondary)', border: 'none', borderRadius: '6px', fontSize: '0.85rem', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '0.4rem', transition: '0.2s' }}
>
<GitCommit size={14} /> Śledzenie zmian
</button>
</div>
</div>
<div style={{ display: 'flex', gap: '1rem' }} className="hide-on-print">
<button onClick={handlePrint} className="btn hover-lift" style={{ background: 'rgba(16, 185, 129, 0.1)', color: 'var(--accent-green)', border: '1px solid rgba(16, 185, 129, 0.3)', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<Printer size={16}/> Eksportuj / Drukuj
</button>
<button onClick={onClose} className="btn hover-lift" style={{ width: '40px', height: '40px', padding: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'rgba(239, 68, 68, 0.15)', color: '#EF4444', border: '1px solid rgba(239, 68, 68, 0.3)', borderRadius: '8px' }}>
<X size={20}/>
</button>
</div>
</div>
{/* Content */}
<div style={{ flex: 1, overflowY: 'auto', padding: '3rem 4rem', background: '#e5e7eb', color: '#111827' }} className="print-area">
{loading ? (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', color: '#6b7280' }}>
<Loader2 size={40} className="spin" style={{ marginBottom: '1rem', color: 'var(--accent-green)' }}/>
Generowanie pełnego dokumentu wniosku...
</div>
) : (
<div style={{ maxWidth: '800px', margin: '0 auto', fontSize: '14pt', lineHeight: 1.6, fontFamily: 'serif' }} className="markdown-prose">
{markdownData ? (
viewMode === 'current' ? <ReactMarkdown>{markdownData}</ReactMarkdown> : renderDiff()
) : (
<p style={{ textAlign: 'center', color: '#6b7280' }}>Brak połączonych sekcji wniosku. Najpierw wygeneruj sekcje by uzyskać całościowy zarys.</p>
)}
</div>
)}
</div>
</motion.div>
<style>{`
@media print {
body * { visibility: hidden; }
.print-area, .print-area * { visibility: visible; }
.print-area { position: absolute; left: 0; top: 0; width: 100vw; height: auto; background: white !important; color: black !important; padding: 0 !important; }
.hide-on-print { display: none !important; }
}
.markdown-prose h1 { font-size: 24pt; margin-bottom: 2rem; border-bottom: 2px solid #ccc; padding-bottom: 0.5rem; }
.markdown-prose h2 { font-size: 18pt; margin-top: 2rem; margin-bottom: 1rem; color: #1f2937; }
.markdown-prose h3 { font-size: 14pt; margin-top: 1.5rem; margin-bottom: 0.5rem; color: #374151; }
.markdown-prose p { margin-bottom: 1rem; }
.markdown-prose ul { margin-left: 2rem; margin-bottom: 1rem; }
.spin { animation: spin 1s linear infinite; }
`}</style>
</div>
);
};
export default LivePreview;