Spaces:
Running
Running
| import React, { useState, useEffect } from 'react'; | |
| import ReactMarkdown from 'react-markdown'; | |
| import { Sparkles, Printer, FileDown, RefreshCw, Layers, ShieldCheck, ShieldAlert, AlertTriangle, History, Save } from 'lucide-react'; | |
| import { compileFinalDocument, auditFinalDocument, getProjectVersions, createProjectVersion, exportProjectPDF, exportProjectDOCX } from '../../api/client'; | |
| import toast from 'react-hot-toast'; | |
| interface FinalDocumentPanelProps { | |
| project: any; | |
| onUpdate: () => void; | |
| } | |
| const FinalDocumentPanel: React.FC<FinalDocumentPanelProps> = ({ project, onUpdate }) => { | |
| const [isCompiling, setIsCompiling] = useState(false); | |
| const [isAuditing, setIsAuditing] = useState(false); | |
| const [isCreatingVersion, setIsCreatingVersion] = useState(false); | |
| const [approvedOnly, setApprovedOnly] = useState(false); | |
| const [showAuditDetails, setShowAuditDetails] = useState(false); | |
| const [acceptedAI, setAcceptedAI] = useState(false); | |
| const auditPanelRef = React.useRef<HTMLDivElement>(null); | |
| const [versions, setVersions] = useState<any[]>([]); | |
| const loadVersions = async () => { | |
| try { | |
| const data = await getProjectVersions(project.id); | |
| setVersions(data); | |
| } catch (err) { | |
| console.error("Failed to load versions", err); | |
| } | |
| }; | |
| useEffect(() => { | |
| loadVersions(); | |
| }, [project.id]); | |
| const formatDate = (dateStr: string) => { | |
| return new Intl.DateTimeFormat('pl-PL', { day: 'numeric', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit' }).format(new Date(dateStr)); | |
| }; | |
| const handleCreateVersion = async () => { | |
| try { | |
| if (!window.confirm("Czy chcesz zapisać obecną wersję wniosku jako nową migawkę?")) return; | |
| const title = prompt("Podaj nazwę dla tej wersji (opcjonalnie):"); | |
| if (title === null) return; // cancelled | |
| setIsCreatingVersion(true); | |
| toast.loading("Zapisywanie wersji...", { id: "version" }); | |
| const result = await createProjectVersion(project.id, title); | |
| if (result && result.version_number) { | |
| toast.success(`Wersja V${result.version_number} została zapisana pomyślnie.`, { id: "version" }); | |
| } else { | |
| toast.success(`Wersja została zapisana pomyślnie.`, { id: "version" }); | |
| } | |
| loadVersions(); | |
| } catch (error: any) { | |
| let errMsg = "Błąd podczas zapisu wersji."; | |
| if (error.response?.data?.detail) { | |
| const detail = error.response.data.detail; | |
| errMsg = typeof detail === 'string' ? detail : (detail.message || JSON.stringify(detail)); | |
| } | |
| toast.error(errMsg, { id: "version" }); | |
| } finally { | |
| setIsCreatingVersion(false); | |
| } | |
| }; | |
| const handleDownloadVers = async (type: 'pdf' | 'docx', v: any) => { | |
| toast(`Pobierasz wersję V${v.version_number} z ${formatDate(v.created_at).split(',')[0]}`, { icon: '⬇️' }); | |
| try { | |
| if (type === 'pdf') { | |
| await exportProjectPDF(project.id, v.id); | |
| } else { | |
| await exportProjectDOCX(project.id, false, v.id); | |
| } | |
| } catch (e: any) { | |
| alert(e.message); | |
| } | |
| }; | |
| const handleAudit = async () => { | |
| try { | |
| setIsAuditing(true); | |
| toast.loading("Przeprowadzanie weryfikacji krzyżowej (Audyt)...", { id: "audit" }); | |
| await auditFinalDocument(project.id); | |
| toast.success("Audyt zakończony.", { id: "audit" }); | |
| setShowAuditDetails(true); | |
| setTimeout(() => { | |
| auditPanelRef.current?.scrollIntoView({ behavior: 'smooth' }); | |
| }, 100); | |
| onUpdate(); | |
| } catch (error: any) { | |
| let errMsg = "Błąd podczas audytu wniosku."; | |
| if (error.response?.data?.detail) { | |
| const detail = error.response.data.detail; | |
| errMsg = typeof detail === 'string' ? detail : (detail.message || JSON.stringify(detail)); | |
| } | |
| toast.error(errMsg, { id: "audit" }); | |
| } finally { | |
| setIsAuditing(false); | |
| } | |
| }; | |
| const handleCompile = async () => { | |
| try { | |
| setIsCompiling(true); | |
| toast.loading("Kompilowanie i redagowanie wniosku...", { id: "compile" }); | |
| const result = await compileFinalDocument(project.id, approvedOnly); | |
| toast.success(`Wniosek gotowy! Połączono ${result.sections_used} sekcji. Pamiętaj o weryfikacji przez Audytor.`, { id: "compile" }); | |
| onUpdate(); // Odśwież dane w nadrzędnym widoku | |
| } catch (error: any) { | |
| let errMsg = "Błąd podczas kompilacji lub audytu wniosku."; | |
| if (error.response?.data?.detail) { | |
| const detail = error.response.data.detail; | |
| errMsg = typeof detail === 'string' ? detail : (detail.message || JSON.stringify(detail)); | |
| } | |
| toast.error(errMsg, { id: "compile" }); | |
| } finally { | |
| setIsCompiling(false); | |
| } | |
| }; | |
| const handlePrint = () => { | |
| window.print(); | |
| }; | |
| const handleDownloadTxt = () => { | |
| if (!project.final_document_markdown) return; | |
| const blob = new Blob([project.final_document_markdown], { type: 'text/markdown;charset=utf-8' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `Wniosek_${project.title.replace(/\s+/g, '_')}.md`; | |
| a.click(); | |
| URL.revokeObjectURL(url); | |
| }; | |
| return ( | |
| <div style={{ padding: '1rem', height: '100%', display: 'flex', flexDirection: 'column' }}> | |
| <div className="print-hide" style={{ display: 'flex', gap: '2rem', marginBottom: '1.5rem' }}> | |
| <div style={{ flex: 1, display: 'flex', flexDirection: 'column', background: 'rgba(255,255,255,0.02)', padding: '1.5rem', borderRadius: '12px', border: '1px solid rgba(255,255,255,0.05)' }}> | |
| <div style={{ marginBottom: '1.5rem' }}> | |
| <h2 style={{ fontSize: '1.4rem', fontWeight: 700, margin: '0 0 0.5rem 0', display: 'flex', alignItems: 'center', gap: '0.5rem' }}> | |
| <Sparkles size={20} color="var(--accent-green)" /> Gotowy Wniosek | |
| </h2> | |
| <p style={{ margin: 0, color: 'var(--text-secondary)', fontSize: '0.9rem' }}> | |
| Połącz wszystkie sekcje robocze, przepuść przez korektorę redaktorską LLM i wyeksportuj swój wniosek. | |
| </p> | |
| </div> | |
| <div style={{ display: 'flex', gap: '1rem', alignItems: 'center', flexWrap: 'wrap' }}> | |
| <div style={{ display: 'flex', flexDirection: 'column', gap: '0.4rem', marginRight: '0.5rem' }}> | |
| <label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', fontSize: '0.85rem', color: 'var(--text-secondary)', cursor: 'pointer' }}> | |
| <input type="checkbox" checked={approvedOnly} onChange={(e) => setApprovedOnly(e.target.checked)} /> | |
| Tylko zatwierdzone sekcje | |
| </label> | |
| <label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', fontSize: '0.85rem', color: 'var(--accent-green)', cursor: 'pointer', fontWeight: 600 }}> | |
| <input type="checkbox" checked={acceptedAI} onChange={(e) => setAcceptedAI(e.target.checked)} /> | |
| Akceptuję, że AI to tylko asystent | |
| </label> | |
| </div> | |
| <button onClick={handleCompile} disabled={isCompiling || !acceptedAI} className="btn" style={{ background: acceptedAI ? 'var(--accent-blue)' : 'rgba(255,255,255,0.1)', color: acceptedAI ? 'white' : 'var(--text-muted)' }}> | |
| {isCompiling ? <RefreshCw className="spin" size={16} /> : <Layers size={16} />} | |
| {project.final_document_markdown ? "Odśwież wniosek końcowy" : "Wygeneruj wniosek końcowy"} | |
| </button> | |
| {project.final_document_markdown && ( | |
| <> | |
| <button | |
| onClick={handleAudit} | |
| disabled={isAuditing || !acceptedAI} | |
| className="btn hover-lift" | |
| style={{ background: 'var(--accent-purple)', color: 'white', opacity: acceptedAI ? 1 : 0.5 }} | |
| > | |
| {isAuditing ? <RefreshCw className="spin" size={16} /> : <ShieldCheck size={16} />} | |
| Przeprowadź Globalny Audyt | |
| </button> | |
| <button | |
| onClick={() => { | |
| if (project.final_document_audit_result && project.final_document_audit_result.overall_score < 60) { | |
| if(!window.confirm("UWAGA: Dokument wykazuje krytyczne ryzyko odrzucenia (wynik < 60). Kontynuować pobieranie?")) return; | |
| } else if (!project.final_document_audit_result) { | |
| if(!window.confirm("Dokument niezweryfikowany Audytorem - Zwiększone ryzyko odrzucenia. Pobrać mimo to?")) return; | |
| } | |
| handlePrint(); | |
| }} | |
| disabled={!acceptedAI} | |
| className="btn hover-lift" | |
| style={{ background: (project.final_document_audit_result && project.final_document_audit_result.overall_score < 60) ? 'rgba(239, 68, 68, 0.2)' : 'rgba(255,255,255,0.1)', opacity: acceptedAI ? 1 : 0.5 }} | |
| > | |
| <Printer size={16} /> Drukuj | |
| </button> | |
| <button | |
| onClick={() => { | |
| if (project.final_document_audit_result && project.final_document_audit_result.overall_score < 60) { | |
| if(!window.confirm("UWAGA: Dokument wykazuje krytyczne ryzyko odrzucenia (wynik < 60). Kontynuować pobieranie?")) return; | |
| } else if (!project.final_document_audit_result) { | |
| if(!window.confirm("Dokument niezweryfikowany Audytorem - Zwiększone ryzyko odrzucenia. Pobrać mimo to?")) return; | |
| } | |
| handleDownloadTxt(); | |
| }} | |
| disabled={!acceptedAI} | |
| className="btn hover-lift" | |
| style={{ background: (project.final_document_audit_result && project.final_document_audit_result.overall_score < 60) ? 'rgba(239, 68, 68, 0.2)' : 'rgba(255,255,255,0.1)', opacity: acceptedAI ? 1 : 0.5 }} | |
| > | |
| <FileDown size={16} /> Markdown | |
| </button> | |
| </> | |
| )} | |
| </div> | |
| </div> | |
| <div style={{ width: '320px', background: 'rgba(255,255,255,0.02)', padding: '1.5rem', borderRadius: '12px', border: '1px solid rgba(255,255,255,0.05)', display: 'flex', flexDirection: 'column', maxHeight: '300px' }}> | |
| <h3 style={{ margin: '0 0 1rem 0', fontSize: '1.1rem', display: 'flex', alignItems: 'center', gap: '0.5rem', color: 'var(--text-primary)' }}> | |
| <History size={18} color="var(--accent-blue)" /> Historia Wersji | |
| </h3> | |
| <button onClick={handleCreateVersion} disabled={isCreatingVersion} className="btn" style={{ background: 'rgba(59, 130, 246, 0.1)', color: 'var(--accent-blue)', border: '1px solid rgba(59, 130, 246, 0.2)', marginBottom: '1rem', padding: '0.4rem 0.8rem', fontSize: '0.85rem' }}> | |
| {isCreatingVersion ? <RefreshCw className="spin" size={14} /> : <Save size={14} />} Zapisz obecny stan | |
| </button> | |
| <div style={{ overflowY: 'auto', flex: 1, display: 'flex', flexDirection: 'column', gap: '0.5rem' }} className="hide-scrollbar"> | |
| {versions.map((v, idx) => ( | |
| <div key={v.id} style={{ background: 'rgba(0,0,0,0.2)', padding: '0.8rem', borderRadius: '8px', fontSize: '0.85rem' }}> | |
| <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.4rem', borderBottom: '1px solid rgba(255,255,255,0.05)', paddingBottom: '0.4rem' }}> | |
| <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}> | |
| <strong style={{ color: '#fff' }}>V{v.version_number}</strong> | |
| {idx === 0 && <span style={{ background: 'var(--accent-green)', color: 'white', padding: '0.1rem 0.4rem', borderRadius: '4px', fontSize: '0.65rem', fontWeight: 'bold' }}>Bieżąca</span>} | |
| </div> | |
| <span style={{ color: 'var(--text-muted)' }}>{formatDate(v.created_at)}</span> | |
| </div> | |
| {v.title && <div style={{ color: 'var(--accent-blue)', marginBottom: '0.4rem', fontStyle: 'italic', fontSize: '0.8rem' }}>"{v.title}"</div>} | |
| <div style={{ display: 'flex', gap: '0.5rem' }}> | |
| <button onClick={() => handleDownloadVers('pdf', v)} style={{ background: 'transparent', border: 'none', color: 'var(--text-secondary)', cursor: 'pointer', padding: 0, fontSize: '0.8rem', outline: 'none' }} className="hover-text-blue">PDF</button> | |
| <span style={{color: 'var(--text-muted)'}}>•</span> | |
| <button onClick={() => handleDownloadVers('docx', v)} style={{ background: 'transparent', border: 'none', color: 'var(--text-secondary)', cursor: 'pointer', padding: 0, fontSize: '0.8rem', outline: 'none' }} className="hover-text-blue">DOCX</button> | |
| </div> | |
| </div> | |
| ))} | |
| {versions.length === 0 && <div style={{ color: 'var(--text-muted)', fontSize: '0.85rem', textAlign: 'center', marginTop: '1rem' }}>Brak zapisanych wersji.</div>} | |
| </div> | |
| </div> | |
| </div> | |
| <div className="print-only-container" style={{ flex: 1, background: '#fff', borderRadius: '12px', padding: '3rem', overflowY: 'auto', color: '#000', boxShadow: '0 10px 30px rgba(0,0,0,0.1)', fontFamily: 'Merriweather, "Times New Roman", serif' }}> | |
| {/* PANEL AUDYTU */} | |
| {!project.final_document_audit_result && project.final_document_markdown && ( | |
| <div className="print-hide" style={{ marginBottom: '2rem', padding: '1.5rem', borderRadius: '12px', background: 'rgba(245, 158, 11, 0.05)', border: '1px solid rgba(245, 158, 11, 0.2)' }}> | |
| <div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}> | |
| <AlertTriangle size={24} color="#f59e0b" /> | |
| <h3 style={{ margin: 0, fontWeight: 600, color: '#111', fontSize: '1.1rem' }}>Dokument niezweryfikowany Audytorem - Zwiększone ryzyko odrzucenia</h3> | |
| </div> | |
| </div> | |
| )} | |
| {project.final_document_audit_result && ( | |
| <div ref={auditPanelRef} className="print-hide" style={{ | |
| marginBottom: '2rem', | |
| padding: '1.5rem', | |
| borderRadius: '12px', | |
| background: project.final_document_audit_result.overall_score >= 80 ? 'rgba(34, 197, 94, 0.05)' : (project.final_document_audit_result.overall_score >= 60 ? 'rgba(234, 179, 8, 0.05)' : 'rgba(239, 68, 68, 0.05)'), | |
| border: `1px solid ${project.final_document_audit_result.overall_score >= 80 ? 'rgba(34,197,94,0.2)' : (project.final_document_audit_result.overall_score >= 60 ? 'rgba(234,179,8,0.2)' : 'rgba(239,68,68,0.2)')}` | |
| }}> | |
| <div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', marginBottom: '1rem' }}> | |
| {project.final_document_audit_result.overall_score >= 80 ? ( | |
| <ShieldCheck size={24} color="#22c55e" /> | |
| ) : ( | |
| <ShieldAlert size={24} color={project.final_document_audit_result.overall_score >= 60 ? "#eab308" : "#ef4444"} /> | |
| )} | |
| <h3 style={{ margin: 0, fontWeight: 600, color: '#111', fontSize: '1.2rem' }}> | |
| Audyt Zgodności Wniosku: {project.final_document_audit_result.overall_score >= 80 ? "Niski poziom ryzyka odrzucenia wniosku. Dokument jest dobrze przygotowany." : (project.final_document_audit_result.overall_score >= 60 ? "Średnie ryzyko. Zalecamy wprowadzenie poprawek zgodnie z poniższymi rekomendacjami przed złożeniem." : "Wysokie ryzyko odrzucenia. Silnie zalecamy wprowadzenie zmian. Eksport możliwy wyłącznie na własne ryzyko.")} | |
| </h3> | |
| <span style={{ marginLeft: 'auto', background: project.final_document_audit_result.overall_score >= 80 ? '#22c55e' : (project.final_document_audit_result.overall_score >= 60 ? '#eab308' : '#ef4444'), color: 'white', padding: '0.25rem 0.75rem', borderRadius: '20px', fontSize: '0.85rem', fontWeight: 600 }}> | |
| {project.final_document_audit_result.overall_score} / 100 | |
| </span> | |
| </div> | |
| {project.final_document_audit_result.issues?.length > 0 && ( | |
| <div style={{ marginTop: '1rem' }}> | |
| {(!showAuditDetails && project.final_document_audit_result.overall_score >= 80) ? ( | |
| <button | |
| onClick={() => setShowAuditDetails(true)} | |
| className="btn" | |
| style={{ background: 'rgba(0,0,0,0.05)', color: '#333', border: '1px solid rgba(0,0,0,0.1)', fontSize: '0.9rem' }} | |
| > | |
| Pokaż szczegóły audytu ({project.final_document_audit_result.issues.length} uwag) | |
| </button> | |
| ) : ( | |
| <div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}> | |
| {project.final_document_audit_result.issues.map((issue: any, idx: number) => ( | |
| <div key={idx} style={{ display: 'flex', gap: '0.75rem', alignItems: 'flex-start', background: '#f9f9f9', padding: '1rem', borderRadius: '8px', borderLeft: `4px solid ${issue.severity === 'high' ? '#ef4444' : issue.severity === 'medium' ? '#eab308' : '#3b82f6'}` }}> | |
| <AlertTriangle size={18} color={issue.severity === 'high' ? '#ef4444' : issue.severity === 'medium' ? '#eab308' : '#3b82f6'} style={{ marginTop: '2px', flexShrink: 0 }} /> | |
| <div style={{ flex: 1 }}> | |
| <strong style={{ display: 'inline-block', fontSize: '0.85rem', color: '#555', marginBottom: '0.5rem', textTransform: 'uppercase' }}>{issue.category}</strong> | |
| <p style={{ margin: 0, fontSize: '0.95rem', color: '#111', lineHeight: 1.5, marginBottom: issue.rule_citation || issue.recommendation ? '0.75rem' : 0 }}>{issue.message}</p> | |
| {issue.rule_citation && ( | |
| <div style={{ background: 'rgba(59, 130, 246, 0.08)', borderLeft: '3px solid #3b82f6', padding: '0.5rem 0.75rem', marginBottom: '0.5rem', fontSize: '0.85rem', color: '#1e3a8a', fontStyle: 'italic' }}> | |
| <strong>Podstawa / Reguła:</strong> {issue.rule_citation} | |
| </div> | |
| )} | |
| {issue.recommendation && ( | |
| <div style={{ background: 'rgba(34, 197, 94, 0.08)', borderLeft: '3px solid #22c55e', padding: '0.5rem 0.75rem', fontSize: '0.85rem', color: '#14532d' }}> | |
| <strong>Rekomendacja:</strong> {issue.recommendation} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ))} | |
| <button | |
| onClick={() => setShowAuditDetails(false)} | |
| className="btn" | |
| style={{ marginTop: '0.5rem', background: 'transparent', color: '#555', border: 'none', textDecoration: 'underline', alignSelf: 'flex-start', padding: 0 }} | |
| > | |
| Ukryj szczegóły | |
| </button> | |
| {project.final_document_audit_result.overall_score < 60 && ( | |
| <button | |
| onClick={() => { | |
| toast("Przejdź do zakładki 'Sekcje', aby poprawić treść.", { icon: "📝" }); | |
| }} | |
| className="btn" | |
| style={{ marginTop: '0.5rem', background: 'var(--accent-red)', color: 'white', border: 'none', alignSelf: 'flex-start' }} | |
| > | |
| Popraw zgodnie z audytem | |
| </button> | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| <div style={{ marginTop: '1rem', paddingTop: '1rem', borderTop: '1px solid rgba(0,0,0,0.05)', fontSize: '0.75rem', color: '#666', fontStyle: 'italic' }}> | |
| * Audyt jest wsparciem AI. Ostateczna odpowiedzialność za treść wniosku spoczywa na użytkowniku. | |
| </div> | |
| </div> | |
| )} | |
| {project.final_document_markdown ? ( | |
| <div className="final-document-content" style={{ maxWidth: '800px', margin: '0 auto', lineHeight: 1.8 }}> | |
| <div className="print-hide" style={{ textAlign: 'center', marginBottom: '2rem', borderBottom: '1px solid #eee', paddingBottom: '1rem' }}> | |
| <p style={{ color: '#666', fontSize: '0.85rem' }}>Podgląd wygenerowanego wniosku. Ostatnia generacja: {new Date(project.final_document_generated_at).toLocaleString('pl-PL')}</p> | |
| </div> | |
| <ReactMarkdown> | |
| {project.final_document_markdown} | |
| </ReactMarkdown> | |
| <div className="ai-disclaimer" style={{ | |
| marginTop: '3rem', | |
| padding: '1.25rem', | |
| background: '#f8fafc', | |
| borderLeft: '4px solid #94a3b8', | |
| borderRadius: '0 8px 8px 0', | |
| fontSize: '0.85rem', | |
| color: '#475569', | |
| lineHeight: 1.6 | |
| }}> | |
| <strong>Wygenerowano przy wsparciu AI</strong><br/> | |
| Ten dokument został częściowo utworzony przez sztuczną inteligencję na podstawie regulaminów urzędowych. Wiedza systemu aktualna na: 12 kwietnia 2026.<br/> | |
| Zawsze zweryfikuj treść samodzielnie przed złożeniem wniosku do instytucji publicznej. | |
| </div> | |
| </div> | |
| ) : ( | |
| <div style={{ height: '100%', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', color: '#999' }}> | |
| <Layers size={48} style={{ opacity: 0.2, marginBottom: '1rem' }} /> | |
| <h3 style={{ margin: 0, fontWeight: 500 }}>Brak wygenerowanego wniosku</h3> | |
| <p style={{ fontSize: '0.9rem' }}>Kliknij na przycisk powyżej, aby scalić części i przygotować dokument.</p> | |
| </div> | |
| )} | |
| </div> | |
| <style>{` | |
| @media print { | |
| body * { | |
| visibility: hidden; | |
| } | |
| .print-only-container, .print-only-container * { | |
| visibility: visible; | |
| } | |
| .print-only-container { | |
| position: absolute; | |
| left: 0; | |
| top: 0; | |
| width: 100% ; | |
| height: auto ; | |
| overflow: visible ; | |
| padding: 0 ; | |
| margin: 0 ; | |
| box-shadow: none ; | |
| border-radius: 0 ; | |
| } | |
| .print-hide { | |
| display: none ; | |
| } | |
| } | |
| .final-document-content h1, | |
| .final-document-content h2, | |
| .final-document-content h3 { | |
| font-family: 'Inter', sans-serif; | |
| color: #111; | |
| margin-top: 2rem; | |
| } | |
| .final-document-content h1 { | |
| font-size: 2rem; | |
| border-bottom: 2px solid #222; | |
| padding-bottom: 0.5rem; | |
| } | |
| .spin { | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { 100% { transform: rotate(360deg); } } | |
| .hover-text-blue:hover { | |
| color: var(--accent-blue) ; | |
| } | |
| `}</style> | |
| </div> | |
| ); | |
| }; | |
| export default FinalDocumentPanel; | |