From 8e35ab099ccbcd0771603b5238a5ecf5a1a93473 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Mon, 2 Feb 2026 08:32:01 -0500 Subject: [PATCH 01/22] WIP: Add dual-panel layout with runs table sidebar --- align_app/app/runs_state_adapter.py | 36 +++++ align_app/app/ui.py | 214 ++++++++++++++++++++++++--- tests/e2e/page_objects/align_page.py | 92 ++++++++---- tests/e2e/test_load_experiments.py | 50 ++++--- tests/e2e/test_pipeline_random.py | 6 +- 5 files changed, 331 insertions(+), 67 deletions(-) diff --git a/align_app/app/runs_state_adapter.py b/align_app/app/runs_state_adapter.py index 86a2ab1..cfa161d 100644 --- a/align_app/app/runs_state_adapter.py +++ b/align_app/app/runs_state_adapter.py @@ -31,6 +31,8 @@ def __init__( self.decider_registry = decider_registry self._add_system_adm_callback = add_system_adm_callback self.server.state.pending_cache_keys = [] + self.server.state.table_collapsed = False + self.server.state.comparison_collapsed = False self.server.state.runs_table_modal_open = False self.server.state.runs_table_selected = [] self.server.state.runs_table_search = "" @@ -44,6 +46,16 @@ def __init__( {"title": "Alignment", "key": "alignment_summary"}, {"title": "Decision", "key": "decision_text"}, ] + self.server.state.runs_table_panel_headers = [ + {"title": "", "key": "in_comparison", "sortable": False, "width": "40px"}, + {"title": "Scenario", "key": "scenario_id"}, + {"title": "Scene", "key": "scene_id"}, + {"title": "Situation", "key": "probe_text", "sortable": False}, + {"title": "Decider", "key": "decider_name"}, + {"title": "LLM", "key": "llm_backbone_name"}, + {"title": "Alignment", "key": "alignment_summary"}, + {"title": "Decision", "key": "decision_text"}, + ] self.server.state.import_experiment_file = None self.server.state.adm_browser_open = False self.server.state.adm_browser_run_id = None @@ -176,6 +188,30 @@ def delete_run_from_compare(self, column_index): runs_to_compare.pop(column_index) self.state.runs_to_compare = runs_to_compare + @controller.set("toggle_table_collapsed") + def toggle_table_collapsed(self): + self.state.table_collapsed = not self.state.table_collapsed + + @controller.set("toggle_comparison_collapsed") + def toggle_comparison_collapsed(self): + self.state.comparison_collapsed = not self.state.comparison_collapsed + + @controller.set("toggle_run_in_comparison") + def toggle_run_in_comparison(self, cache_key): + run = self.runs_registry.get_run_by_cache_key(cache_key) + if not run: + run = self.runs_registry.materialize_experiment_item(cache_key) + if not run: + return + + runs_to_compare = list(self.state.runs_to_compare) + if run.id in runs_to_compare: + runs_to_compare.remove(run.id) + else: + runs_to_compare.append(run.id) + self.state.runs_to_compare = runs_to_compare + self._sync_from_runs_data(self.runs_registry.get_all_runs()) + @controller.set("copy_run") def copy_run(self, run_id, column_index): source_run = self.runs_registry.get_run(run_id) diff --git a/align_app/app/ui.py b/align_app/app/ui.py index 93d51c2..2cc26e2 100644 --- a/align_app/app/ui.py +++ b/align_app/app/ui.py @@ -15,6 +15,8 @@ def reload(m=None): m.__loader__.exec_module(m) +TABLE_PANEL_COLLAPSED_WIDTH = "48px" +COMPARISON_PANEL_COLLAPSED_WIDTH = "48px" RUN_COLUMN_MIN_WIDTH = "28rem" LABEL_COLUMN_WIDTH = "12rem" INDICATOR_SPACE = "3rem" @@ -892,6 +894,183 @@ def filterable_column(key: str, title: str, filter_var: str, options_var: str): cell_with_tooltip(key) +class RunsTablePanel(html.Div): + def __init__(self, **kwargs): + super().__init__( + classes="runs-table-panel d-flex flex-column", + style=( + "table_collapsed ? '" + + "width: " + + TABLE_PANEL_COLLAPSED_WIDTH + + "; min-width: " + + TABLE_PANEL_COLLAPSED_WIDTH + + "; flex-shrink: 0; transition: width 0.2s; height: 100%; overflow: hidden;' " + ": (comparison_collapsed ? '" + + "width: calc(100% - " + + COMPARISON_PANEL_COLLAPSED_WIDTH + + "); min-width: 25vw; flex-shrink: 0; transition: width 0.2s; height: 100%; overflow: hidden;' " + ": 'width: 40%; min-width: 25vw; flex-shrink: 0; transition: width 0.2s; height: 100%; overflow: hidden;')", + ), + **kwargs, + ) + ctrl = self.server.controller + with self: + with vuetify3.VBtn( + v_if=("table_collapsed",), + icon=True, + size="small", + variant="text", + click=(ctrl.toggle_table_collapsed,), + classes="ma-1", + ): + vuetify3.VIcon("mdi-chevron-right") + + with html.Div( + v_if=("!table_collapsed",), + classes="d-flex flex-column", + style="height: 100%; overflow: hidden;", + ): + with html.Div(classes="d-flex align-center pa-1 flex-shrink-0"): + with vuetify3.VBtn( + icon=True, + size="x-small", + variant="text", + click=(ctrl.toggle_table_collapsed,), + ): + vuetify3.VIcon("mdi-chevron-left", size="small") + vuetify3.VTextField( + v_model=("runs_table_search",), + placeholder="Search...", + prepend_inner_icon="mdi-magnify", + clearable=True, + hide_details=True, + density="compact", + style="max-width: 200px;", + classes="mx-2", + ) + vuetify3.VSpacer() + with vuetify3.VBtn( + size="x-small", + variant="text", + click=(ctrl.clear_all_table_filters,), + prepend_icon="mdi-filter-off", + ): + html.Span("Clear") + + with html.Div(style="flex: 1; overflow: auto;"): + with vuetify3.VDataTable( + items=("runs_table_items",), + headers=("runs_table_panel_headers",), + item_value="id", + hover=True, + density="compact", + search=("runs_table_search",), + items_per_page=(-1,), + hide_default_footer=True, + click_row=(ctrl.on_table_row_click, "[$event, item]"), + ): + with html.Template( + raw_attrs=['v-slot:item.in_comparison="{ item }"'] + ): + vuetify3.VCheckbox( + hide_details=True, + density="compact", + raw_attrs=[ + "@click.stop", + ':model-value="runs_to_compare.some(' + 'rid => runs[rid]?.cache_key === item.id)"', + ], + click=(ctrl.toggle_run_in_comparison, "[item.id]"), + ) + filterable_column( + "scenario_id", + "Scenario", + "runs_table_filter_scenario", + "runs_table_scenario_options", + ) + filterable_column( + "scene_id", + "Scene", + "runs_table_filter_scene", + "runs_table_scene_options", + ) + cell_with_tooltip("probe_text") + filterable_column( + "decider_name", + "Decider", + "runs_table_filter_decider", + "runs_table_decider_options", + ) + filterable_column( + "llm_backbone_name", + "LLM", + "runs_table_filter_llm", + "runs_table_llm_options", + ) + filterable_column( + "alignment_summary", + "Alignment", + "runs_table_filter_alignment", + "runs_table_alignment_options", + ) + filterable_column( + "decision_text", + "Decision", + "runs_table_filter_decision", + "runs_table_decision_options", + ) + with html.Template(raw_attrs=["v-slot:no-data"]): + with html.Div( + classes="d-flex flex-column align-center pa-4" + ): + html.Div("No runs found", classes="text-body-2") + + +class ComparisonPanel(html.Div): + def __init__(self, **kwargs): + super().__init__( + classes="comparison-panel d-flex flex-column flex-grow-1", + style=( + "comparison_collapsed ? '" + + "min-width: " + + COMPARISON_PANEL_COLLAPSED_WIDTH + + "; max-width: " + + COMPARISON_PANEL_COLLAPSED_WIDTH + + ";' : 'min-width: 200px;'", + "transition: min-width 0.2s ease-in-out; height: 100%; overflow: hidden;", + ), + **kwargs, + ) + ctrl = self.server.controller + with self: + with vuetify3.VBtn( + v_if=("comparison_collapsed",), + icon=True, + size="small", + variant="text", + click=(ctrl.toggle_comparison_collapsed,), + classes="ma-1", + ): + vuetify3.VIcon("mdi-chevron-left") + + with html.Div( + v_if=("!comparison_collapsed",), + classes="d-flex flex-column", + style="height: 100%; overflow: hidden;", + ): + with html.Div(classes="d-flex align-center pa-1 flex-shrink-0"): + with vuetify3.VBtn( + icon=True, + size="x-small", + variant="text", + click=(ctrl.toggle_comparison_collapsed,), + ): + vuetify3.VIcon("mdi-chevron-right", size="small") + + with html.Div(style="flex: 1; overflow: auto;"): + ResultsComparison() + + class RunsTableModal(html.Div): def __init__(self, **kwargs): super().__init__(**kwargs) @@ -1262,12 +1441,6 @@ def __init__( if reload: with vuetify3.VBtn(icon=True, click=reload): vuetify3.VIcon("mdi-refresh") - with vuetify3.VBtn( - click=self.server.controller.open_runs_table_modal, - disabled=("Object.keys(runs).length === 0",), - prepend_icon="mdi-table", - ): - html.Span("Browse Runs") vuetify3.VFileInput( v_model=("import_experiment_file", None), accept=".zip", @@ -1355,21 +1528,22 @@ def __init__( "'" ) ) - with vuetify3.VContainer( - fluid=True, - classes=( - "isDragging ? 'overflow-auto pa-0 drop-zone-active' : 'overflow-auto pa-0'", - ), + with html.Div( + classes="d-flex", style="height: calc(100vh - 64px);", - raw_attrs=[ - '@dragover.prevent="isDragging = true"', - '@dragleave.prevent="isDragging = false"', - f'@drop.prevent="{DROP_HANDLER_JS}"', - ], ): + RunsTablePanel() with html.Div( - style="min-width: 100%; width: fit-content; padding: 16px;", + classes=( + "isDragging ? 'flex-grow-1 drop-zone-active' : 'flex-grow-1'", + ), + style="overflow: hidden;", + raw_attrs=[ + '@dragover.prevent="isDragging = true"', + '@dragleave.prevent="isDragging = false"', + f'@drop.prevent="{DROP_HANDLER_JS}"', + ], ): - ResultsComparison() - RunsTableModal() - AdmBrowserModal() + ComparisonPanel() + RunsTableModal() + AdmBrowserModal() diff --git a/tests/e2e/page_objects/align_page.py b/tests/e2e/page_objects/align_page.py index a8389f2..beafb85 100644 --- a/tests/e2e/page_objects/align_page.py +++ b/tests/e2e/page_objects/align_page.py @@ -20,21 +20,38 @@ def spinner(self) -> Locator: return self.page.locator(".v-progress-circular") @property - def browse_runs_button(self) -> Locator: - return self.page.get_by_role("button", name="Browse Runs") + def runs_table_panel(self) -> Locator: + return self.page.locator(".runs-table-panel") @property - def runs_modal(self) -> Locator: - # The modal card has a toolbar with "Runs" title - return self.page.get_by_text("Runs", exact=True).locator("xpath=..") + def comparison_panel(self) -> Locator: + return self.page.locator(".comparison-panel") @property - def runs_table(self) -> Locator: - return self.runs_modal.locator(".v-data-table") + def table_collapse_button(self) -> Locator: + return self.runs_table_panel.get_by_role("button").first @property - def table_close_button(self) -> Locator: - return self.runs_modal.get_by_role("button", name="Close") + def comparison_collapse_button(self) -> Locator: + return self.comparison_panel.get_by_role("button").filter( + has=self.page.locator(".mdi-chevron-left, .mdi-chevron-right") + ) + + @property + def table_search_input(self) -> Locator: + return self.runs_table_panel.locator("input[type='text']").first + + @property + def table_filter_toggle(self) -> Locator: + return self.runs_table_panel.get_by_role("button").filter( + has=self.page.locator(".mdi-filter, .mdi-filter-off") + ) + + @property + def table_run_items(self) -> Locator: + return self.runs_table_panel.locator(".v-list-item").filter( + has=self.page.locator(".v-checkbox") + ) @property def decision_button(self) -> Locator: @@ -397,22 +414,47 @@ def get_run_columns(self) -> Locator: def expect_run_count(self, count: int) -> None: expect(self.get_run_columns()).to_have_count(count) - def open_browse_runs_modal(self) -> None: - expect(self.browse_runs_button).to_be_visible() - self.browse_runs_button.click() - expect(self.runs_modal).to_be_visible() - - def close_browse_runs_modal(self) -> None: - # Use Esc or close button - self.page.keyboard.press("Escape") - expect(self.runs_modal).not_to_be_visible() - - def get_table_row_count(self) -> int: - # Count rows in the data table - # Wait for the table body to be present - self.runs_table.locator("tbody").wait_for() - # Count rows - return self.runs_table.locator("tbody tr").count() + def toggle_table_panel(self) -> None: + self.table_collapse_button.click() + + def expand_table_panel(self) -> None: + panel_style = self.runs_table_panel.get_attribute("style") or "" + if "width: 60px" in panel_style: + self.toggle_table_panel() + + def collapse_table_panel(self) -> None: + panel_style = self.runs_table_panel.get_attribute("style") or "" + if "width: 60px" not in panel_style: + self.toggle_table_panel() + + def toggle_comparison_panel(self) -> None: + self.comparison_collapse_button.click() + + def expand_comparison_panel(self) -> None: + panel_style = self.comparison_panel.get_attribute("style") or "" + if "width: 60px" in panel_style: + self.toggle_comparison_panel() + + def collapse_comparison_panel(self) -> None: + panel_style = self.comparison_panel.get_attribute("style") or "" + if "width: 60px" not in panel_style: + self.toggle_comparison_panel() + + def toggle_run_selection(self, index: int) -> None: + checkbox = self.table_run_items.nth(index).locator(".v-checkbox") + expect(checkbox).to_be_visible() + checkbox.click() + + def get_table_run_count(self) -> int: + return self.table_run_items.count() + + def search_table_runs(self, query: str) -> None: + self.expand_table_panel() + expect(self.table_search_input).to_be_visible() + self.table_search_input.fill(query) + + def clear_table_search(self) -> None: + self.table_search_input.clear() def get_alignment_slider(self, index: int) -> Locator: # Find slider in alignment panel diff --git a/tests/e2e/test_load_experiments.py b/tests/e2e/test_load_experiments.py index 9a6f6c7..6288ac9 100644 --- a/tests/e2e/test_load_experiments.py +++ b/tests/e2e/test_load_experiments.py @@ -26,22 +26,17 @@ def test_load_experiments_menu_opens(page, align_app_server): def test_load_experiments_from_zip(page, align_app_server, experiments_fixtures_path): - """Test loading experiments from a zip file adds runs to the table.""" + """Test loading experiments from a zip file adds runs to the table panel.""" align_page = AlignPage(page) align_page.goto(align_app_server) - browse_runs_button = page.get_by_role("button", name="Browse Runs") - browse_runs_button.click() + table_panel = page.locator(".runs-table-panel") + expect(table_panel).to_be_visible() - modal = page.locator(".v-dialog") - expect(modal).to_be_visible() + def get_table_items_count(): + return page.evaluate("window.trame.state.state.runs_table_items?.length || 0") - initial_rows = modal.locator("table tbody tr") - expect(initial_rows.first).to_be_visible() - initial_count = initial_rows.count() - - page.keyboard.press("Escape") - expect(modal).not_to_be_visible() + initial_count = get_table_items_count() load_button = page.get_by_role("button", name="Load Experiments", exact=True) load_button.click() @@ -55,18 +50,33 @@ def test_load_experiments_from_zip(page, align_app_server, experiments_fixtures_ file_chooser = fc_info.value file_chooser.set_files(str(EXPERIMENTS_ZIP)) - page.wait_for_timeout(3000) + page.evaluate( + """ + (async () => { + const input = trame.refs.importFileInput.$el.querySelector('input[type="file"]'); + if (input && input.files && input.files.length > 0) { + const file = input.files[0]; + const arrayBuffer = await file.arrayBuffer(); + const uint8Array = new Uint8Array(arrayBuffer); + await trame.trigger('import_zip_bytes', [Array.from(uint8Array)]); + } + })(); + """ + ) - page.keyboard.press("Escape") - page.wait_for_timeout(500) + def wait_for_import(): + for _ in range(30): + count = get_table_items_count() + if count > initial_count: + return True + page.wait_for_timeout(500) + return False - page.get_by_role("button", name="Browse Runs").click() - page.wait_for_selector(".v-dialog", state="visible", timeout=10000) + wait_for_import() - final_rows = modal.locator("table tbody tr") - expect(final_rows.first).to_be_visible() - final_count = final_rows.count() + final_count = get_table_items_count() assert final_count > initial_count, ( - f"Expected more rows after import. Initial: {initial_count}, Final: {final_count}" + f"Expected more runs in table panel after import. Initial: {initial_count}, " + f"Final: {final_count}" ) diff --git a/tests/e2e/test_pipeline_random.py b/tests/e2e/test_pipeline_random.py index fd50c92..c76c808 100644 --- a/tests/e2e/test_pipeline_random.py +++ b/tests/e2e/test_pipeline_random.py @@ -22,8 +22,10 @@ def test_pipeline_random_scene_change_rerun(align_page_with_decision: AlignPage) align_page.expand_scenario_panel() align_page.scene_dropdown.click() - scene_items = page.locator(".v-list-item") - second_scene = scene_items.nth(1) + scene_listbox = page.get_by_role("listbox", name="Scene-list") + expect(scene_listbox).to_be_visible() + scene_options = scene_listbox.get_by_role("option") + second_scene = scene_options.nth(1) second_scene.click() align_page.wait_for_decision_send_button() From f8985cf92546d8b6cc8ed6cf3b6c211be3c45637 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Mon, 2 Feb 2026 09:34:45 -0500 Subject: [PATCH 02/22] Improve collapsed panel UI with edge tab buttons - Replace floating arrow buttons with labeled edge tabs (icon + text + arrow) - Move collapsed buttons to absolute positioning at top corners - Panels hide completely when collapsed, giving full space to expanded panel - Collapse buttons show consistent styling in both states --- align_app/app/ui.py | 296 +++++++++++++++++++++----------------------- 1 file changed, 140 insertions(+), 156 deletions(-) diff --git a/align_app/app/ui.py b/align_app/app/ui.py index 2cc26e2..f8e057a 100644 --- a/align_app/app/ui.py +++ b/align_app/app/ui.py @@ -15,8 +15,6 @@ def reload(m=None): m.__loader__.exec_module(m) -TABLE_PANEL_COLLAPSED_WIDTH = "48px" -COMPARISON_PANEL_COLLAPSED_WIDTH = "48px" RUN_COLUMN_MIN_WIDTH = "28rem" LABEL_COLUMN_WIDTH = "12rem" INDICATOR_SPACE = "3rem" @@ -897,178 +895,139 @@ def filterable_column(key: str, title: str, filter_var: str, options_var: str): class RunsTablePanel(html.Div): def __init__(self, **kwargs): super().__init__( - classes="runs-table-panel d-flex flex-column", + v_if=("!table_collapsed",), + classes=( + "comparison_collapsed ? " + "'runs-table-panel d-flex flex-column flex-grow-1' : " + "'runs-table-panel d-flex flex-column flex-shrink-0'", + ), style=( - "table_collapsed ? '" - + "width: " - + TABLE_PANEL_COLLAPSED_WIDTH - + "; min-width: " - + TABLE_PANEL_COLLAPSED_WIDTH - + "; flex-shrink: 0; transition: width 0.2s; height: 100%; overflow: hidden;' " - ": (comparison_collapsed ? '" - + "width: calc(100% - " - + COMPARISON_PANEL_COLLAPSED_WIDTH - + "); min-width: 25vw; flex-shrink: 0; transition: width 0.2s; height: 100%; overflow: hidden;' " - ": 'width: 40%; min-width: 25vw; flex-shrink: 0; transition: width 0.2s; height: 100%; overflow: hidden;')", + "comparison_collapsed ? " + "'min-width: 25vw; height: 100%; overflow: hidden;' : " + "'width: 40%; min-width: 25vw; height: 100%; overflow: hidden;'", ), **kwargs, ) ctrl = self.server.controller with self: - with vuetify3.VBtn( - v_if=("table_collapsed",), - icon=True, - size="small", - variant="text", - click=(ctrl.toggle_table_collapsed,), - classes="ma-1", - ): - vuetify3.VIcon("mdi-chevron-right") - - with html.Div( - v_if=("!table_collapsed",), - classes="d-flex flex-column", - style="height: 100%; overflow: hidden;", - ): - with html.Div(classes="d-flex align-center pa-1 flex-shrink-0"): - with vuetify3.VBtn( - icon=True, - size="x-small", - variant="text", - click=(ctrl.toggle_table_collapsed,), - ): - vuetify3.VIcon("mdi-chevron-left", size="small") - vuetify3.VTextField( - v_model=("runs_table_search",), - placeholder="Search...", - prepend_inner_icon="mdi-magnify", - clearable=True, - hide_details=True, - density="compact", - style="max-width: 200px;", - classes="mx-2", - ) - vuetify3.VSpacer() - with vuetify3.VBtn( - size="x-small", - variant="text", - click=(ctrl.clear_all_table_filters,), - prepend_icon="mdi-filter-off", - ): - html.Span("Clear") - - with html.Div(style="flex: 1; overflow: auto;"): - with vuetify3.VDataTable( - items=("runs_table_items",), - headers=("runs_table_panel_headers",), - item_value="id", - hover=True, - density="compact", - search=("runs_table_search",), - items_per_page=(-1,), - hide_default_footer=True, - click_row=(ctrl.on_table_row_click, "[$event, item]"), + with html.Div(classes="d-flex align-center pa-1 flex-shrink-0"): + with vuetify3.VBtn( + variant="text", + click=(ctrl.toggle_table_collapsed,), + classes="text-none", + size="small", + ): + vuetify3.VIcon("mdi-chevron-left", size="small", classes="mr-1") + vuetify3.VIcon("mdi-table", size="small", classes="mr-1") + html.Span("Runs", classes="text-caption") + vuetify3.VTextField( + v_model=("runs_table_search",), + placeholder="Search...", + prepend_inner_icon="mdi-magnify", + clearable=True, + hide_details=True, + density="compact", + style="max-width: 200px;", + classes="mx-2", + ) + with vuetify3.VBtn( + size="x-small", + variant="text", + click=(ctrl.clear_all_table_filters,), + prepend_icon="mdi-filter-off", + ): + html.Span("Clear") + + with html.Div(style="flex: 1; overflow: auto;"): + with vuetify3.VDataTable( + items=("runs_table_items",), + headers=("runs_table_panel_headers",), + item_value="id", + hover=True, + density="compact", + search=("runs_table_search",), + items_per_page=(-1,), + hide_default_footer=True, + click_row=(ctrl.on_table_row_click, "[$event, item]"), + ): + with html.Template( + raw_attrs=['v-slot:item.in_comparison="{ item }"'] ): - with html.Template( - raw_attrs=['v-slot:item.in_comparison="{ item }"'] - ): - vuetify3.VCheckbox( - hide_details=True, - density="compact", - raw_attrs=[ - "@click.stop", - ':model-value="runs_to_compare.some(' - 'rid => runs[rid]?.cache_key === item.id)"', - ], - click=(ctrl.toggle_run_in_comparison, "[item.id]"), - ) - filterable_column( - "scenario_id", - "Scenario", - "runs_table_filter_scenario", - "runs_table_scenario_options", - ) - filterable_column( - "scene_id", - "Scene", - "runs_table_filter_scene", - "runs_table_scene_options", - ) - cell_with_tooltip("probe_text") - filterable_column( - "decider_name", - "Decider", - "runs_table_filter_decider", - "runs_table_decider_options", - ) - filterable_column( - "llm_backbone_name", - "LLM", - "runs_table_filter_llm", - "runs_table_llm_options", - ) - filterable_column( - "alignment_summary", - "Alignment", - "runs_table_filter_alignment", - "runs_table_alignment_options", - ) - filterable_column( - "decision_text", - "Decision", - "runs_table_filter_decision", - "runs_table_decision_options", + vuetify3.VCheckbox( + hide_details=True, + density="compact", + raw_attrs=[ + "@click.stop", + ':model-value="runs_to_compare.some(' + 'rid => runs[rid]?.cache_key === item.id)"', + ], + click=(ctrl.toggle_run_in_comparison, "[item.id]"), ) - with html.Template(raw_attrs=["v-slot:no-data"]): - with html.Div( - classes="d-flex flex-column align-center pa-4" - ): - html.Div("No runs found", classes="text-body-2") + filterable_column( + "scenario_id", + "Scenario", + "runs_table_filter_scenario", + "runs_table_scenario_options", + ) + filterable_column( + "scene_id", + "Scene", + "runs_table_filter_scene", + "runs_table_scene_options", + ) + cell_with_tooltip("probe_text") + filterable_column( + "decider_name", + "Decider", + "runs_table_filter_decider", + "runs_table_decider_options", + ) + filterable_column( + "llm_backbone_name", + "LLM", + "runs_table_filter_llm", + "runs_table_llm_options", + ) + filterable_column( + "alignment_summary", + "Alignment", + "runs_table_filter_alignment", + "runs_table_alignment_options", + ) + filterable_column( + "decision_text", + "Decision", + "runs_table_filter_decision", + "runs_table_decision_options", + ) + with html.Template(raw_attrs=["v-slot:no-data"]): + with html.Div(classes="d-flex flex-column align-center pa-4"): + html.Div("No runs found", classes="text-body-2") class ComparisonPanel(html.Div): def __init__(self, **kwargs): super().__init__( + v_if=("!comparison_collapsed",), classes="comparison-panel d-flex flex-column flex-grow-1", - style=( - "comparison_collapsed ? '" - + "min-width: " - + COMPARISON_PANEL_COLLAPSED_WIDTH - + "; max-width: " - + COMPARISON_PANEL_COLLAPSED_WIDTH - + ";' : 'min-width: 200px;'", - "transition: min-width 0.2s ease-in-out; height: 100%; overflow: hidden;", - ), + style="min-width: 200px; height: 100%; overflow: hidden;", **kwargs, ) ctrl = self.server.controller with self: - with vuetify3.VBtn( - v_if=("comparison_collapsed",), - icon=True, - size="small", - variant="text", - click=(ctrl.toggle_comparison_collapsed,), - classes="ma-1", - ): - vuetify3.VIcon("mdi-chevron-left") - - with html.Div( - v_if=("!comparison_collapsed",), - classes="d-flex flex-column", - style="height: 100%; overflow: hidden;", - ): - with html.Div(classes="d-flex align-center pa-1 flex-shrink-0"): - with vuetify3.VBtn( - icon=True, - size="x-small", - variant="text", - click=(ctrl.toggle_comparison_collapsed,), - ): - vuetify3.VIcon("mdi-chevron-right", size="small") + with html.Div(classes="d-flex justify-end pa-1 flex-shrink-0"): + with vuetify3.VBtn( + variant="text", + click=(ctrl.toggle_comparison_collapsed,), + classes="text-none", + size="small", + ): + html.Span("Compare", classes="text-caption") + vuetify3.VIcon("mdi-compare", size="small", classes="ml-1") + vuetify3.VIcon("mdi-chevron-right", size="small", classes="ml-1") - with html.Div(style="flex: 1; overflow: auto;"): - ResultsComparison() + with html.Div(style="flex: 1; overflow: auto;"): + ResultsComparison() class RunsTableModal(html.Div): @@ -1530,10 +1489,35 @@ def __init__( ) with html.Div( classes="d-flex", - style="height: calc(100vh - 64px);", + style="height: calc(100vh - 64px); position: relative;", ): + with vuetify3.VBtn( + v_if=("table_collapsed",), + variant="text", + click=(server.controller.toggle_table_collapsed,), + classes="text-none", + size="small", + style="position: absolute; top: 4px; left: 4px; z-index: 1;", + ): + vuetify3.VIcon("mdi-table", size="small", classes="mr-1") + html.Span("Runs", classes="text-caption") + vuetify3.VIcon( + "mdi-chevron-right", size="small", classes="ml-1" + ) + with vuetify3.VBtn( + v_if=("comparison_collapsed",), + variant="text", + click=(server.controller.toggle_comparison_collapsed,), + classes="text-none", + size="small", + style="position: absolute; top: 4px; right: 4px; z-index: 1;", + ): + vuetify3.VIcon("mdi-chevron-left", size="small", classes="mr-1") + html.Span("Compare", classes="text-caption") + vuetify3.VIcon("mdi-compare", size="small", classes="ml-1") RunsTablePanel() with html.Div( + v_if=("!comparison_collapsed",), classes=( "isDragging ? 'flex-grow-1 drop-zone-active' : 'flex-grow-1'", ), From 4f696b97c7a53dfbb33bcb545e6297ecac09c1c3 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Mon, 2 Feb 2026 09:50:57 -0500 Subject: [PATCH 03/22] Fix collapse button layout shift and icon ordering --- align_app/app/ui.py | 54 ++++++++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/align_app/app/ui.py b/align_app/app/ui.py index f8e057a..c191f12 100644 --- a/align_app/app/ui.py +++ b/align_app/app/ui.py @@ -910,16 +910,16 @@ def __init__(self, **kwargs): ) ctrl = self.server.controller with self: - with html.Div(classes="d-flex align-center pa-1 flex-shrink-0"): + with html.Div(classes="d-flex align-start pa-1 flex-shrink-0"): with vuetify3.VBtn( variant="text", click=(ctrl.toggle_table_collapsed,), classes="text-none", size="small", ): - vuetify3.VIcon("mdi-chevron-left", size="small", classes="mr-1") vuetify3.VIcon("mdi-table", size="small", classes="mr-1") html.Span("Runs", classes="text-caption") + vuetify3.VIcon("mdi-chevron-left", size="small", classes="ml-1") vuetify3.VTextField( v_model=("runs_table_search",), placeholder="Search...", @@ -1022,8 +1022,8 @@ def __init__(self, **kwargs): classes="text-none", size="small", ): + vuetify3.VIcon("mdi-compare", size="small", classes="mr-1") html.Span("Compare", classes="text-caption") - vuetify3.VIcon("mdi-compare", size="small", classes="ml-1") vuetify3.VIcon("mdi-chevron-right", size="small", classes="ml-1") with html.Div(style="flex: 1; overflow: auto;"): @@ -1491,30 +1491,38 @@ def __init__( classes="d-flex", style="height: calc(100vh - 64px); position: relative;", ): - with vuetify3.VBtn( + with html.Div( v_if=("table_collapsed",), - variant="text", - click=(server.controller.toggle_table_collapsed,), - classes="text-none", - size="small", - style="position: absolute; top: 4px; left: 4px; z-index: 1;", + classes="d-flex align-start pa-1", + style="position: absolute; top: 0; left: 0; z-index: 1;", ): - vuetify3.VIcon("mdi-table", size="small", classes="mr-1") - html.Span("Runs", classes="text-caption") - vuetify3.VIcon( - "mdi-chevron-right", size="small", classes="ml-1" - ) - with vuetify3.VBtn( + with vuetify3.VBtn( + variant="text", + click=(server.controller.toggle_table_collapsed,), + classes="text-none", + size="small", + ): + vuetify3.VIcon("mdi-table", size="small", classes="mr-1") + html.Span("Runs", classes="text-caption") + vuetify3.VIcon( + "mdi-chevron-right", size="small", classes="ml-1" + ) + with html.Div( v_if=("comparison_collapsed",), - variant="text", - click=(server.controller.toggle_comparison_collapsed,), - classes="text-none", - size="small", - style="position: absolute; top: 4px; right: 4px; z-index: 1;", + classes="d-flex align-center pa-1", + style="position: absolute; top: 0; right: 0; z-index: 1;", ): - vuetify3.VIcon("mdi-chevron-left", size="small", classes="mr-1") - html.Span("Compare", classes="text-caption") - vuetify3.VIcon("mdi-compare", size="small", classes="ml-1") + with vuetify3.VBtn( + variant="text", + click=(server.controller.toggle_comparison_collapsed,), + classes="text-none", + size="small", + ): + vuetify3.VIcon("mdi-compare", size="small", classes="mr-1") + html.Span("Compare", classes="text-caption") + vuetify3.VIcon( + "mdi-chevron-left", size="small", classes="ml-1" + ) RunsTablePanel() with html.Div( v_if=("!comparison_collapsed",), From b3176dc976ed6f46e847281aa16e47de191de487 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Mon, 2 Feb 2026 11:43:18 -0500 Subject: [PATCH 04/22] Improve visual consistency between table and comparison panels - Wrap table in VCard with elevation to match expansion panels - Fix header heights (2.5rem) so content sections align vertically - Use underlined variant for search input to reduce header height - Standardize margin (ma-1) on both VCard and VExpansionPanels --- align_app/app/ui.py | 142 +++++++++++++++++++++++--------------------- 1 file changed, 73 insertions(+), 69 deletions(-) diff --git a/align_app/app/ui.py b/align_app/app/ui.py index c191f12..d6b28c6 100644 --- a/align_app/app/ui.py +++ b/align_app/app/ui.py @@ -910,7 +910,7 @@ def __init__(self, **kwargs): ) ctrl = self.server.controller with self: - with html.Div(classes="d-flex align-start pa-1 flex-shrink-0"): + with html.Div(classes="d-flex align-center pa-1 flex-shrink-0", style="height: 2.5rem;"): with vuetify3.VBtn( variant="text", click=(ctrl.toggle_table_collapsed,), @@ -927,6 +927,7 @@ def __init__(self, **kwargs): clearable=True, hide_details=True, density="compact", + variant="underlined", style="max-width: 200px;", classes="mx-2", ) @@ -939,70 +940,73 @@ def __init__(self, **kwargs): html.Span("Clear") with html.Div(style="flex: 1; overflow: auto;"): - with vuetify3.VDataTable( - items=("runs_table_items",), - headers=("runs_table_panel_headers",), - item_value="id", - hover=True, - density="compact", - search=("runs_table_search",), - items_per_page=(-1,), - hide_default_footer=True, - click_row=(ctrl.on_table_row_click, "[$event, item]"), - ): - with html.Template( - raw_attrs=['v-slot:item.in_comparison="{ item }"'] + with vuetify3.VCard(elevation=2, classes="ma-1"): + with vuetify3.VDataTable( + items=("runs_table_items",), + headers=("runs_table_panel_headers",), + item_value="id", + hover=True, + density="compact", + search=("runs_table_search",), + items_per_page=(-1,), + hide_default_footer=True, + click_row=(ctrl.on_table_row_click, "[$event, item]"), ): - vuetify3.VCheckbox( - hide_details=True, - density="compact", - raw_attrs=[ - "@click.stop", - ':model-value="runs_to_compare.some(' - 'rid => runs[rid]?.cache_key === item.id)"', - ], - click=(ctrl.toggle_run_in_comparison, "[item.id]"), + with html.Template( + raw_attrs=['v-slot:item.in_comparison="{ item }"'] + ): + vuetify3.VCheckbox( + hide_details=True, + density="compact", + raw_attrs=[ + "@click.stop", + ':model-value="runs_to_compare.some(' + 'rid => runs[rid]?.cache_key === item.id)"', + ], + click=(ctrl.toggle_run_in_comparison, "[item.id]"), + ) + filterable_column( + "scenario_id", + "Scenario", + "runs_table_filter_scenario", + "runs_table_scenario_options", ) - filterable_column( - "scenario_id", - "Scenario", - "runs_table_filter_scenario", - "runs_table_scenario_options", - ) - filterable_column( - "scene_id", - "Scene", - "runs_table_filter_scene", - "runs_table_scene_options", - ) - cell_with_tooltip("probe_text") - filterable_column( - "decider_name", - "Decider", - "runs_table_filter_decider", - "runs_table_decider_options", - ) - filterable_column( - "llm_backbone_name", - "LLM", - "runs_table_filter_llm", - "runs_table_llm_options", - ) - filterable_column( - "alignment_summary", - "Alignment", - "runs_table_filter_alignment", - "runs_table_alignment_options", - ) - filterable_column( - "decision_text", - "Decision", - "runs_table_filter_decision", - "runs_table_decision_options", - ) - with html.Template(raw_attrs=["v-slot:no-data"]): - with html.Div(classes="d-flex flex-column align-center pa-4"): - html.Div("No runs found", classes="text-body-2") + filterable_column( + "scene_id", + "Scene", + "runs_table_filter_scene", + "runs_table_scene_options", + ) + cell_with_tooltip("probe_text") + filterable_column( + "decider_name", + "Decider", + "runs_table_filter_decider", + "runs_table_decider_options", + ) + filterable_column( + "llm_backbone_name", + "LLM", + "runs_table_filter_llm", + "runs_table_llm_options", + ) + filterable_column( + "alignment_summary", + "Alignment", + "runs_table_filter_alignment", + "runs_table_alignment_options", + ) + filterable_column( + "decision_text", + "Decision", + "runs_table_filter_decision", + "runs_table_decision_options", + ) + with html.Template(raw_attrs=["v-slot:no-data"]): + with html.Div( + classes="d-flex flex-column align-center pa-4" + ): + html.Div("No runs found", classes="text-body-2") class ComparisonPanel(html.Div): @@ -1015,7 +1019,7 @@ def __init__(self, **kwargs): ) ctrl = self.server.controller with self: - with html.Div(classes="d-flex justify-end pa-1 flex-shrink-0"): + with html.Div(classes="d-flex justify-end align-center pa-1 flex-shrink-0", style="height: 2.5rem;"): with vuetify3.VBtn( variant="text", click=(ctrl.toggle_comparison_collapsed,), @@ -1310,9 +1314,9 @@ def __init__(self, **kwargs): class ResultsComparison(html.Div): def __init__(self, **kwargs): - super().__init__(classes="d-inline-flex flex-wrap ga-4 pa-1", **kwargs) + super().__init__(classes="d-inline-flex flex-wrap ga-4", **kwargs) with self: - with vuetify3.VExpansionPanels(multiple=True, variant="accordion"): + with vuetify3.VExpansionPanels(multiple=True, variant="accordion", classes="ma-1"): PanelSection(child=RunNumber) PanelSection(child=Probe) PanelSection(child=Decider) @@ -1493,8 +1497,8 @@ def __init__( ): with html.Div( v_if=("table_collapsed",), - classes="d-flex align-start pa-1", - style="position: absolute; top: 0; left: 0; z-index: 1;", + classes="d-flex align-center pa-1", + style="position: absolute; top: 0; left: 0; z-index: 1; height: 2.5rem;", ): with vuetify3.VBtn( variant="text", @@ -1510,7 +1514,7 @@ def __init__( with html.Div( v_if=("comparison_collapsed",), classes="d-flex align-center pa-1", - style="position: absolute; top: 0; right: 0; z-index: 1;", + style="position: absolute; top: 0; right: 0; z-index: 1; height: 2.5rem;", ): with vuetify3.VBtn( variant="text", From 76b622958f26d4d94c8f328f961be772a799e78e Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Mon, 2 Feb 2026 12:11:41 -0500 Subject: [PATCH 05/22] Consolidate table headers, 50/50 panel split, eye icon toggle - Remove duplicate runs_table_panel_headers definition - Change flex layout to 50/50 split between table and comparison - Replace checkbox with eye/eye-off icon for comparison toggle --- align_app/app/runs_state_adapter.py | 12 +---- align_app/app/ui.py | 74 ++++++++++++++++------------- 2 files changed, 41 insertions(+), 45 deletions(-) diff --git a/align_app/app/runs_state_adapter.py b/align_app/app/runs_state_adapter.py index cfa161d..16b28a9 100644 --- a/align_app/app/runs_state_adapter.py +++ b/align_app/app/runs_state_adapter.py @@ -37,17 +37,7 @@ def __init__( self.server.state.runs_table_selected = [] self.server.state.runs_table_search = "" self.server.state.runs_table_headers = [ - {"title": "", "key": "in_comparison", "sortable": False, "width": "24px"}, - {"title": "Scenario", "key": "scenario_id"}, - {"title": "Scene", "key": "scene_id"}, - {"title": "Situation", "key": "probe_text", "sortable": False}, - {"title": "Decider", "key": "decider_name"}, - {"title": "LLM", "key": "llm_backbone_name"}, - {"title": "Alignment", "key": "alignment_summary"}, - {"title": "Decision", "key": "decision_text"}, - ] - self.server.state.runs_table_panel_headers = [ - {"title": "", "key": "in_comparison", "sortable": False, "width": "40px"}, + {"title": "", "key": "in_comparison", "sortable": False}, {"title": "Scenario", "key": "scenario_id"}, {"title": "Scene", "key": "scene_id"}, {"title": "Situation", "key": "probe_text", "sortable": False}, diff --git a/align_app/app/ui.py b/align_app/app/ui.py index d6b28c6..3619370 100644 --- a/align_app/app/ui.py +++ b/align_app/app/ui.py @@ -896,21 +896,16 @@ class RunsTablePanel(html.Div): def __init__(self, **kwargs): super().__init__( v_if=("!table_collapsed",), - classes=( - "comparison_collapsed ? " - "'runs-table-panel d-flex flex-column flex-grow-1' : " - "'runs-table-panel d-flex flex-column flex-shrink-0'", - ), - style=( - "comparison_collapsed ? " - "'min-width: 25vw; height: 100%; overflow: hidden;' : " - "'width: 40%; min-width: 25vw; height: 100%; overflow: hidden;'", - ), + classes="runs-table-panel d-flex flex-column", + style="flex: 1; min-width: 25vw; height: 100%; overflow: hidden;", **kwargs, ) ctrl = self.server.controller with self: - with html.Div(classes="d-flex align-center pa-1 flex-shrink-0", style="height: 2.5rem;"): + with html.Div( + classes="d-flex align-center pa-1 flex-shrink-0", + style="height: 2.5rem;", + ): with vuetify3.VBtn( variant="text", click=(ctrl.toggle_table_collapsed,), @@ -943,7 +938,7 @@ def __init__(self, **kwargs): with vuetify3.VCard(elevation=2, classes="ma-1"): with vuetify3.VDataTable( items=("runs_table_items",), - headers=("runs_table_panel_headers",), + headers=("runs_table_headers",), item_value="id", hover=True, density="compact", @@ -955,16 +950,18 @@ def __init__(self, **kwargs): with html.Template( raw_attrs=['v-slot:item.in_comparison="{ item }"'] ): - vuetify3.VCheckbox( - hide_details=True, - density="compact", - raw_attrs=[ - "@click.stop", - ':model-value="runs_to_compare.some(' - 'rid => runs[rid]?.cache_key === item.id)"', - ], - click=(ctrl.toggle_run_in_comparison, "[item.id]"), - ) + with html.Div(style="text-overflow: clip;"): + vuetify3.VIcon( + raw_attrs=[ + "@click.stop", + ':icon="runs_to_compare.some(' + "rid => runs[rid]?.cache_key === item.id) " + "? 'mdi-eye' : 'mdi-eye-off'\"", + ], + size="small", + style="cursor: pointer;", + click=(ctrl.toggle_run_in_comparison, "[item.id]"), + ) filterable_column( "scenario_id", "Scenario", @@ -1019,7 +1016,10 @@ def __init__(self, **kwargs): ) ctrl = self.server.controller with self: - with html.Div(classes="d-flex justify-end align-center pa-1 flex-shrink-0", style="height: 2.5rem;"): + with html.Div( + classes="d-flex justify-end align-center pa-1 flex-shrink-0", + style="height: 2.5rem;", + ): with vuetify3.VBtn( variant="text", click=(ctrl.toggle_comparison_collapsed,), @@ -1190,13 +1190,19 @@ def __init__(self, **kwargs): with html.Template( raw_attrs=['v-slot:item.in_comparison="{ item }"'] ): - with html.Span(style="font-size: 12px;"): + with html.Div(style="text-overflow: clip;"): vuetify3.VIcon( - "mdi-eye", - size="12", - v_if=( - "runs_to_compare.some(" - "rid => runs[rid]?.cache_key === item.id)" + raw_attrs=[ + "@click.stop", + ':icon="runs_to_compare.some(' + "rid => runs[rid]?.cache_key === item.id) " + "? 'mdi-eye' : 'mdi-eye-off'\"", + ], + size="small", + style="cursor: pointer;", + click=( + self.server.controller.toggle_run_in_comparison, + "[item.id]", ), ) filterable_column( @@ -1316,7 +1322,9 @@ class ResultsComparison(html.Div): def __init__(self, **kwargs): super().__init__(classes="d-inline-flex flex-wrap ga-4", **kwargs) with self: - with vuetify3.VExpansionPanels(multiple=True, variant="accordion", classes="ma-1"): + with vuetify3.VExpansionPanels( + multiple=True, variant="accordion", classes="ma-1" + ): PanelSection(child=RunNumber) PanelSection(child=Probe) PanelSection(child=Decider) @@ -1530,10 +1538,8 @@ def __init__( RunsTablePanel() with html.Div( v_if=("!comparison_collapsed",), - classes=( - "isDragging ? 'flex-grow-1 drop-zone-active' : 'flex-grow-1'", - ), - style="overflow: hidden;", + classes=("isDragging ? 'drop-zone-active' : ''",), + style="flex: 1; overflow: hidden;", raw_attrs=[ '@dragover.prevent="isDragging = true"', '@dragleave.prevent="isDragging = false"', From 540c9221f7a6c9ce74af8ab67acdd5e84be164da Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Mon, 2 Feb 2026 12:13:45 -0500 Subject: [PATCH 06/22] Mirror Runs button layout: arrow left, table icon right --- align_app/app/ui.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/align_app/app/ui.py b/align_app/app/ui.py index 3619370..a34ccce 100644 --- a/align_app/app/ui.py +++ b/align_app/app/ui.py @@ -912,9 +912,9 @@ def __init__(self, **kwargs): classes="text-none", size="small", ): - vuetify3.VIcon("mdi-table", size="small", classes="mr-1") + vuetify3.VIcon("mdi-chevron-left", size="small", classes="mr-1") html.Span("Runs", classes="text-caption") - vuetify3.VIcon("mdi-chevron-left", size="small", classes="ml-1") + vuetify3.VIcon("mdi-table", size="small", classes="ml-1") vuetify3.VTextField( v_model=("runs_table_search",), placeholder="Search...", @@ -1514,11 +1514,11 @@ def __init__( classes="text-none", size="small", ): - vuetify3.VIcon("mdi-table", size="small", classes="mr-1") - html.Span("Runs", classes="text-caption") vuetify3.VIcon( - "mdi-chevron-right", size="small", classes="ml-1" + "mdi-chevron-right", size="small", classes="mr-1" ) + html.Span("Runs", classes="text-caption") + vuetify3.VIcon("mdi-table", size="small", classes="ml-1") with html.Div( v_if=("comparison_collapsed",), classes="d-flex align-center pa-1", From 116447f8bce0f84b4551a1f61d9200368fd11416 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Mon, 2 Feb 2026 12:17:35 -0500 Subject: [PATCH 07/22] Add slide animation for panel expand/collapse --- align_app/app/ui.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/align_app/app/ui.py b/align_app/app/ui.py index a34ccce..0a1c921 100644 --- a/align_app/app/ui.py +++ b/align_app/app/ui.py @@ -895,9 +895,14 @@ def filterable_column(key: str, title: str, filter_var: str, options_var: str): class RunsTablePanel(html.Div): def __init__(self, **kwargs): super().__init__( - v_if=("!table_collapsed",), classes="runs-table-panel d-flex flex-column", - style="flex: 1; min-width: 25vw; height: 100%; overflow: hidden;", + style=( + "table_collapsed ? " + "'flex: 0; width: 0; min-width: 0; height: 100%; overflow: hidden; " + "transition: flex 0.3s ease, width 0.3s ease, min-width 0.3s ease;' : " + "'flex: 1; min-width: 25vw; height: 100%; overflow: hidden; " + "transition: flex 0.3s ease, width 0.3s ease, min-width 0.3s ease;'", + ), **kwargs, ) ctrl = self.server.controller @@ -1009,9 +1014,8 @@ def __init__(self, **kwargs): class ComparisonPanel(html.Div): def __init__(self, **kwargs): super().__init__( - v_if=("!comparison_collapsed",), classes="comparison-panel d-flex flex-column flex-grow-1", - style="min-width: 200px; height: 100%; overflow: hidden;", + style="min-width: 0; height: 100%; overflow: hidden;", **kwargs, ) ctrl = self.server.controller @@ -1537,9 +1541,14 @@ def __init__( ) RunsTablePanel() with html.Div( - v_if=("!comparison_collapsed",), classes=("isDragging ? 'drop-zone-active' : ''",), - style="flex: 1; overflow: hidden;", + style=( + "comparison_collapsed ? " + "'flex: 0; width: 0; min-width: 0; overflow: hidden; " + "transition: flex 0.3s ease, width 0.3s ease, min-width 0.3s ease;' : " + "'flex: 1; min-width: 200px; overflow: hidden; " + "transition: flex 0.3s ease, width 0.3s ease, min-width 0.3s ease;'", + ), raw_attrs=[ '@dragover.prevent="isDragging = true"', '@dragleave.prevent="isDragging = false"', From b5fd5c598c321b3d52ba9adb82ecbaecd0c9db11 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Mon, 2 Feb 2026 12:25:08 -0500 Subject: [PATCH 08/22] Hide panel header immediately on collapse to prevent button overlap --- align_app/app/ui.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/align_app/app/ui.py b/align_app/app/ui.py index 0a1c921..8866903 100644 --- a/align_app/app/ui.py +++ b/align_app/app/ui.py @@ -908,6 +908,7 @@ def __init__(self, **kwargs): ctrl = self.server.controller with self: with html.Div( + v_show=("!table_collapsed",), classes="d-flex align-center pa-1 flex-shrink-0", style="height: 2.5rem;", ): @@ -1021,6 +1022,7 @@ def __init__(self, **kwargs): ctrl = self.server.controller with self: with html.Div( + v_show=("!comparison_collapsed",), classes="d-flex justify-end align-center pa-1 flex-shrink-0", style="height: 2.5rem;", ): From acb784edd14974e14312120b474e28707be91f38 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Mon, 2 Feb 2026 12:37:34 -0500 Subject: [PATCH 09/22] Fix panel collapse animation direction and header visibility --- align_app/app/ui.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/align_app/app/ui.py b/align_app/app/ui.py index 8866903..1e2cfd7 100644 --- a/align_app/app/ui.py +++ b/align_app/app/ui.py @@ -908,9 +908,12 @@ def __init__(self, **kwargs): ctrl = self.server.controller with self: with html.Div( - v_show=("!table_collapsed",), classes="d-flex align-center pa-1 flex-shrink-0", - style="height: 2.5rem;", + style=( + "table_collapsed ? " + "'height: 2.5rem; visibility: hidden;' : " + "'height: 2.5rem;'", + ), ): with vuetify3.VBtn( variant="text", @@ -1022,9 +1025,12 @@ def __init__(self, **kwargs): ctrl = self.server.controller with self: with html.Div( - v_show=("!comparison_collapsed",), classes="d-flex justify-end align-center pa-1 flex-shrink-0", - style="height: 2.5rem;", + style=( + "comparison_collapsed ? " + "'height: 2.5rem; visibility: hidden;' : " + "'height: 2.5rem;'", + ), ): with vuetify3.VBtn( variant="text", @@ -1546,9 +1552,9 @@ def __init__( classes=("isDragging ? 'drop-zone-active' : ''",), style=( "comparison_collapsed ? " - "'flex: 0; width: 0; min-width: 0; overflow: hidden; " + "'flex: 0; width: 0; min-width: 0; margin-left: auto; overflow: hidden; " "transition: flex 0.3s ease, width 0.3s ease, min-width 0.3s ease;' : " - "'flex: 1; min-width: 200px; overflow: hidden; " + "'flex: 1; min-width: 200px; margin-left: auto; overflow: hidden; " "transition: flex 0.3s ease, width 0.3s ease, min-width 0.3s ease;'", ), raw_attrs=[ From ce25150bfb195175be17f899560386ecbb1d7166 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Mon, 2 Feb 2026 12:57:13 -0500 Subject: [PATCH 10/22] Add table column maxWidth constraints and situation info icon tooltip - Add maxWidth to table columns to prevent unbounded growth - Situation column capped at 200px with text truncation - Add info icon with hover tooltip showing full situation text - Filter controls row can now wrap when space is limited --- align_app/app/runs_state_adapter.py | 21 ++++++++------ align_app/app/ui.py | 43 +++++++++++++++++++++-------- 2 files changed, 45 insertions(+), 19 deletions(-) diff --git a/align_app/app/runs_state_adapter.py b/align_app/app/runs_state_adapter.py index 16b28a9..4a6b259 100644 --- a/align_app/app/runs_state_adapter.py +++ b/align_app/app/runs_state_adapter.py @@ -37,14 +37,19 @@ def __init__( self.server.state.runs_table_selected = [] self.server.state.runs_table_search = "" self.server.state.runs_table_headers = [ - {"title": "", "key": "in_comparison", "sortable": False}, - {"title": "Scenario", "key": "scenario_id"}, - {"title": "Scene", "key": "scene_id"}, - {"title": "Situation", "key": "probe_text", "sortable": False}, - {"title": "Decider", "key": "decider_name"}, - {"title": "LLM", "key": "llm_backbone_name"}, - {"title": "Alignment", "key": "alignment_summary"}, - {"title": "Decision", "key": "decision_text"}, + {"title": "", "key": "in_comparison", "sortable": False, "width": "40px"}, + {"title": "Scenario", "key": "scenario_id", "maxWidth": "150px"}, + {"title": "Scene", "key": "scene_id", "maxWidth": "120px"}, + { + "title": "Situation", + "key": "probe_text", + "sortable": False, + "maxWidth": "200px", + }, + {"title": "Decider", "key": "decider_name", "maxWidth": "180px"}, + {"title": "LLM", "key": "llm_backbone_name", "maxWidth": "180px"}, + {"title": "Alignment", "key": "alignment_summary", "maxWidth": "150px"}, + {"title": "Decision", "key": "decision_text", "maxWidth": "180px"}, ] self.server.state.import_experiment_file = None self.server.state.adm_browser_open = False diff --git a/align_app/app/ui.py b/align_app/app/ui.py index 1e2cfd7..477dbda 100644 --- a/align_app/app/ui.py +++ b/align_app/app/ui.py @@ -886,6 +886,29 @@ def cell_with_tooltip(key: str): html.Span(f"{{{{ item.{key} }}}}", v_bind_title=f"item.{key}") +def situation_cell_with_info_icon(): + """Create situation cell with truncated text and info icon tooltip.""" + with html.Template(raw_attrs=['v-slot:item.probe_text="{ item }"']): + with html.Div(classes="d-flex align-center", style="gap: 4px;"): + html.Span( + "{{ item.probe_text }}", + style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1;", + ) + with vuetify3.VTooltip(location="top", max_width="400px"): + with vuetify3.Template(v_slot_activator="{ props }"): + vuetify3.VIcon( + "mdi-information-outline", + size="x-small", + v_bind="props", + classes="text-grey flex-shrink-0", + style="cursor: help;", + ) + html.Div( + "{{ item.probe_text }}", + style="white-space: normal; word-wrap: break-word;", + ) + + def filterable_column(key: str, title: str, filter_var: str, options_var: str): """Create sortable column header with filter and cell tooltip.""" sortable_filter_header(key, title, filter_var, options_var) @@ -908,11 +931,11 @@ def __init__(self, **kwargs): ctrl = self.server.controller with self: with html.Div( - classes="d-flex align-center pa-1 flex-shrink-0", + classes="d-flex align-center flex-wrap pa-1 flex-shrink-0 ga-1", style=( "table_collapsed ? " - "'height: 2.5rem; visibility: hidden;' : " - "'height: 2.5rem;'", + "'min-height: 2.5rem; visibility: hidden;' : " + "'min-height: 2.5rem;'", ), ): with vuetify3.VBtn( @@ -932,8 +955,7 @@ def __init__(self, **kwargs): hide_details=True, density="compact", variant="underlined", - style="max-width: 200px;", - classes="mx-2", + style="min-width: 100px; max-width: 200px; flex: 1 1 100px;", ) with vuetify3.VBtn( size="x-small", @@ -983,7 +1005,7 @@ def __init__(self, **kwargs): "runs_table_filter_scene", "runs_table_scene_options", ) - cell_with_tooltip("probe_text") + situation_cell_with_info_icon() filterable_column( "decider_name", "Decider", @@ -1229,7 +1251,7 @@ def __init__(self, **kwargs): "runs_table_filter_scene", "runs_table_scene_options", ) - cell_with_tooltip("probe_text") + situation_cell_with_info_icon() filterable_column( "decider_name", "Decider", @@ -1503,10 +1525,9 @@ def __init__( ".v-textarea .v-field__input { overflow-y: hidden !important; }" ".v-expansion-panel { max-width: none !important; }" ".config-textarea textarea { white-space: pre; overflow-x: auto; }" - ".v-data-table table { table-layout: fixed; width: 100%; }" - ".v-data-table td { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }" - ".v-data-table th { vertical-align: top; }" - ".v-data-table th:first-child { padding-top: 8px; }" + ".runs-table-panel .v-data-table td { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }" + ".runs-table-panel .v-data-table th { vertical-align: top; }" + ".runs-table-panel .v-data-table th:first-child { padding-top: 8px; }" ".drop-zone-active { outline: 3px dashed #1976d2 !important; outline-offset: -3px; }" "'" ) From 49c72fb941a13bc25e3878a3c633c19d04efce6d Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Mon, 2 Feb 2026 13:12:53 -0500 Subject: [PATCH 11/22] Add pagination to runs table panel (50 items per page) --- align_app/app/ui.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/align_app/app/ui.py b/align_app/app/ui.py index 477dbda..c06bdfe 100644 --- a/align_app/app/ui.py +++ b/align_app/app/ui.py @@ -974,8 +974,7 @@ def __init__(self, **kwargs): hover=True, density="compact", search=("runs_table_search",), - items_per_page=(-1,), - hide_default_footer=True, + items_per_page=(50,), click_row=(ctrl.on_table_row_click, "[$event, item]"), ): with html.Template( From 661e972e7b73c2e9eb03a3f31816c907a83461c9 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Mon, 2 Feb 2026 14:22:51 -0500 Subject: [PATCH 12/22] Fix config comparison for revert-to-original detection Normalize configs via YAML round-trip before comparing to handle dict ordering/type differences from Hydra vs YAML parsing. --- align_app/app/runs_state_adapter.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/align_app/app/runs_state_adapter.py b/align_app/app/runs_state_adapter.py index 4a6b259..2264b79 100644 --- a/align_app/app/runs_state_adapter.py +++ b/align_app/app/runs_state_adapter.py @@ -505,7 +505,10 @@ def _create_run_with_edited_config( llm_backbone=run.llm_backbone_name, ) - if root_config == new_config: + def normalize_config(cfg): + return yaml.safe_load(yaml.dump(cfg, sort_keys=True)) + + if normalize_config(root_config) == normalize_config(new_config): new_decider_name = root_decider_name else: new_decider_name = self.decider_registry.add_edited_decider( From f07accb76d00903d7ef25df60481f68fada0c4e6 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Mon, 2 Feb 2026 17:03:29 -0500 Subject: [PATCH 13/22] Optimize state.runs to only hold comparison runs - state.runs now only contains runs in runs_to_compare (2-4) instead of all runs - Add incremental update methods: _add_run_to_comparison, _remove_run_from_comparison, _update_run_in_comparison, _rebuild_comparison_runs - Add run_to_table_row_direct() to create table rows without full state dict - Update all callers to use incremental methods instead of full rebuilds - Fix export fallback to use registry instead of state.runs --- align_app/app/runs_presentation.py | 33 ++++++ align_app/app/runs_state_adapter.py | 151 ++++++++++++++++++---------- 2 files changed, 133 insertions(+), 51 deletions(-) diff --git a/align_app/app/runs_presentation.py b/align_app/app/runs_presentation.py index cfb0387..30bb1c8 100644 --- a/align_app/app/runs_presentation.py +++ b/align_app/app/runs_presentation.py @@ -74,6 +74,39 @@ def experiment_item_to_table_row( } +def run_to_table_row_direct(run: Run, probe: Probe) -> Dict[str, Any]: + """Convert Run directly to table row without building full state dict.""" + kdma_values = run.decider_params.alignment_target.kdma_values + alignment_summary = ( + ", ".join(f"{readable(kv.kdma)} {kv.value}" for kv in kdma_values) + if kdma_values + else "None" + ) + + display_state = probe.display_state or "" + choices = probe.choices or [] + choice_texts = " ".join(c.get("unstructured", "") for c in choices) + + decision_text = "" + if run.decision: + choice_letter = chr(run.decision.choice_index + ord("A")) + decision_text = ( + f"{choice_letter}. {run.decision.adm_result.decision.unstructured}" + ) + + return { + "id": run.compute_cache_key(), + "scenario_id": probe.scenario_id, + "scene_id": probe.scene_id, + "probe_text": display_state, + "decider_name": run.decider_name, + "llm_backbone_name": run.llm_backbone_name, + "alignment_summary": alignment_summary, + "decision_text": decision_text, + "searchable_text": f"{display_state} {choice_texts}", + } + + def get_max_alignment_attributes(decider_configs: Dict) -> int: if not decider_configs: return 0 diff --git a/align_app/app/runs_state_adapter.py b/align_app/app/runs_state_adapter.py index 2264b79..4c9471c 100644 --- a/align_app/app/runs_state_adapter.py +++ b/align_app/app/runs_state_adapter.py @@ -1,4 +1,4 @@ -from typing import Dict, Optional, Callable +from typing import Optional, Callable from trame.app import asynchronous from trame.app.file_upload import ClientFile from trame.decorators import TrameApp, controller, change, trigger @@ -58,32 +58,59 @@ def __init__( self.server.state.selected_system_adms = [] self.server.state.probe_dirty = {} self.server.state.config_dirty = {} + self.server.state.runs = {} + self.server.state.runs_to_compare = [] self.table_filter = RunsTableFilter(server) - self._sync_from_runs_data(runs_registry.get_all_runs()) + self._update_table_rows() @property def state(self): return self.server.state - def _sync_from_runs_data(self, runs_dict: Dict[str, Run]): - new_runs = {} - for run_id, run in runs_dict.items(): - new_run = runs_presentation.run_to_state_dict( + def _add_run_to_comparison(self, run: Run): + """Add single run to state.runs.""" + if run.id not in self.state.runs: + run_dict = runs_presentation.run_to_state_dict( run, self.probe_registry, self.decider_registry ) - new_runs[run_id] = new_run + self.state.runs = {**self.state.runs, run.id: run_dict} + + def _remove_run_from_comparison(self, run_id: str): + """Remove run from state.runs.""" + if run_id in self.state.runs: + self.state.runs = {k: v for k, v in self.state.runs.items() if k != run_id} + + def _update_run_in_comparison(self, run: Run): + """Update single run in state.runs if it's in comparison.""" + if run.id in self.state.runs_to_compare: + run_dict = runs_presentation.run_to_state_dict( + run, self.probe_registry, self.decider_registry + ) + self.state.runs = {**self.state.runs, run.id: run_dict} + + def _rebuild_comparison_runs(self): + """Rebuild state.runs from runs_to_compare (for imports/registry changes).""" + new_runs = {} + for run_id in self.state.runs_to_compare: + run = self.runs_registry.get_run(run_id) + if run: + new_runs[run_id] = runs_presentation.run_to_state_dict( + run, self.probe_registry, self.decider_registry + ) self.state.runs = new_runs - run_table_rows_by_id = { - row["id"]: row - for row in ( - runs_presentation.run_to_table_row(run_dict) - for run_dict in new_runs.values() + def _update_table_rows(self): + """Update table rows without touching state.runs.""" + all_runs = self.runs_registry.get_all_runs() + run_table_rows = [ + runs_presentation.run_to_table_row_direct( + run, self.probe_registry.get_probe(run.probe_id) ) - } - run_table_rows = list(run_table_rows_by_id.values()) + for run in all_runs.values() + ] + run_table_rows_by_id = {row["id"]: row for row in run_table_rows} - active_cache_keys = {run.compute_cache_key() for run in runs_dict.values()} + active_cache_keys = {run.compute_cache_key() for run in all_runs.values()} stored_items = self.runs_registry.get_all_experiment_items() experiment_table_rows = [ runs_presentation.experiment_item_to_table_row( @@ -93,31 +120,28 @@ def _sync_from_runs_data(self, runs_dict: Dict[str, Run]): if cache_key not in active_cache_keys ] - self.table_filter.set_all_rows(run_table_rows + experiment_table_rows) + self.table_filter.set_all_rows( + list(run_table_rows_by_id.values()) + experiment_table_rows + ) probes = self.probe_registry.get_probes() self.state.base_scenarios = extract_base_scenarios(probes) - self.state.probe_dirty = {} - self.state.config_dirty = {} - - if not self.state.runs: - self.state.runs_to_compare = [] - self.state.runs_json = "[]" - self.state.run_edit_configs = {} - @controller.set("reset_runs_state") def reset_state(self): self.runs_registry.clear_runs() - self._sync_from_runs_data({}) + self.state.runs = {} + self.state.runs_to_compare = [] + self._update_table_rows() self.create_default_run() @controller.set("clear_all_runs") def clear_all_runs(self): self.runs_registry.clear_all() - self._sync_from_runs_data({}) + self.state.runs = {} self.state.runs_to_compare = [] self.state.runs_table_selected = [] + self._update_table_rows() self.create_default_run() def create_default_run(self): @@ -194,18 +218,25 @@ def toggle_comparison_collapsed(self): @controller.set("toggle_run_in_comparison") def toggle_run_in_comparison(self, cache_key): run = self.runs_registry.get_run_by_cache_key(cache_key) + + if run and run.id in self.state.runs_to_compare: + self.state.runs_to_compare = [ + rid for rid in self.state.runs_to_compare if rid != run.id + ] + return + if not run: run = self.runs_registry.materialize_experiment_item(cache_key) if not run: return - runs_to_compare = list(self.state.runs_to_compare) - if run.id in runs_to_compare: - runs_to_compare.remove(run.id) - else: - runs_to_compare.append(run.id) - self.state.runs_to_compare = runs_to_compare - self._sync_from_runs_data(self.runs_registry.get_all_runs()) + self.state.runs_to_compare = [*self.state.runs_to_compare, run.id] + + if run.id not in self.state.runs: + run_dict = runs_presentation.run_to_state_dict( + run, self.probe_registry, self.decider_registry + ) + self.state.runs = {**self.state.runs, run.id: run_dict} @controller.set("copy_run") def copy_run(self, run_id, column_index): @@ -239,15 +270,13 @@ def _sync_run_to_state(self, run: Run, insert_at_index=None): def _handle_run_update(self, old_run_id: str, new_run: Optional[Run]): if new_run: - # Always replace old run ID with new ID in the comparison view - # This keeps the run in the same UI position - # Note: Registry layer handles whether to keep or remove the old run - # based on decision state (see _create_update_method) self.state.runs_to_compare = [ new_run.id if rid == old_run_id else rid for rid in self.state.runs_to_compare ] - self._sync_from_runs_data(self.runs_registry.get_all_runs()) + self._remove_run_from_comparison(old_run_id) + self._add_run_to_comparison(new_run) + self._update_table_rows() @controller.set("update_run_scene") def update_run_scene(self, run_id: str, scene_id: str): @@ -407,14 +436,15 @@ def save_probe_edits( ) self.runs_registry.add_run(new_run) - # Cleanup: If original run had no decision (was a draft), remove it if run.decision is None: self.runs_registry.remove_run(run_id) self.state.runs_to_compare = [ new_run_id if rid == run_id else rid for rid in self.state.runs_to_compare ] - self._sync_from_runs_data(self.runs_registry.get_all_runs()) + self._remove_run_from_comparison(run_id) + self._add_run_to_comparison(new_run) + self._update_table_rows() @controller.set("save_config_edits") def save_config_edits(self, run_id: str, current_yaml: str = ""): @@ -532,14 +562,15 @@ def normalize_config(cfg): ) self.runs_registry.add_run(new_run) - # Cleanup: If original run had no decision (was a draft), remove it if run.decision is None: self.runs_registry.remove_run(run_id) self.state.runs_to_compare = [ new_run_id if rid == run_id else rid for rid in self.state.runs_to_compare ] - self._sync_from_runs_data(self.runs_registry.get_all_runs()) + self._remove_run_from_comparison(run_id) + self._add_run_to_comparison(new_run) + self._update_table_rows() return new_run_id def _add_pending_cache_key(self, cache_key: str): @@ -587,8 +618,10 @@ async def _execute_run_decision(self, run_id: str): await self.runs_registry.execute_run_decision(run_id) with self.state: - all_runs = self.runs_registry.get_all_runs() - self._sync_from_runs_data(all_runs) + run = self.runs_registry.get_run(run_id) + if run: + self._update_run_in_comparison(run) + self._update_table_rows() self._remove_pending_cache_key(cache_key) @controller.set("execute_run_decision") @@ -606,7 +639,15 @@ def trigger_export_runs_zip(self) -> bytes: def trigger_export_table_runs_zip(self) -> bytes: selected = self.state.runs_table_selected if not selected: - return export_runs_to_zip(self.state.runs) + all_runs = self.runs_registry.get_all_runs() + runs_to_export = { + rid: runs_presentation.run_to_state_dict( + r, self.probe_registry, self.decider_registry + ) + for rid, r in all_runs.items() + if r.decision + } + return export_runs_to_zip(runs_to_export) selected_runs = {} for item in selected: @@ -642,6 +683,7 @@ def add_selected_runs_to_compare(self): return new_runs_to_compare = [] + runs_to_add = [] for item in selected: cache_key = item["id"] if isinstance(item, dict) else item @@ -653,11 +695,16 @@ def add_selected_runs_to_compare(self): if run and run.id not in new_runs_to_compare: new_runs_to_compare.append(run.id) + runs_to_add.append(run) + + self.state.runs = {} + for run in runs_to_add: + self._add_run_to_comparison(run) self.state.runs_to_compare = new_runs_to_compare self.state.runs_table_modal_open = False self.state.runs_table_selected = [] - self._sync_from_runs_data(self.runs_registry.get_all_runs()) + self._update_table_rows() @controller.set("on_table_row_click") def on_table_row_click(self, _event, item): @@ -672,7 +719,8 @@ def on_table_row_click(self, _event, item): if run and run.id not in self.state.runs_to_compare: self.state.runs_to_compare = [*self.state.runs_to_compare, run.id] - self._sync_from_runs_data(self.runs_registry.get_all_runs()) + self._add_run_to_comparison(run) + self._update_table_rows() @change("runs") def update_runs_json(self, **_): @@ -695,7 +743,7 @@ def on_import_experiment_file(self, import_experiment_file, **_): self.decider_registry.add_deciders(result.deciders) self.runs_registry.add_experiment_items(result.items) - self._sync_from_runs_data(self.runs_registry.get_all_runs()) + self._update_table_rows() self.state.import_experiment_file = None @@ -714,7 +762,7 @@ def trigger_import_directory_files(self, files_data): self.probe_registry.add_probes(result.probes) self.decider_registry.add_deciders(result.deciders) self.runs_registry.add_experiment_items(result.items) - self._sync_from_runs_data(self.runs_registry.get_all_runs()) + self._update_table_rows() @trigger("import_zip_bytes") def trigger_import_zip_bytes(self, zip_content): @@ -722,11 +770,12 @@ def trigger_import_zip_bytes(self, zip_content): self.probe_registry.add_probes(result.probes) self.decider_registry.add_deciders(result.deciders) self.runs_registry.add_experiment_items(result.items) - self._sync_from_runs_data(self.runs_registry.get_all_runs()) + self._update_table_rows() def update_decider_registry(self, new_registry): self.decider_registry = new_registry - self._sync_from_runs_data(self.runs_registry.get_all_runs()) + self._rebuild_comparison_runs() + self._update_table_rows() @controller.set("open_adm_browser") def open_adm_browser(self, run_id: str | None = None): From ec1010443f98e02e7c62c288db300ecd31b9b338 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Mon, 2 Feb 2026 17:06:23 -0500 Subject: [PATCH 14/22] Show initial draft run in table --- align_app/app/runs_state_adapter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/align_app/app/runs_state_adapter.py b/align_app/app/runs_state_adapter.py index 4c9471c..bf2f7ee 100644 --- a/align_app/app/runs_state_adapter.py +++ b/align_app/app/runs_state_adapter.py @@ -267,6 +267,7 @@ def _sync_run_to_state(self, run: Run, insert_at_index=None): else: runs_to_compare.insert(insert_at_index, run.id) self.state.runs_to_compare = runs_to_compare + self._update_table_rows() def _handle_run_update(self, old_run_id: str, new_run: Optional[Run]): if new_run: From 7fb717dad3df7a0f99ac530c78e1d1068df98dd3 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Mon, 2 Feb 2026 17:09:06 -0500 Subject: [PATCH 15/22] Stabilize table column widths with fixed layout --- align_app/app/runs_state_adapter.py | 19 +++++++------------ align_app/app/ui.py | 1 + 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/align_app/app/runs_state_adapter.py b/align_app/app/runs_state_adapter.py index bf2f7ee..1a64140 100644 --- a/align_app/app/runs_state_adapter.py +++ b/align_app/app/runs_state_adapter.py @@ -38,18 +38,13 @@ def __init__( self.server.state.runs_table_search = "" self.server.state.runs_table_headers = [ {"title": "", "key": "in_comparison", "sortable": False, "width": "40px"}, - {"title": "Scenario", "key": "scenario_id", "maxWidth": "150px"}, - {"title": "Scene", "key": "scene_id", "maxWidth": "120px"}, - { - "title": "Situation", - "key": "probe_text", - "sortable": False, - "maxWidth": "200px", - }, - {"title": "Decider", "key": "decider_name", "maxWidth": "180px"}, - {"title": "LLM", "key": "llm_backbone_name", "maxWidth": "180px"}, - {"title": "Alignment", "key": "alignment_summary", "maxWidth": "150px"}, - {"title": "Decision", "key": "decision_text", "maxWidth": "180px"}, + {"title": "Scenario", "key": "scenario_id", "width": "150px"}, + {"title": "Scene", "key": "scene_id", "width": "120px"}, + {"title": "Situation", "key": "probe_text", "sortable": False, "width": "200px"}, + {"title": "Decider", "key": "decider_name", "width": "180px"}, + {"title": "LLM", "key": "llm_backbone_name", "width": "180px"}, + {"title": "Alignment", "key": "alignment_summary", "width": "150px"}, + {"title": "Decision", "key": "decision_text", "width": "180px"}, ] self.server.state.import_experiment_file = None self.server.state.adm_browser_open = False diff --git a/align_app/app/ui.py b/align_app/app/ui.py index c06bdfe..26c790d 100644 --- a/align_app/app/ui.py +++ b/align_app/app/ui.py @@ -1524,6 +1524,7 @@ def __init__( ".v-textarea .v-field__input { overflow-y: hidden !important; }" ".v-expansion-panel { max-width: none !important; }" ".config-textarea textarea { white-space: pre; overflow-x: auto; }" + ".runs-table-panel .v-data-table table { table-layout: fixed; width: 100%; }" ".runs-table-panel .v-data-table td { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }" ".runs-table-panel .v-data-table th { vertical-align: top; }" ".runs-table-panel .v-data-table th:first-child { padding-top: 8px; }" From 540086a3a92c46f1a11dc726c68a0ff0fa39ff40 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Mon, 2 Feb 2026 17:11:36 -0500 Subject: [PATCH 16/22] Add toggled runs to front of comparison (left side) --- align_app/app/runs_state_adapter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/align_app/app/runs_state_adapter.py b/align_app/app/runs_state_adapter.py index 1a64140..2015b46 100644 --- a/align_app/app/runs_state_adapter.py +++ b/align_app/app/runs_state_adapter.py @@ -225,7 +225,7 @@ def toggle_run_in_comparison(self, cache_key): if not run: return - self.state.runs_to_compare = [*self.state.runs_to_compare, run.id] + self.state.runs_to_compare = [run.id, *self.state.runs_to_compare] if run.id not in self.state.runs: run_dict = runs_presentation.run_to_state_dict( @@ -714,7 +714,7 @@ def on_table_row_click(self, _event, item): run = self.runs_registry.materialize_experiment_item(cache_key) if run and run.id not in self.state.runs_to_compare: - self.state.runs_to_compare = [*self.state.runs_to_compare, run.id] + self.state.runs_to_compare = [run.id, *self.state.runs_to_compare] self._add_run_to_comparison(run) self._update_table_rows() From e2c4d5679ff574092bea7b5e0ed65f198e0416ef Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Mon, 2 Feb 2026 17:14:57 -0500 Subject: [PATCH 17/22] Keep table pagination visible with internal scroll --- align_app/app/ui.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/align_app/app/ui.py b/align_app/app/ui.py index 26c790d..0a628b9 100644 --- a/align_app/app/ui.py +++ b/align_app/app/ui.py @@ -965,8 +965,12 @@ def __init__(self, **kwargs): ): html.Span("Clear") - with html.Div(style="flex: 1; overflow: auto;"): - with vuetify3.VCard(elevation=2, classes="ma-1"): + with html.Div(style="flex: 1; overflow: hidden; display: flex;"): + with vuetify3.VCard( + elevation=2, + classes="ma-1 d-flex flex-column", + style="flex: 1; overflow: hidden;", + ): with vuetify3.VDataTable( items=("runs_table_items",), headers=("runs_table_headers",), @@ -976,6 +980,8 @@ def __init__(self, **kwargs): search=("runs_table_search",), items_per_page=(50,), click_row=(ctrl.on_table_row_click, "[$event, item]"), + fixed_header=True, + style="flex: 1; overflow: hidden;", ): with html.Template( raw_attrs=['v-slot:item.in_comparison="{ item }"'] @@ -1524,6 +1530,8 @@ def __init__( ".v-textarea .v-field__input { overflow-y: hidden !important; }" ".v-expansion-panel { max-width: none !important; }" ".config-textarea textarea { white-space: pre; overflow-x: auto; }" + ".runs-table-panel .v-data-table { display: flex; flex-direction: column; }" + ".runs-table-panel .v-data-table > .v-table__wrapper { flex: 1; overflow-y: auto; }" ".runs-table-panel .v-data-table table { table-layout: fixed; width: 100%; }" ".runs-table-panel .v-data-table td { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }" ".runs-table-panel .v-data-table th { vertical-align: top; }" From 7d5e719e326a8f076455815add74cf208bb8bc6e Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Mon, 2 Feb 2026 17:48:09 -0500 Subject: [PATCH 18/22] Clear All button only clears comparison, keeps runs --- align_app/app/runs_state_adapter.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/align_app/app/runs_state_adapter.py b/align_app/app/runs_state_adapter.py index 2015b46..ca2a353 100644 --- a/align_app/app/runs_state_adapter.py +++ b/align_app/app/runs_state_adapter.py @@ -132,12 +132,9 @@ def reset_state(self): @controller.set("clear_all_runs") def clear_all_runs(self): - self.runs_registry.clear_all() self.state.runs = {} self.state.runs_to_compare = [] - self.state.runs_table_selected = [] self._update_table_rows() - self.create_default_run() def create_default_run(self): probes = self.probe_registry.get_probes() From fc8d0e5a6d56865efa07656fbf5544f27c0c7d68 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Mon, 2 Feb 2026 18:32:00 -0500 Subject: [PATCH 19/22] Wrap table and comparison panels in cards with tab-styled collapse buttons --- align_app/app/ui.py | 171 +++++++++++++++++++++++--------------------- 1 file changed, 88 insertions(+), 83 deletions(-) diff --git a/align_app/app/ui.py b/align_app/app/ui.py index 0a628b9..864edd7 100644 --- a/align_app/app/ui.py +++ b/align_app/app/ui.py @@ -918,59 +918,58 @@ def filterable_column(key: str, title: str, filter_var: str, options_var: str): class RunsTablePanel(html.Div): def __init__(self, **kwargs): super().__init__( - classes="runs-table-panel d-flex flex-column", + classes="runs-table-panel", style=( "table_collapsed ? " - "'flex: 0; width: 0; min-width: 0; height: 100%; overflow: hidden; " - "transition: flex 0.3s ease, width 0.3s ease, min-width 0.3s ease;' : " - "'flex: 1; min-width: 25vw; height: 100%; overflow: hidden; " - "transition: flex 0.3s ease, width 0.3s ease, min-width 0.3s ease;'", + "'width: 0; min-width: 0; height: 100%; overflow: hidden; " + "transition: all 0.3s ease;' : " + "'flex: 1; min-width: 25vw; height: 100%; " + "transition: all 0.3s ease;'", ), **kwargs, ) ctrl = self.server.controller with self: - with html.Div( - classes="d-flex align-center flex-wrap pa-1 flex-shrink-0 ga-1", - style=( - "table_collapsed ? " - "'min-height: 2.5rem; visibility: hidden;' : " - "'min-height: 2.5rem;'", - ), + with vuetify3.VCard( + v_if=("!table_collapsed",), + elevation=2, + classes="d-flex flex-column ma-1", + style="height: calc(100% - 8px); overflow: hidden;", ): - with vuetify3.VBtn( - variant="text", - click=(ctrl.toggle_table_collapsed,), - classes="text-none", - size="small", - ): - vuetify3.VIcon("mdi-chevron-left", size="small", classes="mr-1") - html.Span("Runs", classes="text-caption") - vuetify3.VIcon("mdi-table", size="small", classes="ml-1") - vuetify3.VTextField( - v_model=("runs_table_search",), - placeholder="Search...", - prepend_inner_icon="mdi-magnify", - clearable=True, - hide_details=True, - density="compact", - variant="underlined", - style="min-width: 100px; max-width: 200px; flex: 1 1 100px;", - ) - with vuetify3.VBtn( - size="x-small", - variant="text", - click=(ctrl.clear_all_table_filters,), - prepend_icon="mdi-filter-off", + with html.Div( + classes="d-flex align-start flex-wrap flex-shrink-0 ga-1", + style="padding: 0 4px 0 0;", ): - html.Span("Clear") + with vuetify3.VBtn( + variant="flat", + color="grey-lighten-3", + click=(ctrl.toggle_table_collapsed,), + classes="text-none", + style="border-radius: 0 0 8px 0;", + size="small", + ): + vuetify3.VIcon("mdi-chevron-left", size="small", classes="mr-1") + html.Span("Runs", classes="text-caption") + vuetify3.VIcon("mdi-table", size="small", classes="ml-1") + vuetify3.VTextField( + v_model=("runs_table_search",), + placeholder="Search...", + prepend_inner_icon="mdi-magnify", + clearable=True, + hide_details=True, + density="compact", + variant="underlined", + style="min-width: 100px; max-width: 200px; flex: 1 1 100px;", + ) + with vuetify3.VBtn( + size="x-small", + variant="text", + click=(ctrl.clear_all_table_filters,), + prepend_icon="mdi-filter-off", + ): + html.Span("Clear") - with html.Div(style="flex: 1; overflow: hidden; display: flex;"): - with vuetify3.VCard( - elevation=2, - classes="ma-1 d-flex flex-column", - style="flex: 1; overflow: hidden;", - ): + with html.Div(style="flex: 1; overflow: hidden; display: flex;"): with vuetify3.VDataTable( items=("runs_table_items",), headers=("runs_table_headers",), @@ -1045,32 +1044,51 @@ def __init__(self, **kwargs): class ComparisonPanel(html.Div): def __init__(self, **kwargs): super().__init__( - classes="comparison-panel d-flex flex-column flex-grow-1", - style="min-width: 0; height: 100%; overflow: hidden;", + classes="comparison-panel d-flex", + style=( + "comparison_collapsed ? " + "'width: 0; min-width: 0; height: 100%; overflow: hidden; " + "transition: all 0.3s ease;' : " + "'flex: 1; min-width: 200px; height: 100%; " + "transition: all 0.3s ease;'", + ), **kwargs, ) ctrl = self.server.controller with self: - with html.Div( - classes="d-flex justify-end align-center pa-1 flex-shrink-0", - style=( - "comparison_collapsed ? " - "'height: 2.5rem; visibility: hidden;' : " - "'height: 2.5rem;'", + with vuetify3.VCard( + v_if=("!comparison_collapsed",), + elevation=2, + classes=( + "isDragging ? 'd-flex flex-column ma-1 drop-zone-active' : 'd-flex flex-column ma-1'", ), + style="flex: 1; height: calc(100% - 8px); overflow: hidden;", + raw_attrs=[ + '@dragover.prevent="isDragging = true"', + '@dragleave.prevent="isDragging = false"', + f'@drop.prevent="{DROP_HANDLER_JS}"', + ], ): - with vuetify3.VBtn( - variant="text", - click=(ctrl.toggle_comparison_collapsed,), - classes="text-none", - size="small", + with html.Div( + classes="d-flex justify-end align-start flex-shrink-0", + style="padding: 0 0 0 4px;", ): - vuetify3.VIcon("mdi-compare", size="small", classes="mr-1") - html.Span("Compare", classes="text-caption") - vuetify3.VIcon("mdi-chevron-right", size="small", classes="ml-1") + with vuetify3.VBtn( + variant="flat", + color="grey-lighten-3", + click=(ctrl.toggle_comparison_collapsed,), + classes="text-none", + style="border-radius: 0 0 0 8px;", + size="small", + ): + vuetify3.VIcon("mdi-compare", size="small", classes="mr-1") + html.Span("Compare", classes="text-caption") + vuetify3.VIcon( + "mdi-chevron-right", size="small", classes="ml-1" + ) - with html.Div(style="flex: 1; overflow: auto;"): - ResultsComparison() + with html.Div(style="flex: 1; overflow: auto;"): + ResultsComparison() class RunsTableModal(html.Div): @@ -1546,13 +1564,14 @@ def __init__( ): with html.Div( v_if=("table_collapsed",), - classes="d-flex align-center pa-1", - style="position: absolute; top: 0; left: 0; z-index: 1; height: 2.5rem;", + style="position: absolute; left: 0; top: 0; z-index: 1;", ): with vuetify3.VBtn( - variant="text", + variant="flat", + color="grey-lighten-3", click=(server.controller.toggle_table_collapsed,), classes="text-none", + style="border-radius: 0 0 8px 0;", size="small", ): vuetify3.VIcon( @@ -1562,13 +1581,14 @@ def __init__( vuetify3.VIcon("mdi-table", size="small", classes="ml-1") with html.Div( v_if=("comparison_collapsed",), - classes="d-flex align-center pa-1", - style="position: absolute; top: 0; right: 0; z-index: 1; height: 2.5rem;", + style="position: absolute; right: 0; top: 0; z-index: 1;", ): with vuetify3.VBtn( - variant="text", + variant="flat", + color="grey-lighten-3", click=(server.controller.toggle_comparison_collapsed,), classes="text-none", + style="border-radius: 0 0 0 8px;", size="small", ): vuetify3.VIcon("mdi-compare", size="small", classes="mr-1") @@ -1577,21 +1597,6 @@ def __init__( "mdi-chevron-left", size="small", classes="ml-1" ) RunsTablePanel() - with html.Div( - classes=("isDragging ? 'drop-zone-active' : ''",), - style=( - "comparison_collapsed ? " - "'flex: 0; width: 0; min-width: 0; margin-left: auto; overflow: hidden; " - "transition: flex 0.3s ease, width 0.3s ease, min-width 0.3s ease;' : " - "'flex: 1; min-width: 200px; margin-left: auto; overflow: hidden; " - "transition: flex 0.3s ease, width 0.3s ease, min-width 0.3s ease;'", - ), - raw_attrs=[ - '@dragover.prevent="isDragging = true"', - '@dragleave.prevent="isDragging = false"', - f'@drop.prevent="{DROP_HANDLER_JS}"', - ], - ): - ComparisonPanel() + ComparisonPanel() RunsTableModal() AdmBrowserModal() From 77bd49d353692fa483af1d51df5ae3f7e9ec3f52 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Mon, 2 Feb 2026 18:33:22 -0500 Subject: [PATCH 20/22] Fix comparison panel expand direction to go right-to-left --- align_app/app/ui.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/align_app/app/ui.py b/align_app/app/ui.py index 864edd7..53c7429 100644 --- a/align_app/app/ui.py +++ b/align_app/app/ui.py @@ -1047,9 +1047,9 @@ def __init__(self, **kwargs): classes="comparison-panel d-flex", style=( "comparison_collapsed ? " - "'width: 0; min-width: 0; height: 100%; overflow: hidden; " + "'width: 0; min-width: 0; height: 100%; overflow: hidden; margin-left: auto; " "transition: all 0.3s ease;' : " - "'flex: 1; min-width: 200px; height: 100%; " + "'flex: 1; min-width: 200px; height: 100%; margin-left: auto; " "transition: all 0.3s ease;'", ), **kwargs, From 054e1cad99d4212f60ada4bf7cbbe67f4e93f1f4 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Mon, 2 Feb 2026 18:57:36 -0500 Subject: [PATCH 21/22] Fix flaky e2e tests by adding waits for textarea state updates --- align_app/app/runs_state_adapter.py | 7 ++++++- tests/e2e/page_objects/align_page.py | 12 ++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/align_app/app/runs_state_adapter.py b/align_app/app/runs_state_adapter.py index ca2a353..1fc6f68 100644 --- a/align_app/app/runs_state_adapter.py +++ b/align_app/app/runs_state_adapter.py @@ -40,7 +40,12 @@ def __init__( {"title": "", "key": "in_comparison", "sortable": False, "width": "40px"}, {"title": "Scenario", "key": "scenario_id", "width": "150px"}, {"title": "Scene", "key": "scene_id", "width": "120px"}, - {"title": "Situation", "key": "probe_text", "sortable": False, "width": "200px"}, + { + "title": "Situation", + "key": "probe_text", + "sortable": False, + "width": "200px", + }, {"title": "Decider", "key": "decider_name", "width": "180px"}, {"title": "LLM", "key": "llm_backbone_name", "width": "180px"}, {"title": "Alignment", "key": "alignment_summary", "width": "150px"}, diff --git a/tests/e2e/page_objects/align_page.py b/tests/e2e/page_objects/align_page.py index beafb85..8ef4381 100644 --- a/tests/e2e/page_objects/align_page.py +++ b/tests/e2e/page_objects/align_page.py @@ -238,7 +238,11 @@ def get_situation_text(self) -> str: def set_situation_text(self, text: str) -> None: expect(self.situation_textarea).to_be_visible() + self.situation_textarea.click() + self.situation_textarea.press("Control+a") self.situation_textarea.fill(text) + self.situation_textarea.blur() + self.page.wait_for_timeout(200) def blur_situation_textarea(self) -> None: self.situation_textarea.blur() @@ -377,7 +381,11 @@ def get_config_yaml(self) -> str: def set_config_yaml(self, yaml_text: str) -> None: expect(self.config_textarea).to_be_visible() + self.config_textarea.click() + self.config_textarea.press("Control+a") self.config_textarea.fill(yaml_text) + self.config_textarea.blur() + self.page.wait_for_timeout(200) def blur_config_textarea(self) -> None: self.config_textarea.blur() @@ -388,7 +396,11 @@ def save_config_button(self) -> Locator: def click_save_config_button(self) -> None: expect(self.save_config_button).to_be_visible() + expect(self.save_config_button).to_be_enabled() + self.save_config_button.scroll_into_view_if_needed() + self.page.wait_for_timeout(500) self.save_config_button.click() + self.page.wait_for_timeout(1000) def get_decider_dropdown_value(self) -> str: dropdown = ( From a7e0a7c2dd8e53d4571671d382db4ac718ca21e4 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Mon, 2 Feb 2026 21:11:12 -0500 Subject: [PATCH 22/22] Fix line too long in CSS styles --- align_app/app/ui.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/align_app/app/ui.py b/align_app/app/ui.py index 53c7429..cec1010 100644 --- a/align_app/app/ui.py +++ b/align_app/app/ui.py @@ -1551,7 +1551,8 @@ def __init__( ".runs-table-panel .v-data-table { display: flex; flex-direction: column; }" ".runs-table-panel .v-data-table > .v-table__wrapper { flex: 1; overflow-y: auto; }" ".runs-table-panel .v-data-table table { table-layout: fixed; width: 100%; }" - ".runs-table-panel .v-data-table td { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }" + ".runs-table-panel .v-data-table td " + "{ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }" ".runs-table-panel .v-data-table th { vertical-align: top; }" ".runs-table-panel .v-data-table th:first-child { padding-top: 8px; }" ".drop-zone-active { outline: 3px dashed #1976d2 !important; outline-offset: -3px; }"