levalencia commited on
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.

Files changed (1) hide show
  1. 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 style="display: flex; gap: 20px; height: 600px; font-family: 'Courier New', monospace; font-size: 12px;">
391
- <div style="flex: 1; border: 1px solid #ddd; border-radius: 5px; overflow: hidden;">
392
- <div style="background-color: #f8f9fa; padding: 10px; border-bottom: 1px solid #ddd; font-weight: bold; color: #1f77b4;">
393
  πŸ“‹ Original Document
394
  </div>
395
- <div id="original-content" style="height: 540px; overflow-y: auto; padding: 10px; background-color: #fff;">
396
  {create_diff_content(original_md, redacted_md, 'original')}
397
  </div>
398
  </div>
399
- <div style="flex: 1; border: 1px solid #ddd; border-radius: 5px; overflow: hidden;">
400
- <div style="background-color: #f8f9fa; padding: 10px; border-bottom: 1px solid #ddd; font-weight: bold; color: #ff7f0e;">
401
  πŸ”’ Redacted Document
402
  </div>
403
- <div id="redacted-content" style="height: 540px; overflow-y: auto; padding: 10px; background-color: #fff;">
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, col3 = st.columns(3)
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
- # Add download buttons and other options
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