cjm-transcript-segment-align 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,624 @@
1
+ """Phase 2 combined step renderer: dual-column layout for Segment & Align"""
2
+
3
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/components/step_renderer.ipynb.
4
+
5
+ # %% auto #0
6
+ __all__ = ['DEBUG_COMBINED_RENDER', 'render_seg_mini_stats_badge', 'render_align_mini_stats_badge',
7
+ 'render_alignment_status_text', 'render_alignment_status', 'render_footer_inner_content',
8
+ 'render_combined_step']
9
+
10
+ # %% ../../nbs/components/step_renderer.ipynb #c3d4e5f6
11
+ from typing import Any, List
12
+
13
+ from fasthtml.common import Div, H2, P, Span, Input, Button
14
+
15
+ # DaisyUI components
16
+ from cjm_fasthtml_daisyui.components.data_display.badge import badge, badge_styles, badge_sizes
17
+ from cjm_fasthtml_daisyui.components.feedback.alert import alert, alert_colors
18
+ from cjm_fasthtml_daisyui.utilities.semantic_colors import bg_dui, text_dui, border_dui, ring_dui
19
+ from cjm_fasthtml_daisyui.utilities.border_radius import border_radius
20
+
21
+ # Tailwind utilities
22
+ from cjm_fasthtml_tailwind.utilities.spacing import p, m
23
+ from cjm_fasthtml_tailwind.utilities.sizing import w, h, min_h
24
+ from cjm_fasthtml_tailwind.utilities.typography import font_size, font_weight, uppercase, tracking
25
+ from cjm_fasthtml_tailwind.utilities.layout import overflow, position, inset, display_tw
26
+ from cjm_fasthtml_tailwind.utilities.borders import border
27
+ from cjm_fasthtml_tailwind.utilities.effects import opacity, ring
28
+ from cjm_fasthtml_tailwind.utilities.transitions_and_animation import transition, duration
29
+ from cjm_fasthtml_tailwind.utilities.flexbox_and_grid import (
30
+ flex_display, flex_direction, justify, items, gap, grow
31
+ )
32
+ from cjm_fasthtml_tailwind.core.base import combine_classes
33
+
34
+ # Interaction context
35
+ from cjm_fasthtml_interactions.core.context import InteractionContext
36
+
37
+ # Card stack library
38
+ from cjm_fasthtml_card_stack.components.controls import render_width_slider
39
+ from cjm_fasthtml_card_stack.components.states import render_loading_state
40
+ from cjm_fasthtml_card_stack.core.constants import DEFAULT_VISIBLE_COUNT, DEFAULT_CARD_WIDTH
41
+
42
+ # Local imports
43
+ # HTML IDs (page-specific)
44
+ from ..html_ids import CombinedHtmlIds
45
+ from cjm_transcript_segmentation.html_ids import SegmentationHtmlIds
46
+ from cjm_transcript_segmentation.models import TextSegment, SegmentationUrls
47
+ from cjm_transcript_vad_align.models import VADChunk, AlignmentUrls
48
+
49
+ # State extraction helpers (combined-level)
50
+ from cjm_transcript_segment_align.components.helpers import (
51
+ extract_seg_state, extract_alignment_state,
52
+ )
53
+
54
+ # Decomposition composable renderers
55
+ from cjm_transcript_segmentation.components.step_renderer import (
56
+ render_seg_column_body, render_toolbar, render_seg_stats,
57
+ render_seg_footer_content, render_seg_mini_stats_text,
58
+ )
59
+
60
+ # Alignment composable renderers
61
+ from cjm_transcript_vad_align.components.step_renderer import (
62
+ render_align_column_body, render_align_mini_stats_text,
63
+ )
64
+ from cjm_transcript_vad_align.components.card_stack_config import (
65
+ ALIGN_CS_IDS,
66
+ )
67
+
68
+ # Shared keyboard config (combined-level)
69
+ from cjm_transcript_segment_align.components.keyboard_config import (
70
+ build_combined_kb_system,
71
+ render_keyboard_hints_collapsible, generate_zone_change_js,
72
+ SWITCH_CHROME_BTN_ID,
73
+ )
74
+ from cjm_transcript_segmentation.components.card_stack_config import (
75
+ SEG_CS_CONFIG, SEG_CS_IDS,
76
+ )
77
+
78
+ # Debug flag for combined step rendering tracing (set False in production)
79
+ DEBUG_COMBINED_RENDER = True
80
+
81
+ # %% ../../nbs/components/step_renderer.ipynb #e5f6a7b8
82
+ def _render_column_header(
83
+ title:str, # Column title (e.g., "Text Decomposition")
84
+ stats_id:str, # HTML ID for the mini-stats badge area
85
+ header_id:str, # HTML ID for the column header container
86
+ initial_text:str="--", # Initial text for the mini-stats badge
87
+ ) -> Any: # Column header component
88
+ """Render a column header with title and mini-stats badge."""
89
+ return Div(
90
+ Span(
91
+ title,
92
+ cls=combine_classes(
93
+ font_size.sm, font_weight.bold,
94
+ uppercase, tracking.wide,
95
+ text_dui.base_content.opacity(50)
96
+ )
97
+ ),
98
+ Span(
99
+ initial_text,
100
+ id=stats_id,
101
+ cls=combine_classes(badge, badge_styles.ghost, badge_sizes.sm)
102
+ ),
103
+ id=header_id,
104
+ cls=combine_classes(
105
+ flex_display, justify.between, items.center,
106
+ p(3), bg_dui.base_200,
107
+ border_dui.base_300, border.b()
108
+ )
109
+ )
110
+
111
+ # %% ../../nbs/components/step_renderer.ipynb #zw8jc4n30b
112
+ def render_seg_mini_stats_badge(
113
+ segments:List[TextSegment], # Current segments
114
+ oob:bool=False, # Whether to render as OOB swap
115
+ ) -> Any: # Mini-stats badge Span
116
+ """Render the segmentation mini-stats badge for the column header."""
117
+ return Span(
118
+ render_seg_mini_stats_text(segments),
119
+ id=CombinedHtmlIds.SEG_MINI_STATS,
120
+ cls=combine_classes(badge, badge_styles.ghost, badge_sizes.sm),
121
+ hx_swap_oob="true" if oob else None,
122
+ )
123
+
124
+ # %% ../../nbs/components/step_renderer.ipynb #3x5ezeou2d3
125
+ def render_align_mini_stats_badge(
126
+ chunks:List[VADChunk], # Current VAD chunks
127
+ oob:bool=False, # Whether to render as OOB swap
128
+ ) -> Any: # Mini-stats badge Span
129
+ """Render the alignment mini-stats badge for the column header."""
130
+ return Span(
131
+ render_align_mini_stats_text(chunks),
132
+ id=CombinedHtmlIds.ALIGNMENT_MINI_STATS,
133
+ cls=combine_classes(badge, badge_styles.ghost, badge_sizes.sm),
134
+ hx_swap_oob="true" if oob else None,
135
+ )
136
+
137
+ # %% ../../nbs/components/step_renderer.ipynb #t247rxpafe
138
+ def render_alignment_status_text(
139
+ segment_count:int, # Number of text segments
140
+ chunk_count:int, # Number of VAD chunks
141
+ ) -> str: # Status message text
142
+ """Generate alignment status message based on segment and VAD chunk counts."""
143
+ if segment_count == 0 or chunk_count == 0:
144
+ return "Waiting for data..."
145
+
146
+ delta = segment_count - chunk_count
147
+
148
+ if delta == 0:
149
+ return f"Aligned ({segment_count})"
150
+ elif delta > 0:
151
+ return f"{segment_count} segments vs {chunk_count} VAD chunks (merge {delta})"
152
+ else:
153
+ return f"{segment_count} segments vs {chunk_count} VAD chunks (split {-delta})"
154
+
155
+
156
+ #| export
157
+ def render_alignment_status(
158
+ segment_count:int, # Number of text segments
159
+ chunk_count:int, # Number of VAD chunks
160
+ oob:bool=False, # Whether to render as OOB swap
161
+ ) -> Any: # Alignment status badge component
162
+ """Render the alignment status indicator badge."""
163
+ is_aligned = segment_count > 0 and chunk_count > 0 and segment_count == chunk_count
164
+
165
+ # Choose badge style based on alignment state
166
+ if is_aligned:
167
+ badge_cls = combine_classes(badge, badge_sizes.sm, text_dui.success, font_weight.bold)
168
+ elif segment_count == 0 or chunk_count == 0:
169
+ badge_cls = combine_classes(badge, badge_styles.ghost, badge_sizes.sm)
170
+ else:
171
+ badge_cls = combine_classes(badge, badge_sizes.sm, text_dui.warning, font_weight.bold)
172
+
173
+ return Span(
174
+ render_alignment_status_text(segment_count, chunk_count),
175
+ id=CombinedHtmlIds.ALIGNMENT_STATUS,
176
+ cls=badge_cls,
177
+ hx_swap_oob="true" if oob else None,
178
+ )
179
+
180
+
181
+ # Shared footer inner content styling
182
+ _FOOTER_INNER_CLS = combine_classes(flex_display, justify.between, items.center, w.full, gap(4))
183
+
184
+
185
+ #| export
186
+ def render_footer_inner_content(
187
+ column_footer:Any, # Column-specific footer content (decomp or align)
188
+ segment_count:int, # Number of text segments
189
+ chunk_count:int, # Number of VAD chunks
190
+ ) -> Any: # Styled wrapper div with column footer and alignment status
191
+ """Render the footer inner content with consistent styling.
192
+
193
+ This ensures the footer layout (justify-between) is preserved across
194
+ all OOB swaps. Both the column-specific footer content and the
195
+ alignment status indicator are wrapped in a flex container.
196
+ """
197
+ return Div(
198
+ column_footer,
199
+ render_alignment_status(segment_count, chunk_count),
200
+ cls=_FOOTER_INNER_CLS
201
+ )
202
+
203
+ # %% ../../nbs/components/step_renderer.ipynb #a7b8c9d0
204
+ def _placeholder(
205
+ text:str, # Placeholder message
206
+ ) -> Any: # Styled placeholder paragraph
207
+ """Render a placeholder text element for uninitialized chrome containers."""
208
+ return P(text, cls=combine_classes(font_size.sm, text_dui.base_content.opacity(50)))
209
+
210
+
211
+ def _render_shared_chrome(
212
+ seg_state:dict=None, # Extracted segmentation state (None = show placeholders)
213
+ align_state:dict=None, # Extracted alignment state (None = no VAD data yet)
214
+ urls:SegmentationUrls=None, # Segmentation URL bundle (required when seg_state provided)
215
+ kb_manager:Any=None, # Keyboard manager (required when seg_state provided)
216
+ ) -> tuple: # (hints, toolbar, controls, footer)
217
+ """Render shared chrome containers, populated with segmentation content when initialized.
218
+
219
+ Takes extracted state dicts from `extract_seg_state()` and `extract_alignment_state()`
220
+ which contain deserialized TextSegment and VADChunk objects.
221
+ """
222
+ is_init = seg_state is not None and seg_state.get("is_initialized", False)
223
+
224
+ # --- Hints ---
225
+ if is_init and kb_manager:
226
+ hints_content = render_keyboard_hints_collapsible(kb_manager)
227
+ else:
228
+ hints_content = _placeholder("Keyboard hints will appear here when a column is active.")
229
+
230
+ hints = Div(
231
+ hints_content,
232
+ id=CombinedHtmlIds.SHARED_HINTS,
233
+ cls=str(p(2))
234
+ )
235
+
236
+ # --- Toolbar ---
237
+ if is_init and urls:
238
+ segments = seg_state.get("segments", [])
239
+ history = seg_state.get("history", [])
240
+ visible_count = seg_state.get("visible_count", DEFAULT_VISIBLE_COUNT)
241
+ toolbar_content = render_toolbar(
242
+ reset_url=urls.reset,
243
+ ai_split_url=urls.ai_split,
244
+ undo_url=urls.undo,
245
+ can_undo=(len(history) > 0),
246
+ visible_count=visible_count,
247
+ )
248
+ else:
249
+ toolbar_content = _placeholder("Toolbar actions will appear here based on the active column.")
250
+
251
+ toolbar = Div(
252
+ toolbar_content,
253
+ id=CombinedHtmlIds.SHARED_TOOLBAR,
254
+ cls=str(p(2))
255
+ )
256
+
257
+ # --- Controls ---
258
+ if is_init:
259
+ card_width = seg_state.get("card_width", DEFAULT_CARD_WIDTH)
260
+ controls_content = render_width_slider(SEG_CS_CONFIG, SEG_CS_IDS, card_width=card_width)
261
+ else:
262
+ controls_content = _placeholder("Column-specific controls will appear here.")
263
+
264
+ controls = Div(
265
+ controls_content,
266
+ id=CombinedHtmlIds.SHARED_CONTROLS,
267
+ cls=str(p(2))
268
+ )
269
+
270
+ # --- Footer with alignment status ---
271
+ segment_count = len(seg_state.get("segments", [])) if seg_state else 0
272
+ chunk_count = len(align_state.get("vad_chunks", [])) if align_state else 0
273
+
274
+ if is_init:
275
+ segments = seg_state.get("segments", [])
276
+ focused_index = seg_state.get("focused_index", 0)
277
+ column_footer = render_seg_footer_content(segments, focused_index)
278
+ else:
279
+ column_footer = _placeholder("Footer with progress and timestamp details will appear here.")
280
+
281
+ footer_inner = render_footer_inner_content(column_footer, segment_count, chunk_count)
282
+
283
+ footer = Div(
284
+ footer_inner,
285
+ id=CombinedHtmlIds.SHARED_FOOTER,
286
+ cls=combine_classes(
287
+ p(1), bg_dui.base_100,
288
+ border_dui.base_300, border.t(),
289
+ flex_display, justify.center, items.center
290
+ )
291
+ )
292
+
293
+ return hints, toolbar, controls, footer
294
+
295
+ # %% ../../nbs/components/step_renderer.ipynb #c9d0e1f2
296
+ # Shared column styling (reused by init handler for outerHTML swap)
297
+ _SEG_COLUMN_CLS = combine_classes(
298
+ w.full, w('[60%]').lg,
299
+ min_h(0),
300
+ flex_display, flex_direction.col,
301
+ bg_dui.base_100, border_dui.base_300, border(1),
302
+ border_radius.box,
303
+ overflow.hidden,
304
+ transition.all, duration._200,
305
+ )
306
+
307
+
308
+ def _render_seg_column(
309
+ is_active:bool=True, # Whether this column is initially active
310
+ column_body:Any=None, # Pre-rendered column body (None = not initialized)
311
+ mini_stats_text:str="--", # Mini-stats badge text
312
+ init_url:str="", # URL for auto-trigger initialization
313
+ ) -> Any: # Left column component
314
+ """Render the left segmentation column."""
315
+ active_cls = combine_classes(ring(1), ring_dui.primary) if is_active else str(opacity(70))
316
+
317
+ # Content area: initialized viewport or loading + auto-trigger
318
+ if column_body is not None:
319
+ content = column_body
320
+ else:
321
+ content = Div(
322
+ render_loading_state(SEG_CS_IDS, message="Initializing segments..."),
323
+ # Auto-trigger initialization on load
324
+ Div(
325
+ hx_post=init_url,
326
+ hx_trigger="load",
327
+ hx_target=CombinedHtmlIds.as_selector(
328
+ CombinedHtmlIds.SEG_COLUMN_CONTENT
329
+ ),
330
+ hx_swap="outerHTML"
331
+ ) if init_url else None,
332
+ id=CombinedHtmlIds.SEG_COLUMN_CONTENT,
333
+ cls=combine_classes(grow(), overflow.hidden, flex_display, flex_direction.col, p(4))
334
+ )
335
+
336
+ return Div(
337
+ _render_column_header(
338
+ title="Text Segmentation",
339
+ stats_id=CombinedHtmlIds.SEG_MINI_STATS,
340
+ header_id=CombinedHtmlIds.SEG_COLUMN_HEADER,
341
+ initial_text=mini_stats_text,
342
+ ),
343
+ content,
344
+ id=CombinedHtmlIds.SEG_COLUMN,
345
+ cls=combine_classes(_SEG_COLUMN_CLS, active_cls)
346
+ )
347
+
348
+ # %% ../../nbs/components/step_renderer.ipynb #d0e1f2a3
349
+ # Shared alignment column styling (reused by init handler for outerHTML swap)
350
+ _ALIGNMENT_COLUMN_CLS = combine_classes(
351
+ w.full, w('[40%]').lg,
352
+ min_h(0),
353
+ flex_display, flex_direction.col,
354
+ bg_dui.base_100, border_dui.base_300, border(1),
355
+ border_radius.box,
356
+ overflow.hidden,
357
+ transition.all, duration._200,
358
+ )
359
+
360
+
361
+ def _render_alignment_column(
362
+ is_active:bool=False, # Whether this column is initially active
363
+ column_body:Any=None, # Pre-rendered column body (None = not initialized)
364
+ mini_stats_text:str="--", # Mini-stats badge text
365
+ init_url:str="", # URL for auto-trigger initialization
366
+ ) -> Any: # Right column component
367
+ """Render the right alignment column."""
368
+ active_cls = combine_classes(ring(1), ring_dui.secondary) if is_active else str(opacity(70))
369
+
370
+ # Content area: initialized viewport or loading + auto-trigger
371
+ if column_body is not None:
372
+ content = column_body
373
+ else:
374
+ content = Div(
375
+ render_loading_state(ALIGN_CS_IDS, message="Analyzing audio..."),
376
+ # Auto-trigger initialization on load
377
+ Div(
378
+ hx_post=init_url,
379
+ hx_trigger="load",
380
+ hx_target=CombinedHtmlIds.as_selector(
381
+ CombinedHtmlIds.ALIGNMENT_COLUMN_CONTENT
382
+ ),
383
+ hx_swap="outerHTML"
384
+ ) if init_url else None,
385
+ id=CombinedHtmlIds.ALIGNMENT_COLUMN_CONTENT,
386
+ cls=combine_classes(grow(), min_h(0), overflow.hidden, flex_display, flex_direction.col, p(4))
387
+ )
388
+
389
+ return Div(
390
+ _render_column_header(
391
+ title="VAD Alignment",
392
+ stats_id=CombinedHtmlIds.ALIGNMENT_MINI_STATS,
393
+ header_id=CombinedHtmlIds.ALIGNMENT_COLUMN_HEADER,
394
+ initial_text=mini_stats_text,
395
+ ),
396
+ content,
397
+ id=CombinedHtmlIds.ALIGNMENT_COLUMN,
398
+ cls=combine_classes(_ALIGNMENT_COLUMN_CLS, active_cls)
399
+ )
400
+
401
+ # %% ../../nbs/components/step_renderer.ipynb #1xws0u33lr
402
+ def _render_keyboard_system_container(
403
+ kb_system:Any=None, # Rendered keyboard system (None = empty container)
404
+ oob:bool=False, # Whether to render as OOB swap
405
+ ) -> Any: # Div with id=KEYBOARD_SYSTEM containing KB elements
406
+ """Render stable container for keyboard navigation system elements."""
407
+ if DEBUG_COMBINED_RENDER:
408
+ print(f"[COMBINED_RENDER] _render_keyboard_system_container called, kb_system={kb_system is not None}")
409
+
410
+ if kb_system is not None:
411
+ content = (kb_system.script, kb_system.hidden_inputs, kb_system.action_buttons)
412
+ else:
413
+ content = ()
414
+
415
+ return Div(
416
+ *content,
417
+ id=CombinedHtmlIds.KEYBOARD_SYSTEM,
418
+ hx_swap_oob="true" if oob else None,
419
+ )
420
+
421
+ # %% ../../nbs/components/step_renderer.ipynb #f2a3b4c5
422
+ def render_combined_step(
423
+ ctx:InteractionContext, # Interaction context with state and data
424
+ seg_urls:SegmentationUrls=None, # URL bundle for segmentation routes
425
+ align_urls:AlignmentUrls=None, # URL bundle for alignment routes
426
+ switch_chrome_url:str="", # URL for chrome switching route
427
+ ) -> Any: # FastHTML component with full dual-column layout
428
+ """Render Phase 2: Combined Segment & Align step with dual-column layout."""
429
+ if DEBUG_COMBINED_RENDER:
430
+ print("[COMBINED_RENDER] render_combined_step called")
431
+
432
+ seg_urls = seg_urls or SegmentationUrls()
433
+
434
+ # Extract state using combined helpers
435
+ seg_state = extract_seg_state(ctx)
436
+ align_state = extract_alignment_state(ctx)
437
+
438
+ is_seg_init = seg_state["is_initialized"]
439
+ is_align_init = align_state["is_initialized"]
440
+
441
+ if DEBUG_COMBINED_RENDER:
442
+ print(f"[COMBINED_RENDER] is_seg_init: {is_seg_init}")
443
+ print(f"[COMBINED_RENDER] is_align_init: {is_align_init}")
444
+
445
+ # Variables for KB system
446
+ kb_manager = None
447
+ kb_system = None
448
+ zone_change_js = None
449
+
450
+ if is_seg_init:
451
+ # Get values from extracted state
452
+ segments = seg_state["segments"]
453
+ focused_index = seg_state["focused_index"]
454
+ history = seg_state["history"]
455
+ visible_count = seg_state["visible_count"]
456
+ card_width = seg_state["card_width"]
457
+
458
+ # Always build combined KB system with both zones
459
+ # (alignment zone will have no items until alignment init completes)
460
+ if align_urls:
461
+ kb_manager, kb_system = build_combined_kb_system(seg_urls, align_urls)
462
+ zone_change_js = generate_zone_change_js(switch_chrome_url)
463
+ if DEBUG_COMBINED_RENDER:
464
+ print(f"[COMBINED_RENDER] built combined kb_system with both zones")
465
+
466
+ # Render column body with viewport (kb_system=None, KB is in stable container)
467
+ column_body = render_seg_column_body(
468
+ segments=segments,
469
+ focused_index=focused_index,
470
+ visible_count=visible_count,
471
+ card_width=card_width,
472
+ urls=seg_urls,
473
+ kb_system=None, # KB system is in stable container, not column body
474
+ )
475
+ mini_stats_text = render_seg_mini_stats_text(segments)
476
+
477
+ # Shared chrome with real segmentation content (includes alignment status)
478
+ hints, toolbar, controls, footer = _render_shared_chrome(
479
+ seg_state=seg_state,
480
+ align_state=align_state,
481
+ urls=seg_urls,
482
+ kb_manager=kb_manager,
483
+ )
484
+
485
+ # Left column with real content
486
+ seg_col = _render_seg_column(
487
+ is_active=True,
488
+ column_body=column_body,
489
+ mini_stats_text=mini_stats_text,
490
+ )
491
+ else:
492
+ # Shared chrome with placeholders (includes alignment status)
493
+ hints, toolbar, controls, footer = _render_shared_chrome(
494
+ align_state=align_state,
495
+ )
496
+
497
+ # Left column with loading state + auto-trigger
498
+ seg_col = _render_seg_column(
499
+ is_active=True,
500
+ init_url=seg_urls.init,
501
+ )
502
+
503
+ # --- Alignment column ---
504
+ if is_align_init and align_urls:
505
+ # Get values from extracted state
506
+ chunks = align_state["vad_chunks"]
507
+ align_focused = align_state["focused_index"]
508
+ align_visible = align_state["visible_count"]
509
+ align_width = align_state["card_width"]
510
+ media_paths = align_state.get("media_paths", [])
511
+ audio_src_url = align_urls.audio_src if align_urls else ""
512
+ audio_urls = [f"{audio_src_url}?path={mp}" for mp in media_paths] if audio_src_url and media_paths else []
513
+
514
+ if DEBUG_COMBINED_RENDER:
515
+ print(f"[COMBINED_RENDER] media_paths: {len(media_paths)}, audio_urls: {len(audio_urls)}")
516
+
517
+ align_body = render_align_column_body(
518
+ chunks=chunks,
519
+ focused_index=align_focused,
520
+ visible_count=align_visible,
521
+ card_width=align_width,
522
+ urls=align_urls,
523
+ kb_system=None, # KB system is in stable container
524
+ audio_urls=audio_urls,
525
+ )
526
+ align_mini_text = render_align_mini_stats_text(chunks)
527
+
528
+ align_col = _render_alignment_column(
529
+ is_active=False,
530
+ column_body=align_body,
531
+ mini_stats_text=align_mini_text,
532
+ )
533
+ elif align_urls and align_urls.init:
534
+ # Show loading state with auto-trigger
535
+ align_col = _render_alignment_column(
536
+ is_active=False,
537
+ init_url=align_urls.init,
538
+ )
539
+ else:
540
+ # No alignment URLs — show column with default placeholder
541
+ align_col = _render_alignment_column(is_active=False)
542
+
543
+ # Hidden input tracking active column (seg or align)
544
+ active_column_input = Input(
545
+ type="hidden",
546
+ id=CombinedHtmlIds.ACTIVE_COLUMN_INPUT,
547
+ name="active_column",
548
+ value="seg",
549
+ )
550
+
551
+ # Stable keyboard system container (outside columns)
552
+ kb_container = _render_keyboard_system_container(kb_system=kb_system)
553
+
554
+ # Hidden button for chrome swap HTMX trigger (clicked by zone change JS)
555
+ chrome_switch_btn = None
556
+ if switch_chrome_url and align_urls:
557
+ chrome_switch_btn = Button(
558
+ id=SWITCH_CHROME_BTN_ID,
559
+ cls=str(display_tw.hidden),
560
+ hx_post=switch_chrome_url,
561
+ hx_include=f"#{CombinedHtmlIds.ACTIVE_COLUMN_INPUT}",
562
+ hx_swap="none",
563
+ )
564
+
565
+ if DEBUG_COMBINED_RENDER:
566
+ print(f"[COMBINED_RENDER] kb_container rendered with kb_system={kb_system is not None}")
567
+ print(f"[COMBINED_RENDER] zone_change_js present: {zone_change_js is not None}")
568
+ print(f"[COMBINED_RENDER] chrome_switch_btn present: {chrome_switch_btn is not None}")
569
+
570
+ return Div(
571
+ # Header
572
+ Div(
573
+ H2("Segment & Align", cls=combine_classes(font_size._3xl, font_weight.bold)),
574
+ P(
575
+ "Decompose text into segments and align with audio timestamps.",
576
+ cls=combine_classes(text_dui.base_content.opacity(70), m.b(2))
577
+ ),
578
+ ),
579
+
580
+ # Shared keyboard hints
581
+ hints,
582
+
583
+ # Shared toolbar and controls
584
+ toolbar,
585
+ controls,
586
+
587
+ # Dual-column content area
588
+ Div(
589
+ seg_col,
590
+ align_col,
591
+ cls=combine_classes(
592
+ grow(),
593
+ min_h(0),
594
+ flex_display,
595
+ flex_direction.col,
596
+ flex_direction.row.lg,
597
+ gap(4),
598
+ overflow.hidden,
599
+ p(1),
600
+ )
601
+ ),
602
+
603
+ # Shared footer
604
+ footer,
605
+
606
+ # Stable keyboard system container (outside columns, won't be swapped)
607
+ kb_container,
608
+
609
+ # Zone change JavaScript (when align_urls available)
610
+ zone_change_js,
611
+
612
+ # Hidden chrome switch button (for HTMX-triggered chrome swaps)
613
+ chrome_switch_btn,
614
+
615
+ # Hidden active column state
616
+ active_column_input,
617
+
618
+ id=SegmentationHtmlIds.SEG_CONTAINER,
619
+ cls=combine_classes(
620
+ w.full, h.full,
621
+ flex_display, flex_direction.col,
622
+ p(4), p.x(2), p.b(0)
623
+ )
624
+ )
@@ -0,0 +1,44 @@
1
+ """HTML ID constants for Phase 2 Shell: Dual-Column Layout shared chrome"""
2
+
3
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/html_ids.ipynb.
4
+
5
+ # %% auto #0
6
+ __all__ = ['CombinedHtmlIds']
7
+
8
+ # %% ../nbs/html_ids.ipynb #c3d4e5f6
9
+ # Framework-agnostic — no external imports needed
10
+
11
+ class CombinedHtmlIds:
12
+ """HTML ID constants for Phase 2 Shell: Dual-Column Layout shared chrome."""
13
+
14
+ @staticmethod
15
+ def as_selector(
16
+ id_str:str # The HTML ID to convert
17
+ ) -> str: # CSS selector with # prefix
18
+ """Convert an ID to a CSS selector format."""
19
+ return f"#{id_str}"
20
+
21
+ # Shared Chrome
22
+ SHARED_HINTS = "sd-shared-hints"
23
+ SHARED_TOOLBAR = "sd-shared-toolbar"
24
+ SHARED_CONTROLS = "sd-shared-controls"
25
+ SHARED_FOOTER = "sd-shared-footer"
26
+ ALIGNMENT_STATUS = "sd-alignment-status"
27
+
28
+ # Segmentation Column
29
+ SEG_COLUMN = "sd-seg-column"
30
+ SEG_COLUMN_HEADER = "sd-seg-column-header"
31
+ SEG_COLUMN_CONTENT = "sd-seg-column-content"
32
+ SEG_MINI_STATS = "sd-seg-mini-stats"
33
+
34
+ # Alignment Column
35
+ ALIGNMENT_COLUMN = "sd-align-column"
36
+ ALIGNMENT_COLUMN_HEADER = "sd-align-column-header"
37
+ ALIGNMENT_COLUMN_CONTENT = "sd-align-column-content"
38
+ ALIGNMENT_MINI_STATS = "sd-align-mini-stats"
39
+
40
+ # State Tracking
41
+ ACTIVE_COLUMN_INPUT = "sd-active-column"
42
+
43
+ # Keyboard System (stable container)
44
+ KEYBOARD_SYSTEM = "sd-keyboard-system"
File without changes