Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ not infrastructure around accounts, queues, consensus, or model training.

- CSV, TSV, JSONL, NDJSON, and plain-text input
- Schema-driven labels with validated, deterministic colors
- Read-only schema preview with label colors and number-key mappings
- Offset-based span annotations with overlap protection
- Multiple-event navigation with unsaved-change prompts
- UTF-8 JSONL export containing source text, metadata, schema, and annotations
Expand Down
55 changes: 55 additions & 0 deletions src/spanselector/assets/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,60 @@ body {
text-overflow: ellipsis;
white-space: nowrap;
}
.schema-preview-trigger {
color: #b9adff !important;
}
.schema-preview-dialog {
width: min(92vw, 620px);
max-height: 86vh;
padding: 24px;
gap: 18px;
overflow-y: auto;
}
.schema-preview-name {
overflow: hidden;
color: var(--text);
font-size: 16px;
font-weight: 600;
text-overflow: ellipsis;
white-space: nowrap;
}
.schema-preview-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
width: 100%;
}
.schema-preview-item {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
padding: 12px;
border: 1px solid var(--border);
border-radius: 12px;
background: #101014;
}
.schema-color-swatch {
flex: 0 0 auto;
width: 14px;
height: 32px;
border: 1px solid rgb(255 255 255 / 16%);
border-radius: 999px;
background: var(--schema-color);
}
.schema-preview-field {
overflow: hidden;
color: #e7e7ec;
font-size: 13px;
font-weight: 600;
text-overflow: ellipsis;
white-space: nowrap;
}
.schema-preview-color {
color: var(--text-muted);
font-size: 10px;
}
.preview-panel {
min-height: 112px;
padding: 18px;
Expand Down Expand Up @@ -454,6 +508,7 @@ body {
.content-panel, .setup-panel { padding: 18px; }
.navigation-actions .q-btn { flex: 1 1 auto; }
.label-grid { grid-template-columns: 1fr 1fr; }
.schema-preview-grid { grid-template-columns: 1fr; }
.shortcut-item:nth-of-type(n+4) { display: none; }
}
@media (prefers-reduced-motion: reduce) {
Expand Down
1 change: 1 addition & 0 deletions src/spanselector/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ class UIRefs(BaseModel):
text_box: Any = None
confirm_dialog: Any = None
schema_confirm_dialog: Any = None
schema_preview_dialog: Any = None
selected_text_label: Any = None
selected_range_label: Any = None
total_assignments_label: Any = None
Expand Down
66 changes: 66 additions & 0 deletions src/spanselector/web_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,43 @@ def apply_schema_change(new_schema: str, *, discard_existing: bool) -> None:
selection_action_tray.refresh()


def close_schema_preview() -> None:
if refs.schema_preview_dialog is not None:
refs.schema_preview_dialog.close()


def open_schema_preview() -> None:
schema_preview_content.refresh()
if refs.schema_preview_dialog is not None:
refs.schema_preview_dialog.open()


@ui.refreshable
def schema_preview_content() -> None:
schema = SCHEMAS[state.selected_schema]
with ui.row().classes("items-center justify-between gap-3 w-full"):
with ui.column().classes("gap-0 min-w-0"):
ui.label(state.selected_schema).classes("schema-preview-name")
ui.label(f"{len(schema.fields)} labels in selection order").classes("supporting-text")
ui.badge(f"{len(schema.fields)} labels").classes("count-pill")

with ui.element("div").classes("schema-preview-grid"):
for index, field in enumerate(schema.fields, start=1):
color = schema.colors[field]
with ui.element("div").classes("schema-preview-item"):
ui.element("span").classes("schema-color-swatch").style(
f"--schema-color: {color}"
).props("aria-hidden=true")
with ui.column().classes("gap-0 min-w-0"):
ui.label(field).classes("schema-preview-field")
ui.label(color.upper()).classes("schema-preview-color font-mono")
ui.space()
if index <= 9:
ui.label(str(index)).classes("keycap").props(
f"aria-label='Keyboard shortcut {index}'"
)


@ui.refreshable
def annotated_preview() -> None:
ui.html(
Expand Down Expand Up @@ -382,6 +419,15 @@ def on_schema_change(e) -> None:
on_change=on_schema_change,
).classes("w-full")

with ui.row().classes("justify-end w-full"):
ui.button(
"Preview schema",
icon="visibility",
on_click=open_schema_preview,
).classes("schema-preview-trigger").props(
"flat dense no-caps aria-label='Preview selected schema'"
)

build_schema_confirm_dialog(schema_select)

assignment_section()
Expand Down Expand Up @@ -551,6 +597,25 @@ def discard_and_change() -> None:
)


def build_schema_preview_dialog() -> None:
refs.schema_preview_dialog = ui.dialog()
with refs.schema_preview_dialog:
with ui.card().classes("schema-preview-dialog"):
with ui.row().classes("items-start justify-between gap-4 w-full"):
with ui.column().classes("gap-1"):
ui.label("Schema preview").classes("dialog-title")
ui.label("Labels available for the active annotation schema.").classes(
"supporting-text"
)
ui.button(icon="close", on_click=close_schema_preview).props(
"flat round dense aria-label='Close schema preview'"
)
ui.separator()
schema_preview_content()
with ui.row().classes("justify-end w-full"):
ui.button("Close", on_click=close_schema_preview).props("outline no-caps")


def create_ui() -> None:
configure_theme()
refs.app_shell = (
Expand All @@ -568,5 +633,6 @@ def create_ui() -> None:
build_main_panel()
build_navigation()
build_confirm_dialog()
build_schema_preview_dialog()
build_shortcuts_dialog()
load_event(state.current_index, refresh=False)
18 changes: 18 additions & 0 deletions tests/e2e/test_selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,24 @@ def test_shortcut_reference_is_available_from_bottom_helper(page: Page, live_ser
expect(page.get_by_text("Keyboard shortcuts", exact=True)).to_be_hidden()


@pytest.mark.e2e
def test_selected_schema_can_be_previewed_and_closed(page: Page, live_server: str) -> None:
page.goto(live_server)

page.get_by_role("button", name="Preview selected schema").click()

dialog = page.locator(".schema-preview-dialog")
expect(dialog).to_be_visible()
expect(dialog.get_by_text("Schema preview", exact=True)).to_be_visible()
expect(dialog.get_by_text("Customer Support Ticket", exact=True)).to_be_visible()
expect(dialog.get_by_text("9 labels in selection order", exact=True)).to_be_visible()
expect(dialog.get_by_text("TicketId", exact=True)).to_be_visible()
expect(dialog.get_by_text("Resolution", exact=True)).to_be_visible()

dialog.get_by_role("button", name="Close schema preview").click()
expect(dialog).to_be_hidden()


@pytest.mark.e2e
def test_shortcuts_can_be_disabled_from_reference(page: Page, live_server: str) -> None:
page.goto(live_server)
Expand Down