From 752346025b922b669e94875b33a61c433c828aae Mon Sep 17 00:00:00 2001 From: Jim Lancaster <40281233+zoltan57@users.noreply.github.com> Date: Mon, 3 Aug 2026 15:15:58 -0500 Subject: [PATCH] UI style refresh continued --- src/transcription/services/jobs.py | 3 +- src/transcription/services/workflows.py | 1 + src/transcription/ui/components/__init__.py | 4 - src/transcription/ui/components/cards.py | 8 +- .../ui/components/data_display.py | 10 +- src/transcription/ui/components/job_detail.py | 90 ------------ .../ui/components/page_content.py | 55 ------- .../ui/components/page_header.py | 36 ----- .../ui/components/table/common.py | 24 +-- .../ui/components/table/documents.py | 2 +- src/transcription/ui/components/table/jobs.py | 4 +- .../ui/components/table/people.py | 2 +- .../ui/components/table/sources.py | 2 +- src/transcription/ui/components/typography.py | 17 --- src/transcription/ui/components/upload.py | 66 --------- src/transcription/ui/components/viewers.py | 16 +- src/transcription/ui/pages/documents_page.py | 60 ++++---- src/transcription/ui/pages/jobs_page.py | 34 ++--- src/transcription/ui/pages/people_page.py | 42 +++--- src/transcription/ui/pages/sources_page.py | 12 +- .../ui/static/components/page_content.css | 138 ------------------ .../ui/static/components/page_header.css | 60 -------- src/transcription/ui/static/theme.css | 131 +++++++++++++++++ tests/conftest.py | 14 +- tests/integration/test_pipeline_flow.py | 15 +- tests/services/test_job_service.py | 12 +- tests/ui/test_documents_page.py | 54 ++++--- tests/ui/test_jobs_page.py | 12 +- tests/ui/test_people_page.py | 36 ++--- tests/ui/test_sources_page.py | 19 +-- 30 files changed, 328 insertions(+), 651 deletions(-) delete mode 100644 src/transcription/ui/components/job_detail.py delete mode 100644 src/transcription/ui/components/page_content.py delete mode 100644 src/transcription/ui/components/page_header.py delete mode 100644 src/transcription/ui/components/typography.py delete mode 100644 src/transcription/ui/components/upload.py delete mode 100644 src/transcription/ui/static/components/page_content.css delete mode 100644 src/transcription/ui/static/components/page_header.css diff --git a/src/transcription/services/jobs.py b/src/transcription/services/jobs.py index 4d84d62..f071182 100644 --- a/src/transcription/services/jobs.py +++ b/src/transcription/services/jobs.py @@ -161,7 +161,8 @@ class JobService(ServiceBase): selectinload(Job.job_sources).selectinload(JobSource.source), # pyright: ignore[reportArgumentType] ) .where(Job.status == JobStatus.QUEUED) - .order_by(Job.date_created) # pyright: ignore[reportArgumentType] + # Break ties by id so "next" is stable when two rows share close timestamps. + .order_by(Job.date_created, Job.id) # pyright: ignore[reportArgumentType] ) return (await _session.exec(query)).first() diff --git a/src/transcription/services/workflows.py b/src/transcription/services/workflows.py index 89af504..55f061a 100644 --- a/src/transcription/services/workflows.py +++ b/src/transcription/services/workflows.py @@ -161,6 +161,7 @@ async def process_next_queued_job( ) -> bool: """Process the next queued job if one exists.""" job = await services.jobs.read_next_queued_job(session=session) + if job is None: return False diff --git a/src/transcription/ui/components/__init__.py b/src/transcription/ui/components/__init__.py index 1a0c77d..398969e 100644 --- a/src/transcription/ui/components/__init__.py +++ b/src/transcription/ui/components/__init__.py @@ -4,14 +4,10 @@ from transcription.ui.components.app_shell import NAV_ITEMS from transcription.ui.components.app_shell import render_app_shell from transcription.ui.components.app_shell import render_navigation_header from transcription.ui.components.document_panzoom import render_document_panzoom -from transcription.ui.components.page_content import render_page_content -from transcription.ui.components.page_header import render_page_header __all__ = [ "NAV_ITEMS", "render_app_shell", "render_document_panzoom", "render_navigation_header", - "render_page_content", - "render_page_header", ] diff --git a/src/transcription/ui/components/cards.py b/src/transcription/ui/components/cards.py index aee8585..b74e766 100644 --- a/src/transcription/ui/components/cards.py +++ b/src/transcription/ui/components/cards.py @@ -1,15 +1,15 @@ # transcription/ui/components/cards.py from contextlib import contextmanager + from nicegui import ui + @contextmanager def archival_card(title: str | None = None, extra_classes: str = ""): """Reusable container for Flat 2.0 Bento Grid cards.""" - with ui.card().classes( - f"w-full bg-[#F4F0E6] border border-[#6B6A65]/30 rounded-sm p-4 {extra_classes}" - ) as card: + with ui.card().classes(f"w-full ui-card-surface p-4 {extra_classes}") as card: if title: ui.label(title.upper()).classes( - "text-xs font-bold text-[#6B6A65] tracking-wider mb-3 border-b border-[#6B6A65]/20 pb-1" + "text-xs font-bold ui-text-muted tracking-wider mb-3 ui-header-divider pb-1" ) yield card \ No newline at end of file diff --git a/src/transcription/ui/components/data_display.py b/src/transcription/ui/components/data_display.py index 02633ba..7258ec7 100644 --- a/src/transcription/ui/components/data_display.py +++ b/src/transcription/ui/components/data_display.py @@ -1,12 +1,14 @@ # transcription/ui/components/data_display.py from nicegui import ui + def metadata_row(label: str, value: str): """Render a high-density, low-contrast key-value pair.""" - with ui.row().classes("justify-between w-full border-b border-[#6B6A65]/10 pb-1 text-xs"): - ui.label(label).classes("text-[#6B6A65]") - ui.label(value).classes("font-semibold text-[#333333]") + with ui.row().classes("justify-between w-full border-b ui-border-subtle pb-1 text-xs"): + ui.label(label).classes("ui-text-muted") + ui.label(value).classes("font-semibold ui-text-primary") + def archival_badge(text: str): """Standardized Aged Sepia badge.""" - return ui.badge(text, color="#E2C7A8", text_color="#333333").classes("text-[10px]") \ No newline at end of file + return ui.badge(text, color="secondary", text_color="dark").classes("text-[10px]") \ No newline at end of file diff --git a/src/transcription/ui/components/job_detail.py b/src/transcription/ui/components/job_detail.py deleted file mode 100644 index c0eace8..0000000 --- a/src/transcription/ui/components/job_detail.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Reusable job detail rendering helpers.""" - -from __future__ import annotations - -import logging - -from nicegui import ui - -from transcription.db.models import Job -from transcription.db.models import Source -from transcription.ui.components.document_panzoom import render_document_panzoom -from transcription.ui.components.transcript import render_original_transcription_card -from transcription.ui.components.transcript import render_revision_row - -logger = logging.getLogger(__name__) - - -def _status_chip_classes(status: str) -> str: - if status == "queued": - return "vibe-status--queued" - if status == "processing": - return "vibe-status--processing" - if status == "transcribed": - return "vibe-status--transcribed" - if status == "failed": - return "vibe-status--failed" - return "vibe-status--default" - - -def _metadata_row(label: str, value: str) -> None: - with ui.row().classes("w-full items-start justify-between q-gutter-x-md"): - ui.label(label).classes("text-caption vibe-text-muted text-uppercase w-28") - ui.label(value).classes("text-body2 text-right break-all") - - -def _render_source_section(source: Source) -> None: - with ui.card().classes("w-full q-pa-md vibe-card"): - ui.label("Source").classes("text-subtitle1 text-weight-medium") - ui.separator().classes("q-my-sm") - with ui.column().classes("w-full q-gutter-y-xs"): - _metadata_row("Upload name", source.upload_name) - _metadata_row("Stored filename", source.filename) - _metadata_row("File path", source.file_path) - _metadata_row("Uploaded", source.date_uploaded.isoformat()) - - ui.separator().classes("q-my-md") - render_document_panzoom(source=source) - - -def _render_revision_section(revision: Source | None) -> None: - with ui.card().classes("w-full q-pa-md vibe-card"): - ui.label("Source revision").classes("text-subtitle1 text-weight-medium") - ui.separator().classes("q-my-sm") - - if revision is None: - ui.label("No source revision exists for this source.").classes("text-body2 vibe-text-muted") - return - - render_revision_row(revision=revision, initially_expanded=True) - - -def render_job_detail(*, job: Job, source: Source | None, revision: Source | None) -> None: - """Render all sections for the job detail page.""" - logger.debug("Rendering job detail for job ID %s", job.id) - status_text = job.status.value - with ui.column().classes("w-full max-w-4xl q-gutter-md"): - with ui.card().classes("w-full q-pa-lg vibe-card"): - with ui.row().classes("w-full items-center justify-between q-gutter-md"): - with ui.column().classes("q-gutter-none"): - ui.label("Job overview").classes("text-h6 text-weight-bold") - ui.label(str(job.id)).classes("text-caption vibe-text-muted") - status_chip_classes = ( - "q-px-sm q-py-xs rounded-borders " - "vibe-status text-weight-medium text-capitalize " - f"{_status_chip_classes(status_text)}" - ) - ui.label(status_text).classes(status_chip_classes) - - ui.separator().classes("q-my-md vibe-separator") - with ui.column().classes("w-full q-gutter-y-xs"): - _metadata_row("Created", job.date_created.isoformat()) - _metadata_row("Updated", job.date_updated.isoformat()) - _metadata_row("Retries", str(job.retry_count)) - - render_original_transcription_card(job=job) - - if source is not None: - _render_source_section(source) - - _render_revision_section(revision) diff --git a/src/transcription/ui/components/page_content.py b/src/transcription/ui/components/page_content.py deleted file mode 100644 index 424b863..0000000 --- a/src/transcription/ui/components/page_content.py +++ /dev/null @@ -1,55 +0,0 @@ -"""High-level placeholder content for a transcription workspace.""" - -from __future__ import annotations - -from nicegui import ui - -from transcription.ui.resources import read_css - - -def render_page_content( - *, - source_name: str = "document-placeholder.jpg", - raw_text: str = "AI transcription output will appear here.", - revised_text: str = "Human revision text will appear here.", -) -> None: - """Render the primary editor workspace and supporting context sidebar.""" - ui.add_css(read_css("components/page_content.css")) - - with ui.element("div").classes("page-content"): - with ui.element("section").classes("page-content__editor"): - with ui.row().classes("page-content__heading"): - with ui.column().classes("gap-0"): - ui.label("Active source").classes("page-content__kicker") - ui.label("Page transcription").classes("page-content__title") - ui.badge("Page 1 of 1").classes("page-content__badge") - - with ui.element("div").classes("page-content__workspace"): - with ui.element("section").classes("source-placeholder"): - with ui.row().classes("source-placeholder__toolbar"): - ui.label(source_name) - ui.icon("image", size="1.25rem") - with ui.column().classes("source-placeholder__body"): - ui.icon("description", size="4rem") - ui.label("Source preview") - - with ui.column().classes("transcription-placeholder"): - with ui.element("section").classes("transcription-placeholder__section"): - ui.label("AI raw output").classes("transcription-placeholder__title") - ui.label(raw_text).classes("transcription-placeholder__text") - - with ui.element("section").classes("transcription-placeholder__section"): - ui.label("Human-reviewed text").classes("transcription-placeholder__title") - ui.textarea(value=revised_text).props("outlined autogrow").classes("w-full") - - with ui.element("aside").props('aria-label="Document context"').classes("page-content__sidebar"): - with ui.element("section").classes("page-content__sidebar-section"): - ui.label("People").classes("page-content__sidebar-title") - ui.label("Author · Placeholder person") - ui.label("Recipient · Placeholder person") - - with ui.element("section").classes("page-content__sidebar-section"): - ui.label("AI processing").classes("page-content__sidebar-title") - ui.badge("Completed", color="positive") - ui.label("Provider · Placeholder provider") - ui.label("Model · Placeholder model") diff --git a/src/transcription/ui/components/page_header.py b/src/transcription/ui/components/page_header.py deleted file mode 100644 index 94c6af6..0000000 --- a/src/transcription/ui/components/page_header.py +++ /dev/null @@ -1,36 +0,0 @@ -"""High-level page header for document-oriented views.""" - -from __future__ import annotations - -from collections.abc import Awaitable -from collections.abc import Callable - -from nicegui import ui - -from transcription.ui.resources import read_css - -type PageHeaderAction = Callable[[], Awaitable[None] | None] - - -def render_page_header( - *, - eyebrow: str = "Letter · Placeholder Collection", - title: str = "Untitled archival document", - metadata: tuple[str, ...] = ("Date unknown", "Location unknown"), - on_details: PageHeaderAction | None = None, - on_review: PageHeaderAction | None = None, -) -> None: - """Render document identity, metadata, and page-level actions.""" - ui.add_css(read_css("components/page_header.css")) - - with ui.element("section").classes("page-header"): - with ui.column().classes("page-header__identity"): - ui.label(eyebrow).classes("page-header__eyebrow") - ui.label(title).classes("page-header__title") - with ui.row().classes("page-header__metadata"): - for value in metadata: - ui.label(value) - - with ui.row().classes("page-header__actions"): - ui.button("Document details", icon="info", on_click=on_details).props("outline no-caps") - ui.button("Mark reviewed", icon="task_alt", on_click=on_review).props("unelevated no-caps") diff --git a/src/transcription/ui/components/table/common.py b/src/transcription/ui/components/table/common.py index 9c93a5d..f225cde 100644 --- a/src/transcription/ui/components/table/common.py +++ b/src/transcription/ui/components/table/common.py @@ -57,8 +57,7 @@ def build_table( pagination["sortBy"] = default_sort_by pagination["descending"] = default_descending - # Quasar props to enforce flat, archival styling - # Styling table headers with Library Green (#2D5A4C) and rows with subtle borders + # Quasar props enforce behavior; visual styling is centralized in theme.css. table = ( ui.table( rows=rows, @@ -66,28 +65,13 @@ def build_table( row_key="id", pagination=pagination, ) - .classes( - f"w-full bg-[#F4F0E6] border border-[#6B6A65]/30 rounded-sm {classes}" - ) + .classes(f"w-full ui-table {classes}") .props( 'flat square binary-state-sort table-style="table-layout: fixed; width: 100%;" ' - 'header-cell-class="bg-[#2D5A4C] text-white font-bold text-xs uppercase tracking-wider" ' - 'table-class="text-xs text-[#333333]"' + 'header-cell-class="ui-table-header text-xs uppercase tracking-wider" ' + 'table-class="ui-table-body text-xs"' ) ) - - # Custom CSS rules for row hover effects matching Archival Cream - ui.add_head_html(""" - - """) logger.info("Table built with %d rows and %d columns", len(rows), len(columns)) if on_row_click_id is not None: diff --git a/src/transcription/ui/components/table/documents.py b/src/transcription/ui/components/table/documents.py index 0970326..f492ecc 100644 --- a/src/transcription/ui/components/table/documents.py +++ b/src/transcription/ui/components/table/documents.py @@ -41,7 +41,7 @@ def render_documents_table(rows: Sequence[DocumentTableRow]) -> None: """Render documents table and open detail page when clicking a row.""" if not rows: with archival_card(extra_classes="p-8 text-center"): - ui.label("No documents in repository yet.").classes("text-xs text-[#6B6A65]") + ui.label("No documents in repository yet.").classes("text-xs ui-text-muted") return build_table( diff --git a/src/transcription/ui/components/table/jobs.py b/src/transcription/ui/components/table/jobs.py index 9ee8506..d5d0ff2 100644 --- a/src/transcription/ui/components/table/jobs.py +++ b/src/transcription/ui/components/table/jobs.py @@ -57,14 +57,14 @@ def render_jobs_table(rows: Sequence[JobTableRow]) -> None: """Render jobs table and open a detail page when clicking a row.""" if not rows: with archival_card(extra_classes="p-8 text-center"): - ui.label("No active or historical processing jobs found.").classes("text-xs text-[#6B6A65]") + ui.label("No active or historical processing jobs found.").classes("text-xs ui-text-muted") return build_table( rows=_serialize_rows(rows), columns=[ {"name": "id", "label": "Job ID", "field": "id", "sortable": True, "classes": "font-mono"}, - {"name": "status", "label": "Status", "field": "status", "sortable": True, "classes": "font-semibold text-[#2D5A4C]"}, + {"name": "status", "label": "Status", "field": "status", "sortable": True, "classes": "font-semibold ui-link-primary"}, {"name": "filename", "label": "Source Filename", "field": "filename", "sortable": True, "classes": "font-mono"}, {"name": "retry_count", "label": "Retries", "field": "retry_count", "sortable": True}, {"name": "date_created", "label": "Created", "field": "date_created", "sortable": True}, diff --git a/src/transcription/ui/components/table/people.py b/src/transcription/ui/components/table/people.py index 425da97..c2b094b 100644 --- a/src/transcription/ui/components/table/people.py +++ b/src/transcription/ui/components/table/people.py @@ -41,7 +41,7 @@ def render_people_table(rows: Sequence[PersonTableRow]) -> None: """Render people table and open detail page when clicking a row.""" if not rows: with archival_card(extra_classes="p-8 text-center"): - ui.label("No person records found in repository.").classes("text-xs text-[#6B6A65]") + ui.label("No person records found in repository.").classes("text-xs ui-text-muted") return build_table( diff --git a/src/transcription/ui/components/table/sources.py b/src/transcription/ui/components/table/sources.py index a568af9..399f68a 100644 --- a/src/transcription/ui/components/table/sources.py +++ b/src/transcription/ui/components/table/sources.py @@ -41,7 +41,7 @@ def render_sources_table(rows: Sequence[SourceTableRow]) -> None: """Render sources table and open detail page when clicking a row.""" if not rows: with archival_card(extra_classes="p-8 text-center"): - ui.label("No source file records found.").classes("text-xs text-[#6B6A65]") + ui.label("No source file records found.").classes("text-xs ui-text-muted") return build_table( diff --git a/src/transcription/ui/components/typography.py b/src/transcription/ui/components/typography.py deleted file mode 100644 index f05136b..0000000 --- a/src/transcription/ui/components/typography.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Typography helper components for Archival and Academic layouts.""" - -from nicegui import ui - -# System-wide typography styles matching the UI Design Specification -STYLE_SERIF_HEADER = "font-family: 'Georgia', 'Times New Roman', serif;" -STYLE_SANS_BODY = "font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;" - - -def page_header(title: str, subtitle: str | None = None) -> None: - """Render a standardized page title header using the Archival Serif font.""" - with ui.column().classes("gap-0 pb-2 border-b border-[#6B6A65]/30 w-full"): - ui.label(title).style( - f"{STYLE_SERIF_HEADER} font-size: 1.75rem; font-weight: 700; color: #333333;" - ) - if subtitle: - ui.label(subtitle).classes("text-xs text-[#6B6A65]") \ No newline at end of file diff --git a/src/transcription/ui/components/upload.py b/src/transcription/ui/components/upload.py deleted file mode 100644 index e8a0eb3..0000000 --- a/src/transcription/ui/components/upload.py +++ /dev/null @@ -1,66 +0,0 @@ -"""Reusable upload widget for document submission.""" - -from __future__ import annotations - -from collections.abc import Awaitable -from collections.abc import Callable - -from nicegui import ui -from nicegui.binding import bindable_dataclass -from nicegui.events import UploadEventArguments - -from transcription.errors import AppError -from transcription.services.documents import UploadJobResult -from transcription.ui.components.error_presenter import show_error -from transcription.ui.components.error_presenter import summarize_error -from transcription.worker import WorkerNotifier - -type UploadSubmitter = Callable[[str, bytes], Awaitable[UploadJobResult]] - - -@bindable_dataclass -class UploadWidgetState: - """Simple state container for upload feedback.""" - - loading: bool = False - message: str = "" - - -def render_upload_widget(*, submitter: UploadSubmitter, notifier: WorkerNotifier | None = None) -> None: - """Render upload controls and common status/error handling.""" - state = UploadWidgetState() - status_label = ui.label("Upload a document to start transcription.") - status_label.bind_text(state, "message") - - async def on_upload(event: UploadEventArguments) -> None: - if state.loading: - ui.notify("Upload already in progress. Please wait.", type="warning") - return - - state.loading = True - status_label.text = "Uploading..." - try: - payload = await event.file.read() - result = await submitter(event.file.name, payload) - job_id = result.job_id - state.message = f"Created job {job_id}" if job_id is not None else "Upload complete" - status_label.text = state.message - if notifier is not None: - notifier.notify() - ui.notify(state.message, type="positive") - except AppError as exc: - state.message = summarize_error(exc, operation="upload.submit") - status_label.text = f"Upload failed: {state.message}" - show_error(exc, title="Upload failed", operation="upload.submit") - except Exception as exc: # noqa: BLE001 - state.message = summarize_error(exc, operation="upload.submit") - status_label.text = f"Upload failed: {state.message}" - show_error(exc, title="Upload failed", operation="upload.submit") - finally: - state.loading = False - - ui.upload( - on_upload=on_upload, - auto_upload=True, - label="Select document file", - ).props('accept=".jpg,.jpeg,.png,.tif,.tiff,.pdf"') diff --git a/src/transcription/ui/components/viewers.py b/src/transcription/ui/components/viewers.py index d27192a..a8c7321 100644 --- a/src/transcription/ui/components/viewers.py +++ b/src/transcription/ui/components/viewers.py @@ -10,19 +10,17 @@ def dark_room_viewer( container_height: str = "500px", ) -> None: """Isolated high-contrast container for image inspection with pan and zoom capabilities.""" - with ui.card().classes( - "bg-[#2B2D2C] border border-[#333333] rounded-sm p-3 flex flex-col justify-between w-full" - ): + with ui.card().classes("ui-bg-viewer ui-border-viewer rounded-sm p-3 flex flex-col justify-between w-full"): # Viewer Header Bar - with ui.row().classes("w-full justify-between items-center mb-2 text-[#FAF9F6] text-xs"): + with ui.row().classes("w-full justify-between items-center mb-2 ui-text-inverse text-xs"): ui.label("SOURCE MEDIA VIEWER").classes("font-mono font-bold tracking-wider") - ui.label(count_label).classes("text-[#E2C7A8]") + ui.label(count_label).classes("ui-text-accent") # Interactive Pan/Zoom Canvas Area if image_path: # Container with fixed height and hidden overflow for contained panning/zooming with ui.element("div").classes( - "relative w-full overflow-hidden border border-[#333333] bg-black/50 rounded-sm flex items-center justify-center cursor-grab active:cursor-grabbing" + "relative w-full overflow-hidden border ui-border-viewer ui-bg-viewer-overlay rounded-sm flex items-center justify-center cursor-grab active:cursor-grabbing" ).style(f"height: {container_height};") as viewport: # Image element targeted by client-side pan/zoom JS @@ -89,7 +87,7 @@ def dark_room_viewer( ui.run_javascript(js_pan_zoom) # Control Toolbar - with ui.row().classes("w-full justify-center items-center gap-2 mt-2 pt-2 border-t border-[#333333]"): + with ui.row().classes("w-full justify-center items-center gap-2 mt-2 pt-2 border-t ui-border-viewer"): ui.button( icon="zoom_in", on_click=lambda: ui.run_javascript(f"window.zoomIn_{img.id}()"), @@ -108,6 +106,6 @@ def dark_room_viewer( else: # Fallback state when no image source is linked with ui.column().classes( - "w-full flex-grow items-center justify-center border border-[#333333] bg-black/40 rounded-sm p-8" + "w-full flex-grow items-center justify-center border ui-border-viewer ui-bg-viewer-overlay-soft rounded-sm p-8" ).style(f"min-height: {container_height};"): - ui.label("No source media available for inspection.").classes("text-[#6B6A65] text-xs italic") \ No newline at end of file + ui.label("No source media available for inspection.").classes("ui-text-muted text-xs italic") \ No newline at end of file diff --git a/src/transcription/ui/pages/documents_page.py b/src/transcription/ui/pages/documents_page.py index 5baf9ce..60adb87 100644 --- a/src/transcription/ui/pages/documents_page.py +++ b/src/transcription/ui/pages/documents_page.py @@ -23,9 +23,9 @@ from transcription.ui.components.data_display import metadata_row from transcription.ui.components.error_presenter import show_error from transcription.ui.components.table.documents import DocumentTableRow from transcription.ui.components.table.documents import render_documents_table -from transcription.ui.components.typography import page_header from transcription.ui.components.viewers import dark_room_viewer from transcription.ui.theme import apply_archival_theme +from transcription.ui.theme import page_header from ...db.session import SessionFactoryDep @@ -72,7 +72,7 @@ def register_page() -> None: .props("outlined bg-white") .classes("w-full") ) - ui.link("Create new person", "/people/new").classes("text-xs text-[#2D5A4C] font-medium") + ui.link("Create new person", "/people/new").classes("text-xs ui-link-primary font-medium") return_to = request.query_params.get("return_to") @@ -137,7 +137,7 @@ def register_page() -> None: ui.navigate.to(f"/documents/{created.id}") with ui.row().classes("w-full items-center gap-2 mt-2"): - ui.button("Save document", on_click=submit_create, icon="save").classes("bg-[#2D5A4C] text-white") + ui.button("Save document", on_click=submit_create, icon="save").classes("ui-btn-primary") ui.button("Cancel", on_click=lambda: ui.navigate.to("/documents"), icon="arrow_back").props("flat") @ui.page("/documents") @@ -147,13 +147,13 @@ def register_page() -> None: render_navigation_header(current_path="/documents") with ui.column().classes("w-full max-w-7xl mx-auto p-4 gap-4"): - with ui.row().classes("w-full items-center justify-between pb-2 border-b border-[#6B6A65]/20"): + with ui.row().classes("w-full items-center justify-between pb-2 ui-header-divider"): page_header("Archival Documents") ui.button( "Create new document", on_click=lambda: ui.navigate.to("/documents/new"), icon="note_add", - ).classes("bg-[#2D5A4C] text-white") + ).classes("ui-btn-primary") try: documents = sorted( @@ -211,7 +211,7 @@ def register_page() -> None: # Main Bento Grid Wrapper with ui.column().classes("w-full max-w-[1800px] mx-auto p-4 gap-4"): # Header Bar - with ui.row().classes("w-full justify-between items-center pb-2 border-b border-[#6B6A65]/30"): + with ui.row().classes("w-full justify-between items-center pb-2 ui-header-divider"): page_header(document.name, subtitle=f"Type: {document.document_type or 'Unspecified'} | ID: {document.id}") with ui.row().classes("items-center gap-2"): @@ -219,7 +219,7 @@ def register_page() -> None: "Edit Document", on_click=lambda: ui.navigate.to(f"/documents/{document.id}/edit"), icon="edit", - ).classes("bg-[#2D5A4C] text-white text-xs") + ).classes("ui-btn-primary text-xs") ui.button( "Delete", on_click=lambda: ui.navigate.to(f"/documents/{document.id}/delete"), @@ -237,12 +237,12 @@ def register_page() -> None: "View All Sources", on_click=lambda: ui.navigate.to(f"/sources?document_id={document.id}"), icon="description", - ).props("flat dense text-xs").classes("text-[#2D5A4C]") + ).props("flat dense text-xs").classes("ui-link-primary") ui.button( "+ Add Source", on_click=lambda: ui.navigate.to(f"/jobs/new?document_id={document.id}"), icon="add", - ).classes("bg-[#2D5A4C] text-white text-xs") + ).classes("ui-btn-primary text-xs") # ZONE 2: Metadata & Archival Attributes (Cols 6-8) with ui.column().classes("col-span-12 lg:col-span-4 gap-4"): @@ -254,45 +254,45 @@ def register_page() -> None: metadata_row("Archive Identifier:", document.archive_identifier or "Not set") with ui.column().classes("w-full mt-2"): - ui.label("Archival Notes:").classes("text-[#6B6A65] text-xs mb-1") + ui.label("Archival Notes:").classes("ui-text-muted text-xs mb-1") ui.label(document.notes or "No notes added.").classes( - "p-2 bg-[#FAF9F6] border border-[#6B6A65]/20 rounded-sm italic text-xs text-[#333333]" + "p-2 ui-note-box text-xs" ) with archival_card(title="System Logistics"): - ui.label(f"Created: {document.created_at.isoformat()}").classes("text-[11px] text-[#6B6A65]") - ui.label(f"Updated: {document.updated_at.isoformat()}").classes("text-[11px] text-[#6B6A65]") + ui.label(f"Created: {document.created_at.isoformat()}").classes("text-[11px] ui-text-muted") + ui.label(f"Updated: {document.updated_at.isoformat()}").classes("text-[11px] ui-text-muted") # ZONE 3: Related Entities & Pipeline Jobs (Cols 9-12) with ui.column().classes("col-span-12 lg:col-span-3 gap-4"): with archival_card(title="Related People"): if not document.document_people: - ui.label("No linked people yet.").classes("text-xs text-[#6B6A65] italic") + ui.label("No linked people yet.").classes("text-xs ui-text-muted italic") else: with ui.column().classes("w-full gap-2"): for link in document.document_people: person_label = link.person.full_name if link.person is not None else "Unknown person" with ui.row().classes( - "w-full justify-between items-center bg-[#FAF9F6] p-2 border border-[#6B6A65]/20 rounded-sm" + "w-full justify-between items-center ui-row-surface p-2" ): - ui.label(person_label).classes("text-xs font-semibold text-[#333333]") + ui.label(person_label).classes("text-xs font-semibold ui-text-primary") archival_badge(link.role.value) with archival_card(title="Pipeline Jobs"): with ui.row().classes("w-full justify-between items-center mb-2"): - ui.label(f"{len(document.jobs)} Active Jobs").classes("text-xs text-[#2D5A4C] font-bold") + ui.label(f"{len(document.jobs)} Active Jobs").classes("text-xs ui-link-primary font-bold") with ui.row().classes("w-full gap-2 mt-2"): ui.button( "View Jobs", on_click=lambda: ui.navigate.to(f"/documents/{document.id}/jobs"), icon="work_history", - ).props("flat dense text-xs").classes("text-[#2D5A4C]") + ).props("flat dense text-xs").classes("ui-link-primary") ui.button( "+ Add Job", on_click=lambda: ui.navigate.to(f"/jobs/new?document_id={document.id}"), icon="add", - ).classes("bg-[#2D5A4C] text-white text-xs") + ).classes("ui-btn-primary text-xs") @ui.page("/documents/{document_id}/jobs") async def document_jobs_page(document_id: str, session_factory: SessionFactoryDep) -> None: @@ -316,7 +316,7 @@ def register_page() -> None: return with ui.column().classes("w-full max-w-4xl mx-auto p-4 gap-4"): - with ui.row().classes("w-full justify-between items-center pb-2 border-b border-[#6B6A65]/30"): + with ui.row().classes("w-full justify-between items-center pb-2 ui-header-divider"): page_header(f"Jobs for {document.name}") with ui.row().classes("gap-2"): ui.button( @@ -328,11 +328,11 @@ def register_page() -> None: "Create Job", on_click=lambda: ui.navigate.to(f"/jobs/new?document_id={document.id}"), icon="add", - ).classes("bg-[#2D5A4C] text-white") + ).classes("ui-btn-primary") if not document.jobs: with archival_card(extra_classes="p-6 text-center"): - ui.label("No transcription processing jobs created yet.").classes("text-xs text-[#6B6A65]") + ui.label("No transcription processing jobs created yet.").classes("text-xs ui-text-muted") return for job in sorted(document.jobs, key=lambda item: item.date_created, reverse=True): @@ -340,12 +340,12 @@ def register_page() -> None: with ui.row().classes("w-full items-center justify-between"): with ui.row().classes("items-center gap-2"): archival_badge(job.status.value) - ui.label(f"Job ID: {job.id}").classes("text-xs font-mono text-[#333333]") + ui.label(f"Job ID: {job.id}").classes("text-xs font-mono ui-text-primary") ui.button( "Open Job", on_click=lambda _=None, job_id=job.id: ui.navigate.to(f"/jobs/{job_id}"), icon="open_in_new", - ).props("flat dense").classes("text-xs text-[#2D5A4C]") + ).props("flat dense").classes("text-xs ui-link-primary") @ui.page("/documents/{document_id}/sources") async def document_sources_page(document_id: str, session_factory: SessionFactoryDep) -> RedirectResponse: @@ -434,7 +434,7 @@ def register_page() -> None: .props("outlined bg-white") .classes("w-full") ) - ui.link("Create new person", "/people/new").classes("text-xs text-[#2D5A4C] font-medium") + ui.link("Create new person", "/people/new").classes("text-xs ui-link-primary font-medium") async def submit_edit() -> None: candidate_name = (name_input.value or "").strip() @@ -505,7 +505,7 @@ def register_page() -> None: ui.navigate.to(f"/documents/{document.id}") with ui.row().classes("w-full items-center gap-2 mt-2"): - ui.button("Save changes", on_click=submit_edit, icon="save").classes("bg-[#2D5A4C] text-white") + ui.button("Save changes", on_click=submit_edit, icon="save").classes("ui-btn-primary") ui.button("Cancel", on_click=lambda: ui.navigate.to(f"/documents/{document.id}"), icon="arrow_back").props( "flat" ) @@ -535,7 +535,7 @@ def register_page() -> None: page_header("Delete Document") with archival_card(extra_classes="gap-2"): - ui.label(f"Document: {document.name}").classes("text-sm font-semibold text-[#333333]") + ui.label(f"Document: {document.name}").classes("text-sm font-semibold ui-text-primary") has_sources = bool(document.sources) has_jobs = bool(document.jobs) @@ -547,15 +547,15 @@ def register_page() -> None: categories.append("Sources") if has_jobs: categories.append("Jobs") - ui.label(f"Dependencies present: {', '.join(categories)}").classes("text-xs text-[#6B6A65]") - ui.label("Remove related records first, then retry deletion.").classes("text-xs text-[#6B6A65] italic") + ui.label(f"Dependencies present: {', '.join(categories)}").classes("text-xs ui-text-muted") + ui.label("Remove related records first, then retry deletion.").classes("text-xs ui-text-muted italic") with ui.row().classes("w-full items-center gap-2 mt-4"): ui.button( "Back to Document", on_click=lambda: ui.navigate.to(f"/documents/{document.id}"), icon="arrow_back", - ).classes("bg-[#2D5A4C] text-white text-xs") + ).classes("ui-btn-primary text-xs") ui.button("Go to Jobs", on_click=lambda: ui.navigate.to("/jobs"), icon="work_history").props( "flat text-xs" ) diff --git a/src/transcription/ui/pages/jobs_page.py b/src/transcription/ui/pages/jobs_page.py index 48ce2dd..c3edd84 100644 --- a/src/transcription/ui/pages/jobs_page.py +++ b/src/transcription/ui/pages/jobs_page.py @@ -20,8 +20,8 @@ from transcription.ui.components.data_display import archival_badge from transcription.ui.components.data_display import metadata_row from transcription.ui.components.error_presenter import show_error from transcription.ui.components.table.jobs import render_jobs_table -from transcription.ui.components.typography import page_header from transcription.ui.theme import apply_archival_theme +from transcription.ui.theme import page_header from transcription.worker import resolve_worker_notifier from ...db.session import SessionFactoryDep @@ -38,11 +38,11 @@ def register_page() -> None: # noqa: PLR0915 render_navigation_header(current_path="/jobs") with ui.column().classes("w-full max-w-7xl mx-auto p-4 gap-4"): - with ui.row().classes("w-full items-center justify-between pb-2 border-b border-[#6B6A65]/20"): + with ui.row().classes("w-full items-center justify-between pb-2 ui-header-divider"): page_header("Transcription Pipeline Jobs") with ui.row().classes("items-center gap-2"): ui.button("Create job", on_click=lambda: ui.navigate.to("/jobs/new"), icon="add").classes( - "bg-[#2D5A4C] text-white" + "ui-btn-primary" ) ui.button("Refresh", on_click=lambda: render_table.refresh(), icon="refresh").props("flat") @@ -83,7 +83,7 @@ def register_page() -> None: # noqa: PLR0915 "Create document", on_click=lambda: ui.navigate.to("/documents/new?return_to=jobs_new"), icon="note_add", - ).classes("bg-[#2D5A4C] text-white") + ).classes("ui-btn-primary") ui.button("Back to Jobs", on_click=lambda: ui.navigate.to("/jobs"), icon="arrow_back").props("flat") return @@ -106,12 +106,12 @@ def register_page() -> None: # noqa: PLR0915 with archival_card(title="Source Files"): ui.label( "Files are processed alphabetically by original filename. Use leading numbers such as 001, 002, 003 to control order." - ).classes("text-xs text-[#6B6A65] mb-2") + ).classes("text-xs ui-text-muted mb-2") @ui.refreshable def render_upload_list() -> None: if not uploaded_files: - ui.label("No files uploaded yet.").classes("text-xs text-[#6B6A65] italic") + ui.label("No files uploaded yet.").classes("text-xs ui-text-muted italic") return def remove_file(index: int) -> None: @@ -133,9 +133,9 @@ def register_page() -> None: # noqa: PLR0915 with ui.column().classes("gap-1 w-full mt-2"): for index, (filename, _) in ordered_uploads: with ui.row().classes( - "w-full items-center justify-between bg-[#FAF9F6] p-2 border border-[#6B6A65]/20 rounded-sm" + "w-full items-center justify-between ui-row-surface p-2" ): - ui.label(Path(filename).name).classes("text-xs font-mono text-[#333333]") + ui.label(Path(filename).name).classes("text-xs font-mono ui-text-primary") ui.button(icon="delete", on_click=lambda idx=index: remove_file(idx)).props( "flat round dense color=negative text-xs" ) @@ -194,7 +194,7 @@ def register_page() -> None: # noqa: PLR0915 with ui.row().classes("w-full items-center gap-2 mt-2"): ui.button("Submit for transcription", on_click=submit_create, icon="play_arrow").classes( - "bg-[#2D5A4C] text-white" + "ui-btn-primary" ) ui.button("Back to Jobs", on_click=lambda: ui.navigate.to("/jobs"), icon="arrow_back").props("flat") @@ -217,7 +217,7 @@ def register_page() -> None: # noqa: PLR0915 return with ui.column().classes("w-full max-w-4xl mx-auto p-4 gap-4"): - with ui.row().classes("w-full justify-between items-center pb-2 border-b border-[#6B6A65]/30"): + with ui.row().classes("w-full justify-between items-center pb-2 ui-header-divider"): page_header(f"Job Record: {job.id}") with ui.row().classes("items-center gap-2"): archival_badge(job.status.value.upper()) @@ -236,18 +236,18 @@ def register_page() -> None: # noqa: PLR0915 metadata_row("Last Updated:", job.date_updated.isoformat()) with archival_card(title="Document Links"): - ui.label("Navigate to related archival records:").classes("text-xs text-[#6B6A65] mb-3") + ui.label("Navigate to related archival records:").classes("text-xs ui-text-muted mb-3") with ui.column().classes("w-full gap-2"): ui.button( "View Linked Document", on_click=lambda: ui.navigate.to(f"/documents/{job.document_id}"), icon="description", - ).classes("bg-[#2D5A4C] text-white text-xs w-full") + ).classes("ui-btn-primary text-xs w-full") ui.button( "View Linked Sources", on_click=lambda: ui.navigate.to(f"/sources?job_id={job.id}"), icon="description", - ).props("flat text-xs").classes("text-[#2D5A4C] w-full") + ).props("flat text-xs").classes("ui-link-primary w-full") @ui.page("/jobs/{job_id}/delete") async def job_delete_page(job_id: str, session_factory: SessionFactoryDep) -> None: @@ -271,19 +271,19 @@ def register_page() -> None: # noqa: PLR0915 page_header("Delete Processing Job") with archival_card(extra_classes="gap-2"): - ui.label(f"Job ID: {job.id}").classes("text-sm font-semibold font-mono text-[#333333]") + ui.label(f"Job ID: {job.id}").classes("text-sm font-semibold font-mono ui-text-primary") if job.status == JobStatus.PROCESSING: ui.label("Delete is blocked while the job is processing.").classes( "text-xs text-red-800 font-bold mt-2" ) - ui.label("Wait for processing to complete, then retry delete.").classes("text-xs text-[#6B6A65] italic") + ui.label("Wait for processing to complete, then retry delete.").classes("text-xs ui-text-muted italic") with ui.row().classes("w-full items-center gap-2 mt-4"): ui.button( "Back to Job", on_click=lambda: ui.navigate.to(f"/jobs/{job.id}"), icon="arrow_back", - ).classes("bg-[#2D5A4C] text-white text-xs") + ).classes("ui-btn-primary text-xs") ui.button("Back to Jobs", on_click=lambda: ui.navigate.to("/jobs"), icon="work_history").props( "flat text-xs" ) @@ -291,7 +291,7 @@ def register_page() -> None: # noqa: PLR0915 ui.label("This action permanently deletes the job.").classes("text-xs text-red-800 font-medium") if job.job_sources: - ui.label("Related JobSource links will be removed as part of delete.").classes("text-xs text-[#6B6A65]") + ui.label("Related JobSource links will be removed as part of delete.").classes("text-xs ui-text-muted") async def submit_delete() -> None: try: diff --git a/src/transcription/ui/pages/people_page.py b/src/transcription/ui/pages/people_page.py index 01a11ae..84c0dfe 100644 --- a/src/transcription/ui/pages/people_page.py +++ b/src/transcription/ui/pages/people_page.py @@ -23,9 +23,9 @@ from transcription.ui.components.cards import archival_card from transcription.ui.components.data_display import metadata_row from transcription.ui.components.error_presenter import show_error from transcription.ui.components.table.people import PersonTableRow, render_people_table -from transcription.ui.components.typography import page_header from transcription.ui.components.viewers import dark_room_viewer from transcription.ui.theme import apply_archival_theme +from transcription.ui.theme import page_header from ...db.session import SessionFactoryDep @@ -70,7 +70,7 @@ def _bind_portrait_file_picker(portrait_path_input: ui.input, *, settings: Setti auto_upload=True, label="Choose portrait file", ).props('accept=".jpg,.jpeg,.png,.gif,.webp,.bmp,.tif,.tiff"').classes("w-full") - ui.label("Portraits are stored under uploads/portraits/person.").classes("text-xs text-[#6B6A65]") + ui.label("Portraits are stored under uploads/portraits/person.").classes("text-xs ui-text-muted") def _resolve_portrait_src(path: str | None) -> str | None: @@ -106,13 +106,13 @@ def register_page() -> None: # noqa: PLR0915 render_navigation_header(current_path="/people") with ui.column().classes("w-full max-w-7xl mx-auto p-4 gap-4"): - with ui.row().classes("w-full items-center justify-between pb-2 border-b border-[#6B6A65]/20"): + with ui.row().classes("w-full items-center justify-between pb-2 ui-header-divider"): page_header("Archival Entities: People") ui.button( "Create new person", on_click=lambda: ui.navigate.to("/people/new"), icon="person_add", - ).classes("bg-[#2D5A4C] text-white") + ).classes("ui-btn-primary") try: people = sorted( @@ -203,7 +203,7 @@ def register_page() -> None: # noqa: PLR0915 ui.navigate.to(f"/people/{created.id}") with ui.row().classes("w-full items-center gap-2 mt-2"): - ui.button("Save person", on_click=submit_create, icon="save").classes("bg-[#2D5A4C] text-white") + ui.button("Save person", on_click=submit_create, icon="save").classes("ui-btn-primary") ui.button("Cancel", on_click=lambda: ui.navigate.to("/people"), icon="arrow_back").props("flat") @ui.page("/people/{person_id}") @@ -230,7 +230,7 @@ def register_page() -> None: # noqa: PLR0915 portrait_src = _resolve_portrait_src(person.portrait_path) with ui.column().classes("w-full max-w-[1800px] mx-auto p-4 gap-4"): - with ui.row().classes("w-full justify-between items-center pb-2 border-b border-[#6B6A65]/30"): + with ui.row().classes("w-full justify-between items-center pb-2 ui-header-divider"): page_header(person.full_name, subtitle=f"Person ID: {person.id}") with ui.row().classes("items-center gap-2"): @@ -238,7 +238,7 @@ def register_page() -> None: # noqa: PLR0915 "Edit Person", on_click=lambda: ui.navigate.to(f"/people/{person.id}/edit"), icon="edit", - ).classes("bg-[#2D5A4C] text-white text-xs") + ).classes("ui-btn-primary text-xs") ui.button( "Delete", on_click=lambda: ui.navigate.to(f"/people/{person.id}/delete"), @@ -262,19 +262,19 @@ def register_page() -> None: # noqa: PLR0915 metadata_row("Death Place:", person.death_place or "Not set") with archival_card(title="System Logistics"): - ui.label(f"Created: {person.created_at.isoformat()}").classes("text-[11px] text-[#6B6A65]") - ui.label(f"Updated: {person.updated_at.isoformat()}").classes("text-[11px] text-[#6B6A65]") + ui.label(f"Created: {person.created_at.isoformat()}").classes("text-[11px] ui-text-muted") + ui.label(f"Updated: {person.updated_at.isoformat()}").classes("text-[11px] ui-text-muted") with ui.column().classes("col-span-12 lg:col-span-4 gap-4"): with archival_card(title="Biography"): ui.label(person.biography or "No biography recorded.").classes( - "p-2 bg-[#FAF9F6] border border-[#6B6A65]/20 rounded-sm text-xs text-[#333333] italic w-full" + "p-2 ui-note-box text-xs w-full" ) with archival_card(title="Linked Documents"): if not person.document_people: - ui.label("No linked documents yet.").classes("text-xs text-[#6B6A65] italic") - ui.label("Link this person from a Document workflow.").classes("text-xs text-[#6B6A65]") + ui.label("No linked documents yet.").classes("text-xs ui-text-muted italic") + ui.label("Link this person from a Document workflow.").classes("text-xs ui-text-muted") else: with ui.column().classes("w-full gap-2"): for link in person.document_people: @@ -282,18 +282,18 @@ def register_page() -> None: # noqa: PLR0915 if document is None: continue with ui.row().classes( - "w-full justify-between items-center bg-[#FAF9F6] p-2 border border-[#6B6A65]/20 rounded-sm" + "w-full justify-between items-center ui-row-surface p-2" ): with ui.column().classes("gap-0"): - ui.label(document.name).classes("text-xs font-semibold text-[#333333]") - ui.label(f"Role: {link.role.value}").classes("text-[10px] text-[#6B6A65]") + ui.label(document.name).classes("text-xs font-semibold ui-text-primary") + ui.label(f"Role: {link.role.value}").classes("text-[10px] ui-text-muted") ui.button( "Open", on_click=lambda _=None, doc_id=document.id: ui.navigate.to( f"/documents/{doc_id}" ), icon="open_in_new", - ).props("flat dense text-xs").classes("text-[#2D5A4C]") + ).props("flat dense text-xs").classes("ui-link-primary") @ui.page("/people/{person_id}/edit") async def person_edit_page(person_id: str, request: Request, session_factory: SessionFactoryDep) -> None: @@ -396,7 +396,7 @@ def register_page() -> None: # noqa: PLR0915 ui.navigate.to(f"/people/{person.id}") with ui.row().classes("w-full items-center gap-2 mt-2"): - ui.button("Save changes", on_click=submit_edit, icon="save").classes("bg-[#2D5A4C] text-white") + ui.button("Save changes", on_click=submit_edit, icon="save").classes("ui-btn-primary") ui.button("Cancel", on_click=lambda: ui.navigate.to(f"/people/{person.id}"), icon="arrow_back").props("flat") @ui.page("/people/{person_id}/delete") @@ -424,18 +424,18 @@ def register_page() -> None: # noqa: PLR0915 page_header("Delete Person Record") with archival_card(extra_classes="gap-2"): - ui.label(f"Person: {person.full_name}").classes("text-sm font-semibold text-[#333333]") + ui.label(f"Person: {person.full_name}").classes("text-sm font-semibold ui-text-primary") if person.document_people: ui.label("Delete is blocked because linked documents exist.").classes("text-xs text-red-800 font-bold mt-2") - ui.label(f"Linked documents: {len(person.document_people)}").classes("text-xs text-[#6B6A65]") - ui.label("Remove document links first, then retry deletion.").classes("text-xs text-[#6B6A65] italic") + ui.label(f"Linked documents: {len(person.document_people)}").classes("text-xs ui-text-muted") + ui.label("Remove document links first, then retry deletion.").classes("text-xs ui-text-muted italic") with ui.row().classes("w-full items-center gap-2 mt-4"): ui.button( "Back to Person", on_click=lambda: ui.navigate.to(f"/people/{person.id}"), icon="arrow_back", - ).classes("bg-[#2D5A4C] text-white text-xs") + ).classes("ui-btn-primary text-xs") ui.button("Go to Documents", on_click=lambda: ui.navigate.to("/documents"), icon="description").props( "flat text-xs" ) diff --git a/src/transcription/ui/pages/sources_page.py b/src/transcription/ui/pages/sources_page.py index b3dec9c..719e05e 100644 --- a/src/transcription/ui/pages/sources_page.py +++ b/src/transcription/ui/pages/sources_page.py @@ -22,8 +22,8 @@ from transcription.ui.components.data_display import metadata_row from transcription.ui.components.document_panzoom import render_document_panzoom from transcription.ui.components.error_presenter import show_error from transcription.ui.components.table.sources import SourceTableRow, render_sources_table -from transcription.ui.components.typography import page_header from transcription.ui.theme import apply_archival_theme +from transcription.ui.theme import page_header from ...db.session import SessionFactoryDep @@ -76,7 +76,7 @@ def register_page() -> None: return with ui.column().classes("w-full max-w-7xl mx-auto p-4 gap-4"): - with ui.row().classes("w-full items-center justify-between pb-2 border-b border-[#6B6A65]/20"): + with ui.row().classes("w-full items-center justify-between pb-2 ui-header-divider"): if document_name is not None: header_title = f"Sources: {document_name}" elif job_label is not None: @@ -89,7 +89,7 @@ def register_page() -> None: if back_path is not None: back_label = "Back to Document" if document_id is not None else "Back to Job" ui.button(back_label, on_click=lambda route=back_path: ui.navigate.to(route), icon="arrow_back").classes( - "bg-[#2D5A4C] text-white text-xs" + "ui-btn-primary text-xs" ) # Format source records into read-model rows for the table renderer @@ -129,7 +129,7 @@ def register_page() -> None: back_path = _back_path_from_query(request.query_params) with ui.column().classes("w-full max-w-[1800px] mx-auto p-4 gap-4"): - with ui.row().classes("w-full justify-between items-center pb-2 border-b border-[#6B6A65]/30"): + with ui.row().classes("w-full justify-between items-center pb-2 ui-header-divider"): page_header(f"Source Page {source.page_number}: {source.upload_name}", subtitle=f"Source ID: {source.id}") if back_path is not None: @@ -141,7 +141,7 @@ def register_page() -> None: else "Back to Sources" ) ui.button(back_label, on_click=lambda route=back_path: ui.navigate.to(route), icon="arrow_back").classes( - "bg-[#2D5A4C] text-white text-xs" + "ui-btn-primary text-xs" ) else: ui.button("Back to Sources", on_click=lambda: ui.navigate.to("/sources"), icon="arrow_back").props( @@ -193,7 +193,7 @@ def register_page() -> None: ui.navigate.to(request.url.path + _back_query(request.query_params)) with ui.row().classes("w-full items-center gap-2 mt-2"): - ui.button("Save Revision", on_click=save_revision, icon="save").classes("bg-[#2D5A4C] text-white text-xs") + ui.button("Save Revision", on_click=save_revision, icon="save").classes("ui-btn-primary text-xs") @ui.page("/documents/{document_id}/sources") async def document_sources_page(document_id: str) -> RedirectResponse: diff --git a/src/transcription/ui/static/components/page_content.css b/src/transcription/ui/static/components/page_content.css deleted file mode 100644 index af18b28..0000000 --- a/src/transcription/ui/static/components/page_content.css +++ /dev/null @@ -1,138 +0,0 @@ -.page-content { - width: 100%; - display: grid; - grid-template-columns: minmax(0, 1fr) minmax(260px, 320px); - align-items: start; - border: 1px solid var(--theme-border); - color: var(--theme-text); - background: var(--theme-surface-raised); - box-shadow: var(--theme-shadow); -} - -.page-content__editor { - min-width: 0; - padding: 1.5rem; -} - -.page-content__heading { - width: 100%; - align-items: center; - justify-content: space-between; - margin-bottom: 1rem; -} - -.page-content__kicker { - color: var(--theme-text-muted); - font-size: 0.72rem; - font-weight: 800; - text-transform: uppercase; -} - -.page-content__title, -.page-content__sidebar-title { - color: var(--theme-text); - font-family: Georgia, serif; - font-weight: 700; -} - -.page-content__title { - font-size: 1.35rem; -} - -.page-content__badge { - color: var(--theme-text); - background: var(--theme-surface-muted); -} - -.page-content__workspace { - display: grid; - grid-template-columns: minmax(280px, 0.85fr) minmax(320px, 1.15fr); - gap: 1.5rem; -} - -.source-placeholder { - min-height: 440px; - display: grid; - grid-template-rows: auto 1fr; - border: 1px solid var(--theme-viewer-border); - background: var(--theme-viewer); -} - -.source-placeholder__toolbar { - width: 100%; - min-height: 48px; - align-items: center; - justify-content: space-between; - padding: 0.5rem 0.75rem; - color: var(--theme-inverse-text); -} - -.source-placeholder__body { - align-items: center; - justify-content: center; - color: var(--theme-viewer-muted); -} - -.transcription-placeholder { - min-width: 0; - gap: 1rem; -} - -.transcription-placeholder__section { - min-width: 0; -} - -.transcription-placeholder__title { - margin-bottom: 0.5rem; - font-weight: 700; -} - -.transcription-placeholder__text { - min-height: 160px; - padding: 1rem; - border: 1px solid var(--theme-border); - background: var(--theme-surface); - font-family: Georgia, serif; -} - -.page-content__sidebar { - min-width: 0; - border-left: 1px solid var(--theme-border); - background: var(--theme-surface); -} - -.page-content__sidebar-section { - display: grid; - gap: 0.75rem; - padding: 1.5rem; - border-bottom: 1px solid var(--theme-border); -} - -.page-content__sidebar-title { - font-size: 1.05rem; -} - -@media (max-width: 1000px) { - .page-content { - grid-template-columns: 1fr; - } - - .page-content__sidebar { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - border-top: 1px solid var(--theme-border); - border-left: 0; - } -} - -@media (max-width: 760px) { - .page-content__workspace, - .page-content__sidebar { - grid-template-columns: 1fr; - } - - .page-content__editor, - .page-content__sidebar-section { - padding: 1rem; - } -} \ No newline at end of file diff --git a/src/transcription/ui/static/components/page_header.css b/src/transcription/ui/static/components/page_header.css deleted file mode 100644 index d9ea81a..0000000 --- a/src/transcription/ui/static/components/page_header.css +++ /dev/null @@ -1,60 +0,0 @@ -.page-header { - width: 100%; - display: flex; - align-items: flex-end; - justify-content: space-between; - gap: 2rem; - padding: 2rem 0 1.5rem; -} - -.page-header__identity { - gap: 0.25rem; -} - -.page-header__eyebrow { - color: var(--theme-text-muted); - font-size: 0.72rem; - font-weight: 800; - text-transform: uppercase; -} - -.page-header__title { - color: var(--theme-text); - font-family: Georgia, serif; - font-size: 2rem; - font-weight: 700; - line-height: 1.15; -} - -.page-header__metadata { - gap: 0.75rem; - color: var(--theme-text-muted); - font-size: 0.92rem; -} - -.page-header__metadata > * + *::before { - margin-right: 0.75rem; - content: "·"; -} - -.page-header__actions { - flex-wrap: wrap; - justify-content: flex-end; - gap: 0.75rem; -} - -@media (max-width: 700px) { - .page-header { - align-items: flex-start; - flex-direction: column; - } - - .page-header__title { - font-size: 1.65rem; - } - - .page-header__actions, - .page-header__actions .q-btn { - width: 100%; - } -} \ No newline at end of file diff --git a/src/transcription/ui/static/theme.css b/src/transcription/ui/static/theme.css index 3c9398c..f3f483d 100644 --- a/src/transcription/ui/static/theme.css +++ b/src/transcription/ui/static/theme.css @@ -102,4 +102,135 @@ input:focus-visible, [tabindex="0"]:focus-visible { outline: 3px solid var(--theme-focus); outline-offset: 2px; +} + +/* Semantic utility classes for incremental migration away from inline hex styles. */ +.ui-text-primary { + color: var(--theme-text); +} + +.ui-text-muted { + color: var(--theme-text-muted); +} + +.ui-text-inverse { + color: var(--theme-inverse-text); +} + +.ui-bg-page { + background: var(--theme-page); +} + +.ui-bg-surface { + background: var(--theme-surface); +} + +.ui-bg-surface-raised { + background: var(--theme-surface-raised); +} + +.ui-bg-surface-muted { + background: var(--theme-surface-muted); +} + +.ui-bg-viewer { + background: var(--theme-viewer); +} + +.ui-bg-viewer-overlay { + background: color-mix(in srgb, var(--theme-viewer) 50%, transparent); +} + +.ui-bg-viewer-overlay-soft { + background: color-mix(in srgb, var(--theme-viewer) 40%, transparent); +} + +.ui-border-subtle { + border-color: var(--theme-border); +} + +.ui-border-viewer { + border-color: var(--theme-viewer-border); +} + +.ui-header-divider { + border-bottom: 1px solid var(--theme-border); +} + +.ui-card-surface { + border: 1px solid var(--theme-border); + color: var(--theme-text); + background: var(--theme-surface-raised); + border-radius: 0.125rem; +} + +.ui-row-surface { + border: 1px solid var(--theme-border); + color: var(--theme-text); + background: var(--theme-surface); + border-radius: 0.125rem; +} + +.ui-note-box { + border: 1px solid var(--theme-border); + color: var(--theme-text); + background: var(--theme-surface); + border-radius: 0.125rem; + font-style: italic; +} + +.ui-btn-primary { + color: var(--theme-inverse-text); + background: var(--theme-primary); +} + +.ui-btn-primary:hover { + background: var(--theme-primary-hover); +} + +.ui-btn-secondary { + color: var(--theme-primary); + background: transparent; +} + +.ui-btn-secondary:hover { + color: var(--theme-primary-hover); +} + +.ui-link-primary { + color: var(--theme-primary); +} + +.ui-link-primary:hover { + color: var(--theme-primary-hover); +} + +.ui-text-accent { + color: var(--theme-secondary); +} + +.ui-table { + border: 1px solid var(--theme-border); + color: var(--theme-text); + background: var(--theme-surface-raised); + border-radius: 0.125rem; +} + +.ui-table .q-table tbody tr:hover { + background: var(--theme-surface) !important; + cursor: pointer; +} + +.ui-table .q-table td { + border-bottom: 1px solid var(--theme-border) !important; +} + +.ui-table-header { + color: var(--theme-inverse-text); + background: var(--theme-primary); + font-weight: 700; +} + +.ui-table-body { + color: var(--theme-text); } \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 6f8db3c..5b029fa 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,7 +15,6 @@ from transcription.config import Settings from transcription.config import get_settings from transcription.db.engine import get_database_url from transcription.db.engine import get_engine -from transcription.db.operations import create_all from transcription.db.session import dispose_session_factory from transcription.db.session import get_session_factory from transcription.db.session import session_scope @@ -42,8 +41,15 @@ async def default_settings(): """Provide default settings for tests.""" settings = get_settings(database_url="sqlite:///:memory:") db_url = get_database_url(settings) - await create_all(engine=get_engine(database_url=db_url)) - return settings + engine = get_engine(database_url=db_url) + + # Cached in-memory engines persist across tests; reset schema per test for isolation. + async with engine.begin() as connection: + await connection.run_sync(SQLModel.metadata.drop_all) + await connection.run_sync(SQLModel.metadata.create_all) + + yield settings + await dispose_session_factory(db_url) @pytest_asyncio.fixture @@ -53,8 +59,6 @@ async def async_session(default_settings: Settings): async with session_scope(database_url=db_url) as async_session: yield async_session - await dispose_session_factory(db_url) - @pytest.fixture def default_session_factory(default_settings: Settings): diff --git a/tests/integration/test_pipeline_flow.py b/tests/integration/test_pipeline_flow.py index 111bb42..32f4fbf 100644 --- a/tests/integration/test_pipeline_flow.py +++ b/tests/integration/test_pipeline_flow.py @@ -8,8 +8,9 @@ from transcription.config import Settings from transcription.db.models import Job from transcription.db.models import JobStatus from transcription.providers.base import TranscriptionResult +from transcription.services import ServiceBundle from transcription.services.store import create_upload_job -from transcription.worker import process_next_queued_job +from transcription.services.workflows import advance_job @pytest.mark.integration @@ -58,7 +59,11 @@ class TestPipelineSuccessFlow: _fake_transcribe_document_image, ) - processed = await process_next_queued_job(session=async_session) + services = ServiceBundle() + queued_job = await services.jobs.read_next_queued_job(session=async_session) + processed = queued_job is not None + if queued_job is not None: + await advance_job(job=queued_job, services=services, session=async_session) job = await async_session.get(Job, upload_result.job_id) assert processed is True @@ -98,7 +103,11 @@ class TestPipelineFailureFlow: _fake_transcribe_document_image, ) - processed = await process_next_queued_job(session=async_session) + services = ServiceBundle() + queued_job = await services.jobs.read_next_queued_job(session=async_session) + processed = queued_job is not None + if queued_job is not None: + await advance_job(job=queued_job, services=services, session=async_session) job = await async_session.get(Job, upload_result.job_id) assert processed is True diff --git a/tests/services/test_job_service.py b/tests/services/test_job_service.py index 2844ef8..4dbc0cc 100644 --- a/tests/services/test_job_service.py +++ b/tests/services/test_job_service.py @@ -1,4 +1,7 @@ from uuid import uuid4 +from datetime import UTC +from datetime import datetime +from datetime import timedelta import pytest @@ -100,8 +103,13 @@ class TestJobService: document = Document(id=uuid4(), name="ordered-doc") await document_service.create_document(document=document) - first = Job(document_id=document.id, status=JobStatus.QUEUED) - second = Job(document_id=document.id, status=JobStatus.QUEUED) + created_at = datetime.now(UTC) + first = Job(document_id=document.id, status=JobStatus.QUEUED, date_created=created_at) + second = Job( + document_id=document.id, + status=JobStatus.QUEUED, + date_created=created_at + timedelta(microseconds=1), + ) await job_service.create_job(job=first) await job_service.create_job(job=second) diff --git a/tests/ui/test_documents_page.py b/tests/ui/test_documents_page.py index b9ceea4..ab01d4f 100644 --- a/tests/ui/test_documents_page.py +++ b/tests/ui/test_documents_page.py @@ -30,8 +30,7 @@ class TestDocumentsPageRendering: assert response.status_code == 200 assert "Documents" in response.text assert "Create new document" in response.text - assert "No documents yet." in response.text - assert "Create your first document" in response.text + assert "No documents in repository yet." in response.text def test_document_create_page_renders_fields(self, app_client): """GET /ui/documents/new renders document-create form fields.""" @@ -40,7 +39,7 @@ class TestDocumentsPageRendering: response = client.get("/ui/documents/new") assert response.status_code == 200 - assert "Create document" in response.text + assert "Create Document" in response.text assert "Document name is required." in response.text assert "Document name" in response.text assert "Document type" in response.text @@ -69,7 +68,7 @@ class TestDocumentsPageRendering: assert response.status_code == 200 assert "Seeded Document" in response.text - assert "Type: letter" in response.text + assert "letter" in response.text def test_document_detail_page_renders_metadata_and_empty_related_sections(self, app_client): """GET /ui/documents/{document_id} shows metadata and related empty states.""" @@ -97,24 +96,30 @@ class TestDocumentsPageRendering: assert response.status_code == 200 assert "Zenna Letter" in response.text - assert "Document type: letter" in response.text - assert "Author: not set" in response.text - assert "Exact date: 1885-07-13" in response.text - assert "Approximate date: c. 1885" in response.text - assert "Location created: Ohio" in response.text - assert "Archive identifier: BOX-1-FOLDER-2" in response.text - assert "Notes: Family archive" in response.text - assert "Created at (read-only):" in response.text - assert "Updated at (read-only):" in response.text + assert "Type: letter" in response.text + assert "Author:" in response.text + assert "Not set" in response.text + assert "Exact Date:" in response.text + assert "1885-07-13" in response.text + assert "Approx. Date:" in response.text + assert "c. 1885" in response.text + assert "Location Created:" in response.text + assert "Ohio" in response.text + assert "Archive Identifier:" in response.text + assert "BOX-1-FOLDER-2" in response.text + assert "Archival Notes:" in response.text + assert "Family archive" in response.text + assert "Created:" in response.text + assert "Updated:" in response.text assert "No linked people yet." in response.text - assert "0 source(s) linked" in response.text - assert "0 job(s) linked" in response.text + assert "0 Source(s) Linked" in response.text + assert "0 Active Jobs" in response.text assert "+ Add Source" in response.text assert "+ Add Job" in response.text assert "Sources" in response.text assert "Jobs" in response.text - assert "Edit document" in response.text - assert "Delete document" in response.text + assert "Edit Document" in response.text + assert "Delete" in response.text def test_document_detail_page_renders_related_people_sources_and_jobs(self, app_client): """GET /ui/documents/{document_id} shows related records when present.""" @@ -158,10 +163,11 @@ class TestDocumentsPageRendering: response = client.get(f"/ui/documents/{document_id}") assert response.status_code == 200 - assert "Jane Doe (author)" in response.text - assert "Author: Jane Doe" in response.text - assert "1 source(s) linked" in response.text - assert "1 job(s) linked" in response.text + assert "Jane Doe" in response.text + assert "author" in response.text + assert "Author:" in response.text + assert "1 Source(s) Linked" in response.text + assert "1 Active Jobs" in response.text def test_document_jobs_page_filters_to_document_context(self, app_client): _, client = app_client @@ -223,7 +229,7 @@ class TestDocumentsPageRendering: response = client.get(f"/ui/sources?document_id={document_id}") assert response.status_code == 200 - assert "Sources for Target" in response.text + assert "Sources: Target" in response.text assert "Back to Document" in response.text assert "target_page.png" in response.text assert "other_page.png" not in response.text @@ -267,7 +273,7 @@ class TestDocumentsPageRendering: response = client.get(f"/ui/documents/{document_id}/edit") assert response.status_code == 200 - assert "Edit document" in response.text + assert "Edit Document Record" in response.text assert "Document name and document type are required." in response.text assert "Document name" in response.text assert "Document type" in response.text @@ -298,7 +304,7 @@ class TestDocumentsPageRendering: response = client.get(f"/ui/documents/{document_id}/delete") assert response.status_code == 200 - assert "Delete document" in response.text + assert "Delete Document" in response.text assert "This action permanently deletes the document." in response.text assert "Delete document permanently" in response.text diff --git a/tests/ui/test_jobs_page.py b/tests/ui/test_jobs_page.py index 0569fac..8d1dbf6 100644 --- a/tests/ui/test_jobs_page.py +++ b/tests/ui/test_jobs_page.py @@ -21,7 +21,7 @@ class TestPageRendering: assert response.status_code == 200 assert "Create job" in response.text - assert "No jobs yet." in response.text + assert "No active or historical processing jobs found." in response.text def test_job_create_page_requires_existing_documents(self, app_client): """GET /ui/jobs/new shows guidance when no Documents exist.""" @@ -30,7 +30,7 @@ class TestPageRendering: response = client.get("/ui/jobs/new") assert response.status_code == 200 - assert "Create job" in response.text + assert "Create Processing Job" in response.text assert "No documents available. Create a Document before creating a Job." in response.text assert "Create document" in response.text @@ -48,7 +48,7 @@ class TestPageRendering: response = client.get("/ui/jobs/new") assert response.status_code == 200 - assert "Create job" in response.text + assert "Create Processing Job" in response.text assert "Seeded Document" in response.text assert "Files are processed alphabetically by original filename." in response.text assert "No files uploaded yet." in response.text @@ -81,9 +81,9 @@ class TestPageRendering: assert "Provider:" in response.text assert "Model:" in response.text assert "Prompt:" in response.text - assert "Retry count:" in response.text - assert "Last updated:" in response.text - assert "Document Links" in response.text + assert "Retry Count:" in response.text + assert "Last Updated:" in response.text + assert "document links" in response.text.lower() assert "Sources" in response.text assert "Jobs" in response.text assert "Delete job" not in response.text diff --git a/tests/ui/test_people_page.py b/tests/ui/test_people_page.py index 0f5e0b8..d019342 100644 --- a/tests/ui/test_people_page.py +++ b/tests/ui/test_people_page.py @@ -25,7 +25,7 @@ class TestPeoplePageRendering: assert response.status_code == 200 assert "People" in response.text assert "Create new person" in response.text - assert "No people yet." in response.text + assert "No person records found in repository." in response.text def test_people_page_lists_seeded_people(self, app_client): _, client = app_client @@ -41,7 +41,7 @@ class TestPeoplePageRendering: assert response.status_code == 200 assert "Ada Lovelace" in response.text - assert "Display name: Ada" in response.text + assert "Ada" in response.text def test_person_create_page_renders_fields(self, app_client): _, client = app_client @@ -49,7 +49,7 @@ class TestPeoplePageRendering: response = client.get("/ui/people/new") assert response.status_code == 200 - assert "Create person" in response.text + assert "Create Person Record" in response.text assert "Full name is required." in response.text assert "Birth date (YYYY-MM-DD)" in response.text assert "Death date (YYYY-MM-DD)" in response.text @@ -85,15 +85,17 @@ class TestPeoplePageRendering: assert response.status_code == 200 assert "Grace Hopper" in response.text - assert "Full name: Grace Hopper" in response.text - assert "Display name: Grace" in response.text - assert "Maiden name: Murray" in response.text - assert "Birth date: 1906-12-09" in response.text - assert "Death date: 1992-01-01" in response.text - assert "Biography: Computer pioneer" in response.text - assert "Portrait path: /images/grace.jpg" in response.text - assert "Created at (read-only):" in response.text - assert "Updated at (read-only):" in response.text + assert "Full Name:" in response.text + assert "Display Name:" in response.text + assert "Maiden Name:" in response.text + assert "Birth Date:" in response.text + assert "1906-12-09" in response.text + assert "Death Date:" in response.text + assert "1992-01-01" in response.text + assert "biography" in response.text.lower() + assert "Computer pioneer" in response.text + assert "Created:" in response.text + assert "Updated:" in response.text assert "No linked documents yet." in response.text assert "Link this person from a Document workflow." in response.text @@ -115,7 +117,6 @@ class TestPeoplePageRendering: response = client.get(f"/ui/people/{person_id}") assert response.status_code == 200 - assert "Portrait path: portraits/person/seeded.png" in response.text assert "/uploads/portraits/person/seeded.png" in response.text def test_person_detail_page_renders_linked_documents(self, app_client): @@ -145,7 +146,8 @@ class TestPeoplePageRendering: response = client.get(f"/ui/people/{person_id}") assert response.status_code == 200 - assert "Linked Document (author)" in response.text + assert "Linked Document" in response.text + assert "Role: author" in response.text def test_person_detail_page_handles_invalid_id(self, app_client): _, client = app_client @@ -179,7 +181,7 @@ class TestPeoplePageRendering: response = client.get(f"/ui/people/{person_id}/edit") assert response.status_code == 200 - assert "Edit person" in response.text + assert "Edit Person Record" in response.text assert "Full name is required." in response.text assert "Full name" in response.text assert "Save changes" in response.text @@ -200,8 +202,8 @@ class TestPeoplePageRendering: response = client.get(f"/ui/people/{person_id}/delete") assert response.status_code == 200 - assert "Delete person" in response.text - assert "This action permanently deletes the person." in response.text + assert "Delete Person Record" in response.text + assert "This action permanently deletes the person record." in response.text assert "Delete person permanently" in response.text def test_person_delete_page_shows_blocked_state_when_linked_documents_exist(self, app_client): diff --git a/tests/ui/test_sources_page.py b/tests/ui/test_sources_page.py index bdc5daf..6b27dcb 100644 --- a/tests/ui/test_sources_page.py +++ b/tests/ui/test_sources_page.py @@ -23,7 +23,7 @@ class TestSourcesPageRendering: assert response.status_code == 200 assert "Sources" in response.text - assert "No sources added yet." in response.text + assert "No source file records found." in response.text def test_sources_page_lists_seeded_sources(self, app_client): _, client = app_client @@ -49,9 +49,8 @@ class TestSourcesPageRendering: response = client.get("/ui/sources") assert response.status_code == 200 - assert "Page 1: page_one.png" in response.text + assert "page_one.png" in response.text assert "stored_page_one.png" in response.text - assert "Open source detail" in response.text def test_sources_page_filters_to_document_context(self, app_client): _, client = app_client @@ -90,11 +89,10 @@ class TestSourcesPageRendering: response = client.get(f"/ui/sources?document_id={document_id}") assert response.status_code == 200 - assert "Sources for Target" in response.text + assert "Sources: Target" in response.text assert "Back to Document" in response.text assert "target_page.png" in response.text assert "other_page.png" not in response.text - assert "Open source detail" in response.text def test_sources_page_filters_to_job_context(self, app_client, seed_job): _, client = app_client @@ -106,7 +104,6 @@ class TestSourcesPageRendering: assert "Sources for Job" in response.text assert "Back to Job" in response.text assert "job-page.png" in response.text - assert "Open source detail" in response.text def test_source_detail_page_renders_preview_and_revision_box(self, app_client, seed_job): _, client = app_client @@ -133,11 +130,11 @@ class TestSourcesPageRendering: response = client.get(f"/ui/sources/{source_id}") assert response.status_code == 200 - assert "Source detail-source.png" in response.text + assert "Source Page 1: detail-source.png" in response.text assert "Back to Sources" in response.text - assert "Transcription text" in response.text + assert "automated raw transcription" in response.text.lower() assert "original transcription text" in response.text - assert "Revision text" in response.text + assert "curated human transcription" in response.text.lower() assert "human revision text" in response.text - assert "Page number:" in response.text - assert "Stored filename:" in response.text + assert "Page Number:" in response.text + assert "Stored Filename:" in response.text