DivYonko commited on
Commit ·
cdf4792
1
Parent(s): e7565ac
Replace broken PDF with html2canvas screenshot export on Stats page
Browse files- app.py +1 -19
- frontend/streamlit_app.py +1 -19
- pages/stats.py +102 -0
app.py
CHANGED
|
@@ -1091,25 +1091,7 @@ with st.sidebar:
|
|
| 1091 |
mime="text/csv",
|
| 1092 |
key=f"dl_{_rkey}",
|
| 1093 |
)
|
| 1094 |
-
#
|
| 1095 |
-
try:
|
| 1096 |
-
from ml.report_generator import generate_report
|
| 1097 |
-
_stream_title = _s.get("video_title") or _s.get("video_id") or _rkey
|
| 1098 |
-
_pdf_bytes = generate_report(
|
| 1099 |
-
all_data=_dl_rows,
|
| 1100 |
-
stream_title=_stream_title,
|
| 1101 |
-
msg_limit=st.session_state.get("msg_limit", 100),
|
| 1102 |
-
)
|
| 1103 |
-
_pdf_fname = f"livepulse_report_{_rkey}_{_ts}.pdf"
|
| 1104 |
-
st.download_button(
|
| 1105 |
-
label=f"\U0001f4cb {_slabel} PDF Report",
|
| 1106 |
-
data=_pdf_bytes,
|
| 1107 |
-
file_name=_pdf_fname,
|
| 1108 |
-
mime="application/pdf",
|
| 1109 |
-
key=f"pdf_{_rkey}",
|
| 1110 |
-
)
|
| 1111 |
-
except Exception as _pdf_err:
|
| 1112 |
-
st.markdown(f'<div style="font-size:0.7rem;color:var(--text-3);">PDF unavailable: {_pdf_err}</div>', unsafe_allow_html=True)
|
| 1113 |
else:
|
| 1114 |
st.markdown(f'<div style="font-size:0.72rem;color:var(--text-3);">{_slabel}: no data yet</div>', unsafe_allow_html=True)
|
| 1115 |
else:
|
|
|
|
| 1091 |
mime="text/csv",
|
| 1092 |
key=f"dl_{_rkey}",
|
| 1093 |
)
|
| 1094 |
+
# PDF button removed — use the Export button on the Stats page instead
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1095 |
else:
|
| 1096 |
st.markdown(f'<div style="font-size:0.72rem;color:var(--text-3);">{_slabel}: no data yet</div>', unsafe_allow_html=True)
|
| 1097 |
else:
|
frontend/streamlit_app.py
CHANGED
|
@@ -752,25 +752,7 @@ with st.sidebar:
|
|
| 752 |
mime="text/csv",
|
| 753 |
key=f"dl_{_rkey}",
|
| 754 |
)
|
| 755 |
-
#
|
| 756 |
-
try:
|
| 757 |
-
from ml.report_generator import generate_report
|
| 758 |
-
_stream_title = _s.get("video_title") or _s.get("video_id") or _rkey
|
| 759 |
-
_pdf_bytes = generate_report(
|
| 760 |
-
all_data=_dl_rows,
|
| 761 |
-
stream_title=_stream_title,
|
| 762 |
-
msg_limit=msg_limit,
|
| 763 |
-
)
|
| 764 |
-
_pdf_fname = f"livepulse_report_{_rkey}_{_ts}.pdf"
|
| 765 |
-
st.download_button(
|
| 766 |
-
label=f"\U0001f4cb {_slabel} PDF Report",
|
| 767 |
-
data=_pdf_bytes,
|
| 768 |
-
file_name=_pdf_fname,
|
| 769 |
-
mime="application/pdf",
|
| 770 |
-
key=f"pdf_{_rkey}",
|
| 771 |
-
)
|
| 772 |
-
except Exception as _pdf_err:
|
| 773 |
-
st.markdown(f'<div style="font-size:0.7rem;color:var(--text-3);">PDF unavailable: {_pdf_err}</div>', unsafe_allow_html=True)
|
| 774 |
else:
|
| 775 |
st.markdown(f'<div style="font-size:0.72rem;color:var(--text-3);">{_slabel}: no data yet</div>', unsafe_allow_html=True)
|
| 776 |
else:
|
|
|
|
| 752 |
mime="text/csv",
|
| 753 |
key=f"dl_{_rkey}",
|
| 754 |
)
|
| 755 |
+
# PDF button removed — use the Export button on the Stats page instead
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 756 |
else:
|
| 757 |
st.markdown(f'<div style="font-size:0.72rem;color:var(--text-3);">{_slabel}: no data yet</div>', unsafe_allow_html=True)
|
| 758 |
else:
|
pages/stats.py
CHANGED
|
@@ -976,6 +976,108 @@ elif len(st.session_state.streams) > 1:
|
|
| 976 |
st.divider()
|
| 977 |
st.info("Add video IDs to your extra stream slots and click ▶ Start to enable multi-stream comparison.")
|
| 978 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 979 |
# -- AUTO REFRESH ---------------------------------------------
|
| 980 |
if auto_refresh:
|
| 981 |
time.sleep(refresh_rate)
|
|
|
|
| 976 |
st.divider()
|
| 977 |
st.info("Add video IDs to your extra stream slots and click ▶ Start to enable multi-stream comparison.")
|
| 978 |
|
| 979 |
+
# -- SCREENSHOT / PDF EXPORT ----------------------------------
|
| 980 |
+
st.divider()
|
| 981 |
+
st.markdown(
|
| 982 |
+
'<div class="sec-hdr"><span class="sec-ttl">Export</span><span class="sec-pill">Stats Page</span></div>',
|
| 983 |
+
unsafe_allow_html=True
|
| 984 |
+
)
|
| 985 |
+
|
| 986 |
+
import streamlit.components.v1 as components
|
| 987 |
+
components.html("""
|
| 988 |
+
<div style="text-align:center; padding: 8px 0;">
|
| 989 |
+
<button id="screenshotBtn" style="
|
| 990 |
+
background: linear-gradient(135deg, #7c3aed, #4f46e5);
|
| 991 |
+
color: #fff;
|
| 992 |
+
border: none;
|
| 993 |
+
border-radius: 10px;
|
| 994 |
+
padding: 10px 28px;
|
| 995 |
+
font-size: 14px;
|
| 996 |
+
font-weight: 600;
|
| 997 |
+
font-family: 'Space Grotesk', sans-serif;
|
| 998 |
+
cursor: pointer;
|
| 999 |
+
box-shadow: 0 4px 16px rgba(124,58,237,0.4);
|
| 1000 |
+
transition: transform 0.2s;
|
| 1001 |
+
"
|
| 1002 |
+
onmouseover="this.style.transform='translateY(-2px)'"
|
| 1003 |
+
onmouseout="this.style.transform='translateY(0)'"
|
| 1004 |
+
onclick="captureAndDownload()">
|
| 1005 |
+
📷 Download Stats as PDF
|
| 1006 |
+
</button>
|
| 1007 |
+
<div id="statusMsg" style="margin-top:8px; font-size:12px; color:#94a3b8;"></div>
|
| 1008 |
+
</div>
|
| 1009 |
+
|
| 1010 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
|
| 1011 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
|
| 1012 |
+
<script>
|
| 1013 |
+
async function captureAndDownload() {
|
| 1014 |
+
const btn = document.getElementById('screenshotBtn');
|
| 1015 |
+
const msg = document.getElementById('statusMsg');
|
| 1016 |
+
btn.disabled = true;
|
| 1017 |
+
btn.textContent = 'Capturing...';
|
| 1018 |
+
msg.textContent = 'Please wait, capturing the page...';
|
| 1019 |
+
|
| 1020 |
+
try {
|
| 1021 |
+
// Target the main Streamlit content area in the parent frame
|
| 1022 |
+
const target = window.parent.document.querySelector('[data-testid="stMain"]')
|
| 1023 |
+
|| window.parent.document.querySelector('.main')
|
| 1024 |
+
|| window.parent.document.body;
|
| 1025 |
+
|
| 1026 |
+
const canvas = await window.parent.html2canvas(target, {
|
| 1027 |
+
scale: 1.5,
|
| 1028 |
+
useCORS: true,
|
| 1029 |
+
allowTaint: true,
|
| 1030 |
+
backgroundColor: '#07070f',
|
| 1031 |
+
logging: false,
|
| 1032 |
+
windowWidth: target.scrollWidth,
|
| 1033 |
+
windowHeight: target.scrollHeight,
|
| 1034 |
+
scrollX: 0,
|
| 1035 |
+
scrollY: 0,
|
| 1036 |
+
});
|
| 1037 |
+
|
| 1038 |
+
const imgData = canvas.toDataURL('image/png', 0.95);
|
| 1039 |
+
const { jsPDF } = window.parent.jspdf;
|
| 1040 |
+
const pdf = new jsPDF({
|
| 1041 |
+
orientation: canvas.width > canvas.height ? 'l' : 'p',
|
| 1042 |
+
unit: 'px',
|
| 1043 |
+
format: [canvas.width, canvas.height],
|
| 1044 |
+
compress: true,
|
| 1045 |
+
});
|
| 1046 |
+
|
| 1047 |
+
pdf.addImage(imgData, 'PNG', 0, 0, canvas.width, canvas.height);
|
| 1048 |
+
const ts = new Date().toISOString().slice(0,16).replace('T','_').replace(':','-');
|
| 1049 |
+
pdf.save('livepulse_stats_' + ts + '.pdf');
|
| 1050 |
+
|
| 1051 |
+
btn.textContent = 'Download Stats as PDF';
|
| 1052 |
+
btn.disabled = false;
|
| 1053 |
+
msg.textContent = 'Done! Check your downloads.';
|
| 1054 |
+
setTimeout(() => { msg.textContent = ''; }, 4000);
|
| 1055 |
+
} catch(e) {
|
| 1056 |
+
btn.textContent = 'Download Stats as PDF';
|
| 1057 |
+
btn.disabled = false;
|
| 1058 |
+
msg.textContent = 'Error: ' + e.message;
|
| 1059 |
+
console.error(e);
|
| 1060 |
+
}
|
| 1061 |
+
}
|
| 1062 |
+
|
| 1063 |
+
// Load libraries into parent frame if not already there
|
| 1064 |
+
function loadScript(src, globalName) {
|
| 1065 |
+
return new Promise((resolve) => {
|
| 1066 |
+
if (window.parent[globalName]) { resolve(); return; }
|
| 1067 |
+
const s = window.parent.document.createElement('script');
|
| 1068 |
+
s.src = src;
|
| 1069 |
+
s.onload = resolve;
|
| 1070 |
+
window.parent.document.head.appendChild(s);
|
| 1071 |
+
});
|
| 1072 |
+
}
|
| 1073 |
+
|
| 1074 |
+
(async () => {
|
| 1075 |
+
await loadScript('https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js', 'html2canvas');
|
| 1076 |
+
await loadScript('https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js', 'jspdf');
|
| 1077 |
+
})();
|
| 1078 |
+
</script>
|
| 1079 |
+
""", height=90)
|
| 1080 |
+
|
| 1081 |
# -- AUTO REFRESH ---------------------------------------------
|
| 1082 |
if auto_refresh:
|
| 1083 |
time.sleep(refresh_rate)
|