Spaces:
Running
Running
Commit
Β·
f102d2f
1
Parent(s):
bb68eb6
Implement synchronized scrolling feature in Streamlit app for document comparison
Browse files- Added CSS styles for synchronized scrolling layout and scrollbar customization.
- Enhanced JavaScript functionality for improved synchronization between original and redacted document views.
- Introduced a status indicator for active synchronization.
- Updated HTML structure to support new scrolling features and removed legacy code.
- src/streamlit_app.py +240 -92
src/streamlit_app.py
CHANGED
@@ -95,7 +95,215 @@ st.markdown("""
|
|
95 |
border-radius: 10px;
|
96 |
border: 1px solid #e9ecef;
|
97 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
98 |
</style>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
99 |
""", unsafe_allow_html=True)
|
100 |
|
101 |
# Configure root logger only once (avoid duplicate handlers on reruns)
|
@@ -385,130 +593,70 @@ if uploaded_files:
|
|
385 |
st.subheader("Original vs Redacted Content")
|
386 |
st.caption("Compare the original document content with the redacted version")
|
387 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
388 |
# Create a diff-like interface with synchronized scrolling and highlighting
|
389 |
diff_html = f"""
|
390 |
-
<div
|
391 |
-
<div
|
392 |
-
<div
|
393 |
π Original Document
|
394 |
</div>
|
395 |
-
<div id="original-content"
|
396 |
{create_diff_content(original_md, redacted_md, 'original')}
|
397 |
</div>
|
398 |
</div>
|
399 |
-
<div
|
400 |
-
<div
|
401 |
π Redacted Document
|
402 |
</div>
|
403 |
-
<div id="redacted-content"
|
404 |
{create_diff_content(original_md, redacted_md, 'redacted')}
|
405 |
</div>
|
406 |
</div>
|
407 |
</div>
|
408 |
-
|
409 |
-
<script>
|
410 |
-
// Synchronized scrolling
|
411 |
-
function syncScroll(sourceId, targetId) {{
|
412 |
-
const source = document.getElementById(sourceId);
|
413 |
-
const target = document.getElementById(targetId);
|
414 |
-
if (source && target) {{
|
415 |
-
target.scrollTop = source.scrollTop;
|
416 |
-
}}
|
417 |
-
}}
|
418 |
-
|
419 |
-
// Add scroll event listeners
|
420 |
-
document.addEventListener('DOMContentLoaded', function() {{
|
421 |
-
const original = document.getElementById('original-content');
|
422 |
-
const redacted = document.getElementById('redacted-content');
|
423 |
-
|
424 |
-
if (original && redacted) {{
|
425 |
-
original.addEventListener('scroll', function() {{
|
426 |
-
syncScroll('original-content', 'redacted-content');
|
427 |
-
}});
|
428 |
-
|
429 |
-
redacted.addEventListener('scroll', function() {{
|
430 |
-
syncScroll('redacted-content', 'original-content');
|
431 |
-
}});
|
432 |
-
}}
|
433 |
-
}});
|
434 |
-
</script>
|
435 |
"""
|
436 |
|
437 |
st.markdown(diff_html, unsafe_allow_html=True)
|
438 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
439 |
# Add legend for the diff highlighting
|
440 |
st.markdown("---")
|
441 |
-
col1, col2
|
442 |
with col1:
|
443 |
st.markdown("**π¨ Diff Legend:**")
|
444 |
st.markdown("π΄ **Red background** = Removed content")
|
445 |
st.markdown("π’ **Green background** = Added content")
|
446 |
-
st.markdown("βͺ **White background** = Unchanged content")
|
447 |
|
448 |
with col2:
|
449 |
-
st.markdown("**π Scrolling:**")
|
450 |
-
st.markdown("Scroll either panel to sync both views")
|
451 |
-
st.markdown("Content is aligned for easy comparison")
|
452 |
-
|
453 |
-
with col3:
|
454 |
st.markdown("**π‘ Tips:**")
|
455 |
st.markdown("Look for red-highlighted sections")
|
456 |
st.markdown("These show what was redacted")
|
457 |
st.markdown("Use scroll to navigate long documents")
|
458 |
|
459 |
-
|
460 |
-
st.markdown("---")
|
461 |
-
st.subheader("π₯ Download Options")
|
462 |
-
|
463 |
-
col1, col2, col3 = st.columns(3)
|
464 |
-
with col1:
|
465 |
-
st.download_button(
|
466 |
-
label="π₯ Download Original Markdown",
|
467 |
-
data=original_md,
|
468 |
-
file_name=f"{selected_file}_original.md",
|
469 |
-
mime="text/markdown",
|
470 |
-
use_container_width=True
|
471 |
-
)
|
472 |
-
with col2:
|
473 |
-
st.download_button(
|
474 |
-
label="π₯ Download Redacted Markdown",
|
475 |
-
data=redacted_md,
|
476 |
-
file_name=f"{selected_file}_redacted.md",
|
477 |
-
mime="text/markdown",
|
478 |
-
use_container_width=True
|
479 |
-
)
|
480 |
-
with col3:
|
481 |
-
# Create a detailed diff view
|
482 |
-
if st.button("π Show Detailed Differences", use_container_width=True):
|
483 |
-
st.session_state.show_diff = True
|
484 |
-
|
485 |
-
# Show detailed diff if requested
|
486 |
-
if st.session_state.get("show_diff", False):
|
487 |
-
st.markdown("---")
|
488 |
-
st.subheader("π Detailed Content Differences")
|
489 |
-
|
490 |
-
# Simple diff visualization
|
491 |
-
original_lines = original_md.split('\n')
|
492 |
-
redacted_lines = redacted_md.split('\n')
|
493 |
-
|
494 |
-
# Find removed lines
|
495 |
-
removed_lines = []
|
496 |
-
for line in original_lines:
|
497 |
-
if line.strip() and line not in redacted_lines:
|
498 |
-
removed_lines.append(line)
|
499 |
-
|
500 |
-
if removed_lines:
|
501 |
-
st.warning(f"**Removed {len(removed_lines)} lines containing medication information:**")
|
502 |
-
for i, line in enumerate(removed_lines[:10]): # Show first 10 removed lines
|
503 |
-
st.text(f"β {line[:100]}{'...' if len(line) > 100 else ''}")
|
504 |
-
if len(removed_lines) > 10:
|
505 |
-
st.text(f"... and {len(removed_lines) - 10} more lines")
|
506 |
-
else:
|
507 |
-
st.info("No significant differences detected in the text content")
|
508 |
|
509 |
with tab2:
|
510 |
-
st.subheader("Document Structure Analysis")
|
511 |
-
|
512 |
# Show JSON structure comparison
|
513 |
col1, col2 = st.columns(2)
|
514 |
|
|
|
95 |
border-radius: 10px;
|
96 |
border: 1px solid #e9ecef;
|
97 |
}
|
98 |
+
|
99 |
+
/* Synchronized scrolling styles */
|
100 |
+
.sync-scroll-container {
|
101 |
+
display: flex;
|
102 |
+
gap: 20px;
|
103 |
+
height: 600px;
|
104 |
+
font-family: 'Courier New', monospace;
|
105 |
+
font-size: 12px;
|
106 |
+
}
|
107 |
+
|
108 |
+
.sync-scroll-panel {
|
109 |
+
flex: 1;
|
110 |
+
border: 1px solid #ddd;
|
111 |
+
border-radius: 5px;
|
112 |
+
overflow: hidden;
|
113 |
+
display: flex;
|
114 |
+
flex-direction: column;
|
115 |
+
}
|
116 |
+
|
117 |
+
.sync-scroll-header {
|
118 |
+
background-color: #f8f9fa;
|
119 |
+
padding: 10px;
|
120 |
+
border-bottom: 1px solid #ddd;
|
121 |
+
font-weight: bold;
|
122 |
+
}
|
123 |
+
|
124 |
+
.sync-scroll-content {
|
125 |
+
flex: 1;
|
126 |
+
overflow-y: auto;
|
127 |
+
padding: 10px;
|
128 |
+
background-color: #fff;
|
129 |
+
scroll-behavior: smooth;
|
130 |
+
transition: scroll-top 0.1s ease-out;
|
131 |
+
}
|
132 |
+
|
133 |
+
/* Prevent scroll chaining */
|
134 |
+
.sync-scroll-content::-webkit-scrollbar {
|
135 |
+
width: 8px;
|
136 |
+
}
|
137 |
+
|
138 |
+
.sync-scroll-content::-webkit-scrollbar-track {
|
139 |
+
background: #f1f1f1;
|
140 |
+
}
|
141 |
+
|
142 |
+
.sync-scroll-content::-webkit-scrollbar-thumb {
|
143 |
+
background: #888;
|
144 |
+
border-radius: 4px;
|
145 |
+
}
|
146 |
+
|
147 |
+
.sync-scroll-content::-webkit-scrollbar-thumb:hover {
|
148 |
+
background: #555;
|
149 |
+
}
|
150 |
</style>
|
151 |
+
|
152 |
+
<script>
|
153 |
+
// Improved synchronized scrolling implementation with better debugging
|
154 |
+
console.log('Starting sync scroll setup...');
|
155 |
+
|
156 |
+
function setupSyncScroll() {
|
157 |
+
console.log('setupSyncScroll called');
|
158 |
+
|
159 |
+
// Wait for elements to be available
|
160 |
+
setTimeout(function() {
|
161 |
+
console.log('Looking for scroll elements...');
|
162 |
+
const originalContent = document.getElementById('original-content');
|
163 |
+
const redactedContent = document.getElementById('redacted-content');
|
164 |
+
|
165 |
+
console.log('Original content element:', originalContent);
|
166 |
+
console.log('Redacted content element:', redactedContent);
|
167 |
+
|
168 |
+
if (originalContent && redactedContent) {
|
169 |
+
console.log('Both elements found, setting up sync...');
|
170 |
+
|
171 |
+
let isScrolling = false;
|
172 |
+
let scrollTimeout;
|
173 |
+
|
174 |
+
function syncScroll(source, target) {
|
175 |
+
if (!isScrolling) {
|
176 |
+
isScrolling = true;
|
177 |
+
console.log('Syncing scroll from', source.id, 'to', target.id, 'scrollTop:', source.scrollTop);
|
178 |
+
target.scrollTop = source.scrollTop;
|
179 |
+
|
180 |
+
// Clear existing timeout
|
181 |
+
if (scrollTimeout) {
|
182 |
+
clearTimeout(scrollTimeout);
|
183 |
+
}
|
184 |
+
|
185 |
+
// Reset flag after a short delay
|
186 |
+
scrollTimeout = setTimeout(() => {
|
187 |
+
isScrolling = false;
|
188 |
+
console.log('Scroll sync completed');
|
189 |
+
}, 100);
|
190 |
+
}
|
191 |
+
}
|
192 |
+
|
193 |
+
// Remove existing listeners to prevent duplicates
|
194 |
+
if (originalContent._syncScrollHandler) {
|
195 |
+
originalContent.removeEventListener('scroll', originalContent._syncScrollHandler);
|
196 |
+
}
|
197 |
+
if (redactedContent._syncScrollHandler) {
|
198 |
+
redactedContent.removeEventListener('scroll', redactedContent._syncScrollHandler);
|
199 |
+
}
|
200 |
+
|
201 |
+
// Create new handlers
|
202 |
+
originalContent._syncScrollHandler = function(e) {
|
203 |
+
console.log('Original content scrolled:', e.target.scrollTop);
|
204 |
+
syncScroll(originalContent, redactedContent);
|
205 |
+
};
|
206 |
+
|
207 |
+
redactedContent._syncScrollHandler = function(e) {
|
208 |
+
console.log('Redacted content scrolled:', e.target.scrollTop);
|
209 |
+
syncScroll(redactedContent, originalContent);
|
210 |
+
};
|
211 |
+
|
212 |
+
// Add event listeners
|
213 |
+
originalContent.addEventListener('scroll', originalContent._syncScrollHandler, { passive: true });
|
214 |
+
redactedContent.addEventListener('scroll', redactedContent._syncScrollHandler, { passive: true });
|
215 |
+
|
216 |
+
console.log('Event listeners added successfully');
|
217 |
+
|
218 |
+
// Show status indicator
|
219 |
+
const statusElement = document.getElementById('sync-status');
|
220 |
+
if (statusElement) {
|
221 |
+
statusElement.style.display = 'block';
|
222 |
+
console.log('Status indicator shown');
|
223 |
+
}
|
224 |
+
|
225 |
+
// Test the synchronization
|
226 |
+
setTimeout(() => {
|
227 |
+
console.log('Testing scroll sync...');
|
228 |
+
console.log('Original scrollTop:', originalContent.scrollTop);
|
229 |
+
console.log('Redacted scrollTop:', redactedContent.scrollTop);
|
230 |
+
|
231 |
+
// Try a small scroll to test
|
232 |
+
originalContent.scrollTop = 10;
|
233 |
+
setTimeout(() => {
|
234 |
+
console.log('After test scroll - Original:', originalContent.scrollTop, 'Redacted:', redactedContent.scrollTop);
|
235 |
+
}, 50);
|
236 |
+
}, 200);
|
237 |
+
|
238 |
+
} else {
|
239 |
+
console.log('Elements not found, will retry...');
|
240 |
+
// Retry with exponential backoff
|
241 |
+
setTimeout(setupSyncScroll, 300);
|
242 |
+
}
|
243 |
+
}, 200);
|
244 |
+
}
|
245 |
+
|
246 |
+
// Multiple initialization strategies
|
247 |
+
function initializeSyncScroll() {
|
248 |
+
console.log('Initializing sync scroll...');
|
249 |
+
|
250 |
+
// Strategy 1: Immediate setup
|
251 |
+
setupSyncScroll();
|
252 |
+
|
253 |
+
// Strategy 2: Setup after DOM ready
|
254 |
+
if (document.readyState === 'loading') {
|
255 |
+
document.addEventListener('DOMContentLoaded', function() {
|
256 |
+
console.log('DOM loaded, setting up sync scroll...');
|
257 |
+
setupSyncScroll();
|
258 |
+
});
|
259 |
+
}
|
260 |
+
|
261 |
+
// Strategy 3: Setup after window load
|
262 |
+
window.addEventListener('load', function() {
|
263 |
+
console.log('Window loaded, setting up sync scroll...');
|
264 |
+
setupSyncScroll();
|
265 |
+
});
|
266 |
+
|
267 |
+
// Strategy 4: Periodic retry for first 10 seconds
|
268 |
+
let attempts = 0;
|
269 |
+
const maxAttempts = 20;
|
270 |
+
const retryInterval = setInterval(function() {
|
271 |
+
attempts++;
|
272 |
+
console.log('Retry attempt', attempts);
|
273 |
+
|
274 |
+
const originalContent = document.getElementById('original-content');
|
275 |
+
const redactedContent = document.getElementById('redacted-content');
|
276 |
+
|
277 |
+
if (originalContent && redactedContent) {
|
278 |
+
console.log('Elements found on retry, setting up...');
|
279 |
+
setupSyncScroll();
|
280 |
+
clearInterval(retryInterval);
|
281 |
+
} else if (attempts >= maxAttempts) {
|
282 |
+
console.log('Max retry attempts reached, giving up');
|
283 |
+
clearInterval(retryInterval);
|
284 |
+
}
|
285 |
+
}, 500);
|
286 |
+
}
|
287 |
+
|
288 |
+
// Start initialization
|
289 |
+
initializeSyncScroll();
|
290 |
+
|
291 |
+
// Listen for Streamlit-specific events
|
292 |
+
if (window.parent && window.parent.postMessage) {
|
293 |
+
console.log('Streamlit environment detected');
|
294 |
+
|
295 |
+
// Listen for any messages that might indicate a rerun
|
296 |
+
window.addEventListener('message', function(event) {
|
297 |
+
console.log('Received message:', event.data);
|
298 |
+
if (event.data && (event.data.type === 'streamlit:rerun' || event.data.type === 'streamlit:setComponentValue')) {
|
299 |
+
console.log('Streamlit rerun detected, reinitializing sync scroll...');
|
300 |
+
setTimeout(setupSyncScroll, 1000);
|
301 |
+
}
|
302 |
+
});
|
303 |
+
}
|
304 |
+
|
305 |
+
console.log('Sync scroll script loaded');
|
306 |
+
</script>
|
307 |
""", unsafe_allow_html=True)
|
308 |
|
309 |
# Configure root logger only once (avoid duplicate handlers on reruns)
|
|
|
593 |
st.subheader("Original vs Redacted Content")
|
594 |
st.caption("Compare the original document content with the redacted version")
|
595 |
|
596 |
+
# Add status indicator
|
597 |
+
st.markdown("""
|
598 |
+
<div id="sync-status" style="padding: 8px; background-color: #e8f5e8; border: 1px solid #4caf50; border-radius: 4px; margin-bottom: 10px; display: none;">
|
599 |
+
β
<strong>Synchronized scrolling is active</strong> - Scroll either panel to sync both views
|
600 |
+
</div>
|
601 |
+
""", unsafe_allow_html=True)
|
602 |
+
|
603 |
# Create a diff-like interface with synchronized scrolling and highlighting
|
604 |
diff_html = f"""
|
605 |
+
<div class="sync-scroll-container">
|
606 |
+
<div class="sync-scroll-panel">
|
607 |
+
<div class="sync-scroll-header">
|
608 |
π Original Document
|
609 |
</div>
|
610 |
+
<div id="original-content" class="sync-scroll-content">
|
611 |
{create_diff_content(original_md, redacted_md, 'original')}
|
612 |
</div>
|
613 |
</div>
|
614 |
+
<div class="sync-scroll-panel">
|
615 |
+
<div class="sync-scroll-header">
|
616 |
π Redacted Document
|
617 |
</div>
|
618 |
+
<div id="redacted-content" class="sync-scroll-content">
|
619 |
{create_diff_content(original_md, redacted_md, 'redacted')}
|
620 |
</div>
|
621 |
</div>
|
622 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
623 |
"""
|
624 |
|
625 |
st.markdown(diff_html, unsafe_allow_html=True)
|
626 |
|
627 |
+
# Add a hidden component to trigger JavaScript setup after Streamlit reruns
|
628 |
+
st.markdown("""
|
629 |
+
<script>
|
630 |
+
// Trigger setup after Streamlit rerun
|
631 |
+
if (window.parent && window.parent.postMessage) {
|
632 |
+
// Wait for Streamlit to finish rendering
|
633 |
+
setTimeout(function() {
|
634 |
+
setupSyncScroll();
|
635 |
+
}, 500);
|
636 |
+
}
|
637 |
+
</script>
|
638 |
+
""", unsafe_allow_html=True)
|
639 |
+
|
640 |
+
|
641 |
# Add legend for the diff highlighting
|
642 |
st.markdown("---")
|
643 |
+
col1, col2 = st.columns(2)
|
644 |
with col1:
|
645 |
st.markdown("**π¨ Diff Legend:**")
|
646 |
st.markdown("π΄ **Red background** = Removed content")
|
647 |
st.markdown("π’ **Green background** = Added content")
|
648 |
+
st.markdown("βͺ **White background** = Unchanged content")
|
649 |
|
650 |
with col2:
|
|
|
|
|
|
|
|
|
|
|
651 |
st.markdown("**π‘ Tips:**")
|
652 |
st.markdown("Look for red-highlighted sections")
|
653 |
st.markdown("These show what was redacted")
|
654 |
st.markdown("Use scroll to navigate long documents")
|
655 |
|
656 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
657 |
|
658 |
with tab2:
|
659 |
+
st.subheader("Document Structure Analysis")
|
|
|
660 |
# Show JSON structure comparison
|
661 |
col1, col2 = st.columns(2)
|
662 |
|